Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0ac71c0
feat(core): Add client request ID and response validation exception
Oct 30, 2026
18faa63
feat(core): Add client request ID and response validation exception
Oct 30, 2026
8ac41c2
feat(transport): support per-request options across all resources
Oct 30, 2026
e4ef129
feat(parse): Add fault-tolerant response model parsing and strict val…
Nov 2, 2026
5c4c30f
feat(pagination): Introduce generic pagination base class and refacto…
Nov 2, 2026
78d0546
feat(transport): Introduce APIResponse and support raw parameter for …
Nov 2, 2026
de398cf
test(v2): Add sync/async interface parity tests
Nov 3, 2026
767cd49
feat: enhance API response handling and resource methods
Nov 3, 2026
c45e502
fix(v2): support wan2.7 image responses
Nov 4, 2026
bd576b4
fix(v2): support more image models and input forms
Nov 4, 2026
520f4e6
model name fix
Nov 4, 2026
d577809
feat(v2): extend typed parsing, per-request options, and pagination f…
Nov 5, 2026
97abe6e
feat(devkit): add offline self-diagnosis for failed calls
Nov 5, 2026
40e393b
feat(devkit): add static/record/replay dev mock at the HTTP transport
Nov 5, 2026
53a95de
Merge remote-tracking branch 'upstream/dev/v2.1-1' into dev/v2.1-1
Nov 5, 2026
3cfa035
ci: pin Python 3.9 leg to ubuntu-24.04
Nov 5, 2026
5e9cd9a
test(ci): treat dashscope.devkit as a neutral current-layer package
Nov 5, 2026
e506860
fix(core): unify API key resolution and improve error handling
Nov 5, 2026
b537cf0
refactor(server): extract client disconnect response logic
Nov 5, 2026
eff4185
fix(reinforcement): allow local wheel paths in pip requirement tokens
Nov 5, 2026
f9c75f0
fix(reinforcement): address agentic RL review findings
Nov 5, 2026
24acb64
test(reinforcement): align legacy tests with shared core
Nov 5, 2026
69de084
Revert "test(v2): Add sync/async interface parity tests"
Nov 5, 2026
89d693a
test(v2): move layer decoupling checks to current suite
Nov 5, 2026
9581926
chore: update LoongSuite and MaaS test models
Nov 9, 2026
95ff24e
Merge remote-tracking branch 'origin/main' into dev/v2.1-1
Nov 9, 2026
8b7fd31
test(cli): align job list output with SDK fields
Nov 9, 2026
f552f91
chore(version): bump version to 2.1.0
Nov 9, 2026
845987e
Merge main into dev/v2.1-1 and resolve SDK conflicts
Nov 9, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,8 @@ cython_debug/

# Local PyPI investigation tooling; the CI guard lives in tests/legacy_1x/unit/.
/scripts/

# Dev mock record/replay fixtures — recorded response bodies can carry
# generated content, tool-call arguments or account data. Never commit them.
dashscope_mock.jsonl
*.dashscope_mock.jsonl
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ repos:
--disable=R1732,
--disable=W0108,
--disable=R1711,
# astroid resolves an @overload set to its first stub body, so a
# resource method typed with overloads (``raw=False`` -> ModelList,
# ``raw=True`` -> APIResponse) infers as None at the call site and
# iterating the result reads as non-iterable. dashscope/ keeps
# E1133 enabled.
--disable=E1133,
]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.32.0
Expand Down
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,82 @@ resp.request_id

All responses share a common envelope: `request_id`, `status_code`, `code`, `message`, `is_error`, and raw `headers`.

### Pagination

List endpoints return a page object you can iterate and advance instead of looping `page_no` by hand:

```python
page = client.models.list(page_no=1, page_size=10)

for model in page: # this page's items
...
page.total_count # catalog total, as reported by the gateway
page.has_next_page()
page.next_page() # None on the last page

for page in client.models.list(page_size=50).iter_pages(): # every page
...
```

On `AsyncClient` use `await page.next_page_async()` and `async for page in page.iter_pages_async()`. Calling the synchronous traversal on an async-fetched page raises `TypeError` naming the async method, rather than handing back an un-awaited coroutine. `client.files.list()` and `client.images.jobs.list()` paginate the same way.

Serialize a page with `model_dump()`. Iterating a page yields its *items*, so `dict(page)` is not a way to read its fields.

`models.list(provider=...)` filters client-side: it narrows each page's `models`, while `total_count` stays the whole-catalog total. Pagination therefore walks the unfiltered catalog and the filter is applied to each page as it arrives.

### Raw responses

Pass `raw=True` to get the response object itself, before any error check:

```python
resp = client.models.list(raw=True)

resp.status_code
resp.headers # includes non-standard gateway headers
resp.request_id
resp.elapsed
resp.json() # decoded body
resp.parse(ModelList) # typed model, memoized per type
```

`raw=True` skips the error check, so a 4xx/5xx is returned to you instead of raising. `raise_for_status()` applies it after the fact.

`parse(...)` needs the model class, and the v2 resource models are not re-exported from the top-level package — import them from where they are defined:

```python
from dashscope.resources.models import ModelList, Model
from dashscope.resources.files import FileList, File
from dashscope.resources._jobs import Job, JobList
from dashscope.resources.embeddings import EmbeddingResponse
```

`resp.parse(...)` is the generic envelope mapping and does not replay a resource's own reshaping — on `models.list`, a page the gateway delivered as a bare list parses through it as empty. Use `resp.json()` when you need exactly what the resource method would have built.

### Per-request options

Every resource method that makes an HTTP request takes the same three escape hatches, for anything this SDK does not model yet:

```python
client.chat.completions.create(
model="qwen-plus",
messages=["hi"],
extra_headers={"x-gateway-experiment": "on"},
extra_query={"debug": "1"},
extra_body={"parameters": {"top_k": 5}},
)
```

`extra_headers` sits above every other layer — the SDK identity headers, the client defaults, your `headers=`, and a per-call `api_key=` / `workspace=` — matched case-insensitively. `extra_body` deep-merges nested dicts, so adding one nested field keeps its siblings; lists and scalars replace. A request whose body is not a JSON object (a file upload, raw bytes) has nothing to merge into, so `extra_body` is dropped with a warning rather than silently lost.

`max_retries` narrows or widens the retry budget for a single call, so a request that must not be replayed no longer needs a whole derived client:

```python
client.deployments.list(max_retries=0) # one attempt, no retry
client.with_options(max_retries=5) # still works, client-wide
```

Streaming methods take all four: `open_stream` retries only while no event has reached you yet, so a mid-stream failure is never replayed. The websocket methods (`audio.asr.recognize` / `recognize_file`, `tingwu.realtime`) take none of them — that transport has no per-request options channel, so a parameter there would be silently inert.

### Streaming

`stream=True` returns a `ChatStream` (`AsyncChatStream` for `AsyncClient`) with typed helpers:
Expand Down Expand Up @@ -283,6 +359,7 @@ dashscope.settings.log_level = "INFO"
| `DASHSCOPE_WEBSOCKET_BASE_URL` | Override the WebSocket endpoint | derived from region/workspace |
| `DASHSCOPE_TIMEOUT` | Request timeout (seconds) | `300` |
| `DASHSCOPE_MAX_RETRIES` | Automatic retries | `2` |
| `DASHSCOPE_STRICT_VALIDATION` | Raise on a response body that does not fit its typed model instead of degrading it | `false` |
| `DASHSCOPE_HTTP_MAX_CONNECTIONS` | Connection-pool size | `100` |
| `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | Keep-alive connections held in the pool | `20` |
| `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | Keep-alive expiry (seconds) | `5.0` |
Expand All @@ -292,6 +369,10 @@ dashscope.settings.log_level = "INFO"

The three `DASHSCOPE_HTTP_*` pool variables are also settable as `http_max_connections`, `http_max_keepalive_connections` and `http_keepalive_expiry` in `settings` and `config.toml`. Passing your own `http_client=` takes precedence over all of them.

By default a response body that does not fit the SDK's typed models is parsed in degraded form — the offending fields fall back to their defaults and the rest of the payload stays usable — with the drift recorded at `DEBUG` on the `dashscope._logging.parse` logger. Set `DASHSCOPE_STRICT_VALIDATION=true` (or `Client(strict_validation=True)`, `settings.strict_validation = True`, `strict_validation = true` in `config.toml`) to raise `APIResponseValidationError` instead, which is what you want in CI or contract tests. Strict validation raises even under `raise_on_error=False`: that flag governs service errors, this one governs parse errors.

One caveat: `DEBUG` is below the SDK's default log level, so a degraded parse is silent unless you raise it — see [Logging](#logging) — and `strict_validation` is the only way to make drift fail loudly. The typed-parse path covers every resource, and not only the final response object: streamed frames (`apps`, `deployments.stream_events`, `fine_tunes.stream_events`, `audio.tts.http.stream`) and websocket results (`audio.asr`) degrade frame by frame as well, so one malformed event cannot break an iteration partway through with a `pydantic.ValidationError` that no `except DashScopeError` would catch.

**Config file** (`~/.dashscope/config.toml`):

```toml
Expand Down
81 changes: 81 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,82 @@ resp.request_id

所有响应共享统一信封:`request_id`、`status_code`、`code`、`message`、`is_error`,以及原始的 `headers`。

### 分页

列表类接口返回一个可迭代、可翻页的页面对象,不必自己循环 `page_no`:

```python
page = client.models.list(page_no=1, page_size=10)

for model in page: # 仅当前页的条目
...
page.total_count # 网关上报的目录总数
page.has_next_page()
page.next_page() # 末页返回 None

for page in client.models.list(page_size=50).iter_pages(): # 遍历所有页
...
```

`AsyncClient` 上请用 `await page.next_page_async()` 和 `async for page in page.iter_pages_async()`。对异步取回的页面调用同步翻页方法会抛出 `TypeError` 并指明该用哪个异步方法,而不是把一个未 await 的 coroutine 交给你。`client.files.list()` 和 `client.images.jobs.list()` 的分页方式相同。

序列化页面请用 `model_dump()`。迭代页面对象得到的是它的**条目**,所以 `dict(page)` 不是读取字段的方式。

`models.list(provider=...)` 是客户端侧过滤:它只收窄每一页的 `models`,而 `total_count` 始终是整个目录的总数。因此翻页走的是未过滤的目录,过滤在每页到达时各自施加。

### 原始响应

传 `raw=True` 可以拿到响应对象本身,且在任何错误检查之前:

```python
resp = client.models.list(raw=True)

resp.status_code
resp.headers # 含网关的非标准响应头
resp.request_id
resp.elapsed
resp.json() # 解码后的报文体
resp.parse(ModelList) # 类型化模型,按类型记忆化
```

`raw=True` 会跳过错误检查,所以 4xx/5xx 是返回给你而不是抛异常。事后可以用 `raise_for_status()` 补上这道检查。

`parse(...)` 需要模型类本身,而 v2 的资源模型没有从顶层包再导出——请从它们各自定义的模块导入:

```python
from dashscope.resources.models import ModelList, Model
from dashscope.resources.files import FileList, File
from dashscope.resources._jobs import Job, JobList
from dashscope.resources.embeddings import EmbeddingResponse
```

`resp.parse(...)` 走的是通用信封映射,不会重放资源自己的重塑逻辑——对 `models.list` 而言,网关以裸数组形式下发的页经它解析会得到空目录。若你需要与资源方法完全一致的结果,请用 `resp.json()`。

### 单次请求定制

所有发起 HTTP 请求的资源方法都接受同样的三个逃生舱,用于承载 SDK 尚未建模的东西:

```python
client.chat.completions.create(
model="qwen-plus",
messages=["hi"],
extra_headers={"x-gateway-experiment": "on"},
extra_query={"debug": "1"},
extra_body={"parameters": {"top_k": 5}},
)
```

`extra_headers` 位于所有其他层之上——SDK 身份头、客户端默认头、你的 `headers=`、以及单次调用的 `api_key=` / `workspace=`——且大小写不敏感匹配。`extra_body` 对嵌套 dict 做深合并,所以新增一个嵌套字段不会丢掉它的兄弟字段;列表和标量则是整体替换。若请求体不是 JSON 对象(文件上传、裸字节),就无处可合并,此时 `extra_body` 会被丢弃并告警,而不是静默丢失。

`max_retries` 为单次调用收窄或放宽重试预算,所以一个绝不能被重放的请求不再需要派生整个客户端:

```python
client.deployments.list(max_retries=0) # 只打一次,不重试
client.with_options(max_retries=5) # 依然有效,作用于整个客户端
```

流式方法四个都接受:`open_stream` 只在还没有任何事件到达你之前重试,所以流中途的失败绝不会被重放。WebSocket 方法(`audio.asr.recognize` / `recognize_file`、`tingwu.realtime`)一个都不接受——那条传输链路根本没有单次请求的 options 通道,加了形参也只会被静默忽略。

### 流式

`stream=True` 返回 `ChatStream`(`AsyncClient` 对应 `AsyncChatStream`),并提供类型化辅助属性:
Expand Down Expand Up @@ -283,6 +359,7 @@ dashscope.settings.log_level = "INFO"
| `DASHSCOPE_WEBSOCKET_BASE_URL` | 覆盖 WebSocket 端点 | 由地域/业务空间推导 |
| `DASHSCOPE_TIMEOUT` | 请求超时(秒) | `300` |
| `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` |
| `DASHSCOPE_STRICT_VALIDATION` | 响应体与类型模型不符时抛错,而不是降级解析 | `false` |
| `DASHSCOPE_HTTP_MAX_CONNECTIONS` | 连接池大小 | `100` |
| `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | 连接池保持的 keep-alive 连接数 | `20` |
| `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | keep-alive 过期时间(秒) | `5.0` |
Expand All @@ -292,6 +369,10 @@ dashscope.settings.log_level = "INFO"

三个 `DASHSCOPE_HTTP_*` 连接池变量同样可以在 `settings` 和 `config.toml` 中以 `http_max_connections`、`http_max_keepalive_connections`、`http_keepalive_expiry` 设置。传入自备的 `http_client=` 时,以它为准,上述配置全部被忽略。

默认情况下,若响应体与 SDK 的类型模型不符,会走降级解析——出问题的字段回退到各自的默认值,报文中其余部分照常可用——同时把漂移以 `DEBUG` 级别记录到 `dashscope._logging.parse` 这个 logger。设置 `DASHSCOPE_STRICT_VALIDATION=true`(或 `Client(strict_validation=True)`、`settings.strict_validation = True`、`config.toml` 里的 `strict_validation = true`)则改为抛出 `APIResponseValidationError`,这正是 CI 或契约测试想要的行为。严格校验即使在 `raise_on_error=False` 下也会抛错:那个开关管的是服务端错误,这个管的是解析错误。

一点需要注意:`DEBUG` 低于 SDK 的默认日志级别,所以除非你调高它(见[日志](#日志)),降级解析是无声的——而 `strict_validation` 是让漂移大声失败的唯一手段。类型化解析路径覆盖全部资源,且不止最终的响应对象:流式帧(`apps`、`deployments.stream_events`、`fine_tunes.stream_events`、`audio.tts.http.stream`)和 WebSocket 结果(`audio.asr`)同样逐帧降级,因此单个畸形事件不会在迭代中途抛出 `except DashScopeError` 抓不到的 `pydantic.ValidationError`。

**配置文件**(`~/.dashscope/config.toml`):

```toml
Expand Down
33 changes: 33 additions & 0 deletions dashscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,36 @@ def set_region(region: str, workspace_id: str = None):
base_websocket_api_url = f"wss://{host}/api-ws/v1/inference"


def enable_dev_mock(
mode: str = "static",
fixture_path: "str | None" = None,
) -> None:
"""Turn on the dev mock process-wide (``static``/``replay``/``record``).

Thin forwarder to :mod:`dashscope.devkit`; imported lazily so the
devkit package is never loaded on the production success path. Once
enabled, every ``Client``/``AsyncClient`` HTTP call is intercepted at
the transport. Equivalent to ``DASHSCOPE_DEV_MOCK=<mode>``.
"""
from dashscope import devkit

devkit.enable_dev_mock(mode, fixture_path)


def disable_dev_mock() -> None:
"""Turn the dev mock back off (forwarder to the devkit package)."""
from dashscope import devkit

devkit.disable_dev_mock()


def dev_mock_active() -> bool:
"""Whether the dev mock is currently active."""
from dashscope import devkit

return devkit.is_active()


# The Assistants API (dashscope.legacy_1x.assistants /
# dashscope.legacy_1x.threads) is deprecated.
# Resolve its names lazily (PEP 562) so a plain `import dashscope` does not emit
Expand Down Expand Up @@ -275,6 +305,9 @@ def __dir__():
"WsConnection",
"WsTask",
"chat",
"dev_mock_active",
"disable_dev_mock",
"enable_dev_mock",
"errors",
"set_region",
]
Expand Down
Loading
Loading