Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,23 @@ repos:
rev: v3.1.0
hooks:
- id: add-trailing-comma
exclude: ^tests/legacy/
# examples/01_atomic_capabilities/audio is ported verbatim from the sibling repo's
# samples/ tree, whose hooks exempt samples/.
exclude: ^(tests/legacy|examples/01_atomic_capabilities/audio)/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.0
hooks:
- id: mypy
# examples/01_atomic_capabilities/audio: mypy cannot map the
# digit-leading 01_atomic_capabilities segment to a module name
# (reports duplicate _common).
exclude:
(?x)(
pb2\.py$
| grpc\.py$
| ^docs
| ^tests/legacy/
| ^examples/01_atomic_capabilities/audio/
| \.html$
| \.pyi$
)
Expand All @@ -61,13 +67,13 @@ repos:
hooks:
- id: black
args: [ --line-length=79 ]
exclude: ^tests/legacy/
exclude: ^(tests/legacy|examples/01_atomic_capabilities/audio)/
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
args: [ "--extend-ignore=E203"]
exclude: ^tests/legacy/
exclude: ^(tests/legacy|examples/01_atomic_capabilities/audio)/
- repo: https://github.com/pylint-dev/pylint
rev: v3.0.2
hooks:
Expand All @@ -76,6 +82,7 @@ repos:
(?x)(
^docs
| ^tests/
| ^examples/01_atomic_capabilities/audio/
| pb2\.py$
| grpc\.py$
| \.demo$
Expand Down Expand Up @@ -168,6 +175,15 @@ repos:
# Long sequential arrange/act scripts are normal in integration
# tests; splitting them for R0915 hurts readability.
--disable=R0915,
# Table-driven tests are branchy by nature (R0912); test doubles
# deliberately hold resources open to assert lifecycle order
# (R1732); passthrough lambda seams read clearer than direct
# method refs (W0108); explicit trailing ``return`` marks the
# end of scripted callbacks (R1711).
--disable=R0912,
--disable=R1732,
--disable=W0108,
--disable=R1711,
]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.32.0
Expand Down
9 changes: 9 additions & 0 deletions dashscope/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ from typing import (
Union,
)

# ---------------------------------------------------------------------------
# dashscope 2.0
# ---------------------------------------------------------------------------
from dashscope.v2 import AsyncClient as AsyncClient
from dashscope.v2 import Client as Client
from dashscope.v2 import WsConnection as WsConnection
from dashscope.v2 import chat as chat
from dashscope.v2 import settings as settings

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
Expand Down
53 changes: 7 additions & 46 deletions dashscope/v2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from pathlib import Path
from typing import Any, Dict, Mapping, Optional, Union
import warnings

import httpx

Expand Down Expand Up @@ -117,33 +116,14 @@ def __init__(
self.models = Models(self)
self.images = Images(self)
self.videos = Videos(self)
self._audio = None
self.audio = Audio(self)
self.fine_tunes = FineTunes(self)
self.deployments = Deployments(self)
self.rl = Rl(self)
self.apps = Apps(self)
self.tingwu = TingWu(self)
self._agentstudio = None

@property
def audio(self) -> "Audio":
"""Audio capabilities (lazily mounted).

Preview: the 2.0 audio API surface is not final and may change
in a future release.
"""
if self._audio is None:
warnings.warn(
"dashscope 2.0 audio API is a preview and may change "
"in a future release.",
FutureWarning,
stacklevel=2,
)
self._audio = Audio(self)
audio = self._audio
assert audio is not None
return audio

@property
def reinforcement_learning(self) -> "Rl":
"""Long-form alias for ``client.rl``."""
Expand Down Expand Up @@ -266,11 +246,11 @@ def ws_connection(
Credentials, region and timeout come from this client's config.
Pass the connection as ``connection=`` to the realtime audio
sessions (``audio.asr.recognize`` / ``audio.tts.connect`` /
``audio.translation.recognize`` / ``tingwu.realtime``) to run
tasks concurrently or back-to-back on one socket, saving the
handshake per task -- the 1.x Java-side connection-reuse pattern
for latency-sensitive audio traffic. The caller owns the
connection and closes it via ``WsConnection.close``.
``tingwu.realtime``) to run tasks concurrently or back-to-back
on one socket, saving the handshake per task -- the 1.x
Java-side connection-reuse pattern for latency-sensitive audio
traffic. The caller owns the connection and closes it via
``WsConnection.close``.
"""
config = self._config
return WsConnection(
Expand Down Expand Up @@ -358,33 +338,14 @@ def __init__(
self.models = AsyncModels(self)
self.images = AsyncImages(self)
self.videos = AsyncVideos(self)
self._audio = None
self.audio = AsyncAudio(self)
self.fine_tunes = AsyncFineTunes(self)
self.deployments = AsyncDeployments(self)
self.rl = AsyncRl(self)
self.apps = AsyncApps(self)
self.tingwu = AsyncTingWu(self)
self._agentstudio = None

@property
def audio(self) -> "AsyncAudio":
"""Audio capabilities (lazily mounted).

Preview: the 2.0 audio API surface is not final and may change
in a future release.
"""
if self._audio is None:
warnings.warn(
"dashscope 2.0 audio API is a preview and may change "
"in a future release.",
FutureWarning,
stacklevel=2,
)
self._audio = AsyncAudio(self)
audio = self._audio
assert audio is not None
return audio

@property
def reinforcement_learning(self) -> "AsyncRl":
"""Long-form alias for ``client.rl``."""
Expand Down
64 changes: 44 additions & 20 deletions dashscope/v2/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,28 +374,52 @@ def error_from_sdk_exception(


def _sse_error_body(text: str) -> Any:
"""Decode an SSE error body into its first ``data:`` JSON payload.

A streaming request the gateway rejects before any output arrives
as a non-2xx response whose body is the error frame itself --
``event:error`` plus a ``data:`` JSON line -- instead of a plain
JSON document. 1.x parses the same shape in
``common.utils._handle_http_failed_response``; like
``resources/_sse.py``, the frame is detected by line shape rather
than content-type, so a mislabeled header still maps. Bodies with
no decodable frame yield None, leaving the raw-text fallback
intact.
"""
for line in text.splitlines():
if not line.startswith("data:"):
continue
"""Decode an SSE error payload, preferring codes over message-only frames."""
data_lines = []
fallback = None

def flush() -> Any:
nonlocal fallback
if not data_lines:
return None
try:
body = json.loads(line[len("data:") :])
bodies = [json.loads("\n".join(data_lines))]
except json.JSONDecodeError:
continue
if isinstance(body, dict):
return body
return None
# Older gateways omit blank lines between complete JSON frames.
bodies = []
for data in data_lines:
try:
bodies.append(json.loads(data))
except json.JSONDecodeError:
continue
for body in bodies:
if not isinstance(body, dict):
continue
error = _extract(body.get("error"))
if body.get("code") or any(
error.get(key) for key in ("code", "type", "message")
):
return body
if (
fallback is None
and isinstance(body.get("message"), str)
and body["message"]
):
fallback = body
return None

for line in text.splitlines():
if line.startswith("data:"):
data_lines.append(line[len("data:") :])
elif line == "data":
data_lines.append("")
elif not line:
body = flush()
data_lines.clear()
if body is not None:
return body
body = flush()
return body if body is not None else fallback


def _parse_body(response: httpx.Response) -> Any:
Expand Down
5 changes: 4 additions & 1 deletion dashscope/v2/protocols/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,10 @@ def build_chat_request(
# unchanged, so this converges both entry points and keeps the
# hatch-wins precedence. Copy first: never mutate the caller's
# dict.
if extra_parameters and "tool_choice" in extra_parameters:
if (
extra_parameters
and extra_parameters.get("tool_choice") is not None
):
extra_parameters = dict(extra_parameters)
extra_parameters["tool_choice"] = _translate_tool_choice(
extra_parameters["tool_choice"],
Expand Down
Loading
Loading