Skip to content
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,35 @@ response = TextReRank.call(

### Image Generation

Image and video generation APIs use server-side asynchronous tasks:

- `async_call()` submits a task and returns task information immediately. It is not a Python `async` coroutine.
- `call()` submits a task and blocks by polling task status until the task finishes.
- Use `fetch()` to query task status manually, or `wait()` to block until completion.
- Use `wait_timeout_seconds` with blocking calls to limit the maximum wait time.

```python
from dashscope import ImageSynthesis

# Async task pattern
# Submit a server-side async task
response = ImageSynthesis.async_call(
model="wanx-v1",
prompt="A serene mountain landscape at sunset",
)

# Wait for result
# Query task status manually
status = ImageSynthesis.fetch(response)

# Or wait for result
result = ImageSynthesis.wait(response)

# Blocking call with timeout
result = ImageSynthesis.call(
model="wanx-v1",
prompt="A serene mountain landscape at sunset",
wait_timeout_seconds=60,
)

# Sync call (for wan2.2-t2i-flash/plus)
result = ImageSynthesis.sync_call(
model="wan2.2-t2i-flash",
Expand All @@ -265,13 +282,21 @@ result = ImageSynthesis.sync_call(
```python
from dashscope import VideoSynthesis

# Text-to-video
# Submit a server-side async task
response = VideoSynthesis.async_call(
model="wan2.7-t2v",
prompt="A cat playing with a ball of yarn",
)

# Wait for result
result = VideoSynthesis.wait(response)

# Blocking call with timeout
result = VideoSynthesis.call(
model="wan2.7-t2v",
prompt="A cat playing with a ball of yarn",
wait_timeout_seconds=60,
)
```

### Speech Synthesis (TTS)
Expand Down
5 changes: 4 additions & 1 deletion dashscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
base_compatible_api_url,
base_http_api_url,
base_websocket_api_url,
trust_env,
)
from dashscope.finetune.deployments import Deployments
from dashscope.finetune.finetunes import FineTunes
Expand All @@ -46,7 +47,7 @@
from dashscope.files import Files
from dashscope.models import Models
from dashscope.nlp.understanding import Understanding
from dashscope.rerank.text_rerank import TextReRank
from dashscope.rerank import AioTextReRank, TextReRank
from dashscope.threads import (
MessageFile,
Messages,
Expand Down Expand Up @@ -74,6 +75,7 @@
"base_websocket_api_url",
"api_key",
"api_key_file_path",
"trust_env",
"save_api_key",
"AioGeneration",
"Conversation",
Expand Down Expand Up @@ -106,6 +108,7 @@
"list_tokenizers",
"Application",
"TextReRank",
"AioTextReRank",
"Assistants",
"Threads",
"Messages",
Expand Down
8 changes: 7 additions & 1 deletion dashscope/aigc/image_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def wait( # type: ignore[override]
task: Union[str, ImageSynthesisResponse],
api_key: str = None,
workspace: str = None,
**kwargs,
) -> ImageSynthesisResponse:
"""Wait for image(s) synthesis task to complete, and return the result.

Expand All @@ -399,7 +400,12 @@ def wait( # type: ignore[override]
Returns:
ImageSynthesisResponse: The task result.
"""
response = super().wait(task, api_key, workspace=workspace)
response = super().wait(
task,
api_key,
workspace=workspace,
**kwargs,
)
return ImageSynthesisResponse.from_api_response(response)

@classmethod
Expand Down
8 changes: 7 additions & 1 deletion dashscope/aigc/video_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ def wait( # type: ignore[override]
task: Union[str, VideoSynthesisResponse],
api_key: str = None,
workspace: str = None,
**kwargs,
) -> VideoSynthesisResponse:
"""Wait for video synthesis task to complete, and return the result.

Expand All @@ -521,7 +522,12 @@ def wait( # type: ignore[override]
Returns:
VideoSynthesisResponse: The task result.
"""
response = super().wait(task, api_key, workspace=workspace)
response = super().wait(
task,
api_key,
workspace=workspace,
**kwargs,
)
return VideoSynthesisResponse.from_api_response(response)

@classmethod
Expand Down
79 changes: 57 additions & 22 deletions dashscope/api_entities/aiohttp_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) Alibaba, Inc. and its affiliates.

import asyncio
import json
from http import HTTPStatus

Expand All @@ -13,6 +14,7 @@
SSE_CONTENT_TYPE,
HTTPMethod,
)
from dashscope.common.env import get_trust_env
from dashscope.common.error import UnsupportedHTTPMethod
from dashscope.common.logging import logger
from dashscope.common.utils import async_to_sync
Expand Down Expand Up @@ -107,25 +109,43 @@ async def aio_call(self):
return result

async def _handle_stream(self, response):
# TODO define done message.
is_error = False
status_code = HTTPStatus.BAD_REQUEST
async for line in response.content:
if line:
line = line.decode("utf8")
line = line.rstrip("\n").rstrip("\r")
if line.startswith("event:error"):
is_error = True
elif line.startswith("status:"):
status_code = line[len("status:") :]
status_code = int(status_code.strip())
elif line.startswith("data:"):
line = line[len("data:") :]
yield (is_error, status_code, line)
if is_error:
break
event_type = None
try:
async for line in response.content:
if line:
line = line.decode("utf8")
line = line.rstrip("\n").rstrip("\r")
if not line:
event_type = None
continue
if line.startswith("event:"):
event_type = line[len("event:") :].strip()
if event_type == "error":
is_error = True
Comment on lines +116 to +126

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The event_type state is not reset when a blank line (event separator) is encountered in the SSE stream. This can cause subsequent events to inherit the previous event's type (e.g., if the previous event was done, all subsequent data lines will be incorrectly ignored). Reset event_type to None when an empty line is parsed.

Suggested change
async for line in response.content:
if line:
line = line.decode("utf8")
line = line.rstrip("\n").rstrip("\r")
if line.startswith("event:"):
event_type = line[len("event:") :].strip()
if event_type == "error":
is_error = True
async for line in response.content:
if line:
line = line.decode("utf8")
line = line.rstrip("\\n").rstrip("\\r")
if not line:
event_type = None
continue
if line.startswith("event:"):
event_type = line[len("event:") :].strip()
if event_type == "error":
is_error = True

elif line.startswith("status:"):
status_code = line[len("status:") :]
status_code = int(status_code.strip())
elif line.startswith("data:"):
line = line[len("data:") :]
if event_type == "done":
continue
yield (is_error, status_code, line)
if is_error:
break
else:
continue # ignore heartbeat...
else:
continue # ignore heartbeat...
event_type = None
except (aiohttp.ClientError, asyncio.TimeoutError):
logger.exception(
"Stream response interrupted while reading aiohttp SSE "
"response, status_code=%s, request_id=%s",
response.status,
response.headers.get("X-Request-Id"),
)
raise

# pylint: disable=too-many-statements
async def _handle_response( # pylint: disable=too-many-branches
Expand Down Expand Up @@ -249,6 +269,7 @@ async def _handle_request(self):
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=self.timeout),
headers=self.headers,
trust_env=get_trust_env(),
) as session:
logger.debug("Starting request: %s", self.url)
if self.method == HTTPMethod.POST:
Expand Down Expand Up @@ -281,9 +302,23 @@ async def _handle_request(self):
async with response:
async for rsp in self._handle_response(response):
yield rsp
except aiohttp.ClientConnectorError as e:
logger.error(e)
raise e
except Exception as e:
logger.error(e)
raise e
except (aiohttp.ClientError, asyncio.TimeoutError):
logger.exception(
"Aio HTTP request failed, url=%s, method=%s, stream=%s, "
"timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise
except Exception:
logger.exception(
"Unexpected aio HTTP request error, url=%s, method=%s, "
"stream=%s, timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise
7 changes: 6 additions & 1 deletion dashscope/api_entities/dashscope_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ def setattr(self, attr, value):
return super().__setitem__(attr, value)

def __getattr__(self, attr):
return self[attr]
try:
return self[attr]
except KeyError:
raise AttributeError(
f"{type(self).__name__!r} object has no attribute {attr!r}",
) from None

def __setattr__(self, attr, value):
self[attr] = value
Expand Down
51 changes: 42 additions & 9 deletions dashscope/api_entities/http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
HTTPMethod,
)
from dashscope.common.error import UnsupportedHTTPMethod
from dashscope.common.env import get_trust_env
from dashscope.common.logging import logger
from dashscope.common.utils import (
_handle_aio_stream,
Expand Down Expand Up @@ -176,6 +177,7 @@ async def _handle_aio_request(self): # pylint: disable=too-many-branches
connector=connector,
timeout=aiohttp.ClientTimeout(total=self.timeout),
headers=self.headers,
trust_env=get_trust_env(),
)
should_close = True

Expand Down Expand Up @@ -223,12 +225,26 @@ async def _handle_aio_request(self): # pylint: disable=too-many-branches
# Only close if we created the session
if should_close:
await session.close()
except aiohttp.ClientConnectorError as e:
logger.error(e)
raise e
except BaseException as e:
logger.error(e)
raise e
except aiohttp.ClientError:
logger.exception(
"Aio HTTP request failed, url=%s, method=%s, stream=%s, "
"timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise
except Exception:
logger.exception(
"Unexpected aio HTTP request error, url=%s, method=%s, "
"stream=%s, timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise

@staticmethod
def __handle_parameters(params: dict) -> dict:
Expand Down Expand Up @@ -507,6 +523,23 @@ def _handle_request(self):
# Only close if we created the session
if should_close:
session.close()
except BaseException as e:
logger.error(e)
raise e
except requests.exceptions.RequestException:
logger.exception(
"HTTP request failed, url=%s, method=%s, stream=%s, "
"timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise
except Exception:
logger.exception(
"Unexpected HTTP request error, url=%s, method=%s, "
"stream=%s, timeout=%s",
self.url,
self.method,
self.stream,
self.timeout,
)
raise
Loading
Loading