From 5cea230ff7911cbc234fb93ed67dea18ef340719 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Wed, 21 Oct 2026 17:31:08 +0800 Subject: [PATCH 01/41] refactor(v2): Refactor configuration and session management modules - Migrate save_api_key function from common module to v2._config module - Update comments to reflect the relocation of key-file authentication implementation - Move SDK session ID implementation to shared module dashscope._shared - Modify v2 package re-export strategy, no longer importing save_api_key from 1.x - Adjust incremental output parameter check logic in chat completion module - Update related tests to verify new module structure and functionality migration --- dashscope/common/_sdk_session.py | 42 +++------------------- dashscope/common/utils.py | 4 +-- dashscope/v2/__init__.py | 9 +++-- dashscope/v2/_config.py | 24 +++++++++++-- dashscope/v2/resources/chat/completions.py | 6 ++-- dashscope/v2/transport/_ident.py | 4 +-- tests/unit/test_sdk_headers.py | 14 ++++++++ tests/v2/test_config.py | 32 +++++++++++++++++ tests/v2/test_package_root.py | 22 ++++++++---- tests/v2/test_transport.py | 2 +- 10 files changed, 102 insertions(+), 57 deletions(-) diff --git a/dashscope/common/_sdk_session.py b/dashscope/common/_sdk_session.py index c5fdea8e..148e269d 100644 --- a/dashscope/common/_sdk_session.py +++ b/dashscope/common/_sdk_session.py @@ -1,43 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Single process-wide SDK session id. +"""Compatibility re-export; the implementation moved to ``dashscope._shared``. -The ``x-dashscope-sdk-session-id`` header lets server-side access logs -group every request one SDK session makes. A process can emit requests -through both stacks at once -- the 1.x API classes and the 2.0 transport --- and the 2.0 client even delegates some calls (rl, agentstudio) -straight into 1.x. If each stack minted its own id, one logical session -would split across several ids and the grouping breaks. - -This leaf module is the one source of truth both import. It depends only -on the stdlib so it can be pulled in from anywhere without dragging -``dashscope/__init__`` (and its v2 surface) into a cycle. - -The embedded ``acli`` deliberately keeps its own id: it is a one-way -sync copy of the agenticCLI repo (design doc §3.2), so wiring it to this -module would be silently reverted by the next sync and cannot be -upstreamed as written. +The session id state lives only in ``dashscope._shared._sdk_session`` so a +process keeps exactly one id. Import from there in new code. """ -from __future__ import annotations - -import threading -import uuid - -_session_id = None -_lock = threading.Lock() - - -def sdk_session_id() -> str: - """Return this process's session id, generating it on first use. +from dashscope._shared._sdk_session import sdk_session_id - Double-checked locking: concurrent callers must not each mint their - own id and break the one-id-per-process invariant log grouping - relies on. - """ - global _session_id # pylint: disable=global-statement - if _session_id is None: - with _lock: - if _session_id is None: - _session_id = uuid.uuid4().hex - return _session_id +__all__ = ["sdk_session_id"] diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index bb9229ba..28de30cf 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -15,15 +15,15 @@ import aiohttp import requests +from dashscope._shared._sdk_session import sdk_session_id from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common._sdk_session import sdk_session_id from dashscope.common.api_key import get_default_api_key from dashscope.common.constants import SSE_CONTENT_TYPE from dashscope.common.logging import logger from dashscope.version import __version__ # Shared with the 2.0 transport so one process reports one -# x-dashscope-sdk-session-id across both stacks (see _sdk_session; +# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session; # acli deliberately keeps its own, it is a one-way sync copy). _SDK_SESSION_ID = sdk_session_id() diff --git a/dashscope/v2/__init__.py b/dashscope/v2/__init__.py index 7217e39b..c50a48fc 100644 --- a/dashscope/v2/__init__.py +++ b/dashscope/v2/__init__.py @@ -7,16 +7,15 @@ """ from dashscope.v2 import errors -from dashscope.v2._config import Config, Settings, settings +from dashscope.v2._config import Config, Settings, save_api_key, settings from dashscope.v2._quickstart import chat from dashscope.v2.client import AsyncClient, Client from dashscope.v2.errors import DashScopeError, ErrorCode from dashscope.v2.transport.ws_connection import WsConnection, WsTask -# Offline helpers delegated to the 1.x implementation, re-exported so the -# 2.0 namespace is self-contained (same objects the package root already -# exposes): the local tiktoken wrapper and the key-file writer. -from dashscope.common.api_key import save_api_key # noqa: E402 +# Offline helper still delegated to the 1.x implementation, re-exported so +# the 2.0 namespace is self-contained (same objects the package root +# already exposes): the local tiktoken wrapper. from dashscope.tokenizers import ( # noqa: E402 get_tokenizer, list_tokenizers, diff --git a/dashscope/v2/_config.py b/dashscope/v2/_config.py index ad8bc0be..48b57ddc 100644 --- a/dashscope/v2/_config.py +++ b/dashscope/v2/_config.py @@ -136,8 +136,8 @@ _LOG_ENV = "DASHSCOPE_LOG" _CONFIG_PATH_ENV = "DASHSCOPE_CONFIG" _DEFAULT_CONFIG_PATH = "~/.dashscope/config.toml" -# 1.x key-file auth (dashscope.save_api_key): env-specified path first, -# then the default file -- the lowest api_key tier. +# Key-file auth (save_api_key, below): env-specified path first, then the +# default file -- the lowest api_key tier. _API_KEY_FILE_PATH_ENV = "DASHSCOPE_API_KEY_FILE_PATH" _DEFAULT_API_KEY_FILE_PATH = "~/.dashscope/api_key" @@ -542,6 +542,26 @@ def _api_key_from_file() -> Optional[str]: return None +def save_api_key( + api_key: str, api_key_file_path: Optional[str] = None +) -> None: + """Store an api key in the key file ``_api_key_from_file`` reads back. + + Without an explicit path this writes ``~/.dashscope/api_key``, the same + fixed location the 1.x ``dashscope.save_api_key`` uses, so a key stored + through either stack is picked up by both. As in 1.x, + ``DASHSCOPE_API_KEY_FILE_PATH`` redirects only the read, not the write. + """ + target = ( + _DEFAULT_API_KEY_FILE_PATH + if api_key_file_path is None + else api_key_file_path + ) + path = Path(target).expanduser() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(api_key, encoding="utf-8") + + def _pick( explicit: Any, settings_value: Any, diff --git a/dashscope/v2/resources/chat/completions.py b/dashscope/v2/resources/chat/completions.py index 17c2b1c2..cadaff6f 100644 --- a/dashscope/v2/resources/chat/completions.py +++ b/dashscope/v2/resources/chat/completions.py @@ -14,13 +14,15 @@ from typing_extensions import Literal -from dashscope.utils.param_utils import ParamUtil from dashscope.v2.errors import response_error from dashscope.v2.protocols.base import ChatRequest from dashscope.v2.resources._oss import ( OSS_RESOLVE_HEADERS, upload_message_media, ) +from dashscope.v2.resources.chat._params import ( + should_modify_incremental_output, +) from dashscope.v2.types.common import Usage, with_headers from dashscope.v2.types.completions import ( ChatCompletion, @@ -114,7 +116,7 @@ def _build_request( merge_incremental = False if stream and incremental_output is not None: if client.protocol.name == "native": - server_side = not ParamUtil.should_modify_incremental_output(model) + server_side = not should_modify_incremental_output(model) if incremental_output is False and not server_side: merge_incremental = True # 1.x also forces the wire incremental while merging diff --git a/dashscope/v2/transport/_ident.py b/dashscope/v2/transport/_ident.py index 7d856e0d..7064ed69 100644 --- a/dashscope/v2/transport/_ident.py +++ b/dashscope/v2/transport/_ident.py @@ -20,8 +20,8 @@ # One process-wide session id shared with the 1.x API classes, so a # single SDK session is not split across ids when the 2.0 client # delegates calls (rl, agentstudio) into 1.x. (acli keeps its own: it is -# a one-way sync copy, see common/_sdk_session.) -from dashscope.common._sdk_session import sdk_session_id +# a one-way sync copy, see _shared/_sdk_session.) +from dashscope._shared._sdk_session import sdk_session_id SDK_CLIENT_HEADER = "x-dashscope-sdk-client" SDK_SESSION_ID_HEADER = "x-dashscope-sdk-session-id" diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index 7a3042f3..49cf937e 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -59,6 +59,20 @@ def test_python_sdk_client_header(): assert headers[SESSION_HEADER] == _SDK_SESSION_ID +def test_session_id_has_one_source_across_stacks(): + # A process using the 1.x classes and the 2.0 client at once must + # report one id, or server-side log grouping splits a single logical + # session. The legacy deep path re-exports the neutral leaf rather + # than holding its own copy of the state. + from dashscope._shared._sdk_session import sdk_session_id as leaf + from dashscope.common._sdk_session import sdk_session_id as via_1x + from dashscope.v2.transport._ident import sdk_session_id as via_v2 + + assert via_1x is leaf + assert via_v2 is leaf + assert leaf() == _SDK_SESSION_ID + + def test_acli_client_header(): # pylint: disable=protected-access print("\n=== acli: x-dashscope-sdk-client ===") diff --git a/tests/v2/test_config.py b/tests/v2/test_config.py index da5a64c6..9e8e7cd2 100644 --- a/tests/v2/test_config.py +++ b/tests/v2/test_config.py @@ -462,6 +462,38 @@ def test_save_api_key_roundtrip(clean_env, tmp_path, monkeypatch): assert cfg.api_key == "sk-saved" +def test_save_api_key_writes_the_default_path( + clean_env, + tmp_path, + monkeypatch, +): + from dashscope.v2 import _config + + default = tmp_path / "home" / ".dashscope" / "api_key" + monkeypatch.setattr(_config, "_DEFAULT_API_KEY_FILE_PATH", str(default)) + # The env var outranks the default path when reading; drop it so this + # exercises the write-default / read-default pair. + monkeypatch.delenv("DASHSCOPE_API_KEY_FILE_PATH", raising=False) + + _config.save_api_key("sk-default") + + assert default.read_text(encoding="utf-8") == "sk-default" + cfg = resolve_config(config_path="/nonexistent/config.toml") + assert cfg.api_key == "sk-default" + + +def test_save_api_key_accepts_a_bare_filename( + clean_env, tmp_path, monkeypatch +): + from dashscope.v2 import save_api_key + + # 1.x does os.makedirs(os.path.dirname(path)), which raises when the + # path has no directory component; the v2 writer resolves against cwd. + monkeypatch.chdir(tmp_path) + save_api_key("sk-bare", "api_key") + assert (tmp_path / "api_key").read_text(encoding="utf-8") == "sk-bare" + + def test_invalid_protocol(clean_env): with pytest.raises(ValueError, match="invalid protocol"): resolve_config(protocol="grpc", config_path="/nonexistent/config.toml") diff --git a/tests/v2/test_package_root.py b/tests/v2/test_package_root.py index f363ed69..af2e4c61 100644 --- a/tests/v2/test_package_root.py +++ b/tests/v2/test_package_root.py @@ -50,21 +50,31 @@ def test_legacy_exports_untouched(): assert hasattr(dashscope, "save_api_key") -def test_v2_namespace_reexports_1x_offline_helpers(): - # The 2.0 namespace delegates the offline helpers to 1.x instead of - # re-implementing them (same objects the package root exposes). +def test_v2_namespace_reexports_1x_tokenizers(): + # The local tiktoken wrapper is still delegated to 1.x rather than + # re-implemented (same objects the package root exposes). from dashscope import v2 - from dashscope.common.api_key import save_api_key as save_1x from dashscope.tokenizers import get_tokenizer as get_1x from dashscope.tokenizers import list_tokenizers as list_1x - assert v2.save_api_key is save_1x assert v2.get_tokenizer is get_1x assert v2.list_tokenizers is list_1x - for name in ("get_tokenizer", "list_tokenizers", "save_api_key"): + for name in ("get_tokenizer", "list_tokenizers"): assert name in v2.__all__ +def test_v2_owns_save_api_key(): + # The key-file writer is v2-native, so reaching for it does not pull in + # the 1.x package root; the package root keeps its own 1.x writer. + from dashscope import v2 + from dashscope.common.api_key import save_api_key as save_1x + + assert v2.save_api_key.__module__ == "dashscope.v2._config" + assert v2.save_api_key is not save_1x + assert dashscope.save_api_key is save_1x + assert "save_api_key" in v2.__all__ + + def test_v2_get_tokenizer_encodes_locally(): from dashscope import v2 from dashscope.common.error import UnsupportedModel diff --git a/tests/v2/test_transport.py b/tests/v2/test_transport.py index faf964f1..82e04875 100644 --- a/tests/v2/test_transport.py +++ b/tests/v2/test_transport.py @@ -430,7 +430,7 @@ async def test_async_sdk_identification_headers(no_sleep): def test_sdk_session_id_cached_per_process(): - from dashscope.common import _sdk_session + from dashscope._shared import _sdk_session from dashscope.v2.transport import _ident original = _sdk_session._session_id From 1070c02200fdf625d71d17a9dc3d9c5b1e0d94a4 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Wed, 21 Oct 2026 17:34:26 +0800 Subject: [PATCH 02/41] fix: add missing dashscope._shared module --- dashscope/_shared/__init__.py | 13 ++++++++ dashscope/_shared/_sdk_session.py | 43 ++++++++++++++++++++++++ dashscope/v2/resources/chat/_params.py | 21 ++++++++++++ tests/v2/test_chat_params.py | 46 ++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 dashscope/_shared/__init__.py create mode 100644 dashscope/_shared/_sdk_session.py create mode 100644 dashscope/v2/resources/chat/_params.py create mode 100644 tests/v2/test_chat_params.py diff --git a/dashscope/_shared/__init__.py b/dashscope/_shared/__init__.py new file mode 100644 index 00000000..6250b559 --- /dev/null +++ b/dashscope/_shared/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Neutral leaf layer shared by the 1.x API and the 2.0 implementation. + +Modules here hold state or values that must exist exactly once per +installed distribution (the process-wide session id, the version +number). Copying them into either stack would silently split that single +source of truth, so both stacks import from here instead. + +Nothing in this package may import ``dashscope`` or any of its +subpackages: it sits below both stacks and is the anchor that keeps them +from forming an import cycle. Stdlib only. +""" diff --git a/dashscope/_shared/_sdk_session.py b/dashscope/_shared/_sdk_session.py new file mode 100644 index 00000000..c5fdea8e --- /dev/null +++ b/dashscope/_shared/_sdk_session.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Single process-wide SDK session id. + +The ``x-dashscope-sdk-session-id`` header lets server-side access logs +group every request one SDK session makes. A process can emit requests +through both stacks at once -- the 1.x API classes and the 2.0 transport +-- and the 2.0 client even delegates some calls (rl, agentstudio) +straight into 1.x. If each stack minted its own id, one logical session +would split across several ids and the grouping breaks. + +This leaf module is the one source of truth both import. It depends only +on the stdlib so it can be pulled in from anywhere without dragging +``dashscope/__init__`` (and its v2 surface) into a cycle. + +The embedded ``acli`` deliberately keeps its own id: it is a one-way +sync copy of the agenticCLI repo (design doc §3.2), so wiring it to this +module would be silently reverted by the next sync and cannot be +upstreamed as written. +""" + +from __future__ import annotations + +import threading +import uuid + +_session_id = None +_lock = threading.Lock() + + +def sdk_session_id() -> str: + """Return this process's session id, generating it on first use. + + Double-checked locking: concurrent callers must not each mint their + own id and break the one-id-per-process invariant log grouping + relies on. + """ + global _session_id # pylint: disable=global-statement + if _session_id is None: + with _lock: + if _session_id is None: + _session_id = uuid.uuid4().hex + return _session_id diff --git a/dashscope/v2/resources/chat/_params.py b/dashscope/v2/resources/chat/_params.py new file mode 100644 index 00000000..134a0e66 --- /dev/null +++ b/dashscope/v2/resources/chat/_params.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Request-parameter rules that depend on the model name.""" + +from __future__ import annotations + +# tts / omni / qwen-deep-research honor incremental_output=False on the +# server; every other model streams incrementally only, so the SDK has to +# keep the wire incremental and assemble the cumulative view itself. +_SERVER_SIDE_MODELS = ("tts", "omni", "qwen-deep-research") + + +def should_modify_incremental_output(model_name: str) -> bool: + """Whether the SDK must build cumulative frames client-side. + + ``False`` for the models that honor ``incremental_output`` server-side. + """ + if not isinstance(model_name, str): + return True + lowered = model_name.lower() + return not any(name in lowered for name in _SERVER_SIDE_MODELS) diff --git a/tests/v2/test_chat_params.py b/tests/v2/test_chat_params.py new file mode 100644 index 00000000..9f85ce6e --- /dev/null +++ b/tests/v2/test_chat_params.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Unit tests for the model-name rule behind incremental_output. + +The streaming behaviour this feeds is covered end to end in test_chat.py +(``test_incremental_output_false_merges_client_side`` and the exempt-model +cases); this pins the rule itself. +""" + +from __future__ import annotations + +import pytest + +from dashscope.v2.resources.chat._params import ( + should_modify_incremental_output, +) + + +@pytest.mark.parametrize( + "model", + [ + "qwen-tts", + "CosyVoice-TTS-v1", + "qwen-omni-turbo", + "Qwen2.5-Omni-7B", + "qwen-deep-research", + "qwen-DEEP-Research", + ], +) +def test_server_side_models_are_exempt(model): + assert should_modify_incremental_output(model) is False + + +@pytest.mark.parametrize( + "model", + ["qwen-plus", "qwen-max", "qwen-vl-max", "qwen3-coder-plus", ""], +) +def test_incremental_only_models_merge_client_side(model): + assert should_modify_incremental_output(model) is True + + +@pytest.mark.parametrize("model", [None, 123, ["qwen-tts"]]) +def test_a_missing_model_name_merges_client_side(model): + # Anything that is not a model name falls back to the common case + # rather than guessing an exemption. + assert should_modify_incremental_output(model) is True From f7d05bf1ff0a1416005a1bb1c23e4c1cfa84ce1d Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 23 Oct 2026 17:40:24 +0800 Subject: [PATCH 03/41] style: wrap comment and docstring lines flagged by flake8 E501 --- dashscope/__init__.py | 93 ++++++++++++++----------- dashscope/api_entities/__init__.py | 12 ++++ dashscope/v2/transport/_ident.py | 14 ++-- tests/unit/test_sdk_headers.py | 108 +++++++++++++++++------------ 4 files changed, 134 insertions(+), 93 deletions(-) diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 99375a16..4b174aa5 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -16,25 +16,33 @@ ) from dashscope.version import __version__ -from dashscope.aigc.code_generation import CodeGeneration -from dashscope.aigc.conversation import Conversation, History, HistoryItem -from dashscope.aigc.generation import AioGeneration, Generation -from dashscope.aigc.image_synthesis import ImageSynthesis -from dashscope.aigc.multimodal_conversation import ( +from dashscope.legacy_1x.aigc.code_generation import CodeGeneration +from dashscope.legacy_1x.aigc.conversation import ( + Conversation, + History, + HistoryItem, +) +from dashscope.legacy_1x.aigc.generation import AioGeneration, Generation +from dashscope.legacy_1x.aigc.image_synthesis import ImageSynthesis +from dashscope.legacy_1x.aigc.multimodal_conversation import ( MultiModalConversation, AioMultiModalConversation, ) -from dashscope.aigc.video_synthesis import VideoSynthesis -from dashscope.app.application import Application -from dashscope.audio.asr.transcription import Transcription -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.aigc.video_synthesis import VideoSynthesis +from dashscope.legacy_1x.app.application import Application +from dashscope.legacy_1x.audio.asr.transcription import Transcription +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, ) -from dashscope.audio.tts.speech_synthesizer import SpeechSynthesizer -from dashscope.api_entities.aio_session import close_shared_aio_session -from dashscope.api_entities.http_request import close_shared_sync_session -from dashscope.common.api_key import save_api_key -from dashscope.common.env import ( +from dashscope.legacy_1x.audio.tts.speech_synthesizer import SpeechSynthesizer +from dashscope.legacy_1x.api_entities.aio_session import ( + close_shared_aio_session, +) +from dashscope.legacy_1x.api_entities.http_request import ( + close_shared_sync_session, +) +from dashscope.legacy_1x.common.api_key import save_api_key +from dashscope.legacy_1x.common.env import ( MAAS_REGIONS, api_key, api_key_file_path, @@ -45,22 +53,24 @@ ) from dashscope.finetune.deployments import Deployments from dashscope.finetune.finetunes import FineTunes -from dashscope.embeddings.batch_text_embedding import BatchTextEmbedding -from dashscope.embeddings.batch_text_embedding_response import ( +from dashscope.legacy_1x.embeddings.batch_text_embedding import ( + BatchTextEmbedding, +) +from dashscope.legacy_1x.embeddings.batch_text_embedding_response import ( BatchTextEmbeddingResponse, ) -from dashscope.embeddings.multimodal_embedding import ( +from dashscope.legacy_1x.embeddings.multimodal_embedding import ( MultiModalEmbedding, MultiModalEmbeddingItemAudio, MultiModalEmbeddingItemImage, MultiModalEmbeddingItemText, AioMultiModalEmbedding, ) -from dashscope.embeddings.text_embedding import TextEmbedding -from dashscope.files import Files -from dashscope.models import Models -from dashscope.nlp.understanding import Understanding -from dashscope.rerank import AioTextReRank, TextReRank +from dashscope.legacy_1x.embeddings.text_embedding import TextEmbedding +from dashscope.legacy_1x.files import Files +from dashscope.legacy_1x.models import Models +from dashscope.legacy_1x.nlp.understanding import Understanding +from dashscope.legacy_1x.rerank import AioTextReRank, TextReRank from dashscope.tokenizers import ( Tokenization, Tokenizer, @@ -114,28 +124,29 @@ def set_region(region: str, workspace_id: str = None): base_websocket_api_url = f"wss://{host}/api-ws/v1/inference" -# The Assistants API (dashscope.assistants / dashscope.threads) is deprecated. +# 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 # the DeprecationWarning for code that never touches them; accessing any of these -# names imports dashscope.assistants, which warns once. +# names imports dashscope.legacy_1x.assistants, which warns once. _DEPRECATED_LAZY_IMPORTS = { - "Assistant": "dashscope.assistants", - "AssistantList": "dashscope.assistants", - "Assistants": "dashscope.assistants", - "AssistantFile": "dashscope.assistants", - "DeleteResponse": "dashscope.assistants", - "MessageFile": "dashscope.threads", - "Messages": "dashscope.threads", - "Run": "dashscope.threads", - "RunList": "dashscope.threads", - "Runs": "dashscope.threads", - "RunStep": "dashscope.threads", - "RunStepList": "dashscope.threads", - "Steps": "dashscope.threads", - "Thread": "dashscope.threads", - "ThreadMessage": "dashscope.threads", - "ThreadMessageList": "dashscope.threads", - "Threads": "dashscope.threads", + "Assistant": "dashscope.legacy_1x.assistants", + "AssistantList": "dashscope.legacy_1x.assistants", + "Assistants": "dashscope.legacy_1x.assistants", + "AssistantFile": "dashscope.legacy_1x.assistants", + "DeleteResponse": "dashscope.legacy_1x.assistants", + "MessageFile": "dashscope.legacy_1x.threads", + "Messages": "dashscope.legacy_1x.threads", + "Run": "dashscope.legacy_1x.threads", + "RunList": "dashscope.legacy_1x.threads", + "Runs": "dashscope.legacy_1x.threads", + "RunStep": "dashscope.legacy_1x.threads", + "RunStepList": "dashscope.legacy_1x.threads", + "Steps": "dashscope.legacy_1x.threads", + "Thread": "dashscope.legacy_1x.threads", + "ThreadMessage": "dashscope.legacy_1x.threads", + "ThreadMessageList": "dashscope.legacy_1x.threads", + "Threads": "dashscope.legacy_1x.threads", } diff --git a/dashscope/api_entities/__init__.py b/dashscope/api_entities/__init__.py index e69de29b..3d4c9c3d 100644 --- a/dashscope/api_entities/__init__.py +++ b/dashscope/api_entities/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to +``dashscope.legacy_1x.api_entities``. + +Import from ``dashscope.legacy_1x.api_entities`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.api_entities") diff --git a/dashscope/v2/transport/_ident.py b/dashscope/v2/transport/_ident.py index 7064ed69..5e1aa9d1 100644 --- a/dashscope/v2/transport/_ident.py +++ b/dashscope/v2/transport/_ident.py @@ -18,9 +18,9 @@ from typing import Optional # One process-wide session id shared with the 1.x API classes, so a -# single SDK session is not split across ids when the 2.0 client -# delegates calls (rl, agentstudio) into 1.x. (acli keeps its own: it is -# a one-way sync copy, see _shared/_sdk_session.) +# single SDK session is not split across ids when one process issues +# requests through both stacks. (acli keeps its own: it is a one-way +# sync copy, see _shared/_sdk_session.) from dashscope._shared._sdk_session import sdk_session_id SDK_CLIENT_HEADER = "x-dashscope-sdk-client" @@ -33,7 +33,8 @@ # ``embeddings`` / ``finetune`` / ...) using the same base vocabulary as 1.x # ``get_api_module``, but carries a trailing ``V2_MODULE_MARKER`` so the two # API generations stay separable. The version segment cannot do this: one -# installed package (e.g. 2.0.0) ships both the 1.x surface (``dashscope.aigc``) +# installed package (e.g. 2.0.0) ships both the 1.x surface +# (``dashscope.legacy_1x.aigc``) # and this v2 surface, and both read the same ``__version__``, so a 1.x # ``Generation.call()`` and a v2 ``client.chat.completions.create()`` would # otherwise both emit ``python-sdk/2.0.0/aigc``. With the marker v2 emits @@ -43,8 +44,9 @@ DEFAULT_MODULE = "dashscope" # Trailing generation marker appended to every v2 module tag (``aigc`` -> # ``aigc2``). Only the v2 transport applies it; the 1.x surface in -# ``common.utils.get_sdk_headers`` emits the unmarked module, and calls that -# delegate into 1.x (e.g. ``client.agentstudio``) inherit the 1.x tag. +# ``common.utils.get_sdk_headers`` emits the unmarked module, and so does +# AgentStudio, whose own transport builds its header from the shared +# ``_shared._sdk_ident`` because the backend already reports on that tag. V2_MODULE_MARKER = "2" diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index 49cf937e..af497df8 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -9,14 +9,14 @@ from dashscope.acli import SDK_SESSION_ID as ACLI_SESSION_ID from dashscope.acli import __version__ as acli_version from dashscope.acli.providers.tongyi import TongyiProvider -from dashscope.common.utils import _SDK_SESSION_ID, get_sdk_headers +from dashscope.legacy_1x.common.utils import _SDK_SESSION_ID, get_sdk_headers CLIENT_HEADER = "x-dashscope-sdk-client" SESSION_HEADER = "x-dashscope-sdk-session-id" DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" # client value sources: -# - "python-sdk": SDK default (_SDK_CLIENT in common/utils.py) +# - "python-sdk": SDK default (_SDK_CLIENT in _shared/_sdk_ident.py) # - "python-cli": dashscope/cli/__init__.py calls # set_sdk_client("python-cli") at CLI process startup # - "acli": acli/providers/tongyi.py builds its own header @@ -35,7 +35,10 @@ @pytest.fixture(autouse=True) def _pin_sdk_client(monkeypatch): # CLI tests switch the client to python-cli in-process; pin the default - monkeypatch.setattr("dashscope.common.utils._SDK_CLIENT", "python-sdk") + monkeypatch.setattr( + "dashscope._shared._sdk_ident._SDK_CLIENT", + "python-sdk", + ) def _check_client_header(value, client, version, module=""): @@ -65,7 +68,9 @@ def test_session_id_has_one_source_across_stacks(): # session. The legacy deep path re-exports the neutral leaf rather # than holding its own copy of the state. from dashscope._shared._sdk_session import sdk_session_id as leaf - from dashscope.common._sdk_session import sdk_session_id as via_1x + from dashscope.legacy_1x.common._sdk_session import ( + sdk_session_id as via_1x, + ) from dashscope.v2.transport._ident import sdk_session_id as via_v2 assert via_1x is leaf @@ -104,7 +109,7 @@ def test_sdk_headers_disabled(monkeypatch): def test_python_cli_client_header(): - from dashscope.common.utils import set_sdk_client + from dashscope.legacy_1x.common.utils import set_sdk_client set_sdk_client("python-cli") try: @@ -121,14 +126,14 @@ def test_python_cli_client_header(): def test_cli_import_marks_process(): - # subprocess isolation check: after importing dashscope.cli, requests in + # subprocess isolation check: after importing dashscope.legacy_1x.cli, requests in # that process must carry client=python-cli (independent of test order) import subprocess import sys code = ( - "import dashscope.cli; " - "from dashscope.common.utils import get_sdk_headers; " + "import dashscope.legacy_1x.cli; " + "from dashscope.legacy_1x.common.utils import get_sdk_headers; " "h = get_sdk_headers(); " "c = h['x-dashscope-sdk-client']; " "assert c.startswith('python-cli/'), c; " @@ -195,27 +200,28 @@ def test_embedded_module_defaults_to_app(monkeypatch): def test_get_api_module_mapping(): - from dashscope.common.utils import get_api_module + from dashscope.legacy_1x.common.utils import get_api_module cases = { - "dashscope.aigc.generation": "aigc", - "dashscope.aigc.multimodal_conversation": "aigc", - "dashscope.audio.tts.speech_synthesizer": "audio", - "dashscope.audio.asr.recognition": "audio", - "dashscope.embeddings.text_embedding": "embeddings", - "dashscope.nlp.understanding": "nlp", - "dashscope.rerank.text_rerank": "rerank", - "dashscope.app.application": "app", + "dashscope.legacy_1x.aigc.generation": "aigc", + "dashscope.legacy_1x.aigc.multimodal_conversation": "aigc", + "dashscope.legacy_1x.audio.tts.speech_synthesizer": "audio", + "dashscope.legacy_1x.audio.asr.recognition": "audio", + "dashscope.legacy_1x.embeddings.text_embedding": "embeddings", + "dashscope.legacy_1x.nlp.understanding": "nlp", + "dashscope.legacy_1x.rerank.text_rerank": "rerank", + "dashscope.legacy_1x.app.application": "app", "dashscope.tokenizers.tokenization": "tokenizers", - "dashscope.assistants.assistants": "assistants", + "dashscope.legacy_1x.assistants.assistants": "assistants", # threads/messages/runs/steps are grouped into the assistants module - "dashscope.threads.threads": "assistants", - "dashscope.threads.runs.runs": "assistants", - "dashscope.models": "models", - "dashscope.files": "files", + "dashscope.legacy_1x.threads.threads": "assistants", + "dashscope.legacy_1x.threads.runs.runs": "assistants", + "dashscope.legacy_1x.models": "models", + "dashscope.legacy_1x.files": "files", "dashscope.finetune.finetunes": "finetune", - "dashscope.multimodal.tingwu.tingwu": "multimodal", - "dashscope.common.utils": "common", # non-API classes never reach this + "dashscope.legacy_1x.multimodal.tingwu.tingwu": "multimodal", + # non-API classes never reach this + "dashscope.legacy_1x.common.utils": "common", "other.lib": "", } for module_name, expected in cases.items(): @@ -237,8 +243,10 @@ def capture_headers(monkeypatch): import aiohttp import requests - from dashscope.api_entities.http_request import HttpRequest - from dashscope.api_entities.websocket_request import WebSocketRequest + from dashscope.legacy_1x.api_entities.http_request import HttpRequest + from dashscope.legacy_1x.api_entities.websocket_request import ( + WebSocketRequest, + ) def _raise(headers): raise _CapturedRequest(headers) @@ -304,29 +312,35 @@ def test_service_api_modules(capture_headers, tmp_path): Understanding, VideoSynthesis, ) - from dashscope.aigc.generation import AioGeneration - from dashscope.aigc.multimodal_conversation import ( + from dashscope.legacy_1x.aigc.generation import AioGeneration + from dashscope.legacy_1x.aigc.multimodal_conversation import ( AioMultiModalConversation, ) - from dashscope.audio.asr.recognition import ( + from dashscope.legacy_1x.audio.asr.recognition import ( Recognition, RecognitionCallback, ) - from dashscope.audio.asr.transcription import Transcription - from dashscope.audio.asr.translation_recognizer import ( + from dashscope.legacy_1x.audio.asr.transcription import Transcription + from dashscope.legacy_1x.audio.asr.translation_recognizer import ( TranslationRecognizerCallback, TranslationRecognizerRealtime, ) - from dashscope.audio.http_tts.http_speech_synthesizer import ( + from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, ) - from dashscope.audio.qwen_asr.qwen_transcription import QwenTranscription - from dashscope.audio.tts.speech_synthesizer import SpeechSynthesizer - from dashscope.embeddings.batch_text_embedding import BatchTextEmbedding - from dashscope.embeddings.multimodal_embedding import ( + from dashscope.legacy_1x.audio.qwen_asr.qwen_transcription import ( + QwenTranscription, + ) + from dashscope.legacy_1x.audio.tts.speech_synthesizer import ( + SpeechSynthesizer, + ) + from dashscope.legacy_1x.embeddings.batch_text_embedding import ( + BatchTextEmbedding, + ) + from dashscope.legacy_1x.embeddings.multimodal_embedding import ( AioMultiModalEmbedding, ) - from dashscope.rerank.text_rerank import AioTextReRank + from dashscope.legacy_1x.rerank.text_rerank import AioTextReRank audio_file = tmp_path / "a.wav" audio_file.write_bytes(b"\x00" * 128) @@ -514,8 +528,8 @@ def test_tasks_module(capture_headers): import asyncio from dashscope import ImageSynthesis - from dashscope.aigc.image_synthesis import AioImageSynthesis - from dashscope.aigc.video_synthesis import VideoSynthesis + from dashscope.legacy_1x.aigc.image_synthesis import AioImageSynthesis + from dashscope.legacy_1x.aigc.video_synthesis import VideoSynthesis cases = [ ( @@ -604,18 +618,20 @@ def test_resource_api_modules(capture_headers): def test_realtime_websocket_modules(): # pylint: disable=import-outside-toplevel,protected-access import dashscope - from dashscope.audio.qwen_omni.omni_realtime import ( + from dashscope.legacy_1x.audio.qwen_omni.omni_realtime import ( OmniRealtimeCallback, OmniRealtimeConversation, ) - from dashscope.audio.qwen_tts_realtime.qwen_tts_realtime import ( + from dashscope.legacy_1x.audio.qwen_tts_realtime.qwen_tts_realtime import ( QwenTtsRealtime, ) - from dashscope.audio.tts_v2.speech_synthesizer import Request as TtsV2Req - from dashscope.multimodal.multimodal_dialog import ( + from dashscope.legacy_1x.audio.tts_v2.speech_synthesizer import ( + Request as TtsV2Req, + ) + from dashscope.legacy_1x.multimodal.multimodal_dialog import ( _Request as DialogRequest, ) - from dashscope.multimodal.tingwu.tingwu_realtime import ( + from dashscope.legacy_1x.multimodal.tingwu.tingwu_realtime import ( _Request as TingwuRequest, ) @@ -666,13 +682,13 @@ class _Cb(OmniRealtimeCallback): def test_encryption_module(monkeypatch): # pylint: disable=import-outside-toplevel - from dashscope.api_entities.encryption import Encryption + from dashscope.legacy_1x.api_entities.encryption import Encryption def fake_get(url, headers=None, timeout=None): raise _CapturedRequest(headers) monkeypatch.setattr( - "dashscope.api_entities.encryption.requests.get", + "dashscope.legacy_1x.api_entities.encryption.requests.get", fake_get, ) monkeypatch.setattr("dashscope.api_key", "sk-test") From eff77e86b61b33885facadf94daab446d9e1020b Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 23 Oct 2026 17:52:54 +0800 Subject: [PATCH 04/41] refactor(shared): update shared module docs and migrate subpackage implementations - Update _shared module documentation to clarify its role as state shared between the two stacks - Migrate the agentstudio subpackage implementation to dashscope.v2.agentstudio - Add a compatibility layer for agentstudio to preserve historical import paths - Migrate aigc, app, assistants, audio, cli, common, embeddings and other subpackage implementations to their corresponding legacy_1x modules - Add backward-compatible import shims for each subpackage - Remove finetune/reinforcement-related demo and parser components - Update the import structure of the finetune/reinforcement module --- MANIFEST.in | 2 +- dashscope/_shared/__init__.py | 30 +- dashscope/_shared/_errors.py | 21 + dashscope/_shared/_sdk_ident.py | 121 +++ dashscope/_shared/_sdk_session.py | 12 +- dashscope/_shared/_validation.py | 64 ++ dashscope/acli/ui/tts.py | 4 +- dashscope/acli/ui/voice.py | 5 +- dashscope/agentstudio/__init__.py | 204 ++--- dashscope/aigc/__init__.py | 29 +- dashscope/app/__init__.py | 9 +- dashscope/assistants/__init__.py | 32 +- dashscope/audio/__init__.py | 16 +- dashscope/cli/__init__.py | 592 +------------- dashscope/common/__init__.py | 11 + dashscope/embeddings/__init__.py | 15 +- dashscope/files.py | 137 +--- dashscope/finetune/agentic_rl.py | 606 ++------------ dashscope/finetune/customize_types.py | 2 +- dashscope/finetune/deployments.py | 2 +- dashscope/finetune/finetunes.py | 4 +- dashscope/finetune/reinforcement/__init__.py | 409 ++++------ .../reinforcement/component/demo/__init__.py | 19 - .../component/parser/__init__.py | 20 - .../component/processor/__init__.py | 20 - dashscope/io/__init__.py | 11 + dashscope/legacy_1x/__init__.py | 13 + dashscope/legacy_1x/_shim.py | 55 ++ dashscope/legacy_1x/aigc/__init__.py | 24 + .../{ => legacy_1x}/aigc/chat_completion.py | 12 +- .../{ => legacy_1x}/aigc/code_generation.py | 10 +- .../{ => legacy_1x}/aigc/conversation.py | 20 +- dashscope/{ => legacy_1x}/aigc/generation.py | 16 +- .../{ => legacy_1x}/aigc/image_generation.py | 14 +- .../{ => legacy_1x}/aigc/image_synthesis.py | 16 +- .../aigc/multimodal_conversation.py | 16 +- .../{ => legacy_1x}/aigc/video_synthesis.py | 10 +- .../api_entities}/__init__.py | 0 .../api_entities/aio_session.py | 2 +- .../api_entities/aiohttp_request.py | 16 +- .../api_entities/api_request_data.py | 4 +- .../api_entities/api_request_factory.py | 23 +- .../api_entities/base_request.py | 6 +- .../api_entities/chat_completion_types.py | 2 +- .../api_entities/dashscope_response.py | 0 .../api_entities/encryption.py | 10 +- .../api_entities/http_request.py | 18 +- .../api_entities/websocket_request.py | 16 +- dashscope/legacy_1x/app/__init__.py | 6 + dashscope/{ => legacy_1x}/app/application.py | 20 +- .../app/application_response.py | 2 +- dashscope/legacy_1x/assistants/__init__.py | 31 + .../assistants/assistant_types.py | 2 +- .../{ => legacy_1x}/assistants/assistants.py | 8 +- dashscope/{ => legacy_1x}/assistants/files.py | 8 +- dashscope/legacy_1x/audio/__init__.py | 13 + .../{ => legacy_1x}/audio/asr/__init__.py | 0 .../audio/asr/asr_phrase_manager.py | 10 +- .../{ => legacy_1x}/audio/asr/recognition.py | 16 +- .../audio/asr/transcription.py | 10 +- .../audio/asr/translation_recognizer.py | 14 +- .../{ => legacy_1x}/audio/asr/vocabulary.py | 6 +- .../audio/http_tts/__init__.py | 2 +- .../audio/http_tts/http_speech_synthesizer.py | 14 +- .../audio/qwen_asr/__init__.py | 0 .../audio/qwen_asr/qwen_transcription.py | 8 +- .../audio/qwen_omni/__init__.py | 0 .../audio/qwen_omni/omni_realtime.py | 6 +- .../audio/qwen_tts/__init__.py | 0 .../audio/qwen_tts/speech_synthesizer.py | 8 +- .../audio/qwen_tts_realtime/__init__.py | 0 .../qwen_tts_realtime/qwen_tts_realtime.py | 6 +- .../{ => legacy_1x}/audio/tts/__init__.py | 0 .../audio/tts/speech_synthesizer.py | 10 +- .../{ => legacy_1x}/audio/tts_v2/__init__.py | 0 .../audio/tts_v2/enrollment.py | 6 +- .../audio/tts_v2/speech_synthesizer.py | 12 +- dashscope/legacy_1x/cli/__init__.py | 591 ++++++++++++++ dashscope/{ => legacy_1x}/cli/__main__.py | 4 +- dashscope/{ => legacy_1x}/cli/agentic_rl.py | 7 +- dashscope/{ => legacy_1x}/cli/application.py | 2 +- dashscope/{ => legacy_1x}/cli/auth.py | 13 +- .../{ => legacy_1x}/cli/code_generation.py | 9 +- dashscope/{ => legacy_1x}/cli/common.py | 2 +- dashscope/{ => legacy_1x}/cli/deployments.py | 4 +- dashscope/{ => legacy_1x}/cli/embeddings.py | 2 +- dashscope/{ => legacy_1x}/cli/files.py | 4 +- dashscope/{ => legacy_1x}/cli/fine_tunes.py | 4 +- dashscope/{ => legacy_1x}/cli/generation.py | 4 +- .../{ => legacy_1x}/cli/image_generation.py | 6 +- .../{ => legacy_1x}/cli/image_synthesis.py | 2 +- dashscope/{ => legacy_1x}/cli/models.py | 2 +- .../cli/multimodal_conversation.py | 2 +- .../cli/multimodal_embedding.py | 2 +- dashscope/{ => legacy_1x}/cli/oss.py | 9 +- dashscope/{ => legacy_1x}/cli/rerank.py | 2 +- .../{ => legacy_1x}/cli/speech_synthesis.py | 2 +- dashscope/{ => legacy_1x}/cli/tokenization.py | 2 +- .../{ => legacy_1x}/cli/transcription.py | 2 +- .../{ => legacy_1x}/cli/understanding.py | 2 +- .../{ => legacy_1x}/cli/video_synthesis.py | 7 +- .../server => legacy_1x/client}/__init__.py | 0 dashscope/{ => legacy_1x}/client/base_api.py | 26 +- .../common}/__init__.py | 0 .../{ => legacy_1x}/common/_sdk_session.py | 0 dashscope/{ => legacy_1x}/common/api_key.py | 4 +- dashscope/{ => legacy_1x}/common/base_type.py | 0 dashscope/{ => legacy_1x}/common/constants.py | 0 dashscope/{ => legacy_1x}/common/env.py | 61 +- dashscope/{ => legacy_1x}/common/error.py | 7 +- dashscope/{ => legacy_1x}/common/logging.py | 2 +- .../{ => legacy_1x}/common/message_manager.py | 2 +- dashscope/{ => legacy_1x}/common/utils.py | 95 +-- dashscope/legacy_1x/embeddings/__init__.py | 12 + .../embeddings/batch_text_embedding.py | 12 +- .../batch_text_embedding_response.py | 2 +- .../embeddings/multimodal_embedding.py | 10 +- .../embeddings/text_embedding.py | 10 +- dashscope/legacy_1x/files.py | 138 ++++ .../functions => legacy_1x/io}/__init__.py | 0 dashscope/{ => legacy_1x}/io/input_output.py | 2 +- dashscope/legacy_1x/model.py | 64 ++ dashscope/legacy_1x/models.py | 80 ++ dashscope/legacy_1x/multimodal/__init__.py | 95 +++ .../multimodal/dialog_state.py | 0 .../multimodal/multimodal_constants.py | 0 .../multimodal/multimodal_dialog.py | 16 +- .../multimodal/multimodal_request_params.py | 0 .../multimodal/tingwu/__init__.py | 0 .../multimodal/tingwu/tingwu.py | 12 +- .../multimodal/tingwu/tingwu_realtime.py | 12 +- dashscope/legacy_1x/nlp/__init__.py | 0 .../{ => legacy_1x}/nlp/understanding.py | 12 +- dashscope/legacy_1x/protocol/__init__.py | 0 .../{ => legacy_1x}/protocol/websocket.py | 0 dashscope/legacy_1x/rerank/__init__.py | 6 + .../{ => legacy_1x}/rerank/text_rerank.py | 8 +- dashscope/legacy_1x/threads/__init__.py | 29 + .../legacy_1x/threads/messages/__init__.py | 0 .../{ => legacy_1x}/threads/messages/files.py | 9 +- .../threads/messages/messages.py | 11 +- dashscope/legacy_1x/threads/runs/__init__.py | 0 .../{ => legacy_1x}/threads/runs/runs.py | 10 +- .../{ => legacy_1x}/threads/runs/steps.py | 8 +- .../{ => legacy_1x}/threads/thread_types.py | 4 +- dashscope/{ => legacy_1x}/threads/threads.py | 10 +- dashscope/legacy_1x/utils/__init__.py | 0 .../{ => legacy_1x}/utils/message_utils.py | 0 dashscope/{ => legacy_1x}/utils/oss_utils.py | 14 +- .../{ => legacy_1x}/utils/param_utils.py | 0 dashscope/model.py | 63 +- dashscope/models.py | 79 +- dashscope/multimodal/__init__.py | 96 +-- dashscope/nlp/__init__.py | 11 + dashscope/protocol/__init__.py | 11 + dashscope/rerank/__init__.py | 9 +- dashscope/threads/__init__.py | 30 +- dashscope/tokenizers/__init__.py | 12 +- dashscope/tokenizers/qwen_tokenizer.py | 198 +---- dashscope/tokenizers/tokenization.py | 10 +- dashscope/tokenizers/tokenizer.py | 86 +- dashscope/tokenizers/tokenizer_base.py | 33 +- dashscope/utils/__init__.py | 11 + dashscope/v2/__init__.py | 9 +- dashscope/v2/_config.py | 5 +- dashscope/v2/agentstudio/__init__.py | 164 ++++ dashscope/{ => v2}/agentstudio/client.py | 52 +- dashscope/{ => v2}/agentstudio/constants.py | 0 dashscope/{ => v2}/agentstudio/exceptions.py | 2 +- dashscope/{ => v2}/agentstudio/pagination.py | 0 .../agentstudio/resources/__init__.py | 20 +- .../agentstudio/resources/_helpers.py | 2 +- .../{ => v2}/agentstudio/resources/agents.py | 8 +- .../agentstudio/resources/deployments.py | 8 +- .../agentstudio/resources/environments.py | 8 +- .../{ => v2}/agentstudio/resources/files.py | 8 +- .../agentstudio/resources/memory_stores.py | 8 +- .../agentstudio/resources/security.py | 4 +- .../agentstudio/resources/session_events.py | 12 +- .../resources/session_resources.py | 8 +- .../agentstudio/resources/session_threads.py | 8 +- .../agentstudio/resources/sessions.py | 14 +- .../{ => v2}/agentstudio/resources/skills.py | 8 +- .../{ => v2}/agentstudio/resources/vaults.py | 8 +- .../resources/webhook_endpoints.py | 8 +- dashscope/{ => v2}/agentstudio/streaming.py | 10 +- dashscope/{ => v2}/agentstudio/transport.py | 6 +- .../{ => v2}/agentstudio/types/__init__.py | 6 +- .../{ => v2}/agentstudio/types/models.py | 2 +- .../{ => v2}/agentstudio/types/params.py | 2 +- dashscope/v2/client.py | 22 +- dashscope/v2/finetune/__init__.py | 16 + dashscope/v2/finetune/_submit.py | 140 ++++ dashscope/v2/finetune/agentic_rl.py | 745 ++++++++++++++++++ .../v2/finetune/reinforcement/__init__.py | 257 ++++++ .../finetune/reinforcement/common/__init__.py | 0 .../reinforcement/common/constants.py | 7 + .../finetune/reinforcement/common/errors.py | 0 .../finetune/reinforcement/common/log.py | 2 +- .../finetune/reinforcement/common/model.py | 12 +- .../reinforcement/common/model_types.py | 0 .../finetune/reinforcement/common/utils.py | 10 +- .../reinforcement/component/__init__.py | 28 +- .../reinforcement/component/data/__init__.py | 14 +- .../component/data/base_data_model.py | 2 +- .../component/data/group_reward_input.py | 4 +- .../component/data/group_reward_output.py | 4 +- .../component/data/reward_input.py | 4 +- .../component/data/reward_output.py | 2 +- .../component/data/rollout_input.py | 4 +- .../component/data/rollout_output.py | 2 +- .../reinforcement/component/demo/__init__.py | 19 + .../demo/group_reward_processor_demo.py | 12 +- .../component/demo/reward_processor_demo.py | 10 +- .../component/demo/rollout_processor_demo.py | 10 +- .../reinforcement/component/func_decorator.py | 4 +- .../reinforcement/component/func_manager.py | 18 +- .../component/observability/__init__.py | 8 +- .../component/observability/genai/__init__.py | 10 +- .../component/observability/genai/_core.py | 0 .../observability/genai/llm_dashscope.py | 8 +- .../observability/genai/llm_openai.py | 8 +- .../component/observability/genai/llm_span.py | 6 +- .../component/observability/genai/messages.py | 6 +- .../component/observability/genai/tools.py | 2 +- .../component/observability/processor_span.py | 8 +- .../component/observability/tracing.py | 9 +- .../component/parser/__init__.py | 20 + .../component/parser/base_parser.py | 2 +- .../component/parser/group_reward_parser.py | 6 +- .../component/parser/reward_parser.py | 6 +- .../component/parser/rollout_parser.py | 6 +- .../component/processor/__init__.py | 20 + .../abstract_group_reward_processor.py | 6 +- .../component/processor/abstract_processor.py | 2 +- .../processor/abstract_reward_processor.py | 8 +- .../processor/abstract_rollout_processor.py | 6 +- .../component/server/__init__.py | 0 .../reinforcement/component/server/server.py | 18 +- .../examples/observability/__init__.py | 0 .../observability/smoke_observability.py | 0 .../examples/workspace/README-zh.md | 0 .../examples/workspace/README.md | 0 .../examples/workspace/UserGuide-zh.md | 0 .../examples/workspace/UserGuide.md | 0 .../reinforcement/examples/workspace/cli.sh | 0 .../workspace/data/calc_train_min.jsonl | 0 .../workspace/data/calc_validation_min.jsonl | 0 .../examples/workspace/functions/__init__.py | 0 .../functions/reward/group_reward.py | 0 .../workspace/functions/reward/reward.py | 0 .../functions/reward/reward_decorator.py | 0 .../workspace/functions/rollout/rollout.py | 0 .../functions/rollout/rollout_only.py | 0 .../examples/workspace/opd-job.yaml | 0 .../examples/workspace/requirements.txt | 0 .../resources/group_reward_input.json | 0 .../resources/reward_decorator_input.json | 0 .../workspace/resources/reward_input.json | 0 .../workspace/resources/rollout_input.json | 0 .../examples/workspace/rl-job.yaml | 0 .../examples/workspace/scripts/build.sh | 2 +- .../scripts/query_local_group_reward.sh | 0 .../workspace/scripts/query_local_reward.sh | 0 .../scripts/query_local_reward_decorator.sh | 0 .../workspace/scripts/query_local_rollout.sh | 0 .../workspace/scripts/query_online_tuning.sh | 0 .../scripts/start_local_group_reward.sh | 0 .../workspace/scripts/start_local_reward.sh | 0 .../scripts/start_local_reward_decorator.sh | 0 .../workspace/scripts/start_local_rollout.sh | 0 .../examples/workspace/submit_job.py | 4 +- .../examples/workspace/test_functions.py | 0 dashscope/v2/resources/_agentstudio.py | 21 +- dashscope/v2/resources/_oss.py | 72 +- dashscope/v2/resources/_oss_upload.py | 216 +++++ dashscope/v2/resources/audio/customization.py | 6 +- dashscope/v2/resources/chat/completions.py | 2 +- .../v2/resources/reinforcement_learning.py | 72 +- dashscope/v2/resources/tingwu.py | 2 +- dashscope/v2/tokenizers/__init__.py | 17 + .../tokenizers}/qwen.tiktoken | 0 dashscope/v2/tokenizers/qwen_tokenizer.py | 167 ++++ dashscope/v2/tokenizers/tokenizer.py | 43 + dashscope/v2/tokenizers/tokenizer_base.py | 31 + dashscope/v2/types/messages.py | 4 +- .../audio/direct_call/realtime_qwen_audio.py | 2 +- .../audio/realtime_qwen_audio.py | 2 +- examples/legacy_1x/aio_generation/basic.py | 2 +- .../aio_generation/custom_session.py | 2 +- .../legacy_1x/aio_generation/deep_research.py | 2 +- .../legacy_1x/aio_generation/reasoning.py | 2 +- .../legacy_1x/aio_generation/tool_calls.py | 2 +- examples/legacy_1x/aio_image_synthesis.py | 2 +- examples/legacy_1x/aio_video_synthesis.py | 2 +- examples/legacy_1x/assistant_api_file.py | 2 +- examples/legacy_1x/cv_models.py | 4 +- examples/legacy_1x/image_generation.py | 4 +- examples/legacy_1x/set_region_example.py | 4 +- examples/legacy_1x/tingwu_realtime.py | 2 +- examples/legacy_1x/tingwu_usages.py | 2 +- tests/integration/test_large_utf8_payload.py | 2 +- tests/legacy/conftest.py | 2 +- tests/legacy/test_async_api.py | 6 +- tests/legacy/test_encryption.py | 2 +- tests/legacy/test_image_synthesis.py | 2 +- tests/legacy/test_speech_recognizer.py | 6 +- tests/legacy/test_speech_synthesis.py | 4 +- tests/legacy/test_speech_transcription.py | 8 +- tests/legacy/test_video_synthesis.py | 2 +- tests/legacy/test_websocket_async_api.py | 2 +- tests/legacy/test_websocket_parameters.py | 2 +- tests/legacy/test_websocket_sync_api.py | 2 +- tests/unit/conftest.py | 4 +- tests/unit/http_task_request.py | 8 +- tests/unit/mock_server.py | 2 +- tests/unit/test_acli_sdk_contract.py | 362 +++++++++ tests/unit/test_agentic_rl_cli_opd.py | 16 +- tests/unit/test_agentic_rl_submit_job.py | 13 +- tests/unit/test_aio_session.py | 2 +- tests/unit/test_aio_session_cleanup.py | 12 +- tests/unit/test_application.py | 2 +- tests/unit/test_asr_phrases.py | 11 +- tests/unit/test_assistant_files.py | 2 +- tests/unit/test_async_custom_session.py | 151 ++-- tests/unit/test_cli_agent_route.py | 2 +- tests/unit/test_cli_application.py | 2 +- tests/unit/test_cli_auth.py | 8 +- tests/unit/test_cli_code_generation.py | 2 +- tests/unit/test_cli_common.py | 6 +- tests/unit/test_cli_embeddings.py | 2 +- tests/unit/test_cli_image_generation.py | 2 +- tests/unit/test_cli_image_synthesis.py | 2 +- tests/unit/test_cli_main.py | 82 +- tests/unit/test_cli_models.py | 2 +- .../unit/test_cli_multimodal_conversation.py | 2 +- tests/unit/test_cli_multimodal_embedding.py | 2 +- tests/unit/test_cli_rerank.py | 2 +- tests/unit/test_cli_speech_synthesis.py | 2 +- tests/unit/test_cli_tokenization.py | 2 +- tests/unit/test_cli_transcription.py | 2 +- tests/unit/test_cli_understanding.py | 2 +- tests/unit/test_cli_video_synthesis.py | 2 +- tests/unit/test_code_generation.py | 2 +- tests/unit/test_conversation.py | 14 +- tests/unit/test_dashscope_response.py | 2 +- tests/unit/test_http_speech_synthesizer.py | 6 +- tests/unit/test_import_deprecation.py | 11 +- tests/unit/test_layer_decoupling.py | 246 ++++++ tests/unit/test_maas_region.py | 26 +- tests/unit/test_messages.py | 2 +- tests/unit/test_multimodal_dialog.py | 24 +- tests/unit/test_opd.py | 33 +- tests/unit/test_oss_utils.py | 6 +- .../unit/test_recognition_explicit_params.py | 5 +- tests/unit/test_rerank.py | 4 +- tests/unit/test_speech_synthesis_v2.py | 6 +- tests/unit/test_sync_custom_session.py | 38 +- tests/unit/test_tokenizer.py | 84 +- tests/unit/test_translation_recognizer.py | 4 +- ..._translation_recognizer_explicit_params.py | 2 +- tests/unit/test_utf8_encoding.py | 26 +- .../websocket_mock_server_task_handler.py | 4 +- tests/unit/websocket_task_request.py | 10 +- tests/v2/test_agentstudio_shim.py | 306 +++++++ tests/v2/test_audio_management.py | 2 +- tests/v2/test_chat.py | 16 +- tests/v2/test_config.py | 4 +- tests/v2/test_finetune_rl_shim.py | 446 +++++++++++ tests/v2/test_images_videos.py | 8 +- tests/v2/test_oss_upload.py | 302 +++++++ tests/v2/test_package_root.py | 14 +- tests/v2/test_rl.py | 8 +- 373 files changed, 6684 insertions(+), 3362 deletions(-) create mode 100644 dashscope/_shared/_errors.py create mode 100644 dashscope/_shared/_sdk_ident.py create mode 100644 dashscope/_shared/_validation.py delete mode 100644 dashscope/finetune/reinforcement/component/demo/__init__.py delete mode 100644 dashscope/finetune/reinforcement/component/parser/__init__.py delete mode 100644 dashscope/finetune/reinforcement/component/processor/__init__.py create mode 100644 dashscope/legacy_1x/__init__.py create mode 100644 dashscope/legacy_1x/_shim.py create mode 100644 dashscope/legacy_1x/aigc/__init__.py rename dashscope/{ => legacy_1x}/aigc/chat_completion.py (97%) rename dashscope/{ => legacy_1x}/aigc/code_generation.py (97%) rename dashscope/{ => legacy_1x}/aigc/conversation.py (97%) rename dashscope/{ => legacy_1x}/aigc/generation.py (97%) rename dashscope/{ => legacy_1x}/aigc/image_generation.py (98%) rename dashscope/{ => legacy_1x}/aigc/image_synthesis.py (99%) rename dashscope/{ => legacy_1x}/aigc/multimodal_conversation.py (97%) rename dashscope/{ => legacy_1x}/aigc/video_synthesis.py (99%) rename dashscope/{finetune/reinforcement/common => legacy_1x/api_entities}/__init__.py (100%) rename dashscope/{ => legacy_1x}/api_entities/aio_session.py (98%) rename dashscope/{ => legacy_1x}/api_entities/aiohttp_request.py (96%) rename dashscope/{ => legacy_1x}/api_entities/api_request_data.py (97%) rename dashscope/{ => legacy_1x}/api_entities/api_request_factory.py (92%) rename dashscope/{ => legacy_1x}/api_entities/base_request.py (83%) rename dashscope/{ => legacy_1x}/api_entities/chat_completion_types.py (99%) rename dashscope/{ => legacy_1x}/api_entities/dashscope_response.py (100%) rename dashscope/{ => legacy_1x}/api_entities/encryption.py (95%) rename dashscope/{ => legacy_1x}/api_entities/http_request.py (97%) rename dashscope/{ => legacy_1x}/api_entities/websocket_request.py (97%) create mode 100644 dashscope/legacy_1x/app/__init__.py rename dashscope/{ => legacy_1x}/app/application.py (95%) rename dashscope/{ => legacy_1x}/app/application_response.py (99%) create mode 100644 dashscope/legacy_1x/assistants/__init__.py rename dashscope/{ => legacy_1x}/assistants/assistant_types.py (98%) rename dashscope/{ => legacy_1x}/assistants/assistants.py (98%) rename dashscope/{ => legacy_1x}/assistants/files.py (96%) create mode 100644 dashscope/legacy_1x/audio/__init__.py rename dashscope/{ => legacy_1x}/audio/asr/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/asr/asr_phrase_manager.py (96%) rename dashscope/{ => legacy_1x}/audio/asr/recognition.py (98%) rename dashscope/{ => legacy_1x}/audio/asr/transcription.py (96%) rename dashscope/{ => legacy_1x}/audio/asr/translation_recognizer.py (99%) rename dashscope/{ => legacy_1x}/audio/asr/vocabulary.py (97%) rename dashscope/{ => legacy_1x}/audio/http_tts/__init__.py (74%) rename dashscope/{ => legacy_1x}/audio/http_tts/http_speech_synthesizer.py (95%) rename dashscope/{ => legacy_1x}/audio/qwen_asr/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/qwen_asr/qwen_transcription.py (96%) rename dashscope/{ => legacy_1x}/audio/qwen_omni/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/qwen_omni/omni_realtime.py (99%) rename dashscope/{ => legacy_1x}/audio/qwen_tts/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/qwen_tts/speech_synthesizer.py (92%) rename dashscope/{ => legacy_1x}/audio/qwen_tts_realtime/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/qwen_tts_realtime/qwen_tts_realtime.py (98%) rename dashscope/{ => legacy_1x}/audio/tts/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/tts/speech_synthesizer.py (96%) rename dashscope/{ => legacy_1x}/audio/tts_v2/__init__.py (100%) rename dashscope/{ => legacy_1x}/audio/tts_v2/enrollment.py (97%) rename dashscope/{ => legacy_1x}/audio/tts_v2/speech_synthesizer.py (99%) create mode 100644 dashscope/legacy_1x/cli/__init__.py rename dashscope/{ => legacy_1x}/cli/__main__.py (70%) rename dashscope/{ => legacy_1x}/cli/agentic_rl.py (99%) rename dashscope/{ => legacy_1x}/cli/application.py (96%) rename dashscope/{ => legacy_1x}/cli/auth.py (91%) rename dashscope/{ => legacy_1x}/cli/code_generation.py (93%) rename dashscope/{ => legacy_1x}/cli/common.py (99%) rename dashscope/{ => legacy_1x}/cli/deployments.py (98%) rename dashscope/{ => legacy_1x}/cli/embeddings.py (96%) rename dashscope/{ => legacy_1x}/cli/files.py (97%) rename dashscope/{ => legacy_1x}/cli/fine_tunes.py (99%) rename dashscope/{ => legacy_1x}/cli/generation.py (98%) rename dashscope/{ => legacy_1x}/cli/image_generation.py (96%) rename dashscope/{ => legacy_1x}/cli/image_synthesis.py (99%) rename dashscope/{ => legacy_1x}/cli/models.py (94%) rename dashscope/{ => legacy_1x}/cli/multimodal_conversation.py (98%) rename dashscope/{ => legacy_1x}/cli/multimodal_embedding.py (98%) rename dashscope/{ => legacy_1x}/cli/oss.py (92%) rename dashscope/{ => legacy_1x}/cli/rerank.py (95%) rename dashscope/{ => legacy_1x}/cli/speech_synthesis.py (98%) rename dashscope/{ => legacy_1x}/cli/tokenization.py (94%) rename dashscope/{ => legacy_1x}/cli/transcription.py (99%) rename dashscope/{ => legacy_1x}/cli/understanding.py (95%) rename dashscope/{ => legacy_1x}/cli/video_synthesis.py (98%) rename dashscope/{finetune/reinforcement/component/server => legacy_1x/client}/__init__.py (100%) rename dashscope/{ => legacy_1x}/client/base_api.py (98%) rename dashscope/{finetune/reinforcement/examples/observability => legacy_1x/common}/__init__.py (100%) rename dashscope/{ => legacy_1x}/common/_sdk_session.py (100%) rename dashscope/{ => legacy_1x}/common/api_key.py (95%) rename dashscope/{ => legacy_1x}/common/base_type.py (100%) rename dashscope/{ => legacy_1x}/common/constants.py (100%) rename dashscope/{ => legacy_1x}/common/env.py (62%) rename dashscope/{ => legacy_1x}/common/error.py (92%) rename dashscope/{ => legacy_1x}/common/logging.py (93%) rename dashscope/{ => legacy_1x}/common/message_manager.py (95%) rename dashscope/{ => legacy_1x}/common/utils.py (88%) create mode 100644 dashscope/legacy_1x/embeddings/__init__.py rename dashscope/{ => legacy_1x}/embeddings/batch_text_embedding.py (95%) rename dashscope/{ => legacy_1x}/embeddings/batch_text_embedding_response.py (96%) rename dashscope/{ => legacy_1x}/embeddings/multimodal_embedding.py (96%) rename dashscope/{ => legacy_1x}/embeddings/text_embedding.py (90%) create mode 100644 dashscope/legacy_1x/files.py rename dashscope/{finetune/reinforcement/examples/workspace/functions => legacy_1x/io}/__init__.py (100%) rename dashscope/{ => legacy_1x}/io/input_output.py (98%) create mode 100644 dashscope/legacy_1x/model.py create mode 100644 dashscope/legacy_1x/models.py create mode 100644 dashscope/legacy_1x/multimodal/__init__.py rename dashscope/{ => legacy_1x}/multimodal/dialog_state.py (100%) rename dashscope/{ => legacy_1x}/multimodal/multimodal_constants.py (100%) rename dashscope/{ => legacy_1x}/multimodal/multimodal_dialog.py (98%) rename dashscope/{ => legacy_1x}/multimodal/multimodal_request_params.py (100%) rename dashscope/{ => legacy_1x}/multimodal/tingwu/__init__.py (100%) rename dashscope/{ => legacy_1x}/multimodal/tingwu/tingwu.py (88%) rename dashscope/{ => legacy_1x}/multimodal/tingwu/tingwu_realtime.py (98%) create mode 100644 dashscope/legacy_1x/nlp/__init__.py rename dashscope/{ => legacy_1x}/nlp/understanding.py (89%) create mode 100644 dashscope/legacy_1x/protocol/__init__.py rename dashscope/{ => legacy_1x}/protocol/websocket.py (100%) create mode 100644 dashscope/legacy_1x/rerank/__init__.py rename dashscope/{ => legacy_1x}/rerank/text_rerank.py (94%) create mode 100644 dashscope/legacy_1x/threads/__init__.py create mode 100644 dashscope/legacy_1x/threads/messages/__init__.py rename dashscope/{ => legacy_1x}/threads/messages/files.py (94%) rename dashscope/{ => legacy_1x}/threads/messages/messages.py (96%) create mode 100644 dashscope/legacy_1x/threads/runs/__init__.py rename dashscope/{ => legacy_1x}/threads/runs/runs.py (98%) rename dashscope/{ => legacy_1x}/threads/runs/steps.py (93%) rename dashscope/{ => legacy_1x}/threads/thread_types.py (99%) rename dashscope/{ => legacy_1x}/threads/threads.py (96%) create mode 100644 dashscope/legacy_1x/utils/__init__.py rename dashscope/{ => legacy_1x}/utils/message_utils.py (100%) rename dashscope/{ => legacy_1x}/utils/oss_utils.py (96%) rename dashscope/{ => legacy_1x}/utils/param_utils.py (100%) create mode 100644 dashscope/v2/agentstudio/__init__.py rename dashscope/{ => v2}/agentstudio/client.py (79%) rename dashscope/{ => v2}/agentstudio/constants.py (100%) rename dashscope/{ => v2}/agentstudio/exceptions.py (99%) rename dashscope/{ => v2}/agentstudio/pagination.py (100%) rename dashscope/{ => v2}/agentstudio/resources/__init__.py (72%) rename dashscope/{ => v2}/agentstudio/resources/_helpers.py (99%) rename dashscope/{ => v2}/agentstudio/resources/agents.py (97%) rename dashscope/{ => v2}/agentstudio/resources/deployments.py (98%) rename dashscope/{ => v2}/agentstudio/resources/environments.py (96%) rename dashscope/{ => v2}/agentstudio/resources/files.py (97%) rename dashscope/{ => v2}/agentstudio/resources/memory_stores.py (99%) rename dashscope/{ => v2}/agentstudio/resources/security.py (96%) rename dashscope/{ => v2}/agentstudio/resources/session_events.py (97%) rename dashscope/{ => v2}/agentstudio/resources/session_resources.py (95%) rename dashscope/{ => v2}/agentstudio/resources/session_threads.py (97%) rename dashscope/{ => v2}/agentstudio/resources/sessions.py (95%) rename dashscope/{ => v2}/agentstudio/resources/skills.py (97%) rename dashscope/{ => v2}/agentstudio/resources/vaults.py (98%) rename dashscope/{ => v2}/agentstudio/resources/webhook_endpoints.py (97%) rename dashscope/{ => v2}/agentstudio/streaming.py (95%) rename dashscope/{ => v2}/agentstudio/transport.py (99%) rename dashscope/{ => v2}/agentstudio/types/__init__.py (93%) rename dashscope/{ => v2}/agentstudio/types/models.py (99%) rename dashscope/{ => v2}/agentstudio/types/params.py (99%) create mode 100644 dashscope/v2/finetune/__init__.py create mode 100644 dashscope/v2/finetune/_submit.py create mode 100644 dashscope/v2/finetune/agentic_rl.py create mode 100644 dashscope/v2/finetune/reinforcement/__init__.py create mode 100644 dashscope/v2/finetune/reinforcement/common/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/common/constants.py (90%) rename dashscope/{ => v2}/finetune/reinforcement/common/errors.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/common/log.py (91%) rename dashscope/{ => v2}/finetune/reinforcement/common/model.py (99%) rename dashscope/{ => v2}/finetune/reinforcement/common/model_types.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/common/utils.py (99%) rename dashscope/{ => v2}/finetune/reinforcement/component/__init__.py (74%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/__init__.py (70%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/base_data_model.py (98%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/group_reward_input.py (90%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/group_reward_output.py (90%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/reward_input.py (89%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/reward_output.py (96%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/rollout_input.py (93%) rename dashscope/{ => v2}/finetune/reinforcement/component/data/rollout_output.py (95%) create mode 100644 dashscope/v2/finetune/reinforcement/component/demo/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/component/demo/group_reward_processor_demo.py (85%) rename dashscope/{ => v2}/finetune/reinforcement/component/demo/reward_processor_demo.py (85%) rename dashscope/{ => v2}/finetune/reinforcement/component/demo/rollout_processor_demo.py (85%) rename dashscope/{ => v2}/finetune/reinforcement/component/func_decorator.py (98%) rename dashscope/{ => v2}/finetune/reinforcement/component/func_manager.py (93%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/__init__.py (91%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/__init__.py (87%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/_core.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/llm_dashscope.py (93%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/llm_openai.py (98%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/llm_span.py (95%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/messages.py (98%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/genai/tools.py (99%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/processor_span.py (97%) rename dashscope/{ => v2}/finetune/reinforcement/component/observability/tracing.py (98%) create mode 100644 dashscope/v2/finetune/reinforcement/component/parser/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/component/parser/base_parser.py (90%) rename dashscope/{ => v2}/finetune/reinforcement/component/parser/group_reward_parser.py (81%) rename dashscope/{ => v2}/finetune/reinforcement/component/parser/reward_parser.py (79%) rename dashscope/{ => v2}/finetune/reinforcement/component/parser/rollout_parser.py (79%) create mode 100644 dashscope/v2/finetune/reinforcement/component/processor/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/component/processor/abstract_group_reward_processor.py (90%) rename dashscope/{ => v2}/finetune/reinforcement/component/processor/abstract_processor.py (94%) rename dashscope/{ => v2}/finetune/reinforcement/component/processor/abstract_reward_processor.py (95%) rename dashscope/{ => v2}/finetune/reinforcement/component/processor/abstract_rollout_processor.py (88%) create mode 100644 dashscope/v2/finetune/reinforcement/component/server/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/component/server/server.py (96%) create mode 100644 dashscope/v2/finetune/reinforcement/examples/observability/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/examples/observability/smoke_observability.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/README-zh.md (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/README.md (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/UserGuide-zh.md (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/UserGuide.md (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/cli.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl (100%) create mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/functions/reward/reward.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/opd-job.yaml (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/requirements.txt (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/resources/group_reward_input.json (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/resources/reward_input.json (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/resources/rollout_input.json (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/rl-job.yaml (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/build.sh (98%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh (100%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/submit_job.py (98%) rename dashscope/{ => v2}/finetune/reinforcement/examples/workspace/test_functions.py (100%) create mode 100644 dashscope/v2/resources/_oss_upload.py create mode 100644 dashscope/v2/tokenizers/__init__.py rename dashscope/{resources => v2/tokenizers}/qwen.tiktoken (100%) create mode 100644 dashscope/v2/tokenizers/qwen_tokenizer.py create mode 100644 dashscope/v2/tokenizers/tokenizer.py create mode 100644 dashscope/v2/tokenizers/tokenizer_base.py create mode 100644 tests/unit/test_acli_sdk_contract.py create mode 100644 tests/unit/test_layer_decoupling.py create mode 100644 tests/v2/test_agentstudio_shim.py create mode 100644 tests/v2/test_finetune_rl_shim.py create mode 100644 tests/v2/test_oss_upload.py diff --git a/MANIFEST.in b/MANIFEST.in index 5ddd1f49..cbf2d6b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include dashscope/resources/qwen.tiktoken +include dashscope/v2/tokenizers/qwen.tiktoken include dashscope/py.typed include dashscope/__init__.pyi include NOTICE diff --git a/dashscope/_shared/__init__.py b/dashscope/_shared/__init__.py index 6250b559..2b1e5986 100644 --- a/dashscope/_shared/__init__.py +++ b/dashscope/_shared/__init__.py @@ -2,12 +2,28 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Neutral leaf layer shared by the 1.x API and the 2.0 implementation. -Modules here hold state or values that must exist exactly once per -installed distribution (the process-wide session id, the version -number). Copying them into either stack would silently split that single -source of truth, so both stacks import from here instead. +What belongs here is anything the two stacks must agree on exactly once +per installed distribution: the process-wide session id, the mutable +client tag, the exception base class that ``except`` clauses on both +sides match against, and the validators that keep a request pointed at +``*.maas.aliyuncs.com``. Copying one of those into either stack would +silently split a single source of truth, so both stacks import from +here instead. -Nothing in this package may import ``dashscope`` or any of its -subpackages: it sits below both stacks and is the anchor that keeps them -from forming an import cycle. Stdlib only. +The only thing this package reaches outside itself is +``dashscope.version``, read defensively by ``_sdk_ident``. That module +is a bare constant with no imports of its own, and it deliberately stays +where it is: ``setup.py`` and ``docs/source/conf.py`` ``exec()`` the +file to read the version *without* importing the package, and turning +it into a re-export of something here would drag the whole SDK and its +runtime dependencies into any build environment that only wanted a +string. + +What this package must never import is the 1.x business surface +(``client``, ``common``, ``finetune``, ``audio`` and the rest). +``common/utils`` reaches ``_sdk_ident`` while ``dashscope/__init__`` is +still executing, so an import back into that surface would close a cycle +through a partially initialized root. ``dashscope.version`` is safe +precisely because it has no imports of its own; the business modules +all have plenty. """ diff --git a/dashscope/_shared/_errors.py b/dashscope/_shared/_errors.py new file mode 100644 index 00000000..d291c803 --- /dev/null +++ b/dashscope/_shared/_errors.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The one exception base class both stacks inherit from. + +``except dashscope.legacy_1x.common.error.DashScopeException`` has to catch errors +raised by the 1.x API classes *and* by the AgentStudio sub-SDK, which the +2.0 client mounts directly. That only works if every subclass in both +stacks derives from the same class object, so the base lives here rather +than in either stack: copying it would silently split the hierarchy and +let errors escape a caller's ``except`` clause. + +``dashscope.legacy_1x.common.error`` re-exports this name, so the 1.x import path +keeps working and keeps the identical class. +""" + + +class DashScopeException(Exception): + """Base class for every error the dashscope SDK raises.""" + + +__all__ = ["DashScopeException"] diff --git a/dashscope/_shared/_sdk_ident.py b/dashscope/_shared/_sdk_ident.py new file mode 100644 index 00000000..45cf03b9 --- /dev/null +++ b/dashscope/_shared/_sdk_ident.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""SDK identification headers shared by the 1.x API and the 2.0 package. + +``x-dashscope-sdk-client`` tells the backend which client and which +functional area issued a request, and ``x-dashscope-sdk-session-id`` +groups every request one interpreter process makes. + +The client segment is process-wide *mutable* state: entry points that +embed the SDK overwrite it (the ``dashscope`` CLI marks itself as +``python-cli`` via ``set_sdk_client``), and every caller in both stacks +must see the same value. Duplicating it would silently drop the override +for whichever stack held the copy, so it lives here -- one source of +truth, same reason as the session id in ``_shared._sdk_session``. + +The 2.0 transport builds its own header in +``dashscope.v2.transport._ident`` because it appends a trailing +generation marker (``aigc`` -> ``aigc2``) that this unmarked 1.x shape +must not carry. Sub-SDKs that predate the marker -- AgentStudio, whose +``agentstudio`` tag the backend already reports on -- keep using this +module. + +``get_user_agent`` belongs here for the same reason as the two headers: +it is the other half of the request identity, and it must agree with +``get_sdk_headers`` on the version segment. It is the *1.x shape* +(``dashscope/; python/...; platform/...; processor/...``) and is +deliberately not shared with the 2.0 transport's own +``dashscope.v2.transport.http.default_user_agent()`` +(``dashscope-v2-python/``). Those are two distinct backend +contracts, not two spellings of one value, so they must not be merged. + +Only the stdlib is imported at module level; ``dashscope.version`` is +pulled in lazily and defensively so this leaf stays below both stacks. +""" + +import os +import platform +from typing import Dict + +from dashscope._shared._sdk_session import sdk_session_id + +_SDK_CLIENT = "python-sdk" + +# Shared with the 2.0 transport so one process reports one +# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session; +# acli deliberately keeps its own, it is a one-way sync copy). +_SDK_SESSION_ID = sdk_session_id() + +__all__ = ["get_sdk_headers", "get_user_agent", "set_sdk_client"] + + +def _sdk_version() -> str: + try: + from dashscope.version import __version__ + except Exception: # pragma: no cover - defensive import guard + return "0.0.0" + return __version__ + + +# Read once at import so the value is stable for the process lifetime. +_VERSION = _sdk_version() + + +def set_sdk_client(client: str) -> None: + """Override the client identifier in the x-dashscope-sdk-client header. + + Process-wide, used by entry points that embed the SDK (e.g. the + ``dashscope`` CLI marks itself as ``python-cli``). + """ + global _SDK_CLIENT # pylint: disable=global-statement + if client: + _SDK_CLIENT = client + + +def get_sdk_headers(module: str = "") -> Dict[str, str]: + """Structured SDK identification headers for backend statistics. + + Set DASHSCOPE_DISABLE_SDK_HEADERS=1 to omit them. + + Args: + module: Optional module name (e.g. agentstudio, rl, app). + """ + if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): + return {} + parts = [_SDK_CLIENT, _VERSION] + if module: + parts.append(module) + return { + "x-dashscope-sdk-client": "/".join(parts), + "x-dashscope-sdk-session-id": _SDK_SESSION_ID, + } + + +def get_user_agent() -> str: + """The 1.x-shaped ``user-agent`` value. + + Kept byte-identical to the pre-move implementation: the version + segment is ``_VERSION`` here rather than a second read of + ``dashscope.version``, so it can never drift from the one in + ``get_sdk_headers``. + """ + try: + platform_ = ( + platform.platform().replace("\n", "").replace("\r", "").strip() + ) + except Exception: + platform_ = "unknown" + + try: + processor_ = ( + platform.processor().replace("\n", "").replace("\r", "").strip() + ) + except Exception: + processor_ = "unknown" + + ua = ( + f"dashscope/{_VERSION}; python/{platform.python_version()}; " + f"platform/{platform_}; " + f"processor/{processor_}" + ) + return ua diff --git a/dashscope/_shared/_sdk_session.py b/dashscope/_shared/_sdk_session.py index c5fdea8e..044c9455 100644 --- a/dashscope/_shared/_sdk_session.py +++ b/dashscope/_shared/_sdk_session.py @@ -5,18 +5,18 @@ The ``x-dashscope-sdk-session-id`` header lets server-side access logs group every request one SDK session makes. A process can emit requests through both stacks at once -- the 1.x API classes and the 2.0 transport --- and the 2.0 client even delegates some calls (rl, agentstudio) -straight into 1.x. If each stack minted its own id, one logical session -would split across several ids and the grouping breaks. +-- and the sub-SDKs owned by 2.0 (AgentStudio, agentic RL) build their +request headers from ``_shared._sdk_ident`` rather than from the v2 +transport. If each stack minted its own id, one logical session would +split across several ids and the grouping breaks. This leaf module is the one source of truth both import. It depends only on the stdlib so it can be pulled in from anywhere without dragging ``dashscope/__init__`` (and its v2 surface) into a cycle. The embedded ``acli`` deliberately keeps its own id: it is a one-way -sync copy of the agenticCLI repo (design doc §3.2), so wiring it to this -module would be silently reverted by the next sync and cannot be -upstreamed as written. +sync copy of the agenticCLI repo, so wiring it to this module would be +silently reverted by the next sync and cannot be upstreamed as written. """ from __future__ import annotations diff --git a/dashscope/_shared/_validation.py b/dashscope/_shared/_validation.py new file mode 100644 index 00000000..0190e5fa --- /dev/null +++ b/dashscope/_shared/_validation.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Host-label validation shared by the 1.x API and the 2.0 implementation. + +``workspace_id`` and ``region`` are interpolated into the host of the MaaS +endpoint, so both stacks must reject the same malformed values with the +same rule. Keeping the check in one leaf module means a value that is +safe for one stack is safe for the other, and neither stack has to import +the other to get it. + +Stdlib only, like the rest of ``dashscope._shared``. +""" + +import re + +# workspace_id and region become DNS labels of the MaaS endpoint host, so +# they must not contain characters (/, ?, #, @, :, whitespace, ...) that +# could break out of the host and redirect requests elsewhere. +# \A...\Z rather than ^...$: $ also matches before a trailing newline. +_WORKSPACE_ID_PATTERN = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\Z") +# RFC 1123 hostname label: 1-63 chars, no leading/trailing hyphen. Upper +# case is allowed because DNS is case-insensitive and httpx lowercases the +# host, so rejecting it would break working callers for no security gain. +_REGION_PATTERN = re.compile( + r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z", +) + +__all__ = ["validate_region", "validate_workspace_id"] + + +def validate_workspace_id(workspace_id): + """Validate that workspace_id is safe to use as a URL subdomain. + + Raises: + ValueError: If workspace_id contains characters outside + [A-Za-z0-9_-], is empty, or exceeds 64 characters. + """ + if not workspace_id or not _WORKSPACE_ID_PATTERN.fullmatch( + workspace_id, + ): + raise ValueError( + f"Invalid workspace_id {workspace_id!r}: must match " + "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$", + ) + return workspace_id + + +def validate_region(region): + """Validate that region is safe to use as a URL subdomain. + + Deliberately a hostname-label check and not a MAAS_REGIONS whitelist: + rejecting an unknown-but-well-formed region would break callers as soon + as a subpackage supports a region this list has not caught up with. + + Raises: + ValueError: If region is not a valid hostname label. + """ + if not region or not _REGION_PATTERN.fullmatch(region): + raise ValueError( + f"Invalid region {region!r}: must be a hostname label " + "(1-63 chars of [A-Za-z0-9-], not starting or ending " + "with '-')", + ) + return region diff --git a/dashscope/acli/ui/tts.py b/dashscope/acli/ui/tts.py index a02e1f3d..d5eef739 100644 --- a/dashscope/acli/ui/tts.py +++ b/dashscope/acli/ui/tts.py @@ -3,7 +3,7 @@ Supports both streaming playback (low-latency, audio starts before synthesis finishes) and one-shot synthesis. Uses -``dashscope.audio.tts_v2.SpeechSynthesizer``. +``dashscope.legacy_1x.audio.tts_v2.SpeechSynthesizer``. """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements @@ -284,7 +284,7 @@ def _synthesize_and_play(self, text: str) -> Optional[str]: dashscope.api_key = self.api_key try: - from dashscope.audio.tts_v2 import ( + from dashscope.legacy_1x.audio.tts_v2 import ( AudioFormat, ResultCallback, SpeechSynthesizer, diff --git a/dashscope/acli/ui/voice.py b/dashscope/acli/ui/voice.py index 35af0d03..8ba069a7 100644 --- a/dashscope/acli/ui/voice.py +++ b/dashscope/acli/ui/voice.py @@ -58,7 +58,10 @@ class _RealtimeASR: def __init__(self, api_key: str, model: str): os.environ.setdefault("DASHSCOPE_API_KEY", api_key) - from dashscope.audio.asr import Recognition, RecognitionCallback + from dashscope.legacy_1x.audio.asr import ( + Recognition, + RecognitionCallback, + ) self._model = model self._Recognition = Recognition diff --git a/dashscope/agentstudio/__init__.py b/dashscope/agentstudio/__init__.py index edd58186..5152e1fc 100644 --- a/dashscope/agentstudio/__init__.py +++ b/dashscope/agentstudio/__init__.py @@ -1,164 +1,58 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""AgentStudio sub-SDK for the dashscope Python client. +"""Compatibility shim; the implementation moved to ``dashscope.v2.agentstudio``. -The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP -API. All resource classes are accessed through a client instance:: +The AgentStudio sub-SDK is owned by the 2.0 package. This module keeps the +historical import path working: every public name is re-exported, and every +submodule is aliased in ``sys.modules`` so deep imports such as +``from dashscope.agentstudio.resources.agents import Agents`` resolve to the +very same module object -- and therefore the very same classes -- as the +``dashscope.v2.agentstudio`` path. - from dashscope.agentstudio import Client - from dashscope.agentstudio.types import ( - user_message, user_tool_result, - ) - -Example -------- ->>> from dashscope.agentstudio import Client ->>> from dashscope.agentstudio.types import user_message ->>> client = Client(api_key="sk-xxx") ->>> agent = client.agents.create(name="demo", model="qwen-plus") ->>> session = client.sessions.create(agent=agent.id) ->>> client.sessions.events.send(session.id, [user_message("Hello!")]) ->>> with client.sessions.events.stream(session.id) as stream: -... for event in stream: -... print(event.type, event.to_dict()) -... if event.type == "session_status": -... break +Import from ``dashscope.v2.agentstudio`` in new code. """ -# yapf: disable +import pkgutil +import sys -from .client import Client, AsyncClient -from .transport import APIResponse, RequestOptions -from . import types -from .exceptions import ( - AgentStudioError, - APIConnectionError, - APIStatusError, - APITimeoutError, - AuthenticationError, - ConflictError, - InternalServerError, - InvalidRequestError, - NotFoundError, - OverloadedError, - PermissionDeniedError, - RateLimitError, - StreamClosedError, - StreamError, -) -from .pagination import ( - AsyncCursorPage, - CursorPage, -) -from .types import ( - Memory, - MemoryStore, - MemoryVersion, - Message, - PermissionPolicy, - SecurityAlert, - SecurityAlertList, - SecurityOverview, - ServerEvent, - SessionResource, - StopReason, - user_define_outcome, - user_interrupt, - user_message, - user_tool_approval_response, - user_custom_tool_result, - user_tool_result, -) -from .types.params import ( - AgentCreateParams, - AgentUpdateParams, - SessionCreateParams, - SessionUpdateParams, - EnvironmentCreateParams, - EnvironmentUpdateParams, - DeploymentCreateParams, - DeploymentUpdateParams, - SessionEventSendParams, - WebhookEndpointCreateParams, - WebhookEndpointUpdateParams, - WebhookEventListParams, -) -from .constants import ( - PermissionPolicyType, - SSEEventType, - SessionStatus, - StopReasonType, - WebhookDeliveryStatus, - WebhookDisabledReason, - WebhookEventType, - WebhookStatus, +# Importing the package runs its own eager imports, so every submodule is +# already in sys.modules by the time the walk below starts. +from dashscope.v2 import agentstudio as _impl + +# pylint: disable-next=wildcard-import,unused-wildcard-import +from dashscope.v2.agentstudio import * # noqa: F401,F403 + +# The wildcard only carries the names in the implementation's __all__, which +# lists classes and helpers but not its submodules. Naming them here keeps +# ``from dashscope.agentstudio import exceptions`` resolvable statically; at +# runtime they are already imported by the package's own eager imports. +# pylint: disable-next=unused-import +from dashscope.v2.agentstudio import ( # noqa: F401 + client, + constants, + exceptions, + pagination, + resources, + streaming, + transport, + types, ) -__all__ = [ - # client - "Client", - "AsyncClient", - # base types - "RequestOptions", - "APIResponse", - # exceptions - "AgentStudioError", - "APIConnectionError", - "APIStatusError", - "APITimeoutError", - "AuthenticationError", - "ConflictError", - "InternalServerError", - "InvalidRequestError", - "NotFoundError", - "OverloadedError", - "PermissionDeniedError", - "RateLimitError", - "StreamError", - "StreamClosedError", - # pagination - "CursorPage", - "AsyncCursorPage", - # unified message type - "Memory", - "MemoryStore", - "MemoryVersion", - "Message", - "PermissionPolicy", - "SecurityAlert", - "SecurityAlertList", - "SecurityOverview", - "ServerEvent", - "SessionResource", - "StopReason", - # client event helpers (re-exported for convenience) - "user_message", - "user_interrupt", - "user_tool_approval_response", - "user_custom_tool_result", - "user_tool_result", - "user_define_outcome", - # typing module - "types", - # params (commonly used request-body types) - "AgentCreateParams", - "AgentUpdateParams", - "SessionCreateParams", - "SessionUpdateParams", - "EnvironmentCreateParams", - "EnvironmentUpdateParams", - "DeploymentCreateParams", - "DeploymentUpdateParams", - "SessionEventSendParams", - "WebhookEndpointCreateParams", - "WebhookEndpointUpdateParams", - "WebhookEventListParams", - "WebhookStatus", - "WebhookDisabledReason", - "WebhookDeliveryStatus", - "WebhookEventType", - "SSEEventType", - "SessionStatus", - "StopReasonType", - "PermissionPolicyType", -] +_V2_PACKAGE = _impl.__name__ + +__all__ = list(_impl.__all__) + +# Alias each submodule under the old dotted path. Registering in +# sys.modules alone is not enough: the import system skips a module it +# finds there, so the attribute on the parent would never be set and +# ``dashscope.agentstudio.resources`` would raise AttributeError. +for _info in pkgutil.walk_packages(_impl.__path__, prefix=_V2_PACKAGE + "."): + _module = sys.modules.get(_info.name) + if _module is None: # pragma: no cover - walk_packages imports as it goes + continue + _alias = __name__ + _info.name[len(_V2_PACKAGE) :] + sys.modules.setdefault(_alias, _module) + _parent_name, _, _leaf = _alias.rpartition(".") + _parent = sys.modules.get(_parent_name) + if _parent is not None: + setattr(_parent, _leaf, _module) diff --git a/dashscope/aigc/__init__.py b/dashscope/aigc/__init__.py index 6f1e32c8..eba54d24 100644 --- a/dashscope/aigc/__init__.py +++ b/dashscope/aigc/__init__.py @@ -1,24 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from .conversation import Conversation, History, HistoryItem -from .generation import Generation, AioGeneration -from .image_synthesis import ImageSynthesis, AioImageSynthesis -from .multimodal_conversation import ( - MultiModalConversation, - AioMultiModalConversation, -) -from .video_synthesis import VideoSynthesis, AioVideoSynthesis +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.aigc``. -__all__ = [ - "Generation", - "AioGeneration", - "Conversation", - "HistoryItem", - "History", - "ImageSynthesis", - "AioImageSynthesis", - "MultiModalConversation", - "AioMultiModalConversation", - "VideoSynthesis", - "AioVideoSynthesis", -] +Import from ``dashscope.legacy_1x.aigc`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.aigc") diff --git a/dashscope/app/__init__.py b/dashscope/app/__init__.py index e0f1ba76..5b018c18 100644 --- a/dashscope/app/__init__.py +++ b/dashscope/app/__init__.py @@ -1,6 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.app``. -from .application import Application +Import from ``dashscope.legacy_1x.app`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = ["Application"] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.app") diff --git a/dashscope/assistants/__init__.py b/dashscope/assistants/__init__.py index 2cc9f714..292369d2 100644 --- a/dashscope/assistants/__init__.py +++ b/dashscope/assistants/__init__.py @@ -1,31 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.assistants``. -import warnings +Import from ``dashscope.legacy_1x.assistants`` (or the public ``dashscope`` +root re-exports) in new code. +""" -# yapf: disable +from dashscope.legacy_1x._shim import install as _install -from dashscope.assistants.assistant_types import ( - Assistant, AssistantFile, - AssistantList, - DeleteResponse, -) -from dashscope.assistants.assistants import Assistants - -__all__ = [ - 'Assistant', - 'Assistants', - 'AssistantList', - 'AssistantFile', - 'DeleteResponse', -] - -# Deprecation warning -_DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " - "removed in a future release. Please migrate to the Responses API. " - "See https://help.aliyun.com/zh/model-studio/" - "synchronous-call-api-reference for migration details." -) - -warnings.warn(_DEPRECATION_MSG, category=DeprecationWarning, stacklevel=2) +_install(__name__, "dashscope.legacy_1x.assistants") diff --git a/dashscope/audio/__init__.py b/dashscope/audio/__init__.py index bdf170a7..f4bd5f18 100644 --- a/dashscope/audio/__init__.py +++ b/dashscope/audio/__init__.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.audio``. -from . import asr, tts, tts_v2, qwen_tts, qwen_tts_realtime, qwen_omni +Import from ``dashscope.legacy_1x.audio`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = [ # type: ignore[misc] - asr, - tts, - tts_v2, - qwen_tts, - qwen_tts_realtime, - qwen_omni, -] # noqa: E501 +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.audio") diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index 244d227b..cd5ca981 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -1,591 +1,11 @@ # -*- coding: utf-8 -*- -# pylint: disable=wrong-import-position -"""DashScope command-line entry point. +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.cli``. -This package is intentionally thin — all command-specific logic lives in -sub-modules (generation, fine_tunes, files, etc.). +Import from ``dashscope.legacy_1x.cli`` (or the public ``dashscope`` +root re-exports) in new code. """ -import sys -import warnings -from pathlib import Path -from typing import Optional -# Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL -warnings.filterwarnings( - "ignore", - message=".*urllib3.*only supports OpenSSL.*", - category=Warning, -) +from dashscope.legacy_1x._shim import install as _install -import typer # noqa: E402 -from rich.markup import escape # noqa: E402 - -import dashscope # noqa: E402 -from dashscope.cli.common import console, err_console # noqa: E402 -from dashscope.common.error import AuthenticationError # noqa: E402 -from dashscope.common.utils import set_sdk_client # noqa: E402 -from dashscope.cli import ( # noqa: E402 - application, - auth, - deployments, - embeddings, - files, - fine_tunes, - generation, - image_generation, - image_synthesis, - models, - multimodal_conversation, - multimodal_embedding, - oss, - rerank, - speech_synthesis, - tokenization, - transcription, - video_synthesis, -) - -set_sdk_client("python-cli") - - -# --------------------------------------------------------------------------- -# Legacy command compatibility layer -# --------------------------------------------------------------------------- - -# Command name mapping: old -> new -_COMMAND_MAP = { - "fine_tunes.call": "fine-tunes create", - "fine_tunes.get": "fine-tunes get", - "fine_tunes.list": "fine-tunes list", - "fine_tunes.stream": "fine-tunes stream", - "fine_tunes.cancel": "fine-tunes cancel", - "fine_tunes.delete": "fine-tunes delete", - "generation.call": "generation create", - "files.upload": "files upload", - "files.get": "files get", - "files.list": "files list", - "files.delete": "files delete", - "deployments.call": "deployments create", - "deployments.get": "deployments get", - "deployments.list": "deployments list", - "deployments.scale": "deployments scale", - "deployments.delete": "deployments delete", - "oss.upload": "oss upload", -} - -# Parameter name mapping: old -> new (underscore to dash) -_PARAM_MAP = { - "--training_file_ids": "--training-file-ids", - "--validation_file_ids": "--validation-file-ids", - "--n_epochs": "--n-epochs", - "--batch_size": "--batch-size", - "--learning_rate": "--learning-rate", - "--prompt_loss": "--prompt-loss", - "--hyper_parameters": "--hyper-parameters", - "--file_id": "--file-id", - "--deployed_model": "--deployed-model", - "--base_url": "--base-url", - "--api_key": "--api-key", - "--start_page": "--start-page", - "--page_size": "--page-size", -} - -_TOP_LEVEL_COMMANDS = { - "auth", - "generation", - "ft", - "fine-tunes", - "files", - "deployments", - "oss", - "rerank", - "embeddings", - "tokenization", - "models", - "application", - "image-synthesis", - "video-synthesis", - "image-generation", - "multimodal-conversation", - "multimodal-embedding", - "transcription", - "speech-synthesis", - "rl", - "agentic-rl", -} - - -_COMMANDS_WITH_LOCAL_API_KEY = {"oss", "rl", "agentic-rl"} - -# Routing keyword for the bundled agent, handled in main() before typer sees -# argv. Deliberately NOT a member of _TOP_LEVEL_COMMANDS: that set mirrors the -# typer-registered command groups, and `expert` is not one of them. -_EXPERT_COMMAND = "expert" - -_EXPERT_USAGE = """[bold]DashScope SDK Expert[/bold] — the built-in AI assistant. - -[bold]Usage:[/bold] - dashscope interactive assistant (TUI by default) - dashscope expert the same, spelled explicitly - dashscope expert --cli interactive, plain REPL instead of the TUI - dashscope expert --tui interactive, force the TUI - dashscope expert "question" ask once and exit - -With no argument, piped stdin is read as a one-shot prompt. -SDK subcommands (generation, ft, files, models, ...) dispatch as usual; an -unrecognized command is reported as an error rather than treated as a -question, so ask one with [bold]dashscope expert "question"[/bold]. - -Requires [bold]pip install dashscope[acli][/bold].""" - -_LEGACY_COMMANDS_WITH_SIZE_OPTION = { - "files.list", - "fine_tunes.list", - "deployments.list", -} - - -def _translate_param_name(arg): - """Translate legacy underscore option names to Typer hyphen names.""" - if arg in _PARAM_MAP: - return _PARAM_MAP[arg] - if arg.startswith("--") and "=" in arg: - option_name, option_value = arg.split("=", 1) - if option_name in _PARAM_MAP: - return f"{_PARAM_MAP[option_name]}={option_value}" - return arg - - -def _translate_legacy_args(argv): - """Translate legacy argparse command format to Typer format. - - Legacy format: dashscope fine_tunes.call --training_file_ids ... - New format: dashscope fine-tunes create --training-file-ids ... - - Returns modified argv list. - """ - if len(argv) < 2: - return argv - - is_legacy_command = argv[1] in _COMMAND_MAP - if not is_legacy_command: - return [argv[0]] + [_translate_param_name(arg) for arg in argv[1:]] - - command_args = _COMMAND_MAP[argv[1]].split() - top_level_args = [] - translated_args = [] - i = 2 - while i < len(argv): - arg = argv[i] - translated_arg = _translate_param_name(arg) - if argv[1] in _LEGACY_COMMANDS_WITH_SIZE_OPTION: - if translated_arg == "--page-size": - translated_arg = "--size" - elif translated_arg.startswith("--page-size="): - translated_arg = translated_arg.replace( - "--page-size=", - "--size=", - 1, - ) - - if translated_arg == "--api-key": - top_level_args.append("--api-key") - if i + 1 < len(argv): - top_level_args.append(argv[i + 1]) - i += 2 - else: - i += 1 - continue - - if translated_arg.startswith("--api-key="): - top_level_args.append(translated_arg) - i += 1 - continue - - translated_args.append(translated_arg) - i += 1 - - return [argv[0]] + top_level_args + command_args + translated_args - - -def _exit_missing_api_key_value(option_name): - err_console.print( - f"[red]Error:[/red] Option '{option_name}' requires an argument.", - ) - sys.exit(2) - - -def _extract_global_api_key(argv): - """Extract global -k/--api-key from argv and set dashscope.api_key. - - For backward compatibility, global api-key can appear before or after most - command names. Commands that define their own local api-key option are left - untouched once their command name has been reached. - """ - new_argv = [] - i = 0 - current_command = None - while i < len(argv): - arg = argv[i] - - if i > 0 and current_command is None and arg in _TOP_LEVEL_COMMANDS: - current_command = arg - - should_parse_global_api_key = ( - current_command not in _COMMANDS_WITH_LOCAL_API_KEY - ) - - if arg in ("-k", "--api-key"): - if i + 1 >= len(argv): - _exit_missing_api_key_value(arg) - next_arg = argv[i + 1] - if not next_arg or next_arg.startswith("-"): - _exit_missing_api_key_value(arg) - if should_parse_global_api_key: - if next_arg in _TOP_LEVEL_COMMANDS: - _exit_missing_api_key_value(arg) - dashscope.api_key = next_arg - i += 2 - continue - - if arg.startswith("--api-key="): - api_key = arg.split("=", 1)[1] - if not api_key: - _exit_missing_api_key_value("--api-key") - if should_parse_global_api_key: - dashscope.api_key = api_key - i += 1 - continue - - new_argv.append(arg) - i += 1 - - return new_argv - - -def _translate_help_shortcut(argv): - """Translate -h to --help for convenience.""" - return [arg if arg != "-h" else "--help" for arg in argv] - - -_MD_MARKER = "" -_PY_MARKER = "# managed-by: dashscope-expert" -_EXPERT_EXAMPLE = "dashscope-sdk-expert" - - -def _cleanup_legacy_expert_sync(): - """Remove files the deleted expert bundling synced into ~/.acli. - - Only files carrying the managed marker are deleted — without it the file - is user-owned and left untouched. Needed so stale global copies don't - shadow the workspace ./.acli/ configs installed via example download. - """ - try: - from dashscope.acli.config import CONFIG_DIR - except ImportError: - return - md_targets = [] - for subdir in ("skills", "references"): - target_dir = CONFIG_DIR / subdir - if target_dir.is_dir(): - md_targets.extend(target_dir.glob("*.md")) - for path in md_targets: - try: - if _MD_MARKER in path.read_text(encoding="utf-8"): - path.unlink() - except OSError: - pass - build = CONFIG_DIR / "build_sdk_index.py" - try: - if build.is_file() and _PY_MARKER in build.read_text(encoding="utf-8"): - build.unlink() - except OSError: - pass - - -def _maybe_offer_example_download(): - """First interactive run without ./.acli: offer the SDK-expert example. - - Accepting merges the example into ./.acli/ via the same code path as - `example download` (copied from the packaged install dir when present); - declining writes a marker into ./.acli/ so we never ask again here. - """ - if not sys.stdin.isatty() or (Path.cwd() / ".acli").is_dir(): - return - try: - from dashscope.acli.cli.examples import _handle_example_command - from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale - except ImportError: - return - err_console.print( - "[cyan]First run:[/cyan] download the dashscope-sdk-expert " - "example config into ./.acli/\n" - "(SDK Q&A expert persona + skill templates + SDK knowledge " - "index; pure config, editable anytime).", - ) - err_console.print( - f"[dim]Guide: {_GUIDE_DOC.format(_doc_locale())}[/dim]", - ) - try: - answer = input("Download the example config? [Y/n] ").strip().lower() - except (EOFError, KeyboardInterrupt): - err_console.print() - return - if answer in ("n", "no"): - try: - marker = Path.cwd() / ".acli" / ".dashscope-example-declined" - marker.parent.mkdir(parents=True, exist_ok=True) - marker.write_text("declined\n", encoding="utf-8") - except OSError: - pass - err_console.print( - "[dim]Skipped. To customize later, run: " - "python -m dashscope.acli example download " - "dashscope-sdk-expert[/dim]", - ) - return - _handle_example_command(["download", _EXPERT_EXAMPLE]) - - -def _route_to_expert(command, tui=None): - """Run the vendored acli agent (bare ``dashscope`` or ``dashscope expert``). - - ``command`` is a one-shot prompt, or None for the interactive REPL. - """ - try: - from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale - from dashscope.acli.ui.embedded import run - except ImportError as exception: - err_console.print( - "[yellow]Tip:[/yellow] Install dashscope[acli] to enable " - "the agent " - f"({exception}).", - ) - return - - if command is None and not sys.stdin.isatty(): - # The interactive REPL needs a real terminal; prompt_toolkit crashes - # on non-tty stdin (e.g. kqueue rejects /dev/null with EINVAL). - # Treat piped text as a one-shot prompt instead. - try: - piped = sys.stdin.read().strip() - except OSError: - piped = "" - if not piped: - err_console.print( - "[red]Error:[/red] Interactive mode requires a terminal. " - 'Pass a prompt instead, e.g. dashscope expert "your question".', - ) - sys.exit(2) - command = piped - - _cleanup_legacy_expert_sync() - if command is None: - _maybe_offer_example_download() - - try: - run( - app_name="DashScope SDK Expert", - prompt_symbol="dashscope> ", - api_key=dashscope.api_key or None, - command=command, - tui=tui, - module="expert", - guide_url=_GUIDE_DOC.format(_doc_locale()), - ) - except SystemExit: - pass - except Exception as exception: # pylint: disable=broad-except - err_console.print( - f"[yellow]Tip:[/yellow] Agent failed to start ({exception}).", - ) - - -# --------------------------------------------------------------------------- -# Typer app -# --------------------------------------------------------------------------- - -app = typer.Typer( - name="dashscope", - help="DashScope command line tools.", - add_completion=True, - no_args_is_help=True, - rich_markup_mode="rich", -) - - -@app.callback() -def callback( - api_key: Optional[str] = typer.Option( - None, - "-k", - "--api-key", - help="DashScope API Key.", - ), -): - """Configure global CLI options.""" - if api_key: - dashscope.api_key = api_key - - -# Register sub-command groups -app.add_typer(auth.app) -app.add_typer(generation.app) -app.add_typer(fine_tunes.app, name="ft") -app.add_typer(fine_tunes.app, name="fine-tunes", hidden=True) -app.add_typer(files.app) -app.add_typer(deployments.app) -app.add_typer(oss.app) -app.add_typer(rerank.app) -app.add_typer(embeddings.app) -app.add_typer(tokenization.app) -app.add_typer(models.app) -app.add_typer(application.app) -app.add_typer(image_synthesis.app) -app.add_typer(video_synthesis.app) -app.add_typer(image_generation.app) -app.add_typer(multimodal_conversation.app) -app.add_typer(multimodal_embedding.app) -app.add_typer(transcription.app) -app.add_typer(speech_synthesis.app) - - -# Records why the Agentic-RL Typer app could not be registered, so the reason -# can be reported when `rl` is actually invoked instead of on every command. -_RL_IMPORT_ERROR: Optional[Exception] = None - -_RL_COMMAND_NAMES = ("rl", "agentic-rl") - - -def _register_rl_app(): - """Lazily import and register the Agentic-RL Typer app. - - Wrapped in a function so that a missing optional dependency - won't crash the entire CLI at import time. - - The failure is recorded rather than reported: `rl` is an optional - extra, so a user running any other command has no reason to be told - about it. ``main()`` surfaces it when the rl command is invoked. - """ - global _RL_IMPORT_ERROR # pylint: disable=global-statement - - try: - from dashscope.cli.agentic_rl import app as rl_app - - app.add_typer( - rl_app, - name="rl", - help="🚀 Agentic RL fine-tuning commands", - ) - app.add_typer( - rl_app, - name="agentic-rl", - help="🚀 Agentic RL fine-tuning commands", - hidden=True, - ) - except Exception as exception: # pylint: disable=broad-except - _RL_IMPORT_ERROR = exception - - -def _rl_unavailable_detail(exception: Exception) -> str: - """Render a deferred rl import failure as an actionable message.""" - # Unescaped, rich reads a bracketed run in the exception text as a style - # tag and drops it from the printed output. - detail = escape(str(exception)) - if "pip install" not in detail: - detail += ( - ". Reinstall to pull in the missing dependencies: " - "[bold]pip install -U dashscope[/bold]" - ) - return detail - - -_register_rl_app() - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - - -def _first_non_option(argv): - """Return the first non-option argument in *argv* (skip index 0), - or None if all arguments are options.""" - for arg in argv[1:]: - if not arg.startswith("-"): - return arg - return None - - -def main(): - """Entry point for the ``dashscope`` console script.""" - # Translate legacy command format first so legacy --api_key can be treated - # as the global --api-key option. - argv = _translate_legacy_args(sys.argv) - - # Extract global api-key parameter after legacy argument normalization. - argv = _extract_global_api_key(argv) - - # Extract agent mode flags (--cli / --tui); they belong to the vendored - # acli and must not leak into typer or the one-shot prompt text. - # Bare `dashscope` defaults to the TUI; --cli opts into the plain CLI REPL. - forced_tui = True - if "--cli" in argv: - forced_tui = False - argv = [a for a in argv if a != "--cli"] - if "--tui" in argv: - forced_tui = True - argv = [a for a in argv if a != "--tui"] - - first_cmd = _first_non_option(argv) - - # `rl` is an optional extra: report a missing dependency only when the - # user asks for that command, never on unrelated invocations. Without - # this gate the dispatch below reaches typer, which only knows that `rl` - # is in _TOP_LEVEL_COMMANDS but was never registered — "No such command" - # does not tell the user how to get it. - if _RL_IMPORT_ERROR is not None and first_cmd in _RL_COMMAND_NAMES: - err_console.print( - f"[red]Error:[/red] {_rl_unavailable_detail(_RL_IMPORT_ERROR)}", - ) - sys.exit(1) - - # No arguments at all → the agent, interactively. -k/--api-key and - # --cli/--tui were stripped above, so `dashscope -k sk-x` lands here too. - if len(argv) == 1: - _route_to_expert(None, tui=forced_tui) - return - - # `dashscope expert ...` → the agent, explicitly. Handled before the - # --help gate so `dashscope expert --help` describes the agent instead of - # reaching typer, which has no such command. - if first_cmd == _EXPERT_COMMAND: - idx = argv.index(_EXPERT_COMMAND) - expert_args = argv[idx + 1 :] - if "--help" in expert_args or "-h" in expert_args: - console.print(_EXPERT_USAGE) - return - prompt = " ".join(expert_args).strip() - _route_to_expert(prompt or None, tui=forced_tui) - return - - # Top-level --help / -h → show help and exit normally - if "--help" in argv or "-h" in argv: - argv = _translate_help_shortcut(argv) - sys.argv = argv - app() - return - - # Anything that is not bare `dashscope` and not `dashscope expert ...` - # belongs to typer, so a typo or a stray option gets a real error instead - # of silently opening a chat. Forwarding an unrecognized token as a - # question used to swallow whole command lines -- `dashscope auth whoami` - # opened a session whose first message was the command itself. - argv = _translate_help_shortcut(argv) - sys.argv = argv - - try: - app() - except AuthenticationError as exception: - err_console.print(f"[red]Error:[/red] {exception}") - sys.exit(1) +_install(__name__, "dashscope.legacy_1x.cli") diff --git a/dashscope/common/__init__.py b/dashscope/common/__init__.py index e69de29b..105fba9c 100644 --- a/dashscope/common/__init__.py +++ b/dashscope/common/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.common``. + +Import from ``dashscope.legacy_1x.common`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.common") diff --git a/dashscope/embeddings/__init__.py b/dashscope/embeddings/__init__.py index 998f1a1f..e421da08 100644 --- a/dashscope/embeddings/__init__.py +++ b/dashscope/embeddings/__init__.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.embeddings``. -from .batch_text_embedding import BatchTextEmbedding -from .batch_text_embedding_response import BatchTextEmbeddingResponse -from .text_embedding import TextEmbedding +Import from ``dashscope.legacy_1x.embeddings`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = [ - "TextEmbedding", - "BatchTextEmbedding", - "BatchTextEmbeddingResponse", -] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.embeddings") diff --git a/dashscope/files.py b/dashscope/files.py index b62d3da8..55ede462 100644 --- a/dashscope/files.py +++ b/dashscope/files.py @@ -1,136 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.files``. -import os -from typing import Optional +Import from ``dashscope.legacy_1x.files`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import ( - DeleteMixin, - FileUploadMixin, - GetMixin, - ListMixin, -) -from dashscope.common.constants import FilePurpose -from dashscope.common.error import InvalidFileFormat -from dashscope.common.utils import is_validate_fine_tune_file +from dashscope.legacy_1x._shim import install as _install - -class Files(FileUploadMixin, ListMixin, DeleteMixin, GetMixin): - SUB_PATH = "files" - - @classmethod - def upload( # pylint: disable=arguments-renamed - cls, - file_path: str, # type: ignore[override] - purpose: str = FilePurpose.fine_tune, # type: ignore[override] - description: Optional[str] = None, # type: ignore[override] - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Upload file for model fine-tune or other tasks. - - Args: - file_path (str): The local file name to upload. - purpose (str): The purpose of the file[fine-tune|inference] - description (str, optional): The file description message. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The upload information - """ - if purpose == FilePurpose.fine_tune: - if not is_validate_fine_tune_file(file_path): - raise InvalidFileFormat( - f"The file {file_path} is not in valid jsonl format", - ) - with open(file_path, "rb") as f: - return super().upload( # type: ignore[return-value] - files=[ - ( - "files", - ( - os.path.basename(f.name), - f, - None, - ), - ), - ], - descriptions=[description] if description is not None else [], - api_key=api_key, - workspace=workspace, - **kwargs, - ) - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List uploaded files. - - Args: - api_key (str, optional): - The api api_key, can be None, - if None, will get by default rule(TODO: api key doc). - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The fine-tune jobs in the result. - """ - return super().list( # type: ignore[return-value] - page, - page_size, - api_key, - workspace=workspace, - **kwargs, - ) - - @classmethod - def get( # type: ignore[override] - cls, - file_id: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the file info. - - Args: - file_id (str): The file id. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The job info - """ - # type: ignore - return super().get(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 - - @classmethod - def delete( # type: ignore[override] - cls, - file_id: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Delete uploaded file. - - Args: - file_id (str): The file id want to delete. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: Delete result. - """ # type: ignore - return super().delete(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 +_install(__name__, "dashscope.legacy_1x.files") diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 6eb707a2..1220656b 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -1,13 +1,25 @@ # -*- coding: utf-8 -*- -from __future__ import annotations - # Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim plus the 1.x surface; the implementation moved to +``dashscope.v2.finetune.agentic_rl``. + +The agentic-RL workflow is owned by the 2.0 SDK, but this import path has +to keep resolving: the ``dashscope`` CLI, the samples under +``reinforcement/examples/workspace`` and the acli skill docs all name +``dashscope.finetune.agentic_rl.AgenticRL``. + +What stays here is the half of the surface that belongs to 1.x -- the +five job-management verbs, which delegate to ``dashscope.FineTunes`` (a +1.x root public API, alongside ``Deployments``), and the ``FineTune`` +dataclasses they return. ``submit_job`` therefore returns a ``FineTune`` +from this path and a ``SubmitResult`` from the 2.0 one; the payload and +the POST are built once, in the 2.0 class. -import time -from typing import Union, List, Optional, ClassVar, Dict, Any -from typing_extensions import Self +Import from ``dashscope.v2.finetune.agentic_rl`` in new code. +""" + +from typing import Dict, List, Optional, Union -from dashscope.client.base_api import CreateMixin from dashscope.finetune.customize_types import ( FineTune, FineTuneCancel, @@ -15,143 +27,70 @@ FineTuneList, ) from dashscope.finetune.finetunes import FineTunes -from dashscope.finetune.reinforcement import ( +from dashscope.v2.finetune.agentic_rl import AgenticRL as _AgenticRLBase +from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult + +# Re-exported so every name this module historically carried stays +# importable from here. Both spellings resolve to the same objects: the +# reverse-reference shim under ``dashscope/finetune/reinforcement`` +# aliases each submodule to the 2.0 one. +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement import ( # noqa: F401 + DASHSCOPE_HTTP_BASE_URL, AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, + AgenticRLTuning, Dataset, - TrainingDataset, - ValidationDataset, -) -from dashscope.finetune.reinforcement import AgenticRLTuning, TuningModel -from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.finetune.reinforcement import ( - FunctionType, DatasetsType, - TrainingType, -) -from dashscope.finetune.reinforcement import ( + FunctionType, + GroupRewardInput, + RewardFunctionComponent, RewardInput, + RolloutFunctionComponent, RolloutInput, - GroupRewardInput, -) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement import ( - set_api_key, + TrainingDataset, + TrainingType, + TuningModel, + ValidationDataset, + deep_remove_none, generate_random_id, get_func_type_id, - deep_remove_none, + logger, + set_api_key, ) -from dashscope.finetune.reinforcement.common.errors import ( - RegistrationError, - ValidationError, + +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement.common.errors import ( # noqa: F401 + DatasetsError, InstanceQueryError, + RegistrationError, RuntimeErrorWithCode, + ValidationError, ValueErrorWithCode, - DatasetsError, ) - -class AgenticRL(AgenticRLTuning, CreateMixin): - SUB_PATH: ClassVar[str] = "fine-tunes" - - def __init__(self, api_key: str = None): - super().__init__() - - try: - set_api_key(api_key) - except Exception as e: - raise ValueErrorWithCode( - "Invalid API key configuration", - error_code=3001, - ) from e - - def init(self, config_path: Optional[str] = None, **kwargs) -> Self: - """ - Initialize an AgenticRL instance from a YAML configuration file. - """ - self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) - - return self - - async def register_functions( - self, - functions: Optional[ - Union[ - List[Union[RolloutFunctionComponent, RewardFunctionComponent]], - RolloutFunctionComponent, - RewardFunctionComponent, - ] - ] = None, - lazy_load: Optional[bool] = True, - ) -> tuple[ - List[str], - List[str], - List[str], - List[str], - List[str], - List[str], - ]: - """Register function components and return entity/instance IDs.""" - if functions: - self.tuning.functions = functions - - try: - ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) = await self.tuning.register_functions( - lazy_load=lazy_load, - ) - logger.info("Function components registered") - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RegistrationError( - "Function registration failed", - error_code=3002, - ) from e - - return ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) - - async def upload_datasets( - self, - datasets: Optional[List[Dataset]] = None, - training_files: Optional[Union[List[str], str]] = None, - validation_files: Optional[Union[List[str], str]] = None, - ) -> tuple[List[str], List[str]]: - if datasets: - self.tuning.datasets = datasets - - try: - ( - uploaded_training_ids, - uploaded_validation_ids, - ) = await self.tuning.upload_datasets( - training_files=training_files or [], - validation_files=validation_files or [], - ) - logger.info("Datasets uploaded") - except Exception as e: - raise DatasetsError( - "Datasets upload failed", - error_code=3003, - ) from e - - return uploaded_training_ids, uploaded_validation_ids - - def submit_job( +__all__ = [ + "AgenticRL", + "DASHSCOPE_HTTP_BASE_URL", + "FineTune", + "FineTuneCancel", + "FineTuneDelete", + "FineTuneList", + "FineTunes", + "SubmitOutput", + "SubmitResult", +] + + +class AgenticRL(_AgenticRLBase): + """The 1.x surface: ``FineTune`` returns plus the ``FineTunes`` verbs. + + The five verbs below are 1.x-only. ``Client(...).rl.jobs`` already + implements the same lifecycle natively on the 2.0 transport, so + duplicating them into the 2.0 class would have given the SDK two + copies of every job verb. + """ + + def submit_job( # type: ignore[override] self, model: Optional[str] = None, datasets: Optional[List[Dataset]] = None, @@ -178,168 +117,18 @@ def submit_job( """ Submit RL tuning job to the platform. """ - teacher_model = teacher_model or self.tuning.teacher_model - - # Resolve job name (fallback to class default) - if job_name: - self.tuning.name = job_name - job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" - - # Model name - if model: - self.tuning.model.name = model - - # rollouts/rewards - if functions: - self.tuning.functions = functions - rollouts = self.tuning.combine_ids_runtimes( - functype=FunctionType.ROLLOUT, - ) - rewards = self.tuning.combine_ids_runtimes( - functype=FunctionType.REWARD, - ) - rewards.extend( - self.tuning.combine_ids_runtimes( - functype=FunctionType.GROUP_REWARD, - id_str=get_func_type_id(FunctionType.REWARD), - ), - ) - # names of functions - if not self.tuning.check_function_names(): - raise ValueErrorWithCode( - "Duplicate function names detected. All function names must " - "be unique.", - error_code=3004, - ) - - # datasets - if datasets: - self.tuning.datasets = datasets - training_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.TRAINING - ] - validation_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.VALIDATION - ] - - # hyper_parameters - if hyper_parameters: - self.tuning.training.hyper_parameters = hyper_parameters - - # resources - if resources: - self.tuning.training.resources = resources - - training_type = self.tuning.training.type - if teacher_model: - training_type = TrainingType.PG_OPD - elif training_type == TrainingType.PG_OPD: - raise ValueErrorWithCode( - f"teacher_model is required when training.type is {training_type}", - error_code=3006, - ) - - request = { - "model": self.tuning.model.name, - "training_datasets": [ds.model_dump() for ds in training_datasets], - "validation_datasets": [ - ds.model_dump() for ds in validation_datasets - ], - "rollout": rollouts[0] if rollouts else None, - "rewards": rewards, - "hyper_parameters": self.tuning.training.hyper_parameters, - "resource_config": self.tuning.training.resources, - "training_type": str(training_type), - "teacher_model": teacher_model, - "job_name": job_name_with_suffix, - } - request = deep_remove_none(request) - logger.info(f"agentic_rl submit_job request: {request}") - - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - try: - resp = super().call( - request, - **kwargs, - ) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode( - "Job submission failed", - error_code=3005, - ) from e - - return FineTune(**resp) - - async def run( - self, - model: Optional[str] = None, - # Datasets parameters - training_datasets: Optional[List[TrainingDataset]] = None, - validation_datasets: Optional[List[ValidationDataset]] = None, - # Path-driven parameters (auto-register & upload) - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - # Common parameters - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - **kwargs, - ) -> FineTune: - """ - Execute RL tuning workflow. - """ - try: - logger.info( - "Path-Driven mode: Registering functions & uploading " - "datasets...", - ) - await self.register_functions( - functions=functions, - lazy_load=True, - ) - - datasets = list(training_datasets or []) + list( - validation_datasets or [], - ) - await self.upload_datasets( - datasets=datasets, - ) - - return self.submit_job( + return FineTune( + **self._submit_envelope( model=model, datasets=datasets, + functions=functions, hyper_parameters=hyper_parameters, resources=resources, job_name=job_name, teacher_model=teacher_model, **kwargs, - ) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode( - "RL tuning workflow failed", - error_code=3006, - ) from e + ), + ) @classmethod def cancel( @@ -436,250 +225,3 @@ def logs( workspace=workspace, **kwargs, ) - - @classmethod - async def test_functions( - cls, - instance_id: str, - functype: FunctionType, - input_data: Dict[str, Any], - api_key: str = None, - pull_logs: bool = False, - log_page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ): - """Test a deployed function instance with custom input data. - - Args: - instance_id: Target function instance ID. - functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). - input_data: Test input payload. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - pull_logs: If True, pull all logs of the function instance - (with pagination) after verification and print them - between separator markers. - log_page_size: Page size used when pulling logs. - start_time: Optional start time filter (in seconds) for log - pulling. Defaults to 24 hours before ``end_time``. - end_time: Optional end time filter (in seconds) for log - pulling. Defaults to the current time (now). - """ - try: - set_api_key(api_key) - - if functype == FunctionType.ROLLOUT: - value = RolloutInput.model_validate(input_data) - elif functype == FunctionType.REWARD: - value = RewardInput.model_validate(input_data) - elif functype == FunctionType.GROUP_REWARD: - value = GroupRewardInput.model_validate(input_data) - else: - raise ValueErrorWithCode( - f"Unsupported function type: {functype}", - error_code=3007, - ) - - logger.info( - f"Starting {str(functype)} verification", - extra={ - "instance_id": instance_id, - "input_params": value.model_dump(exclude={"api_key"}), - }, - ) - - result = await AgenticRLFunctionComponent.verify_function( - value, - instance_id, - ) - - if pull_logs: - await cls._pull_function_instance_logs( - function_instance_id=instance_id, - page_size=log_page_size, - start_time=start_time, - end_time=end_time, - ) - - return result - - except Exception as e: - raise ValidationError( - "Function test failed", - error_code=3008, - ) from e - - @classmethod - async def query_function_instance_logs( - cls, - function_instance_id: str, - page_number: int = 1, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> Dict[str, Any]: - """Query one page of logs for a function (faas) instance. - - Args: - function_instance_id: Target function instance ID. - page_number: Page number, starting from 1. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the log query API. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_function_instance_logs( - function_instance_id=function_instance_id, - page_number=page_number, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3009, - ) from e - - @classmethod - async def query_all_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> List[str]: - """Fetch all logs of a function (faas) instance. - - Pages are fetched one by one via the paginated log query until - all entries are collected. - - Args: - function_instance_id: Target function instance ID. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Aggregated list of log messages across all pages. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3010, - ) from e - - @classmethod - async def delete_function_instance( - cls, - function_instance_id: str, - api_key: str = None, - ) -> Dict[str, Any]: - """Delete a function (faas) runtime instance. - - Args: - function_instance_id: Target function instance ID. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the delete API, containing - ``data.status`` (e.g. "deleted") and - ``data.sandbox_code``. - - Raises: - InstanceQueryError: If the deletion fails. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.delete_function_instance( - function_instance_id=function_instance_id, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance deletion failed", - error_code=3011, - ) from e - - @classmethod - async def _pull_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ) -> None: - """Pull all logs of a function instance and print them between - separator markers (best effort, never raises). - - When ``start_time``/``end_time`` are not provided, defaults to - pulling logs from 24 hours ago until now. - """ - if end_time is None: - end_time = int(time.time()) - if start_time is None: - start_time = end_time - 24 * 60 * 60 - logger.info( - "************start query log " - f"(function_instance_id={function_instance_id})*******", - ) - try: - logs = await cls.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - ) - for entry in logs: - logger.info(f"[function instance log] {entry}") - logger.info( - f"Pulled {len(logs)} log entries for function instance " - f"{function_instance_id}", - ) - except Exception as e: - logger.warning( - f"Failed to pull logs for function instance " - f"{function_instance_id}: {e}", - ) - finally: - logger.info("************end query log *******") diff --git a/dashscope/finetune/customize_types.py b/dashscope/finetune/customize_types.py index 1fa101a8..53fa4de7 100644 --- a/dashscope/finetune/customize_types.py +++ b/dashscope/finetune/customize_types.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import Dict, List, Optional -from dashscope.common.base_type import BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseObjectMixin __all__ = ["Deployment", "FineTune", "DeploymentList", "FineTuneList"] diff --git a/dashscope/finetune/deployments.py b/dashscope/finetune/deployments.py index 4bfb916b..0a06ed46 100644 --- a/dashscope/finetune/deployments.py +++ b/dashscope/finetune/deployments.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from typing import Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetMixin, diff --git a/dashscope/finetune/finetunes.py b/dashscope/finetune/finetunes.py index 8f061153..83745783 100644 --- a/dashscope/finetune/finetunes.py +++ b/dashscope/finetune/finetunes.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import Iterator, Optional, Union -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, DeleteMixin, @@ -14,7 +14,7 @@ LogMixin, StreamEventMixin, ) -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from dashscope.finetune.customize_types import ( FineTune, FineTuneCancel, diff --git a/dashscope/finetune/reinforcement/__init__.py b/dashscope/finetune/reinforcement/__init__.py index bd4d16c5..ed6d74fe 100644 --- a/dashscope/finetune/reinforcement/__init__.py +++ b/dashscope/finetune/reinforcement/__init__.py @@ -1,267 +1,164 @@ # -*- coding: utf-8 -*- -try: - import pydantic # noqa: F401 - import tenacity # noqa: F401 - import yaml # noqa: F401 -except ImportError as _rl_dep_err: - raise ImportError( - "Agentic RL fine-tuning dependencies are missing from this " - "install. Reinstall with: pip install -U dashscope", - ) from _rl_dep_err +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim; the implementation moved to +``dashscope.v2.finetune.reinforcement``. -from dashscope.finetune.reinforcement.common.constants import ( - BAILIAN_FILE_API, - BAILIAN_FILE_TIMEOUT, - DASHSCOPE_API_KEY, - DASHSCOPE_HTTP_BASE_URL, - LOG_LEVEL, - FC_API_KEY, - FC_DELETE_API, - FC_FILES_START, - FC_LOAD_API, - FC_PYPI_LIB, - FC_PYPI_REPO, - FC_QUERY_API, - FC_QUERY_LOG_API, - FC_REGISTER_REWARD_API, - FC_REGISTER_ROLLOUT_API, - FC_REGISTER_GROUP_REWARD_API, - FC_SERVER_CLASSPATH, - FC_UPLOAD_OSS_API, - FC_WORKERS_COUNT, - FC_LAYER_CREATE_API, - FC_LAYER_QUERY_API, - FC_LAYER_USED, - FC_LAYER_NAME, - FC_REQUIREMENTS_FILE, - FC_ZIP_EXCLUDE_PATTERNS, - FC_OSS_FILE_SIZE_WARNING, - DATASETS_FILE_SIZE_WARNING, - HTTP_REQUEST_TIMEOUT, - LOGGER_FILTER_FIELDS, -) -from dashscope.finetune.reinforcement.common.errors import ( - AgenticRLError, - InputError, - OutputError, - BaseConnectionError, - OSSConnectionError, - OSSUploadError, - DeploymentError, - RegistrationError, - FunctionLoadError, - InstanceWarmupError, - InstanceQueryError, - FunctionLayerError, - ValidationError, - ConfigurationError, - BasePermissionError, - IOErrorWithCode, - RuntimeErrorWithCode, - ValueErrorWithCode, -) -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model import ( - FunctionComponentModel, - TuningModel, - FoundationModel, - FunctionComponentRuntime, - AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, - GroupRewardFunctionComponent, - Datasets, - Dataset, - TrainingDataset, - ValidationDataset, - MountStorage, - Training, - Observability, - AgenticRLTuning, - Models, -) -from dashscope.finetune.reinforcement.common.model_types import ( - FileSpec, - FunctionType, - DatasetsType, - DataSourceType, - RequestFC, - ResponseFC, - Status, - StatusType, - TrainingType, -) -from dashscope.finetune.reinforcement.common.utils import ( - check_file, - client_fc, - create_deployment_files, - deep_mask, - generate_random_id, - get_filepath_classname, - upload_zip_to_oss_and_by_signed_url, - set_api_key, - to_bailian_data, - zip_dir, - get_func_type_id, - deep_remove_none, - extract_reward_weights, - get_weights_from_file, - serialize_for_output, -) -from dashscope.finetune.reinforcement.component import ( - reward_func, - sub_reward_func, - aggregate_func, -) -from dashscope.finetune.reinforcement.component.data import ( - AgentOutput, - Reward, - RewardInput, - RewardOutput, - RolloutInput, - RolloutOutput, - GroupReward, - GroupRewardInput, - GroupRewardOutput, - TaskStatus, -) -from dashscope.finetune.reinforcement.component.func_decorator import ( - RewardProcessorMeta, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractRolloutProcessor, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractGroupRewardProcessor, +The agentic-RL workflow package is owned by the 2.0 SDK. This path has to +keep resolving indefinitely, because two of its consumers resolve the +module *by name* in a process this repo does not control: + +- ``FC_SERVER_CLASSPATH`` is written into the ``start.sh`` that gets + deployed to a Function Compute sandbox, where the installed dashscope + may be any published version. +- ``examples/workspace/scripts/start_local_*.sh`` launches the reward / + rollout server with ``python3 -m + dashscope.finetune.reinforcement.component.server.server``, and + ``server.py`` hands the same dotted string to ``uvicorn.run``, which + imports it inside its worker process. + +Unlike the AgentStudio shim, aliasing here cannot be eager. Nine of the +package's modules -- ``component.server.*`` and +``component.observability.genai.*`` -- are imported lazily and need +fastapi / opentelemetry, both optional. Walking the tree up front would +either miss them or promote two optional dependencies to import-time +hard ones, so a ``sys.meta_path`` finder aliases each submodule on +demand and hands back the *same module object* the v2 path uses. + +Import from ``dashscope.v2.finetune.reinforcement`` in new code. +""" + +import importlib +import importlib.util +import sys + +# Importing the package runs its own dependency guard, so a missing +# pydantic / tenacity / pyyaml still raises from this path. +from dashscope.v2.finetune import reinforcement as _impl + +# pylint: disable-next=wildcard-import,unused-wildcard-import +from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 + +# The wildcard only carries the names in the implementation's __all__, +# which lists classes and helpers but not its subpackages. Binding them +# here keeps the deep imports the samples use statically resolvable for +# pylint and IDEs, to whom the meta_path finder below is invisible. At +# runtime both are already imported by the package's own eager imports. +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement import ( # noqa: F401 + common, + component, ) +_OLD = __name__ +_NEW = _impl.__name__ + +# ``app`` is the 1.x CLI's typer app and is reachable from this path +# only; the 2.0 package must not depend on the CLI. It stays lazy +# because resolving it pulls in typer and the rest of the CLI. +# pylint: disable-next=undefined-all-variable +__all__ = list(_impl.__all__) + ["app"] # noqa: F405 + def __getattr__(name): if name == "app": - from dashscope.cli.agentic_rl import app + from dashscope.legacy_1x.cli.agentic_rl import app return app - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + raise AttributeError(f"module {_OLD!r} has no attribute {name!r}") + + +def _restore_identity(module, v2_spec): + """Put the v2 identity back on a module object both paths share. + + ``importlib._bootstrap._init_module_attrs`` stamps the spec it was + handed onto whatever ``create_module`` returned, and that spec names + the legacy path. The object is the v2 module, so it must keep + reporting the v2 name: pickling, ``importlib.reload`` and anything + comparing ``module.__spec__.name`` against the ``sys.modules`` key + all read it. + """ + module.__name__ = v2_spec.name + module.__spec__ = v2_spec + module.__loader__ = v2_spec.loader + module.__package__ = v2_spec.parent + module.__file__ = v2_spec.origin + if v2_spec.submodule_search_locations is not None: + module.__path__ = v2_spec.submodule_search_locations + + +class _AliasLoader: + """Loader that hands back the already-imported v2 module. + + Not an ``importlib.abc.Loader`` subclass on purpose: the import + system resolves loaders by duck typing, and the ABC still declares + ``module_repr`` abstract on this interpreter even though 3.12 + removed it. + """ + + def __init__(self, v2_spec): + self._v2_spec = v2_spec + + def create_module(self, spec): + module = sys.modules.get(self._v2_spec.name) + if module is None: + module = importlib.import_module(self._v2_spec.name) + # Binding the attribute on the parent is what makes + # ``dashscope.finetune.reinforcement.common.utils`` work as an + # expression and not just as an import target: the import system + # skips a module it already finds in sys.modules and never sets + # the attribute itself. + parent_name, _, leaf = spec.name.rpartition(".") + parent = sys.modules.get(parent_name) + if parent is not None: + setattr(parent, leaf, module) + return module + + def exec_module(self, module): + # Deliberately does not re-run the module body. It already ran + # under the v2 name, and a second pass would build a duplicate + # FastAPI app, OpenTelemetry tracer and log handler. + _restore_identity(module, self._v2_spec) + + def get_code(self, fullname): # pylint: disable=unused-argument + # ``runpy`` -- and therefore ``python -m`` -- refuses a loader + # that yields no code object, and uvicorn resolves its import + # string through the same path. + return self._v2_spec.loader.get_code(self._v2_spec.name) + + +class _Finder: + """Resolve ``dashscope.finetune.reinforcement.*`` to the v2 modules. + + Duck-typed for the same reason as ``_AliasLoader``: ``sys.meta_path`` + entries only need a ``find_spec``. + """ + + def find_spec( # pylint: disable=unused-argument + self, + fullname, + path=None, + target=None, + ): + if not fullname.startswith(_OLD + "."): + return None + v2_name = _NEW + fullname[len(_OLD) :] + # find_spec resolves without importing the leaf, so a module that + # needs fastapi or opentelemetry stays unimported until someone + # actually asks for it. + v2_spec = importlib.util.find_spec(v2_name) + if v2_spec is None or v2_spec.loader is None: + return None + return importlib.util.spec_from_file_location( + fullname, + v2_spec.origin, + loader=_AliasLoader(v2_spec), + submodule_search_locations=v2_spec.submodule_search_locations, + ) -# pylint: disable=undefined-all-variable -__all__ = [ - # Constants - "BAILIAN_FILE_API", - "BAILIAN_FILE_TIMEOUT", - "DASHSCOPE_API_KEY", - "DASHSCOPE_HTTP_BASE_URL", - "LOG_LEVEL", - "FC_API_KEY", - "FC_DELETE_API", - "FC_FILES_START", - "FC_LOAD_API", - "FC_PYPI_LIB", - "FC_PYPI_REPO", - "FC_QUERY_API", - "FC_QUERY_LOG_API", - "FC_REGISTER_REWARD_API", - "FC_REGISTER_ROLLOUT_API", - "FC_REGISTER_GROUP_REWARD_API", - "FC_SERVER_CLASSPATH", - "FC_UPLOAD_OSS_API", - "FC_WORKERS_COUNT", - "FC_LAYER_CREATE_API", - "FC_LAYER_QUERY_API", - "FC_LAYER_USED", - "FC_LAYER_NAME", - "FC_REQUIREMENTS_FILE", - "FC_ZIP_EXCLUDE_PATTERNS", - "FC_OSS_FILE_SIZE_WARNING", - "DATASETS_FILE_SIZE_WARNING", - "HTTP_REQUEST_TIMEOUT", - "LOGGER_FILTER_FIELDS", - # Errors - "AgenticRLError", - "InputError", - "OutputError", - "BaseConnectionError", - "OSSConnectionError", - "OSSUploadError", - "DeploymentError", - "RegistrationError", - "FunctionLoadError", - "FunctionLayerError", - "InstanceWarmupError", - "InstanceQueryError", - "ValidationError", - "ConfigurationError", - "BasePermissionError", - "IOErrorWithCode", - "RuntimeErrorWithCode", - "ValueErrorWithCode", - # Logging - "logger", - # Utilities - "check_file", - "client_fc", - "create_deployment_files", - "deep_mask", - "generate_random_id", - "get_filepath_classname", - "set_api_key", - "to_bailian_data", - "upload_zip_to_oss_and_by_signed_url", - "zip_dir", - "get_func_type_id", - "deep_remove_none", - "extract_reward_weights", - "get_weights_from_file", - "serialize_for_output", - # Model Types - "FileSpec", - "FunctionType", - "DatasetsType", - "DataSourceType", - "RequestFC", - "ResponseFC", - "Status", - "StatusType", - "TrainingType", - # Data models - "RolloutInput", - "RolloutOutput", - "RewardInput", - "RewardOutput", - "GroupRewardInput", - "GroupRewardOutput", - "GroupReward", - "Reward", - "AgentOutput", - "TaskStatus", - # Func Manager - "reward_func", - "sub_reward_func", - "aggregate_func", - "RewardProcessorMeta", - "AbstractRolloutProcessor", - "AbstractRewardProcessor", - "AbstractGroupRewardProcessor", - # Core Models - "FunctionComponentModel", - "TuningModel", - "FoundationModel", - "FunctionComponentRuntime", - "AgenticRLFunctionComponent", - "RolloutFunctionComponent", - "RewardFunctionComponent", - "GroupRewardFunctionComponent", - "Datasets", - "Dataset", - "TrainingDataset", - "ValidationDataset", - "MountStorage", - "Training", - "Observability", - "AgenticRLTuning", - "Models", - "app", -] +# Inserted ahead of PathFinder rather than appended: the alias inherits +# the v2 package's __path__, so PathFinder would find the real file +# there and execute it a *second* time under the legacy name, yielding +# two distinct class objects for the same source. +if not any(isinstance(entry, _Finder) for entry in sys.meta_path): + sys.meta_path.insert(0, _Finder()) diff --git a/dashscope/finetune/reinforcement/component/demo/__init__.py b/dashscope/finetune/reinforcement/component/demo/__init__.py deleted file mode 100644 index 6100812f..00000000 --- a/dashscope/finetune/reinforcement/component/demo/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRolloutProcessor, -) - -__all__ = [ - "DemoRewardProcessor", - # Inherits AbstractRewardProcessor, returns RewardOutput - "DemoRolloutProcessor", - # Inherits AbstractRolloutProcessor, returns RolloutOutput - "DemoGroupRewardProcessor", - # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput -] diff --git a/dashscope/finetune/reinforcement/component/parser/__init__.py b/dashscope/finetune/reinforcement/component/parser/__init__.py deleted file mode 100644 index 6f5dedd0..00000000 --- a/dashscope/finetune/reinforcement/component/parser/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.parser.base_parser import ( - BaseRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 - GroupRewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( - RewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( - RolloutRequestParser, -) - -__all__ = [ - "BaseRequestParser", - "RewardRequestParser", - "RolloutRequestParser", - "GroupRewardRequestParser", -] diff --git a/dashscope/finetune/reinforcement/component/processor/__init__.py b/dashscope/finetune/reinforcement/component/processor/__init__.py deleted file mode 100644 index 9246fa56..00000000 --- a/dashscope/finetune/reinforcement/component/processor/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRolloutProcessor, -) - -__all__ = [ - "AbstractProcessor", - "AbstractRewardProcessor", - "AbstractRolloutProcessor", - "AbstractGroupRewardProcessor", -] diff --git a/dashscope/io/__init__.py b/dashscope/io/__init__.py index e69de29b..75c5b903 100644 --- a/dashscope/io/__init__.py +++ b/dashscope/io/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.io``. + +Import from ``dashscope.legacy_1x.io`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.io") diff --git a/dashscope/legacy_1x/__init__.py b/dashscope/legacy_1x/__init__.py new file mode 100644 index 00000000..64709f71 --- /dev/null +++ b/dashscope/legacy_1x/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The dashscope 1.x SDK. + +Everything under this package is the legacy 1.x business surface. It is +kept importable both at its canonical ``dashscope.legacy_1x.*`` path and, +for backward compatibility, at the historical top-level paths (for +example ``dashscope.aigc`` -> ``dashscope.legacy_1x.aigc``) via thin +alias shims installed at the package root. + +New code should target the 2.0 SDK, which lives at the top level of +``dashscope`` (``dashscope.Client``, ``dashscope.chat``, ...). +""" diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py new file mode 100644 index 00000000..2ce6ec93 --- /dev/null +++ b/dashscope/legacy_1x/_shim.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat aliasing for the historical top-level 1.x import paths. + +The 1.x business surface now lives under ``dashscope.legacy_1x``. The thin +shim packages left at the old top-level names (``dashscope/aigc/__init__.py`` +and friends) call :func:`install` so that ``import dashscope.aigc`` and +``from dashscope.aigc.generation import Generation`` keep resolving. + +The shim re-exports the implementation's public names and points its +``__path__`` at the implementation package, so a submodule import loads the +real file. Submodules reached through the old spelling are therefore distinct +module objects from their ``dashscope.legacy_1x.*`` twins; nothing inside this +repository relies on that (all internal code, tests and examples use the +canonical ``dashscope.legacy_1x`` path, and ``import dashscope`` re-exports the +public names directly), so the aliasing is best-effort back-compat for external +callers only. It is deliberately lazy -- it never imports submodules itself -- +so importing a shim cannot trigger import-time side effects in the 1.x tree +(the ``dashscope.legacy_1x.cli`` package has some). +""" + +from __future__ import annotations + +import importlib +import sys + + +def install(shim_name: str, impl_name: str): + """Alias the package/module ``shim_name`` onto ``impl_name``. + + Returns the implementation module so a shim can also do + ``_impl = install(__name__, "dashscope.legacy_1x.aigc")`` when it wants + to reference it. + """ + impl = importlib.import_module(impl_name) + shim = sys.modules[shim_name] + + # Some 1.x packages declare a malformed ``__all__`` (e.g. ``audio`` + # lists module objects rather than names), so union the string entries + # of ``__all__`` with every public attribute instead of trusting it. + declared = getattr(impl, "__all__", None) or [] + names = {name for name in declared if isinstance(name, str)} + names.update(name for name in dir(impl) if not name.startswith("_")) + for name in names: + try: + setattr(shim, name, getattr(impl, name)) + except AttributeError: # pragma: no cover - name vanished mid-walk + continue + + if hasattr(impl, "__path__"): + # Let ``import dashscope..`` find the implementation's + # files without eagerly importing them here. + shim.__path__ = impl.__path__ + + return impl diff --git a/dashscope/legacy_1x/aigc/__init__.py b/dashscope/legacy_1x/aigc/__init__.py new file mode 100644 index 00000000..6f1e32c8 --- /dev/null +++ b/dashscope/legacy_1x/aigc/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +from .conversation import Conversation, History, HistoryItem +from .generation import Generation, AioGeneration +from .image_synthesis import ImageSynthesis, AioImageSynthesis +from .multimodal_conversation import ( + MultiModalConversation, + AioMultiModalConversation, +) +from .video_synthesis import VideoSynthesis, AioVideoSynthesis + +__all__ = [ + "Generation", + "AioGeneration", + "Conversation", + "HistoryItem", + "History", + "ImageSynthesis", + "AioImageSynthesis", + "MultiModalConversation", + "AioMultiModalConversation", + "VideoSynthesis", + "AioVideoSynthesis", +] diff --git a/dashscope/aigc/chat_completion.py b/dashscope/legacy_1x/aigc/chat_completion.py similarity index 97% rename from dashscope/aigc/chat_completion.py rename to dashscope/legacy_1x/aigc/chat_completion.py index 2ec2c512..99957e65 100644 --- a/dashscope/aigc/chat_completion.py +++ b/dashscope/legacy_1x/aigc/chat_completion.py @@ -5,18 +5,18 @@ from typing import Any, Dict, Generator, List, Union import dashscope -from dashscope.aigc.generation import Generation -from dashscope.api_entities.chat_completion_types import ( +from dashscope.legacy_1x.aigc.generation import Generation +from dashscope.legacy_1x.api_entities.chat_completion_types import ( ChatCompletion, ChatCompletionChunk, ) -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( GenerationResponse, Message, ) -from dashscope.client.base_api import BaseAioApi, CreateMixin -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseAioApi, CreateMixin +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Completions(CreateMixin): diff --git a/dashscope/aigc/code_generation.py b/dashscope/legacy_1x/aigc/code_generation.py similarity index 97% rename from dashscope/aigc/code_generation.py rename to dashscope/legacy_1x/aigc/code_generation.py index 4dbf422e..281c72b6 100644 --- a/dashscope/aigc/code_generation.py +++ b/dashscope/legacy_1x/aigc/code_generation.py @@ -3,15 +3,15 @@ from typing import Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import MESSAGE, SCENE -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import MESSAGE, SCENE +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class MessageParam(DictMixin): diff --git a/dashscope/aigc/conversation.py b/dashscope/legacy_1x/aigc/conversation.py similarity index 97% rename from dashscope/aigc/conversation.py rename to dashscope/legacy_1x/aigc/conversation.py index 65a2f856..1f937f3f 100644 --- a/dashscope/aigc/conversation.py +++ b/dashscope/legacy_1x/aigc/conversation.py @@ -6,16 +6,24 @@ from http import HTTPStatus from typing import Any, Dict, Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( ConversationResponse, Message, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import DEPRECATED_MESSAGE, HISTORY, PROMPT -from dashscope.common.error import InputRequired, InvalidInput, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ( + DEPRECATED_MESSAGE, + HISTORY, + PROMPT, +) +from dashscope.legacy_1x.common.error import ( + InputRequired, + InvalidInput, + ModelRequired, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class HistoryItem(dict): diff --git a/dashscope/aigc/generation.py b/dashscope/legacy_1x/aigc/generation.py similarity index 97% rename from dashscope/aigc/generation.py rename to dashscope/legacy_1x/aigc/generation.py index 0c6f0341..0f80f85c 100644 --- a/dashscope/aigc/generation.py +++ b/dashscope/legacy_1x/aigc/generation.py @@ -5,24 +5,24 @@ import json from typing import Any, Dict, Generator, List, Union, AsyncGenerator -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( GenerationResponse, Message, Role, ) -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ( CUSTOMIZED_MODEL_ID, DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import merge_single_response class Generation(BaseApi): diff --git a/dashscope/aigc/image_generation.py b/dashscope/legacy_1x/aigc/image_generation.py similarity index 98% rename from dashscope/aigc/image_generation.py rename to dashscope/legacy_1x/aigc/image_generation.py index b8304957..8d896bae 100644 --- a/dashscope/aigc/image_generation.py +++ b/dashscope/legacy_1x/aigc/image_generation.py @@ -3,22 +3,22 @@ from typing import Generator, List, Union, AsyncGenerator -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( Message, DashScopeAPIResponse, ImageGenerationResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( BaseAioApi, BaseApi, BaseAsyncApi, BaseAsyncAioApi, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import merge_single_response class ImageGeneration(BaseApi, BaseAsyncApi): diff --git a/dashscope/aigc/image_synthesis.py b/dashscope/legacy_1x/aigc/image_synthesis.py similarity index 99% rename from dashscope/aigc/image_synthesis.py rename to dashscope/legacy_1x/aigc/image_synthesis.py index eb25276a..fd56c717 100644 --- a/dashscope/aigc/image_synthesis.py +++ b/dashscope/legacy_1x/aigc/image_synthesis.py @@ -3,20 +3,24 @@ from typing import Any, Dict, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, ImageSynthesisResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( BaseAsyncApi, BaseApi, BaseAsyncAioApi, BaseAioApi, ) -from dashscope.common.constants import IMAGES, NEGATIVE_PROMPT, PROMPT -from dashscope.common.error import InputRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.common.constants import ( + IMAGES, + NEGATIVE_PROMPT, + PROMPT, +) +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local class ImageSynthesis(BaseAsyncApi): diff --git a/dashscope/aigc/multimodal_conversation.py b/dashscope/legacy_1x/aigc/multimodal_conversation.py similarity index 97% rename from dashscope/aigc/multimodal_conversation.py rename to dashscope/legacy_1x/aigc/multimodal_conversation.py index c3d900b7..67cf0e7c 100644 --- a/dashscope/aigc/multimodal_conversation.py +++ b/dashscope/legacy_1x/aigc/multimodal_conversation.py @@ -4,15 +4,17 @@ import copy from typing import Any, AsyncGenerator, Dict, Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( MultiModalConversationResponse, ) -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.error import ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_multimodal_single_response +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import ( + merge_multimodal_single_response, +) class MultiModalConversation(BaseApi): diff --git a/dashscope/aigc/video_synthesis.py b/dashscope/legacy_1x/aigc/video_synthesis.py similarity index 99% rename from dashscope/aigc/video_synthesis.py rename to dashscope/legacy_1x/aigc/video_synthesis.py index 0c46257e..12c34384 100644 --- a/dashscope/aigc/video_synthesis.py +++ b/dashscope/legacy_1x/aigc/video_synthesis.py @@ -5,19 +5,19 @@ from concurrent.futures import ThreadPoolExecutor, as_completed -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, VideoSynthesisResponse, ) -from dashscope.client.base_api import BaseAsyncApi, BaseAsyncAioApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseAsyncApi, BaseAsyncAioApi +from dashscope.legacy_1x.common.constants import ( PROMPT, REFERENCE_VIDEO_URLS, REFERENCE_URLS, MEDIA_URLS, ) -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local class VideoSynthesis(BaseAsyncApi): diff --git a/dashscope/finetune/reinforcement/common/__init__.py b/dashscope/legacy_1x/api_entities/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/common/__init__.py rename to dashscope/legacy_1x/api_entities/__init__.py diff --git a/dashscope/api_entities/aio_session.py b/dashscope/legacy_1x/api_entities/aio_session.py similarity index 98% rename from dashscope/api_entities/aio_session.py rename to dashscope/legacy_1x/api_entities/aio_session.py index 2de1c46e..7edd7d74 100644 --- a/dashscope/api_entities/aio_session.py +++ b/dashscope/legacy_1x/api_entities/aio_session.py @@ -16,7 +16,7 @@ import aiohttp import certifi -from dashscope.common.logging import logger +from dashscope.legacy_1x.common.logging import logger _shared_ssl_context: Optional[ssl.SSLContext] = None _aio_sessions: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary() diff --git a/dashscope/api_entities/aiohttp_request.py b/dashscope/legacy_1x/api_entities/aiohttp_request.py similarity index 96% rename from dashscope/api_entities/aiohttp_request.py rename to dashscope/legacy_1x/api_entities/aiohttp_request.py index 611e72ec..e2784627 100644 --- a/dashscope/api_entities/aiohttp_request.py +++ b/dashscope/legacy_1x/api_entities/aiohttp_request.py @@ -7,20 +7,22 @@ import aiohttp -from dashscope.api_entities.aio_session import ( +from dashscope.legacy_1x.api_entities.aio_session import ( get_shared_aio_session, send_with_retry_async, ) -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SSE_CONTENT_TYPE, HTTPMethod, ) -from dashscope.common.error import UnsupportedHTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import async_to_sync +from dashscope.legacy_1x.common.error import UnsupportedHTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import async_to_sync class AioHttpRequest(AioBaseRequest): diff --git a/dashscope/api_entities/api_request_data.py b/dashscope/legacy_1x/api_entities/api_request_data.py similarity index 97% rename from dashscope/api_entities/api_request_data.py rename to dashscope/legacy_1x/api_entities/api_request_data.py index c6ea54ab..4b839103 100644 --- a/dashscope/api_entities/api_request_data.py +++ b/dashscope/legacy_1x/api_entities/api_request_data.py @@ -7,8 +7,8 @@ import aiohttp -from dashscope.common.constants import ApiProtocol -from dashscope.io.input_output import InputResolver +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.io.input_output import InputResolver class ApiRequestData: diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/legacy_1x/api_entities/api_request_factory.py similarity index 92% rename from dashscope/api_entities/api_request_factory.py rename to dashscope/legacy_1x/api_entities/api_request_factory.py index 6511c29c..1ff1fc83 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/legacy_1x/api_entities/api_request_factory.py @@ -7,20 +7,23 @@ import requests import dashscope -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.api_entities.encryption import Encryption -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.websocket_request import WebSocketRequest -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.websocket_request import WebSocketRequest +from dashscope.legacy_1x.common.constants import ( SERVICE_API_PATH, ApiProtocol, HTTPMethod, ) -from dashscope.common.error import InputDataRequired, UnsupportedApiProtocol -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers -from dashscope.common.env import resolve_base_url -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.error import ( + InputDataRequired, + UnsupportedApiProtocol, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode def _build_api_request( # pylint: disable=too-many-branches diff --git a/dashscope/api_entities/base_request.py b/dashscope/legacy_1x/api_entities/base_request.py similarity index 83% rename from dashscope/api_entities/base_request.py rename to dashscope/legacy_1x/api_entities/base_request.py index 1a9a664f..eb3b9847 100644 --- a/dashscope/api_entities/base_request.py +++ b/dashscope/legacy_1x/api_entities/base_request.py @@ -4,8 +4,10 @@ import os from abc import ABC, abstractmethod -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.constants import ( + DASHSCOPE_DISABLE_DATA_INSPECTION_ENV, +) +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class BaseRequest(ABC): diff --git a/dashscope/api_entities/chat_completion_types.py b/dashscope/legacy_1x/api_entities/chat_completion_types.py similarity index 99% rename from dashscope/api_entities/chat_completion_types.py rename to dashscope/legacy_1x/api_entities/chat_completion_types.py index 5d8ac2f9..d2ace56a 100644 --- a/dashscope/api_entities/chat_completion_types.py +++ b/dashscope/legacy_1x/api_entities/chat_completion_types.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import List, Literal, Optional -from dashscope.common.base_type import BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseObjectMixin @dataclass(init=False) diff --git a/dashscope/api_entities/dashscope_response.py b/dashscope/legacy_1x/api_entities/dashscope_response.py similarity index 100% rename from dashscope/api_entities/dashscope_response.py rename to dashscope/legacy_1x/api_entities/dashscope_response.py diff --git a/dashscope/api_entities/encryption.py b/dashscope/legacy_1x/api_entities/encryption.py similarity index 95% rename from dashscope/api_entities/encryption.py rename to dashscope/legacy_1x/api_entities/encryption.py index a40dc2bf..c6dac207 100644 --- a/dashscope/api_entities/encryption.py +++ b/dashscope/legacy_1x/api_entities/encryption.py @@ -13,15 +13,15 @@ from cryptography.hazmat.backends import default_backend import dashscope -from dashscope.common.constants import ( +from dashscope.legacy_1x.common.constants import ( ENCRYPTION_AES_SECRET_KEY_BYTES, ENCRYPTION_AES_IV_LENGTH, DEFAULT_REQUEST_TIMEOUT_SECONDS, ) -from dashscope.common.logging import logger -from dashscope.common.env import resolve_base_url -from dashscope.common.api_key import get_default_api_key -from dashscope.common.utils import get_sdk_headers +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.utils import get_sdk_headers class Encryption: diff --git a/dashscope/api_entities/http_request.py b/dashscope/legacy_1x/api_entities/http_request.py similarity index 97% rename from dashscope/api_entities/http_request.py rename to dashscope/legacy_1x/api_entities/http_request.py index 96ee9ea8..4f283625 100644 --- a/dashscope/api_entities/http_request.py +++ b/dashscope/legacy_1x/api_entities/http_request.py @@ -12,26 +12,28 @@ import requests from requests.adapters import HTTPAdapter -from dashscope.api_entities.aio_session import ( +from dashscope.legacy_1x.api_entities.aio_session import ( get_shared_aio_session, send_with_retry_async, ) -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SSE_CONTENT_TYPE, HTTPMethod, ) -from dashscope.common.error import UnsupportedHTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import ( +from dashscope.legacy_1x.common.error import UnsupportedHTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import ( _handle_aio_stream, _handle_aiohttp_failed_response, _handle_http_failed_response, _handle_stream, ) -from dashscope.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.encryption import Encryption # Shared synchronous session for connection pooling _shared_sync_session: Optional[requests.Session] = None diff --git a/dashscope/api_entities/websocket_request.py b/dashscope/legacy_1x/api_entities/websocket_request.py similarity index 97% rename from dashscope/api_entities/websocket_request.py rename to dashscope/legacy_1x/api_entities/websocket_request.py index 2358b4ec..7e285aa8 100644 --- a/dashscope/api_entities/websocket_request.py +++ b/dashscope/legacy_1x/api_entities/websocket_request.py @@ -9,21 +9,23 @@ import aiohttp -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SERVICE_503_MESSAGE, WEBSOCKET_ERROR_CODE, ) -from dashscope.common.error import ( +from dashscope.legacy_1x.common.error import ( RequestFailure, UnexpectedMessageReceived, UnknownMessageReceived, ) -from dashscope.common.logging import logger -from dashscope.common.utils import async_to_sync -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import async_to_sync +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, ERROR_MESSAGE, ERROR_NAME, diff --git a/dashscope/legacy_1x/app/__init__.py b/dashscope/legacy_1x/app/__init__.py new file mode 100644 index 00000000..e0f1ba76 --- /dev/null +++ b/dashscope/legacy_1x/app/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .application import Application + +__all__ = ["Application"] diff --git a/dashscope/app/application.py b/dashscope/legacy_1x/app/application.py similarity index 95% rename from dashscope/app/application.py rename to dashscope/legacy_1x/app/application.py index 6afb72cf..90d70fa3 100644 --- a/dashscope/app/application.py +++ b/dashscope/legacy_1x/app/application.py @@ -8,20 +8,22 @@ import copy from typing import Any, Dict, Generator, List, Optional, Union -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import Message, Role -from dashscope.app.application_response import ApplicationResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.api_key import get_default_api_key -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.app.application_response import ApplicationResponse +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import ( DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, InvalidInput -from dashscope.common.logging import logger -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, InvalidInput +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.utils.message_utils import merge_single_response class Application(BaseApi): diff --git a/dashscope/app/application_response.py b/dashscope/legacy_1x/app/application_response.py similarity index 99% rename from dashscope/app/application_response.py rename to dashscope/legacy_1x/app/application_response.py index 19af1668..976d7150 100644 --- a/dashscope/app/application_response.py +++ b/dashscope/legacy_1x/app/application_response.py @@ -9,7 +9,7 @@ from http import HTTPStatus from typing import Dict, List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) diff --git a/dashscope/legacy_1x/assistants/__init__.py b/dashscope/legacy_1x/assistants/__init__.py new file mode 100644 index 00000000..9c63d153 --- /dev/null +++ b/dashscope/legacy_1x/assistants/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import warnings + +# yapf: disable + +from dashscope.legacy_1x.assistants.assistant_types import ( + Assistant, AssistantFile, + AssistantList, + DeleteResponse, +) +from dashscope.legacy_1x.assistants.assistants import Assistants + +__all__ = [ + 'Assistant', + 'Assistants', + 'AssistantList', + 'AssistantFile', + 'DeleteResponse', +] + +# Deprecation warning +_DEPRECATION_MSG = ( + "The Assistants API (dashscope.assistants) is deprecated and will be " + "removed in a future release. Please migrate to the Responses API. " + "See https://help.aliyun.com/zh/model-studio/" + "synchronous-call-api-reference for migration details." +) + +warnings.warn(_DEPRECATION_MSG, category=DeprecationWarning, stacklevel=2) diff --git a/dashscope/assistants/assistant_types.py b/dashscope/legacy_1x/assistants/assistant_types.py similarity index 98% rename from dashscope/assistants/assistant_types.py rename to dashscope/legacy_1x/assistants/assistant_types.py index 624c4c8e..71691752 100644 --- a/dashscope/assistants/assistant_types.py +++ b/dashscope/legacy_1x/assistants/assistant_types.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Dict, List, Optional, Union -from dashscope.common.base_type import BaseList, BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseList, BaseObjectMixin __all__ = [ "Assistant", diff --git a/dashscope/assistants/assistants.py b/dashscope/legacy_1x/assistants/assistants.py similarity index 98% rename from dashscope/assistants/assistants.py rename to dashscope/legacy_1x/assistants/assistants.py index 5342f785..2036050e 100644 --- a/dashscope/assistants/assistants.py +++ b/dashscope/legacy_1x/assistants/assistants.py @@ -4,12 +4,12 @@ import warnings from typing import Dict, List, Optional -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( Assistant, AssistantList, DeleteResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, DeleteMixin, @@ -17,13 +17,13 @@ ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import ModelRequired +from dashscope.legacy_1x.common.error import ModelRequired __all__ = ["Assistants"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.assistants) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/assistants/files.py b/dashscope/legacy_1x/assistants/files.py similarity index 96% rename from dashscope/assistants/files.py rename to dashscope/legacy_1x/assistants/files.py index 6e118001..6cc21c88 100644 --- a/dashscope/assistants/files.py +++ b/dashscope/legacy_1x/assistants/files.py @@ -3,24 +3,24 @@ import warnings from typing import Optional -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( AssistantFile, AssistantFileList, DeleteResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetStatusMixin, ListObjectMixin, ) -from dashscope.common.error import InputRequired +from dashscope.legacy_1x.common.error import InputRequired __all__ = ["Files"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.assistants) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/audio/__init__.py b/dashscope/legacy_1x/audio/__init__.py new file mode 100644 index 00000000..bdf170a7 --- /dev/null +++ b/dashscope/legacy_1x/audio/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from . import asr, tts, tts_v2, qwen_tts, qwen_tts_realtime, qwen_omni + +__all__ = [ # type: ignore[misc] + asr, + tts, + tts_v2, + qwen_tts, + qwen_tts_realtime, + qwen_omni, +] # noqa: E501 diff --git a/dashscope/audio/asr/__init__.py b/dashscope/legacy_1x/audio/asr/__init__.py similarity index 100% rename from dashscope/audio/asr/__init__.py rename to dashscope/legacy_1x/audio/asr/__init__.py diff --git a/dashscope/audio/asr/asr_phrase_manager.py b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py similarity index 96% rename from dashscope/audio/asr/asr_phrase_manager.py rename to dashscope/legacy_1x/audio/asr/asr_phrase_manager.py index c1dd1a5f..d493a41e 100644 --- a/dashscope/audio/asr/asr_phrase_manager.py +++ b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py @@ -4,10 +4,12 @@ from http import HTTPStatus from typing import Any, Dict -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.error import InvalidParameter -from dashscope.common.logging import logger +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.error import InvalidParameter +from dashscope.legacy_1x.common.logging import logger from dashscope.finetune.finetunes import FineTunes diff --git a/dashscope/audio/asr/recognition.py b/dashscope/legacy_1x/audio/asr/recognition.py similarity index 98% rename from dashscope/audio/asr/recognition.py rename to dashscope/legacy_1x/audio/asr/recognition.py index b9bf978e..0967f549 100644 --- a/dashscope/audio/asr/recognition.py +++ b/dashscope/legacy_1x/audio/asr/recognition.py @@ -12,19 +12,21 @@ from threading import Timer from typing import Any, Dict, List, Optional, Union -from dashscope.api_entities.dashscope_response import RecognitionResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.error import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + RecognitionResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.error import ( InputDataRequired, InputRequired, InvalidParameter, InvalidTask, ModelRequired, ) -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode def _merge_recognition_params( diff --git a/dashscope/audio/asr/transcription.py b/dashscope/legacy_1x/audio/asr/transcription.py similarity index 96% rename from dashscope/audio/asr/transcription.py rename to dashscope/legacy_1x/audio/asr/transcription.py index 88631aca..609f6c11 100644 --- a/dashscope/audio/asr/transcription.py +++ b/dashscope/legacy_1x/audio/asr/transcription.py @@ -6,14 +6,14 @@ import requests -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, TranscriptionResponse, ) -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Transcription(BaseAsyncApi): diff --git a/dashscope/audio/asr/translation_recognizer.py b/dashscope/legacy_1x/audio/asr/translation_recognizer.py similarity index 99% rename from dashscope/audio/asr/translation_recognizer.py rename to dashscope/legacy_1x/audio/asr/translation_recognizer.py index d24d277f..bd32fea9 100644 --- a/dashscope/audio/asr/translation_recognizer.py +++ b/dashscope/legacy_1x/audio/asr/translation_recognizer.py @@ -11,19 +11,19 @@ from threading import Timer from typing import Any, Dict, List, Optional -from dashscope.audio.asr.recognition import _merge_recognition_params -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.error import ( +from dashscope.legacy_1x.audio.asr.recognition import _merge_recognition_params +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.error import ( InputDataRequired, InputRequired, InvalidParameter, InvalidTask, ModelRequired, ) -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode DASHSCOPE_TRANSLATION_KEY = "translations" DASHSCOPE_TRANSCRIPTION_KEY = "transcription" diff --git a/dashscope/audio/asr/vocabulary.py b/dashscope/legacy_1x/audio/asr/vocabulary.py similarity index 97% rename from dashscope/audio/asr/vocabulary.py rename to dashscope/legacy_1x/audio/asr/vocabulary.py index 9e8510b4..9a3111ba 100644 --- a/dashscope/audio/asr/vocabulary.py +++ b/dashscope/legacy_1x/audio/asr/vocabulary.py @@ -6,9 +6,9 @@ import requests -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class VocabularyServiceException(Exception): diff --git a/dashscope/audio/http_tts/__init__.py b/dashscope/legacy_1x/audio/http_tts/__init__.py similarity index 74% rename from dashscope/audio/http_tts/__init__.py rename to dashscope/legacy_1x/audio/http_tts/__init__.py index 9d304d32..d0e451c2 100644 --- a/dashscope/audio/http_tts/__init__.py +++ b/dashscope/legacy_1x/audio/http_tts/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesisResult, HttpSpeechSynthesizer, ) diff --git a/dashscope/audio/http_tts/http_speech_synthesizer.py b/dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py similarity index 95% rename from dashscope/audio/http_tts/http_speech_synthesizer.py rename to dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py index b2e3e84e..ca831955 100644 --- a/dashscope/audio/http_tts/http_speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py @@ -5,12 +5,12 @@ from http import HTTPStatus from typing import Dict, Iterator, List, Optional, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, SpeechSynthesisResponse, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import HTTPMethod +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import HTTPMethod class HttpSpeechSynthesisResult: @@ -199,12 +199,12 @@ def _http_call( url: Optional[str] = None, ): """Make HTTP API call using BaseApi infrastructure.""" - from dashscope.api_entities.http_request import HttpRequest + from dashscope.legacy_1x.api_entities.http_request import HttpRequest # Get base URL import dashscope - from dashscope.common.env import resolve_base_url - from dashscope.common.utils import get_sdk_headers, join_url + from dashscope.legacy_1x.common.env import resolve_base_url + from dashscope.legacy_1x.common.utils import get_sdk_headers, join_url if not url: url = dashscope.base_http_api_url @@ -215,7 +215,7 @@ def _http_call( ) # Get API key - from dashscope.common.api_key import get_default_api_key + from dashscope.legacy_1x.common.api_key import get_default_api_key if api_key is None: api_key = get_default_api_key() diff --git a/dashscope/audio/qwen_asr/__init__.py b/dashscope/legacy_1x/audio/qwen_asr/__init__.py similarity index 100% rename from dashscope/audio/qwen_asr/__init__.py rename to dashscope/legacy_1x/audio/qwen_asr/__init__.py diff --git a/dashscope/audio/qwen_asr/qwen_transcription.py b/dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py similarity index 96% rename from dashscope/audio/qwen_asr/qwen_transcription.py rename to dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py index f731ff92..f32b5f78 100644 --- a/dashscope/audio/qwen_asr/qwen_transcription.py +++ b/dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py @@ -6,13 +6,13 @@ import requests -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, TranscriptionResponse, ) -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class QwenTranscription(BaseAsyncApi): diff --git a/dashscope/audio/qwen_omni/__init__.py b/dashscope/legacy_1x/audio/qwen_omni/__init__.py similarity index 100% rename from dashscope/audio/qwen_omni/__init__.py rename to dashscope/legacy_1x/audio/qwen_omni/__init__.py diff --git a/dashscope/audio/qwen_omni/omni_realtime.py b/dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py similarity index 99% rename from dashscope/audio/qwen_omni/omni_realtime.py rename to dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py index 8f5251a2..ddef0037 100644 --- a/dashscope/audio/qwen_omni/omni_realtime.py +++ b/dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py @@ -12,9 +12,9 @@ import websocket # pylint: disable=wrong-import-order import dashscope -from dashscope.common.error import ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class OmniRealtimeCallback: diff --git a/dashscope/audio/qwen_tts/__init__.py b/dashscope/legacy_1x/audio/qwen_tts/__init__.py similarity index 100% rename from dashscope/audio/qwen_tts/__init__.py rename to dashscope/legacy_1x/audio/qwen_tts/__init__.py diff --git a/dashscope/audio/qwen_tts/speech_synthesizer.py b/dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py similarity index 92% rename from dashscope/audio/qwen_tts/speech_synthesizer.py rename to dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py index f15f7c89..423b3598 100644 --- a/dashscope/audio/qwen_tts/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py @@ -3,9 +3,11 @@ from typing import Generator, Union -from dashscope.api_entities.dashscope_response import TextToSpeechResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.api_entities.dashscope_response import ( + TextToSpeechResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired class SpeechSynthesizer(BaseApi): diff --git a/dashscope/audio/qwen_tts_realtime/__init__.py b/dashscope/legacy_1x/audio/qwen_tts_realtime/__init__.py similarity index 100% rename from dashscope/audio/qwen_tts_realtime/__init__.py rename to dashscope/legacy_1x/audio/qwen_tts_realtime/__init__.py diff --git a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py b/dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py similarity index 98% rename from dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py rename to dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py index 31eb0380..48c67856 100644 --- a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py +++ b/dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py @@ -10,9 +10,9 @@ import websocket # pylint: disable=wrong-import-order import dashscope -from dashscope.common.error import ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class QwenTtsRealtimeCallback: diff --git a/dashscope/audio/tts/__init__.py b/dashscope/legacy_1x/audio/tts/__init__.py similarity index 100% rename from dashscope/audio/tts/__init__.py rename to dashscope/legacy_1x/audio/tts/__init__.py diff --git a/dashscope/audio/tts/speech_synthesizer.py b/dashscope/legacy_1x/audio/tts/speech_synthesizer.py similarity index 96% rename from dashscope/audio/tts/speech_synthesizer.py rename to dashscope/legacy_1x/audio/tts/speech_synthesizer.py index 0381abe8..c3328681 100644 --- a/dashscope/audio/tts/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/tts/speech_synthesizer.py @@ -4,10 +4,12 @@ from http import HTTPStatus from typing import Dict, List -from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + SpeechSynthesisResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class SpeechSynthesisResult: diff --git a/dashscope/audio/tts_v2/__init__.py b/dashscope/legacy_1x/audio/tts_v2/__init__.py similarity index 100% rename from dashscope/audio/tts_v2/__init__.py rename to dashscope/legacy_1x/audio/tts_v2/__init__.py diff --git a/dashscope/audio/tts_v2/enrollment.py b/dashscope/legacy_1x/audio/tts_v2/enrollment.py similarity index 97% rename from dashscope/audio/tts_v2/enrollment.py rename to dashscope/legacy_1x/audio/tts_v2/enrollment.py index 239606a1..073f7086 100644 --- a/dashscope/audio/tts_v2/enrollment.py +++ b/dashscope/legacy_1x/audio/tts_v2/enrollment.py @@ -6,9 +6,9 @@ import requests -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class VoiceEnrollmentException(Exception): diff --git a/dashscope/audio/tts_v2/speech_synthesizer.py b/dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py similarity index 99% rename from dashscope/audio/tts_v2/speech_synthesizer.py rename to dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py index eea32e3f..a96b132f 100644 --- a/dashscope/audio/tts_v2/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py @@ -14,17 +14,17 @@ import websocket import dashscope -from dashscope.common.constants import WEBSOCKET_ERROR_CODE -from dashscope.common.env import resolve_base_url -from dashscope.common.error import ( +from dashscope.legacy_1x.common.constants import WEBSOCKET_ERROR_CODE +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.error import ( InputRequired, InvalidTask, ModelRequired, RequestFailure, ) -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, ERROR_MESSAGE, ERROR_NAME, diff --git a/dashscope/legacy_1x/cli/__init__.py b/dashscope/legacy_1x/cli/__init__.py new file mode 100644 index 00000000..b72c1dde --- /dev/null +++ b/dashscope/legacy_1x/cli/__init__.py @@ -0,0 +1,591 @@ +# -*- coding: utf-8 -*- +# pylint: disable=wrong-import-position +"""DashScope command-line entry point. + +This package is intentionally thin — all command-specific logic lives in +sub-modules (generation, fine_tunes, files, etc.). +""" +import sys +import warnings +from pathlib import Path +from typing import Optional + +# Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL +warnings.filterwarnings( + "ignore", + message=".*urllib3.*only supports OpenSSL.*", + category=Warning, +) + +import typer # noqa: E402 +from rich.markup import escape # noqa: E402 + +import dashscope # noqa: E402 +from dashscope.legacy_1x.cli.common import console, err_console # noqa: E402 +from dashscope.legacy_1x.common.error import AuthenticationError # noqa: E402 +from dashscope.legacy_1x.common.utils import set_sdk_client # noqa: E402 +from dashscope.legacy_1x.cli import ( # noqa: E402 + application, + auth, + deployments, + embeddings, + files, + fine_tunes, + generation, + image_generation, + image_synthesis, + models, + multimodal_conversation, + multimodal_embedding, + oss, + rerank, + speech_synthesis, + tokenization, + transcription, + video_synthesis, +) + +set_sdk_client("python-cli") + + +# --------------------------------------------------------------------------- +# Legacy command compatibility layer +# --------------------------------------------------------------------------- + +# Command name mapping: old -> new +_COMMAND_MAP = { + "fine_tunes.call": "fine-tunes create", + "fine_tunes.get": "fine-tunes get", + "fine_tunes.list": "fine-tunes list", + "fine_tunes.stream": "fine-tunes stream", + "fine_tunes.cancel": "fine-tunes cancel", + "fine_tunes.delete": "fine-tunes delete", + "generation.call": "generation create", + "files.upload": "files upload", + "files.get": "files get", + "files.list": "files list", + "files.delete": "files delete", + "deployments.call": "deployments create", + "deployments.get": "deployments get", + "deployments.list": "deployments list", + "deployments.scale": "deployments scale", + "deployments.delete": "deployments delete", + "oss.upload": "oss upload", +} + +# Parameter name mapping: old -> new (underscore to dash) +_PARAM_MAP = { + "--training_file_ids": "--training-file-ids", + "--validation_file_ids": "--validation-file-ids", + "--n_epochs": "--n-epochs", + "--batch_size": "--batch-size", + "--learning_rate": "--learning-rate", + "--prompt_loss": "--prompt-loss", + "--hyper_parameters": "--hyper-parameters", + "--file_id": "--file-id", + "--deployed_model": "--deployed-model", + "--base_url": "--base-url", + "--api_key": "--api-key", + "--start_page": "--start-page", + "--page_size": "--page-size", +} + +_TOP_LEVEL_COMMANDS = { + "auth", + "generation", + "ft", + "fine-tunes", + "files", + "deployments", + "oss", + "rerank", + "embeddings", + "tokenization", + "models", + "application", + "image-synthesis", + "video-synthesis", + "image-generation", + "multimodal-conversation", + "multimodal-embedding", + "transcription", + "speech-synthesis", + "rl", + "agentic-rl", +} + + +_COMMANDS_WITH_LOCAL_API_KEY = {"oss", "rl", "agentic-rl"} + +# Routing keyword for the bundled agent, handled in main() before typer sees +# argv. Deliberately NOT a member of _TOP_LEVEL_COMMANDS: that set mirrors the +# typer-registered command groups, and `expert` is not one of them. +_EXPERT_COMMAND = "expert" + +_EXPERT_USAGE = """[bold]DashScope SDK Expert[/bold] — the built-in AI assistant. + +[bold]Usage:[/bold] + dashscope interactive assistant (TUI by default) + dashscope expert the same, spelled explicitly + dashscope expert --cli interactive, plain REPL instead of the TUI + dashscope expert --tui interactive, force the TUI + dashscope expert "question" ask once and exit + +With no argument, piped stdin is read as a one-shot prompt. +SDK subcommands (generation, ft, files, models, ...) dispatch as usual; an +unrecognized command is reported as an error rather than treated as a +question, so ask one with [bold]dashscope expert "question"[/bold]. + +Requires [bold]pip install dashscope[acli][/bold].""" + +_LEGACY_COMMANDS_WITH_SIZE_OPTION = { + "files.list", + "fine_tunes.list", + "deployments.list", +} + + +def _translate_param_name(arg): + """Translate legacy underscore option names to Typer hyphen names.""" + if arg in _PARAM_MAP: + return _PARAM_MAP[arg] + if arg.startswith("--") and "=" in arg: + option_name, option_value = arg.split("=", 1) + if option_name in _PARAM_MAP: + return f"{_PARAM_MAP[option_name]}={option_value}" + return arg + + +def _translate_legacy_args(argv): + """Translate legacy argparse command format to Typer format. + + Legacy format: dashscope fine_tunes.call --training_file_ids ... + New format: dashscope fine-tunes create --training-file-ids ... + + Returns modified argv list. + """ + if len(argv) < 2: + return argv + + is_legacy_command = argv[1] in _COMMAND_MAP + if not is_legacy_command: + return [argv[0]] + [_translate_param_name(arg) for arg in argv[1:]] + + command_args = _COMMAND_MAP[argv[1]].split() + top_level_args = [] + translated_args = [] + i = 2 + while i < len(argv): + arg = argv[i] + translated_arg = _translate_param_name(arg) + if argv[1] in _LEGACY_COMMANDS_WITH_SIZE_OPTION: + if translated_arg == "--page-size": + translated_arg = "--size" + elif translated_arg.startswith("--page-size="): + translated_arg = translated_arg.replace( + "--page-size=", + "--size=", + 1, + ) + + if translated_arg == "--api-key": + top_level_args.append("--api-key") + if i + 1 < len(argv): + top_level_args.append(argv[i + 1]) + i += 2 + else: + i += 1 + continue + + if translated_arg.startswith("--api-key="): + top_level_args.append(translated_arg) + i += 1 + continue + + translated_args.append(translated_arg) + i += 1 + + return [argv[0]] + top_level_args + command_args + translated_args + + +def _exit_missing_api_key_value(option_name): + err_console.print( + f"[red]Error:[/red] Option '{option_name}' requires an argument.", + ) + sys.exit(2) + + +def _extract_global_api_key(argv): + """Extract global -k/--api-key from argv and set dashscope.api_key. + + For backward compatibility, global api-key can appear before or after most + command names. Commands that define their own local api-key option are left + untouched once their command name has been reached. + """ + new_argv = [] + i = 0 + current_command = None + while i < len(argv): + arg = argv[i] + + if i > 0 and current_command is None and arg in _TOP_LEVEL_COMMANDS: + current_command = arg + + should_parse_global_api_key = ( + current_command not in _COMMANDS_WITH_LOCAL_API_KEY + ) + + if arg in ("-k", "--api-key"): + if i + 1 >= len(argv): + _exit_missing_api_key_value(arg) + next_arg = argv[i + 1] + if not next_arg or next_arg.startswith("-"): + _exit_missing_api_key_value(arg) + if should_parse_global_api_key: + if next_arg in _TOP_LEVEL_COMMANDS: + _exit_missing_api_key_value(arg) + dashscope.api_key = next_arg + i += 2 + continue + + if arg.startswith("--api-key="): + api_key = arg.split("=", 1)[1] + if not api_key: + _exit_missing_api_key_value("--api-key") + if should_parse_global_api_key: + dashscope.api_key = api_key + i += 1 + continue + + new_argv.append(arg) + i += 1 + + return new_argv + + +def _translate_help_shortcut(argv): + """Translate -h to --help for convenience.""" + return [arg if arg != "-h" else "--help" for arg in argv] + + +_MD_MARKER = "" +_PY_MARKER = "# managed-by: dashscope-expert" +_EXPERT_EXAMPLE = "dashscope-sdk-expert" + + +def _cleanup_legacy_expert_sync(): + """Remove files the deleted expert bundling synced into ~/.acli. + + Only files carrying the managed marker are deleted — without it the file + is user-owned and left untouched. Needed so stale global copies don't + shadow the workspace ./.acli/ configs installed via example download. + """ + try: + from dashscope.acli.config import CONFIG_DIR + except ImportError: + return + md_targets = [] + for subdir in ("skills", "references"): + target_dir = CONFIG_DIR / subdir + if target_dir.is_dir(): + md_targets.extend(target_dir.glob("*.md")) + for path in md_targets: + try: + if _MD_MARKER in path.read_text(encoding="utf-8"): + path.unlink() + except OSError: + pass + build = CONFIG_DIR / "build_sdk_index.py" + try: + if build.is_file() and _PY_MARKER in build.read_text(encoding="utf-8"): + build.unlink() + except OSError: + pass + + +def _maybe_offer_example_download(): + """First interactive run without ./.acli: offer the SDK-expert example. + + Accepting merges the example into ./.acli/ via the same code path as + `example download` (copied from the packaged install dir when present); + declining writes a marker into ./.acli/ so we never ask again here. + """ + if not sys.stdin.isatty() or (Path.cwd() / ".acli").is_dir(): + return + try: + from dashscope.acli.cli.examples import _handle_example_command + from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale + except ImportError: + return + err_console.print( + "[cyan]First run:[/cyan] download the dashscope-sdk-expert " + "example config into ./.acli/\n" + "(SDK Q&A expert persona + skill templates + SDK knowledge " + "index; pure config, editable anytime).", + ) + err_console.print( + f"[dim]Guide: {_GUIDE_DOC.format(_doc_locale())}[/dim]", + ) + try: + answer = input("Download the example config? [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + err_console.print() + return + if answer in ("n", "no"): + try: + marker = Path.cwd() / ".acli" / ".dashscope-example-declined" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text("declined\n", encoding="utf-8") + except OSError: + pass + err_console.print( + "[dim]Skipped. To customize later, run: " + "python -m dashscope.acli example download " + "dashscope-sdk-expert[/dim]", + ) + return + _handle_example_command(["download", _EXPERT_EXAMPLE]) + + +def _route_to_expert(command, tui=None): + """Run the vendored acli agent (bare ``dashscope`` or ``dashscope expert``). + + ``command`` is a one-shot prompt, or None for the interactive REPL. + """ + try: + from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale + from dashscope.acli.ui.embedded import run + except ImportError as exception: + err_console.print( + "[yellow]Tip:[/yellow] Install dashscope[acli] to enable " + "the agent " + f"({exception}).", + ) + return + + if command is None and not sys.stdin.isatty(): + # The interactive REPL needs a real terminal; prompt_toolkit crashes + # on non-tty stdin (e.g. kqueue rejects /dev/null with EINVAL). + # Treat piped text as a one-shot prompt instead. + try: + piped = sys.stdin.read().strip() + except OSError: + piped = "" + if not piped: + err_console.print( + "[red]Error:[/red] Interactive mode requires a terminal. " + 'Pass a prompt instead, e.g. dashscope expert "your question".', + ) + sys.exit(2) + command = piped + + _cleanup_legacy_expert_sync() + if command is None: + _maybe_offer_example_download() + + try: + run( + app_name="DashScope SDK Expert", + prompt_symbol="dashscope> ", + api_key=dashscope.api_key or None, + command=command, + tui=tui, + module="expert", + guide_url=_GUIDE_DOC.format(_doc_locale()), + ) + except SystemExit: + pass + except Exception as exception: # pylint: disable=broad-except + err_console.print( + f"[yellow]Tip:[/yellow] Agent failed to start ({exception}).", + ) + + +# --------------------------------------------------------------------------- +# Typer app +# --------------------------------------------------------------------------- + +app = typer.Typer( + name="dashscope", + help="DashScope command line tools.", + add_completion=True, + no_args_is_help=True, + rich_markup_mode="rich", +) + + +@app.callback() +def callback( + api_key: Optional[str] = typer.Option( + None, + "-k", + "--api-key", + help="DashScope API Key.", + ), +): + """Configure global CLI options.""" + if api_key: + dashscope.api_key = api_key + + +# Register sub-command groups +app.add_typer(auth.app) +app.add_typer(generation.app) +app.add_typer(fine_tunes.app, name="ft") +app.add_typer(fine_tunes.app, name="fine-tunes", hidden=True) +app.add_typer(files.app) +app.add_typer(deployments.app) +app.add_typer(oss.app) +app.add_typer(rerank.app) +app.add_typer(embeddings.app) +app.add_typer(tokenization.app) +app.add_typer(models.app) +app.add_typer(application.app) +app.add_typer(image_synthesis.app) +app.add_typer(video_synthesis.app) +app.add_typer(image_generation.app) +app.add_typer(multimodal_conversation.app) +app.add_typer(multimodal_embedding.app) +app.add_typer(transcription.app) +app.add_typer(speech_synthesis.app) + + +# Records why the Agentic-RL Typer app could not be registered, so the reason +# can be reported when `rl` is actually invoked instead of on every command. +_RL_IMPORT_ERROR: Optional[Exception] = None + +_RL_COMMAND_NAMES = ("rl", "agentic-rl") + + +def _register_rl_app(): + """Lazily import and register the Agentic-RL Typer app. + + Wrapped in a function so that a missing optional dependency + won't crash the entire CLI at import time. + + The failure is recorded rather than reported: `rl` is an optional + extra, so a user running any other command has no reason to be told + about it. ``main()`` surfaces it when the rl command is invoked. + """ + global _RL_IMPORT_ERROR # pylint: disable=global-statement + + try: + from dashscope.legacy_1x.cli.agentic_rl import app as rl_app + + app.add_typer( + rl_app, + name="rl", + help="🚀 Agentic RL fine-tuning commands", + ) + app.add_typer( + rl_app, + name="agentic-rl", + help="🚀 Agentic RL fine-tuning commands", + hidden=True, + ) + except Exception as exception: # pylint: disable=broad-except + _RL_IMPORT_ERROR = exception + + +def _rl_unavailable_detail(exception: Exception) -> str: + """Render a deferred rl import failure as an actionable message.""" + # Unescaped, rich reads a bracketed run in the exception text as a style + # tag and drops it from the printed output. + detail = escape(str(exception)) + if "pip install" not in detail: + detail += ( + ". Reinstall to pull in the missing dependencies: " + "[bold]pip install -U dashscope[/bold]" + ) + return detail + + +_register_rl_app() + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def _first_non_option(argv): + """Return the first non-option argument in *argv* (skip index 0), + or None if all arguments are options.""" + for arg in argv[1:]: + if not arg.startswith("-"): + return arg + return None + + +def main(): + """Entry point for the ``dashscope`` console script.""" + # Translate legacy command format first so legacy --api_key can be treated + # as the global --api-key option. + argv = _translate_legacy_args(sys.argv) + + # Extract global api-key parameter after legacy argument normalization. + argv = _extract_global_api_key(argv) + + # Extract agent mode flags (--cli / --tui); they belong to the vendored + # acli and must not leak into typer or the one-shot prompt text. + # Bare `dashscope` defaults to the TUI; --cli opts into the plain CLI REPL. + forced_tui = True + if "--cli" in argv: + forced_tui = False + argv = [a for a in argv if a != "--cli"] + if "--tui" in argv: + forced_tui = True + argv = [a for a in argv if a != "--tui"] + + first_cmd = _first_non_option(argv) + + # `rl` is an optional extra: report a missing dependency only when the + # user asks for that command, never on unrelated invocations. Without + # this gate the dispatch below reaches typer, which only knows that `rl` + # is in _TOP_LEVEL_COMMANDS but was never registered — "No such command" + # does not tell the user how to get it. + if _RL_IMPORT_ERROR is not None and first_cmd in _RL_COMMAND_NAMES: + err_console.print( + f"[red]Error:[/red] {_rl_unavailable_detail(_RL_IMPORT_ERROR)}", + ) + sys.exit(1) + + # No arguments at all → the agent, interactively. -k/--api-key and + # --cli/--tui were stripped above, so `dashscope -k sk-x` lands here too. + if len(argv) == 1: + _route_to_expert(None, tui=forced_tui) + return + + # `dashscope expert ...` → the agent, explicitly. Handled before the + # --help gate so `dashscope expert --help` describes the agent instead of + # reaching typer, which has no such command. + if first_cmd == _EXPERT_COMMAND: + idx = argv.index(_EXPERT_COMMAND) + expert_args = argv[idx + 1 :] + if "--help" in expert_args or "-h" in expert_args: + console.print(_EXPERT_USAGE) + return + prompt = " ".join(expert_args).strip() + _route_to_expert(prompt or None, tui=forced_tui) + return + + # Top-level --help / -h → show help and exit normally + if "--help" in argv or "-h" in argv: + argv = _translate_help_shortcut(argv) + sys.argv = argv + app() + return + + # Anything that is not bare `dashscope` and not `dashscope expert ...` + # belongs to typer, so a typo or a stray option gets a real error instead + # of silently opening a chat. Forwarding an unrecognized token as a + # question used to swallow whole command lines -- `dashscope auth whoami` + # opened a session whose first message was the command itself. + argv = _translate_help_shortcut(argv) + sys.argv = argv + + try: + app() + except AuthenticationError as exception: + err_console.print(f"[red]Error:[/red] {exception}") + sys.exit(1) diff --git a/dashscope/cli/__main__.py b/dashscope/legacy_1x/cli/__main__.py similarity index 70% rename from dashscope/cli/__main__.py rename to dashscope/legacy_1x/cli/__main__.py index 9f75714a..f4406ab1 100644 --- a/dashscope/cli/__main__.py +++ b/dashscope/legacy_1x/cli/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable=wrong-import-position -"""Allow ``python -m dashscope.cli`` invocation.""" +"""Allow ``python -m dashscope.legacy_1x.cli`` invocation.""" import warnings # Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL @@ -11,7 +11,7 @@ ) # fmt: off -from dashscope.cli import main # noqa: E402 +from dashscope.legacy_1x.cli import main # noqa: E402 # fmt: on main() diff --git a/dashscope/cli/agentic_rl.py b/dashscope/legacy_1x/cli/agentic_rl.py similarity index 99% rename from dashscope/cli/agentic_rl.py rename to dashscope/legacy_1x/cli/agentic_rl.py index f850f718..f6b8fbdd 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/legacy_1x/cli/agentic_rl.py @@ -17,14 +17,13 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table -from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.finetune.agentic_rl import AgenticRL, SubmitResult from dashscope.finetune.reinforcement import ( serialize_for_output, FunctionType, ) from dashscope.finetune.reinforcement.common.errors import OutputError -from dashscope.finetune.customize_types import FineTune -from dashscope.cli.common import error, normalize_local_path_or_url +from dashscope.legacy_1x.cli.common import error, normalize_local_path_or_url app = typer.Typer( @@ -427,7 +426,7 @@ async def _run_workflow_async( config_path: Optional[str], api_key: Optional[str], run_kwargs: Dict[str, Any], -) -> FineTune: +) -> SubmitResult: """ Execute the RL tuning workflow asynchronously. diff --git a/dashscope/cli/application.py b/dashscope/legacy_1x/cli/application.py similarity index 96% rename from dashscope/cli/application.py rename to dashscope/legacy_1x/cli/application.py index a2ef3e72..3c834619 100644 --- a/dashscope/cli/application.py +++ b/dashscope/legacy_1x/cli/application.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="application", diff --git a/dashscope/cli/auth.py b/dashscope/legacy_1x/cli/auth.py similarity index 91% rename from dashscope/cli/auth.py rename to dashscope/legacy_1x/cli/auth.py index 89aff97c..f2144f8f 100644 --- a/dashscope/cli/auth.py +++ b/dashscope/legacy_1x/cli/auth.py @@ -6,10 +6,15 @@ import typer import dashscope -from dashscope.cli.common import console, err_console -from dashscope.common.api_key import get_default_api_key, save_api_key -from dashscope.common.constants import DEFAULT_DASHSCOPE_API_KEY_FILE_PATH -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.cli.common import console, err_console +from dashscope.legacy_1x.common.api_key import ( + get_default_api_key, + save_api_key, +) +from dashscope.legacy_1x.common.constants import ( + DEFAULT_DASHSCOPE_API_KEY_FILE_PATH, +) +from dashscope.legacy_1x.common.error import AuthenticationError app = typer.Typer( name="auth", diff --git a/dashscope/cli/code_generation.py b/dashscope/legacy_1x/cli/code_generation.py similarity index 93% rename from dashscope/cli/code_generation.py rename to dashscope/legacy_1x/cli/code_generation.py index 414941fd..5366e451 100644 --- a/dashscope/cli/code_generation.py +++ b/dashscope/legacy_1x/cli/code_generation.py @@ -6,11 +6,16 @@ import typer import dashscope -from dashscope.aigc.code_generation import ( +from dashscope.legacy_1x.aigc.code_generation import ( AttachmentRoleMessageParam, UserRoleMessageParam, ) -from dashscope.cli.common import console, ensure_ok, error, handle_sdk_error +from dashscope.legacy_1x.cli.common import ( + console, + ensure_ok, + error, + handle_sdk_error, +) app = typer.Typer( name="code-generation", diff --git a/dashscope/cli/common.py b/dashscope/legacy_1x/cli/common.py similarity index 99% rename from dashscope/cli/common.py rename to dashscope/legacy_1x/cli/common.py index 68f0eea4..b05d0504 100644 --- a/dashscope/cli/common.py +++ b/dashscope/legacy_1x/cli/common.py @@ -10,7 +10,7 @@ import typer from rich.console import Console -logger = logging.getLogger("dashscope.cli") +logger = logging.getLogger("dashscope.legacy_1x.cli") CommandFunction = TypeVar("CommandFunction", bound=Callable) # --------------------------------------------------------------------------- diff --git a/dashscope/cli/deployments.py b/dashscope/legacy_1x/cli/deployments.py similarity index 98% rename from dashscope/cli/deployments.py rename to dashscope/legacy_1x/cli/deployments.py index 6fffe1ec..c3bb664e 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/legacy_1x/cli/deployments.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import DeploymentStatus -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import DeploymentStatus +from dashscope.legacy_1x.cli.common import ( POLL_INTERVAL, console, err_console, diff --git a/dashscope/cli/embeddings.py b/dashscope/legacy_1x/cli/embeddings.py similarity index 96% rename from dashscope/cli/embeddings.py rename to dashscope/legacy_1x/cli/embeddings.py index ae61ccab..392614dc 100644 --- a/dashscope/cli/embeddings.py +++ b/dashscope/legacy_1x/cli/embeddings.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="embeddings", diff --git a/dashscope/cli/files.py b/dashscope/legacy_1x/cli/files.py similarity index 97% rename from dashscope/cli/files.py rename to dashscope/legacy_1x/cli/files.py index cb25a710..8b5f25aa 100644 --- a/dashscope/cli/files.py +++ b/dashscope/legacy_1x/cli/files.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import FilePurpose -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import FilePurpose +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, error, diff --git a/dashscope/cli/fine_tunes.py b/dashscope/legacy_1x/cli/fine_tunes.py similarity index 99% rename from dashscope/cli/fine_tunes.py rename to dashscope/legacy_1x/cli/fine_tunes.py index d5317527..2694b654 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/legacy_1x/cli/fine_tunes.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import TaskStatus -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import TaskStatus +from dashscope.legacy_1x.cli.common import ( LOG_PAGE_SIZE, POLL_INTERVAL, console, diff --git a/dashscope/cli/generation.py b/dashscope/legacy_1x/cli/generation.py similarity index 98% rename from dashscope/cli/generation.py rename to dashscope/legacy_1x/cli/generation.py index 2305dae7..9b7e6677 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/legacy_1x/cli/generation.py @@ -7,8 +7,8 @@ import typer -from dashscope.aigc import Generation -from dashscope.cli.common import ( +from dashscope.legacy_1x.aigc import Generation +from dashscope.legacy_1x.cli.common import ( error, extract_text, handle_sdk_error, diff --git a/dashscope/cli/image_generation.py b/dashscope/legacy_1x/cli/image_generation.py similarity index 96% rename from dashscope/cli/image_generation.py rename to dashscope/legacy_1x/cli/image_generation.py index a2efb627..7ae850dc 100644 --- a/dashscope/cli/image_generation.py +++ b/dashscope/legacy_1x/cli/image_generation.py @@ -5,9 +5,9 @@ import typer -from dashscope.aigc.image_generation import ImageGeneration -from dashscope.api_entities.dashscope_response import Message, Role -from dashscope.cli.common import ( +from dashscope.legacy_1x.aigc.image_generation import ImageGeneration +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/image_synthesis.py b/dashscope/legacy_1x/cli/image_synthesis.py similarity index 99% rename from dashscope/cli/image_synthesis.py rename to dashscope/legacy_1x/cli/image_synthesis.py index 63e393cb..37754ca1 100644 --- a/dashscope/cli/image_synthesis.py +++ b/dashscope/legacy_1x/cli/image_synthesis.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/models.py b/dashscope/legacy_1x/cli/models.py similarity index 94% rename from dashscope/cli/models.py rename to dashscope/legacy_1x/cli/models.py index dc0dc747..f4bc96b8 100644 --- a/dashscope/cli/models.py +++ b/dashscope/legacy_1x/cli/models.py @@ -5,7 +5,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="models", diff --git a/dashscope/cli/multimodal_conversation.py b/dashscope/legacy_1x/cli/multimodal_conversation.py similarity index 98% rename from dashscope/cli/multimodal_conversation.py rename to dashscope/legacy_1x/cli/multimodal_conversation.py index a1133993..1031db24 100644 --- a/dashscope/cli/multimodal_conversation.py +++ b/dashscope/legacy_1x/cli/multimodal_conversation.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/multimodal_embedding.py b/dashscope/legacy_1x/cli/multimodal_embedding.py similarity index 98% rename from dashscope/cli/multimodal_embedding.py rename to dashscope/legacy_1x/cli/multimodal_embedding.py index ea4db7b8..c48299e5 100644 --- a/dashscope/cli/multimodal_embedding.py +++ b/dashscope/legacy_1x/cli/multimodal_embedding.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, error, diff --git a/dashscope/cli/oss.py b/dashscope/legacy_1x/cli/oss.py similarity index 92% rename from dashscope/cli/oss.py rename to dashscope/legacy_1x/cli/oss.py index 80bb815b..e6e52bf2 100644 --- a/dashscope/cli/oss.py +++ b/dashscope/legacy_1x/cli/oss.py @@ -6,8 +6,13 @@ import typer import dashscope -from dashscope.utils.oss_utils import OssUtils -from dashscope.cli.common import console, error, handle_sdk_error, success +from dashscope.legacy_1x.utils.oss_utils import OssUtils +from dashscope.legacy_1x.cli.common import ( + console, + error, + handle_sdk_error, + success, +) app = typer.Typer( name="oss", diff --git a/dashscope/cli/rerank.py b/dashscope/legacy_1x/cli/rerank.py similarity index 95% rename from dashscope/cli/rerank.py rename to dashscope/legacy_1x/cli/rerank.py index 07ea0307..f61f4814 100644 --- a/dashscope/cli/rerank.py +++ b/dashscope/legacy_1x/cli/rerank.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="rerank", diff --git a/dashscope/cli/speech_synthesis.py b/dashscope/legacy_1x/cli/speech_synthesis.py similarity index 98% rename from dashscope/cli/speech_synthesis.py rename to dashscope/legacy_1x/cli/speech_synthesis.py index 6978c394..0fba7bd2 100644 --- a/dashscope/cli/speech_synthesis.py +++ b/dashscope/legacy_1x/cli/speech_synthesis.py @@ -7,7 +7,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, error, handle_sdk_error, diff --git a/dashscope/cli/tokenization.py b/dashscope/legacy_1x/cli/tokenization.py similarity index 94% rename from dashscope/cli/tokenization.py rename to dashscope/legacy_1x/cli/tokenization.py index a869d01a..6e212fa7 100644 --- a/dashscope/cli/tokenization.py +++ b/dashscope/legacy_1x/cli/tokenization.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="tokenization", diff --git a/dashscope/cli/transcription.py b/dashscope/legacy_1x/cli/transcription.py similarity index 99% rename from dashscope/cli/transcription.py rename to dashscope/legacy_1x/cli/transcription.py index af25d27e..098098a0 100644 --- a/dashscope/cli/transcription.py +++ b/dashscope/legacy_1x/cli/transcription.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/understanding.py b/dashscope/legacy_1x/cli/understanding.py similarity index 95% rename from dashscope/cli/understanding.py rename to dashscope/legacy_1x/cli/understanding.py index 30fdfca2..f430e866 100644 --- a/dashscope/cli/understanding.py +++ b/dashscope/legacy_1x/cli/understanding.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="understanding", diff --git a/dashscope/cli/video_synthesis.py b/dashscope/legacy_1x/cli/video_synthesis.py similarity index 98% rename from dashscope/cli/video_synthesis.py rename to dashscope/legacy_1x/cli/video_synthesis.py index 8c46492f..39455a50 100644 --- a/dashscope/cli/video_synthesis.py +++ b/dashscope/legacy_1x/cli/video_synthesis.py @@ -6,7 +6,12 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error, success +from dashscope.legacy_1x.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + success, +) app = typer.Typer( name="video-synthesis", diff --git a/dashscope/finetune/reinforcement/component/server/__init__.py b/dashscope/legacy_1x/client/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/component/server/__init__.py rename to dashscope/legacy_1x/client/__init__.py diff --git a/dashscope/client/base_api.py b/dashscope/legacy_1x/client/base_api.py similarity index 98% rename from dashscope/client/base_api.py rename to dashscope/legacy_1x/client/base_api.py index 8ae18173..deb1ba81 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/legacy_1x/client/base_api.py @@ -10,10 +10,14 @@ import requests import dashscope -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.api_key import get_default_api_key -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, REPEATABLE_STATUS, REQUEST_TIMEOUT_KEYWORD, @@ -21,10 +25,14 @@ TaskStatus, HTTPMethod, ) -from dashscope.common.error import InvalidParameter, InvalidTask, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.env import resolve_base_url -from dashscope.common.utils import ( +from dashscope.legacy_1x.common.error import ( + InvalidParameter, + InvalidTask, + ModelRequired, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.utils import ( _handle_http_failed_response, _handle_http_response, _handle_http_stream_response, @@ -411,7 +419,7 @@ async def list( message="", ) else: - from dashscope.common.utils import ( # pylint: disable=import-outside-toplevel # noqa: E501 + from dashscope.legacy_1x.common.utils import ( # pylint: disable=import-outside-toplevel # noqa: E501 _handle_aiohttp_failed_response, ) diff --git a/dashscope/finetune/reinforcement/examples/observability/__init__.py b/dashscope/legacy_1x/common/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/observability/__init__.py rename to dashscope/legacy_1x/common/__init__.py diff --git a/dashscope/common/_sdk_session.py b/dashscope/legacy_1x/common/_sdk_session.py similarity index 100% rename from dashscope/common/_sdk_session.py rename to dashscope/legacy_1x/common/_sdk_session.py diff --git a/dashscope/common/api_key.py b/dashscope/legacy_1x/common/api_key.py similarity index 95% rename from dashscope/common/api_key.py rename to dashscope/legacy_1x/common/api_key.py index d083e849..74bc785f 100644 --- a/dashscope/common/api_key.py +++ b/dashscope/legacy_1x/common/api_key.py @@ -5,11 +5,11 @@ from typing import Optional import dashscope -from dashscope.common.constants import ( +from dashscope.legacy_1x.common.constants import ( DEFAULT_DASHSCOPE_API_KEY_FILE_PATH, DEFAULT_DASHSCOPE_CACHE_PATH, ) -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.common.error import AuthenticationError def get_default_api_key(): diff --git a/dashscope/common/base_type.py b/dashscope/legacy_1x/common/base_type.py similarity index 100% rename from dashscope/common/base_type.py rename to dashscope/legacy_1x/common/base_type.py diff --git a/dashscope/common/constants.py b/dashscope/legacy_1x/common/constants.py similarity index 100% rename from dashscope/common/constants.py rename to dashscope/legacy_1x/common/constants.py diff --git a/dashscope/common/env.py b/dashscope/legacy_1x/common/env.py similarity index 62% rename from dashscope/common/env.py rename to dashscope/legacy_1x/common/env.py index 330baca9..df8afa0c 100644 --- a/dashscope/common/env.py +++ b/dashscope/legacy_1x/common/env.py @@ -2,64 +2,21 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import os -import re -from dashscope.common.constants import ( +# Re-exported for callers that import them from here; ``validate_region`` +# is not used in this module itself. +# pylint: disable-next=unused-import +from dashscope._shared._validation import ( # noqa: F401 + validate_region, + validate_workspace_id, +) +from dashscope.legacy_1x.common.constants import ( DASHSCOPE_API_KEY_ENV, DASHSCOPE_API_KEY_FILE_PATH_ENV, DASHSCOPE_API_REGION_ENV, DASHSCOPE_API_VERSION_ENV, ) -from dashscope.common.error import InputRequired - -# workspace_id and region become DNS labels of the MaaS endpoint host, so -# they must not contain characters (/, ?, #, @, :, whitespace, ...) that -# could break out of the host and redirect requests elsewhere. -# \A...\Z rather than ^...$: $ also matches before a trailing newline. -_WORKSPACE_ID_PATTERN = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\Z") -# RFC 1123 hostname label: 1-63 chars, no leading/trailing hyphen. Upper -# case is allowed because DNS is case-insensitive and httpx lowercases the -# host, so rejecting it would break working callers for no security gain. -_REGION_PATTERN = re.compile( - r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z", -) - - -def validate_workspace_id(workspace_id): - """Validate that workspace_id is safe to use as a URL subdomain. - - Raises: - ValueError: If workspace_id contains characters outside - [A-Za-z0-9_-], is empty, or exceeds 64 characters. - """ - if not workspace_id or not _WORKSPACE_ID_PATTERN.fullmatch( - workspace_id, - ): - raise ValueError( - f"Invalid workspace_id {workspace_id!r}: must match " - "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$", - ) - return workspace_id - - -def validate_region(region): - """Validate that region is safe to use as a URL subdomain. - - Deliberately a hostname-label check and not a MAAS_REGIONS whitelist: - rejecting an unknown-but-well-formed region would break callers as soon - as a subpackage supports a region this list has not caught up with. - - Raises: - ValueError: If region is not a valid hostname label. - """ - if not region or not _REGION_PATTERN.fullmatch(region): - raise ValueError( - f"Invalid region {region!r}: must be a hostname label " - "(1-63 chars of [A-Za-z0-9-], not starting or ending " - "with '-')", - ) - return region - +from dashscope.legacy_1x.common.error import InputRequired # MaaS regions: region -> URL subdomain identifier # cn-beijing is deliberately excluded: it is the default diff --git a/dashscope/common/error.py b/dashscope/legacy_1x/common/error.py similarity index 92% rename from dashscope/common/error.py rename to dashscope/legacy_1x/common/error.py index 2b49cc71..73efc01e 100644 --- a/dashscope/common/error.py +++ b/dashscope/legacy_1x/common/error.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. - -class DashScopeException(Exception): - pass +# The base class lives in the neutral layer so the AgentStudio sub-SDK +# (owned by the 2.0 package) and the 1.x classes below inherit from one +# and the same object. +from dashscope._shared._errors import DashScopeException class AuthenticationError(DashScopeException): diff --git a/dashscope/common/logging.py b/dashscope/legacy_1x/common/logging.py similarity index 93% rename from dashscope/common/logging.py rename to dashscope/legacy_1x/common/logging.py index a5ca4abb..f345e053 100644 --- a/dashscope/common/logging.py +++ b/dashscope/legacy_1x/common/logging.py @@ -4,7 +4,7 @@ import logging import os -from dashscope.common.constants import DASHSCOPE_LOGGING_LEVEL_ENV +from dashscope.legacy_1x.common.constants import DASHSCOPE_LOGGING_LEVEL_ENV logger = logging.getLogger("dashscope") diff --git a/dashscope/common/message_manager.py b/dashscope/legacy_1x/common/message_manager.py similarity index 95% rename from dashscope/common/message_manager.py rename to dashscope/legacy_1x/common/message_manager.py index 9b546b9e..1fc34617 100644 --- a/dashscope/common/message_manager.py +++ b/dashscope/legacy_1x/common/message_manager.py @@ -4,7 +4,7 @@ from collections import deque from typing import List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( ConversationResponse, GenerationResponse, Message, diff --git a/dashscope/common/utils.py b/dashscope/legacy_1x/common/utils.py similarity index 88% rename from dashscope/common/utils.py rename to dashscope/legacy_1x/common/utils.py index 28de30cf..732e0971 100644 --- a/dashscope/common/utils.py +++ b/dashscope/legacy_1x/common/utils.py @@ -4,7 +4,6 @@ import asyncio import json import os -import platform import queue import threading from dataclasses import dataclass @@ -15,17 +14,23 @@ import aiohttp import requests -from dashscope._shared._sdk_session import sdk_session_id -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.api_key import get_default_api_key -from dashscope.common.constants import SSE_CONTENT_TYPE -from dashscope.common.logging import logger -from dashscope.version import __version__ - -# Shared with the 2.0 transport so one process reports one -# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session; -# acli deliberately keeps its own, it is a one-way sync copy). -_SDK_SESSION_ID = sdk_session_id() +# The x-dashscope-sdk-client state (including the CLI's ``python-cli`` +# override), the request-identity builders and the session id live in the +# neutral layer so both stacks share one copy; re-exported here for +# callers that import them from this module. +# pylint: disable-next=unused-import +from dashscope._shared._sdk_ident import ( # noqa: F401 + _SDK_SESSION_ID, + get_sdk_headers, + get_user_agent, + set_sdk_client, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import SSE_CONTENT_TYPE +from dashscope.legacy_1x.common.logging import logger def is_validate_fine_tune_file(file_path): @@ -143,31 +148,6 @@ def async_to_sync(async_generator): yield message -def get_user_agent(): - try: - platform_ = ( - platform.platform().replace("\n", "").replace("\r", "").strip() - ) - except Exception: - platform_ = "unknown" - - try: - processor_ = ( - platform.processor().replace("\n", "").replace("\r", "").strip() - ) - except Exception: - processor_ = "unknown" - - ua = ( - f"dashscope/{__version__}; python/{platform.python_version()}; " - f"platform/{platform_}; " - f"processor/{processor_}" - ) - return ua - - -_SDK_CLIENT = "python-sdk" - # Package name -> sdk module tag, for packages whose directory name does # not match the module segment (threads/messages/runs/steps belong to the # assistants module). @@ -179,46 +159,21 @@ def get_user_agent(): def get_api_module(module_name: str) -> str: """Derive the sdk module tag from an API class's __module__. - e.g. "dashscope.aigc.generation" -> "aigc", - "dashscope.threads.runs.runs" -> "assistants". + e.g. "dashscope.legacy_1x.aigc.generation" -> "aigc", + "dashscope.legacy_1x.threads.runs.runs" -> "assistants". """ parts = module_name.split(".") if len(parts) < 2 or parts[0] != "dashscope": return "" - pkg = parts[1] + # The 1.x tree now lives under ``dashscope.legacy_1x``; the logical API + # package is the segment after that container, not ``legacy_1x`` itself. + idx = 2 if parts[1] == "legacy_1x" else 1 + if len(parts) <= idx: + return "" + pkg = parts[idx] return _SDK_MODULE_PACKAGE_OVERRIDES.get(pkg, pkg) -def set_sdk_client(client: str) -> None: - """Override the client identifier in the x-dashscope-sdk-client header. - - Process-wide, used by entry points that embed the SDK (e.g. the - ``dashscope`` CLI marks itself as ``python-cli``). - """ - global _SDK_CLIENT # pylint: disable=global-statement - if client: - _SDK_CLIENT = client - - -def get_sdk_headers(module: str = "") -> Dict[str, str]: - """Structured SDK identification headers for backend statistics. - - Set DASHSCOPE_DISABLE_SDK_HEADERS=1 to omit them. - - Args: - module: Optional module name (e.g. agentstudio, rl, app). - """ - if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): - return {} - parts = [_SDK_CLIENT, __version__] - if module: - parts.append(module) - return { - "x-dashscope-sdk-client": "/".join(parts), - "x-dashscope-sdk-session-id": _SDK_SESSION_ID, - } - - def default_headers(api_key: str = None, module: str = "") -> Dict[str, str]: ua = get_user_agent() headers = {"user-agent": ua} diff --git a/dashscope/legacy_1x/embeddings/__init__.py b/dashscope/legacy_1x/embeddings/__init__.py new file mode 100644 index 00000000..998f1a1f --- /dev/null +++ b/dashscope/legacy_1x/embeddings/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .batch_text_embedding import BatchTextEmbedding +from .batch_text_embedding_response import BatchTextEmbeddingResponse +from .text_embedding import TextEmbedding + +__all__ = [ + "TextEmbedding", + "BatchTextEmbedding", + "BatchTextEmbeddingResponse", +] diff --git a/dashscope/embeddings/batch_text_embedding.py b/dashscope/legacy_1x/embeddings/batch_text_embedding.py similarity index 95% rename from dashscope/embeddings/batch_text_embedding.py rename to dashscope/legacy_1x/embeddings/batch_text_embedding.py index c2c9b9cb..d2ef1832 100644 --- a/dashscope/embeddings/batch_text_embedding.py +++ b/dashscope/legacy_1x/embeddings/batch_text_embedding.py @@ -3,11 +3,13 @@ from typing import Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.error import InputRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.embeddings.batch_text_embedding_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.embeddings.batch_text_embedding_response import ( BatchTextEmbeddingResponse, ) diff --git a/dashscope/embeddings/batch_text_embedding_response.py b/dashscope/legacy_1x/embeddings/batch_text_embedding_response.py similarity index 96% rename from dashscope/embeddings/batch_text_embedding_response.py rename to dashscope/legacy_1x/embeddings/batch_text_embedding_response.py index 405ee102..762653d9 100644 --- a/dashscope/embeddings/batch_text_embedding_response.py +++ b/dashscope/legacy_1x/embeddings/batch_text_embedding_response.py @@ -5,7 +5,7 @@ from attr import dataclass -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) diff --git a/dashscope/embeddings/multimodal_embedding.py b/dashscope/legacy_1x/embeddings/multimodal_embedding.py similarity index 96% rename from dashscope/embeddings/multimodal_embedding.py rename to dashscope/legacy_1x/embeddings/multimodal_embedding.py index d68c13f5..846a31bd 100644 --- a/dashscope/embeddings/multimodal_embedding.py +++ b/dashscope/legacy_1x/embeddings/multimodal_embedding.py @@ -4,14 +4,14 @@ from dataclasses import dataclass from typing import List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) -from dashscope.client.base_api import BaseApi, BaseAioApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.client.base_api import BaseApi, BaseAioApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element @dataclass(init=False) diff --git a/dashscope/embeddings/text_embedding.py b/dashscope/legacy_1x/embeddings/text_embedding.py similarity index 90% rename from dashscope/embeddings/text_embedding.py rename to dashscope/legacy_1x/embeddings/text_embedding.py index b388bad2..9835e194 100644 --- a/dashscope/embeddings/text_embedding.py +++ b/dashscope/legacy_1x/embeddings/text_embedding.py @@ -3,10 +3,12 @@ from typing import List, Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import TEXT_EMBEDDING_INPUT_KEY -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import TEXT_EMBEDDING_INPUT_KEY +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class TextEmbedding(BaseApi): diff --git a/dashscope/legacy_1x/files.py b/dashscope/legacy_1x/files.py new file mode 100644 index 00000000..c3e908c8 --- /dev/null +++ b/dashscope/legacy_1x/files.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import os +from typing import Optional + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import ( + DeleteMixin, + FileUploadMixin, + GetMixin, + ListMixin, +) +from dashscope.legacy_1x.common.constants import FilePurpose +from dashscope.legacy_1x.common.error import InvalidFileFormat +from dashscope.legacy_1x.common.utils import is_validate_fine_tune_file + + +class Files(FileUploadMixin, ListMixin, DeleteMixin, GetMixin): + SUB_PATH = "files" + + @classmethod + def upload( # pylint: disable=arguments-renamed + cls, + file_path: str, # type: ignore[override] + purpose: str = FilePurpose.fine_tune, # type: ignore[override] + description: Optional[str] = None, # type: ignore[override] + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Upload file for model fine-tune or other tasks. + + Args: + file_path (str): The local file name to upload. + purpose (str): The purpose of the file[fine-tune|inference] + description (str, optional): The file description message. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The upload information + """ + if purpose == FilePurpose.fine_tune: + if not is_validate_fine_tune_file(file_path): + raise InvalidFileFormat( + f"The file {file_path} is not in valid jsonl format", + ) + with open(file_path, "rb") as f: + return super().upload( # type: ignore[return-value] + files=[ + ( + "files", + ( + os.path.basename(f.name), + f, + None, + ), + ), + ], + descriptions=[description] if description is not None else [], + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List uploaded files. + + Args: + api_key (str, optional): + The api api_key, can be None, + if None, will get by default rule(TODO: api key doc). + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The fine-tune jobs in the result. + """ + return super().list( # type: ignore[return-value] + page, + page_size, + api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def get( # type: ignore[override] + cls, + file_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the file info. + + Args: + file_id (str): The file id. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The job info + """ + # type: ignore + return super().get(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 + + @classmethod + def delete( # type: ignore[override] + cls, + file_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Delete uploaded file. + + Args: + file_id (str): The file id want to delete. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: Delete result. + """ # type: ignore + return super().delete(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/legacy_1x/io/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py rename to dashscope/legacy_1x/io/__init__.py diff --git a/dashscope/io/input_output.py b/dashscope/legacy_1x/io/input_output.py similarity index 98% rename from dashscope/io/input_output.py rename to dashscope/legacy_1x/io/input_output.py index ea6caf27..688a5916 100644 --- a/dashscope/io/input_output.py +++ b/dashscope/legacy_1x/io/input_output.py @@ -5,7 +5,7 @@ import io from typing import Generator -from dashscope.common.error import UnsupportedDataType +from dashscope.legacy_1x.common.error import UnsupportedDataType class InputResolver: diff --git a/dashscope/legacy_1x/model.py b/dashscope/legacy_1x/model.py new file mode 100644 index 00000000..2e5a2bdf --- /dev/null +++ b/dashscope/legacy_1x/model.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin, ListMixin + + +class Model(ListMixin, GetMixin): + SUB_PATH = "models" + + @classmethod + def get( # type: ignore[override] + cls, + name: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the model information. + + Args: + name (str): The model name. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The model information. + """ + # type: ignore + return super().get( # type: ignore[return-value] + name, + api_key, + workspace=workspace, + **kwargs, + ) # noqa: E501 + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List models. + + Args: + api_key (str, optional): The api key + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + + Returns: + DashScopeAPIResponse: The models. + """ + return super().list( # type: ignore[return-value] + api_key, + page, + page_size, + workspace=workspace, + **kwargs, + ) diff --git a/dashscope/legacy_1x/models.py b/dashscope/legacy_1x/models.py new file mode 100644 index 00000000..300a4931 --- /dev/null +++ b/dashscope/legacy_1x/models.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin, ListMixin, _get +from dashscope.legacy_1x.common.utils import get_api_module, join_url +import dashscope + + +class Models(ListMixin, GetMixin): + SUB_PATH = "models" + + @classmethod + def get( # type: ignore[override] + cls, + name: str, + api_key: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the model information. + + Args: + name (str): The model name. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The model information. + """ + from http import HTTPStatus + + # Use query parameter to filter by model name on server side + # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 + url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) + params = {"model": name, "page_no": 1, "page_size": 1} + + response = _get( + url, + params=params, + api_key=api_key, + module=get_api_module(cls.__module__), + **kwargs, + ) + + if response.status_code != HTTPStatus.OK: + return response # type: ignore[return-value] + + output = response.output + if not output or "models" not in output or not output["models"]: + response.status_code = 404 + response.message = f"Model '{name}' not found" + response.output = None + return response # type: ignore[return-value] + + # Return the first (and only) model from the filtered list + response.output = output["models"][0] + return response # type: ignore[return-value] + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List models. + + Args: + api_key (str, optional): The api key + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + + Returns: + DashScopeAPIResponse: The models. + """ + # type: ignore + return super().list(page, page_size, api_key=api_key, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 diff --git a/dashscope/legacy_1x/multimodal/__init__.py b/dashscope/legacy_1x/multimodal/__init__.py new file mode 100644 index 00000000..540bb57a --- /dev/null +++ b/dashscope/legacy_1x/multimodal/__init__.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .tingwu import tingwu +from .tingwu.tingwu import TingWu +from .tingwu.tingwu_realtime import TingWuRealtime, TingWuRealtimeCallback + +from .multimodal_dialog import MultiModalDialog, MultiModalCallback +from .dialog_state import DialogState +from .multimodal_constants import ( + RequestToRespondType, + RESPONSE_NAME_TASK_STARTED, + RESPONSE_NAME_RESULT_GENERATED, + RESPONSE_NAME_TASK_FINISHED, + RESPONSE_NAME_TASK_FAILED, + RESPONSE_NAME_STARTED, + RESPONSE_NAME_STOPPED, + RESPONSE_NAME_STATE_CHANGED, + RESPONSE_NAME_REQUEST_ACCEPTED, + RESPONSE_NAME_SPEECH_STARTED, + RESPONSE_NAME_SPEECH_ENDED, + RESPONSE_NAME_RESPONDING_STARTED, + RESPONSE_NAME_RESPONDING_ENDED, + RESPONSE_NAME_SPEECH_CONTENT, + RESPONSE_NAME_RESPONDING_CONTENT, + RESPONSE_NAME_ERROR, + RESPONSE_NAME_HEART_BEAT, +) +from .multimodal_request_params import ( + DashHeader, + DashPayloadParameters, + DashPayloadInput, + DashPayload, + RequestBodyInput, + AsrPostProcessing, + ReplaceWord, + Upstream, + Downstream, + DialogAttributes, + Locations, + Network, + Device, + ClientInfo, + BizParams, + RequestParameters, + RequestToRespondParameters, + RequestToRespondBodyInput, + get_random_uuid, +) + +__all__ = [ + "tingwu", + "TingWu", + "TingWuRealtime", + "TingWuRealtimeCallback", + "MultiModalDialog", + "MultiModalCallback", + "DialogState", + "RequestToRespondType", + "RESPONSE_NAME_TASK_STARTED", + "RESPONSE_NAME_RESULT_GENERATED", + "RESPONSE_NAME_TASK_FINISHED", + "RESPONSE_NAME_TASK_FAILED", + "RESPONSE_NAME_STARTED", + "RESPONSE_NAME_STOPPED", + "RESPONSE_NAME_STATE_CHANGED", + "RESPONSE_NAME_REQUEST_ACCEPTED", + "RESPONSE_NAME_SPEECH_STARTED", + "RESPONSE_NAME_SPEECH_ENDED", + "RESPONSE_NAME_RESPONDING_STARTED", + "RESPONSE_NAME_RESPONDING_ENDED", + "RESPONSE_NAME_SPEECH_CONTENT", + "RESPONSE_NAME_RESPONDING_CONTENT", + "RESPONSE_NAME_ERROR", + "RESPONSE_NAME_HEART_BEAT", + "DashHeader", + "DashPayloadParameters", + "DashPayloadInput", + "DashPayload", + "RequestBodyInput", + "AsrPostProcessing", + "ReplaceWord", + "Upstream", + "Downstream", + "DialogAttributes", + "Locations", + "Network", + "Device", + "ClientInfo", + "BizParams", + "RequestParameters", + "RequestToRespondParameters", + "RequestToRespondBodyInput", + "get_random_uuid", +] diff --git a/dashscope/multimodal/dialog_state.py b/dashscope/legacy_1x/multimodal/dialog_state.py similarity index 100% rename from dashscope/multimodal/dialog_state.py rename to dashscope/legacy_1x/multimodal/dialog_state.py diff --git a/dashscope/multimodal/multimodal_constants.py b/dashscope/legacy_1x/multimodal/multimodal_constants.py similarity index 100% rename from dashscope/multimodal/multimodal_constants.py rename to dashscope/legacy_1x/multimodal/multimodal_constants.py diff --git a/dashscope/multimodal/multimodal_dialog.py b/dashscope/legacy_1x/multimodal/multimodal_dialog.py similarity index 98% rename from dashscope/multimodal/multimodal_dialog.py rename to dashscope/legacy_1x/multimodal/multimodal_dialog.py index 63a04f84..1b563c52 100644 --- a/dashscope/multimodal/multimodal_dialog.py +++ b/dashscope/legacy_1x/multimodal/multimodal_dialog.py @@ -7,12 +7,12 @@ import websocket import dashscope -from dashscope.common.env import resolve_base_url -from dashscope.common.logging import logger -from dashscope.common.error import InputRequired -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.multimodal import dialog_state -from dashscope.multimodal.multimodal_constants import ( +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.multimodal import dialog_state +from dashscope.legacy_1x.multimodal.multimodal_constants import ( RESPONSE_NAME_STARTED, RESPONSE_NAME_STOPPED, RESPONSE_NAME_STATE_CHANGED, @@ -26,7 +26,7 @@ RESPONSE_NAME_ERROR, RESPONSE_NAME_HEART_BEAT, ) -from dashscope.multimodal.multimodal_request_params import ( +from dashscope.legacy_1x.multimodal.multimodal_request_params import ( RequestParameters, get_random_uuid, DashHeader, @@ -35,7 +35,7 @@ RequestToRespondParameters, RequestToRespondBodyInput, ) -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.protocol.websocket import ActionType class MultiModalCallback: diff --git a/dashscope/multimodal/multimodal_request_params.py b/dashscope/legacy_1x/multimodal/multimodal_request_params.py similarity index 100% rename from dashscope/multimodal/multimodal_request_params.py rename to dashscope/legacy_1x/multimodal/multimodal_request_params.py diff --git a/dashscope/multimodal/tingwu/__init__.py b/dashscope/legacy_1x/multimodal/tingwu/__init__.py similarity index 100% rename from dashscope/multimodal/tingwu/__init__.py rename to dashscope/legacy_1x/multimodal/tingwu/__init__.py diff --git a/dashscope/multimodal/tingwu/tingwu.py b/dashscope/legacy_1x/multimodal/tingwu/tingwu.py similarity index 88% rename from dashscope/multimodal/tingwu/tingwu.py rename to dashscope/legacy_1x/multimodal/tingwu/tingwu.py index fbceeb77..bf62aada 100644 --- a/dashscope/multimodal/tingwu/tingwu.py +++ b/dashscope/legacy_1x/multimodal/tingwu/tingwu.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- from typing import Dict, Any -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import ModelRequired +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import ModelRequired class TingWu(BaseApi): diff --git a/dashscope/multimodal/tingwu/tingwu_realtime.py b/dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py similarity index 98% rename from dashscope/multimodal/tingwu/tingwu_realtime.py rename to dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py index 899c09e9..577a8658 100644 --- a/dashscope/multimodal/tingwu/tingwu_realtime.py +++ b/dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py @@ -8,15 +8,15 @@ from dataclasses import dataclass, field from queue import Queue import dashscope -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InvalidParameter, ModelRequired -from dashscope.common.env import resolve_base_url +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InvalidParameter, ModelRequired +from dashscope.legacy_1x.common.env import resolve_base_url import websocket # pylint: disable=wrong-import-order # pylint: disable=ungrouped-imports -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.protocol.websocket import ActionType class TingWuRealtimeCallback: diff --git a/dashscope/legacy_1x/nlp/__init__.py b/dashscope/legacy_1x/nlp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/nlp/understanding.py b/dashscope/legacy_1x/nlp/understanding.py similarity index 89% rename from dashscope/nlp/understanding.py rename to dashscope/legacy_1x/nlp/understanding.py index e6be2944..b42db4a3 100644 --- a/dashscope/nlp/understanding.py +++ b/dashscope/legacy_1x/nlp/understanding.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Understanding(BaseApi): diff --git a/dashscope/legacy_1x/protocol/__init__.py b/dashscope/legacy_1x/protocol/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/protocol/websocket.py b/dashscope/legacy_1x/protocol/websocket.py similarity index 100% rename from dashscope/protocol/websocket.py rename to dashscope/legacy_1x/protocol/websocket.py diff --git a/dashscope/legacy_1x/rerank/__init__.py b/dashscope/legacy_1x/rerank/__init__.py new file mode 100644 index 00000000..a271a68e --- /dev/null +++ b/dashscope/legacy_1x/rerank/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.rerank.text_rerank import AioTextReRank, TextReRank + +__all__ = ["AioTextReRank", "TextReRank"] diff --git a/dashscope/rerank/text_rerank.py b/dashscope/legacy_1x/rerank/text_rerank.py similarity index 94% rename from dashscope/rerank/text_rerank.py rename to dashscope/legacy_1x/rerank/text_rerank.py index 960a8357..b3990562 100644 --- a/dashscope/rerank/text_rerank.py +++ b/dashscope/legacy_1x/rerank/text_rerank.py @@ -3,10 +3,10 @@ from typing import Any, Dict, List, Tuple -from dashscope.api_entities.dashscope_response import ReRankResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ReRankResponse +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task __all__ = ["TextReRank", "AioTextReRank"] diff --git a/dashscope/legacy_1x/threads/__init__.py b/dashscope/legacy_1x/threads/__init__.py new file mode 100644 index 00000000..0249d6c8 --- /dev/null +++ b/dashscope/legacy_1x/threads/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +# yapf: disable + +from dashscope.legacy_1x.threads.messages.messages import Messages +from dashscope.legacy_1x.threads.runs.runs import Runs +from dashscope.legacy_1x.threads.runs.steps import Steps +from dashscope.legacy_1x.threads.thread_types import ( + MessageFile, Run, RunList, RunStep, + RunStepList, Thread, ThreadMessage, + ThreadMessageList, +) +from dashscope.legacy_1x.threads.threads import Threads + +__all__ = [ + 'MessageFile', + 'Messages', + 'Run', + 'Runs', + 'RunList', + 'Steps', + 'RunStep', + 'RunStepList', + 'Threads', + 'Thread', + 'ThreadMessage', + 'ThreadMessageList', +] diff --git a/dashscope/legacy_1x/threads/messages/__init__.py b/dashscope/legacy_1x/threads/messages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/threads/messages/files.py b/dashscope/legacy_1x/threads/messages/files.py similarity index 94% rename from dashscope/threads/messages/files.py rename to dashscope/legacy_1x/threads/messages/files.py index 81e5c00c..54488dbb 100644 --- a/dashscope/threads/messages/files.py +++ b/dashscope/legacy_1x/threads/messages/files.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.client.base_api import GetStatusMixin, ListObjectMixin -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import MessageFile, MessageFileList +from dashscope.legacy_1x.client.base_api import GetStatusMixin, ListObjectMixin +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import ( + MessageFile, + MessageFileList, +) __all__ = ["Files"] diff --git a/dashscope/threads/messages/messages.py b/dashscope/legacy_1x/threads/messages/messages.py similarity index 96% rename from dashscope/threads/messages/messages.py rename to dashscope/legacy_1x/threads/messages/messages.py index 10953af4..8f2f2f0a 100644 --- a/dashscope/threads/messages/messages.py +++ b/dashscope/legacy_1x/threads/messages/messages.py @@ -4,20 +4,23 @@ import warnings from typing import Dict, List, Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, GetStatusMixin, ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import ThreadMessage, ThreadMessageList +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import ( + ThreadMessage, + ThreadMessageList, +) __all__ = ["Messages"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/threads/runs/__init__.py b/dashscope/legacy_1x/threads/runs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/threads/runs/runs.py b/dashscope/legacy_1x/threads/runs/runs.py similarity index 98% rename from dashscope/threads/runs/runs.py rename to dashscope/legacy_1x/threads/runs/runs.py index 756d1ed9..904836ad 100644 --- a/dashscope/threads/runs/runs.py +++ b/dashscope/legacy_1x/threads/runs/runs.py @@ -6,20 +6,20 @@ from http import HTTPStatus from typing import Dict, List, Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, GetStatusMixin, ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import ( +from dashscope.legacy_1x.common.error import ( AssistantError, InputRequired, TimeoutException, ) -from dashscope.common.logging import logger -from dashscope.threads.thread_types import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.threads.thread_types import ( Run, RunList, RunStep, @@ -33,7 +33,7 @@ # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/threads/runs/steps.py b/dashscope/legacy_1x/threads/runs/steps.py similarity index 93% rename from dashscope/threads/runs/steps.py rename to dashscope/legacy_1x/threads/runs/steps.py index e7e0c86a..2dcc80d9 100644 --- a/dashscope/threads/runs/steps.py +++ b/dashscope/legacy_1x/threads/runs/steps.py @@ -3,15 +3,15 @@ import warnings -from dashscope.client.base_api import GetStatusMixin, ListObjectMixin -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import RunStep, RunStepList +from dashscope.legacy_1x.client.base_api import GetStatusMixin, ListObjectMixin +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import RunStep, RunStepList __all__ = ["Steps"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/threads/thread_types.py b/dashscope/legacy_1x/threads/thread_types.py similarity index 99% rename from dashscope/threads/thread_types.py rename to dashscope/legacy_1x/threads/thread_types.py index b47ff070..aa552416 100644 --- a/dashscope/threads/thread_types.py +++ b/dashscope/legacy_1x/threads/thread_types.py @@ -6,10 +6,10 @@ from dataclasses import dataclass from typing import Dict, List, Literal, Optional, Union -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( Tool, convert_tools_dict_to_objects, ) -from dashscope.common.base_type import BaseList, BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseList, BaseObjectMixin __all__ = [ 'MessageFile', 'MessageFileList', 'Usage', 'ImageFile', diff --git a/dashscope/threads/threads.py b/dashscope/legacy_1x/threads/threads.py similarity index 96% rename from dashscope/threads/threads.py rename to dashscope/legacy_1x/threads/threads.py index e4effe33..788a4e57 100644 --- a/dashscope/threads/threads.py +++ b/dashscope/legacy_1x/threads/threads.py @@ -4,20 +4,20 @@ import warnings from typing import Dict, List, Optional -from dashscope.assistants.assistant_types import DeleteResponse -from dashscope.client.base_api import ( +from dashscope.legacy_1x.assistants.assistant_types import DeleteResponse +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetStatusMixin, UpdateMixin, ) -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import Run, Thread +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import Run, Thread __all__ = ["Threads"] _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/utils/__init__.py b/dashscope/legacy_1x/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/utils/message_utils.py b/dashscope/legacy_1x/utils/message_utils.py similarity index 100% rename from dashscope/utils/message_utils.py rename to dashscope/legacy_1x/utils/message_utils.py diff --git a/dashscope/utils/oss_utils.py b/dashscope/legacy_1x/utils/oss_utils.py similarity index 96% rename from dashscope/utils/oss_utils.py rename to dashscope/legacy_1x/utils/oss_utils.py index 95187dff..65410244 100644 --- a/dashscope/utils/oss_utils.py +++ b/dashscope/legacy_1x/utils/oss_utils.py @@ -11,12 +11,14 @@ import requests -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin -from dashscope.common.constants import FILE_PATH_SCHEMA -from dashscope.common.error import InvalidInput, UploadFileException -from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin +from dashscope.legacy_1x.common.constants import FILE_PATH_SCHEMA +from dashscope.legacy_1x.common.error import InvalidInput, UploadFileException +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_user_agent class OssUtils(GetMixin): diff --git a/dashscope/utils/param_utils.py b/dashscope/legacy_1x/utils/param_utils.py similarity index 100% rename from dashscope/utils/param_utils.py rename to dashscope/legacy_1x/utils/param_utils.py diff --git a/dashscope/model.py b/dashscope/model.py index 6aea734d..b979ec12 100644 --- a/dashscope/model.py +++ b/dashscope/model.py @@ -1,62 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.model``. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin, ListMixin +Import from ``dashscope.legacy_1x.model`` (or the public ``dashscope`` +root re-exports) in new code. +""" +from dashscope.legacy_1x._shim import install as _install -class Model(ListMixin, GetMixin): - SUB_PATH = "models" - - @classmethod - def get( # type: ignore[override] - cls, - name: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the model information. - - Args: - name (str): The model name. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The model information. - """ - # type: ignore - return super().get( # type: ignore[return-value] - name, - api_key, - workspace=workspace, - **kwargs, - ) # noqa: E501 - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List models. - - Args: - api_key (str, optional): The api key - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - - Returns: - DashScopeAPIResponse: The models. - """ - return super().list( # type: ignore[return-value] - api_key, - page, - page_size, - workspace=workspace, - **kwargs, - ) +_install(__name__, "dashscope.legacy_1x.model") diff --git a/dashscope/models.py b/dashscope/models.py index fee38a4d..9f97c04f 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -1,78 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.models``. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin, ListMixin, _get -from dashscope.common.utils import get_api_module, join_url -import dashscope +Import from ``dashscope.legacy_1x.models`` (or the public ``dashscope`` +root re-exports) in new code. +""" +from dashscope.legacy_1x._shim import install as _install -class Models(ListMixin, GetMixin): - SUB_PATH = "models" - - @classmethod - def get( # type: ignore[override] - cls, - name: str, - api_key: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the model information. - - Args: - name (str): The model name. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The model information. - """ - from http import HTTPStatus - - # Use query parameter to filter by model name on server side - # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 - url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) - params = {"model": name, "page_no": 1, "page_size": 1} - - response = _get( - url, - params=params, - api_key=api_key, - module=get_api_module(cls.__module__), - **kwargs, - ) - - if response.status_code != HTTPStatus.OK: - return response # type: ignore[return-value] - - output = response.output - if not output or "models" not in output or not output["models"]: - response.status_code = 404 - response.message = f"Model '{name}' not found" - response.output = None - return response # type: ignore[return-value] - - # Return the first (and only) model from the filtered list - response.output = output["models"][0] - return response # type: ignore[return-value] - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List models. - - Args: - api_key (str, optional): The api key - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - - Returns: - DashScopeAPIResponse: The models. - """ - # type: ignore - return super().list(page, page_size, api_key=api_key, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 +_install(__name__, "dashscope.legacy_1x.models") diff --git a/dashscope/multimodal/__init__.py b/dashscope/multimodal/__init__.py index 540bb57a..61f1c31b 100644 --- a/dashscope/multimodal/__init__.py +++ b/dashscope/multimodal/__init__.py @@ -1,95 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.multimodal``. -from .tingwu import tingwu -from .tingwu.tingwu import TingWu -from .tingwu.tingwu_realtime import TingWuRealtime, TingWuRealtimeCallback +Import from ``dashscope.legacy_1x.multimodal`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from .multimodal_dialog import MultiModalDialog, MultiModalCallback -from .dialog_state import DialogState -from .multimodal_constants import ( - RequestToRespondType, - RESPONSE_NAME_TASK_STARTED, - RESPONSE_NAME_RESULT_GENERATED, - RESPONSE_NAME_TASK_FINISHED, - RESPONSE_NAME_TASK_FAILED, - RESPONSE_NAME_STARTED, - RESPONSE_NAME_STOPPED, - RESPONSE_NAME_STATE_CHANGED, - RESPONSE_NAME_REQUEST_ACCEPTED, - RESPONSE_NAME_SPEECH_STARTED, - RESPONSE_NAME_SPEECH_ENDED, - RESPONSE_NAME_RESPONDING_STARTED, - RESPONSE_NAME_RESPONDING_ENDED, - RESPONSE_NAME_SPEECH_CONTENT, - RESPONSE_NAME_RESPONDING_CONTENT, - RESPONSE_NAME_ERROR, - RESPONSE_NAME_HEART_BEAT, -) -from .multimodal_request_params import ( - DashHeader, - DashPayloadParameters, - DashPayloadInput, - DashPayload, - RequestBodyInput, - AsrPostProcessing, - ReplaceWord, - Upstream, - Downstream, - DialogAttributes, - Locations, - Network, - Device, - ClientInfo, - BizParams, - RequestParameters, - RequestToRespondParameters, - RequestToRespondBodyInput, - get_random_uuid, -) +from dashscope.legacy_1x._shim import install as _install -__all__ = [ - "tingwu", - "TingWu", - "TingWuRealtime", - "TingWuRealtimeCallback", - "MultiModalDialog", - "MultiModalCallback", - "DialogState", - "RequestToRespondType", - "RESPONSE_NAME_TASK_STARTED", - "RESPONSE_NAME_RESULT_GENERATED", - "RESPONSE_NAME_TASK_FINISHED", - "RESPONSE_NAME_TASK_FAILED", - "RESPONSE_NAME_STARTED", - "RESPONSE_NAME_STOPPED", - "RESPONSE_NAME_STATE_CHANGED", - "RESPONSE_NAME_REQUEST_ACCEPTED", - "RESPONSE_NAME_SPEECH_STARTED", - "RESPONSE_NAME_SPEECH_ENDED", - "RESPONSE_NAME_RESPONDING_STARTED", - "RESPONSE_NAME_RESPONDING_ENDED", - "RESPONSE_NAME_SPEECH_CONTENT", - "RESPONSE_NAME_RESPONDING_CONTENT", - "RESPONSE_NAME_ERROR", - "RESPONSE_NAME_HEART_BEAT", - "DashHeader", - "DashPayloadParameters", - "DashPayloadInput", - "DashPayload", - "RequestBodyInput", - "AsrPostProcessing", - "ReplaceWord", - "Upstream", - "Downstream", - "DialogAttributes", - "Locations", - "Network", - "Device", - "ClientInfo", - "BizParams", - "RequestParameters", - "RequestToRespondParameters", - "RequestToRespondBodyInput", - "get_random_uuid", -] +_install(__name__, "dashscope.legacy_1x.multimodal") diff --git a/dashscope/nlp/__init__.py b/dashscope/nlp/__init__.py index e69de29b..48fa8237 100644 --- a/dashscope/nlp/__init__.py +++ b/dashscope/nlp/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.nlp``. + +Import from ``dashscope.legacy_1x.nlp`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.nlp") diff --git a/dashscope/protocol/__init__.py b/dashscope/protocol/__init__.py index e69de29b..4f4774b0 100644 --- a/dashscope/protocol/__init__.py +++ b/dashscope/protocol/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.protocol``. + +Import from ``dashscope.legacy_1x.protocol`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.protocol") diff --git a/dashscope/rerank/__init__.py b/dashscope/rerank/__init__.py index 2f0829c3..dc612489 100644 --- a/dashscope/rerank/__init__.py +++ b/dashscope/rerank/__init__.py @@ -1,6 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.rerank``. -from dashscope.rerank.text_rerank import AioTextReRank, TextReRank +Import from ``dashscope.legacy_1x.rerank`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = ["AioTextReRank", "TextReRank"] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.rerank") diff --git a/dashscope/threads/__init__.py b/dashscope/threads/__init__.py index 5ae531f2..f0f9f935 100644 --- a/dashscope/threads/__init__.py +++ b/dashscope/threads/__init__.py @@ -1,29 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.threads``. -# yapf: disable +Import from ``dashscope.legacy_1x.threads`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from dashscope.threads.messages.messages import Messages -from dashscope.threads.runs.runs import Runs -from dashscope.threads.runs.steps import Steps -from dashscope.threads.thread_types import ( - MessageFile, Run, RunList, RunStep, - RunStepList, Thread, ThreadMessage, - ThreadMessageList, -) -from dashscope.threads.threads import Threads +from dashscope.legacy_1x._shim import install as _install -__all__ = [ - 'MessageFile', - 'Messages', - 'Run', - 'Runs', - 'RunList', - 'Steps', - 'RunStep', - 'RunStepList', - 'Threads', - 'Thread', - 'ThreadMessage', - 'ThreadMessageList', -] +_install(__name__, "dashscope.legacy_1x.threads") diff --git a/dashscope/tokenizers/__init__.py b/dashscope/tokenizers/__init__.py index e43a8af3..bf30f999 100644 --- a/dashscope/tokenizers/__init__.py +++ b/dashscope/tokenizers/__init__.py @@ -2,8 +2,16 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from .tokenization import Tokenization -from .tokenizer import get_tokenizer, list_tokenizers -from .tokenizer_base import Tokenizer + +# The offline tokenizer implementation lives in the 2.0 package; both stacks +# share it so there is one copy of the code and of the bundled vocab. It is +# imported second on purpose: the 1.x chain above must finish loading before +# the v2 chain starts pulling modules in. +from dashscope.v2.tokenizers import ( # pylint: disable=wrong-import-order + Tokenizer, + get_tokenizer, + list_tokenizers, +) __all__ = [ "Tokenization", diff --git a/dashscope/tokenizers/qwen_tokenizer.py b/dashscope/tokenizers/qwen_tokenizer.py index 9a538f95..2b2baa57 100644 --- a/dashscope/tokenizers/qwen_tokenizer.py +++ b/dashscope/tokenizers/qwen_tokenizer.py @@ -1,167 +1,39 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -# mypy: disable-error-code="annotation-unchecked" - -import base64 -import unicodedata -from typing import Collection, Dict, List, Set, Union - -from .tokenizer_base import Tokenizer - -PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long -ENDOFTEXT = "<|endoftext|>" -IMSTART = "<|im_start|>" -IMEND = "<|im_end|>" -# as the default behavior is changed to allow special tokens in -# regular texts, the surface forms of special tokens need to be -# as different as possible to minimize the impact -EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) -# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 -SPECIAL_START_ID = 151643 -SPECIAL_TOKENS = tuple( - enumerate( - ( - ( - ENDOFTEXT, - IMSTART, - IMEND, - ) - + EXTRAS - ), - start=SPECIAL_START_ID, - ), +"""Compatibility re-export; the implementation moved to the 2.0 package. + +The Qwen tiktoken tokenizer and its special-token tables live only in +``dashscope.v2.tokenizers.qwen_tokenizer``, next to the bundled vocab, so +both stacks share one copy. Import from there in new code. + +``Tokenizer`` is re-exported because the old module imported it into its +own namespace, making ``from dashscope.tokenizers.qwen_tokenizer import +Tokenizer`` a working (if incidental) path. +""" + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 + ENDOFTEXT, + EXTRAS, + IMEND, + IMSTART, + PAT_STR, + SPECIAL_START_ID, + SPECIAL_TOKENS, + SPECIAL_TOKENS_SET, + QwenTokenizer, + Tokenizer, ) -SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) - -# tiktoken's BPE merges tokens recursively in Rust, which can overflow the -# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). -# Split text into chunks below this threshold before encoding. -_CHUNK_SIZE = 100_000 - - -class QwenTokenizer(Tokenizer): - @staticmethod - def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: - with open(tiktoken_bpe_file, "rb") as f: - contents = f.read() - return { - base64.b64decode(token): int(rank) - for token, rank in ( - line.split() for line in contents.splitlines() if line - ) - } - - def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): - self._errors = errors - self._vocab_file = vocab_file - self._extra_vocab_file = extra_vocab_file - - self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( - vocab_file, - ) # type: Dict[bytes, int] - self._special_tokens = { - token: index for index, token in SPECIAL_TOKENS - } - - # try load extra vocab from file - if extra_vocab_file is not None: - used_ids = set(self._mergeable_ranks.values()) | set( - self._special_tokens.values(), - ) - extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) - for token, index in extra_mergeable_ranks.items(): - if token in self._mergeable_ranks: - continue - if index in used_ids: - continue - self._mergeable_ranks[token] = index - # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long - import tiktoken - - enc = tiktoken.Encoding( - "Qwen", - pat_str=PAT_STR, - mergeable_ranks=self._mergeable_ranks, - special_tokens=self._special_tokens, - ) - assert ( - len(self._mergeable_ranks) + len(self._special_tokens) - == enc.n_vocab - ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long - - self.decoder = { - v: k for k, v in self._mergeable_ranks.items() - } # type: dict[int, bytes|str] - self.decoder.update({v: k for k, v in self._special_tokens.items()}) - - self._tokenizer = enc # type: tiktoken.Encoding - - self.eod_id = self._tokenizer.eot_token - self.im_start_id = self._special_tokens[IMSTART] - self.im_end_id = self._special_tokens[IMEND] - - def encode( # type: ignore[override] - self, - text: str, - allowed_special: Union[Set, str] = "all", - disallowed_special: Union[Collection, str] = (), - ) -> Union[List[List], List]: - text = unicodedata.normalize("NFC", text) - if len(text) <= _CHUNK_SIZE: - return self._tokenizer.encode( - text, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ) - - result = [] - for chunk in self._split_text(text): - result.extend( - self._tokenizer.encode( - chunk, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ), - ) - return result - - @staticmethod - def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: - """Split text into chunks at safe tokenization boundaries.""" - parts: List[str] = [] - for i, line in enumerate(text.split("\n")): - piece = line if i == 0 else "\n" + line - if len(piece) <= chunk_size: - parts.append(piece) - else: - for j in range(0, len(piece), chunk_size): - parts.append(piece[j : j + chunk_size]) - - chunks: List[str] = [] - current_chunk: List[str] = [] - current_len = 0 - for part in parts: - if current_len + len(part) <= chunk_size: - current_chunk.append(part) - current_len += len(part) - else: - if current_chunk: - chunks.append("".join(current_chunk)) - current_chunk = [part] - current_len = len(part) - if current_chunk: - chunks.append("".join(current_chunk)) - return chunks - def decode( - self, - token_ids: Union[int, List[int]], - skip_special_tokens: bool = False, - errors: str = None, - **kwargs, - ) -> str: - if isinstance(token_ids, int): - token_ids = [token_ids] - if skip_special_tokens: - token_ids = [i for i in token_ids if i < self.eod_id] - return self._tokenizer.decode(token_ids, errors=errors or self._errors) +__all__ = [ + "ENDOFTEXT", + "EXTRAS", + "IMEND", + "IMSTART", + "PAT_STR", + "SPECIAL_START_ID", + "SPECIAL_TOKENS", + "SPECIAL_TOKENS_SET", + "QwenTokenizer", + "Tokenizer", +] diff --git a/dashscope/tokenizers/tokenization.py b/dashscope/tokenizers/tokenization.py index 9a29de53..ba40b69f 100644 --- a/dashscope/tokenizers/tokenization.py +++ b/dashscope/tokenizers/tokenization.py @@ -4,21 +4,21 @@ import copy from typing import Any, List, Optional -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, Message, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ( CUSTOMIZED_MODEL_ID, DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger class Tokenization(BaseApi): diff --git a/dashscope/tokenizers/tokenizer.py b/dashscope/tokenizers/tokenizer.py index 05ba3b0f..b3218769 100644 --- a/dashscope/tokenizers/tokenizer.py +++ b/dashscope/tokenizers/tokenizer.py @@ -1,49 +1,41 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. - -import os -from typing import List - -from dashscope.common.error import UnsupportedModel -from dashscope.tokenizers.qwen_tokenizer import QwenTokenizer - -from .tokenizer_base import Tokenizer - -QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] -current_path = os.path.dirname(os.path.abspath(__file__)) -root_path = os.path.dirname(current_path) - - -def get_tokenizer(model: str) -> Tokenizer: - """Get a tokenizer based on model name. - - Args: - model (str): The model name. - - Raises: - UnsupportedModel: Not support model - - Returns: - Tokenizer: The `Tokenizer` of the model. - """ - if model in QWEN_SERIALS: - return QwenTokenizer( - os.path.join(root_path, "resources", "qwen.tiktoken"), - ) - elif model.startswith("qwen"): - return QwenTokenizer( - os.path.join(root_path, "resources", "qwen.tiktoken"), - ) - else: - raise UnsupportedModel( - f"Not support model: {model}, currently only support qwen models.", - ) - - -def list_tokenizers() -> List[str]: - """List support models - - Returns: - List[str]: The model list. - """ - return QWEN_SERIALS +"""Compatibility re-export; the implementation moved to the 2.0 package. + +The offline tokenizer factory lives only in +``dashscope.v2.tokenizers.tokenizer``, next to the bundled vocab, so both +stacks share one copy of the code and of the vocab. Import from there in +new code. + +Note that :func:`get_tokenizer` raises ``ValueError`` for an unknown model; +it used to raise ``dashscope.legacy_1x.common.error.UnsupportedModel``, which the 2.0 +leaf must not depend on. + +``Tokenizer`` and ``QwenTokenizer`` are re-exported because the old module +imported them into its own namespace. The old module-level ``current_path`` +and ``root_path`` are deliberately not restored: they only located the +vocab, which no longer ships next to this file. +""" + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.tokenizer import ( # noqa: F401 + QWEN_SERIALS, + get_tokenizer, + list_tokenizers, +) + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 + QwenTokenizer, +) + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.tokenizer_base import Tokenizer # noqa: F401 + +__all__ = [ + "QWEN_SERIALS", + "QwenTokenizer", + "Tokenizer", + "get_tokenizer", + "list_tokenizers", +] diff --git a/dashscope/tokenizers/tokenizer_base.py b/dashscope/tokenizers/tokenizer_base.py index 2e385bf8..89520f92 100644 --- a/dashscope/tokenizers/tokenizer_base.py +++ b/dashscope/tokenizers/tokenizer_base.py @@ -1,31 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility re-export; the implementation moved to the 2.0 package. -from typing import List +The offline tokenizer base class lives only in +``dashscope.v2.tokenizers.tokenizer_base`` so both stacks share one copy. +Import from there in new code. +""" +from dashscope.v2.tokenizers.tokenizer_base import Tokenizer -class Tokenizer: - """Base tokenizer interface for local tokenizers.""" - - def __init__(self): - pass - - def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 - """Encode input text string to token ids. - - Args: - text (str): The string to be encoded. - - Returns: - List[int]: The token ids. - """ - - def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 - """Decode token ids to string. - - Args: - token_ids (List[int]): The input token ids. - - Returns: - str: The string of the token ids. - """ +__all__ = ["Tokenizer"] diff --git a/dashscope/utils/__init__.py b/dashscope/utils/__init__.py index e69de29b..5f7f9be0 100644 --- a/dashscope/utils/__init__.py +++ b/dashscope/utils/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.utils``. + +Import from ``dashscope.legacy_1x.utils`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.utils") diff --git a/dashscope/v2/__init__.py b/dashscope/v2/__init__.py index c50a48fc..d6a03811 100644 --- a/dashscope/v2/__init__.py +++ b/dashscope/v2/__init__.py @@ -11,16 +11,9 @@ from dashscope.v2._quickstart import chat from dashscope.v2.client import AsyncClient, Client from dashscope.v2.errors import DashScopeError, ErrorCode +from dashscope.v2.tokenizers import get_tokenizer, list_tokenizers from dashscope.v2.transport.ws_connection import WsConnection, WsTask -# Offline helper still delegated to the 1.x implementation, re-exported so -# the 2.0 namespace is self-contained (same objects the package root -# already exposes): the local tiktoken wrapper. -from dashscope.tokenizers import ( # noqa: E402 - get_tokenizer, - list_tokenizers, -) - __all__ = [ "AsyncClient", "Client", diff --git a/dashscope/v2/_config.py b/dashscope/v2/_config.py index 48b57ddc..a727faeb 100644 --- a/dashscope/v2/_config.py +++ b/dashscope/v2/_config.py @@ -528,7 +528,7 @@ def _load_config_file(path: Path) -> Dict[str, Any]: def _api_key_from_file() -> Optional[str]: """Lowest api_key tier: the 1.x key file. - Same order as ``dashscope.common.api_key.get_default_api_key`` -- + Same order as ``dashscope.legacy_1x.common.api_key.get_default_api_key`` -- ``DASHSCOPE_API_KEY_FILE_PATH`` first, then the default ``~/.dashscope/api_key`` written by ``save_api_key``. A missing or unreadable file resolves to None rather than raising, since this is @@ -543,7 +543,8 @@ def _api_key_from_file() -> Optional[str]: def save_api_key( - api_key: str, api_key_file_path: Optional[str] = None + api_key: str, + api_key_file_path: Optional[str] = None, ) -> None: """Store an api key in the key file ``_api_key_from_file`` reads back. diff --git a/dashscope/v2/agentstudio/__init__.py b/dashscope/v2/agentstudio/__init__.py new file mode 100644 index 00000000..edd58186 --- /dev/null +++ b/dashscope/v2/agentstudio/__init__.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""AgentStudio sub-SDK for the dashscope Python client. + +The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP +API. All resource classes are accessed through a client instance:: + + from dashscope.agentstudio import Client + from dashscope.agentstudio.types import ( + user_message, user_tool_result, + ) + +Example +------- +>>> from dashscope.agentstudio import Client +>>> from dashscope.agentstudio.types import user_message +>>> client = Client(api_key="sk-xxx") +>>> agent = client.agents.create(name="demo", model="qwen-plus") +>>> session = client.sessions.create(agent=agent.id) +>>> client.sessions.events.send(session.id, [user_message("Hello!")]) +>>> with client.sessions.events.stream(session.id) as stream: +... for event in stream: +... print(event.type, event.to_dict()) +... if event.type == "session_status": +... break +""" + +# yapf: disable + +from .client import Client, AsyncClient +from .transport import APIResponse, RequestOptions +from . import types +from .exceptions import ( + AgentStudioError, + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + ConflictError, + InternalServerError, + InvalidRequestError, + NotFoundError, + OverloadedError, + PermissionDeniedError, + RateLimitError, + StreamClosedError, + StreamError, +) +from .pagination import ( + AsyncCursorPage, + CursorPage, +) +from .types import ( + Memory, + MemoryStore, + MemoryVersion, + Message, + PermissionPolicy, + SecurityAlert, + SecurityAlertList, + SecurityOverview, + ServerEvent, + SessionResource, + StopReason, + user_define_outcome, + user_interrupt, + user_message, + user_tool_approval_response, + user_custom_tool_result, + user_tool_result, +) +from .types.params import ( + AgentCreateParams, + AgentUpdateParams, + SessionCreateParams, + SessionUpdateParams, + EnvironmentCreateParams, + EnvironmentUpdateParams, + DeploymentCreateParams, + DeploymentUpdateParams, + SessionEventSendParams, + WebhookEndpointCreateParams, + WebhookEndpointUpdateParams, + WebhookEventListParams, +) +from .constants import ( + PermissionPolicyType, + SSEEventType, + SessionStatus, + StopReasonType, + WebhookDeliveryStatus, + WebhookDisabledReason, + WebhookEventType, + WebhookStatus, +) + +__all__ = [ + # client + "Client", + "AsyncClient", + # base types + "RequestOptions", + "APIResponse", + # exceptions + "AgentStudioError", + "APIConnectionError", + "APIStatusError", + "APITimeoutError", + "AuthenticationError", + "ConflictError", + "InternalServerError", + "InvalidRequestError", + "NotFoundError", + "OverloadedError", + "PermissionDeniedError", + "RateLimitError", + "StreamError", + "StreamClosedError", + # pagination + "CursorPage", + "AsyncCursorPage", + # unified message type + "Memory", + "MemoryStore", + "MemoryVersion", + "Message", + "PermissionPolicy", + "SecurityAlert", + "SecurityAlertList", + "SecurityOverview", + "ServerEvent", + "SessionResource", + "StopReason", + # client event helpers (re-exported for convenience) + "user_message", + "user_interrupt", + "user_tool_approval_response", + "user_custom_tool_result", + "user_tool_result", + "user_define_outcome", + # typing module + "types", + # params (commonly used request-body types) + "AgentCreateParams", + "AgentUpdateParams", + "SessionCreateParams", + "SessionUpdateParams", + "EnvironmentCreateParams", + "EnvironmentUpdateParams", + "DeploymentCreateParams", + "DeploymentUpdateParams", + "SessionEventSendParams", + "WebhookEndpointCreateParams", + "WebhookEndpointUpdateParams", + "WebhookEventListParams", + "WebhookStatus", + "WebhookDisabledReason", + "WebhookDeliveryStatus", + "WebhookEventType", + "SSEEventType", + "SessionStatus", + "StopReasonType", + "PermissionPolicyType", +] diff --git a/dashscope/agentstudio/client.py b/dashscope/v2/agentstudio/client.py similarity index 79% rename from dashscope/agentstudio/client.py rename to dashscope/v2/agentstudio/client.py index be840af8..87573d6e 100644 --- a/dashscope/agentstudio/client.py +++ b/dashscope/v2/agentstudio/client.py @@ -9,39 +9,59 @@ import httpx -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope._shared._validation import ( + validate_region, + validate_workspace_id, +) +from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.v2.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.v2.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles +from dashscope.v2.agentstudio.resources.memory_stores import ( MemoryStores, AsyncMemoryStores, ) -from dashscope.agentstudio.resources.security import Security, AsyncSecurity -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import Skills, AsyncSkills -from dashscope.agentstudio.resources.vaults import Vaults, AsyncVaults -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.v2.agentstudio.resources.security import Security, AsyncSecurity +from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.v2.agentstudio.resources.skills import Skills, AsyncSkills +from dashscope.v2.agentstudio.resources.vaults import Vaults, AsyncVaults +from dashscope.v2.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_BASE_URL_TEMPLATE, AGENTSTUDIO_DEFAULT_REGION, AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.agentstudio.transport import SyncTransport, AsyncTransport -from dashscope.common.api_key import get_default_api_key -from dashscope.common.env import validate_region, validate_workspace_id +from dashscope.v2.agentstudio.transport import SyncTransport, AsyncTransport +from dashscope.v2._config import _api_key_from_file, settings + + +def _default_api_key() -> Optional[str]: + """Resolve the api key when ``Client(api_key=...)`` was not given. + + Same tiers as the 2.0 client: ``settings.api_key`` (explicit value or + the ``DASHSCOPE_API_KEY`` env var), then the key file written by + ``save_api_key``. Returns ``None`` rather than raising -- the + transport turns a missing key into this SDK's own + ``AuthenticationError`` on the first request, with a message that + names the AgentStudio entry points. + + This deliberately does not read the 1.x ``dashscope.api_key`` module + global; set ``dashscope.v2.settings.api_key`` or the environment + variable instead. + """ + return settings.api_key or _api_key_from_file() def _resolve_base_url( @@ -132,7 +152,7 @@ def __init__( ) self.transport = SyncTransport( base_url=resolved_base, - api_key=api_key or get_default_api_key(), + api_key=api_key or _default_api_key(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), @@ -207,7 +227,7 @@ def __init__( ) self.transport = AsyncTransport( base_url=resolved_base, - api_key=api_key or get_default_api_key(), + api_key=api_key or _default_api_key(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), diff --git a/dashscope/agentstudio/constants.py b/dashscope/v2/agentstudio/constants.py similarity index 100% rename from dashscope/agentstudio/constants.py rename to dashscope/v2/agentstudio/constants.py diff --git a/dashscope/agentstudio/exceptions.py b/dashscope/v2/agentstudio/exceptions.py similarity index 99% rename from dashscope/agentstudio/exceptions.py rename to dashscope/v2/agentstudio/exceptions.py index 124ab5e8..055e3143 100644 --- a/dashscope/agentstudio/exceptions.py +++ b/dashscope/v2/agentstudio/exceptions.py @@ -20,7 +20,7 @@ from typing import Any, Dict, Mapping, Optional -from dashscope.common.error import DashScopeException +from dashscope._shared._errors import DashScopeException class AgentStudioError(DashScopeException): diff --git a/dashscope/agentstudio/pagination.py b/dashscope/v2/agentstudio/pagination.py similarity index 100% rename from dashscope/agentstudio/pagination.py rename to dashscope/v2/agentstudio/pagination.py diff --git a/dashscope/agentstudio/resources/__init__.py b/dashscope/v2/agentstudio/resources/__init__.py similarity index 72% rename from dashscope/agentstudio/resources/__init__.py rename to dashscope/v2/agentstudio/resources/__init__.py index 76c65123..aeeacbc8 100644 --- a/dashscope/agentstudio/resources/__init__.py +++ b/dashscope/v2/agentstudio/resources/__init__.py @@ -15,19 +15,19 @@ client.sessions.events.send(session.id, [user_message("hello")]) """ -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.v2.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.v2.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles +from dashscope.v2.agentstudio.resources.memory_stores import ( AsyncMemories, AsyncMemoryStores, AsyncMemoryVersions, @@ -35,26 +35,26 @@ MemoryStores, MemoryVersions, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.v2.agentstudio.resources.session_events import ( AsyncSessionEvents, SessionEvents, _AioTypedEventStream, _TypedEventStream, ) -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import ( +from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.v2.agentstudio.resources.skills import ( AsyncSkillVersions, AsyncSkills, SkillVersions, Skills, ) -from dashscope.agentstudio.resources.vaults import ( +from dashscope.v2.agentstudio.resources.vaults import ( AsyncCredentials, AsyncVaults, Credentials, Vaults, ) -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.v2.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) diff --git a/dashscope/agentstudio/resources/_helpers.py b/dashscope/v2/agentstudio/resources/_helpers.py similarity index 99% rename from dashscope/agentstudio/resources/_helpers.py rename to dashscope/v2/agentstudio/resources/_helpers.py index f47d6d94..0978b1ab 100644 --- a/dashscope/agentstudio/resources/_helpers.py +++ b/dashscope/v2/agentstudio/resources/_helpers.py @@ -6,7 +6,7 @@ from typing import Any, Mapping -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( Agent, AgentVersion, Credential, diff --git a/dashscope/agentstudio/resources/agents.py b/dashscope/v2/agentstudio/resources/agents.py similarity index 97% rename from dashscope/agentstudio/resources/agents.py rename to dashscope/v2/agentstudio/resources/agents.py index 91bf1553..738eb2d3 100644 --- a/dashscope/agentstudio/resources/agents.py +++ b/dashscope/v2/agentstudio/resources/agents.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_agent, _coerce_agent_version, ) -from dashscope.agentstudio.types import Agent, AgentVersion -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import Agent, AgentVersion +from dashscope.v2.agentstudio.types.params import ( AgentCreateParams, AgentListParams, AgentUpdateParams, diff --git a/dashscope/agentstudio/resources/deployments.py b/dashscope/v2/agentstudio/resources/deployments.py similarity index 98% rename from dashscope/agentstudio/resources/deployments.py rename to dashscope/v2/agentstudio/resources/deployments.py index 149b64f2..4e93b234 100644 --- a/dashscope/agentstudio/resources/deployments.py +++ b/dashscope/v2/agentstudio/resources/deployments.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_deployment, _coerce_deployment_run, ) -from dashscope.agentstudio.types import Deployment, DeploymentRun -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import Deployment, DeploymentRun +from dashscope.v2.agentstudio.types.params import ( _NOT_GIVEN, DeploymentCreateParams, DeploymentListParams, diff --git a/dashscope/agentstudio/resources/environments.py b/dashscope/v2/agentstudio/resources/environments.py similarity index 96% rename from dashscope/agentstudio/resources/environments.py rename to dashscope/v2/agentstudio/resources/environments.py index 8aed2eeb..26a4ab38 100644 --- a/dashscope/agentstudio/resources/environments.py +++ b/dashscope/v2/agentstudio/resources/environments.py @@ -6,16 +6,16 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_env, ) -from dashscope.agentstudio.types import DeleteResponse, Environment -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Environment +from dashscope.v2.agentstudio.types.params import ( EnvironmentCreateParams, EnvironmentListParams, EnvironmentUpdateParams, diff --git a/dashscope/agentstudio/resources/files.py b/dashscope/v2/agentstudio/resources/files.py similarity index 97% rename from dashscope/agentstudio/resources/files.py rename to dashscope/v2/agentstudio/resources/files.py index e05c7be8..a2c9936e 100644 --- a/dashscope/agentstudio/resources/files.py +++ b/dashscope/v2/agentstudio/resources/files.py @@ -18,16 +18,16 @@ Union, ) -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_file, ) -from dashscope.agentstudio.types import DeleteResponse, File -from dashscope.agentstudio.types.params import FileListParams +from dashscope.v2.agentstudio.types import DeleteResponse, File +from dashscope.v2.agentstudio.types.params import FileListParams _PATH_FILES = "/files" diff --git a/dashscope/agentstudio/resources/memory_stores.py b/dashscope/v2/agentstudio/resources/memory_stores.py similarity index 99% rename from dashscope/agentstudio/resources/memory_stores.py rename to dashscope/v2/agentstudio/resources/memory_stores.py index 573084a0..1308a20e 100644 --- a/dashscope/agentstudio/resources/memory_stores.py +++ b/dashscope/v2/agentstudio/resources/memory_stores.py @@ -41,12 +41,12 @@ from typing import Any, List, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_memory, _coerce_memory_store, _coerce_memory_version, @@ -59,13 +59,13 @@ _memory_version_redact_path, _memory_versions_path, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( DeleteResponse, Memory, MemoryStore, MemoryVersion, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( MemoryCreateParams, MemoryListParams, MemoryStoreCreateParams, diff --git a/dashscope/agentstudio/resources/security.py b/dashscope/v2/agentstudio/resources/security.py similarity index 96% rename from dashscope/agentstudio/resources/security.py rename to dashscope/v2/agentstudio/resources/security.py index a6ebc723..8e5af669 100644 --- a/dashscope/agentstudio/resources/security.py +++ b/dashscope/v2/agentstudio/resources/security.py @@ -12,8 +12,8 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.types import SecurityAlertList, SecurityOverview -from dashscope.agentstudio.types.params import SecurityListAgentLogsParams +from dashscope.v2.agentstudio.types import SecurityAlertList, SecurityOverview +from dashscope.v2.agentstudio.types.params import SecurityListAgentLogsParams _PATH_SECURITY_OVERVIEW = "/security/overview" _PATH_SECURITY_AGENT_LOGS = "/security/agent_logs" diff --git a/dashscope/agentstudio/resources/session_events.py b/dashscope/v2/agentstudio/resources/session_events.py similarity index 97% rename from dashscope/agentstudio/resources/session_events.py rename to dashscope/v2/agentstudio/resources/session_events.py index 617acaf9..6fcccc61 100644 --- a/dashscope/agentstudio/resources/session_events.py +++ b/dashscope/v2/agentstudio/resources/session_events.py @@ -14,23 +14,23 @@ Sequence, ) -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_event, _events_path, _stream_path, ) -from dashscope.agentstudio.streaming import AsyncEventStream, EventStream -from dashscope.agentstudio.types import ServerEvent -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.streaming import AsyncEventStream, EventStream +from dashscope.v2.agentstudio.types import ServerEvent +from dashscope.v2.agentstudio.types.params import ( SessionEventListParams, SessionEventSendParams, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, BlockType, SessionStatus, diff --git a/dashscope/agentstudio/resources/session_resources.py b/dashscope/v2/agentstudio/resources/session_resources.py similarity index 95% rename from dashscope/agentstudio/resources/session_resources.py rename to dashscope/v2/agentstudio/resources/session_resources.py index 1691a654..fc2e4b37 100644 --- a/dashscope/agentstudio/resources/session_resources.py +++ b/dashscope/v2/agentstudio/resources/session_resources.py @@ -10,18 +10,18 @@ from typing import Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_session_resource, _session_resource_item_path, _session_resources_path, ) -from dashscope.agentstudio.types import DeleteResponse, SessionResource -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, SessionResource +from dashscope.v2.agentstudio.types.params import ( SessionResourceAddParams, SessionResourceListParams, ) diff --git a/dashscope/agentstudio/resources/session_threads.py b/dashscope/v2/agentstudio/resources/session_threads.py similarity index 97% rename from dashscope/agentstudio/resources/session_threads.py rename to dashscope/v2/agentstudio/resources/session_threads.py index e2f20313..6c8164c1 100644 --- a/dashscope/agentstudio/resources/session_threads.py +++ b/dashscope/v2/agentstudio/resources/session_threads.py @@ -10,12 +10,12 @@ from typing import Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_event, _coerce_session_thread, _session_thread_archive_path, @@ -23,8 +23,8 @@ _session_thread_item_path, _session_threads_path, ) -from dashscope.agentstudio.types import ServerEvent, SessionThread -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import ServerEvent, SessionThread +from dashscope.v2.agentstudio.types.params import ( SessionEventListParams, SessionThreadListParams, ) diff --git a/dashscope/agentstudio/resources/sessions.py b/dashscope/v2/agentstudio/resources/sessions.py similarity index 95% rename from dashscope/agentstudio/resources/sessions.py rename to dashscope/v2/agentstudio/resources/sessions.py index 4bb68f15..e5be7178 100644 --- a/dashscope/agentstudio/resources/sessions.py +++ b/dashscope/v2/agentstudio/resources/sessions.py @@ -6,28 +6,28 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_session, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.v2.agentstudio.resources.session_events import ( SessionEvents, AsyncSessionEvents, ) -from dashscope.agentstudio.resources.session_resources import ( +from dashscope.v2.agentstudio.resources.session_resources import ( SessionResources, AsyncSessionResources, ) -from dashscope.agentstudio.resources.session_threads import ( +from dashscope.v2.agentstudio.resources.session_threads import ( SessionThreads, AsyncSessionThreads, ) -from dashscope.agentstudio.types import DeleteResponse, Session -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Session +from dashscope.v2.agentstudio.types.params import ( SessionCreateParams, SessionListParams, SessionUpdateParams, diff --git a/dashscope/agentstudio/resources/skills.py b/dashscope/v2/agentstudio/resources/skills.py similarity index 97% rename from dashscope/agentstudio/resources/skills.py rename to dashscope/v2/agentstudio/resources/skills.py index c9d9833e..3ad70cf8 100644 --- a/dashscope/agentstudio/resources/skills.py +++ b/dashscope/v2/agentstudio/resources/skills.py @@ -7,17 +7,17 @@ import os # pylint: disable=unused-import # noqa: F401 from typing import Any, BinaryIO, Dict, Optional, Tuple, Union -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_skill, _coerce_skill_version, ) -from dashscope.agentstudio.types import DeleteResponse, Skill, SkillVersion -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Skill, SkillVersion +from dashscope.v2.agentstudio.types.params import ( SkillCreateParams, SkillListParams, SkillVersionCreateParams, diff --git a/dashscope/agentstudio/resources/vaults.py b/dashscope/v2/agentstudio/resources/vaults.py similarity index 98% rename from dashscope/agentstudio/resources/vaults.py rename to dashscope/v2/agentstudio/resources/vaults.py index c68e4922..7440ec69 100644 --- a/dashscope/agentstudio/resources/vaults.py +++ b/dashscope/v2/agentstudio/resources/vaults.py @@ -6,21 +6,21 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_credential, _coerce_vault, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( Credential, DeleteResponse, Vault, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( CredentialCreateParams, CredentialListParams, CredentialUpdateParams, diff --git a/dashscope/agentstudio/resources/webhook_endpoints.py b/dashscope/v2/agentstudio/resources/webhook_endpoints.py similarity index 97% rename from dashscope/agentstudio/resources/webhook_endpoints.py rename to dashscope/v2/agentstudio/resources/webhook_endpoints.py index fc1a4485..c20ee2c4 100644 --- a/dashscope/agentstudio/resources/webhook_endpoints.py +++ b/dashscope/v2/agentstudio/resources/webhook_endpoints.py @@ -6,23 +6,23 @@ from typing import Any, Dict, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_webhook_endpoint, _coerce_webhook_event, _coerce_webhook_secret_reset, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( WebhookEndpoint, WebhookEndpointList, WebhookEvent, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( WebhookEndpointCreateParams, WebhookEndpointUpdateParams, WebhookEventListParams, diff --git a/dashscope/agentstudio/streaming.py b/dashscope/v2/agentstudio/streaming.py similarity index 95% rename from dashscope/agentstudio/streaming.py rename to dashscope/v2/agentstudio/streaming.py index bdbdc687..b338462d 100644 --- a/dashscope/agentstudio/streaming.py +++ b/dashscope/v2/agentstudio/streaming.py @@ -30,6 +30,7 @@ import asyncio import json +import logging import queue import threading from dataclasses import dataclass, field @@ -38,8 +39,13 @@ import httpx from httpx_sse import EventSource -from dashscope.agentstudio import exceptions -from dashscope.common.logging import logger +from dashscope.v2.agentstudio import exceptions + +# A child of the "dashscope" logger, so the level and handler installed by +# dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via +# propagation -- without importing that module (and its import-time side +# effects) just to name a logger. +logger = logging.getLogger("dashscope.agentstudio.streaming") _DEFAULT_IDLE_TIMEOUT = 120.0 diff --git a/dashscope/agentstudio/transport.py b/dashscope/v2/agentstudio/transport.py similarity index 99% rename from dashscope/agentstudio/transport.py rename to dashscope/v2/agentstudio/transport.py index 9083ba49..0a4e1e19 100644 --- a/dashscope/agentstudio/transport.py +++ b/dashscope/v2/agentstudio/transport.py @@ -27,12 +27,12 @@ import httpx -from dashscope.agentstudio import exceptions -from dashscope.agentstudio.constants import ( +from dashscope._shared._sdk_ident import get_sdk_headers +from dashscope.v2.agentstudio import exceptions +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.common.utils import get_sdk_headers logger = logging.getLogger("dashscope.agentstudio") diff --git a/dashscope/agentstudio/types/__init__.py b/dashscope/v2/agentstudio/types/__init__.py similarity index 93% rename from dashscope/agentstudio/types/__init__.py rename to dashscope/v2/agentstudio/types/__init__.py index 9cd37476..7c20eeab 100644 --- a/dashscope/agentstudio/types/__init__.py +++ b/dashscope/v2/agentstudio/types/__init__.py @@ -4,8 +4,8 @@ Re-exports all public symbols from sub-modules for backward compatibility: from dashscope.agentstudio.types import Agent, Message, user_message """ -from dashscope.agentstudio.types.models import * # noqa: F401,F403 -from dashscope.agentstudio.types.models import ( # noqa: F401 +from dashscope.v2.agentstudio.types.models import * # noqa: F401,F403 +from dashscope.v2.agentstudio.types.models import ( # noqa: F401 BaseModel, Metadata, Scope, @@ -93,7 +93,7 @@ WebhookEventData, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( # noqa: F401 +from dashscope.v2.agentstudio.types.params import ( # noqa: F401 AgentCreateParams, AgentUpdateParams, AgentListParams, diff --git a/dashscope/agentstudio/types/models.py b/dashscope/v2/agentstudio/types/models.py similarity index 99% rename from dashscope/agentstudio/types/models.py rename to dashscope/v2/agentstudio/types/models.py index f56ef695..4339dab3 100644 --- a/dashscope/agentstudio/types/models.py +++ b/dashscope/v2/agentstudio/types/models.py @@ -27,7 +27,7 @@ Union, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( SSEEventType, MessageRole, BlockType, diff --git a/dashscope/agentstudio/types/params.py b/dashscope/v2/agentstudio/types/params.py similarity index 99% rename from dashscope/agentstudio/types/params.py rename to dashscope/v2/agentstudio/types/params.py index d85faa63..a4c9268e 100644 --- a/dashscope/agentstudio/types/params.py +++ b/dashscope/v2/agentstudio/types/params.py @@ -18,7 +18,7 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.types.models import BaseModel +from dashscope.v2.agentstudio.types.models import BaseModel _NOT_GIVEN = object() diff --git a/dashscope/v2/client.py b/dashscope/v2/client.py index f2c91dc6..97dacbcf 100644 --- a/dashscope/v2/client.py +++ b/dashscope/v2/client.py @@ -151,22 +151,24 @@ def reinforcement_learning(self) -> "Rl": @property def agentstudio(self): - """Lazily mounted 1.x AgentStudio sub-SDK (design doc M3 3.8). + """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). The client is wrapped in a transparent proxy (design doc M3 3.12) - that translates 1.x AgentStudio errors into the ``dashscope.v2`` + that translates AgentStudio errors into the ``dashscope.v2`` exception family, so ``except dashscope.v2.errors.DashScopeError`` covers them. The proxy spoofs ``__class__`` and forwards attributes, so ``isinstance(client.agentstudio, AgentStudioClient)`` - and ``.transport`` access keep working; no 1.x code is touched. + and ``.transport`` access keep working. - AgentStudio rides the shared 1.x transport, so its requests keep - the unmarked 1.x module tag (``agentstudio``), not the v2 ``2`` - marker -- intentional, since this is the 1.x code path. + AgentStudio ships its own transport rather than riding this + client's, so its requests keep the unmarked module tag + (``agentstudio``) instead of the v2 ``2`` marker -- the backend + already reports on that tag, and changing it would split the + server-side statistics. """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import Client as AgentStudioClient + from dashscope.v2.agentstudio import Client as AgentStudioClient from dashscope.v2.resources._agentstudio import wrap_agentstudio # agentstudio resolves its own base_url from workspace + @@ -392,15 +394,15 @@ def reinforcement_learning(self) -> "AsyncRl": @property def agentstudio(self): - """Lazily mounted 1.x AgentStudio sub-SDK (design doc M3 3.8). + """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). See :attr:`Client.agentstudio`: the returned client is wrapped in - a transparent proxy that aligns 1.x AgentStudio errors with the + a transparent proxy that aligns AgentStudio errors with the ``dashscope.v2`` exception family (design doc M3 3.12). """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import AsyncClient as AsClient + from dashscope.v2.agentstudio import AsyncClient as AsClient from dashscope.v2.resources._agentstudio import wrap_agentstudio # See Client.agentstudio: agentstudio builds its own base_url diff --git a/dashscope/v2/finetune/__init__.py b/dashscope/v2/finetune/__init__.py new file mode 100644 index 00000000..ed3aa25f --- /dev/null +++ b/dashscope/v2/finetune/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Agentic fine-tuning, owned by the 2.0 package. + +Holds the agentic-RL workflow package (``reinforcement``) and the +``AgenticRL`` driver that ``Client(...).rl`` is built on. + +The 1.x job-management surface -- ``dashscope.FineTunes`` and +``dashscope.Deployments`` -- stays at ``dashscope.finetune``; only the +modules listed in the decoupling plan moved here, and each keeps a +reverse-reference shim at its old import path. + +Deliberately imports nothing at module level: ``reinforcement`` requires +pydantic / tenacity / pyyaml, and pulling those into ``import +dashscope.v2`` would make the 2.0 client depend on them. +""" diff --git a/dashscope/v2/finetune/_submit.py b/dashscope/v2/finetune/_submit.py new file mode 100644 index 00000000..a698173c --- /dev/null +++ b/dashscope/v2/finetune/_submit.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The job-submit POST for ``AgenticRL``, self-contained inside 2.0. + +``AgenticRL.submit_job`` used to inherit ``CreateMixin`` from +``dashscope.legacy_1x.client.base_api`` for one non-streaming POST. Keeping that +would have left the last import of the 1.x client inside ``dashscope.v2``, +so the call is spelled out here instead: a few dozen lines, against +pulling the 1.x request/response stack (``default_headers``, ``join_url``, +``_handle_http_stream_response``) into the 2.0 package. + +The wire format is deliberately byte-identical to what ``CreateMixin`` +sent, header order included, because the backend keys its statistics off +``x-dashscope-sdk-client`` and ``user-agent``: + +- The module tag stays the *unmarked* ``finetune``. Deriving it from this + module's dotted name would now yield ``v2``, and the v2 transport's + marked form (``finetune2``) belongs to ``client.rl.jobs`` rather than + to the submit path. Both spellings already exist today; each keeps its + own. +- The identity headers come from ``dashscope._shared._sdk_ident``, so the + CLI's ``set_sdk_client("python-cli")`` override and the process-wide + session id still apply, and ``DASHSCOPE_DISABLE_SDK_HEADERS`` still + suppresses them. + +Only the non-SSE branches of the 1.x response parser are mirrored. +``submit_job`` posts without streaming to ``/fine-tunes``, which answers +``application/json``; a ``text/event-stream`` reply could not be consumed +meaningfully on this path, so there is nothing to reproduce. +""" + +from __future__ import annotations + +import json +from typing import Any, Dict + +import requests + +from dashscope._shared._sdk_ident import get_sdk_headers, get_user_agent + +__all__ = ["SUBMIT_TIMEOUT", "post_submit"] + +# Mirrors 1.x's DEFAULT_REQUEST_TIMEOUT_SECONDS. +SUBMIT_TIMEOUT = 300 + +_JSON = "application/json; charset=utf-8" + +# Hard-coded rather than derived from __module__: see the module +# docstring. The backend already reports on the unmarked spelling. +_SDK_MODULE = "finetune" + + +def _headers(api_key: str) -> Dict[str, str]: + """Build the request headers in 1.x's insertion order.""" + headers = {"Content-Type": _JSON, "user-agent": get_user_agent()} + headers.update(get_sdk_headers(module=_SDK_MODULE)) + headers["Authorization"] = f"Bearer {api_key}" + headers["Accept"] = _JSON + return headers + + +def _success(response: requests.Response) -> Dict[str, Any]: + body = response.json() + output: Any = body.get("data") + if "output" in body: + output = body["output"] + request_id = body.pop("request_id", "") + if "data" not in body and "output" not in body: + output = body + return { + "request_id": request_id, + "status_code": response.status_code, + "code": body.get("code"), + "message": body.get("message", ""), + "output": output, + "usage": body.get("usage"), + "headers": dict(response.headers), + } + + +def _failure(response: requests.Response) -> Dict[str, Any]: + code: Any = "Unknown" + message: Any = response.content.decode("utf-8") + request_id = "" + if "application/json" in response.headers.get("content-type", ""): + error = response.json() + code = error.get("code") + message = error.get("message", "") + if "msg" in error: + message = error["msg"] + request_id = error.get("request_id", "") + return { + "request_id": request_id, + "status_code": response.status_code, + "code": code, + "message": message, + "output": None, + "usage": None, + "headers": dict(response.headers), + } + + +def post_submit( + request: Dict[str, Any], + *, + api_key: str, + base_url: str, + path: str, + timeout: int = SUBMIT_TIMEOUT, +) -> Dict[str, Any]: + """POST ``request`` and return the raw response envelope. + + The seven keys are the ones ``DashScopeAPIResponse`` carries, so the + 1.x surface can keep building ``FineTune(**envelope)`` and the 2.0 + surface can wrap the same dict in ``SubmitResult``. + + ``base_url`` is used verbatim. The RL constants hold no + ``{workspace_id}`` placeholder and ``submit_job`` never sends a + workspace, so 1.x's ``resolve_base_url`` and ``_workspace_header`` + were both no-ops on this path and are not reproduced. + + Args: + request: The job payload, serialized with ``ensure_ascii=False`` + so non-ASCII job names reach the server intact. + api_key: Bearer token; the caller resolves it. + base_url: Management-plane host, without a trailing slash. + path: Endpoint under ``base_url``. + timeout: Seconds for the whole request. + """ + url = f"{base_url.rstrip('/')}/{path.lstrip('/')}" + with requests.Session() as session: + response = session.post( + url, + data=json.dumps(request, ensure_ascii=False).encode("utf-8"), + headers=_headers(api_key), + timeout=timeout, + ) + if response.status_code in (200, 201): + return _success(response) + return _failure(response) diff --git a/dashscope/v2/finetune/agentic_rl.py b/dashscope/v2/finetune/agentic_rl.py new file mode 100644 index 00000000..8fa398cd --- /dev/null +++ b/dashscope/v2/finetune/agentic_rl.py @@ -0,0 +1,745 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The agentic-RL workflow driver, owned by the 2.0 package. + +``Client(...).rl`` is built on this class. The 1.x job-management surface +-- ``FineTunes`` / ``Deployments`` and the ``FineTune`` dataclasses -- +stays at ``dashscope.finetune``; the thin subclass there re-adds those +five verbs and swaps ``SubmitResult`` back for ``FineTune``. + +Two deliberate differences from the 1.x class this was split out of: + +- ``submit_job`` returns ``SubmitResult`` instead of a 1.x ``FineTune`` + dataclass, and posts through ``dashscope.v2.finetune._submit`` instead + of inheriting ``CreateMixin``. +- The api key is the constructor argument or ``DASHSCOPE_API_KEY``, never + the ``dashscope.api_key`` root global. ``set_api_key`` already writes + the argument into that env var and raises when it cannot, so resolution + here is unambiguous -- reading the root global could silently disagree + with the key this instance was built with. +""" + +from __future__ import annotations + +import os +import time +from typing import Union, List, Optional, ClassVar, Dict, Any +from typing_extensions import Self + +from dashscope.v2.finetune import _submit +from dashscope.v2.finetune.reinforcement import ( + AgenticRLFunctionComponent, + RolloutFunctionComponent, + RewardFunctionComponent, + Dataset, + TrainingDataset, + ValidationDataset, +) +from dashscope.v2.finetune.reinforcement import AgenticRLTuning, TuningModel +from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.v2.finetune.reinforcement import ( + FunctionType, + DatasetsType, + TrainingType, +) +from dashscope.v2.finetune.reinforcement import ( + RewardInput, + RolloutInput, + GroupRewardInput, +) +from dashscope.v2.finetune.reinforcement import logger +from dashscope.v2.finetune.reinforcement import ( + set_api_key, + generate_random_id, + get_func_type_id, + deep_remove_none, +) +from dashscope.v2.finetune.reinforcement.common.errors import ( + RegistrationError, + ValidationError, + InstanceQueryError, + RuntimeErrorWithCode, + ValueErrorWithCode, + DatasetsError, +) + +__all__ = ["AgenticRL", "SubmitOutput", "SubmitResult"] + + +class SubmitOutput: + """The ``output`` payload nested in a submit response. + + A plain attribute container on purpose: the server's job payload is + open-ended, and ``reinforcement_learning._to_job`` flattens it with + ``vars(output)``, which needs a ``__dict__``. + """ + + def __init__(self, **kwargs: Any): + self.__dict__.update(kwargs) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.__dict__!r})" + + +class SubmitResult: + """The envelope ``AgenticRL.submit_job`` returns. + + Carries the same seven fields as 1.x's ``DashScopeAPIResponse``, so + ``FineTune(**envelope)`` and ``SubmitResult(**envelope)`` accept the + same dict. A failed submit is a non-200 ``status_code`` here rather + than an exception, exactly as it was under ``FineTune``. + """ + + def __init__( + self, + status_code: int = None, + request_id: str = "", + code: str = None, + message: str = "", + output: Any = None, + usage: Any = None, + headers: Dict[str, Any] = None, + **kwargs: Any, + ): + self.__dict__.update(kwargs) + self.status_code = status_code + self.request_id = request_id + self.code = code + self.message = message + self.output = ( + SubmitOutput(**output) if isinstance(output, dict) else output + ) + self.usage = usage + self.headers = headers or {} + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(status_code={self.status_code!r}, " + f"request_id={self.request_id!r}, code={self.code!r}, " + f"message={self.message!r}, output={self.output!r}, " + f"usage={self.usage!r})" + ) + + +class AgenticRL(AgenticRLTuning): + SUB_PATH: ClassVar[str] = "fine-tunes" + + def __init__(self, api_key: str = None): + super().__init__() + + try: + set_api_key(api_key) + except Exception as e: + raise ValueErrorWithCode( + "Invalid API key configuration", + error_code=3001, + ) from e + + self._api_key = api_key + + def _resolve_api_key(self) -> str: + """The key to submit with; see the module docstring.""" + return self._api_key or os.environ.get("DASHSCOPE_API_KEY", "") + + def init(self, config_path: Optional[str] = None, **kwargs) -> Self: + """ + Initialize an AgenticRL instance from a YAML configuration file. + """ + self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) + + return self + + async def register_functions( + self, + functions: Optional[ + Union[ + List[Union[RolloutFunctionComponent, RewardFunctionComponent]], + RolloutFunctionComponent, + RewardFunctionComponent, + ] + ] = None, + lazy_load: Optional[bool] = True, + ) -> tuple[ + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + ]: + """Register function components and return entity/instance IDs.""" + if functions: + self.tuning.functions = functions + + try: + ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) = await self.tuning.register_functions( + lazy_load=lazy_load, + ) + logger.info("Function components registered") + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RegistrationError( + "Function registration failed", + error_code=3002, + ) from e + + return ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) + + async def upload_datasets( + self, + datasets: Optional[List[Dataset]] = None, + training_files: Optional[Union[List[str], str]] = None, + validation_files: Optional[Union[List[str], str]] = None, + ) -> tuple[List[str], List[str]]: + if datasets: + self.tuning.datasets = datasets + + try: + ( + uploaded_training_ids, + uploaded_validation_ids, + ) = await self.tuning.upload_datasets( + training_files=training_files or [], + validation_files=validation_files or [], + ) + logger.info("Datasets uploaded") + except Exception as e: + raise DatasetsError( + "Datasets upload failed", + error_code=3003, + ) from e + + return uploaded_training_ids, uploaded_validation_ids + + def _build_submit_request( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + ) -> Dict[str, Any]: + """Assemble the job payload, folding the arguments into ``tuning``. + + Split out of ``submit_job`` so the 1.x surface can reuse it + without touching the transport. + """ + teacher_model = teacher_model or self.tuning.teacher_model + + # Resolve job name (fallback to class default) + if job_name: + self.tuning.name = job_name + job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" + + # Model name + if model: + self.tuning.model.name = model + + # rollouts/rewards + if functions: + self.tuning.functions = functions + rollouts = self.tuning.combine_ids_runtimes( + functype=FunctionType.ROLLOUT, + ) + rewards = self.tuning.combine_ids_runtimes( + functype=FunctionType.REWARD, + ) + rewards.extend( + self.tuning.combine_ids_runtimes( + functype=FunctionType.GROUP_REWARD, + id_str=get_func_type_id(FunctionType.REWARD), + ), + ) + # names of functions + if not self.tuning.check_function_names(): + raise ValueErrorWithCode( + "Duplicate function names detected. All function names must " + "be unique.", + error_code=3004, + ) + + # datasets + if datasets: + self.tuning.datasets = datasets + training_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.TRAINING + ] + validation_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.VALIDATION + ] + + # hyper_parameters + if hyper_parameters: + self.tuning.training.hyper_parameters = hyper_parameters + + # resources + if resources: + self.tuning.training.resources = resources + + training_type = self.tuning.training.type + if teacher_model: + training_type = TrainingType.PG_OPD + elif training_type == TrainingType.PG_OPD: + raise ValueErrorWithCode( + f"teacher_model is required when training.type is {training_type}", + error_code=3006, + ) + + request = { + "model": self.tuning.model.name, + "training_datasets": [ds.model_dump() for ds in training_datasets], + "validation_datasets": [ + ds.model_dump() for ds in validation_datasets + ], + "rollout": rollouts[0] if rollouts else None, + "rewards": rewards, + "hyper_parameters": self.tuning.training.hyper_parameters, + "resource_config": self.tuning.training.resources, + "training_type": str(training_type), + "teacher_model": teacher_model, + "job_name": job_name_with_suffix, + } + request = deep_remove_none(request) + logger.info(f"agentic_rl submit_job request: {request}") + + return request + + def _submit_envelope( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> Dict[str, Any]: + """Build the payload, POST it, and return the raw envelope. + + Shared by both surfaces: this class wraps the result in + ``SubmitResult``, the 1.x subclass in a ``FineTune`` dataclass. + ``kwargs`` are forwarded to ``post_submit`` (``path``, + ``timeout``, and overrides for ``api_key`` / ``base_url``). + """ + request = self._build_submit_request( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + ) + + kwargs.setdefault("api_key", self._resolve_api_key()) + kwargs.setdefault("base_url", DASHSCOPE_HTTP_BASE_URL) + kwargs.setdefault("path", self.SUB_PATH) + try: + return _submit.post_submit(request, **kwargs) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode( + "Job submission failed", + error_code=3005, + ) from e + + def submit_job( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: + """ + Submit RL tuning job to the platform. + """ + return SubmitResult( + **self._submit_envelope( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ), + ) + + async def run( + self, + model: Optional[str] = None, + # Datasets parameters + training_datasets: Optional[List[TrainingDataset]] = None, + validation_datasets: Optional[List[ValidationDataset]] = None, + # Path-driven parameters (auto-register & upload) + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + # Common parameters + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: + """ + Execute RL tuning workflow. + """ + try: + logger.info( + "Path-Driven mode: Registering functions & uploading " + "datasets...", + ) + await self.register_functions( + functions=functions, + lazy_load=True, + ) + + datasets = list(training_datasets or []) + list( + validation_datasets or [], + ) + await self.upload_datasets( + datasets=datasets, + ) + + return self.submit_job( + model=model, + datasets=datasets, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode( + "RL tuning workflow failed", + error_code=3006, + ) from e + + @classmethod + async def test_functions( + cls, + instance_id: str, + functype: FunctionType, + input_data: Dict[str, Any], + api_key: str = None, + pull_logs: bool = False, + log_page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ): + """Test a deployed function instance with custom input data. + + Args: + instance_id: Target function instance ID. + functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). + input_data: Test input payload. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + pull_logs: If True, pull all logs of the function instance + (with pagination) after verification and print them + between separator markers. + log_page_size: Page size used when pulling logs. + start_time: Optional start time filter (in seconds) for log + pulling. Defaults to 24 hours before ``end_time``. + end_time: Optional end time filter (in seconds) for log + pulling. Defaults to the current time (now). + """ + try: + set_api_key(api_key) + + if functype == FunctionType.ROLLOUT: + value = RolloutInput.model_validate(input_data) + elif functype == FunctionType.REWARD: + value = RewardInput.model_validate(input_data) + elif functype == FunctionType.GROUP_REWARD: + value = GroupRewardInput.model_validate(input_data) + else: + raise ValueErrorWithCode( + f"Unsupported function type: {functype}", + error_code=3007, + ) + + logger.info( + f"Starting {str(functype)} verification", + extra={ + "instance_id": instance_id, + "input_params": value.model_dump(exclude={"api_key"}), + }, + ) + + result = await AgenticRLFunctionComponent.verify_function( + value, + instance_id, + ) + + if pull_logs: + await cls._pull_function_instance_logs( + function_instance_id=instance_id, + page_size=log_page_size, + start_time=start_time, + end_time=end_time, + ) + + return result + + except Exception as e: + raise ValidationError( + "Function test failed", + error_code=3008, + ) from e + + @classmethod + async def query_function_instance_logs( + cls, + function_instance_id: str, + page_number: int = 1, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, + api_key: str = None, + ) -> Dict[str, Any]: + """Query one page of logs for a function (faas) instance. + + Args: + function_instance_id: Target function instance ID. + page_number: Page number, starting from 1. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the log query API. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_function_instance_logs( + function_instance_id=function_instance_id, + page_number=page_number, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3009, + ) from e + + @classmethod + async def query_all_function_instance_logs( + cls, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, + api_key: str = None, + ) -> List[str]: + """Fetch all logs of a function (faas) instance. + + Pages are fetched one by one via the paginated log query until + all entries are collected. + + Args: + function_instance_id: Target function instance ID. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Aggregated list of log messages across all pages. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3010, + ) from e + + @classmethod + async def delete_function_instance( + cls, + function_instance_id: str, + api_key: str = None, + ) -> Dict[str, Any]: + """Delete a function (faas) runtime instance. + + Args: + function_instance_id: Target function instance ID. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the delete API, containing + ``data.status`` (e.g. "deleted") and + ``data.sandbox_code``. + + Raises: + InstanceQueryError: If the deletion fails. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.delete_function_instance( + function_instance_id=function_instance_id, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance deletion failed", + error_code=3011, + ) from e + + @classmethod + async def _pull_function_instance_logs( + cls, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ) -> None: + """Pull all logs of a function instance and print them between + separator markers (best effort, never raises). + + When ``start_time``/``end_time`` are not provided, defaults to + pulling logs from 24 hours ago until now. + """ + if end_time is None: + end_time = int(time.time()) + if start_time is None: + start_time = end_time - 24 * 60 * 60 + logger.info( + "************start query log " + f"(function_instance_id={function_instance_id})*******", + ) + try: + logs = await cls.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + ) + for entry in logs: + logger.info(f"[function instance log] {entry}") + logger.info( + f"Pulled {len(logs)} log entries for function instance " + f"{function_instance_id}", + ) + except Exception as e: + logger.warning( + f"Failed to pull logs for function instance " + f"{function_instance_id}: {e}", + ) + finally: + logger.info("************end query log *******") diff --git a/dashscope/v2/finetune/reinforcement/__init__.py b/dashscope/v2/finetune/reinforcement/__init__.py new file mode 100644 index 00000000..391e24fd --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/__init__.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +try: + import pydantic # noqa: F401 + import tenacity # noqa: F401 + import yaml # noqa: F401 +except ImportError as _rl_dep_err: + raise ImportError( + "Agentic RL fine-tuning dependencies are missing from this " + "install. Reinstall with: pip install -U dashscope", + ) from _rl_dep_err + +from dashscope.v2.finetune.reinforcement.common.constants import ( + BAILIAN_FILE_API, + BAILIAN_FILE_TIMEOUT, + DASHSCOPE_API_KEY, + DASHSCOPE_HTTP_BASE_URL, + LOG_LEVEL, + FC_API_KEY, + FC_DELETE_API, + FC_FILES_START, + FC_LOAD_API, + FC_PYPI_LIB, + FC_PYPI_REPO, + FC_QUERY_API, + FC_QUERY_LOG_API, + FC_REGISTER_REWARD_API, + FC_REGISTER_ROLLOUT_API, + FC_REGISTER_GROUP_REWARD_API, + FC_SERVER_CLASSPATH, + FC_UPLOAD_OSS_API, + FC_WORKERS_COUNT, + FC_LAYER_CREATE_API, + FC_LAYER_QUERY_API, + FC_LAYER_USED, + FC_LAYER_NAME, + FC_REQUIREMENTS_FILE, + FC_ZIP_EXCLUDE_PATTERNS, + FC_OSS_FILE_SIZE_WARNING, + DATASETS_FILE_SIZE_WARNING, + HTTP_REQUEST_TIMEOUT, + LOGGER_FILTER_FIELDS, +) +from dashscope.v2.finetune.reinforcement.common.errors import ( + AgenticRLError, + InputError, + OutputError, + BaseConnectionError, + OSSConnectionError, + OSSUploadError, + DeploymentError, + RegistrationError, + FunctionLoadError, + InstanceWarmupError, + InstanceQueryError, + FunctionLayerError, + ValidationError, + ConfigurationError, + BasePermissionError, + IOErrorWithCode, + RuntimeErrorWithCode, + ValueErrorWithCode, +) +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model import ( + FunctionComponentModel, + TuningModel, + FoundationModel, + FunctionComponentRuntime, + AgenticRLFunctionComponent, + RolloutFunctionComponent, + RewardFunctionComponent, + GroupRewardFunctionComponent, + Datasets, + Dataset, + TrainingDataset, + ValidationDataset, + MountStorage, + Training, + Observability, + AgenticRLTuning, + Models, +) +from dashscope.v2.finetune.reinforcement.common.model_types import ( + FileSpec, + FunctionType, + DatasetsType, + DataSourceType, + RequestFC, + ResponseFC, + Status, + StatusType, + TrainingType, +) +from dashscope.v2.finetune.reinforcement.common.utils import ( + check_file, + client_fc, + create_deployment_files, + deep_mask, + generate_random_id, + get_filepath_classname, + upload_zip_to_oss_and_by_signed_url, + set_api_key, + to_bailian_data, + zip_dir, + get_func_type_id, + deep_remove_none, + extract_reward_weights, + get_weights_from_file, + serialize_for_output, +) +from dashscope.v2.finetune.reinforcement.component import ( + reward_func, + sub_reward_func, + aggregate_func, +) +from dashscope.v2.finetune.reinforcement.component.data import ( + AgentOutput, + Reward, + RewardInput, + RewardOutput, + RolloutInput, + RolloutOutput, + GroupReward, + GroupRewardInput, + GroupRewardOutput, + TaskStatus, +) +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( + RewardProcessorMeta, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractRolloutProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractGroupRewardProcessor, +) + + +__all__ = [ + # Constants + "BAILIAN_FILE_API", + "BAILIAN_FILE_TIMEOUT", + "DASHSCOPE_API_KEY", + "DASHSCOPE_HTTP_BASE_URL", + "LOG_LEVEL", + "FC_API_KEY", + "FC_DELETE_API", + "FC_FILES_START", + "FC_LOAD_API", + "FC_PYPI_LIB", + "FC_PYPI_REPO", + "FC_QUERY_API", + "FC_QUERY_LOG_API", + "FC_REGISTER_REWARD_API", + "FC_REGISTER_ROLLOUT_API", + "FC_REGISTER_GROUP_REWARD_API", + "FC_SERVER_CLASSPATH", + "FC_UPLOAD_OSS_API", + "FC_WORKERS_COUNT", + "FC_LAYER_CREATE_API", + "FC_LAYER_QUERY_API", + "FC_LAYER_USED", + "FC_LAYER_NAME", + "FC_REQUIREMENTS_FILE", + "FC_ZIP_EXCLUDE_PATTERNS", + "FC_OSS_FILE_SIZE_WARNING", + "DATASETS_FILE_SIZE_WARNING", + "HTTP_REQUEST_TIMEOUT", + "LOGGER_FILTER_FIELDS", + # Errors + "AgenticRLError", + "InputError", + "OutputError", + "BaseConnectionError", + "OSSConnectionError", + "OSSUploadError", + "DeploymentError", + "RegistrationError", + "FunctionLoadError", + "FunctionLayerError", + "InstanceWarmupError", + "InstanceQueryError", + "ValidationError", + "ConfigurationError", + "BasePermissionError", + "IOErrorWithCode", + "RuntimeErrorWithCode", + "ValueErrorWithCode", + # Logging + "logger", + # Utilities + "check_file", + "client_fc", + "create_deployment_files", + "deep_mask", + "generate_random_id", + "get_filepath_classname", + "set_api_key", + "to_bailian_data", + "upload_zip_to_oss_and_by_signed_url", + "zip_dir", + "get_func_type_id", + "deep_remove_none", + "extract_reward_weights", + "get_weights_from_file", + "serialize_for_output", + # Model Types + "FileSpec", + "FunctionType", + "DatasetsType", + "DataSourceType", + "RequestFC", + "ResponseFC", + "Status", + "StatusType", + "TrainingType", + # Data models + "RolloutInput", + "RolloutOutput", + "RewardInput", + "RewardOutput", + "GroupRewardInput", + "GroupRewardOutput", + "GroupReward", + "Reward", + "AgentOutput", + "TaskStatus", + # Func Manager + "reward_func", + "sub_reward_func", + "aggregate_func", + "RewardProcessorMeta", + "AbstractRolloutProcessor", + "AbstractRewardProcessor", + "AbstractGroupRewardProcessor", + # Core Models + "FunctionComponentModel", + "TuningModel", + "FoundationModel", + "FunctionComponentRuntime", + "AgenticRLFunctionComponent", + "RolloutFunctionComponent", + "RewardFunctionComponent", + "GroupRewardFunctionComponent", + "Datasets", + "Dataset", + "TrainingDataset", + "ValidationDataset", + "MountStorage", + "Training", + "Observability", + "AgenticRLTuning", + "Models", +] diff --git a/dashscope/v2/finetune/reinforcement/common/__init__.py b/dashscope/v2/finetune/reinforcement/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/common/constants.py b/dashscope/v2/finetune/reinforcement/common/constants.py similarity index 90% rename from dashscope/finetune/reinforcement/common/constants.py rename to dashscope/v2/finetune/reinforcement/common/constants.py index e87de62e..dcab4d9e 100644 --- a/dashscope/finetune/reinforcement/common/constants.py +++ b/dashscope/v2/finetune/reinforcement/common/constants.py @@ -35,6 +35,9 @@ def get_int_env(env_var: str, default: int) -> int: # -------------------------- # Logging Configuration # -------------------------- +# The logger name is an observability contract: user log filters and +# existing pipelines match on it, so it keeps the published name even +# though the package itself now lives under ``dashscope.v2``. LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.finetune.reinforcement") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { @@ -112,6 +115,10 @@ def get_int_env(env_var: str, default: int) -> int: ) FC_SERVER_CLASSPATH = os.environ.get( "FC_SERVER_CLASSPATH", + # Written into the start.sh deployed to the Function Compute + # sandbox, where the installed dashscope may be any published + # version. The public path keeps resolving through the + # reverse-reference shim at dashscope/finetune/reinforcement. "dashscope.finetune.reinforcement.component.server.server", ) FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/v2/finetune/reinforcement/common/errors.py similarity index 100% rename from dashscope/finetune/reinforcement/common/errors.py rename to dashscope/v2/finetune/reinforcement/common/errors.py diff --git a/dashscope/finetune/reinforcement/common/log.py b/dashscope/v2/finetune/reinforcement/common/log.py similarity index 91% rename from dashscope/finetune/reinforcement/common/log.py rename to dashscope/v2/finetune/reinforcement/common/log.py index 3db44a58..0dd13fb0 100644 --- a/dashscope/finetune/reinforcement/common/log.py +++ b/dashscope/v2/finetune/reinforcement/common/log.py @@ -2,7 +2,7 @@ import logging import sys -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.v2.finetune.reinforcement.common.constants import ( LOGGER_NAME, LOG_LEVEL, ) diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/v2/finetune/reinforcement/common/model.py similarity index 99% rename from dashscope/finetune/reinforcement/common/model.py rename to dashscope/v2/finetune/reinforcement/common/model.py index d0eb5a25..8baf5eb3 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/v2/finetune/reinforcement/common/model.py @@ -23,7 +23,7 @@ import yaml # Local Application -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.v2.finetune.reinforcement.common.constants import ( FC_API_KEY, FC_DELETE_API, FC_LOAD_API, @@ -40,7 +40,7 @@ FC_LAYER_QUERY_API, LOG_LEVEL, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, DatasetsType, @@ -51,7 +51,7 @@ StatusType, TrainingType, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, RolloutInput, @@ -59,7 +59,7 @@ GroupRewardInput, GroupRewardOutput, ) -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.v2.finetune.reinforcement.common.utils import ( check_file, client_fc, create_deployment_files, @@ -73,8 +73,8 @@ get_weights_from_file, deep_remove_none, ) -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.errors import ( InputError, OutputError, OSSConnectionError, diff --git a/dashscope/finetune/reinforcement/common/model_types.py b/dashscope/v2/finetune/reinforcement/common/model_types.py similarity index 100% rename from dashscope/finetune/reinforcement/common/model_types.py rename to dashscope/v2/finetune/reinforcement/common/model_types.py diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/v2/finetune/reinforcement/common/utils.py similarity index 99% rename from dashscope/finetune/reinforcement/common/utils.py rename to dashscope/v2/finetune/reinforcement/common/utils.py index a3d58a57..64a583d1 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/v2/finetune/reinforcement/common/utils.py @@ -20,9 +20,9 @@ retry_if_exception_type, ) -from dashscope.common.utils import get_sdk_headers -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope._shared._sdk_ident import get_sdk_headers +from dashscope.v2.finetune.reinforcement import logger +from dashscope.v2.finetune.reinforcement.common.errors import ( InputError, OutputError, ConfigurationError, @@ -30,7 +30,7 @@ RuntimeErrorWithCode, OSSUploadError, ) -from dashscope.finetune.reinforcement import ( +from dashscope.v2.finetune.reinforcement import ( LOG_LEVEL, DASHSCOPE_API_KEY, BAILIAN_FILE_API, @@ -47,7 +47,7 @@ LOGGER_FILTER_FIELDS, FC_WORKERS_COUNT, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, ) diff --git a/dashscope/finetune/reinforcement/component/__init__.py b/dashscope/v2/finetune/reinforcement/component/__init__.py similarity index 74% rename from dashscope/finetune/reinforcement/component/__init__.py rename to dashscope/v2/finetune/reinforcement/component/__init__.py index 93d768ba..bc2a7334 100644 --- a/dashscope/finetune/reinforcement/component/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/__init__.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- # # Base Components # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -17,28 +17,28 @@ # -------------------------------------------------------------------------- # # Data Models # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, RewardOutput, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) # -------------------------------------------------------------------------- # # Demos # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.demo import ( +from dashscope.v2.finetune.reinforcement.component.demo import ( DemoRewardProcessor, DemoRolloutProcessor, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( reward_func, sub_reward_func, aggregate_func, @@ -47,7 +47,7 @@ # -------------------------------------------------------------------------- # # Func Manager # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.func_manager import ( +from dashscope.v2.finetune.reinforcement.component.func_manager import ( FuncManager, ) @@ -55,30 +55,30 @@ # Observability # # -------------------------------------------------------------------------- # # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.v2.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) # -------------------------------------------------------------------------- # # Parsers # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( RolloutRequestParser, ) # -------------------------------------------------------------------------- # # Processors # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/data/__init__.py b/dashscope/v2/finetune/reinforcement/component/data/__init__.py similarity index 70% rename from dashscope/finetune/reinforcement/component/data/__init__.py rename to dashscope/v2/finetune/reinforcement/component/data/__init__.py index 6277aa74..51727288 100644 --- a/dashscope/finetune/reinforcement/component/data/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/data/__init__.py @@ -2,7 +2,7 @@ # ========================================================================== # # Base Components # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -12,10 +12,10 @@ RequestMetadata, AgentOutput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) @@ -23,21 +23,21 @@ # ========================================================================== # # Input Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) # ========================================================================== # # Output Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) diff --git a/dashscope/finetune/reinforcement/component/data/base_data_model.py b/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py similarity index 98% rename from dashscope/finetune/reinforcement/component/data/base_data_model.py rename to dashscope/v2/finetune/reinforcement/component/data/base_data_model.py index c1970c53..1b59e38b 100644 --- a/dashscope/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py similarity index 90% rename from dashscope/finetune/reinforcement/component/data/group_reward_input.py rename to dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py index acd897f6..dc03f39d 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py @@ -9,10 +9,10 @@ from typing import Any, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_output.py b/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py similarity index 90% rename from dashscope/finetune/reinforcement/component/data/group_reward_output.py rename to dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py index 07208c84..a1fdd8bc 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py @@ -8,10 +8,10 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, ) diff --git a/dashscope/finetune/reinforcement/component/data/reward_input.py b/dashscope/v2/finetune/reinforcement/component/data/reward_input.py similarity index 89% rename from dashscope/finetune/reinforcement/component/data/reward_input.py rename to dashscope/v2/finetune/reinforcement/component/data/reward_input.py index f21bd71e..ef7435a7 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/reward_input.py @@ -9,10 +9,10 @@ from typing import Any, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/reward_output.py b/dashscope/v2/finetune/reinforcement/component/data/reward_output.py similarity index 96% rename from dashscope/finetune/reinforcement/component/data/reward_output.py rename to dashscope/v2/finetune/reinforcement/component/data/reward_output.py index 93c3061c..1264d780 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/reward_output.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, _normalize_error_code, ) diff --git a/dashscope/finetune/reinforcement/component/data/rollout_input.py b/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py similarity index 93% rename from dashscope/finetune/reinforcement/component/data/rollout_input.py rename to dashscope/v2/finetune/reinforcement/component/data/rollout_input.py index ea7bfa1a..485da9ef 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py @@ -9,10 +9,10 @@ from typing import Any, Dict, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ModelResource, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/rollout_output.py b/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py similarity index 95% rename from dashscope/finetune/reinforcement/component/data/rollout_output.py rename to dashscope/v2/finetune/reinforcement/component/data/rollout_output.py index 1b923558..a2518126 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, AgentOutput, _normalize_error_code, diff --git a/dashscope/v2/finetune/reinforcement/component/demo/__init__.py b/dashscope/v2/finetune/reinforcement/component/demo/__init__.py new file mode 100644 index 00000000..f43bc02a --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/demo/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoGroupRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRolloutProcessor, +) + +__all__ = [ + "DemoRewardProcessor", + # Inherits AbstractRewardProcessor, returns RewardOutput + "DemoRolloutProcessor", + # Inherits AbstractRolloutProcessor, returns RolloutOutput + "DemoGroupRewardProcessor", + # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput +] diff --git a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py index a43688d6..dd3476d9 100644 --- a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -6,21 +6,21 @@ Demonstrates rule-based scoring for multiple Agent outputs in a group. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py index d99ad89a..2ac8bc01 100644 --- a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -6,18 +6,18 @@ Demonstrates rule-based scoring for Agent outputs. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py index 8099cc18..51a5f74f 100644 --- a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -8,18 +8,18 @@ import time -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/func_decorator.py b/dashscope/v2/finetune/reinforcement/component/func_decorator.py similarity index 98% rename from dashscope/finetune/reinforcement/component/func_decorator.py rename to dashscope/v2/finetune/reinforcement/component/func_decorator.py index e10d9464..26a99c23 100644 --- a/dashscope/finetune/reinforcement/component/func_decorator.py +++ b/dashscope/v2/finetune/reinforcement/component/func_decorator.py @@ -4,8 +4,8 @@ from functools import wraps from typing import Callable, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, Reward, diff --git a/dashscope/finetune/reinforcement/component/func_manager.py b/dashscope/v2/finetune/reinforcement/component/func_manager.py similarity index 93% rename from dashscope/finetune/reinforcement/component/func_manager.py rename to dashscope/v2/finetune/reinforcement/component/func_manager.py index a1ee558c..d135ff43 100644 --- a/dashscope/finetune/reinforcement/component/func_manager.py +++ b/dashscope/v2/finetune/reinforcement/component/func_manager.py @@ -20,31 +20,31 @@ from functools import partial from typing import Any, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, trace_processor_process, async_trace_processor_process, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( BaseRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( GroupRewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( RolloutRequestParser, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/__init__.py b/dashscope/v2/finetune/reinforcement/component/observability/__init__.py similarity index 91% rename from dashscope/finetune/reinforcement/component/observability/__init__.py rename to dashscope/v2/finetune/reinforcement/component/observability/__init__.py index 96a00c34..5701ec1e 100644 --- a/dashscope/finetune/reinforcement/component/observability/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/__init__.py @@ -4,7 +4,7 @@ This module provides a *stable public surface* while keeping heavy observability dependencies lazily imported. User-facing imports such as:: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, @@ -81,7 +81,7 @@ def __getattr__(name: str) -> Any: "trace_client", "trace_tool", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( genai as _genai, ) @@ -89,7 +89,7 @@ def __getattr__(name: str) -> Any: # Processor-span helpers if name in {"observe_processor", "trace_processor_span"}: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( processor_span as _processor_span, ) @@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any: "span_payload_preview", "trace_processor_process", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( tracing as _tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py similarity index 87% rename from dashscope/finetune/reinforcement/component/observability/genai/__init__.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py index 2beb244e..21670a57 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py @@ -25,23 +25,23 @@ from typing import Any -from dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long instrument_dashscope_generation_call, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long instrument_openai_chat_completions, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long genai_llm_span, observe_llm, run_with_genai_llm_span, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long observe_tool, trace_tool, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/_core.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py similarity index 100% rename from dashscope/finetune/reinforcement/component/observability/genai/_core.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py similarity index 93% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py index 2676e132..b72c7d40 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -7,16 +7,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py index 3814c38f..0a0dd205 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -16,17 +16,17 @@ from opentelemetry import trace as otel_trace from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long openai_chat_messages_to_input_messages, openai_completion_to_output_messages, unwrap_openai_completion, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py similarity index 95% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_span.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py index 62e435ce..645a39d0 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py @@ -12,16 +12,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/messages.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py index 44abe0f5..e1824654 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py @@ -27,9 +27,9 @@ import os from typing import Any, Dict, List, Optional -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import deep_mask -from dashscope.finetune.reinforcement.component.observability import genai +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.utils import deep_mask +from dashscope.v2.finetune.reinforcement.component.observability import genai # pylint: disable=protected-access _core = genai._core diff --git a/dashscope/finetune/reinforcement/component/observability/genai/tools.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py similarity index 99% rename from dashscope/finetune/reinforcement/component/observability/genai/tools.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py index c0d48b85..c6c260e5 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/tools.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py @@ -50,7 +50,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.v2.finetune.reinforcement.component.observability import ( genai, tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/processor_span.py b/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py similarity index 97% rename from dashscope/finetune/reinforcement/component/observability/processor_span.py rename to dashscope/v2/finetune/reinforcement/component/observability/processor_span.py index 65fc3824..3014ab13 100644 --- a/dashscope/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py @@ -34,11 +34,11 @@ from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar, cast -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( _REWARD_SPAN_PREFIX, _ROLLOUT_SPAN_PREFIX, apply_processor_span_attributes_after, @@ -233,7 +233,7 @@ def observe_processor( Replaces the explicit ``@trace_processor_span(logical_kind=...)`` style. """ - from dashscope.finetune.reinforcement.component.processor import ( + from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/tracing.py b/dashscope/v2/finetune/reinforcement/component/observability/tracing.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/tracing.py rename to dashscope/v2/finetune/reinforcement/component/observability/tracing.py index 1da284b7..447109e4 100644 --- a/dashscope/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/tracing.py @@ -36,11 +36,11 @@ from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) @@ -64,6 +64,9 @@ _DEFAULT_FORCE_FLUSH_TIMEOUT_MS = 3000 _MAX_FORCE_FLUSH_TIMEOUT_MS = 60_000 +# OpenTelemetry instrumentation scope name. Renaming it would split +# already-exported traces across two scopes, so it keeps the published +# name even though the package now lives under ``dashscope.v2``. _TRACER_NAME = "dashscope.finetune.reinforcement" # I/O preview limits for span attributes. _IO_MAX_LEN = 128 * 1024 # 128KB per input/output preview diff --git a/dashscope/v2/finetune/reinforcement/component/parser/__init__.py b/dashscope/v2/finetune/reinforcement/component/parser/__init__.py new file mode 100644 index 00000000..bb442998 --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/parser/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( + BaseRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 + GroupRewardRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( + RewardRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( + RolloutRequestParser, +) + +__all__ = [ + "BaseRequestParser", + "RewardRequestParser", + "RolloutRequestParser", + "GroupRewardRequestParser", +] diff --git a/dashscope/finetune/reinforcement/component/parser/base_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py similarity index 90% rename from dashscope/finetune/reinforcement/component/parser/base_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/base_parser.py index fac27693..0d7d3b6f 100644 --- a/dashscope/finetune/reinforcement/component/parser/base_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py similarity index 81% rename from dashscope/finetune/reinforcement/component/parser/group_reward_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py index aceb78d7..e03c850e 100644 --- a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/reward_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py similarity index 79% rename from dashscope/finetune/reinforcement/component/parser/reward_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py index 0dff8195..c8d36b51 100644 --- a/dashscope/finetune/reinforcement/component/parser/reward_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py similarity index 79% rename from dashscope/finetune/reinforcement/component/parser/rollout_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py index 0aaed03b..738b6104 100644 --- a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/v2/finetune/reinforcement/component/processor/__init__.py b/dashscope/v2/finetune/reinforcement/component/processor/__init__.py new file mode 100644 index 00000000..b579f7a4 --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/processor/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractGroupRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRolloutProcessor, +) + +__all__ = [ + "AbstractProcessor", + "AbstractRewardProcessor", + "AbstractRolloutProcessor", + "AbstractGroupRewardProcessor", +] diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py similarity index 90% rename from dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py index 5a8f60a2..df618859 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py similarity index 94% rename from dashscope/finetune/reinforcement/component/processor/abstract_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py index 8f13afd3..f4fd4f5a 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py similarity index 95% rename from dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py index 0b64262f..c9b4aec6 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py @@ -14,18 +14,18 @@ from concurrent.futures import ThreadPoolExecutor, Executor from typing import Optional, Dict -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( RewardProcessorMeta, SubRewardFunction, AggregateFunction, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py similarity index 88% rename from dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py index 9c6dace9..bace5d53 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/server/__init__.py b/dashscope/v2/finetune/reinforcement/component/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/v2/finetune/reinforcement/component/server/server.py similarity index 96% rename from dashscope/finetune/reinforcement/component/server/server.py rename to dashscope/v2/finetune/reinforcement/component/server/server.py index ce2d400d..854e2699 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/v2/finetune/reinforcement/component/server/server.py @@ -47,17 +47,19 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.utils import ( get_fc_request_id, get_business_summary, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component import BaseDataModel -from dashscope.finetune.reinforcement.component.func_manager import FuncManager -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component import BaseDataModel +from dashscope.v2.finetune.reinforcement.component.func_manager import ( + FuncManager, +) +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( ensure_agentic_rl_baggage_span_processor, is_tracing_enabled, maybe_force_flush_async, @@ -622,7 +624,9 @@ async def health_check_endpoint() -> JSONResponse: # Start uvicorn with calculated worker count uvicorn.run( - # app + # uvicorn resolves this import string inside its own worker + # process, so it must stay the public path used by + # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. "dashscope.finetune.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, diff --git a/dashscope/v2/finetune/reinforcement/examples/observability/__init__.py b/dashscope/v2/finetune/reinforcement/examples/observability/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/observability/smoke_observability.py rename to dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/README-zh.md b/dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README-zh.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/README.md b/dashscope/v2/finetune/reinforcement/examples/workspace/README.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/README.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/cli.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/cli.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl b/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl rename to dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl b/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl rename to dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml b/dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml rename to dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/requirements.txt b/dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/requirements.txt rename to dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml b/dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml rename to dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh index 14400acc..b5454eef 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh @@ -11,7 +11,7 @@ #***************************************************************# # Configuration -WORK_DIR='dashscope/finetune/reinforcement/examples/workspace/' +WORK_DIR='dashscope/v2/finetune/reinforcement/examples/workspace/' # Parse command line arguments INSTALL_FLAG=0 diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/submit_job.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py index c4b971c7..7311f4ad 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py @@ -117,7 +117,7 @@ async def main_workflow(): ], # Function Component Configuration functions=[ - # RolloutFunctionComponent:Environment simulation/rollout + # RolloutFunctionComponent: Environment simulation/rollout # generation RolloutFunctionComponent( name="rollout-1", @@ -127,7 +127,7 @@ async def main_workflow(): ), runtime=FunctionComponentRuntime(**rollout_runtime), ), - # RewardFunctionComponent:Reward calculation engine + # RewardFunctionComponent: Reward calculation engine RewardFunctionComponent( name="reward-1", weight=1.0, diff --git a/dashscope/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/test_functions.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py diff --git a/dashscope/v2/resources/_agentstudio.py b/dashscope/v2/resources/_agentstudio.py index a58700b7..fc963ae0 100644 --- a/dashscope/v2/resources/_agentstudio.py +++ b/dashscope/v2/resources/_agentstudio.py @@ -2,18 +2,18 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Transparent v2 error-alignment proxy for the mounted AgentStudio sub-SDK. -Design doc M3 task 3.12. ``client.agentstudio`` lazily mounts the 1.x +Design doc M3 task 3.12. ``client.agentstudio`` lazily mounts the AgentStudio sub-SDK, which keeps its own exception family rooted at -``dashscope.common.error.DashScopeException``. That family shares no +``dashscope._shared._errors.DashScopeException``. That family shares no lineage with ``dashscope.v2.errors.DashScopeError``, so a user writing ``except dashscope.v2.errors.DashScopeError`` would miss every failure the mounted sub-SDK raises. This module closes that gap at the *mount boundary only*: it wraps the constructed AgentStudio client in a transparent proxy that catches -``AgentStudioError`` and re-raises the equivalent v2 error. No 1.x code -is touched, and the standalone ``dashscope.agentstudio.Client`` keeps its -native error contract -- only the v2 mount is translated. +``AgentStudioError`` and re-raises the equivalent v2 error. The +standalone ``dashscope.agentstudio.Client`` keeps its native error +contract -- only the v2 mount is translated. Two-branch translation (design doc §7): @@ -43,9 +43,9 @@ import httpx -from dashscope.agentstudio import exceptions as _as_exceptions -from dashscope.agentstudio.pagination import AsyncCursorPage, CursorPage from dashscope.v2 import errors as _v2_errors +from dashscope.v2.agentstudio import exceptions as _as_exceptions +from dashscope.v2.agentstudio.pagination import AsyncCursorPage, CursorPage _AGENTSTUDIO_FAMILY = "sdk.agentstudio" @@ -63,7 +63,7 @@ # Resource classes (and the typed event streams) live under this package; # attribute access recurses into them so nested method calls stay guarded. -_RESOURCE_PACKAGE = "dashscope.agentstudio.resources" +_RESOURCE_PACKAGE = "dashscope.v2.agentstudio.resources" # AgentStudio raises a few HTTP-family errors locally, without a response, # so they carry no ``status_code`` (the missing-``api_key`` guard in @@ -98,11 +98,12 @@ def _effective_status(exc: _as_exceptions.AgentStudioError) -> int: def translate_agentstudio_error( exc: _as_exceptions.AgentStudioError, ) -> _v2_errors.DashScopeError: - """Map a 1.x AgentStudio exception into the v2 error family.""" + """Map an AgentStudio exception into the v2 error family.""" status = getattr(exc, "status_code", None) if isinstance(exc, _as_exceptions.APIStatusError) or status is not None: - # V1 already resolved compatibility fields and header precedence. + # AgentStudio's own exception already resolved the compatibility + # fields and the header precedence. request_id = getattr(exc, "request_id", None) headers = {"x-request-id": request_id} if request_id else None error = _v2_errors.error_from_status( diff --git a/dashscope/v2/resources/_oss.py b/dashscope/v2/resources/_oss.py index 2235e4a0..42999805 100644 --- a/dashscope/v2/resources/_oss.py +++ b/dashscope/v2/resources/_oss.py @@ -4,16 +4,15 @@ DashScope serves media (image / audio / video) from URLs; a local path or ``file://`` URI must be uploaded to OSS first and the request opted into -OSS resolution. This reuses the 1.x uploader (``dashscope.utils.oss_utils``) -so 2.0 keeps the same upload behavior instead of re-implementing it. +OSS resolution. The uploader itself is ``_oss_upload``. -Two things are anchored to the v2 client rather than to 1.x globals: +Two things are anchored to the v2 client rather than to module globals: -* The upload certificate is fetched from the client's own ``base_url`` - (``base_address=`` below), so a region- or gateway-scoped client uploads - to the matching regional OSS and the returned ``oss://`` URL resolves - under the same endpoint the inference request is sent to. The 1.x global - endpoint would otherwise pin every upload to the default region. +* The upload certificate is fetched from the client's own ``base_url``, so a + region- or gateway-scoped client uploads to the matching regional OSS and + the returned ``oss://`` URL resolves under the same endpoint the inference + request is sent to. A global endpoint would otherwise pin every upload to + the default region. * The effective per-call ``api_key`` / ``workspace`` (the override, else the client default) drive both the certificate fetch and the upload, so the asset lands in the same account the request runs under. @@ -23,7 +22,7 @@ from typing import Any, Dict, List, Optional, Tuple -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.v2.resources._oss_upload import check_and_upload_local from dashscope.v2.types.messages import ( AudioPart, ImagePart, @@ -48,17 +47,16 @@ def _effective( client, api_key: Optional[str], workspace: Optional[str], -) -> Tuple[Optional[str], Optional[str], str]: +) -> Tuple[Optional[str], Optional[str]]: """Resolve the per-call override against the client defaults. - Returns ``(api_key, workspace, base_url)``. The certificate fetch and - the upload both run under these, so the asset lands in the same - account/region the request is sent to. + Returns ``(api_key, workspace)``. The certificate fetch and the upload + both run under these, so the asset lands in the same account the request + is sent to; the endpoint comes from the client itself. """ return ( api_key or client.config.api_key, workspace or client.config.workspace, - client.config.base_url, ) @@ -74,7 +72,7 @@ def upload_message_media( Returns True when any part holds an ``oss://`` URL afterwards (uploaded or already remote), so the caller opts the request into OSS resolution. """ - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) has_upload = False cert: Optional[dict] = None for message in messages: @@ -87,12 +85,12 @@ def upload_message_media( # Inline base64 audio has no URL to upload. continue is_up, new, cert = check_and_upload_local( + client, model, part.url, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: part.url = new @@ -100,12 +98,12 @@ def upload_message_media( elif isinstance(part, VideoPart): if part.url is not None: is_up, new, cert = check_and_upload_local( + client, model, part.url, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: part.url = new @@ -114,12 +112,12 @@ def upload_message_media( frames = [] for frame in part.frames: is_up, new, cert = check_and_upload_local( + client, model, frame, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) has_upload = has_upload or is_up frames.append(new) @@ -128,21 +126,21 @@ def upload_message_media( def _upload_value( + client, value: Any, model: str, api_key: Optional[str], workspace: Optional[str], - base_url: str, cert: Optional[dict], ) -> Tuple[Any, bool, Optional[dict]]: if isinstance(value, str): is_up, new, cert = check_and_upload_local( + client, model, value, - api_key, - cert, - base_address=base_url, + api_key=api_key, workspace=workspace, + certificate=cert, ) return new, is_up, cert if isinstance(value, list): @@ -150,11 +148,11 @@ def _upload_value( out = [] for item in value: new_item, is_up, cert = _upload_value( + client, item, model, api_key, workspace, - base_url, cert, ) has_upload = has_upload or is_up @@ -165,11 +163,11 @@ def _upload_value( out = {} for key, item in value.items(): new_item, is_up, cert = _upload_value( + client, item, model, api_key, workspace, - base_url, cert, ) has_upload = has_upload or is_up @@ -188,17 +186,17 @@ def upload_media_value( """Recursively upload local/oss refs in a media container. Walks strings, lists and dicts (``image_urls`` / ``media`` / - ``extra_input``); each string goes through the 1.x uploader, which only + ``extra_input``); each string goes through the uploader, which only rewrites ``file://`` URIs, existing local paths and ``oss://`` URLs and passes http(s) and other strings through unchanged. """ - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) new_value, has_upload, _ = _upload_value( + client, value, model, key, space, - base_url, None, ) return new_value, has_upload @@ -212,19 +210,19 @@ def upload_embedding_media( workspace: Optional[str] = None, ) -> None: """Upload the image/audio/video field of each embedding item in place.""" - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) cert: Optional[dict] = None for item in contents: for field_key in _EMBEDDING_MEDIA_KEYS: field = item.get(field_key) if isinstance(field, str) and field: is_up, new, cert = check_and_upload_local( + client, model, field, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: item[field_key] = new diff --git a/dashscope/v2/resources/_oss_upload.py b/dashscope/v2/resources/_oss_upload.py new file mode 100644 index 00000000..fb1eebe2 --- /dev/null +++ b/dashscope/v2/resources/_oss_upload.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Local-file → OSS upload, native to the 2.0 SDK. + +DashScope serves media from URLs, so a local path or ``file://`` URI has to +be posted to OSS first and the resulting ``oss://`` URL sent instead. The +upload is two hops with opposite trust levels: + +* the policy fetch (``GET /uploads?action=getPolicy``) goes to DashScope and + **does** carry the credential, so it rides the client transport and picks + up its connection pool, retries and per-call api_key/workspace; +* the post-object upload goes to the OSS host and must **not** carry it. The + transport merges its default ``Authorization: Bearer`` into every request, + and httpx only strips that header on cross-origin *redirects* -- posting an + absolute OSS URL straight through the transport would hand the API key to + the storage host. Hence the separate auth-free client in ``_post_to_oss``. +""" + +from __future__ import annotations + +import mimetypes +import os +from typing import Any, Dict, Optional, Tuple +from urllib.parse import unquote_plus, urlparse + +import httpx + +from dashscope.v2._logging import get_logger +from dashscope.v2.errors import check_response, error_from_status +from dashscope.v2.transport.http import default_user_agent + +__all__ = [ + "check_and_upload_local", + "get_upload_certificate", + "upload_local_file", +] + +_FILE_URI_SCHEME = "file://" +_OSS_SCHEME = "oss://" +_CERT_PATH = "/uploads" +# The client default (300s) is far too short for a large video. +_OSS_POST_TIMEOUT = 3600.0 + +logger = get_logger("oss") + + +def _resolve_file_uri_path(file_uri: str) -> str: + """Turn a ``file://`` URI into a filesystem path.""" + parsed = urlparse(file_uri) + if parsed.netloc: + file_path = parsed.netloc + unquote_plus(parsed.path) + else: + file_path = unquote_plus(parsed.path) + + # A Windows URI parses to "/C:/dir/file"; drop the leading slash. + if ( + file_path.startswith("/") + and len(file_path) > 2 + and file_path[2] == ":" + ): + file_path = file_path[1:] + + return os.path.expanduser(file_path) + + +def get_upload_certificate( + client, + model: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, +) -> Dict[str, Any]: + """Fetch the OSS post-object policy for ``model``. + + Raises even when the client was built with ``raise_on_error=False``: + without a policy there is no URL to upload to and nothing to hand back. + """ + response = client.transport.request( + "GET", + _CERT_PATH, + params={"action": "getPolicy", "model": model}, + api_key=api_key, + workspace=workspace, + ) + check_response(response) + return response.json()["output"] + + +def _post_to_oss( + upload_host: str, + form: Dict[str, Any], + file_name: str, + file_path: str, + transport: Optional[httpx.BaseTransport] = None, +) -> httpx.Response: + """Post one file to OSS with a client that carries no DashScope auth. + + The file is streamed from a live handle rather than read into memory, so + a large video does not have to fit in the process. ``transport`` exists + for tests to inject an ``httpx.MockTransport``. + """ + headers = { + "Accept": "application/json", + "User-Agent": default_user_agent(), + } + with httpx.Client( + timeout=_OSS_POST_TIMEOUT, + transport=transport, + ) as oss_client: + with open(file_path, "rb") as handle: + return oss_client.post( + upload_host, + data=form, + files={"file": (file_name, handle)}, + headers=headers, + ) + + +def upload_local_file( + client, + model: str, + file_path: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + certificate: Optional[Dict[str, Any]] = None, +) -> Tuple[str, Dict[str, Any]]: + """Upload one local file and return ``(oss_url, certificate)``. + + Pass the returned certificate back in to reuse it across a batch instead + of fetching a fresh policy per file. + """ + cert = certificate + if cert is None: + cert = get_upload_certificate( + client, + model, + api_key=api_key, + workspace=workspace, + ) + key = cert["upload_dir"] + "/" + os.path.basename(file_path) + form = { + "OSSAccessKeyId": cert["oss_access_key_id"], + "Signature": cert["signature"], + "policy": cert["policy"], + "key": key, + "x-oss-object-acl": cert["x_oss_object_acl"], + "x-oss-forbid-overwrite": cert["x_oss_forbid_overwrite"], + "success_action_status": "200", + "x-oss-content-type": ( + mimetypes.guess_type(file_path)[0] or "application/octet-stream" + ), + } + response = _post_to_oss( + cert["upload_host"], + form, + os.path.basename(file_path), + file_path, + ) + if not response.is_success: + logger.error( + "uploading file %s to oss failed, status %s", + file_path, + response.status_code, + ) + # OSS answers with XML, so there is no business code to read and the + # error is mapped from the status code alone. + raise error_from_status( + response.status_code, + message=response.text, + headers=response.headers, + ) + return _OSS_SCHEME + key, cert + + +def check_and_upload_local( + client, + model: str, + content: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + certificate: Optional[Dict[str, Any]] = None, +) -> Tuple[bool, str, Optional[Dict[str, Any]]]: + """Upload ``content`` to OSS when it names a local file. + + Returns ``(is_upload, url_or_content, certificate)``. An existing + ``oss://`` URL passes through untouched but still reports + ``is_upload=True``, so the caller opts the request into OSS resolution; + ``http(s)://`` and anything unrecognized pass through as ``False``. + """ + if content.startswith(_FILE_URI_SCHEME): + file_path = _resolve_file_uri_path(content) + if not os.path.isfile(file_path): + raise ValueError(f"The file: {file_path} is not exists!") + url, cert = upload_local_file( + client, + model, + file_path, + api_key=api_key, + workspace=workspace, + certificate=certificate, + ) + return True, url, cert + if content.startswith(_OSS_SCHEME): + return True, content, certificate + if not content.startswith("http"): + content = os.path.expanduser(content) + if os.path.isfile(content): + url, cert = upload_local_file( + client, + model, + content, + api_key=api_key, + workspace=workspace, + certificate=certificate, + ) + return True, url, cert + return False, content, certificate diff --git a/dashscope/v2/resources/audio/customization.py b/dashscope/v2/resources/audio/customization.py index 89675473..f96de9f3 100644 --- a/dashscope/v2/resources/audio/customization.py +++ b/dashscope/v2/resources/audio/customization.py @@ -5,7 +5,7 @@ V2 form of 1.x ``VocabularyService`` (asr hot-word tables, ``audio/asr/vocabulary.py``) and ``VoiceEnrollmentService`` (voice cloning, ``audio/tts_v2/enrollment.py``), plus the qwen voice-design -family (声音设计, no 1.x SDK class — modeled from the official REST API). +family (no 1.x SDK class — modeled from the official REST API). All three ride the same envelope: ``POST /services/audio/{asr|tts}/ customization`` with ``{model, input: {action, ...}, parameters?}``; only the action words and id fields differ per family. 1.x retried @@ -293,7 +293,7 @@ def delete( class Voices(_CustomizationBase): - """``client.audio.voices`` -- voice cloning (声音复刻).""" + """``client.audio.voices`` -- voice cloning.""" PATH = "/services/audio/tts/customization" DEFAULT_MODEL = "voice-enrollment" @@ -453,7 +453,7 @@ def delete( class VoiceDesign(_CustomizationBase): - """``client.audio.voice_design`` -- qwen voice design (声音设计). + """``client.audio.voice_design`` -- qwen voice design. No 1.x SDK class; modeled from the official REST API, which shares the voices endpoint with shorter action words and a ``voice`` id diff --git a/dashscope/v2/resources/chat/completions.py b/dashscope/v2/resources/chat/completions.py index cadaff6f..258f9024 100644 --- a/dashscope/v2/resources/chat/completions.py +++ b/dashscope/v2/resources/chat/completions.py @@ -105,7 +105,7 @@ def _build_request( ) -> Tuple[ChatRequest, str, bool, bool]: normalized = normalize_messages(messages) # 1.x tags the whole chat family "aigc" (MultiModalConversation lives - # in dashscope.aigc); media routing does not change the module tag. + # in dashscope.legacy_1x.aigc); media routing does not change the module tag. module = "aigc" # incremental_output=False asks for cumulative snapshots. Most native # models only stream incrementally, so (like 1.x) the wire keeps diff --git a/dashscope/v2/resources/reinforcement_learning.py b/dashscope/v2/resources/reinforcement_learning.py index ce80824c..10b868af 100644 --- a/dashscope/v2/resources/reinforcement_learning.py +++ b/dashscope/v2/resources/reinforcement_learning.py @@ -2,12 +2,12 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """client.rl thin mount layer (design doc M3 3.7). -The agentic-RL business logic stays in the 1.x implementation -(``dashscope.finetune.agentic_rl``); this layer only mounts it on the -2.0 Client and routes both halves through the v2 error model -- job -management through the v2 transport, and the workflow steps by -translating 1.x exceptions into ``SdkError`` (catalog ``code`` plus -``solution``). One ``except DashScopeError`` therefore covers both. +The agentic-RL workflow lives in the 2.0 package +(``dashscope.v2.finetune``); this layer only mounts it on the 2.0 Client +and routes both halves through the v2 error model -- job management +through the v2 transport, and the workflow steps by translating the RL +exception family into ``SdkError`` (catalog ``code`` plus ``solution``). +One ``except DashScopeError`` therefore covers both. Two halves, matching the rest of the SDK: ``rl.jobs`` is the training job lifecycle (``create`` submits and never blocks, ``wait`` blocks, @@ -15,13 +15,20 @@ itself carries the RL-specific workflow steps that have no ``fine_tunes`` equivalent, including the function-instance queries. +The two halves also identify themselves differently on the wire, and +that split predates this layer: ``rl.jobs`` rides the v2 transport and +sends the marked ``finetune2`` module tag, while the workflow/submit +path keeps sending the unmarked ``finetune`` tag the feature has always +sent. Both are backend statistics contracts, so neither is folded into +the other. + ``register_functions`` / ``test_functions`` / ``upload_data`` / ``jobs.create`` / the function-instance methods are the SDK's only -public methods without ``timeout=``: the 1.x implementation they wrap -accepts none, so declaring it here would silently do nothing. The other -``jobs`` methods take ``timeout=`` as everywhere else. Those same -1.x-wrapped methods accept a per-call ``api_key`` but no ``workspace``: -the 1.x ``AgenticRL`` they delegate to has no workspace concept. +public methods without ``timeout=``: the workflow implementation they +wrap accepts none, so declaring it here would silently do nothing. The +other ``jobs`` methods take ``timeout=`` as everywhere else. Those same +workflow-wrapping methods accept a per-call ``api_key`` but no +``workspace``: ``AgenticRL`` has no workspace concept. """ from __future__ import annotations @@ -69,11 +76,12 @@ "UploadedData", ] -# Imported lazily below via the module object so tests can monkeypatch -# ``dashscope.finetune.agentic_rl.AgenticRL`` without touching 1.x code. -from dashscope.finetune import agentic_rl as _agentic_rl # noqa: E402 -from dashscope.finetune import reinforcement as _reinforcement # noqa: E402 -from dashscope.finetune.reinforcement.common import ( # noqa: E402 +# Imported below via the module object so tests can monkeypatch +# ``dashscope.v2.finetune.agentic_rl.AgenticRL`` without touching the +# workflow implementation. +from dashscope.v2.finetune import agentic_rl as _agentic_rl # noqa: E402 +from dashscope.v2.finetune import reinforcement as _reinforcement # noqa: E402 +from dashscope.v2.finetune.reinforcement.common import ( # noqa: E402 errors as _rl_errors, ) @@ -113,10 +121,10 @@ class UploadedData(BaseModel): # These three bases cover all 19 exception classes in common/errors.py: # AgenticRLError roots 17, and the two *WithCode classes derive from # RuntimeError / ValueError instead. Nothing wider is needed, since -# common/utils.py:139 already funnels transport, timeout and unexpected -# failures into RuntimeErrorWithCode; catching more would relabel bugs -# in this module as SDK errors. pydantic's ValidationError, which is -# also a ValueError, stays outside on purpose. +# common/utils.py's request helper already funnels transport, timeout +# and unexpected failures into RuntimeErrorWithCode; catching more would +# relabel bugs in this module as SDK errors. pydantic's ValidationError, +# which is also a ValueError, stays outside on purpose. _RL_EXCEPTIONS = ( _rl_errors.AgenticRLError, _rl_errors.RuntimeErrorWithCode, @@ -126,7 +134,7 @@ class UploadedData(BaseModel): @contextlib.contextmanager def _translate_rl_errors(): - """Keep 1.x agentic-RL failures inside the v2 exception family. + """Keep agentic-RL workflow failures inside the v2 exception family. A plain sync context manager, so it wraps ``asyncio.run(...)`` and ``await ...`` bodies alike. @@ -157,9 +165,9 @@ def _new_agentic_rl( def _to_job(result: Any) -> FineTune: - """Adapt the 1.x ``FineTune`` dataclass into the v2 model. + """Adapt the RL submit result into the v2 job model. - The 1.x submit returns ``output`` as a nested object and spells the + The submit returns ``output`` as a nested object and spells the timestamps ``create_time`` / ``end_time``; the v2 model's aliases absorb both once the envelope is flattened by hand here. """ @@ -239,14 +247,14 @@ async def _register_functions_workflow( class RlJobs: """``client.rl.jobs``: the agentic-RL training job lifecycle. - ``create`` runs the whole 1.x workflow — register functions, upload + ``create`` runs the whole workflow — register functions, upload datasets, submit — and returns the job without blocking; ``wait`` polls it to a terminal status. The remaining verbs share the ``fine_tunes`` wire contract (RL jobs are fine-tune jobs: same backend, same response models) and ride the v2 client endpoint - like every other resource; 1.x forces a management-plane host - only in its workflow/submit path, which is a config quirk there, - not parity. + like every other resource. The workflow/submit path is the + exception: it posts to the management-plane host its own constants + name, as it always has. """ def __init__(self, client) -> None: @@ -420,10 +428,10 @@ def logs( class Rl: - """Sync mount of the 1.x agentic-RL workflow. + """Sync mount of the agentic-RL workflow. The workflow methods (``register_functions``/``test_functions``/ - ``upload_data``/``jobs.create``) wrap the 1.x async implementation + ``upload_data``/``jobs.create``) wrap the async implementation with ``asyncio.run``, so they must not be called from a thread that already runs an event loop (Jupyter, FastAPI, pytest-asyncio) — use :class:`AsyncRl` there instead. @@ -471,7 +479,7 @@ def test_functions( """Returns whatever the customer's own function returns. The ``pull_logs``/``log_page_size``/``start_time``/``end_time`` - parameters forward to the 1.x log-pulling step that runs after + parameters forward to the log-pulling step that runs after verification. """ with _translate_rl_errors(): @@ -762,7 +770,7 @@ async def logs( class AsyncRl: - """Async mount of the 1.x agentic-RL workflow.""" + """Async mount of the agentic-RL workflow.""" def __init__(self, client) -> None: self._client = client @@ -804,7 +812,7 @@ async def test_functions( """Returns whatever the customer's own function returns. The ``pull_logs``/``log_page_size``/``start_time``/``end_time`` - parameters forward to the 1.x log-pulling step that runs after + parameters forward to the log-pulling step that runs after verification. """ with _translate_rl_errors(): diff --git a/dashscope/v2/resources/tingwu.py b/dashscope/v2/resources/tingwu.py index c776c97e..198f0412 100644 --- a/dashscope/v2/resources/tingwu.py +++ b/dashscope/v2/resources/tingwu.py @@ -5,7 +5,7 @@ Offline summarization (``call``, one POST returning the parsed result) and realtime speech understanding (``realtime``, a duplex websocket session used as a context manager). Mirrors the 1.x -``dashscope.multimodal.tingwu`` pair. +``dashscope.legacy_1x.multimodal.tingwu`` pair. """ # "input" is the wire-protocol field name for task payloads. diff --git a/dashscope/v2/tokenizers/__init__.py b/dashscope/v2/tokenizers/__init__.py new file mode 100644 index 00000000..4e340f37 --- /dev/null +++ b/dashscope/v2/tokenizers/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Offline tokenizers owned by the 2.0 SDK. + +Pure local counting over the bundled tiktoken vocab -- no network, no +dependency on the 1.x package. ``dashscope.tokenizers`` re-exports these +so both stacks share one implementation and one copy of the vocab. +""" + +from .tokenizer import get_tokenizer, list_tokenizers +from .tokenizer_base import Tokenizer + +__all__ = [ + "Tokenizer", + "get_tokenizer", + "list_tokenizers", +] diff --git a/dashscope/resources/qwen.tiktoken b/dashscope/v2/tokenizers/qwen.tiktoken similarity index 100% rename from dashscope/resources/qwen.tiktoken rename to dashscope/v2/tokenizers/qwen.tiktoken diff --git a/dashscope/v2/tokenizers/qwen_tokenizer.py b/dashscope/v2/tokenizers/qwen_tokenizer.py new file mode 100644 index 00000000..9a538f95 --- /dev/null +++ b/dashscope/v2/tokenizers/qwen_tokenizer.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +# mypy: disable-error-code="annotation-unchecked" + +import base64 +import unicodedata +from typing import Collection, Dict, List, Set, Union + +from .tokenizer_base import Tokenizer + +PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long +ENDOFTEXT = "<|endoftext|>" +IMSTART = "<|im_start|>" +IMEND = "<|im_end|>" +# as the default behavior is changed to allow special tokens in +# regular texts, the surface forms of special tokens need to be +# as different as possible to minimize the impact +EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) +# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 +SPECIAL_START_ID = 151643 +SPECIAL_TOKENS = tuple( + enumerate( + ( + ( + ENDOFTEXT, + IMSTART, + IMEND, + ) + + EXTRAS + ), + start=SPECIAL_START_ID, + ), +) +SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) + +# tiktoken's BPE merges tokens recursively in Rust, which can overflow the +# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). +# Split text into chunks below this threshold before encoding. +_CHUNK_SIZE = 100_000 + + +class QwenTokenizer(Tokenizer): + @staticmethod + def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: + with open(tiktoken_bpe_file, "rb") as f: + contents = f.read() + return { + base64.b64decode(token): int(rank) + for token, rank in ( + line.split() for line in contents.splitlines() if line + ) + } + + def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): + self._errors = errors + self._vocab_file = vocab_file + self._extra_vocab_file = extra_vocab_file + + self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( + vocab_file, + ) # type: Dict[bytes, int] + self._special_tokens = { + token: index for index, token in SPECIAL_TOKENS + } + + # try load extra vocab from file + if extra_vocab_file is not None: + used_ids = set(self._mergeable_ranks.values()) | set( + self._special_tokens.values(), + ) + extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) + for token, index in extra_mergeable_ranks.items(): + if token in self._mergeable_ranks: + continue + if index in used_ids: + continue + self._mergeable_ranks[token] = index + # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long + import tiktoken + + enc = tiktoken.Encoding( + "Qwen", + pat_str=PAT_STR, + mergeable_ranks=self._mergeable_ranks, + special_tokens=self._special_tokens, + ) + assert ( + len(self._mergeable_ranks) + len(self._special_tokens) + == enc.n_vocab + ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long + + self.decoder = { + v: k for k, v in self._mergeable_ranks.items() + } # type: dict[int, bytes|str] + self.decoder.update({v: k for k, v in self._special_tokens.items()}) + + self._tokenizer = enc # type: tiktoken.Encoding + + self.eod_id = self._tokenizer.eot_token + self.im_start_id = self._special_tokens[IMSTART] + self.im_end_id = self._special_tokens[IMEND] + + def encode( # type: ignore[override] + self, + text: str, + allowed_special: Union[Set, str] = "all", + disallowed_special: Union[Collection, str] = (), + ) -> Union[List[List], List]: + text = unicodedata.normalize("NFC", text) + if len(text) <= _CHUNK_SIZE: + return self._tokenizer.encode( + text, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ) + + result = [] + for chunk in self._split_text(text): + result.extend( + self._tokenizer.encode( + chunk, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ), + ) + return result + + @staticmethod + def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: + """Split text into chunks at safe tokenization boundaries.""" + parts: List[str] = [] + for i, line in enumerate(text.split("\n")): + piece = line if i == 0 else "\n" + line + if len(piece) <= chunk_size: + parts.append(piece) + else: + for j in range(0, len(piece), chunk_size): + parts.append(piece[j : j + chunk_size]) + + chunks: List[str] = [] + current_chunk: List[str] = [] + current_len = 0 + for part in parts: + if current_len + len(part) <= chunk_size: + current_chunk.append(part) + current_len += len(part) + else: + if current_chunk: + chunks.append("".join(current_chunk)) + current_chunk = [part] + current_len = len(part) + if current_chunk: + chunks.append("".join(current_chunk)) + return chunks + + def decode( + self, + token_ids: Union[int, List[int]], + skip_special_tokens: bool = False, + errors: str = None, + **kwargs, + ) -> str: + if isinstance(token_ids, int): + token_ids = [token_ids] + if skip_special_tokens: + token_ids = [i for i in token_ids if i < self.eod_id] + return self._tokenizer.decode(token_ids, errors=errors or self._errors) diff --git a/dashscope/v2/tokenizers/tokenizer.py b/dashscope/v2/tokenizers/tokenizer.py new file mode 100644 index 00000000..e8625bd7 --- /dev/null +++ b/dashscope/v2/tokenizers/tokenizer.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import os +from typing import List + +from .qwen_tokenizer import QwenTokenizer +from .tokenizer_base import Tokenizer + +QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] +# The vocab ships next to this module, so the leaf stays relocatable. +_VOCAB_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "qwen.tiktoken", +) + + +def get_tokenizer(model: str) -> Tokenizer: + """Get a tokenizer based on model name. + + Args: + model (str): The model name. + + Raises: + ValueError: Not support model + + Returns: + Tokenizer: The `Tokenizer` of the model. + """ + if model.startswith("qwen"): + return QwenTokenizer(_VOCAB_PATH) + raise ValueError( + f"Not support model: {model}, currently only support qwen models.", + ) + + +def list_tokenizers() -> List[str]: + """List support models + + Returns: + List[str]: The model list. + """ + return QWEN_SERIALS diff --git a/dashscope/v2/tokenizers/tokenizer_base.py b/dashscope/v2/tokenizers/tokenizer_base.py new file mode 100644 index 00000000..2e385bf8 --- /dev/null +++ b/dashscope/v2/tokenizers/tokenizer_base.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from typing import List + + +class Tokenizer: + """Base tokenizer interface for local tokenizers.""" + + def __init__(self): + pass + + def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 + """Encode input text string to token ids. + + Args: + text (str): The string to be encoded. + + Returns: + List[int]: The token ids. + """ + + def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 + """Decode token ids to string. + + Args: + token_ids (List[int]): The input token ids. + + Returns: + str: The string of the token ids. + """ diff --git a/dashscope/v2/types/messages.py b/dashscope/v2/types/messages.py index 3d7c7043..d0fea9f5 100644 --- a/dashscope/v2/types/messages.py +++ b/dashscope/v2/types/messages.py @@ -32,8 +32,8 @@ class TextPart(BaseModel): type: Literal["text"] = "text" text: str - # Explicit context-cache marker (Bailian 上下文缓存): a part-level - # sibling such as {"type": "ephemeral"} on the native and + # Explicit context-cache marker (Bailian context caching): a + # part-level sibling such as {"type": "ephemeral"} on the native and # OpenAI-compatible wires; the cached prefix ends at this part. # 1.x passed content blocks through unvalidated, so the marker must # stay accepted on every part type. diff --git a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py index 55bb909a..97be0ef5 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Qwen-Audio 单轮实时语音对话:发送 WAV 或麦克风音频,保存并播放回答。""" +"""Single-turn realtime voice chat with Qwen-Audio: send WAV or microphone audio, save and play the reply.""" import argparse import base64 diff --git a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py index 6e4406d7..ff1ad099 100644 --- a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Qwen-Audio 单轮实时语音对话:发送 WAV 或麦克风音频,保存并播放回答。""" +"""Single-turn realtime voice chat with Qwen-Audio: send WAV or microphone audio, save and play the reply.""" import argparse import base64 diff --git a/examples/legacy_1x/aio_generation/basic.py b/examples/legacy_1x/aio_generation/basic.py index e58c610b..51794a95 100644 --- a/examples/legacy_1x/aio_generation/basic.py +++ b/examples/legacy_1x/aio_generation/basic.py @@ -5,7 +5,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/custom_session.py b/examples/legacy_1x/aio_generation/custom_session.py index 940457b3..cd647fb5 100644 --- a/examples/legacy_1x/aio_generation/custom_session.py +++ b/examples/legacy_1x/aio_generation/custom_session.py @@ -10,7 +10,7 @@ import aiohttp import certifi -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/deep_research.py b/examples/legacy_1x/aio_generation/deep_research.py index e442c10c..8ce3ad50 100644 --- a/examples/legacy_1x/aio_generation/deep_research.py +++ b/examples/legacy_1x/aio_generation/deep_research.py @@ -16,7 +16,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/reasoning.py b/examples/legacy_1x/aio_generation/reasoning.py index 19e03d2e..c7534d36 100644 --- a/examples/legacy_1x/aio_generation/reasoning.py +++ b/examples/legacy_1x/aio_generation/reasoning.py @@ -6,7 +6,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/tool_calls.py b/examples/legacy_1x/aio_generation/tool_calls.py index c1992ab3..a76e8448 100644 --- a/examples/legacy_1x/aio_generation/tool_calls.py +++ b/examples/legacy_1x/aio_generation/tool_calls.py @@ -5,7 +5,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_image_synthesis.py b/examples/legacy_1x/aio_image_synthesis.py index 1f096a4f..b60cf764 100644 --- a/examples/legacy_1x/aio_image_synthesis.py +++ b/examples/legacy_1x/aio_image_synthesis.py @@ -9,7 +9,7 @@ import os from http import HTTPStatus -from dashscope.aigc.image_synthesis import AioImageSynthesis +from dashscope.legacy_1x.aigc.image_synthesis import AioImageSynthesis # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/aio_video_synthesis.py b/examples/legacy_1x/aio_video_synthesis.py index 92a60632..931c04da 100644 --- a/examples/legacy_1x/aio_video_synthesis.py +++ b/examples/legacy_1x/aio_video_synthesis.py @@ -10,7 +10,7 @@ import os from http import HTTPStatus -from dashscope.aigc.video_synthesis import AioVideoSynthesis +from dashscope.legacy_1x.aigc.video_synthesis import AioVideoSynthesis # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/assistant_api_file.py b/examples/legacy_1x/assistant_api_file.py index 47bf5d52..6f0d770e 100644 --- a/examples/legacy_1x/assistant_api_file.py +++ b/examples/legacy_1x/assistant_api_file.py @@ -9,7 +9,7 @@ import os from dashscope import Assistants # pylint: disable=no-name-in-module -from dashscope.assistants.files import Files +from dashscope.legacy_1x.assistants.files import Files # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), and the diff --git a/examples/legacy_1x/cv_models.py b/examples/legacy_1x/cv_models.py index f5e425c0..cc17d717 100644 --- a/examples/legacy_1x/cv_models.py +++ b/examples/legacy_1x/cv_models.py @@ -7,8 +7,8 @@ import os -from dashscope.client.base_api import BaseAsyncApi -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/image_generation.py b/examples/legacy_1x/image_generation.py index 9b427d49..19224315 100644 --- a/examples/legacy_1x/image_generation.py +++ b/examples/legacy_1x/image_generation.py @@ -10,8 +10,8 @@ import os -from dashscope.aigc.image_generation import ImageGeneration -from dashscope.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.aigc.image_generation import ImageGeneration +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/set_region_example.py b/examples/legacy_1x/set_region_example.py index 45b45d54..c3a74028 100644 --- a/examples/legacy_1x/set_region_example.py +++ b/examples/legacy_1x/set_region_example.py @@ -11,8 +11,8 @@ """ import dashscope -from dashscope.aigc.chat_completion import Completions -from dashscope.aigc.generation import Generation +from dashscope.legacy_1x.aigc.chat_completion import Completions +from dashscope.legacy_1x.aigc.generation import Generation def example_basic_usage(): diff --git a/examples/legacy_1x/tingwu_realtime.py b/examples/legacy_1x/tingwu_realtime.py index 0ec959f1..97cba0e8 100644 --- a/examples/legacy_1x/tingwu_realtime.py +++ b/examples/legacy_1x/tingwu_realtime.py @@ -16,7 +16,7 @@ import os import time -from dashscope.multimodal.tingwu.tingwu_realtime import ( +from dashscope.legacy_1x.multimodal.tingwu.tingwu_realtime import ( TingWuRealtime, TingWuRealtimeCallback, ) diff --git a/examples/legacy_1x/tingwu_usages.py b/examples/legacy_1x/tingwu_usages.py index c5a84f44..f0ed25bd 100644 --- a/examples/legacy_1x/tingwu_usages.py +++ b/examples/legacy_1x/tingwu_usages.py @@ -3,7 +3,7 @@ import os -from dashscope.multimodal.tingwu.tingwu import TingWu +from dashscope.legacy_1x.multimodal.tingwu.tingwu import TingWu # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/tests/integration/test_large_utf8_payload.py b/tests/integration/test_large_utf8_payload.py index 9a52320a..ef84ae99 100644 --- a/tests/integration/test_large_utf8_payload.py +++ b/tests/integration/test_large_utf8_payload.py @@ -13,7 +13,7 @@ import pytest -from dashscope.aigc.generation import Generation, AioGeneration +from dashscope.legacy_1x.aigc.generation import Generation, AioGeneration WEBSEARCH_JSON = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), diff --git a/tests/legacy/conftest.py b/tests/legacy/conftest.py index e546841c..b2e886ed 100644 --- a/tests/legacy/conftest.py +++ b/tests/legacy/conftest.py @@ -7,7 +7,7 @@ import pytest import dashscope -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV +from dashscope.legacy_1x.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV from tests.unit.mock_server import create_mock_server, run_server diff --git a/tests/legacy/test_async_api.py b/tests/legacy/test_async_api.py index e8b53594..4729f220 100644 --- a/tests/legacy/test_async_api.py +++ b/tests/legacy/test_async_api.py @@ -6,9 +6,9 @@ import pytest from typing import Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod, TaskStatus +from dashscope.legacy_1x.api_entities.dashscope_response import DashScopeAPIResponse +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod, TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_encryption.py b/tests/legacy/test_encryption.py index 5b199612..b1ffcd6f 100644 --- a/tests/legacy/test_encryption.py +++ b/tests/legacy/test_encryption.py @@ -4,7 +4,7 @@ import pytest -from dashscope.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.encryption import Encryption class TestEncryption: diff --git a/tests/legacy/test_image_synthesis.py b/tests/legacy/test_image_synthesis.py index 947297b4..ff39b3ba 100644 --- a/tests/legacy/test_image_synthesis.py +++ b/tests/legacy/test_image_synthesis.py @@ -6,7 +6,7 @@ import pytest from dashscope import ImageSynthesis -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_speech_recognizer.py b/tests/legacy/test_speech_recognizer.py index 3227a169..90d89c20 100644 --- a/tests/legacy/test_speech_recognizer.py +++ b/tests/legacy/test_speech_recognizer.py @@ -11,7 +11,7 @@ import pytest import dashscope -from dashscope.audio.asr import ( +from dashscope.legacy_1x.audio.asr import ( Recognition, RecognitionCallback, RecognitionResult, @@ -119,10 +119,10 @@ def complete_url(url: str) -> str: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [base_url, "api-ws", dashscope.legacy_1x.common.env.api_version, "inference"], ) dashscope.base_http_api_url = url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/legacy/test_speech_synthesis.py b/tests/legacy/test_speech_synthesis.py index 1e4d059c..28e51fbb 100644 --- a/tests/legacy/test_speech_synthesis.py +++ b/tests/legacy/test_speech_synthesis.py @@ -10,8 +10,8 @@ import pytest import dashscope -from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse -from dashscope.audio.tts import ( +from dashscope.legacy_1x.api_entities.dashscope_response import SpeechSynthesisResponse +from dashscope.legacy_1x.audio.tts import ( ResultCallback, SpeechSynthesisResult, SpeechSynthesizer, diff --git a/tests/legacy/test_speech_transcription.py b/tests/legacy/test_speech_transcription.py index b196d526..293cc610 100644 --- a/tests/legacy/test_speech_transcription.py +++ b/tests/legacy/test_speech_transcription.py @@ -10,8 +10,8 @@ import pytest import dashscope -from dashscope.audio.asr import Transcription -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.audio.asr import Transcription +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment HTTPS_16K_CH1_WAV = "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example.wav" # noqa: * @@ -132,10 +132,10 @@ def complete_url(url: str) -> str: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [base_url, "api-ws", dashscope.legacy_1x.common.env.api_version, "inference"], ) dashscope.base_http_api_url = url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/legacy/test_video_synthesis.py b/tests/legacy/test_video_synthesis.py index 8b197469..678e2097 100644 --- a/tests/legacy/test_video_synthesis.py +++ b/tests/legacy/test_video_synthesis.py @@ -6,7 +6,7 @@ import pytest from dashscope import VideoSynthesis -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_websocket_async_api.py b/tests/legacy/test_websocket_async_api.py index 00a6a970..4015481a 100644 --- a/tests/legacy/test_websocket_async_api.py +++ b/tests/legacy/test_websocket_async_api.py @@ -4,7 +4,7 @@ import pytest import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import TestTasks from tests.unit.websocket_task_request import WebSocketRequest diff --git a/tests/legacy/test_websocket_parameters.py b/tests/legacy/test_websocket_parameters.py index 2211ec5a..4e240b00 100644 --- a/tests/legacy/test_websocket_parameters.py +++ b/tests/legacy/test_websocket_parameters.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, diff --git a/tests/legacy/test_websocket_sync_api.py b/tests/legacy/test_websocket_sync_api.py index 46dede12..c61cdfac 100644 --- a/tests/legacy/test_websocket_sync_api.py +++ b/tests/legacy/test_websocket_sync_api.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import TestTasks from tests.unit.websocket_task_request import WebSocketRequest diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 78068085..fdcb6efc 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -6,7 +6,9 @@ import pytest -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV +from dashscope.legacy_1x.common.constants import ( + DASHSCOPE_DISABLE_DATA_INSPECTION_ENV, +) from tests.unit.mock_server import create_mock_server, run_server diff --git a/tests/unit/http_task_request.py b/tests/unit/http_task_request.py index b8e893e6..47f4c05f 100644 --- a/tests/unit/http_task_request.py +++ b/tests/unit/http_task_request.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class HttpRequest(BaseApi, BaseAioApi): diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 0713088c..cd2e578f 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -11,7 +11,7 @@ import aiohttp from aiohttp import web -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.protocol.websocket import ActionType from tests.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, TEST_ENABLE_DATA_INSPECTION_REQUEST_ID, diff --git a/tests/unit/test_acli_sdk_contract.py b/tests/unit/test_acli_sdk_contract.py new file mode 100644 index 00000000..baa56ec2 --- /dev/null +++ b/tests/unit/test_acli_sdk_contract.py @@ -0,0 +1,362 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The acli <-> SDK contract, pinned so the 1.x/2.0 layout move is safe. + +``dashscope/acli/`` is a one-way sync copy of the agenticCLI repo and a +neutral top-level product surface: it serves both stacks, belongs to +neither, and must not be refactored here because the next sync would +overwrite the edit. What this repo *does* own is the SDK surface acli +consumes, so that is what gets pinned. + +The dependency surface is derived from acli's own AST instead of being +hardcoded. acli is re-synced from upstream, so line numbers drift and a +sync can introduce new coupling; deriving the check from the tree means +the test reports that coupling rather than silently going stale. + +The load-bearing assertion is ``api_key``. ``acli/ui/tts.py`` writes the +1.x root global ``dashscope.api_key`` because the audio SDK reads it on +paths that do not consult the environment. Promoting 2.0 to the top +level rewrites ``dashscope/__init__.py``, and 2.0 takes the key as +``Client(api_key=...)`` with no root global -- dropping the attribute +would break acli's voice output at ``if not dashscope.api_key``, while +``is_available()`` (which only does ``import dashscope``) keeps +reporting True and defers the failure to the end user. + +acli's deliberately separate session id is covered by +``tests/unit/test_sdk_headers.py``; it is not repeated here. +""" + +from __future__ import annotations + +import ast +import importlib +import importlib.util +import pathlib +from functools import lru_cache + +import dashscope + +_REPO = pathlib.Path(__file__).resolve().parents[2] +_ACLI = _REPO / "dashscope" / "acli" +_V2 = _REPO / "dashscope" / "v2" +_ROOT_INIT = _REPO / "dashscope" / "__init__.py" + +# What acli is allowed to couple to. The bare ``dashscope`` entry is here +# because acli reads and writes the root global ``dashscope.api_key``; the +# two audio modules are the deep paths the design doc calls out. Anything +# else means a sync added coupling this repo should accept consciously. +_ALLOWLIST = frozenset( + { + "dashscope", + "dashscope.legacy_1x.audio.asr", + "dashscope.legacy_1x.audio.tts_v2", + }, +) + +_ACLI_EXTRAS = frozenset( + { + "acli", + "acli-all", + "acli-anthropic", + "acli-camera", + "acli-openai", + "acli-voice", + }, +) + + +def _is_acli_internal(module: str) -> bool: + parts = module.split(".") + return len(parts) > 1 and parts[1] == "acli" + + +def _parse(root: pathlib.Path): + """Parse every module under ``root`` (or ``root`` itself, if a file). + + A syntax error is a failure, not a skip: silently skipping would let + an unparseable file drop out of the survey and hollow out the guard. + """ + paths = [root] if root.is_file() else sorted(root.rglob("*.py")) + parsed = [] + for path in paths: + source = path.read_text(encoding="utf-8") + try: + parsed.append((path, ast.parse(source, str(path)))) + except SyntaxError as exc: + raise AssertionError(f"{path} does not parse: {exc}") from exc + return parsed + + +@lru_cache(maxsize=None) +def _parsed(root_str: str): + """Cached ``_parse``; four helpers walk the ~38k-line acli tree.""" + return tuple(_parse(pathlib.Path(root_str))) + + +@lru_cache(maxsize=None) +def _imports_under(root_str: str): + """{module: [file:line]} for absolute ``dashscope.*`` imports.""" + found = {} + for path, tree in _parsed(root_str): + for node in ast.walk(tree): + if isinstance(node, ast.Import): + modules = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom): + if node.level != 0 or not node.module: + continue + modules = [node.module] + else: + continue + for module in modules: + if module.split(".")[0] != "dashscope": + continue + found.setdefault(module, []).append(f"{path}:{node.lineno}") + return found + + +@lru_cache(maxsize=None) +def _from_import_names(root_str: str): + """{module: sorted(names)} for ``from import a, b``. + + Only ``dashscope.*`` is collected: acli's own optional extras (openai, + playwright, PIL, sounddevice) are upstream's business and are not + installed here, so probing them would test this machine rather than + the contract. + """ + found = {} + for _, tree in _parsed(root_str): + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom): + continue + if node.level != 0 or not node.module: + continue + if node.module.split(".")[0] != "dashscope": + continue + names = [alias.name for alias in node.names if alias.name != "*"] + if names: + found.setdefault(node.module, set()).update(names) + return {mod: sorted(names) for mod, names in found.items()} + + +def _acli_external_imports(): + imports = _imports_under(str(_ACLI)) + return {m: loc for m, loc in imports.items() if not _is_acli_internal(m)} + + +def _acli_external_names(): + names = _from_import_names(str(_ACLI)) + return {m: n for m, n in names.items() if not _is_acli_internal(m)} + + +def _root_global_writes(): + """[(attr, file:line)] where acli assigns to ``dashscope.``.""" + writes = [] + for path, tree in _parsed(str(_ACLI)): + for node in ast.walk(tree): + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if not isinstance(target, ast.Attribute): + continue + base = target.value + if isinstance(base, ast.Name) and base.id == "dashscope": + writes.append((target.attr, f"{path}:{node.lineno}")) + return writes + + +def _environ_setdefault_calls(): + """[file:line] of ``os.environ.setdefault("DASHSCOPE_API_KEY", ...)``.""" + calls = [] + for path, tree in _parsed(str(_ACLI)): + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not node.args: + continue + func = node.func + if not isinstance(func, ast.Attribute): + continue + if func.attr != "setdefault": + continue + env = func.value + if not ( + isinstance(env, ast.Attribute) + and env.attr == "environ" + and isinstance(env.value, ast.Name) + and env.value.id == "os" + ): + continue + key = node.args[0] + if isinstance(key, ast.Constant) and ( + key.value == "DASHSCOPE_API_KEY" + ): + calls.append(f"{path}:{node.lineno}") + return calls + + +# --------------------------------------------------------------------------- +# Dependency surface +# --------------------------------------------------------------------------- + + +def test_acli_external_dashscope_imports_match_allowlist(): + found = _acli_external_imports() + added = sorted(set(found) - _ALLOWLIST) + gone = sorted(_ALLOWLIST - set(found)) + assert not added and not gone, ( + "acli's coupling to the SDK changed.\n" + f" added by a sync (accept consciously, then extend _ALLOWLIST): " + f"{ {m: found[m] for m in added} }\n" + f" no longer imported (_ALLOWLIST is stale): {gone}" + ) + + +def test_acli_does_not_import_the_v2_package(): + v2 = { + m: loc + for m, loc in _acli_external_imports().items() + if m.split(".")[1:2] == ["v2"] + } + assert not v2, ( + "acli is a 1.x-side consumer today; moving it onto the 2.0 client " + f"is upstream agenticCLI work, not this repo's: {v2}" + ) + + +# --------------------------------------------------------------------------- +# Audio contract +# --------------------------------------------------------------------------- + + +def test_every_name_acli_imports_from_the_sdk_resolves(): + names = _acli_external_names() + assert names, "expected acli to import names from the SDK" + broken = {} + for module, wanted in sorted(names.items()): + try: + imported = importlib.import_module(module) + except ImportError as exc: + broken[module] = f"import failed: {exc}" + continue + absent = [name for name in wanted if not hasattr(imported, name)] + if absent: + broken[module] = absent + assert not broken, f"SDK surface acli relies on is incomplete: {broken}" + + +def test_tts_v2_exposes_the_names_acli_uses(): + module = importlib.import_module("dashscope.legacy_1x.audio.tts_v2") + for name in ("AudioFormat", "ResultCallback", "SpeechSynthesizer"): + assert hasattr( + module, + name, + ), f"dashscope.legacy_1x.audio.tts_v2.{name} is gone" + + +def test_asr_exposes_the_names_acli_uses(): + module = importlib.import_module("dashscope.legacy_1x.audio.asr") + for name in ("Recognition", "RecognitionCallback"): + assert hasattr( + module, + name, + ), f"dashscope.legacy_1x.audio.asr.{name} is gone" + + +# --------------------------------------------------------------------------- +# The root global acli writes +# --------------------------------------------------------------------------- + + +def test_root_api_key_is_readable_and_writable(monkeypatch): + assert hasattr(dashscope, "api_key"), ( + "the root package must keep exposing ``api_key``: acli's voice " + "output reads it and then assigns to it" + ) + monkeypatch.setattr(dashscope, "api_key", "sk-acli-contract") + assert dashscope.api_key == "sk-acli-contract" + + +def test_acli_tts_still_writes_the_root_api_key(): + attrs = {attr for attr, _ in _root_global_writes()} + assert "api_key" in attrs, ( + "acli no longer assigns ``dashscope.api_key``. If upstream moved " + "to the environment variable alone, the root-global requirement " + "has genuinely relaxed -- confirm that, then drop this test and " + "the one above it." + ) + + +def test_acli_also_sets_the_env_var(): + assert _environ_setdefault_calls(), ( + "acli is expected to os.environ.setdefault('DASHSCOPE_API_KEY', " + "...) alongside the root global, so both discovery paths work" + ) + + +# --------------------------------------------------------------------------- +# Neutrality: acli belongs to neither side +# --------------------------------------------------------------------------- + + +def test_v2_package_does_not_import_acli(): + hits = { + m: loc + for m, loc in _imports_under(str(_V2)).items() + if _is_acli_internal(m) + } + assert not hits, f"2.0 must not depend on acli: {hits}" + + +def test_package_root_does_not_import_acli(): + hits = { + m: loc + for m, loc in _imports_under(str(_ROOT_INIT)).items() + if _is_acli_internal(m) + } + assert not hits, f"the 1.x package root must not depend on acli: {hits}" + + +def test_acli_lives_at_the_top_level(): + """acli is a neutral surface: neither the 2.0 nor the legacy bucket.""" + assert _ACLI.is_dir(), "dashscope/acli/ must stay at the top level" + moved_into_v2 = (_REPO / "dashscope" / "v2" / "acli").exists() + moved_into_legacy = (_REPO / "dashscope" / "legacy" / "acli").exists() + assert not moved_into_v2, "acli must not be folded into 2.0" + assert not moved_into_legacy, "acli must not land in the legacy bucket" + + +def test_acli_is_not_reexported_at_the_package_root(): + assert "acli" not in dashscope.__all__ + source = _ROOT_INIT.read_text(encoding="utf-8") + assert "acli" not in source, ( + "acli is an independently installed entry point, not part of the " + "1.x API layer" + ) + + +# --------------------------------------------------------------------------- +# Entry point and packaging +# --------------------------------------------------------------------------- + + +def test_module_entry_point_resolvable(): + spec = importlib.util.find_spec("dashscope.acli.__main__") + assert spec is not None, "``python -m dashscope.acli`` needs __main__.py" + main = importlib.import_module("dashscope.acli.cli").main + assert callable(main) + + +def test_setup_declares_the_acli_extras(): + tree = ast.parse( + (_REPO / "setup.py").read_text(encoding="utf-8"), + "setup.py", + ) + groups = None + for node in ast.walk(tree): + if isinstance(node, ast.keyword) and node.arg == "extras_require": + groups = { + key.value + for key in node.value.keys + if isinstance(key, ast.Constant) + } + assert groups is not None, "setup.py lost its extras_require" + missing = sorted(_ACLI_EXTRAS - groups) + assert not missing, f"setup.py extras_require is missing: {missing}" diff --git a/tests/unit/test_agentic_rl_cli_opd.py b/tests/unit/test_agentic_rl_cli_opd.py index e217842e..59620f36 100644 --- a/tests/unit/test_agentic_rl_cli_opd.py +++ b/tests/unit/test_agentic_rl_cli_opd.py @@ -8,7 +8,7 @@ from typer.main import get_command from typer.testing import CliRunner -from dashscope.cli.agentic_rl import _run_workflow_async, app +from dashscope.legacy_1x.cli.agentic_rl import _run_workflow_async, app from dashscope.finetune.agentic_rl import AgenticRL @@ -16,14 +16,16 @@ def test_run_help_explains_conditional_function_requirements(): result = CliRunner().invoke(app, ["run", "--help"]) assert result.exit_code == 0 - # 参数存在性走 typer 命令模型校验:渲染文本在不同终端宽度/平台下会被 - # rich 截断或连字符换行(CI Linux 曾因此误报 --teacher-model 缺失), - # 不断言渲染后的 help 文本里出现完整 option 名。 + # Parameter presence is asserted through typer's command model: rich + # truncates or hyphen-wraps rendered help at some terminal widths + # (CI Linux once false-reported --teacher-model as missing), so we do + # not assert full option names in the rendered text. run_cmd = get_command(app).commands["run"] assert "teacher_model" in {param.name for param in run_cmd.params} - # help 文档仍须说明 OPD 的条件化函数要求;剥离 ANSI 后按空白归一, - # 只断言散文式描述(按空格折行,归一化后稳定)。 + # The help text must still document OPD's conditional function + # requirements; strip ANSI and normalize whitespace, then assert only + # the prose description (folded at spaces, stable once normalized). help_text = " ".join(re.sub(r"\x1b\[[0-9;]*m", "", result.output).split()) assert ( "1. Configuration-driven: Use -c/--config to specify a YAML file" @@ -52,7 +54,7 @@ async def inspect_config(client): with ( patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ), patch.object( AgenticRL, diff --git a/tests/unit/test_agentic_rl_submit_job.py b/tests/unit/test_agentic_rl_submit_job.py index 9fc79ffb..0da6ed38 100644 --- a/tests/unit/test_agentic_rl_submit_job.py +++ b/tests/unit/test_agentic_rl_submit_job.py @@ -392,7 +392,10 @@ async def test_main_workflow_run_exception(self): new_callable=AsyncMock, side_effect=Exception("Network error"), ): + # astroid reads __new__ as a classmethod through the 1.x->v2 MRO + # pylint: disable=no-value-for-parameter client = AgenticRL.__new__(AgenticRL) + # pylint: enable=no-value-for-parameter with pytest.raises(Exception, match="Network error"): await client.run( model="qwen3.5-9b", @@ -499,7 +502,7 @@ def test_init_with_valid_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: client = AgenticRL(api_key="sk-test-key") mock_set_key.assert_called_once_with("sk-test-key") @@ -512,7 +515,7 @@ def test_init_with_invalid_api_key(self): ) with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", side_effect=Exception("Invalid key"), ): with pytest.raises(ValueErrorWithCode): @@ -522,7 +525,7 @@ def test_init_without_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL() mock_set_key.assert_called_once_with(None) @@ -531,7 +534,7 @@ def test_init_with_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL(api_key="your_api_key_here") mock_set_key.assert_called_once_with("your_api_key_here") @@ -659,7 +662,7 @@ def test_submit_job_builds_correct_request(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.generate_random_id", + "dashscope.v2.finetune.agentic_rl.generate_random_id", return_value="abcdef1234567890", ): client = AgenticRL() diff --git a/tests/unit/test_aio_session.py b/tests/unit/test_aio_session.py index b82061a9..e236fd90 100644 --- a/tests/unit/test_aio_session.py +++ b/tests/unit/test_aio_session.py @@ -16,7 +16,7 @@ import aiohttp import pytest -from dashscope.api_entities import aio_session +from dashscope.legacy_1x.api_entities import aio_session class TestSSLContextCaching: diff --git a/tests/unit/test_aio_session_cleanup.py b/tests/unit/test_aio_session_cleanup.py index 2e2c4c0d..3d95cf3d 100644 --- a/tests/unit/test_aio_session_cleanup.py +++ b/tests/unit/test_aio_session_cleanup.py @@ -33,7 +33,9 @@ def test_single_thread(): warnings.filterwarnings("always", category=ResourceWarning) async def simulate_multimodal_conversation(): - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) print("[Step 1] Creating shared aiohttp session...") session = await get_shared_aio_session() @@ -63,7 +65,9 @@ def run_async_in_thread(thread_id): """Run async code in a separate thread with its own event loop.""" async def create_session(): - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) session = await get_shared_aio_session() print(f" Thread {thread_id}: Session {id(session)} created") @@ -146,7 +150,9 @@ async def test_concurrent_tasks_same_loop(): print("=" * 70) print() - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) num_tasks = 10 print(f"Creating {num_tasks} concurrent tasks in same event loop...") diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 480c0712..ead3b65f 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -10,7 +10,7 @@ from typing import Dict from dashscope import Application -from dashscope.app.application_response import ApplicationResponse +from dashscope.legacy_1x.app.application_response import ApplicationResponse from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_asr_phrases.py b/tests/unit/test_asr_phrases.py index 942b55bb..29d2f75c 100644 --- a/tests/unit/test_asr_phrases.py +++ b/tests/unit/test_asr_phrases.py @@ -10,7 +10,7 @@ import pytest import dashscope -from dashscope.audio.asr import AsrPhraseManager +from dashscope.legacy_1x.audio.asr import AsrPhraseManager from tests.unit.constants import TEST_JOB_ID from tests.unit.mock_request_base import MockRequestBase @@ -115,10 +115,15 @@ def complete_url(url: str) -> None: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [ + base_url, + "api-ws", + dashscope.legacy_1x.common.env.api_version, + "inference", + ], ) dashscope.base_http_api_url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/unit/test_assistant_files.py b/tests/unit/test_assistant_files.py index 1ea08119..b6c22365 100644 --- a/tests/unit/test_assistant_files.py +++ b/tests/unit/test_assistant_files.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope.assistants.files import Files +from dashscope.legacy_1x.assistants.files import Files from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_async_custom_session.py b/tests/unit/test_async_custom_session.py index 10d31051..4d8f1757 100644 --- a/tests/unit/test_async_custom_session.py +++ b/tests/unit/test_async_custom_session.py @@ -2,16 +2,16 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """ -异步 HTTP 自定义 Session 功能单元测试 +Unit tests for async HTTP custom session support. -测试范围: -1. HttpRequest 接受自定义 aiohttp.ClientSession 参数 -2. 自定义 aio_session 的使用和资源管理 -3. 临时 aio_session 的创建和清理 -4. Session 优先级逻辑 -5. 不同场景下的异步 Session 行为 +Scope: +1. HttpRequest accepts a custom aiohttp.ClientSession parameter +2. Usage and resource management of a custom aio_session +3. Creation and cleanup of temporary aio_session instances +4. Session priority logic +5. Async session behavior across different scenarios -注意:所有测试都不依赖真实的 API Key +Note: none of the tests rely on a real API key. """ # pylint: disable=protected-access,unused-argument,unused-variable @@ -24,17 +24,17 @@ import certifi import pytest -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class TestAsyncSessionBasics: - """测试异步 Session 基本功能""" + """Basic tests for async sessions""" @pytest.mark.asyncio async def test_http_request_accepts_aio_session_parameter(self): - """测试 HttpRequest 接受 aio_session 参数""" + """Test that HttpRequest accepts the aio_session parameter""" connector = aiohttp.TCPConnector() custom_session = aiohttp.ClientSession(connector=connector) @@ -53,7 +53,7 @@ async def test_http_request_accepts_aio_session_parameter(self): @pytest.mark.asyncio async def test_http_request_without_aio_session_parameter(self): - """测试 HttpRequest 不传 aio_session 参数""" + """Test HttpRequest without the aio_session parameter""" http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -64,8 +64,8 @@ async def test_http_request_without_aio_session_parameter(self): @pytest.mark.asyncio async def test_aio_session_parameter_is_optional(self): - """测试 aio_session 参数是可选的""" - # 不传 aio_session 参数应该正常工作 + """Test that the aio_session parameter is optional""" + # Works without the aio_session parameter http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -78,12 +78,12 @@ async def test_aio_session_parameter_is_optional(self): class TestAsyncSessionUsage: - """测试异步 Session 的实际使用""" + """Tests for actual async session usage""" @pytest.mark.asyncio async def test_custom_aio_session_is_used_for_request(self): - """测试自定义 aio_session 被实际用于请求""" - # 创建 mock session + """Test that the custom aio_session is actually used for the request""" + # Create a mock session mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -97,7 +97,7 @@ async def mock_request(*_args, **_kwargs): mock_session.request = mock_request - # 创建 HttpRequest 并传入自定义 session + # Create an HttpRequest with the custom session http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -106,7 +106,7 @@ async def mock_request(*_args, **_kwargs): session=mock_session, ) - # 添加请求数据 + # Add request data request_data = ApiRequestData( model="test-model", task_group="test", @@ -119,7 +119,7 @@ async def mock_request(*_args, **_kwargs): ) http_request.data = request_data - # 执行请求 + # Execute the request async def mock_handle_response(_response): yield mock_response @@ -130,15 +130,16 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # 验证自定义 session 没有被关闭 + # Verify the custom session was not closed mock_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_is_used_when_no_custom_session( self, ): - """测试没有自定义 aio_session 时使用共享 session(不被关闭)""" - # 创建 mock shared session + """Test that the shared session is used (and not closed) when no + custom aio_session is given""" + # Create a mock shared session mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -147,7 +148,7 @@ async def test_shared_aio_session_is_used_when_no_custom_session( mock_response.__aexit__ = AsyncMock(return_value=None) mock_session.request.return_value = mock_response - # 创建 HttpRequest 不传 aio_session + # Create an HttpRequest without aio_session http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -155,7 +156,7 @@ async def test_shared_aio_session_is_used_when_no_custom_session( stream=False, ) - # 添加请求数据 + # Add request data request_data = ApiRequestData( model="test-model", task_group="test", @@ -168,12 +169,12 @@ async def test_shared_aio_session_is_used_when_no_custom_session( ) http_request.data = request_data - # 执行请求 + # Execute the request async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -183,16 +184,17 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # 共享 session 不应被关闭(由 aio_session 模块管理生命周期) + # The shared session must not be closed (lifecycle is managed + # by the aio_session module) mock_session.close.assert_not_called() class TestAsyncSessionResourceManagement: - """测试异步 Session 资源管理""" + """Tests for async session resource management""" @pytest.mark.asyncio async def test_custom_aio_session_not_closed_by_http_request(self): - """测试自定义 aio_session 不会被 HttpRequest 关闭""" + """Test that HttpRequest does not close the custom aio_session""" custom_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -236,12 +238,13 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # 验证自定义 aio_session 没有被关闭 + # Verify the custom aio_session was not closed custom_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_not_closed_on_success(self): - """测试共享 aio_session 在成功后不被关闭(由模块管理生命周期)""" + """Test that the shared aio_session is not closed on success + (lifecycle managed by the module)""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -273,7 +276,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -283,12 +286,12 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # 共享 session 不应被关闭 + # The shared session must not be closed mock_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_not_closed_on_exception(self): - """测试共享 aio_session 在异常时也不被关闭""" + """Test that the shared aio_session is not closed on exceptions either""" mock_session = AsyncMock() # Make request() raise an exception @@ -316,24 +319,24 @@ async def mock_request(*_args, **_kwargs): ) http_request.data = request_data - # 执行请求应该抛出异常 + # Executing the request should raise with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(Exception, match="Network error"): _ = await http_request.aio_call() - # 共享 session 仍然不应被关闭(由模块管理生命周期) + # The shared session still must not be closed (lifecycle managed by the module) mock_session.close.assert_not_called() class TestAsyncSessionWithCustomConfiguration: - """测试自定义配置的异步 Session""" + """Tests for async sessions with custom configuration""" @pytest.mark.asyncio async def test_custom_aio_session_with_connector(self): - """测试带自定义 connector 的 aio_session""" + """Test an aio_session with a custom connector""" connector = aiohttp.TCPConnector( limit=100, limit_per_host=30, @@ -350,14 +353,14 @@ async def test_custom_aio_session_with_connector(self): ) assert http_request._external_aio_session is custom_session - # 验证 connector 已配置 + # Verify the connector is configured assert custom_session.connector is not None finally: await custom_session.close() @pytest.mark.asyncio async def test_custom_aio_session_with_headers(self): - """测试带自定义 headers 的 aio_session""" + """Test an aio_session with custom headers""" custom_headers = { "User-Agent": "Custom-Agent/1.0", "X-Custom-Header": "custom-value", @@ -373,14 +376,14 @@ async def test_custom_aio_session_with_headers(self): ) assert http_request._external_aio_session is custom_session - # 验证 headers 已配置 + # Verify the headers are configured assert "User-Agent" in custom_session.headers finally: await custom_session.close() @pytest.mark.asyncio async def test_custom_aio_session_with_timeout(self): - """测试带自定义 timeout 的 aio_session""" + """Test an aio_session with a custom timeout""" timeout = aiohttp.ClientTimeout(total=60) custom_session = aiohttp.ClientSession(timeout=timeout) @@ -393,18 +396,18 @@ async def test_custom_aio_session_with_timeout(self): ) assert http_request._external_aio_session is custom_session - # 验证 timeout 已配置 + # Verify the timeout is configured assert custom_session.timeout is not None finally: await custom_session.close() class TestAsyncSessionPriority: - """测试异步 Session 优先级""" + """Tests for async session priority""" @pytest.mark.asyncio async def test_custom_aio_session_has_priority(self): - """测试自定义 aio_session 优先于临时 aio_session""" + """Test that a custom aio_session takes priority over a temporary one""" connector = aiohttp.TCPConnector() custom_session = aiohttp.ClientSession(connector=connector) @@ -416,7 +419,7 @@ async def test_custom_aio_session_has_priority(self): session=custom_session, ) - # 验证存储了自定义 aio_session + # Verify the custom aio_session is stored assert http_request._external_aio_session is custom_session assert http_request._external_aio_session is not None finally: @@ -424,11 +427,11 @@ async def test_custom_aio_session_has_priority(self): class TestAsyncSessionWithDifferentMethods: - """测试不同 HTTP 方法的异步 Session 使用""" + """Tests for async session usage across HTTP methods""" @pytest.mark.asyncio async def test_custom_aio_session_with_post_request(self): - """测试 POST 请求使用自定义 session""" + """Test that POST requests use the custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -475,7 +478,7 @@ async def mock_handle_response(_response): @pytest.mark.asyncio async def test_custom_aio_session_with_get_request(self): - """测试 GET 请求使用自定义 session""" + """Test that GET requests use the custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -522,11 +525,11 @@ async def mock_handle_response(_response): class TestAsyncBackwardCompatibility: - """测试异步向后兼容性""" + """Tests for async backward compatibility""" @pytest.mark.asyncio async def test_works_without_aio_session_parameter(self): - """测试不传 aio_session 参数时保持原有行为""" + """Test that omitting aio_session keeps the original behavior""" http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -534,16 +537,16 @@ async def test_works_without_aio_session_parameter(self): stream=False, ) - # 验证不传 aio_session 时,_external_aio_session 为 None + # Verify _external_aio_session is None when aio_session is not passed assert http_request._external_aio_session is None - # 验证其他参数正常 + # Verify the other parameters are unaffected assert http_request.url == "http://example.com/api" assert http_request.method == HTTPMethod.POST @pytest.mark.asyncio async def test_default_behavior_uses_shared_session(self): - """测试默认行为使用共享 session(不被关闭)""" + """Test that the default behavior uses the shared session (not closed)""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -552,7 +555,7 @@ async def test_default_behavior_uses_shared_session(self): mock_response.__aexit__ = AsyncMock(return_value=None) mock_session.request.return_value = mock_response - # 不传 aio_session 参数 + # Do not pass the aio_session parameter http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -576,7 +579,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -586,16 +589,17 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # 共享 session 不应被关闭(生命周期由 aio_session 模块管理) + # The shared session must not be closed (lifecycle managed + # by the aio_session module) mock_session.close.assert_not_called() class TestAsyncSessionLifecycle: - """测试异步 Session 生命周期""" + """Tests for the async session lifecycle""" @pytest.mark.asyncio async def test_multiple_requests_with_same_custom_session(self): - """测试使用同一个自定义 session 进行多次请求""" + """Test multiple requests with the same custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -608,7 +612,7 @@ async def mock_request(*_args, **_kwargs): mock_session.request = mock_request - # 第一次请求 + # First request http_request1 = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -639,7 +643,7 @@ async def mock_handle_response(_response): ): _ = await http_request1.aio_call() - # 第二次请求 + # Second request http_request2 = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -667,12 +671,13 @@ async def mock_handle_response(_response): ): _ = await http_request2.aio_call() - # 验证 session 没有被关闭(因为是外部 session) + # Verify the session was not closed (it is an external session) mock_session.close.assert_not_called() class TestAsyncConnectionRetry: - """测试异步池化连接被远端关闭后的重试行为""" + """Tests for retry behavior after a pooled async connection is closed + by the remote peer""" def _build_post_request(self): http_request = HttpRequest( @@ -704,7 +709,7 @@ def _ok_response(): @pytest.mark.asyncio async def test_retry_once_on_connection_error(self): - """连接被远端关闭时重试一次并成功""" + """Retries once and succeeds when the connection is closed by the remote peer""" mock_session = AsyncMock() mock_response = self._ok_response() mock_session.request.side_effect = [ @@ -718,7 +723,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -732,7 +737,7 @@ async def mock_handle_response(_response): @pytest.mark.asyncio async def test_retry_exhausted_raises(self): - """连续两次 ClientConnectionError 后向上抛出""" + """Raises after two consecutive ClientConnectionErrors""" mock_session = AsyncMock() mock_session.request.side_effect = aiohttp.ClientConnectionError( "Server disconnected", @@ -741,7 +746,7 @@ async def test_retry_exhausted_raises(self): http_request = self._build_post_request() with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(aiohttp.ClientConnectionError): @@ -751,14 +756,14 @@ async def test_retry_exhausted_raises(self): @pytest.mark.asyncio async def test_no_retry_on_other_exceptions(self): - """非连接类异常不重试""" + """Does not retry non-connection errors""" mock_session = AsyncMock() mock_session.request.side_effect = ValueError("bad request") http_request = self._build_post_request() with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(ValueError, match="bad request"): diff --git a/tests/unit/test_cli_agent_route.py b/tests/unit/test_cli_agent_route.py index 46375f57..e5200335 100644 --- a/tests/unit/test_cli_agent_route.py +++ b/tests/unit/test_cli_agent_route.py @@ -23,7 +23,7 @@ import pytest import dashscope -from dashscope import cli +from dashscope.legacy_1x import cli @pytest.fixture() diff --git a/tests/unit/test_cli_application.py b/tests/unit/test_cli_application.py index 957f8300..71458a55 100644 --- a/tests/unit/test_cli_application.py +++ b/tests/unit/test_cli_application.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import application +from dashscope.legacy_1x.cli import application class TestCliApplication: diff --git a/tests/unit/test_cli_auth.py b/tests/unit/test_cli_auth.py index f3997b45..84e95725 100644 --- a/tests/unit/test_cli_auth.py +++ b/tests/unit/test_cli_auth.py @@ -17,9 +17,9 @@ from typer.testing import CliRunner import dashscope -from dashscope.cli import auth as auth_cli -from dashscope.common import api_key as api_key_mod -from dashscope.common.constants import ( +from dashscope.legacy_1x.cli import auth as auth_cli +from dashscope.legacy_1x.common import api_key as api_key_mod +from dashscope.legacy_1x.common.constants import ( DASHSCOPE_API_KEY_ENV, DASHSCOPE_API_KEY_FILE_PATH_ENV, ) @@ -67,7 +67,7 @@ def key_store(monkeypatch, tmp_path): key_file = cache_dir / "api_key" cache_dir.mkdir(parents=True, exist_ok=True) # ``save_api_key``/``get_default_api_key`` read the constant from - # dashscope.common.api_key, while the printed path and the source check in + # dashscope.legacy_1x.common.api_key, while the printed path and the source check in # auth read its own imported copy. Both bindings have to move, or login and # whoami silently disagree about where the key lives. monkeypatch.setattr( diff --git a/tests/unit/test_cli_code_generation.py b/tests/unit/test_cli_code_generation.py index c421e5ee..8d2435e1 100644 --- a/tests/unit/test_cli_code_generation.py +++ b/tests/unit/test_cli_code_generation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import code_generation +from dashscope.legacy_1x.cli import code_generation class TestCliCodeGeneration: diff --git a/tests/unit/test_cli_common.py b/tests/unit/test_cli_common.py index f28f7375..ca149057 100644 --- a/tests/unit/test_cli_common.py +++ b/tests/unit/test_cli_common.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -"""Unit tests for CLI exit-code classification in dashscope.cli.common.""" +"""Unit tests for CLI exit-code classification in dashscope.legacy_1x.cli.common.""" from types import SimpleNamespace from typer.testing import CliRunner -from dashscope.cli.common import _exit_code_for -from dashscope.cli import embeddings +from dashscope.legacy_1x.cli.common import _exit_code_for +from dashscope.legacy_1x.cli import embeddings # --------------------------------------------------------------------------- diff --git a/tests/unit/test_cli_embeddings.py b/tests/unit/test_cli_embeddings.py index 9fd69d39..88bbf085 100644 --- a/tests/unit/test_cli_embeddings.py +++ b/tests/unit/test_cli_embeddings.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import embeddings +from dashscope.legacy_1x.cli import embeddings class TestCliEmbeddings: diff --git a/tests/unit/test_cli_image_generation.py b/tests/unit/test_cli_image_generation.py index 6a19f296..66788fc4 100644 --- a/tests/unit/test_cli_image_generation.py +++ b/tests/unit/test_cli_image_generation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import image_generation +from dashscope.legacy_1x.cli import image_generation class TestCliImageGeneration: diff --git a/tests/unit/test_cli_image_synthesis.py b/tests/unit/test_cli_image_synthesis.py index 142d6cac..b03835f4 100644 --- a/tests/unit/test_cli_image_synthesis.py +++ b/tests/unit/test_cli_image_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import image_synthesis +from dashscope.legacy_1x.cli import image_synthesis class TestCliImageSynthesis: diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index aac880ae..f013171b 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -13,12 +13,12 @@ from typer.testing import CliRunner import dashscope -from dashscope.cli import agentic_rl -from dashscope.cli import ( +from dashscope.legacy_1x.cli import agentic_rl +from dashscope.legacy_1x.cli import ( app as cli_app, main as cli_main, ) -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.common.error import AuthenticationError def strip_ansi_codes(text): @@ -53,7 +53,7 @@ def mock_list(**kwargs): raise AuthenticationError("No api key provided.") monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(sys, "argv", ["dashscope", "models", "list"]) @@ -77,7 +77,7 @@ def test_top_level_help_shows_global_api_key(self): def test_python_module_help_suppresses_urllib3_warning(self): result = subprocess.run( - [sys.executable, "-m", "dashscope.cli", "--help"], + [sys.executable, "-m", "dashscope.legacy_1x.cli", "--help"], check=False, capture_output=True, text=True, @@ -204,7 +204,7 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) @@ -236,7 +236,7 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) @@ -267,22 +267,25 @@ def test_rl_import_failure_is_silent_until_the_command_is_used( monkeypatch, capsys, ): - # The rl import is deferred, so importing dashscope.cli must not tell + # The rl import is deferred, so importing dashscope.legacy_1x.cli must not tell # the user about a dependency they may never ask for. - monkeypatch.setattr(dashscope.cli, "_RL_IMPORT_ERROR", None) + monkeypatch.setattr(dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", None) monkeypatch.setitem( sys.modules, - "dashscope.cli.agentic_rl", + "dashscope.legacy_1x.cli.agentic_rl", _ImportErrorOnApp( "Agentic RL fine-tuning dependencies are missing from this " "install. Reinstall with: pip install -U dashscope", ), ) - dashscope.cli._register_rl_app() + dashscope.legacy_1x.cli._register_rl_app() assert capsys.readouterr().err == "" - assert isinstance(dashscope.cli._RL_IMPORT_ERROR, ImportError) + assert isinstance( + dashscope.legacy_1x.cli._RL_IMPORT_ERROR, + ImportError, + ) def test_rl_command_prints_the_install_command_verbatim( self, @@ -292,7 +295,7 @@ def test_rl_command_prints_the_install_command_verbatim( # The error already carries its own remedy, so the fallback hint in # _rl_unavailable_detail must not be appended a second time. monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError( "Agentic RL fine-tuning dependencies are missing from this " @@ -315,7 +318,7 @@ def test_rl_command_adds_a_hint_when_the_error_has_none( capsys, ): monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError("No module named 'typer_x'"), ) @@ -334,13 +337,13 @@ def test_unrelated_command_is_not_told_about_missing_rl_dependencies( capsys, ): monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError("Reinstall with: pip install -U dashscope"), ) monkeypatch.setattr(sys, "argv", ["dashscope"]) monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_route_to_expert", lambda *args, **kwargs: None, ) @@ -363,10 +366,13 @@ def mock_upload(model, file_path, api_key, base_address=None): return "oss://uploaded", None monkeypatch.setattr( - "dashscope.cli.oss.os.path.exists", + "dashscope.legacy_1x.cli.oss.os.path.exists", lambda path: True, ) - monkeypatch.setattr("dashscope.cli.oss.OssUtils.upload", mock_upload) + monkeypatch.setattr( + "dashscope.legacy_1x.cli.oss.OssUtils.upload", + mock_upload, + ) monkeypatch.setattr(dashscope, "api_key", "global-key") monkeypatch.setattr( sys, @@ -471,7 +477,7 @@ def mock_call(model, prompt, stream=False): ) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) monkeypatch.setattr( @@ -530,15 +536,15 @@ def mock_deployments_list(**kwargs): return result monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", + "dashscope.legacy_1x.cli.files.dashscope.Files.list", mock_files_list, ) monkeypatch.setattr( - "dashscope.cli.fine_tunes.dashscope.FineTunes.list", + "dashscope.legacy_1x.cli.fine_tunes.dashscope.FineTunes.list", mock_fine_tunes_list, ) monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.list", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.list", mock_deployments_list, ) @@ -571,7 +577,7 @@ def mock_files_list(**kwargs): return SimpleNamespace(status_code=200, output={}) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", + "dashscope.legacy_1x.cli.files.dashscope.Files.list", mock_files_list, ) monkeypatch.setattr( @@ -592,7 +598,7 @@ def test_files_upload_missing_file_exits_without_traceback( capsys, ): monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", + "dashscope.legacy_1x.cli.files.os.path.exists", lambda path: False, ) monkeypatch.setattr( @@ -620,11 +626,11 @@ def mock_upload(**kwargs): raise RuntimeError("upload failed") monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", + "dashscope.legacy_1x.cli.files.os.path.exists", lambda path: True, ) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.upload", + "dashscope.legacy_1x.cli.files.dashscope.Files.upload", mock_upload, ) monkeypatch.setattr( @@ -699,7 +705,7 @@ def mock_call(*_args, **_kwargs): return FailingStream() monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -728,7 +734,7 @@ def mock_list(**kwargs): raise RuntimeError("models failed") monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) @@ -746,7 +752,7 @@ def mock_call(**kwargs): raise RuntimeError("deployment failed") monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.call", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.call", mock_call, ) @@ -773,11 +779,11 @@ def mock_get(*_args, **_kwargs): raise RuntimeError("network down") monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.call", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.call", mock_call, ) monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.get", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.get", mock_get, ) @@ -799,7 +805,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.application.dashscope.Application.call", + "dashscope.legacy_1x.cli.application.dashscope.Application.call", mock_call, ) @@ -817,7 +823,7 @@ def mock_call(*_args, **_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -840,7 +846,7 @@ def mock_call(*_args, **_kwargs): ) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -869,7 +875,7 @@ def mock_call(**kwargs): raise RuntimeError("image failed") monkeypatch.setattr( - "dashscope.cli.image_generation.ImageGeneration.call", + "dashscope.legacy_1x.cli.image_generation.ImageGeneration.call", mock_call, ) @@ -897,7 +903,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.multimodal_conversation.dashscope." + "dashscope.legacy_1x.cli.multimodal_conversation.dashscope." "MultiModalConversation.call", mock_call, ) @@ -926,7 +932,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"embeddings": []}) monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope." + "dashscope.legacy_1x.cli.multimodal_embedding.dashscope." "MultiModalEmbedding.call", mock_call, ) @@ -955,7 +961,7 @@ def mock_call(**_kwargs): raise RuntimeError("embedding failed") monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope." + "dashscope.legacy_1x.cli.multimodal_embedding.dashscope." "MultiModalEmbedding.call", mock_call, ) diff --git a/tests/unit/test_cli_models.py b/tests/unit/test_cli_models.py index 60917fc0..7f6b0b37 100644 --- a/tests/unit/test_cli_models.py +++ b/tests/unit/test_cli_models.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import models +from dashscope.legacy_1x.cli import models class TestCliModels: diff --git a/tests/unit/test_cli_multimodal_conversation.py b/tests/unit/test_cli_multimodal_conversation.py index 245efbc6..ebbf7b64 100644 --- a/tests/unit/test_cli_multimodal_conversation.py +++ b/tests/unit/test_cli_multimodal_conversation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import multimodal_conversation +from dashscope.legacy_1x.cli import multimodal_conversation class TestCliMultiModalConversation: diff --git a/tests/unit/test_cli_multimodal_embedding.py b/tests/unit/test_cli_multimodal_embedding.py index 0058b0e7..d52621ed 100644 --- a/tests/unit/test_cli_multimodal_embedding.py +++ b/tests/unit/test_cli_multimodal_embedding.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import multimodal_embedding +from dashscope.legacy_1x.cli import multimodal_embedding class TestCliMultiModalEmbedding: diff --git a/tests/unit/test_cli_rerank.py b/tests/unit/test_cli_rerank.py index 6380b39f..ff09ac6a 100644 --- a/tests/unit/test_cli_rerank.py +++ b/tests/unit/test_cli_rerank.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import rerank +from dashscope.legacy_1x.cli import rerank class TestCliRerank: diff --git a/tests/unit/test_cli_speech_synthesis.py b/tests/unit/test_cli_speech_synthesis.py index efb7fd1e..5a561706 100644 --- a/tests/unit/test_cli_speech_synthesis.py +++ b/tests/unit/test_cli_speech_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import speech_synthesis +from dashscope.legacy_1x.cli import speech_synthesis class TestCliSpeechSynthesis: diff --git a/tests/unit/test_cli_tokenization.py b/tests/unit/test_cli_tokenization.py index 9da078b2..69b6ec4b 100644 --- a/tests/unit/test_cli_tokenization.py +++ b/tests/unit/test_cli_tokenization.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import tokenization +from dashscope.legacy_1x.cli import tokenization class TestCliTokenization: diff --git a/tests/unit/test_cli_transcription.py b/tests/unit/test_cli_transcription.py index 20cfc425..28ce93fa 100644 --- a/tests/unit/test_cli_transcription.py +++ b/tests/unit/test_cli_transcription.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import transcription +from dashscope.legacy_1x.cli import transcription class TestCliTranscription: diff --git a/tests/unit/test_cli_understanding.py b/tests/unit/test_cli_understanding.py index c15ea145..b0ca73d8 100644 --- a/tests/unit/test_cli_understanding.py +++ b/tests/unit/test_cli_understanding.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import understanding +from dashscope.legacy_1x.cli import understanding class TestCliUnderstanding: diff --git a/tests/unit/test_cli_video_synthesis.py b/tests/unit/test_cli_video_synthesis.py index c1af60ce..16930ec2 100644 --- a/tests/unit/test_cli_video_synthesis.py +++ b/tests/unit/test_cli_video_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import video_synthesis +from dashscope.legacy_1x.cli import video_synthesis class TestCliVideoSynthesis: diff --git a/tests/unit/test_code_generation.py b/tests/unit/test_code_generation.py index 831ce9c4..f378ad81 100644 --- a/tests/unit/test_code_generation.py +++ b/tests/unit/test_code_generation.py @@ -5,7 +5,7 @@ from http import HTTPStatus from dashscope import CodeGeneration -from dashscope.aigc.code_generation import ( +from dashscope.legacy_1x.aigc.code_generation import ( AttachmentRoleMessageParam, UserRoleMessageParam, ) diff --git a/tests/unit/test_conversation.py b/tests/unit/test_conversation.py index 8dedcc04..c1dc3265 100644 --- a/tests/unit/test_conversation.py +++ b/tests/unit/test_conversation.py @@ -5,9 +5,17 @@ from http import HTTPStatus from dashscope import Generation -from dashscope.aigc.conversation import Conversation, History, HistoryItem -from dashscope.api_entities.dashscope_response import Choice, Message, Role -from dashscope.common.message_manager import MessageManager +from dashscope.legacy_1x.aigc.conversation import ( + Conversation, + History, + HistoryItem, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + Choice, + Message, + Role, +) +from dashscope.legacy_1x.common.message_manager import MessageManager from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_dashscope_response.py b/tests/unit/test_dashscope_response.py index ed7a135c..599d6c5e 100644 --- a/tests/unit/test_dashscope_response.py +++ b/tests/unit/test_dashscope_response.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DictMixin +from dashscope.legacy_1x.api_entities.dashscope_response import DictMixin class TestDictMixin: diff --git a/tests/unit/test_http_speech_synthesizer.py b/tests/unit/test_http_speech_synthesizer.py index d59410df..bd72d156 100644 --- a/tests/unit/test_http_speech_synthesizer.py +++ b/tests/unit/test_http_speech_synthesizer.py @@ -5,8 +5,10 @@ import base64 from http import HTTPStatus -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, ) diff --git a/tests/unit/test_import_deprecation.py b/tests/unit/test_import_deprecation.py index 6ecce74d..86fe722c 100644 --- a/tests/unit/test_import_deprecation.py +++ b/tests/unit/test_import_deprecation.py @@ -78,19 +78,22 @@ def test_accessing_threads_emits_deprecation_warning(): def test_direct_assistants_import_emits_deprecation_warning(): - result = _run("import dashscope.assistants", error_on_deprecation=True) + result = _run( + "import dashscope.legacy_1x.assistants", + error_on_deprecation=True, + ) assert result.returncode != 0 assert "DeprecationWarning" in result.stderr def test_lazy_names_resolve_to_real_objects(): import dashscope - from dashscope.assistants import Assistant, Assistants - from dashscope.assistants.assistant_types import ( + from dashscope.legacy_1x.assistants import Assistant, Assistants + from dashscope.legacy_1x.assistants.assistant_types import ( AssistantFile, DeleteResponse, ) - from dashscope.threads import RunStepList, Threads + from dashscope.legacy_1x.threads import RunStepList, Threads assert dashscope.Assistants is Assistants assert dashscope.Assistant is Assistant diff --git a/tests/unit/test_layer_decoupling.py b/tests/unit/test_layer_decoupling.py new file mode 100644 index 00000000..a7ef2350 --- /dev/null +++ b/tests/unit/test_layer_decoupling.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The layer boundaries the 1.x / 2.0 decoupling rests on. + +``dashscope/_shared`` is the neutral layer both stacks import from, and +the 2.0 package must not reach into the 1.x *business* surface at all. +Neither rule is enforced by the interpreter: both are import-graph +properties that a well-meaning edit undoes silently, and both only fail +at the moment the 1.x tree moves to ``legacy_1x/`` and 2.0 is promoted to +the top level. So they are surveyed here as AST rather than left as +prose. + +``test_v2_does_not_import_the_1x_business_surface`` is where the design +doc's "hits must decrease monotonically" principle lands: zero. That +zero is the precondition for the physical move, not a consequence of it. +""" + +from __future__ import annotations + +import ast +import importlib +import pathlib + +_REPO = pathlib.Path(__file__).resolve().parents[2] +_DASHSCOPE = _REPO / "dashscope" +_SHARED = _DASHSCOPE / "_shared" +_V2 = _DASHSCOPE / "v2" + +_VERSION_FILE = "dashscope/version.py" + +# Second segment of a ``dashscope.*`` module that is not 1.x surface. +# ``acli`` serves both stacks and stays at the top level; ``_shared`` is +# the neutral layer itself. +_NEUTRAL_PACKAGES = frozenset({"v2", "_shared", "acli"}) + +# ``dashscope.version`` is distribution metadata, not 1.x API surface: it +# is a bare constant module with no imports of its own, so it does not +# move to ``legacy_1x/`` and importing it cannot close a cycle. It is read by +# the neutral layer and by the three v2 identity builders. +_NEUTRAL_LEAVES = frozenset({"dashscope.version"}) + +# Reward-server payloads that live under v2 but are deliberately not +# packaged. They import the old deep paths on purpose: once the layout +# move makes those paths canonical again the shims disappear and these +# files are correct without being edited. +_SKIP_PARTS = frozenset({"examples"}) + +# ``__version__`` bindings that are not the distribution version. +_OTHER_VERSION_OWNERS = frozenset( + { + # acli versions itself and is a one-way sync copy of the + # agenticCLI repo, so it must stay independent. + "dashscope/acli/__init__.py", + # The version of the vendored error-registry data. + "dashscope/v2/_error_registry.py", + }, +) + +_DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" + + +def _parse(path: pathlib.Path) -> ast.AST: + """Parse one module. A syntax error fails rather than skips.""" + source = path.read_text(encoding="utf-8") + try: + return ast.parse(source, str(path)) + except SyntaxError as exc: + raise AssertionError(f"{path} does not parse: {exc}") from exc + + +def _modules(root: pathlib.Path): + """Every module under ``root``, minus the excluded subtrees.""" + return [ + path + for path in sorted(root.rglob("*.py")) + if not _SKIP_PARTS.intersection(path.parts) + ] + + +def _dashscope_imports(tree: ast.AST): + """[(module, lineno)] for absolute ``dashscope.*`` imports.""" + found = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found.extend((alias.name, node.lineno) for alias in node.names) + elif isinstance(node, ast.ImportFrom): + if node.level == 0 and node.module: + found.append((node.module, node.lineno)) + return [ + (module, lineno) + for module, lineno in found + if module.split(".")[0] == "dashscope" + ] + + +def _is_neutral(module: str) -> bool: + if module in _NEUTRAL_LEAVES: + return True + parts = module.split(".") + return len(parts) > 1 and parts[1] in _NEUTRAL_PACKAGES + + +def _module_level_version_owners(tree: ast.AST): + """Line numbers of module-scope ``__version__ =`` assignments. + + Function-scope bindings are deliberately excluded. Two of the four + version readers -- ``v2/transport/http.default_user_agent`` and + ``v2/agentstudio/client._user_agent`` -- degrade with a local + ``except Exception: __version__ = "0.0.0"``. That rebinds a name + inside one function; it does not define the distribution version. + """ + found = [] + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if any( + isinstance(t, ast.Name) and t.id == "__version__" + for t in node.targets + ): + found.append(node.lineno) + return found + + +def _rel(path: pathlib.Path) -> str: + return path.relative_to(_REPO).as_posix() + + +def test_v2_does_not_import_the_1x_business_surface(): + """2.0 depends on the neutral layer and on nothing else in 1.x. + + This is the decreasing gate the decoupling is measured by, and the + precondition for moving 1.x to ``legacy_1x/``: every hit here is a deep + path that stops resolving the moment the tree it points at moves. + """ + offenders = [] + for path in _modules(_V2): + for module, lineno in _dashscope_imports(_parse(path)): + if _is_neutral(module): + continue + offenders.append(f"{_rel(path)}:{lineno} -> {module}") + assert not offenders, ( + "2.0 must not import the 1.x business surface; these would break " + "the moment 1.x moves to legacy_1x/:\n " + "\n ".join(offenders) + ) + + +def test_shared_layer_imports_no_1x_business_surface(): + """``_shared`` stays below both stacks, or it cannot anchor them. + + ``common/utils.py`` reaches ``_shared._sdk_ident`` while + ``dashscope/__init__`` is still executing, so a ``_shared`` module + that imports the 1.x surface closes a cycle through a partially + initialized root. ``dashscope.version`` is the one allowed exception + and is safe only because it imports nothing -- see + ``test_version_file_has_no_imports``. + """ + offenders = [] + for path in _modules(_SHARED): + for module, lineno in _dashscope_imports(_parse(path)): + if _is_neutral(module): + continue + offenders.append(f"{_rel(path)}:{lineno} -> {module}") + assert not offenders, ( + "dashscope/_shared may import only the stdlib, itself, and " + "dashscope.version:\n " + "\n ".join(offenders) + ) + + +def test_version_file_has_no_imports(): + """``dashscope/version.py`` must stay readable without importing. + + ``setup.py`` and ``docs/source/conf.py`` ``exec()`` this file to read + the version rather than importing it, which is what lets packaging + work in an isolated build environment where the runtime dependencies + are not installed. Any import added here -- a re-export of the value + from elsewhere, for instance -- turns reading one string into loading + the whole SDK and its dependencies. + """ + imports = [ + node.lineno + for node in ast.walk(_parse(_DASHSCOPE / "version.py")) + if isinstance(node, (ast.Import, ast.ImportFrom)) + ] + assert not imports, ( + f"{_VERSION_FILE} is exec'd standalone by setup.py and " + f"docs/source/conf.py, so it must not import anything; found " + f"imports at lines {imports}" + ) + + +def test_distribution_version_is_assigned_in_one_place(): + """One distribution version, defined once. + + A second literal would let the wheel's version and the version + reported in request headers drift apart, since ``setup.py`` reads the + file directly while the SDK reads the imported value. Only + module-scope bindings count; ``_OTHER_VERSION_OWNERS`` lists the two + that mean something else. + """ + owners = [] + for path in _modules(_DASHSCOPE): + rel = _rel(path) + if rel in _OTHER_VERSION_OWNERS: + continue + for lineno in _module_level_version_owners(_parse(path)): + owners.append((rel, lineno)) + located = [f"{rel}:{lineno}" for rel, lineno in owners] + assert [rel for rel, _ in owners] == [_VERSION_FILE], ( + "the distribution version must be assigned only in " + f"{_VERSION_FILE}; found: {located}" + ) + + +def test_one_version_reaches_every_identity_string(monkeypatch): + """Every request identity carries the same version segment. + + Each of these four sites reads the version through its own + defensive fallback to ``"0.0.0"``, so a read that failed would + degrade one telemetry string instead of failing loudly -- and the + header and the user-agent would disagree within one process. This + pins that they agree, including the two user-agents whose version + segment no other test looks at. + """ + monkeypatch.delenv(_DISABLE_ENV, raising=False) + version = importlib.import_module("dashscope.version").__version__ + + root = importlib.import_module("dashscope") + assert root.__version__ == version + + ident = importlib.import_module("dashscope._shared._sdk_ident") + headers = ident.get_sdk_headers() + assert headers["x-dashscope-sdk-client"].split("/")[1] == version + assert ident.get_user_agent().startswith(f"dashscope/{version};") + + v2_ident = importlib.import_module("dashscope.v2.transport._ident") + assert v2_ident.SDK_CLIENT_VALUE.split("/")[1] == version + + http = importlib.import_module("dashscope.v2.transport.http") + assert http.default_user_agent() == f"dashscope-v2-python/{version}" + + studio = importlib.import_module("dashscope.v2.agentstudio.client") + agent = studio._user_agent("https://agentstudio.invalid") + assert agent == ( + f"dashscope-agentstudio-python/{version} " + "(+https://agentstudio.invalid)" + ) diff --git a/tests/unit/test_maas_region.py b/tests/unit/test_maas_region.py index 0e1d340b..081ebc65 100644 --- a/tests/unit/test_maas_region.py +++ b/tests/unit/test_maas_region.py @@ -4,8 +4,8 @@ import unittest from unittest.mock import patch -from dashscope.common.env import MAAS_REGIONS, resolve_base_url -from dashscope.common.error import InputRequired +from dashscope.legacy_1x.common.env import MAAS_REGIONS, resolve_base_url +from dashscope.legacy_1x.common.error import InputRequired class TestResolveBaseUrl(unittest.TestCase): @@ -24,7 +24,7 @@ def test_resolve_with_workspace(self): ) def test_resolve_without_workspace_raises(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", None): @@ -32,7 +32,7 @@ def test_resolve_without_workspace_raises(self): resolve_base_url(url, None) def test_resolve_with_env_default_workspace(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", "ws-env"): @@ -58,7 +58,7 @@ def test_resolve_rejects_invalid_workspace(self): resolve_base_url(url, bad) def test_resolve_rejects_invalid_env_default_workspace(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", "evil.com/x"): @@ -70,13 +70,13 @@ class TestValidateWorkspaceId(unittest.TestCase): """Test validate_workspace_id character restrictions.""" def test_valid_ids(self): - from dashscope.common.env import validate_workspace_id + from dashscope.legacy_1x.common.env import validate_workspace_id for valid in ["ws-123", "llm-abc_def", "A0", "a" * 64]: self.assertEqual(validate_workspace_id(valid), valid) def test_invalid_ids(self): - from dashscope.common.env import validate_workspace_id + from dashscope.legacy_1x.common.env import validate_workspace_id for invalid in [ "", @@ -101,7 +101,7 @@ class TestValidateRegion(unittest.TestCase): """Test validate_region character restrictions.""" def test_valid_regions(self): - from dashscope.common.env import validate_region + from dashscope.legacy_1x.common.env import validate_region # Upper case is accepted: DNS is case-insensitive and httpx # lowercases the host, so rejecting it broke working callers. @@ -109,7 +109,7 @@ def test_valid_regions(self): self.assertEqual(validate_region(valid), valid) def test_invalid_regions(self): - from dashscope.common.env import validate_region + from dashscope.legacy_1x.common.env import validate_region for invalid in [ "", @@ -150,7 +150,7 @@ class TestMaasEnvLoading(unittest.TestCase): def _reload_env(self, env_vars): """Reload env module with given env vars.""" import importlib - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod with patch.dict(os.environ, env_vars, clear=False): importlib.reload(env_mod) @@ -159,7 +159,7 @@ def _reload_env(self, env_vars): def _restore_env(self): """Restore env module to default state.""" import importlib - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod with patch.dict( os.environ, @@ -271,7 +271,7 @@ def tearDown(self): dashscope.base_http_api_url = self._orig_http_url def _capture_request_url(self, func): - from dashscope.client import base_api as base_api_mod + from dashscope.legacy_1x.client import base_api as base_api_mod class ShortCircuit(Exception): pass @@ -326,7 +326,7 @@ def test_async_task_call_resolves_workspace(self): ) def test_call_without_workspace_raises_clear_error(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod from dashscope import Generation with patch.object(env_mod, "_default_workspace_id", None): diff --git a/tests/unit/test_messages.py b/tests/unit/test_messages.py index 633ccb86..8cbf0213 100644 --- a/tests/unit/test_messages.py +++ b/tests/unit/test_messages.py @@ -5,7 +5,7 @@ import uuid from dashscope import Messages # pylint: disable=no-name-in-module -from dashscope.threads.messages.files import Files +from dashscope.legacy_1x.threads.messages.files import Files from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_multimodal_dialog.py b/tests/unit/test_multimodal_dialog.py index ceaedb59..710c08ac 100644 --- a/tests/unit/test_multimodal_dialog.py +++ b/tests/unit/test_multimodal_dialog.py @@ -5,12 +5,12 @@ import time import pytest -from dashscope.multimodal.dialog_state import DialogState -from dashscope.multimodal.multimodal_dialog import ( +from dashscope.legacy_1x.multimodal.dialog_state import DialogState +from dashscope.legacy_1x.multimodal.multimodal_dialog import ( MultiModalDialog, MultiModalCallback, ) -from dashscope.multimodal.multimodal_request_params import ( +from dashscope.legacy_1x.multimodal.multimodal_request_params import ( Upstream, Downstream, ClientInfo, @@ -29,7 +29,7 @@ conver_instance = None -# 定义voice chat服务回调 +# Define the voice chat service callback class TestCallback(MultiModalCallback): def on_connected(self): logger.debug("connected with server.") @@ -43,13 +43,13 @@ def on_stopped(self): def on_state_changed(self, state: DialogState): if state == DialogState.LISTENING: - # app.update_text("开始收音。。。请提问") + # app.update_text("Listening... please ask") return if state == DialogState.THINKING: - # app.update_text("思考中。。。请耐心等待") + # app.update_text("Thinking... please wait") return if state == DialogState.RESPONDING: - # app.update_text("正在回答。。。") + # app.update_text("Answering...") return def on_speech_audio_data( @@ -64,7 +64,7 @@ def on_error(self, error): sys.exit(0) def on_responding_started(self): - # 开始端侧播放 + # Start device-side playback # pcm_play.start_play() global conver_instance # pylint: disable=global-variable-not-assigned if conver_instance is not None: @@ -86,7 +86,7 @@ def on_responding_content(self, payload): logger.debug(payload) def on_request_accepted(self): - # 服务端接受打断当前播报 + # The server accepts interrupting the current playback # pcm_play.cancel_play() return @@ -107,7 +107,7 @@ def setup_class(cls): @pytest.mark.skip def test_multimodal_dialog_one_turn(self): - # 对话状态 Listening->Thinking->Responding->Listening + # Dialog states: Listening->Thinking->Responding->Listening up_stream = Upstream( type="AudioOnly", mode="push2talk", @@ -136,7 +136,7 @@ def test_multimodal_dialog_one_turn(self): ) self.conversation.start("") - # 首轮进入Listening + # First turn enters Listening while ( DialogState.LISTENING is not self.conversation.get_dialog_state() ): @@ -146,7 +146,7 @@ def test_multimodal_dialog_one_turn(self): "今天天气不错", parameters=None, ) - # 等待第二轮Listening + # Wait for the second Listening while ( DialogState.LISTENING is not self.conversation.get_dialog_state() ): diff --git a/tests/unit/test_opd.py b/tests/unit/test_opd.py index 95748bf7..ac600e59 100644 --- a/tests/unit/test_opd.py +++ b/tests/unit/test_opd.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """Minimal OPD contract tests.""" -# pylint: disable=redefined-outer-name # pytest fixture 标准用法 +# pylint: disable=redefined-outer-name # standard pytest fixture usage from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest -from dashscope.client.base_api import CreateMixin from dashscope.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import ( DataSourceType, @@ -21,7 +20,9 @@ @pytest.fixture def rl_client(): - with patch("dashscope.finetune.agentic_rl.set_api_key"): + # ``set_api_key`` is looked up in the module that defines __init__, + # which is the 2.0 one; patching the 1.x shim's copy would miss. + with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): return AgenticRL(api_key="test-key") @@ -69,9 +70,8 @@ def test_teacher_model_enables_opd( has_rollout, reward_count, ): - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job( @@ -96,9 +96,8 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): ) rl_client.init(config_path=str(config)) - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) @@ -111,15 +110,14 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): def test_opd_example_reuses_supported_rl_configuration(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) config_text = config.read_text(encoding="utf-8") rl_client.init(config_path=str(config)) - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job() @@ -160,7 +158,7 @@ def test_opd_example_reuses_supported_rl_configuration(rl_client): async def test_opd_example_uploads_local_datasets(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) rl_client.init(config_path=str(config)) @@ -182,7 +180,7 @@ async def test_opd_example_uploads_local_datasets(rl_client): def test_regular_rl_example_keeps_resources_and_rl_hyperparameters(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "rl-job.yaml" ) rl_client.init(config_path=str(config)) @@ -212,9 +210,8 @@ def test_pg_opd_requires_teacher_model(rl_client): def test_regular_reinforcement_is_unchanged(rl_client): - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) diff --git a/tests/unit/test_oss_utils.py b/tests/unit/test_oss_utils.py index 505d384b..2b4eb022 100644 --- a/tests/unit/test_oss_utils.py +++ b/tests/unit/test_oss_utils.py @@ -5,9 +5,9 @@ import pytest -from dashscope.common.error import InvalidInput -from dashscope.utils import oss_utils -from dashscope.utils.oss_utils import OssUtils +from dashscope.legacy_1x.common.error import InvalidInput +from dashscope.legacy_1x.utils import oss_utils +from dashscope.legacy_1x.utils.oss_utils import OssUtils class FakeUploadResponse: diff --git a/tests/unit/test_recognition_explicit_params.py b/tests/unit/test_recognition_explicit_params.py index dcb4016b..7a9c6136 100644 --- a/tests/unit/test_recognition_explicit_params.py +++ b/tests/unit/test_recognition_explicit_params.py @@ -7,7 +7,10 @@ from contextlib import contextmanager from unittest import mock -from dashscope.audio.asr.recognition import Recognition, RecognitionCallback +from dashscope.legacy_1x.audio.asr.recognition import ( + Recognition, + RecognitionCallback, +) class MockRecognitionCallback(RecognitionCallback): diff --git a/tests/unit/test_rerank.py b/tests/unit/test_rerank.py index b7427944..fcccd575 100644 --- a/tests/unit/test_rerank.py +++ b/tests/unit/test_rerank.py @@ -18,14 +18,14 @@ def test_call(self, mock_server: MockServer): { "index": 1, "relevance_score": 0.987654, - "document": { # 如果return_documents=true + "document": { # when return_documents=true "text": "哈尔滨是中国黑龙江省的省会,位于中国东北", }, }, { "index": 0, "relevance_score": 0.876543, - "document": { # 如果return_documents=true + "document": { # when return_documents=true "text": "黑龙江离俄罗斯很近", }, }, diff --git a/tests/unit/test_speech_synthesis_v2.py b/tests/unit/test_speech_synthesis_v2.py index 798db207..52dee3d3 100644 --- a/tests/unit/test_speech_synthesis_v2.py +++ b/tests/unit/test_speech_synthesis_v2.py @@ -6,9 +6,9 @@ import pytest import dashscope -from dashscope.audio.tts_v2 import ResultCallback, SpeechSynthesizer -from dashscope.common.error import RequestFailure -from dashscope.protocol.websocket import ActionType, EventType +from dashscope.legacy_1x.audio.tts_v2 import ResultCallback, SpeechSynthesizer +from dashscope.legacy_1x.common.error import RequestFailure +from dashscope.legacy_1x.protocol.websocket import ActionType, EventType from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/unit/test_sync_custom_session.py b/tests/unit/test_sync_custom_session.py index 2d45475c..c9e60b26 100644 --- a/tests/unit/test_sync_custom_session.py +++ b/tests/unit/test_sync_custom_session.py @@ -24,10 +24,12 @@ import requests from requests.adapters import HTTPAdapter -from dashscope.api_entities import http_request as http_request_module -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities import ( + http_request as http_request_module, +) +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class TestSyncSessionBasics: @@ -121,7 +123,9 @@ def test_custom_session_is_used_for_request(self, _mock_session_class): # verify the custom session was not closed mock_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_is_used_when_no_custom_session( self, mock_get_session, @@ -214,7 +218,9 @@ def test_custom_session_not_closed_by_http_request(self): # verify the custom session was not closed custom_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_not_closed_on_success(self, mock_get_session): """Shared session is not closed after success""" mock_session = Mock() @@ -254,7 +260,9 @@ def test_shared_session_not_closed_on_success(self, mock_get_session): # verify the shared session was not closed mock_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_not_closed_on_exception(self, mock_get_session): """Shared session is not closed on exception""" mock_session = Mock() @@ -474,7 +482,9 @@ def test_works_without_session_parameter(self): assert http_request.url == "http://example.com/api" assert http_request.method == HTTPMethod.POST - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_default_behavior_unchanged(self, mock_get_session): """Default behavior: use the shared session""" mock_session = Mock() @@ -548,7 +558,9 @@ def _ok_response(): mock_response.text = '{"status": "success"}' return mock_response - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_retry_once_on_connection_error(self, mock_get_session): """Retry once and succeed when the peer drops the connection""" mock_session = Mock() @@ -572,7 +584,9 @@ def test_retry_once_on_connection_error(self, mock_get_session): assert mock_session.post.call_count == 2 - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_retry_exhausted_raises(self, mock_get_session): """Raise after two consecutive ConnectionErrors""" mock_session = Mock() @@ -588,7 +602,9 @@ def test_retry_exhausted_raises(self, mock_get_session): assert mock_session.post.call_count == 2 - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_no_retry_on_other_exceptions(self, mock_get_session): """Non-connection exceptions are not retried""" mock_session = Mock() diff --git a/tests/unit/test_tokenizer.py b/tests/unit/test_tokenizer.py index 682c6a15..4742d56f 100644 --- a/tests/unit/test_tokenizer.py +++ b/tests/unit/test_tokenizer.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +import importlib import os -from dashscope.tokenizers.tokenizer import get_tokenizer -from dashscope.tokenizers.qwen_tokenizer import _CHUNK_SIZE +import pytest + +from dashscope.v2.tokenizers.tokenizer import get_tokenizer +from dashscope.v2.tokenizers.qwen_tokenizer import _CHUNK_SIZE class TestTokenization: @@ -78,3 +81,80 @@ def test_encode_chunk_size_exceed(self): ) decoded_with_special = tokenizer.decode(tokens_with_special) assert decoded_with_special == long_text_with_special + + +class TestLegacyTokenizerPaths: + """The published ``dashscope.tokenizers.*`` deep paths must keep working. + + The implementation moved to ``dashscope.v2.tokenizers``; the 1.x paths + are thin re-export shims, so they have to resolve to the very same + objects rather than to a second copy. + """ + + @pytest.mark.parametrize( + "legacy_path,v2_path,name", + [ + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "get_tokenizer", + ), + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "list_tokenizers", + ), + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "QWEN_SERIALS", + ), + ( + "dashscope.tokenizers.qwen_tokenizer", + "dashscope.v2.tokenizers.qwen_tokenizer", + "QwenTokenizer", + ), + ( + "dashscope.tokenizers.qwen_tokenizer", + "dashscope.v2.tokenizers.qwen_tokenizer", + "SPECIAL_TOKENS_SET", + ), + ( + "dashscope.tokenizers.tokenizer_base", + "dashscope.v2.tokenizers.tokenizer_base", + "Tokenizer", + ), + ], + ) + def test_deep_imports_resolve_to_the_v2_objects( + self, + legacy_path, + v2_path, + name, + ): + legacy = importlib.import_module(legacy_path) + v2 = importlib.import_module(v2_path) + + assert getattr(legacy, name) is getattr(v2, name) + + def test_package_level_reexports_are_untouched(self): + import dashscope.tokenizers as legacy_pkg + import dashscope.v2.tokenizers as v2_pkg + + for name in v2_pkg.__all__: + assert getattr(legacy_pkg, name) is getattr(v2_pkg, name) + + def test_legacy_path_returns_a_working_tokenizer(self): + from dashscope.tokenizers.tokenizer import get_tokenizer as legacy + + tokenizer = legacy("qwen-turbo") + tokens = tokenizer.encode("hello world") + + assert isinstance(tokens, list) + assert tokenizer.decode(tokens) == "hello world" + + def test_from_import_syntax_resolves(self): + from dashscope.tokenizers.qwen_tokenizer import QwenTokenizer + from dashscope.tokenizers.tokenizer_base import Tokenizer + + assert issubclass(QwenTokenizer, Tokenizer) diff --git a/tests/unit/test_translation_recognizer.py b/tests/unit/test_translation_recognizer.py index 5f9782fe..84fc7bb9 100644 --- a/tests/unit/test_translation_recognizer.py +++ b/tests/unit/test_translation_recognizer.py @@ -6,8 +6,8 @@ import pytest -from dashscope.audio.asr import TranslationRecognizerCallback -from dashscope.audio.asr.translation_recognizer import ( +from dashscope.legacy_1x.audio.asr import TranslationRecognizerCallback +from dashscope.legacy_1x.audio.asr.translation_recognizer import ( TranscriptionResult, TranslationRecognizerRealtime, TranslationResult, diff --git a/tests/unit/test_translation_recognizer_explicit_params.py b/tests/unit/test_translation_recognizer_explicit_params.py index 8be53bbe..63f50a5c 100644 --- a/tests/unit/test_translation_recognizer_explicit_params.py +++ b/tests/unit/test_translation_recognizer_explicit_params.py @@ -3,7 +3,7 @@ """Test explicit parameters for TranslationRecognizer classes.""" # pylint: disable=protected-access -from dashscope.audio.asr.translation_recognizer import ( +from dashscope.legacy_1x.audio.asr.translation_recognizer import ( TranslationRecognizerRealtime, TranslationRecognizerChat, TranslationRecognizerCallback, diff --git a/tests/unit/test_utf8_encoding.py b/tests/unit/test_utf8_encoding.py index 32dcaf2d..c91f5cf2 100644 --- a/tests/unit/test_utf8_encoding.py +++ b/tests/unit/test_utf8_encoding.py @@ -15,9 +15,9 @@ import pytest -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.aiohttp_request import AioHttpRequest -from dashscope.common.constants import HTTPMethod +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.aiohttp_request import AioHttpRequest +from dashscope.legacy_1x.common.constants import HTTPMethod NON_ASCII_PAYLOAD = { @@ -162,7 +162,9 @@ def test_accept_has_charset(self): class TestHttpRequestUtf8Body: # pylint: disable=protected-access - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_body_is_utf8_bytes(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -186,7 +188,9 @@ def test_body_is_utf8_bytes(self, mock_session_cls): assert isinstance(sent_body, bytes), "Body must be bytes, not str" assert sent_body == EXPECTED_BODY - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_no_unicode_escapes_in_body(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -212,7 +216,9 @@ def test_no_unicode_escapes_in_body(self, mock_session_cls): assert "Оцени идею" in body_str assert "こんにちは" in body_str - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_body_is_smaller_than_ascii_escaped(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -311,7 +317,9 @@ class TestWebSocketRequestUtf8: # pylint: disable=protected-access def test_build_up_message_no_unicode_escapes(self): - from dashscope.api_entities.websocket_request import WebSocketRequest + from dashscope.legacy_1x.api_entities.websocket_request import ( + WebSocketRequest, + ) req = WebSocketRequest( url="wss://example.com/api-ws/v1/test", @@ -330,7 +338,9 @@ def test_build_up_message_no_unicode_escapes(self): assert "你好世界" in message def test_build_up_message_smaller_than_ascii_escaped(self): - from dashscope.api_entities.websocket_request import WebSocketRequest + from dashscope.legacy_1x.api_entities.websocket_request import ( + WebSocketRequest, + ) req = WebSocketRequest( url="wss://example.com/api-ws/v1/test", diff --git a/tests/unit/websocket_mock_server_task_handler.py b/tests/unit/websocket_mock_server_task_handler.py index 80743d18..f54f07ff 100644 --- a/tests/unit/websocket_mock_server_task_handler.py +++ b/tests/unit/websocket_mock_server_task_handler.py @@ -6,8 +6,8 @@ import aiohttp -from dashscope.common.error import UnexpectedMessageReceived -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.error import UnexpectedMessageReceived +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, EVENT_KEY, ActionType, diff --git a/tests/unit/websocket_task_request.py b/tests/unit/websocket_task_request.py index 00d1b1e2..78221c29 100644 --- a/tests/unit/websocket_task_request.py +++ b/tests/unit/websocket_task_request.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode class WebSocketRequest(BaseApi, BaseAioApi): diff --git a/tests/v2/test_agentstudio_shim.py b/tests/v2/test_agentstudio_shim.py new file mode 100644 index 00000000..9e3e442c --- /dev/null +++ b/tests/v2/test_agentstudio_shim.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the AgentStudio relocation into the 2.0 package (D7). + +The sub-SDK moved to ``dashscope.v2.agentstudio``; ``dashscope.agentstudio`` +is now a shim. Three contracts are pinned here: + +* the old import path -- including deep submodule paths -- resolves to the + very same module and class objects, so nothing downstream sees a fork; +* the pieces AgentStudio used to borrow from the 1.x ``common`` package now + come from the neutral ``dashscope._shared`` layer, with identity intact + (the exception base class and the mutable sdk-client override are both + load-bearing singletons); +* AgentStudio keeps emitting the *unmarked* ``agentstudio`` module tag, and + resolves its api key without reading the 1.x ``dashscope.api_key`` global. +""" + +from __future__ import annotations + +import sys + +import pytest + +import dashscope.agentstudio as shim +import dashscope.v2.agentstudio as impl +from dashscope._shared import _sdk_ident, _validation +from dashscope.agentstudio import exceptions as shim_exc +from dashscope.agentstudio.transport import build_headers +from dashscope.legacy_1x.common import error as legacy_error +from dashscope.legacy_1x.common import env as legacy_env +from dashscope.legacy_1x.common import utils as legacy_utils +from dashscope.v2.agentstudio import client as as_client +from dashscope.v2.agentstudio import exceptions as impl_exc +from dashscope.v2.resources._agentstudio import _is_resource, wrap_agentstudio + +BASE_URL = "https://as.test.local" + +# Deep paths the ecosystem relies on: the unit tests, the examples and the +# acli skill docs all import these directly. +SUBMODULES = [ + "client", + "constants", + "exceptions", + "pagination", + "streaming", + "transport", + "resources", + "resources._helpers", + "resources.agents", + "resources.deployments", + "resources.environments", + "resources.files", + "resources.memory_stores", + "resources.security", + "resources.session_events", + "resources.session_resources", + "resources.session_threads", + "resources.sessions", + "resources.skills", + "resources.vaults", + "resources.webhook_endpoints", + "types", + "types.models", + "types.params", +] + + +def _make_client(**kwargs): + kwargs.setdefault("api_key", "sk-test") + kwargs.setdefault("base_url", BASE_URL) + return impl.Client(**kwargs) + + +@pytest.fixture +def _clean_env(monkeypatch): + """Keep ambient env vars from changing base-url resolution.""" + + for var in ( + "DASHSCOPE_WORKSPACE", + "DASHSCOPE_AGENTSTUDIO_URL", + "AGENTSTUDIO_URL", + ): + monkeypatch.delenv(var, raising=False) + + +# --------------------------------------------------------------------------- +# The shim resolves to one and the same implementation +# --------------------------------------------------------------------------- + + +def test_public_names_are_the_same_objects(): + assert shim.__all__ == impl.__all__ + for name in impl.__all__: + assert getattr(shim, name) is getattr(impl, name), name + + +def test_client_classes_are_the_same_objects(): + assert shim.Client is impl.Client + assert shim.AsyncClient is impl.AsyncClient + assert shim.Client.__module__ == "dashscope.v2.agentstudio.client" + + +def test_exception_classes_are_the_same_objects(): + assert shim_exc.AgentStudioError is impl_exc.AgentStudioError + assert shim_exc.AuthenticationError is impl_exc.AuthenticationError + assert shim_exc.NotFoundError is impl_exc.NotFoundError + + +@pytest.mark.parametrize("name", SUBMODULES) +def test_deep_imports_alias_the_v2_module(name): + __import__(f"dashscope.agentstudio.{name}") + assert ( + sys.modules[f"dashscope.agentstudio.{name}"] + is sys.modules[f"dashscope.v2.agentstudio.{name}"] + ) + + +@pytest.mark.parametrize("name", SUBMODULES) +def test_submodules_are_reachable_as_attributes(name): + """sys.modules alone is not enough -- attribute access must work too.""" + + __import__("dashscope.agentstudio") + obj = shim + for part in name.split("."): + obj = getattr(obj, part) + assert obj is sys.modules[f"dashscope.v2.agentstudio.{name}"] + + +def test_deep_import_from_syntax_resolves(): + from dashscope.agentstudio.resources.agents import Agents + from dashscope.agentstudio.types.models import Agent + from dashscope.v2.agentstudio.resources.agents import Agents as V2Agents + from dashscope.v2.agentstudio.types.models import Agent as V2Agent + + assert Agents is V2Agents + assert Agent is V2Agent + + +# --------------------------------------------------------------------------- +# The neutral layer holds the singletons AgentStudio depends on +# --------------------------------------------------------------------------- + + +def test_exception_base_class_identity_is_preserved(): + """Both stacks must inherit from one and the same class object.""" + + from dashscope._shared._errors import DashScopeException + + assert legacy_error.DashScopeException is DashScopeException + assert issubclass( + impl_exc.AgentStudioError, + legacy_error.DashScopeException, + ) + assert issubclass( + legacy_error.InvalidParameter, + legacy_error.DashScopeException, + ) + + +def test_validators_are_shared_and_still_reject_host_breakouts(): + assert legacy_env.validate_region is _validation.validate_region + assert ( + legacy_env.validate_workspace_id is _validation.validate_workspace_id + ) + # These values would move the effective host off *.maas.aliyuncs.com + # while the transport still sends Authorization: Bearer . + breakout = ("a@evil.com/#", "ws/evil", "ws evil", "", "-lead") + for bad in breakout + ("x" * 65,): + with pytest.raises(ValueError): + _validation.validate_workspace_id(bad) + for bad in breakout + ("x" * 64, "trail-"): + with pytest.raises(ValueError): + _validation.validate_region(bad) + + +def test_workspace_and_region_still_build_a_maas_host(_clean_env): + client = impl.Client( + api_key="sk-test", + workspace="ws-demo", + region="cn-hangzhou", + ) + assert client.transport.base_url == ( + "https://ws-demo.cn-hangzhou.maas.aliyuncs.com/api/v1/agentstudio" + ) + + +def test_untrusted_workspace_is_rejected_before_the_host_is_built(_clean_env): + with pytest.raises(ValueError): + impl.Client(api_key="sk-test", workspace="a@evil.com/#") + + +# --------------------------------------------------------------------------- +# The unmarked module tag, and the CLI's process-wide override +# --------------------------------------------------------------------------- + + +def test_agentstudio_tag_carries_no_v2_marker(monkeypatch): + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-sdk") + value = build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-client"] + assert value.endswith("/agentstudio") + assert not value.endswith("agentstudio2") + + +def test_cli_sdk_client_override_reaches_agentstudio(monkeypatch): + """``_SDK_CLIENT`` is process-wide mutable state: one copy only.""" + + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-cli") + value = build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-client"] + assert value.split("/")[0] == "python-cli" + # the 1.x re-export reads the very same state + assert ( + legacy_utils.get_sdk_headers(module="agentstudio")[ + "x-dashscope-sdk-client" + ] + == value + ) + + +def test_session_id_is_shared_with_the_v2_transport(): + from dashscope.v2.transport._ident import sdk_headers + + assert ( + build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-session-id"] + == sdk_headers("aigc")["x-dashscope-sdk-session-id"] + ) + + +# --------------------------------------------------------------------------- +# api key resolution no longer reads the 1.x ``dashscope.api_key`` global +# --------------------------------------------------------------------------- + + +@pytest.fixture +def _no_key_anywhere(monkeypatch): + monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: None) + monkeypatch.setattr(as_client.settings, "api_key", None) + + +def test_api_key_prefers_settings_over_the_key_file(monkeypatch): + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: "sk-file") + assert as_client._default_api_key() == "sk-env" + + +def test_api_key_falls_back_to_the_key_file(monkeypatch): + monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: "sk-file") + monkeypatch.setattr(as_client.settings, "api_key", None) + assert as_client._default_api_key() == "sk-file" + + +def test_missing_api_key_resolves_to_none(_no_key_anywhere): + assert as_client._default_api_key() is None + + +def test_missing_api_key_surfaces_at_request_time(_no_key_anywhere): + """Construction succeeds; the transport raises the native error.""" + + client = impl.Client(base_url=BASE_URL) + assert client.transport.api_key is None + with pytest.raises(impl_exc.AuthenticationError): + build_headers( + api_key=client.transport.api_key, + uid=None, + user_agent="ua", + ) + + +# --------------------------------------------------------------------------- +# The v2 error-alignment proxy still recognises sub-resources +# --------------------------------------------------------------------------- + + +def test_resource_detection_survives_the_move(): + """``_RESOURCE_PACKAGE`` must track the new module path. + + If it still pointed at ``dashscope.agentstudio.resources`` the proxy + would silently stop wrapping sub-resources, and their errors would + escape ``except dashscope.v2.errors.DashScopeError``. + """ + + client = _make_client() + assert _is_resource(client.agents) + assert _is_resource(client.sessions) + assert not _is_resource(client.transport) + + +def test_wrapped_resource_is_proxied(): + wrapped = wrap_agentstudio(_make_client()) + assert wrapped.agents is not _make_client().agents + assert type(wrapped.agents).__module__.startswith( + "dashscope.v2.resources._agentstudio", + ) diff --git a/tests/v2/test_audio_management.py b/tests/v2/test_audio_management.py index f51386b3..3d4700b8 100644 --- a/tests/v2/test_audio_management.py +++ b/tests/v2/test_audio_management.py @@ -227,7 +227,7 @@ def handler(request): assert all(call["model"] == "voice-enrollment" for call in calls) -# ── voice design (声音设计, qwen-voice-design) ───────────────────────── +# ── voice design (qwen-voice-design) ───────────────────────── def test_voice_design_crud(): diff --git a/tests/v2/test_chat.py b/tests/v2/test_chat.py index f9ce1c93..0bc51ed4 100644 --- a/tests/v2/test_chat.py +++ b/tests/v2/test_chat.py @@ -1532,14 +1532,14 @@ def handler(request): def test_media_upload_uses_the_per_call_api_key(monkeypatch): - # The OSS upload must run under the per-call key (1.x behavior); - # uploading with the client key would park the file in the wrong - # account when the two differ. + # The OSS upload must run under the per-call key; uploading with the + # client key would park the file in the wrong account when the two + # differ. seen = {} - def fake_upload(model, content, api_key, cert, **kwargs): - seen["api_key"] = api_key - seen["base_address"] = kwargs.get("base_address") + def fake_upload(client, model, content, **kwargs): + seen["client"] = client + seen["api_key"] = kwargs.get("api_key") return True, "oss://bucket/cat.png", {} monkeypatch.setattr( @@ -1563,9 +1563,9 @@ def handler(request): api_key="sk-override", ) assert seen["api_key"] == "sk-override" - # The certificate is fetched from the client's own base_url so a + # The certificate is fetched through the client's own transport, so a # region/gateway-scoped client uploads to the matching regional OSS. - assert seen["base_address"] == client.config.base_url + assert seen["client"] is client assert captured["auth"] == "Bearer sk-override" assert captured["oss"] == "enable" assert captured["body"]["input"]["messages"][0]["content"] == [ diff --git a/tests/v2/test_config.py b/tests/v2/test_config.py index 9e8e7cd2..b1df7548 100644 --- a/tests/v2/test_config.py +++ b/tests/v2/test_config.py @@ -483,7 +483,9 @@ def test_save_api_key_writes_the_default_path( def test_save_api_key_accepts_a_bare_filename( - clean_env, tmp_path, monkeypatch + clean_env, + tmp_path, + monkeypatch, ): from dashscope.v2 import save_api_key diff --git a/tests/v2/test_finetune_rl_shim.py b/tests/v2/test_finetune_rl_shim.py new file mode 100644 index 00000000..e1af41cd --- /dev/null +++ b/tests/v2/test_finetune_rl_shim.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Reverse-reference shims and the submit transport for the RL move. + +``dashscope.finetune.reinforcement`` and ``dashscope.finetune.agentic_rl`` +are now compatibility surfaces over ``dashscope.v2.finetune``. The old +paths have to keep resolving *by name* in processes this repo does not +control -- a Function Compute sandbox running ``FC_SERVER_CLASSPATH``, and +``uvicorn`` / ``python -m`` resolving the reward-server import string -- +so what is pinned here is object identity, laziness and the wire format, +not just the happy path. +""" + +from __future__ import annotations + +import importlib +import importlib.util +import json +import pathlib +import platform +import runpy +import sys +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +import dashscope +from dashscope._shared import _sdk_ident +from dashscope._shared._sdk_ident import set_sdk_client +from dashscope.finetune.agentic_rl import AgenticRL as LegacyAgenticRL +from dashscope.version import __version__ +from dashscope.v2.errors import SdkError +from dashscope.v2.finetune import _submit +from dashscope.v2.finetune.agentic_rl import AgenticRL as V2AgenticRL +from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.v2.finetune.reinforcement import TrainingType +from dashscope.v2.finetune.reinforcement.common.errors import ( + ValueErrorWithCode, +) +from dashscope.v2.resources import reinforcement_learning as _rl_mount + +LEGACY = "dashscope.finetune.reinforcement" +V2 = "dashscope.v2.finetune.reinforcement" + +# The modules the package imports eagerly, so both spellings are already +# in sys.modules before anything asks for them. The nine that are *not* +# here (component.server.* and component.observability.genai.*) are the +# reason the shim is a meta_path finder rather than an eager walk. +EAGER_SUBMODULES = [ + "common.constants", + "common.errors", + "common.log", + "common.model", + "common.model_types", + "common.utils", + "component.data.base_data_model", + "component.func_decorator", + "component.func_manager", + "component.observability.tracing", + "component.parser.reward_parser", + "component.processor.abstract_reward_processor", +] + +SUBMIT_PATH = f"{LEGACY}.component.server.server" + +ENVELOPE = { + "status_code": 200, + "request_id": "req-1", + "code": None, + "message": "", + "output": { + "job_id": "ft-1", + "status": "RUNNING", + "create_time": "2026-01-02 03:04:05", + "finetuned_output": "qwen-ft", + }, + "usage": None, + "headers": {"x-request-id": "h-1"}, +} + + +class _StubTuning: + """The smallest ``tuning`` that ``_build_submit_request`` accepts.""" + + def __init__(self): + self.teacher_model = None + self.name = "stub-job" + self.model = SimpleNamespace(name="qwen-test") + self.functions = [] + self.datasets = [] + self.training = SimpleNamespace( + hyper_parameters=None, + resources=None, + type=TrainingType.TRAINING_TYPE, + ) + + def combine_ids_runtimes(self, functype, id_str=None): + return [] + + def check_function_names(self): + return True + + +def _stub(cls, api_key="sk-submit"): + # ``set_api_key`` writes DASHSCOPE_API_KEY for real; these tests are + # about the submit call, not about credential discovery. + with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): + instance = cls(api_key=api_key) + instance.tuning = _StubTuning() + return instance + + +class _FakeResponse: + status_code = 200 + headers = {"content-type": "application/json"} + content = b"" + + @staticmethod + def json(): + return {"request_id": "req-1", "output": {"job_id": "ft-1"}} + + +@pytest.fixture(name="post_calls") +def fixture_post_calls(monkeypatch): + """Record every ``post_submit`` call instead of hitting the network.""" + calls = [] + + # ``_SDK_CLIENT`` is process-wide mutable state, and importing the CLI + # (which ``test_app_is_reachable_only_from_the_legacy_path`` does) + # marks the whole process ``python-cli``. Pinning it here keeps these + # assertions independent of test order; monkeypatch restores whatever + # the ambient value was. + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-sdk") + + class _Session: + def __enter__(self): + return self + + def __exit__(self, *exc_info): + return False + + def post(self, url, data=None, headers=None, timeout=None): + calls.append( + { + "url": url, + "data": data, + "headers": headers, + "timeout": timeout, + }, + ) + return _FakeResponse() + + monkeypatch.setattr(_submit.requests, "Session", _Session) + return calls + + +# -- identity ------------------------------------------------------------- + + +def test_every_exported_name_is_the_same_object(): + legacy = importlib.import_module(LEGACY) + v2 = importlib.import_module(V2) + assert v2.__all__ + assert legacy.__all__ == list(v2.__all__) + ["app"] + for name in v2.__all__: + assert getattr(legacy, name) is getattr(v2, name), name + + +def test_legacy_agentic_rl_subclasses_the_v2_one(): + assert LegacyAgenticRL is not V2AgenticRL + assert issubclass(LegacyAgenticRL, V2AgenticRL) + + +def test_only_the_1x_surface_keeps_the_job_verbs(): + assert "CreateMixin" not in [c.__name__ for c in V2AgenticRL.__mro__] + for verb in ("cancel", "list", "get", "delete", "logs"): + assert verb not in V2AgenticRL.__dict__, verb + assert verb in LegacyAgenticRL.__dict__, verb + + +@pytest.mark.parametrize("submodule", EAGER_SUBMODULES) +def test_eager_deep_imports_alias_to_the_same_module(submodule): + legacy = importlib.import_module(f"{LEGACY}.{submodule}") + v2 = importlib.import_module(f"{V2}.{submodule}") + assert legacy is v2 + # The shared object keeps reporting the v2 name, so pickling and + # reload resolve against the sys.modules key that owns it. + assert legacy.__name__ == f"{V2}.{submodule}" + assert sys.modules[f"{LEGACY}.{submodule}"] is v2 + + +def test_deep_import_works_as_an_attribute_expression(): + legacy = importlib.import_module(LEGACY) + v2_utils = importlib.import_module(f"{V2}.common.utils") + assert legacy.common.utils.set_api_key is v2_utils.set_api_key + + +# -- laziness ------------------------------------------------------------- + + +def test_server_module_resolves_without_importing_fastapi(): + spec = importlib.util.find_spec(SUBMIT_PATH) + assert spec is not None + assert "dashscope/v2/finetune/reinforcement" in spec.origin.replace( + "\\", + "/", + ) + assert spec.loader.get_code(SUBMIT_PATH) is not None + assert "fastapi" not in sys.modules + + +def test_python_dash_m_resolves_the_legacy_server_path(): + # ``runpy`` refuses a loader that yields no code object, which is how + # examples/workspace/scripts/start_local_*.sh launches the server. + name, _, code = runpy._get_module_details(SUBMIT_PATH) + assert name == SUBMIT_PATH + assert code is not None + assert "fastapi" not in sys.modules + + +@pytest.mark.skipif( + importlib.util.find_spec("opentelemetry") is not None, + reason="opentelemetry is installed, so the guard cannot be observed", +) +def test_genai_stays_behind_its_optional_dependency_on_both_paths(): + # ``genai/__init__`` imports ``llm_dashscope`` eagerly, which needs + # opentelemetry. Both spellings must fail identically rather than one + # quietly succeeding through the alias. + for name in ( + f"{LEGACY}.component.observability.genai.llm_dashscope", + f"{V2}.component.observability.genai.llm_dashscope", + ): + with pytest.raises(ModuleNotFoundError) as ei: + importlib.import_module(name) + assert "opentelemetry" in str(ei.value) + + +def test_app_is_reachable_only_from_the_legacy_path(): + v2 = importlib.import_module(V2) + assert not hasattr(v2, "app") + + from dashscope.legacy_1x.cli.agentic_rl import app as cli_app + + assert importlib.import_module(LEGACY).app is cli_app + + +# -- the submit POST ------------------------------------------------------ + + +def test_submit_post_keeps_the_published_wire_format(post_calls): + _stub(V2AgenticRL).submit_job(job_name="计算任务") + + assert len(post_calls) == 1 + call = post_calls[0] + assert call["url"] == f"{DASHSCOPE_HTTP_BASE_URL}/fine-tunes" + assert call["timeout"] == _submit.SUBMIT_TIMEOUT + + headers = call["headers"] + # Insertion order is part of the contract: it is the order 1.x's + # CreateMixin produced, and the backend sees the same bytes. + assert list(headers) == [ + "Content-Type", + "user-agent", + "x-dashscope-sdk-client", + "x-dashscope-sdk-session-id", + "Authorization", + "Accept", + ] + assert headers["Content-Type"] == "application/json; charset=utf-8" + assert headers["Accept"] == headers["Content-Type"] + assert headers["Authorization"] == "Bearer sk-submit" + + # Unmarked module tag: the v2 transport's ``finetune2`` belongs to + # client.rl.jobs, not to the submit path. + assert headers["x-dashscope-sdk-client"] == ( + f"python-sdk/{__version__}/finetune" + ) + + user_agent = headers["user-agent"] + assert user_agent.startswith("dashscope/") + assert f"python/{platform.python_version()}" in user_agent + + body = call["data"] + assert isinstance(body, bytes) + payload = json.loads(body.decode("utf-8")) + # ensure_ascii=False: a non-ASCII job name must not be escaped. + assert "计算任务".encode("utf-8") in body + assert payload["job_name"].startswith("计算任务-") + assert payload["model"] == "qwen-test" + assert payload["training_type"] == str(TrainingType.TRAINING_TYPE) + assert "teacher_model" not in payload + + +def test_cli_sdk_client_override_reaches_the_submit_tag(post_calls): + # ``set_sdk_client`` is the CLI's own entry point into the mutable + # global; the ``post_calls`` fixture pins and restores it. + set_sdk_client("python-cli") + + _stub(V2AgenticRL).submit_job() + + assert post_calls[0]["headers"]["x-dashscope-sdk-client"] == ( + f"python-cli/{__version__}/finetune" + ) + + +def test_sdk_header_kill_switch_still_applies(monkeypatch, post_calls): + monkeypatch.setenv("DASHSCOPE_DISABLE_SDK_HEADERS", "1") + + _stub(V2AgenticRL).submit_job() + + headers = post_calls[0]["headers"] + assert "x-dashscope-sdk-client" not in headers + assert "x-dashscope-sdk-session-id" not in headers + assert headers["Authorization"] == "Bearer sk-submit" + + +def test_session_id_is_one_value_across_both_stacks(): + from dashscope.legacy_1x.common.utils import get_sdk_headers + from dashscope.v2.transport._ident import sdk_headers + + legacy = get_sdk_headers("finetune")["x-dashscope-sdk-session-id"] + v2 = sdk_headers("finetune")["x-dashscope-sdk-session-id"] + assert legacy == v2 + + +# -- api key resolution --------------------------------------------------- + + +def test_submit_ignores_the_1x_root_api_key_global(monkeypatch, post_calls): + monkeypatch.setattr(dashscope, "api_key", "sk-root-global", raising=False) + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + + rl = _stub(V2AgenticRL, api_key="sk-submit") + assert rl._api_key == "sk-submit" + rl.submit_job() + + assert post_calls[0]["headers"]["Authorization"] == "Bearer sk-submit" + + +def test_submit_falls_back_to_the_environment(monkeypatch, post_calls): + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + + rl = _stub(V2AgenticRL, api_key=None) + assert rl._api_key is None + rl.submit_job() + + assert post_calls[0]["headers"]["Authorization"] == "Bearer sk-env" + + +# -- the two return types ------------------------------------------------- + + +def test_v2_submit_returns_submit_result_and_feeds_to_job(): + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(ENVELOPE), + ): + result = _stub(V2AgenticRL).submit_job() + + assert isinstance(result, SubmitResult) + assert isinstance(result.output, SubmitOutput) + assert result.output.job_id == "ft-1" + assert result.request_id == "req-1" + assert result.headers == {"x-request-id": "h-1"} + + job = _rl_mount._to_job(result) + assert job.job_id == "ft-1" + assert job.status == "RUNNING" + # The v2 model's aliases absorb the submit's 1.x-era field names. + assert job.created_at == "2026-01-02 03:04:05" + assert job.finetuned_model == "qwen-ft" + assert job.request_id == "req-1" + + +def test_legacy_submit_returns_the_finetune_dataclass(): + from dashscope.finetune.customize_types import FineTune + + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(ENVELOPE), + ): + result = _stub(LegacyAgenticRL).submit_job() + + assert isinstance(result, FineTune) + assert result.status_code == 200 + assert result.output.job_id == "ft-1" + assert result.output.finetuned_output == "qwen-ft" + + +def test_a_failed_submit_is_an_envelope_not_an_exception(): + failure = { + "status_code": 400, + "request_id": "req-2", + "code": "InvalidParameter", + "message": "bad job", + "output": None, + "usage": None, + "headers": {}, + } + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(failure), + ): + result = _stub(V2AgenticRL).submit_job() + + assert result.status_code == 400 + assert result.code == "InvalidParameter" + assert result.output is None + + +# -- single implementation ------------------------------------------------ + + +def test_the_legacy_package_directory_holds_only_the_shim(): + legacy = importlib.import_module(LEGACY) + package_dir = pathlib.Path(legacy.__file__).parent + assert [p.name for p in package_dir.rglob("*.py")] == ["__init__.py"] + + +def test_the_v2_mount_points_at_the_v2_workflow_modules(): + assert _rl_mount._agentic_rl is importlib.import_module( + "dashscope.v2.finetune.agentic_rl", + ) + assert _rl_mount._reinforcement is importlib.import_module(V2) + + +def test_the_rl_error_module_is_shared_by_both_paths(): + legacy_errors = importlib.import_module(f"{LEGACY}.common.errors") + assert _rl_mount._rl_errors is legacy_errors + assert _rl_mount._rl_errors is importlib.import_module( + f"{V2}.common.errors", + ) + for name in _rl_mount._RL_EXCEPTIONS: + assert getattr(legacy_errors, name.__name__) is name + + +def test_translate_rl_errors_still_maps_the_family(): + with pytest.raises(SdkError) as ei: + with _rl_mount._translate_rl_errors(): + raise ValueErrorWithCode("duplicate names", error_code=3004) + + assert ei.value.code == "sdk.agentic_rl.ValueErrorWithCode" + assert "duplicate names" in ei.value.message + assert isinstance(ei.value.__cause__, ValueErrorWithCode) diff --git a/tests/v2/test_images_videos.py b/tests/v2/test_images_videos.py index c8fdb497..a7dc46eb 100644 --- a/tests/v2/test_images_videos.py +++ b/tests/v2/test_images_videos.py @@ -665,9 +665,9 @@ def handler(request): def test_image_jobs_create_uploads_with_the_per_call_api_key(monkeypatch): seen = {} - def fake_upload(model, content, api_key, cert, **kwargs): - seen["api_key"] = api_key - seen["base_address"] = kwargs.get("base_address") + def fake_upload(client, model, content, **kwargs): + seen["client"] = client + seen["api_key"] = kwargs.get("api_key") return True, "oss://bucket/ref.png", {} monkeypatch.setattr( @@ -690,7 +690,7 @@ def handler(request): api_key="sk-override", ) assert seen["api_key"] == "sk-override" - assert seen["base_address"] == client.config.base_url + assert seen["client"] is client assert captured["auth"] == "Bearer sk-override" assert captured["oss"] == "enable" content = captured["body"]["input"]["messages"][0]["content"] diff --git a/tests/v2/test_oss_upload.py b/tests/v2/test_oss_upload.py new file mode 100644 index 00000000..b75c190a --- /dev/null +++ b/tests/v2/test_oss_upload.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the v2-native local-file → OSS uploader.""" + +# Mock handlers intentionally ignore the request argument. +# pylint: disable=unused-argument + +from __future__ import annotations + +import os + +import httpx +import pytest + +from dashscope.v2 import Client +from dashscope.v2.errors import AuthenticationError, PermissionDeniedError +from dashscope.v2.resources import _oss_upload + +BASE_URL = "https://api.test.local/api/v1" +OSS_HOST = "https://dashscope-oss.oss-cn-beijing.aliyuncs.com" + +_CERT = { + "oss_access_key_id": "STS.access-key-id", + "signature": "sig==", + "policy": "pol==", + "upload_dir": "upload/2026/abc", + "x_oss_object_acl": "private", + "x_oss_forbid_overwrite": "true", + "upload_host": OSS_HOST, +} + + +def _make_client(handler, **kwargs): + mock = httpx.MockTransport(handler) + http_client = httpx.Client(transport=mock, base_url=BASE_URL) + kwargs.setdefault("base_url", BASE_URL) + return Client(api_key="sk-test", http_client=http_client, **kwargs) + + +def _policy_client(status=200): + """Client whose transport answers ``GET /uploads`` with a policy.""" + calls = [] + + def handler(request): + calls.append(request) + if status != 200: + return httpx.Response(status, json={"message": "denied"}) + return httpx.Response( + 200, + json={"request_id": "r-1", "output": _CERT}, + ) + + return _make_client(handler), calls + + +def _capture_oss(monkeypatch, status=200): + """Send the OSS POST through a MockTransport and record what it carried.""" + seen = {} + real_post = _oss_upload._post_to_oss + + def handler(request): + seen["url"] = str(request.url) + seen["headers"] = request.headers + seen["body"] = request.content.decode("utf-8", "replace") + if status == 200: + return httpx.Response(200, text="") + return httpx.Response( + status, + text="AccessDenied", + ) + + def patched(upload_host, form, file_name, file_path, transport=None): + seen["form"] = form + seen["file_name"] = file_name + return real_post( + upload_host, + form, + file_name, + file_path, + transport=httpx.MockTransport(handler), + ) + + monkeypatch.setattr(_oss_upload, "_post_to_oss", patched) + return seen + + +def _png(tmp_path, name="cat.png"): + path = tmp_path / name + path.write_bytes(b"\x89PNG\r\n\x1a\n") + return str(path) + + +def test_certificate_fetch_rides_the_client_transport(): + client, calls = _policy_client() + + cert = _oss_upload.get_upload_certificate( + client, + "qwen-vl-max", + api_key="sk-override", + workspace="ws-1", + ) + + assert cert == _CERT + request = calls[0] + assert request.url.path == "/api/v1/uploads" + assert request.url.params["action"] == "getPolicy" + assert request.url.params["model"] == "qwen-vl-max" + # This hop talks to DashScope, so it does carry the per-call credential. + assert request.headers.get("Authorization") == "Bearer sk-override" + assert request.headers.get("X-DashScope-WorkSpace") == "ws-1" + + +def test_certificate_fetch_failure_raises(): + client, _ = _policy_client(status=401) + with pytest.raises(AuthenticationError): + _oss_upload.get_upload_certificate(client, "qwen-vl-max") + + +def test_upload_local_file_builds_the_oss_form(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, calls = _policy_client() + file_path = _png(tmp_path) + + url, cert = _oss_upload.upload_local_file( + client, + "qwen-vl-max", + file_path, + ) + + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + assert cert == _CERT + assert seen["url"] == OSS_HOST + assert seen["form"] == { + "OSSAccessKeyId": _CERT["oss_access_key_id"], + "Signature": _CERT["signature"], + "policy": _CERT["policy"], + "key": _CERT["upload_dir"] + "/cat.png", + "x-oss-object-acl": "private", + "x-oss-forbid-overwrite": "true", + "success_action_status": "200", + "x-oss-content-type": "image/png", + } + assert seen["file_name"] == "cat.png" + # OSS rejects a post-object request whose "file" part is not last. + assert seen["body"].rindex('name="key"') < seen["body"].rindex( + 'name="file"', + ) + assert len(calls) == 1 + + +def test_unknown_extension_falls_back_to_octet_stream(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, _ = _policy_client() + file_path = str(tmp_path / "blob") + with open(file_path, "wb") as handle: + handle.write(b"x") + + _oss_upload.upload_local_file(client, "qwen-vl-max", file_path) + + assert seen["form"]["x-oss-content-type"] == "application/octet-stream" + + +def test_oss_post_carries_no_dashscope_credential(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, _ = _policy_client() + + _oss_upload.upload_local_file(client, "qwen-vl-max", _png(tmp_path)) + + # The credential boundary: the storage host must never see the API key, + # so this hop uses its own auth-free client rather than the transport. + assert seen["headers"].get("Authorization") is None + assert "sk-test" not in seen["body"] + assert seen["headers"].get("Accept") == "application/json" + user_agent = seen["headers"].get("User-Agent", "") + assert user_agent.startswith("dashscope-v2-python/") + + +def test_oss_post_failure_maps_to_a_dashscope_error(tmp_path, monkeypatch): + _capture_oss(monkeypatch, status=403) + client, _ = _policy_client() + + with pytest.raises(PermissionDeniedError): + _oss_upload.upload_local_file(client, "qwen-vl-max", _png(tmp_path)) + + +def test_certificate_is_reused_across_uploads(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, calls = _policy_client() + first = _png(tmp_path, "a.png") + second = _png(tmp_path, "b.png") + + url_a, cert = _oss_upload.upload_local_file(client, "m", first) + url_b, _ = _oss_upload.upload_local_file( + client, + "m", + second, + certificate=cert, + ) + + assert url_a == f"oss://{_CERT['upload_dir']}/a.png" + assert url_b == f"oss://{_CERT['upload_dir']}/b.png" + # Only the first upload needed a policy. + assert len(calls) == 1 + + +def test_check_and_upload_local_keeps_remote_urls_untouched(): + client, calls = _policy_client() + + for content in ( + "https://example.com/cat.png", + "http://example.com/cat.png", + ): + assert _oss_upload.check_and_upload_local(client, "m", content) == ( + False, + content, + None, + ) + assert calls == [] + + +def test_check_and_upload_local_flags_an_existing_oss_url(): + client, calls = _policy_client() + + result = _oss_upload.check_and_upload_local( + client, + "m", + "oss://bucket/cat.png", + ) + + # Nothing was uploaded, but the caller still has to opt the request into + # OSS resolution. + assert result == (True, "oss://bucket/cat.png", None) + assert calls == [] + + +def test_check_and_upload_local_uploads_a_file_uri(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, calls = _policy_client() + file_path = _png(tmp_path) + + is_up, url, cert = _oss_upload.check_and_upload_local( + client, + "qwen-vl-max", + f"file://{file_path}", + api_key="sk-override", + ) + + assert is_up is True + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + assert cert == _CERT + assert len(calls) == 1 + assert calls[0].headers.get("Authorization") == "Bearer sk-override" + + +def test_check_and_upload_local_uploads_a_bare_path(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, _ = _policy_client() + + is_up, url, _ = _oss_upload.check_and_upload_local( + client, + "m", + _png(tmp_path), + ) + + assert is_up is True + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + + +def test_check_and_upload_local_expands_a_path_that_is_not_a_file(): + client, calls = _policy_client() + + is_up, value, cert = _oss_upload.check_and_upload_local( + client, + "m", + "~/nope.png", + ) + + # Passed through, but with ~ expanded as in 1.x. + assert (is_up, cert) == (False, None) + assert value == os.path.expanduser("~/nope.png") + assert calls == [] + + +def test_missing_file_uri_raises_value_error(tmp_path): + client, calls = _policy_client() + missing = (tmp_path / "gone.png").as_uri() + + with pytest.raises(ValueError, match="is not exists"): + _oss_upload.check_and_upload_local(client, "m", missing) + assert calls == [] + + +@pytest.mark.parametrize( + "uri,expected", + [ + ("file:///tmp/a%20b.png", "/tmp/a b.png"), + ("file://localhost/tmp/a.png", "localhost/tmp/a.png"), + ("file:///C:/Users/me/a.png", "C:/Users/me/a.png"), + ], +) +def test_resolve_file_uri_path(uri, expected): + assert _oss_upload._resolve_file_uri_path(uri) == expected diff --git a/tests/v2/test_package_root.py b/tests/v2/test_package_root.py index af2e4c61..7cd65572 100644 --- a/tests/v2/test_package_root.py +++ b/tests/v2/test_package_root.py @@ -50,15 +50,18 @@ def test_legacy_exports_untouched(): assert hasattr(dashscope, "save_api_key") -def test_v2_namespace_reexports_1x_tokenizers(): - # The local tiktoken wrapper is still delegated to 1.x rather than - # re-implemented (same objects the package root exposes). +def test_tokenizers_are_shared_between_stacks(): + # The local tiktoken wrapper is owned by 2.0; 1.x and the package root + # re-export the same objects, so there is one implementation and one + # copy of the bundled vocab. from dashscope import v2 from dashscope.tokenizers import get_tokenizer as get_1x from dashscope.tokenizers import list_tokenizers as list_1x + assert v2.get_tokenizer.__module__.startswith("dashscope.v2.tokenizers") assert v2.get_tokenizer is get_1x assert v2.list_tokenizers is list_1x + assert dashscope.get_tokenizer is v2.get_tokenizer for name in ("get_tokenizer", "list_tokenizers"): assert name in v2.__all__ @@ -67,7 +70,7 @@ def test_v2_owns_save_api_key(): # The key-file writer is v2-native, so reaching for it does not pull in # the 1.x package root; the package root keeps its own 1.x writer. from dashscope import v2 - from dashscope.common.api_key import save_api_key as save_1x + from dashscope.legacy_1x.common.api_key import save_api_key as save_1x assert v2.save_api_key.__module__ == "dashscope.v2._config" assert v2.save_api_key is not save_1x @@ -77,14 +80,13 @@ def test_v2_owns_save_api_key(): def test_v2_get_tokenizer_encodes_locally(): from dashscope import v2 - from dashscope.common.error import UnsupportedModel tokenizer = v2.get_tokenizer("qwen-turbo") ids = tokenizer.encode("hello world") assert ids and all(isinstance(i, int) for i in ids) assert tokenizer.decode(ids) == "hello world" assert "qwen-turbo" in v2.list_tokenizers() - with pytest.raises(UnsupportedModel): + with pytest.raises(ValueError): v2.get_tokenizer("not-a-qwen-model") diff --git a/tests/v2/test_rl.py b/tests/v2/test_rl.py index 29e189b8..dd982c92 100644 --- a/tests/v2/test_rl.py +++ b/tests/v2/test_rl.py @@ -10,7 +10,7 @@ import httpx import pytest -from dashscope.finetune import agentic_rl as agentic_rl_module +from dashscope.v2.finetune import agentic_rl as agentic_rl_module from dashscope.finetune.reinforcement.common.errors import ( AgenticRLError, InstanceQueryError, @@ -644,7 +644,7 @@ def test_rl_long_form_alias(): assert async_client.reinforcement_learning is async_client.rl -# -- 1.x exceptions translated into the v2 error family ----------------- +# -- RL exceptions translated into the v2 error family ------------------- @pytest.fixture(name="fail_step") @@ -710,8 +710,8 @@ def test_exact_catalog_hit_carries_its_own_solution(fail_step): def test_uncatalogued_rl_class_falls_back_to_the_family_entry(fail_step): - # AgenticRLError is the 1.x base but has no catalog entry of its own, - # so it must land on the family fallback rather than escape raw. + # AgenticRLError is the RL family's base but has no catalog entry of + # its own, so it must land on the family fallback, not escape raw. fail_step("register_functions", AgenticRLError("something odd")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: From cc84887521f43a1b59b305d4478ef39ed8104b75 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 23 Oct 2026 18:01:19 +0800 Subject: [PATCH 05/41] fix(tests): fix string formatting issue in tests --- tests/unit/test_acli_sdk_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_acli_sdk_contract.py b/tests/unit/test_acli_sdk_contract.py index baa56ec2..d8a32936 100644 --- a/tests/unit/test_acli_sdk_contract.py +++ b/tests/unit/test_acli_sdk_contract.py @@ -204,7 +204,7 @@ def test_acli_external_dashscope_imports_match_allowlist(): assert not added and not gone, ( "acli's coupling to the SDK changed.\n" f" added by a sync (accept consciously, then extend _ALLOWLIST): " - f"{ {m: found[m] for m in added} }\n" + f"{str({m: found[m] for m in added})}\n" f" no longer imported (_ALLOWLIST is stale): {gone}" ) From 64daf224d564d00fc6ae415f95b953e7a38d7ae7 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Fri, 23 Oct 2026 17:25:36 +0800 Subject: [PATCH 06/41] feat(v2): back transport and file I/O errors with catalog sdk.* entries Sync the vendored error registry to upstream 1.0.11 (commit 5b2d2e1), which adds the generic sdk.ConnectionError / sdk.ConnectionInterrupted / sdk.Timeout / sdk.IoError entries shared with the Java SDK. APIConnectionError / APITimeoutError become SdkError subclasses whose code / solution / allow_retry default from the catalog entries, and the httpx transport now formats their messages from the entry templates. Local file I/O failures in client.files raise sdk.IoError via the new sdk_error() factory instead of raw FileNotFoundError / OSError. sdk.ConnectionInterrupted is vendored for parity but intentionally not wired (a Java threading artifact). --- dashscope/v2/_error_registry.py | 58 ++++++++++++- dashscope/v2/errors.py | 139 ++++++++++++++++++++++---------- dashscope/v2/resources/files.py | 35 +++++--- dashscope/v2/transport/http.py | 11 ++- tests/v2/test_error_registry.py | 28 +++++-- tests/v2/test_errors.py | 44 ++++++++++ tests/v2/test_files_models.py | 25 +++++- tests/v2/test_transport.py | 7 +- 8 files changed, 277 insertions(+), 70 deletions(-) diff --git a/dashscope/v2/_error_registry.py b/dashscope/v2/_error_registry.py index 9c2675fa..54333af4 100644 --- a/dashscope/v2/_error_registry.py +++ b/dashscope/v2/_error_registry.py @@ -4,8 +4,8 @@ from dataclasses import dataclass, field from typing import Dict, List, Optional -__version__ = "1.0.6" -__commit__ = "56b7208" +__version__ = "1.0.11" +__commit__ = "5b2d2e1" __snapshot__ = False @@ -208,6 +208,56 @@ def format_message( vars=[], ) +SDK_CONNECTION_ERROR = SdkErrorDef( + name="sdk.ConnectionError", + message="Connection failed: {detail}.", + solution=( + "Check network connectivity, proxy and firewall " + "settings, and confirm the service endpoint is " + "reachable. Retry the request; persistent failures " + "usually indicate a local network problem." + ), + allow_retry=True, + vars=["detail"], +) + +SDK_CONNECTION_INTERRUPTED = SdkErrorDef( + name="sdk.ConnectionInterrupted", + message="Interrupted while waiting: {detail}.", + solution=( + "The thread was interrupted, typically because the " + "caller cancelled the operation. Re-issue the request " + "from a fresh thread if the cancellation was not " + "intentional." + ), + allow_retry=False, + vars=["detail"], +) + +SDK_TIMEOUT = SdkErrorDef( + name="sdk.Timeout", + message="Operation timed out: {detail}.", + solution=( + "The server or gateway did not respond within the " + "deadline. Check network latency and service status, " + "then retry; consider raising the timeout for " + "long-running operations." + ), + allow_retry=True, + vars=["detail"], +) + +SDK_IO_ERROR = SdkErrorDef( + name="sdk.IoError", + message="Local I/O failed: {detail}.", + solution=( + "Verify the file path exists and is readable, and that " + "the process has the required filesystem permissions." + ), + allow_retry=False, + vars=["detail"], +) + SDK_AGENTIC_RL_FUNCTION_REGISTRATION_FAILED = SdkErrorDef( name="sdk.agentic_rl.FunctionRegistrationFailed", message="Function component registration failed: {inner_code}.", @@ -616,6 +666,10 @@ def format_message( ] SDK_ERRORS = [ SDK_INVALID_API_KEY, + SDK_CONNECTION_ERROR, + SDK_CONNECTION_INTERRUPTED, + SDK_TIMEOUT, + SDK_IO_ERROR, SDK_AGENTIC_RL_FUNCTION_REGISTRATION_FAILED, SDK_AGENTIC_RL_DATASETS_UPLOAD_FAILED, SDK_AGENTIC_RL_DUPLICATE_FUNCTION_NAMES, diff --git a/dashscope/v2/errors.py b/dashscope/v2/errors.py index c2435535..018eb310 100644 --- a/dashscope/v2/errors.py +++ b/dashscope/v2/errors.py @@ -18,8 +18,11 @@ A third source has no response at all: failures the SDK reports on its own side. Those come from the catalog's ``SDK_ERRORS`` table as -:class:`SdkError`, and the v2 mount layers over 1.x subsystems -translate with :func:`error_from_sdk_exception`. +:class:`SdkError`; the transport errors (:class:`APIConnectionError` / +:class:`APITimeoutError`) are SdkError subclasses backed by the generic +``sdk.ConnectionError`` / ``sdk.Timeout`` entries, and the mounts over +sub-SDKs outside this error family (1.x agentic_rl, the 2.x AgentStudio +sub-SDK) translate with :func:`error_from_sdk_exception`. """ from __future__ import annotations @@ -85,12 +88,74 @@ def __str__(self) -> str: return " | ".join(parts) -class APIConnectionError(DashScopeError): - """Network-level failure (connection refused, DNS, TLS...).""" +_SDK_ERRORS: Dict[str, SdkErrorDef] = { + entry.name: entry for entry in _registry.SDK_ERRORS +} + + +class SdkError(DashScopeError): + """A failure the SDK reported itself, with no HTTP response behind it. + + ``code`` is the catalog name (``sdk.agentic_rl.OSSUploadError``) and + ``solution`` the fix guidance shipped alongside it. ``status_code`` + stays ``None``: the catalog gives SDK-side errors no public + projection, so inventing a status would add meaning the standard + does not carry. + """ + + def __init__( + self, + message: str, + *, + code: Optional[str] = None, + solution: Optional[str] = None, + allow_retry: bool = False, + request_id: Optional[str] = None, + body: Any = None, + headers: Optional[Mapping[str, str]] = None, + ) -> None: + super().__init__( + message, + code=code, + request_id=request_id, + body=body, + headers=headers, + ) + self.solution = solution + self.allow_retry = allow_retry + + +class APIConnectionError(SdkError): + """Network-level failure (connection refused, DNS, TLS...). + + Backed by the catalog's ``sdk.ConnectionError`` entry: ``code`` / + ``solution`` / ``allow_retry`` default from it, so every raise site + is catalog-identical without repeating the wiring. Pass ``detail`` + instead of ``message`` to use the entry's message template. + """ + + _SDK_ERROR_NAME = "sdk.ConnectionError" + + def __init__( + self, + message: Optional[str] = None, + *, + detail: Optional[str] = None, + **kwargs: Any, + ) -> None: + entry = _SDK_ERRORS[self._SDK_ERROR_NAME] + if message is None: + message = entry.format_message({"detail": detail or ""}) + kwargs.setdefault("code", entry.name) + kwargs.setdefault("solution", entry.solution) + kwargs.setdefault("allow_retry", entry.allow_retry) + super().__init__(message, **kwargs) class APITimeoutError(APIConnectionError): - """Request timed out.""" + """Request timed out; backed by the catalog's ``sdk.Timeout`` entry.""" + + _SDK_ERROR_NAME = "sdk.Timeout" class APIStatusError(DashScopeError): @@ -161,38 +226,6 @@ class ContentFilterError(BadRequestError): """ -class SdkError(DashScopeError): - """A failure the SDK reported itself, with no HTTP response behind it. - - ``code`` is the catalog name (``sdk.agentic_rl.OSSUploadError``) and - ``solution`` the fix guidance shipped alongside it. ``status_code`` - stays ``None``: the catalog gives SDK-side errors no public - projection, so inventing a status would add meaning the standard - does not carry. - """ - - def __init__( - self, - message: str, - *, - code: Optional[str] = None, - solution: Optional[str] = None, - allow_retry: bool = False, - request_id: Optional[str] = None, - body: Any = None, - headers: Optional[Mapping[str, str]] = None, - ) -> None: - super().__init__( - message, - code=code, - request_id=request_id, - body=body, - headers=headers, - ) - self.solution = solution - self.allow_retry = allow_retry - - _CATALOG_CLASSES: Dict[str, Type[APIStatusError]] = { cls.__name__: cls for cls in ( @@ -263,10 +296,6 @@ def _entry(key: str) -> PublicError: for wire, entry in _WIRE_TO_PUBLIC.items() } -_SDK_ERRORS: Dict[str, SdkErrorDef] = { - entry.name: entry for entry in _registry.SDK_ERRORS -} - def _extract( body: Any, @@ -347,19 +376,41 @@ def error_from_business_code( ) +def sdk_error( + name: str, + variables: Optional[Dict[str, str]] = None, + **kwargs: Any, +) -> SdkError: + """Build an :class:`SdkError` straight from a catalog entry. + + For ``SDK_ERRORS`` entries with no dedicated exception class (e.g. + ``sdk.IoError``): message text, ``code``, ``solution`` and + ``allow_retry`` all come from the entry, so the raise site carries + no catalog knowledge of its own. + """ + entry = _SDK_ERRORS[name] + return SdkError( + entry.format_message(variables), + code=entry.name, + solution=entry.solution, + allow_retry=entry.allow_retry, + **kwargs, + ) + + def error_from_sdk_exception( exc: BaseException, *, family: str, ) -> SdkError: - """Translate a 1.x SDK-side exception into the v2 error family. + """Translate a mounted sub-SDK's own exception into the v2 family. ``family`` is the catalog namespace the subsystem owns, e.g. ``sdk.agentic_rl``. Lookup is two-tier and needs no hand-maintained table: ``.`` when the catalog carries that exact class, otherwise the family's ``.Error`` fallback. Message - text and the 1.x numeric ``error_code`` are deliberately never - sniffed -- both are 1.x internals. + text and any numeric ``error_code`` are deliberately never sniffed + -- both are the source SDK's internals. """ name = f"{family}.{type(exc).__name__}" entry = _SDK_ERRORS.get(name) diff --git a/dashscope/v2/resources/files.py b/dashscope/v2/resources/files.py index 3d1b94e2..23654c6e 100644 --- a/dashscope/v2/resources/files.py +++ b/dashscope/v2/resources/files.py @@ -16,6 +16,7 @@ NotFoundError, check_response, response_error, + sdk_error, ) from dashscope.v2.types.common import ( ResponseBase, @@ -123,15 +124,24 @@ def _parse_delete(body: Dict[str, Any]) -> DeletionStatus: def _is_valid_jsonl(file_path: str) -> bool: - with open(file_path, encoding="utf-8") as f: - for line in f: - try: - json.loads(line) - except json.decoder.JSONDecodeError: - return False + try: + with open(file_path, encoding="utf-8") as f: + for line in f: + try: + json.loads(line) + except json.decoder.JSONDecodeError: + return False + except OSError as exc: + raise _io_error(exc) from exc return True +def _io_error(exc: BaseException) -> Exception: + # Local filesystem failures are SDK-side, not gateway errors: the + # catalog's sdk.IoError entry supplies message/code/solution. + return sdk_error("sdk.IoError", {"detail": str(exc)}) + + def _validate_upload(file_path: str, purpose: str) -> None: if purpose in ("fine-tune", "fine_tune"): if not _is_valid_jsonl(file_path): @@ -139,17 +149,20 @@ def _validate_upload(file_path: str, purpose: str) -> None: f"file {file_path} is not in valid jsonl format", ) if not os.path.isfile(file_path): - raise FileNotFoundError(f"file not found: {file_path}") + raise _io_error(FileNotFoundError(f"file not found: {file_path}")) def _upload_form(file_path: str, description: Optional[str]): data = {} if description is not None: data["descriptions"] = description - with open(file_path, "rb") as f: - # Bytes, not a live handle: transport retries must be able - # to re-send the full body. - content = f.read() + try: + with open(file_path, "rb") as f: + # Bytes, not a live handle: transport retries must be able + # to re-send the full body. + content = f.read() + except OSError as exc: + raise _io_error(exc) from exc return data, {"files": (os.path.basename(file_path), content)} diff --git a/dashscope/v2/transport/http.py b/dashscope/v2/transport/http.py index ffc58c46..7bb805e1 100644 --- a/dashscope/v2/transport/http.py +++ b/dashscope/v2/transport/http.py @@ -28,10 +28,15 @@ def _map_transport_error(exc: httpx.TransportError) -> DashScopeError: - """Map raw httpx transport failures into the SDK error hierarchy.""" + """Map raw httpx transport failures into the SDK error hierarchy. + + The message text comes from the catalog entry behind each class + (``sdk.Timeout`` / ``sdk.ConnectionError``), with the httpx error as + the ``{detail}``. + """ if isinstance(exc, httpx.TimeoutException): - return APITimeoutError(f"request timed out: {exc}") - return APIConnectionError(f"connection error: {exc}") + return APITimeoutError(detail=str(exc)) + return APIConnectionError(detail=str(exc)) def _resolve_timeout( diff --git a/tests/v2/test_error_registry.py b/tests/v2/test_error_registry.py index 3ba08e95..d605a041 100644 --- a/tests/v2/test_error_registry.py +++ b/tests/v2/test_error_registry.py @@ -4,7 +4,7 @@ ``dashscope/v2/_error_registry.py`` is a generated artifact from the separate ``dashscope-error-registry`` repo (``tools/generate.py`` over -``data/*.textproto``), vendored at ``__version__ == "1.0.6"``. Only the +``data/*.textproto``), vendored at ``__version__ == "1.0.11"``. Only the ``PublicError`` and ``SdkErrorDef`` tables are carried; see ``test_vendored_copy_is_pruned_to_the_tables_v2_consumes``. @@ -83,11 +83,11 @@ def _by_key(key): def test_catalog_size_and_provenance(): - assert registry.__version__ == "1.0.6" - assert registry.__commit__ == "56b7208" + assert registry.__version__ == "1.0.11" + assert registry.__commit__ == "5b2d2e1" assert registry.__snapshot__ is False assert len(PUBLIC) == 16 - assert len(SDK) == 32 + assert len(SDK) == 36 def test_generated_header_survives_vendoring(): @@ -97,7 +97,7 @@ def test_generated_header_survives_vendoring(): def test_vendored_copy_is_pruned_to_the_tables_v2_consumes(): - """Upstream 1.0.6 also ships a 277-entry ``INTERNAL_ERRORS`` table. + """Upstream 1.0.11 also ships a 299-entry ``INTERNAL_ERRORS`` table. It describes server-side components (``gateway.*`` / ``mint.*`` / ``api.*`` / ``turbo.*`` / ``model.*``) through ``InternalErrorDef``, @@ -287,10 +287,24 @@ def test_agentstudio_has_no_family_fallback(): assert status_classes, "expected 1.x agentstudio HTTP error classes" -def test_invalid_api_key_is_the_only_unfamily_entry(): +def test_unfamily_entries_are_the_generic_sdk_ones(): + """Entries outside a product family are the cross-SDK generics. + + ``sdk.InvalidApiKey`` plus the transport/file quartet shared with + the Java SDK (Python wires up ConnectionError / Timeout / IoError; + ConnectionInterrupted is a Java threading artifact carried for + parity). Anything else landing here means a product family grew + without its ``sdk..`` prefix -- that is a catalog bug. + """ others = [ e.name for e in SDK if not e.name.startswith(("sdk.agentic_rl.", "sdk.agentstudio.")) ] - assert others == ["sdk.InvalidApiKey"] + assert others == [ + "sdk.InvalidApiKey", + "sdk.ConnectionError", + "sdk.ConnectionInterrupted", + "sdk.Timeout", + "sdk.IoError", + ] diff --git a/tests/v2/test_errors.py b/tests/v2/test_errors.py index f22b3ca5..b58767fa 100644 --- a/tests/v2/test_errors.py +++ b/tests/v2/test_errors.py @@ -22,7 +22,9 @@ from dashscope.v2 import _error_registry as registry from dashscope.v2 import errors from dashscope.v2.errors import ( + APIConnectionError, APIStatusError, + APITimeoutError, AuthenticationError, BadRequestError, ContentFilterError, @@ -39,6 +41,7 @@ check_response, error_from_business_code, error_from_sdk_exception, + sdk_error, ) from dashscope.v2.errors import ( _CODE_MAP, @@ -828,6 +831,47 @@ def test_sdk_error_has_no_http_semantics(): assert SdkError("boom").status_code is None +def test_transport_errors_are_catalog_backed_sdk_errors(): + assert issubclass(APIConnectionError, SdkError) + assert issubclass(APITimeoutError, APIConnectionError) + + conn = APIConnectionError("boom") + conn_entry = _SDK_ERRORS["sdk.ConnectionError"] + assert conn.code == conn_entry.name + assert conn.solution == conn_entry.solution + assert conn.allow_retry == conn_entry.allow_retry + assert conn.status_code is None + + timeout = APITimeoutError("boom") + timeout_entry = _SDK_ERRORS["sdk.Timeout"] + assert timeout.code == timeout_entry.name + assert timeout.solution == timeout_entry.solution + assert timeout.allow_retry == timeout_entry.allow_retry + + +def test_transport_error_detail_fills_the_catalog_template(): + err = APIConnectionError(detail="dns blew up") + assert err.message == "Connection failed: dns blew up." + err = APITimeoutError(detail="slow") + assert err.message == "Operation timed out: slow." + + +def test_transport_error_explicit_fields_win_over_catalog_defaults(): + err = APIConnectionError("boom", code="custom", allow_retry=False) + assert err.code == "custom" + assert err.allow_retry is False + + +def test_sdk_error_factory_builds_classless_entries_from_the_catalog(): + entry = _SDK_ERRORS["sdk.IoError"] + err = sdk_error("sdk.IoError", {"detail": "disk full"}) + assert type(err) is SdkError + assert err.message == "Local I/O failed: disk full." + assert err.code == entry.name + assert err.solution == entry.solution + assert err.allow_retry is entry.allow_retry + + def test_sdk_error_translation_resolves_the_catalog_by_class_name(): err = error_from_sdk_exception( OSSUploadError("bucket denied"), diff --git a/tests/v2/test_files_models.py b/tests/v2/test_files_models.py index ff76efbe..ee76334d 100644 --- a/tests/v2/test_files_models.py +++ b/tests/v2/test_files_models.py @@ -8,7 +8,7 @@ import pytest from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import BadRequestError, NotFoundError +from dashscope.v2.errors import BadRequestError, NotFoundError, SdkError BASE_URL = "https://api.test.local/api/v1" @@ -64,8 +64,11 @@ def handler(request): def test_files_upload_validation(tmp_path): client = _make_client(lambda r: httpx.Response(200, json={"data": {}})) - with pytest.raises(FileNotFoundError): + with pytest.raises(SdkError) as missing: client.files.upload(file_path=str(tmp_path / "nope.jsonl")) + assert missing.value.code == "sdk.IoError" + assert "nope.jsonl" in missing.value.message + assert missing.value.allow_retry is False bad = tmp_path / "bad.jsonl" bad.write_text("not json\n", encoding="utf-8") @@ -76,6 +79,24 @@ def test_files_upload_validation(tmp_path): assert resp is not None +def test_files_upload_unreadable_file_is_catalog_io_error( + tmp_path, + monkeypatch, +): + client = _make_client(lambda r: httpx.Response(200, json={"data": {}})) + path = tmp_path / "x.jsonl" + path.write_text("{}\n", encoding="utf-8") + + def _deny(*args, **kwargs): + raise PermissionError("denied") + + monkeypatch.setattr("builtins.open", _deny) + with pytest.raises(SdkError) as ei: + client.files.upload(file_path=str(path)) + assert ei.value.code == "sdk.IoError" + assert ei.value.message == "Local I/O failed: denied." + + def test_files_upload_retry_resends_full_body(tmp_path, monkeypatch): bodies = [] diff --git a/tests/v2/test_transport.py b/tests/v2/test_transport.py index 82e04875..f4820014 100644 --- a/tests/v2/test_transport.py +++ b/tests/v2/test_transport.py @@ -133,6 +133,8 @@ def test_connection_error_retried_then_raises(no_sleep): with pytest.raises(APIConnectionError) as exc_info: t.request("GET", "/x") assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + assert exc_info.value.code == "sdk.ConnectionError" + assert exc_info.value.allow_retry is True assert rec.calls == 3 assert len(no_sleep) == 2 @@ -148,14 +150,17 @@ def test_timeout_maps_to_api_timeout_error(no_sleep): t.request("GET", "/x") assert isinstance(exc_info.value, APIConnectionError) assert isinstance(exc_info.value.__cause__, httpx.ConnectTimeout) + assert exc_info.value.code == "sdk.Timeout" + assert exc_info.value.allow_retry is True assert rec.calls == 3 def test_open_stream_transport_error_mapped(no_sleep): rec = _Recorder() t = _make_sync(rec, [httpx.ConnectError("boom")], max_retries=2) - with pytest.raises(APIConnectionError): + with pytest.raises(APIConnectionError) as exc_info: t.open_stream("POST", "/x") + assert exc_info.value.code == "sdk.ConnectionError" # Streams are never retried. assert rec.calls == 1 From ba7aabda09ff4f45d8953b9f99ee0b73a34c0910 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Fri, 23 Oct 2026 18:47:14 +0800 Subject: [PATCH 07/41] feat(v2): sync error registry to cd873b6 and wire sdk.ws.* errors Re-vendor the catalog at upstream 1.0.11 commit cd873b6, which adds the public response_error entry and five sdk.ws.* entries. ResponseError joins the status-error hierarchy as a flat APIStatusError sibling. It shares status 500 with internal_error, so the status map now lets the first catalog claimant win: a bare 500 still resolves to InternalServerError, and 500 joins 400/401/429 as a status too ambiguous for catalog fallback text. ResponseError itself stays SDK-side with no wire-code or status mapping. The websocket transport's ad-hoc RuntimeError/WsEventError raises are replaced by catalog-backed SdkError subclasses: WsEventError now carries sdk.ws.ProtocolError, and the new WsStateError / WsBinaryOwnerConflictError carry sdk.ws.IllegalState / sdk.ws.BinaryOwnerConflict. sdk.ws.UpstreamQueueFull (synchronous send, no buffer) and sdk.ws.QueueCapacityExceeded (unbounded per-task queues) describe mechanisms the Python transport does not have and are vendored for parity only. --- dashscope/v2/_error_registry.py | 79 ++++++++++++++++++++- dashscope/v2/errors.py | 94 ++++++++++++++++++++----- dashscope/v2/transport/ws.py | 26 ++++--- dashscope/v2/transport/ws_connection.py | 46 ++++++------ tests/v2/test_audio.py | 8 +-- tests/v2/test_audio_connection.py | 6 +- tests/v2/test_audio_tts_session.py | 4 +- tests/v2/test_error_registry.py | 48 +++++++++++-- tests/v2/test_errors.py | 60 ++++++++++++++-- tests/v2/test_ws_connection.py | 31 ++++---- 10 files changed, 316 insertions(+), 86 deletions(-) diff --git a/dashscope/v2/_error_registry.py b/dashscope/v2/_error_registry.py index 54333af4..b6dcea3a 100644 --- a/dashscope/v2/_error_registry.py +++ b/dashscope/v2/_error_registry.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional __version__ = "1.0.11" -__commit__ = "5b2d2e1" +__commit__ = "cd873b6" __snapshot__ = False @@ -174,6 +174,14 @@ def format_message( anthropic_error_code="api_error", ) +RESPONSE_ERROR = PublicError( + key="response_error", + status_code=500, + error_code="ResponseError", + error_msg="The server returned a response that could not be parsed.", + anthropic_error_code="api_error", +) + SERVICE_UNAVAILABLE = PublicError( key="service_unavailable", status_code=503, @@ -644,6 +652,69 @@ def format_message( vars=[], ) +SDK_WS_PROTOCOL_ERROR = SdkErrorDef( + name="sdk.ws.ProtocolError", + message="WebSocket protocol violation: {detail}.", + solution=( + "The gateway sent a frame that violates the task protocol " + "(missing header, unknown task_id, or an unexpected event). " + "Not retryable; if it persists, report it at " + "https://github.com/dashscope/dashscope-sdk-python/issues " + "with the frame detail." + ), + allow_retry=False, + vars=["detail"], +) + +SDK_WS_UPSTREAM_QUEUE_FULL = SdkErrorDef( + name="sdk.ws.UpstreamQueueFull", + message="WebSocket upstream send queue is full.", + solution=( + "The client is producing frames faster than the network " + "drains them (the send buffer is full). Slow the producer " + "down — pace audio frames to real time instead of buffering " + "unboundedly." + ), + allow_retry=False, + vars=[], +) + +SDK_WS_QUEUE_CAPACITY_EXCEEDED = SdkErrorDef( + name="sdk.ws.QueueCapacityExceeded", + message="A task receive queue exceeded its configured capacity.", + solution=( + "The consumer is slower than the producer and the bounded " + "receive queue overflowed. Drain events faster or raise the " + "capacity; frames are never dropped silently." + ), + allow_retry=False, + vars=[], +) + +SDK_WS_BINARY_OWNER_CONFLICT = SdkErrorDef( + name="sdk.ws.BinaryOwnerConflict", + message="Binary stream ownership conflict: {detail}.", + solution=( + "A shared WebSocket connection supports at most one binary " + "uplink (ASR) and one binary downlink (TTS). Use a separate " + "connection for a second binary task." + ), + allow_retry=False, + vars=["detail"], +) + +SDK_WS_ILLEGAL_STATE = SdkErrorDef( + name="sdk.ws.IllegalState", + message="Illegal WebSocket task state: {detail}.", + solution=( + "The operation is not valid for the task's current state " + "(duplicate task_id, send after close, receive on a closed " + "session). Check the session lifecycle." + ), + allow_retry=False, + vars=["detail"], +) + # -- Catalogs ------------------------------------------------------ PUBLIC_ERRORS = [ @@ -661,6 +732,7 @@ def format_message( CONCURRENCY_LIMIT_EXCEEDED, INSUFFICIENT_QUOTA, INTERNAL_ERROR, + RESPONSE_ERROR, SERVICE_UNAVAILABLE, REQUEST_TIMEOUT, ] @@ -701,4 +773,9 @@ def format_message( SDK_AGENTSTUDIO_API_TIMEOUT_ERROR, SDK_AGENTSTUDIO_STREAM_ERROR, SDK_AGENTSTUDIO_STREAM_CLOSED_ERROR, + SDK_WS_PROTOCOL_ERROR, + SDK_WS_UPSTREAM_QUEUE_FULL, + SDK_WS_QUEUE_CAPACITY_EXCEEDED, + SDK_WS_BINARY_OWNER_CONFLICT, + SDK_WS_ILLEGAL_STATE, ] diff --git a/dashscope/v2/errors.py b/dashscope/v2/errors.py index 018eb310..77c83fac 100644 --- a/dashscope/v2/errors.py +++ b/dashscope/v2/errors.py @@ -20,9 +20,12 @@ own side. Those come from the catalog's ``SDK_ERRORS`` table as :class:`SdkError`; the transport errors (:class:`APIConnectionError` / :class:`APITimeoutError`) are SdkError subclasses backed by the generic -``sdk.ConnectionError`` / ``sdk.Timeout`` entries, and the mounts over -sub-SDKs outside this error family (1.x agentic_rl, the 2.x AgentStudio -sub-SDK) translate with :func:`error_from_sdk_exception`. +``sdk.ConnectionError`` / ``sdk.Timeout`` entries, the websocket +transport errors (:class:`WsEventError` / :class:`WsStateError` / +:class:`WsBinaryOwnerConflictError`) are backed by the ``sdk.ws.*`` +entries, and the mounts over sub-SDKs outside this error family (1.x +agentic_rl, the 2.x AgentStudio sub-SDK) translate with +:func:`error_from_sdk_exception`. """ from __future__ import annotations @@ -125,16 +128,10 @@ def __init__( self.allow_retry = allow_retry -class APIConnectionError(SdkError): - """Network-level failure (connection refused, DNS, TLS...). +class _CatalogSdkError(SdkError): + """SdkError whose message defaults to the entry's ``{detail}`` template.""" - Backed by the catalog's ``sdk.ConnectionError`` entry: ``code`` / - ``solution`` / ``allow_retry`` default from it, so every raise site - is catalog-identical without repeating the wiring. Pass ``detail`` - instead of ``message`` to use the entry's message template. - """ - - _SDK_ERROR_NAME = "sdk.ConnectionError" + _SDK_ERROR_NAME = "" def __init__( self, @@ -152,12 +149,54 @@ def __init__( super().__init__(message, **kwargs) +class APIConnectionError(_CatalogSdkError): + """Network-level failure (connection refused, DNS, TLS...). + + Backed by the catalog's ``sdk.ConnectionError`` entry: ``code`` / + ``solution`` / ``allow_retry`` default from it, so every raise site + is catalog-identical without repeating the wiring. Pass ``detail`` + instead of ``message`` to use the entry's message template. + """ + + _SDK_ERROR_NAME = "sdk.ConnectionError" + + class APITimeoutError(APIConnectionError): """Request timed out; backed by the catalog's ``sdk.Timeout`` entry.""" _SDK_ERROR_NAME = "sdk.Timeout" +class WsEventError(_CatalogSdkError): + """The server sent an unexpected websocket protocol event. + + Backed by the catalog's ``sdk.ws.ProtocolError`` entry: ``code`` / + ``solution`` / ``allow_retry`` default from it. Pass ``detail`` + instead of ``message`` to use the entry's message template. + """ + + _SDK_ERROR_NAME = "sdk.ws.ProtocolError" + + +class WsStateError(_CatalogSdkError): + """Operation invalid for the websocket task's current state. + + Backed by the catalog's ``sdk.ws.IllegalState`` entry: duplicate + start, send after close, receive on a closed session. + """ + + _SDK_ERROR_NAME = "sdk.ws.IllegalState" + + +class WsBinaryOwnerConflictError(_CatalogSdkError): + """A shared connection allows one binary owner per direction. + + Backed by the catalog's ``sdk.ws.BinaryOwnerConflict`` entry. + """ + + _SDK_ERROR_NAME = "sdk.ws.BinaryOwnerConflict" + + class APIStatusError(DashScopeError): """A failure the gateway declared, on either layer of the model. @@ -207,6 +246,17 @@ class InternalServerError(APIStatusError): status_code = _CATALOG_STATUS["InternalServerError"] +class ResponseError(APIStatusError): + """The server returned a response that could not be parsed. + + SDK-side, never gateway-sent: it shares status 500 with + ``internal_error``, so the status maps below must keep resolving a + bare 500 to :class:`InternalServerError`. + """ + + status_code = _CATALOG_STATUS["ResponseError"] + + class ServiceUnavailableError(APIStatusError): status_code = _CATALOG_STATUS["ServiceUnavailableError"] @@ -236,23 +286,29 @@ class ContentFilterError(BadRequestError): RequestTooLargeError, RateLimitError, InternalServerError, + ResponseError, ServiceUnavailableError, GatewayTimeoutError, ) } -_STATUS_MAP: Dict[int, Type[APIStatusError]] = { - entry.status_code: _CATALOG_CLASSES[entry.error_code] - for entry in _registry.PUBLIC_ERRORS -} +# First entry per status wins: response_error shares 500 with +# internal_error, and a bare 500 from the gateway means internal_error -- +# ResponseError is raised by the SDK itself, never picked by status. +_STATUS_MAP: Dict[int, Type[APIStatusError]] = {} +for _entry_ in _registry.PUBLIC_ERRORS: + _STATUS_MAP.setdefault( + _entry_.status_code, _CATALOG_CLASSES[_entry_.error_code] + ) +del _entry_ _STATUS_COUNTS = Counter( entry.status_code for entry in _registry.PUBLIC_ERRORS ) -# Only the statuses a single entry claims. 400 / 401 / 429 carry siblings -# with different ``error_msg`` text, so a status alone cannot pick one -# without stating a reason the gateway never gave. +# Only the statuses a single entry claims. 400 / 401 / 429 / 500 carry +# siblings with different ``error_msg`` text, so a status alone cannot +# pick one without stating a reason the gateway never gave. _STATUS_ENTRY: Dict[int, PublicError] = { entry.status_code: entry for entry in _registry.PUBLIC_ERRORS diff --git a/dashscope/v2/transport/ws.py b/dashscope/v2/transport/ws.py index 71b2c9fa..e5f073f9 100644 --- a/dashscope/v2/transport/ws.py +++ b/dashscope/v2/transport/ws.py @@ -18,7 +18,8 @@ ) from dashscope.v2.errors import ( APIConnectionError, - DashScopeError, + WsEventError, + WsStateError, error_from_business_code, ) from dashscope.v2.transport import default_user_agent @@ -31,6 +32,7 @@ "EVENT_STARTED", "WsEventError", "WsSession", + "WsStateError", ] EVENT_STARTED = "task-started" @@ -41,10 +43,6 @@ Connector = Callable[[str, Dict[str, str]], Any] -class WsEventError(DashScopeError): - """The server sent an unexpected websocket protocol event.""" - - def _default_connector( url: str, headers: Dict[str, str], @@ -145,11 +143,11 @@ def _process_event(self, event: Dict[str, Any]) -> Dict[str, Any]: event.get("header"), dict, ): - raise WsEventError("expected a websocket event header") + raise WsEventError(detail="expected a websocket event header") header = event["header"] if header.get("task_id") not in (None, self.task_id): raise WsEventError( - "websocket event belongs to a different task", + detail="websocket event belongs to a different task", request_id=self._request_id, body=event, ) @@ -269,14 +267,14 @@ def run_task( frame["header"]["streaming"] = streaming with self._lock: if self._closed or self._run_sent: - raise RuntimeError("websocket task already started or closed") + raise WsStateError(detail="task already started or closed") self._run_sent = True self._send(frame) try: event = self.recv_event() if event["header"].get("event") != EVENT_STARTED: raise WsEventError( - "expected task-started, got " + detail="expected task-started, got " f"{event['header'].get('event')!r}", request_id=self.request_id, ) @@ -288,9 +286,9 @@ def run_task( def _check_input(self) -> None: if self._closed or not self._started or self._finished: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._input_finished: - raise RuntimeError("websocket task input is finished") + raise WsStateError(detail="task input is finished") def send_binary(self, data: bytes) -> None: if not isinstance(data, (bytes, bytearray, memoryview)): @@ -312,7 +310,7 @@ def finish_task(self, input: Optional[Dict[str, Any]] = None) -> None: """ with self._lock: if self._closed or not self._started: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._finished: return cancel = input is not None and input.get("directive") == "cancel" @@ -327,7 +325,7 @@ def recv_event(self) -> Dict[str, Any]: while True: frame = self.recv() if frame is None: - raise RuntimeError("websocket task is finished") + raise WsStateError(detail="task is finished") if isinstance(frame, dict): return frame @@ -336,7 +334,7 @@ def recv(self) -> Union[Dict[str, Any], bytes, None]: if self._finished: return None if self._closed: - raise RuntimeError("websocket session is closed") + raise WsStateError(detail="session is closed") try: raw = _recv_frame(self._ws) if raw is None or raw == "": diff --git a/dashscope/v2/transport/ws_connection.py b/dashscope/v2/transport/ws_connection.py index 450873dc..27165983 100644 --- a/dashscope/v2/transport/ws_connection.py +++ b/dashscope/v2/transport/ws_connection.py @@ -40,7 +40,11 @@ settings, workspace_ws_url, ) -from dashscope.v2.errors import APIConnectionError +from dashscope.v2.errors import ( + APIConnectionError, + WsBinaryOwnerConflictError, + WsStateError, +) from dashscope.v2.transport.ws import ( EVENT_FAILED, EVENT_FINISHED, @@ -149,14 +153,14 @@ def run_task( frame["header"]["streaming"] = streaming with self._lock: if self._closed or self._run_sent: - raise RuntimeError("websocket task already started or closed") + raise WsStateError(detail="task already started or closed") self._run_sent = True self._conn._send(frame) try: event = self.recv_event() if event["header"].get("event") != EVENT_STARTED: raise WsEventError( - "expected task-started, got " + detail="expected task-started, got " f"{event['header'].get('event')!r}", request_id=self.request_id, ) @@ -168,9 +172,9 @@ def run_task( def _check_input(self) -> None: if self._closed or not self._started or self._finished: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._input_finished: - raise RuntimeError("websocket task input is finished") + raise WsStateError(detail="task input is finished") def claim_binary_downlink(self) -> None: """Route binary output frames to this task. @@ -198,7 +202,7 @@ def finish_task(self, input: Optional[Dict[str, Any]] = None) -> None: """End input, or cancel synthesis while output is still arriving.""" with self._lock: if self._closed or not self._started: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._finished: return cancel = input is not None and input.get("directive") == "cancel" @@ -215,7 +219,7 @@ def recv_event(self) -> Dict[str, Any]: while True: frame = self.recv() if frame is None: - raise RuntimeError("websocket task is finished") + raise WsStateError(detail="task is finished") if isinstance(frame, dict): return frame @@ -309,7 +313,7 @@ def open_task(self, *, timeout: Optional[float] = None) -> WsTask: raise ValueError("timeout must be positive") with self._lock: if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") task = WsTask( self, uuid.uuid4().hex, @@ -321,7 +325,7 @@ def open_task(self, *, timeout: Optional[float] = None) -> WsTask: def _send(self, data: Any, *, opcode: int = 1) -> None: with self._send_lock: if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") try: if opcode == 2: self._ws.send(data, opcode=2) @@ -334,22 +338,23 @@ def _send(self, data: Any, *, opcode: int = 1) -> None: def _check_binary_owner(self, task: WsTask) -> None: """Validate under the connection lock; pre-start claims are allowed.""" if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") if ( self._tasks.get(task.task_id) is not task or task._closed or task._finished or task._routing_finished ): - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") def _claim_binary_uplink(self, task: WsTask) -> None: with self._lock: self._check_binary_owner(task) owner = self._binary_uplink_owner if owner is not None and owner is not task: - raise RuntimeError( - "another task owns the binary uplink on this connection", + raise WsBinaryOwnerConflictError( + detail="another task owns the binary uplink on this " + "connection", ) self._binary_uplink_owner = task @@ -358,8 +363,9 @@ def _claim_binary_downlink(self, task: WsTask) -> None: self._check_binary_owner(task) owner = self._binary_downlink_owner if owner is not None and owner is not task: - raise RuntimeError( - "another task owns the binary downlink on this connection", + raise WsBinaryOwnerConflictError( + detail="another task owns the binary downlink on this " + "connection", ) self._binary_downlink_owner = task @@ -372,7 +378,7 @@ def _release_binary(self, task: WsTask) -> None: def _detach(self, task: WsTask) -> None: with self._lock: - task._enqueue((_ERROR, RuntimeError("websocket task is closed"))) + task._enqueue((_ERROR, WsStateError(detail="task is closed"))) task._closed = True task._routing_active = False if self._tasks.get(task.task_id) is task: @@ -397,10 +403,10 @@ def _reader_main(self) -> None: def _route_event(self, event: Any) -> None: header = event.get("header") if isinstance(event, dict) else None if not isinstance(header, dict): - raise WsEventError("expected a websocket event header") + raise WsEventError(detail="expected a websocket event header") task_id = header.get("task_id") if not isinstance(task_id, str) or not task_id: - raise WsEventError("expected a non-empty websocket task_id") + raise WsEventError(detail="expected a non-empty websocket task_id") with self._lock: task = self._tasks.get(task_id) if self._closed or task is None or task._routing_finished: @@ -431,8 +437,8 @@ def _route_binary(self, data: bytes) -> None: return if len(active) != 1: raise WsEventError( - "binary frame with multiple active tasks; claim the " - "downlink with claim_binary_downlink()", + detail="binary frame with multiple active tasks; " + "claim the downlink with claim_binary_downlink()", ) owner = active[0] self._binary_downlink_owner = owner diff --git a/tests/v2/test_audio.py b/tests/v2/test_audio.py index e918f513..3a9ea729 100644 --- a/tests/v2/test_audio.py +++ b/tests/v2/test_audio.py @@ -13,7 +13,7 @@ import pytest from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import APIConnectionError, DashScopeError +from dashscope.v2.errors import APIConnectionError, DashScopeError, WsStateError from dashscope.v2.resources import audio as audio_resources from dashscope.v2.resources.audio._callbacks import run_with_callback from dashscope.v2.resources.audio.asr import RecognitionResult @@ -463,9 +463,9 @@ def connector(url, headers): rec.send_audio_frame(b"second") rec.stop() rec.stop() - with pytest.raises(RuntimeError, match="input is finished"): + with pytest.raises(WsStateError, match="input is finished"): rec.send_audio_frame(b"late") - with pytest.raises(RuntimeError, match="input is finished"): + with pytest.raises(WsStateError, match="input is finished"): rec.update_context(context) assert next(events)["header"]["event"] == "task-finished" assert list(events) == [] @@ -543,7 +543,7 @@ def send(self, payload, opcode=1): with pytest.raises(OSError, match="send failed"): rec.send_audio_frame(b"audio") assert ws.closed - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): rec.send_audio_frame(b"more") diff --git a/tests/v2/test_audio_connection.py b/tests/v2/test_audio_connection.py index d8d962d4..29d8194c 100644 --- a/tests/v2/test_audio_connection.py +++ b/tests/v2/test_audio_connection.py @@ -10,7 +10,7 @@ import pytest from dashscope.v2.client import Client -from dashscope.v2.errors import DashScopeError +from dashscope.v2.errors import DashScopeError, WsBinaryOwnerConflictError BASE_URL = "https://api.test.local/api/v1" WS_URL = "wss://ws.test.local/api-ws/v1/inference" @@ -269,7 +269,7 @@ def test_two_syntheses_on_one_connection_fail_fast(): tts1 = client.audio.tts.connect(model="cosyvoice-v1", connection=conn) tts1.start() tts2 = client.audio.tts.connect(model="cosyvoice-v1", connection=conn) - with pytest.raises(RuntimeError, match="binary downlink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary downlink"): tts2.start() # The first session is unaffected. tts1.send_text("继续") @@ -502,7 +502,7 @@ def test_shared_recognition_close_after_uplink_conflict_does_not_break_owner( first.start() second.start() first.send_audio_frame(b"first") - with pytest.raises(RuntimeError, match="binary uplink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary uplink"): second.send_audio_frame(b"blocked") second.close() first.send_audio_frame(b"still first") diff --git a/tests/v2/test_audio_tts_session.py b/tests/v2/test_audio_tts_session.py index cafdebd3..34b0875b 100644 --- a/tests/v2/test_audio_tts_session.py +++ b/tests/v2/test_audio_tts_session.py @@ -15,7 +15,7 @@ import pytest from dashscope.v2 import Client -from dashscope.v2.errors import APIConnectionError, BadRequestError +from dashscope.v2.errors import APIConnectionError, BadRequestError, WsStateError from dashscope.v2.resources.audio.tts import SynthesisSession @@ -278,7 +278,7 @@ def test_no_more_text_or_flush_after_input_ends(client, ending, operation): with _session(client, ws) as session: getattr(session, ending)() sent_count = len(ws.sent) - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): if operation == "send_text": session.send_text("too late") else: diff --git a/tests/v2/test_error_registry.py b/tests/v2/test_error_registry.py index d605a041..89f2815f 100644 --- a/tests/v2/test_error_registry.py +++ b/tests/v2/test_error_registry.py @@ -48,7 +48,7 @@ "sdk.agentic_rl.FunctionTestTimeout", } -# The nine exception class names the catalog requires. +# The ten exception class names the catalog requires. _REQUIRED_CLASSES = { "AuthenticationError", "BadRequestError", @@ -58,6 +58,7 @@ "PermissionDeniedError", "RateLimitError", "RequestTooLargeError", + "ResponseError", "ServiceUnavailableError", } @@ -84,10 +85,10 @@ def _by_key(key): def test_catalog_size_and_provenance(): assert registry.__version__ == "1.0.11" - assert registry.__commit__ == "5b2d2e1" + assert registry.__commit__ == "cd873b6" assert registry.__snapshot__ is False - assert len(PUBLIC) == 16 - assert len(SDK) == 36 + assert len(PUBLIC) == 17 + assert len(SDK) == 41 def test_generated_header_survives_vendoring(): @@ -130,7 +131,7 @@ def test_public_entries_are_immutable(): PUBLIC[0].key = "mutated" -def test_catalog_requires_exactly_nine_exception_classes(): +def test_catalog_requires_exactly_ten_exception_classes(): assert {entry.error_code for entry in PUBLIC} == _REQUIRED_CLASSES @@ -299,7 +300,9 @@ def test_unfamily_entries_are_the_generic_sdk_ones(): others = [ e.name for e in SDK - if not e.name.startswith(("sdk.agentic_rl.", "sdk.agentstudio.")) + if not e.name.startswith( + ("sdk.agentic_rl.", "sdk.agentstudio.", "sdk.ws.") + ) ] assert others == [ "sdk.InvalidApiKey", @@ -308,3 +311,36 @@ def test_unfamily_entries_are_the_generic_sdk_ones(): "sdk.Timeout", "sdk.IoError", ] + + +def test_ws_family_entries(): + """The five websocket entries; three wired, two carried for parity. + + ProtocolError / IllegalState / BinaryOwnerConflict back the v2 + websocket transport's exception classes. UpstreamQueueFull (sync + send, no send buffer) and QueueCapacityExceeded (unbounded per-task + queues) describe mechanisms the Python transport does not have. + """ + entries = _by_name(SDK) + expected = { + "sdk.ws.ProtocolError", + "sdk.ws.UpstreamQueueFull", + "sdk.ws.QueueCapacityExceeded", + "sdk.ws.BinaryOwnerConflict", + "sdk.ws.IllegalState", + } + assert { + e.name for e in SDK if e.name.startswith("sdk.ws.") + } == expected + + from dashscope.v2 import errors as v2_errors + + wired = { + "sdk.ws.ProtocolError": "WsEventError", + "sdk.ws.IllegalState": "WsStateError", + "sdk.ws.BinaryOwnerConflict": "WsBinaryOwnerConflictError", + } + for name, cls_name in wired.items(): + cls = getattr(v2_errors, cls_name) + assert cls._SDK_ERROR_NAME == name + assert entries[name].vars == ["detail"] diff --git a/tests/v2/test_errors.py b/tests/v2/test_errors.py index b58767fa..4568e6a8 100644 --- a/tests/v2/test_errors.py +++ b/tests/v2/test_errors.py @@ -36,8 +36,12 @@ PermissionDeniedError, RateLimitError, RequestTooLargeError, + ResponseError, SdkError, ServiceUnavailableError, + WsBinaryOwnerConflictError, + WsEventError, + WsStateError, check_response, error_from_business_code, error_from_sdk_exception, @@ -659,7 +663,7 @@ def test_catalog_classes_are_flat_siblings(): entry.error_code: getattr(errors, entry.error_code) for entry in registry.PUBLIC_ERRORS } - assert len(classes) == 9 + assert len(classes) == 10 for name, cls in classes.items(): assert cls.__bases__ == (APIStatusError,), name for other_name, other in classes.items(): @@ -670,11 +674,59 @@ def test_catalog_classes_are_flat_siblings(): def test_status_map_is_derived_from_the_catalog(): catalogued = {e.status_code for e in registry.PUBLIC_ERRORS} assert set(_STATUS_MAP) == catalogued + # First claimant of a status wins the status-only mapping; later + # siblings on the same status (response_error on 500) are reachable + # only by class, never selected by the status alone. + seen = set() for entry in registry.PUBLIC_ERRORS: + if entry.status_code in seen: + continue + seen.add(entry.status_code) expected = getattr(errors, entry.error_code) assert _STATUS_MAP[entry.status_code] is expected, entry.key +def test_bare_500_still_maps_to_internal_server_error(): + # response_error shares status 500 with internal_error; the gateway + # only ever means internal_error by a bare 500. + assert _STATUS_MAP[500] is InternalServerError + with pytest.raises(InternalServerError) as ei: + check_response(_resp(500, {"message": "boom"})) + assert type(ei.value) is InternalServerError + + +def test_response_error_is_sdk_side_only(): + assert issubclass(ResponseError, APIStatusError) + assert ResponseError.status_code == 500 + assert ResponseError not in _STATUS_MAP.values() + assert ResponseError not in { + cls for cls in _CODE_MAP.values() + } + + +@pytest.mark.parametrize( + "cls,name", + [ + (WsEventError, "sdk.ws.ProtocolError"), + (WsStateError, "sdk.ws.IllegalState"), + (WsBinaryOwnerConflictError, "sdk.ws.BinaryOwnerConflict"), + ], +) +def test_ws_errors_default_from_their_catalog_entries(cls, name): + entry = _SDK_ERRORS[name] + err = cls(detail="some detail") + assert isinstance(err, SdkError) + assert err.message == entry.format_message({"detail": "some detail"}) + assert err.code == entry.name + assert err.solution == entry.solution + assert err.allow_retry is entry.allow_retry + # An explicit message bypasses the template but keeps the wiring. + err = cls("custom", request_id="req-1") + assert err.message == "custom" + assert err.code == entry.name + assert err.request_id == "req-1" + + def test_catalog_keys_and_wire_codes_are_separate_namespaces(): # Why the crosswalk has to exist at all: the catalog's `key` is an # internal canonical name, never a value the gateway sends. @@ -777,9 +829,9 @@ def test_placeholders_survive_the_catalog_fallback(): def test_ambiguous_status_gets_no_catalog_text(): - # 400 / 401 / 429 each carry several entries with different text, so - # a status alone cannot pick one without inventing a reason. - for status in (400, 401, 429): + # 400 / 401 / 429 / 500 each carry several entries with different + # text, so a status alone cannot pick one without inventing a reason. + for status in (400, 401, 429, 500): with pytest.raises(APIStatusError) as ei: check_response(_bare(status)) assert ei.value.message == "", status diff --git a/tests/v2/test_ws_connection.py b/tests/v2/test_ws_connection.py index 5716cb4c..f7235866 100644 --- a/tests/v2/test_ws_connection.py +++ b/tests/v2/test_ws_connection.py @@ -10,7 +10,12 @@ import pytest -from dashscope.v2.errors import APIConnectionError, DashScopeError +from dashscope.v2.errors import ( + APIConnectionError, + DashScopeError, + WsBinaryOwnerConflictError, + WsStateError, +) from dashscope.v2.transport.ws import WsEventError, WsSession from dashscope.v2.transport.ws_connection import WsConnection @@ -161,7 +166,7 @@ def test_binary_downlink_two_active_tasks_requires_claim(): _started(conn, t1) _started(conn, t2) t2.claim_binary_downlink() - with pytest.raises(RuntimeError, match="binary downlink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary downlink"): t1.claim_binary_downlink() conn._ws.inbox.put(b"audio-for-t2") assert t2.recv() == b"audio-for-t2" @@ -183,7 +188,7 @@ def test_binary_downlink_ambiguous_fails_connection(): with pytest.raises(WsEventError, match="claim_binary_downlink"): t2.recv() assert conn.closed - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): conn.open_task() @@ -194,7 +199,7 @@ def test_binary_uplink_single_owner_and_release_on_finish(): _started(conn, t1) _started(conn, t2) t1.send_binary(b"frame-1") - with pytest.raises(RuntimeError, match="binary uplink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary uplink"): t2.send_binary(b"frame-2") assert conn._ws.sent[-1] == b"frame-1" @@ -250,7 +255,7 @@ def test_task_close_detaches_and_late_events_drop(): conn._ws.inbox.put(_event("result-generated", t2.task_id, {"ok": 1})) assert t2.recv_event()["payload"] == {"ok": 1} assert not conn.closed - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): t1.recv() conn.close() @@ -433,7 +438,7 @@ def send(self, payload, opcode=1): assert conn.closed and ws.closed with pytest.raises(APIConnectionError): other.recv() - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): conn.open_task() task.close() other.close() @@ -547,7 +552,7 @@ def test_inactive_task_cannot_reclaim_binary_downlink( else: conn._ws.inbox.put(_event(terminal, task.task_id)) task.recv_event() - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): task.claim_binary_downlink() next_task.claim_binary_downlink() conn._ws.inbox.put(b"next-output") @@ -572,7 +577,7 @@ def test_connection_shutdown_releases_binary_owners( task.recv() assert conn._binary_uplink_owner is None assert conn._binary_downlink_owner is None - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): task.claim_binary_downlink() assert conn._binary_downlink_owner is None @@ -601,9 +606,9 @@ def test_reader_releases_terminal_owners_before_consumer_finishes( assert not first.finished # The consumer must still drain its frames. assert conn._binary_uplink_owner is None assert conn._binary_downlink_owner is None - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): first.claim_binary_downlink() - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): first.send_binary(b"too late") second.send_binary(b"second-input") if claim: @@ -634,7 +639,7 @@ def test_binary_owner_must_be_attached_to_this_connection( foreign._task_id = ( task.task_id ) # Matching ids alone do not imply attachment. - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): getattr(conn, "_claim_binary_" + direction)(foreign) assert getattr(conn, "_binary_" + direction + "_owner") is None task.claim_binary_downlink() @@ -676,7 +681,7 @@ def receive(): receiver.join(timeout=1) assert not receiver.is_alive(), "close did not wake the receiver" expected = ( - RuntimeError if close_target == "task" else APIConnectionError + WsStateError if close_target == "task" else APIConnectionError ) assert len(errors) == 1 and isinstance(errors[0], expected), errors finally: @@ -742,7 +747,7 @@ def test_error_is_persistent_but_preserves_earlier_frames( barrier.recv_event() # Earlier frames have reached the task's queue. if shutdown in ("task", "connection"): (task if shutdown == "task" else conn).close() - expected = RuntimeError if shutdown == "task" else APIConnectionError + expected = WsStateError if shutdown == "task" else APIConnectionError else: conn._ws.inbox.put( OSError("receive failed") if shutdown == "recv" else "{", From 3fe047526c914225e78300aa9071a6a11fb8c446 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 11:43:53 +0800 Subject: [PATCH 08/41] refactor(v2): unify AgentStudio and AgenticRL errors Move both implementations onto native v2 exceptions while preserving legacy imports and AgenticRL numeric error codes. --- dashscope/v2/agentstudio/exceptions.py | 241 +++++------ dashscope/v2/agentstudio/streaming.py | 14 +- dashscope/v2/client.py | 44 +- dashscope/v2/errors.py | 6 +- dashscope/v2/finetune/agentic_rl.py | 51 ++- .../finetune/reinforcement/common/errors.py | 276 ++++++++----- dashscope/v2/resources/_agentstudio.py | 385 ------------------ .../v2/resources/reinforcement_learning.py | 282 ++++++------- tests/v2/test_agentstudio_errors.py | 363 ++++------------- tests/v2/test_agentstudio_shim.py | 49 +-- tests/v2/test_apps.py | 4 +- tests/v2/test_error_registry.py | 35 +- tests/v2/test_finetune_rl_shim.py | 61 ++- tests/v2/test_rl.py | 36 +- 14 files changed, 619 insertions(+), 1228 deletions(-) delete mode 100644 dashscope/v2/resources/_agentstudio.py diff --git a/dashscope/v2/agentstudio/exceptions.py b/dashscope/v2/agentstudio/exceptions.py index 055e3143..6ebb62ab 100644 --- a/dashscope/v2/agentstudio/exceptions.py +++ b/dashscope/v2/agentstudio/exceptions.py @@ -1,6 +1,21 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Exception hierarchy for the AgentStudio SDK. +"""AgentStudio errors: the ``dashscope.v2.errors`` family, no second hierarchy. + +The sub-SDK raises v2 errors natively, so the v2 mount needs no +translation layer. Old class names stay importable as aliases of the +v2 classes they map to; new code should use ``dashscope.v2.errors``. + +Mapping notes: + +- ``InvalidRequestError`` -> ``BadRequestError``, + ``OverloadedError`` -> ``ServiceUnavailableError``. +- ``ConflictError`` (409) has no catalog entry: 409 falls back to the + ``APIStatusError`` base class, and the old name aliases that base. +- ``AgentStudioError`` aliases ``DashScopeError``; the 1.x + ``DashScopeException`` lineage is gone. +- ``StreamError`` / ``StreamClosedError`` are ``SdkError`` subclasses + backed by the catalog's ``sdk.agentstudio.*`` entries. The AgentStudio service returns errors in the canonical CMA shape:: @@ -20,157 +35,83 @@ from typing import Any, Dict, Mapping, Optional -from dashscope._shared._errors import DashScopeException - - -class AgentStudioError(DashScopeException): - """Base exception for all AgentStudio SDK errors. - - Attributes - ---------- - code: str - Machine-readable error code (e.g. ``invalid_request_error``). - message: str - Human-readable error description from the server. - request_id: Optional[str] - Correlation identifier for log lookups (``req_``). - status_code: Optional[int] - HTTP status code if the error originated from a HTTP response. - raw: Optional[Mapping[str, Any]] - Original response payload for debugging. +from dashscope.v2 import _error_registry as _registry +from dashscope.v2 import errors as _v2_errors +from dashscope.v2.errors import ( # noqa: F401 + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + BadRequestError, + DashScopeError, + InternalServerError, + NotFoundError, + PermissionDeniedError, + RateLimitError, + SdkError, + ServiceUnavailableError, + _CatalogSdkError, + _STATUS_MAP, +) + +# Deprecated aliases of the v2 classes (kept for the 1.x import path). +AgentStudioError = DashScopeError +InvalidRequestError = BadRequestError +ConflictError = APIStatusError +OverloadedError = ServiceUnavailableError + + +class StreamError(_CatalogSdkError): + """The SSE stream encountered a fatal protocol error. + + Backed by the catalog's ``sdk.agentstudio.StreamError`` entry: + ``code`` / ``solution`` / ``allow_retry`` default from it. """ - code: str = "agentstudio_error" - - def __init__( - self, - message: str, - *, - code: Optional[str] = None, - request_id: Optional[str] = None, - status_code: Optional[int] = None, - raw: Optional[Mapping[str, Any]] = None, - ) -> None: - super().__init__(message) - if code is not None: - self.code = code - self.message = message - self.request_id = request_id - self.status_code = status_code - self.raw = raw - - def __repr__(self) -> str: # pragma: no cover - debug helper - return ( - f"{type(self).__name__}(code={self.code!r}, " - f"message={self.message!r}, request_id={self.request_id!r}, " - f"status_code={self.status_code!r})" - ) - - -# --------------------------------------------------------------------------- -# Connection / transport layer errors (no HTTP response received) -# --------------------------------------------------------------------------- - - -class APIConnectionError(AgentStudioError): - """Raised when the HTTP request fails before a response is read.""" - - code = "api_connection_error" - - -class APITimeoutError(APIConnectionError): - """Raised on connect / read timeouts.""" - - code = "api_timeout_error" - - -# --------------------------------------------------------------------------- -# Server-side errors (HTTP response received) -# --------------------------------------------------------------------------- - - -class APIStatusError(AgentStudioError): - """Raised when the server returns a non-2xx status.""" - - code = "api_status_error" - - -class InvalidRequestError(APIStatusError): - code = "invalid_request_error" - - -class AuthenticationError(APIStatusError): - code = "authentication_error" - - -class PermissionDeniedError(APIStatusError): - code = "permission_denied_error" - - -class NotFoundError(APIStatusError): - code = "not_found_error" - - -class ConflictError(APIStatusError): - code = "conflict_error" - - -class RateLimitError(APIStatusError): - code = "rate_limit_error" - - -class OverloadedError(APIStatusError): - code = "overloaded_error" - - -class InternalServerError(APIStatusError): - code = "api_error" - - -# --------------------------------------------------------------------------- -# Streaming errors -# --------------------------------------------------------------------------- - - -class StreamError(AgentStudioError): - """Raised when an SSE stream encounters a fatal protocol error.""" - - code = "stream_error" + _SDK_ERROR_NAME = "sdk.agentstudio.StreamError" class StreamClosedError(StreamError): - """Raised when consumers attempt I/O on an already-closed stream.""" - - code = "stream_closed" + """Attempted I/O on an already-closed stream. + Backed by the catalog's ``sdk.agentstudio.StreamClosedError`` entry. + """ -# --------------------------------------------------------------------------- -# Dispatcher -# --------------------------------------------------------------------------- + _SDK_ERROR_NAME = "sdk.agentstudio.StreamClosedError" -_STATUS_TO_DEFAULT: Dict[int, type] = { - 400: InvalidRequestError, - 401: AuthenticationError, - 403: PermissionDeniedError, - 404: NotFoundError, - 409: ConflictError, - 429: RateLimitError, - 500: InternalServerError, - 502: InternalServerError, - 503: OverloadedError, - 504: InternalServerError, -} +def _build_code_to_class() -> Dict[str, type]: + """Wire-code crosswalk derived from the catalog's anthropic codes. -_CODE_TO_CLASS: Dict[str, type] = { - "invalid_request_error": InvalidRequestError, - "authentication_error": AuthenticationError, - "permission_denied_error": PermissionDeniedError, - "not_found_error": NotFoundError, - "conflict_error": ConflictError, - "rate_limit_error": RateLimitError, - "overloaded_error": OverloadedError, - "api_error": InternalServerError, + Two codes need explicit entries: the gateway's actual + ``permission_denied_error`` (the catalog field says + ``permission_error``), and ``conflict_error`` / 409, which has no + catalog entry and therefore lands on the base class. + """ + out: Dict[str, type] = {} + for entry in _registry.PUBLIC_ERRORS: + cls = getattr(_v2_errors, entry.error_code, None) + if cls is not None: + out.setdefault(entry.anthropic_error_code, cls) + out["permission_denied_error"] = PermissionDeniedError + out["conflict_error"] = APIStatusError + return out + + +_CODE_TO_CLASS = _build_code_to_class() + +# Default wire code when the body carries none (legacy behaviour). +_STATUS_DEFAULT_CODE = { + 400: "invalid_request_error", + 401: "authentication_error", + 403: "permission_denied_error", + 404: "not_found_error", + 409: "conflict_error", + 429: "rate_limit_error", + 500: "api_error", + 502: "api_error", + 503: "overloaded_error", + 504: "timeout_error", } @@ -179,14 +120,17 @@ def from_response( status_code: int, body: Any, headers: Optional[Mapping[str, str]] = None, -) -> AgentStudioError: - """Build an :class:`AgentStudioError` instance from a HTTP response. +) -> DashScopeError: + """Build a v2 error from a HTTP response. Accepts both the documented ``{type, error:{code,message}, request_id}`` shape and the pre-release ``{type, error:{error_code, error_message}}`` shape. Falls back to a Spring default ``{timestamp,status,error,path}`` when the body is not JSON-serializable. + Class selection: the catalog-derived code crosswalk first, then the + v2 status map, then the ``APIStatusError`` base class. + The ``x-request-id`` response header is preferred over the body ``request_id`` field (server-generated IDs are more reliable for tracing). """ @@ -225,14 +169,11 @@ def from_response( if message is None: message = f"HTTP {status_code}" if code is None: - code = _STATUS_TO_DEFAULT.get( # type: ignore[attr-defined] - status_code, - APIStatusError, - ).code + code = _STATUS_DEFAULT_CODE.get(status_code, "api_status_error") cls = ( _CODE_TO_CLASS.get(code) - or _STATUS_TO_DEFAULT.get(status_code) + or _STATUS_MAP.get(status_code) or APIStatusError ) return cls( @@ -240,5 +181,5 @@ def from_response( code=code, request_id=request_id, status_code=status_code, - raw=body if isinstance(body, Mapping) else {"raw": body}, + body=body if isinstance(body, Mapping) else {"raw": body}, ) diff --git a/dashscope/v2/agentstudio/streaming.py b/dashscope/v2/agentstudio/streaming.py index b338462d..414f61c9 100644 --- a/dashscope/v2/agentstudio/streaming.py +++ b/dashscope/v2/agentstudio/streaming.py @@ -50,6 +50,16 @@ _DEFAULT_IDLE_TIMEOUT = 120.0 +def _raise_stream_error(exc: Exception) -> None: + if isinstance(exc, exceptions.AgentStudioError): + raise exc + if isinstance(exc, httpx.TimeoutException): + raise exceptions.APITimeoutError(str(exc)) from exc + if isinstance(exc, (httpx.NetworkError, httpx.RemoteProtocolError)): + raise exceptions.APIConnectionError(str(exc)) from exc + raise exc + + # --------------------------------------------------------------------------- # Iterators # --------------------------------------------------------------------------- @@ -128,7 +138,7 @@ def _produce() -> None: if item is None: return if isinstance(item, Exception): - raise item + _raise_stream_error(item) yield item finally: self.close() @@ -211,7 +221,7 @@ async def _produce() -> None: if item is None: return if isinstance(item, Exception): - raise item + _raise_stream_error(item) yield item finally: await self.aclose() diff --git a/dashscope/v2/client.py b/dashscope/v2/client.py index 97dacbcf..2b34c3ec 100644 --- a/dashscope/v2/client.py +++ b/dashscope/v2/client.py @@ -153,33 +153,23 @@ def reinforcement_learning(self) -> "Rl": def agentstudio(self): """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). - The client is wrapped in a transparent proxy (design doc M3 3.12) - that translates AgentStudio errors into the ``dashscope.v2`` - exception family, so ``except dashscope.v2.errors.DashScopeError`` - covers them. The proxy spoofs ``__class__`` and forwards - attributes, so ``isinstance(client.agentstudio, AgentStudioClient)`` - and ``.transport`` access keep working. - - AgentStudio ships its own transport rather than riding this - client's, so its requests keep the unmarked module tag - (``agentstudio``) instead of the v2 ``2`` marker -- the backend - already reports on that tag, and changing it would split the - server-side statistics. + AgentStudio raises ``dashscope.v2.errors`` natively. It ships its + own transport rather than riding this client's, so its requests + keep the unmarked module tag (``agentstudio``) instead of the v2 + ``2`` marker -- the backend already reports on that tag, and + changing it would split the server-side statistics. """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel from dashscope.v2.agentstudio import Client as AgentStudioClient - from dashscope.v2.resources._agentstudio import wrap_agentstudio # agentstudio resolves its own base_url from workspace + # region (the v2 base_url does not propagate: the agentstudio # path differs), so forward both. - self._agentstudio = wrap_agentstudio( - AgentStudioClient( - api_key=self._config.api_key, - workspace=self._config.workspace, - region=self._config.region, - ), + self._agentstudio = AgentStudioClient( + api_key=self._config.api_key, + workspace=self._config.workspace, + region=self._config.region, ) return self._agentstudio @@ -396,23 +386,19 @@ def reinforcement_learning(self) -> "AsyncRl": def agentstudio(self): """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). - See :attr:`Client.agentstudio`: the returned client is wrapped in - a transparent proxy that aligns AgentStudio errors with the - ``dashscope.v2`` exception family (design doc M3 3.12). + See :attr:`Client.agentstudio`; errors use the ``dashscope.v2`` + exception family natively. """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel from dashscope.v2.agentstudio import AsyncClient as AsClient - from dashscope.v2.resources._agentstudio import wrap_agentstudio # See Client.agentstudio: agentstudio builds its own base_url # from workspace + region, so forward both. - self._agentstudio = wrap_agentstudio( - AsClient( - api_key=self._config.api_key, - workspace=self._config.workspace, - region=self._config.region, - ), + self._agentstudio = AsClient( + api_key=self._config.api_key, + workspace=self._config.workspace, + region=self._config.region, ) return self._agentstudio diff --git a/dashscope/v2/errors.py b/dashscope/v2/errors.py index 77c83fac..f9b8a710 100644 --- a/dashscope/v2/errors.py +++ b/dashscope/v2/errors.py @@ -23,9 +23,9 @@ ``sdk.ConnectionError`` / ``sdk.Timeout`` entries, the websocket transport errors (:class:`WsEventError` / :class:`WsStateError` / :class:`WsBinaryOwnerConflictError`) are backed by the ``sdk.ws.*`` -entries, and the mounts over sub-SDKs outside this error family (1.x -agentic_rl, the 2.x AgentStudio sub-SDK) translate with -:func:`error_from_sdk_exception`. +entries. AgentStudio and AgenticRL also raise catalog-backed v2 errors +natively; :func:`error_from_sdk_exception` remains available for mounts +over external SDK families. """ from __future__ import annotations diff --git a/dashscope/v2/finetune/agentic_rl.py b/dashscope/v2/finetune/agentic_rl.py index 8fa398cd..003ab06e 100644 --- a/dashscope/v2/finetune/agentic_rl.py +++ b/dashscope/v2/finetune/agentic_rl.py @@ -66,6 +66,10 @@ __all__ = ["AgenticRL", "SubmitOutput", "SubmitResult"] +def _inner_code(exc: Exception) -> str: + return getattr(exc, "code", None) or type(exc).__name__ + + class SubmitOutput: """The ``output`` payload nested in a submit response. @@ -130,8 +134,8 @@ def __init__(self, api_key: str = None): try: set_api_key(api_key) except Exception as e: - raise ValueErrorWithCode( - "Invalid API key configuration", + raise ValueErrorWithCode.from_catalog( + "sdk.InvalidApiKey", error_code=3001, ) from e @@ -186,9 +190,10 @@ async def register_functions( except Exception as e: if hasattr(e, "error_code"): raise - raise RegistrationError( - "Function registration failed", + raise RegistrationError.from_catalog( + "sdk.agentic_rl.FunctionRegistrationFailed", error_code=3002, + variables={"inner_code": _inner_code(e)}, ) from e return ( @@ -219,9 +224,10 @@ async def upload_datasets( ) logger.info("Datasets uploaded") except Exception as e: - raise DatasetsError( - "Datasets upload failed", + raise DatasetsError.from_catalog( + "sdk.agentic_rl.DatasetsUploadFailed", error_code=3003, + variables={"inner_code": _inner_code(e)}, ) from e return uploaded_training_ids, uploaded_validation_ids @@ -282,10 +288,17 @@ def _build_submit_request( ) # names of functions if not self.tuning.check_function_names(): - raise ValueErrorWithCode( - "Duplicate function names detected. All function names must " - "be unique.", + names = [ + getattr(fc, "name", "") + for fc in self.tuning.functions + ] + duplicates = sorted( + {name for name in names if names.count(name) > 1}, + ) + raise ValueErrorWithCode.from_catalog( + "sdk.agentic_rl.DuplicateFunctionNames", error_code=3004, + variables={"names": ", ".join(duplicates or names)}, ) # datasets @@ -387,9 +400,10 @@ def _submit_envelope( except Exception as e: if hasattr(e, "error_code"): raise - raise RuntimeErrorWithCode( - "Job submission failed", + raise RuntimeErrorWithCode.from_catalog( + "sdk.agentic_rl.JobSubmissionFailed", error_code=3005, + variables={"inner_code": _inner_code(e)}, ) from e def submit_job( @@ -492,9 +506,10 @@ async def run( except Exception as e: if hasattr(e, "error_code"): raise - raise RuntimeErrorWithCode( - "RL tuning workflow failed", + raise RuntimeErrorWithCode.from_catalog( + "sdk.agentic_rl.WorkflowFailed", error_code=3006, + variables={"inner_code": _inner_code(e)}, ) from e @classmethod @@ -536,9 +551,10 @@ async def test_functions( elif functype == FunctionType.GROUP_REWARD: value = GroupRewardInput.model_validate(input_data) else: - raise ValueErrorWithCode( - f"Unsupported function type: {functype}", + raise ValueErrorWithCode.from_catalog( + "sdk.agentic_rl.UnsupportedFunctionType", error_code=3007, + variables={"functype": str(functype)}, ) logger.info( @@ -565,9 +581,10 @@ async def test_functions( return result except Exception as e: - raise ValidationError( - "Function test failed", + raise ValidationError.from_catalog( + "sdk.agentic_rl.FunctionTestFailed", error_code=3008, + variables={"inner_code": _inner_code(e)}, ) from e @classmethod diff --git a/dashscope/v2/finetune/reinforcement/common/errors.py b/dashscope/v2/finetune/reinforcement/common/errors.py index 0d9f31da..a80a2d0a 100644 --- a/dashscope/v2/finetune/reinforcement/common/errors.py +++ b/dashscope/v2/finetune/reinforcement/common/errors.py @@ -1,19 +1,38 @@ # -*- coding: utf-8 -*- -""" -Custom exception hierarchy for the AgenticRL system -""" +"""Native v2 errors for AgenticRL, with legacy numeric-code compatibility.""" from datetime import datetime -from typing import Optional, Dict +from typing import Any, Dict, Optional + +from dashscope.v2.errors import _CatalogSdkError, sdk_error + +_LEGACY_ERROR_CODES = { + "sdk.agentic_rl.Error": 1000, + "sdk.agentic_rl.IOErrorWithCode": 1800, + "sdk.agentic_rl.RuntimeErrorWithCode": 0, + "sdk.agentic_rl.ValueErrorWithCode": 0, + "sdk.agentic_rl.InputError": 1100, + "sdk.agentic_rl.OutputError": 1200, + "sdk.agentic_rl.BaseConnectionError": 1300, + "sdk.agentic_rl.OSSConnectionError": 1310, + "sdk.agentic_rl.OSSUploadError": 1320, + "sdk.agentic_rl.DeploymentError": 1400, + "sdk.agentic_rl.RegistrationError": 1410, + "sdk.agentic_rl.DatasetsError": 1460, + "sdk.agentic_rl.FunctionLoadError": 1420, + "sdk.agentic_rl.FunctionLayerError": 1450, + "sdk.agentic_rl.InstanceWarmupError": 1430, + "sdk.agentic_rl.InstanceQueryError": 1440, + "sdk.agentic_rl.ValidationError": 1500, + "sdk.agentic_rl.ConfigurationError": 1510, + "sdk.agentic_rl.BasePermissionError": 1700, +} class _RootCauseMixin: - """Mixin that provides root-cause traversal and formatting for exceptions - that carry an error code.""" - @property - def root_cause(self) -> "Exception": - root: "Exception" = self # type: ignore[assignment] + def root_cause(self) -> Exception: + root: Exception = self # type: ignore[assignment] seen = {id(root)} while root.__cause__ and id(root.__cause__) not in seen: root = root.__cause__ @@ -21,7 +40,6 @@ def root_cause(self) -> "Exception": return root def _format_cause(self) -> str: - """Format root cause information if available.""" if self.__cause__ is not None: root = self.root_cause if self is not root: @@ -32,282 +50,326 @@ def _format_cause(self) -> str: return "" -class AgenticRLError(_RootCauseMixin, Exception): - """Base class for all Agentic RL exceptions.""" +class AgenticRLError(_RootCauseMixin, _CatalogSdkError): + """Base class for AgenticRL failures in the v2 error family.""" + + _SDK_ERROR_NAME = "sdk.agentic_rl.Error" - def __init__(self, message: str, error_code: int = 1000): - super().__init__(message) - self.error_code = error_code + def __init__( + self, + message: str, + error_code: Optional[int] = None, + **kwargs: Any, + ) -> None: + self._legacy_error_code = error_code self.timestamp = datetime.now().isoformat() - self.message = message + super().__init__(message, **kwargs) + + @classmethod + def from_catalog( + cls, + name: str, + *, + error_code: int, + variables: Optional[Dict[str, str]] = None, + ) -> "AgenticRLError": + error = sdk_error(name, variables) + return cls( + error.message, + error_code=error_code, + code=error.code, + solution=error.solution, + allow_retry=error.allow_retry, + ) - def __str__(self): - base = f"[{self.error_code}] {self.message} (at {self.timestamp})" - return f"{base}{self._format_cause()}" + @property + def error_code(self) -> int: + if self._legacy_error_code is not None: + return self._legacy_error_code + return _LEGACY_ERROR_CODES.get(self.code or "", 1000) + + def __str__(self) -> str: + return f"{super().__str__()}{self._format_cause()}" class IOErrorWithCode(AgenticRLError): - """Raised for general I/O operation failures""" + _SDK_ERROR_NAME = "sdk.agentic_rl.IOErrorWithCode" def __init__( self, message: str, - error_code: int = 1800, + error_code: Optional[int] = None, path: Optional[str] = None, operation: Optional[str] = None, - ): - super().__init__(f"I/O error: {message}", error_code) + **kwargs: Any, + ) -> None: + super().__init__(f"I/O error: {message}", error_code, **kwargs) self.path = path self.operation = operation -class RuntimeErrorWithCode(_RootCauseMixin, RuntimeError): - """Enhanced RuntimeError that supports error codes for better error - categorization.""" - - def __init__(self, message: str, error_code: int = 0): - super().__init__(message) - self.error_code = error_code - self.message = message - - def __str__(self): - return f"[{self.error_code}] {self.message}{self._format_cause()}" +class RuntimeErrorWithCode(AgenticRLError): + _SDK_ERROR_NAME = "sdk.agentic_rl.RuntimeErrorWithCode" -class ValueErrorWithCode(_RootCauseMixin, ValueError): - """Enhanced ValueError that supports error codes for better error - categorization.""" - - def __init__(self, message: str, error_code: int = 0): - super().__init__(message) - self.error_code = error_code - self.message = message - - def __str__(self): - return f"[{self.error_code}] {self.message}{self._format_cause()}" +class ValueErrorWithCode(AgenticRLError): + _SDK_ERROR_NAME = "sdk.agentic_rl.ValueErrorWithCode" class InputError(AgenticRLError): - """Raised when invalid input data is detected during validation""" + _SDK_ERROR_NAME = "sdk.agentic_rl.InputError" def __init__( self, message: str, - error_code: int = 1100, + error_code: Optional[int] = None, field: Optional[str] = None, - ): - super().__init__(message, error_code) + **kwargs: Any, + ) -> None: + super().__init__(message, error_code, **kwargs) self.field = field class OutputError(AgenticRLError): - """Raised when service response fails output validation checks""" + _SDK_ERROR_NAME = "sdk.agentic_rl.OutputError" def __init__( self, message: str, - error_code: int = 1200, + error_code: Optional[int] = None, response: Optional[Dict] = None, - ): - super().__init__(message, error_code) + **kwargs: Any, + ) -> None: + super().__init__(message, error_code, **kwargs) self.response = response class BaseConnectionError(AgenticRLError): - """Base class for connection-related errors""" + _SDK_ERROR_NAME = "sdk.agentic_rl.BaseConnectionError" def __init__( self, message: str, - error_code: int = 1300, + error_code: Optional[int] = None, endpoint: Optional[str] = None, - ): - super().__init__(message, error_code) + **kwargs: Any, + ) -> None: + super().__init__(message, error_code, **kwargs) self.endpoint = endpoint class OSSConnectionError(BaseConnectionError): - """Raised when connecting to OSS storage service fails""" + _SDK_ERROR_NAME = "sdk.agentic_rl.OSSConnectionError" def __init__( self, message: str, - error_code: int = 1310, - endpoint: str = None, - ): + error_code: Optional[int] = None, + endpoint: Optional[str] = None, + **kwargs: Any, + ) -> None: super().__init__( f"OSS connection failed: {message}", error_code, endpoint, + **kwargs, ) class OSSUploadError(BaseConnectionError): - """Raised when file upload operation to OSS fails""" + _SDK_ERROR_NAME = "sdk.agentic_rl.OSSUploadError" def __init__( self, message: str, - error_code: int = 1320, - endpoint: str = None, + error_code: Optional[int] = None, + endpoint: Optional[str] = None, bucket: Optional[str] = None, object_key: Optional[str] = None, file_size: Optional[int] = None, - ): - super().__init__(f"OSS upload failed: {message}", error_code, endpoint) + **kwargs: Any, + ) -> None: + super().__init__( + f"OSS upload failed: {message}", + error_code, + endpoint, + **kwargs, + ) self.bucket = bucket self.object_key = object_key self.file_size = file_size class DeploymentError(AgenticRLError): - """Base class for deployment-related errors""" + _SDK_ERROR_NAME = "sdk.agentic_rl.DeploymentError" def __init__( self, message: str, - error_code: int = 1400, + error_code: Optional[int] = None, resource_id: Optional[str] = None, - ): - super().__init__(message, error_code) + **kwargs: Any, + ) -> None: + super().__init__(message, error_code, **kwargs) self.resource_id = resource_id class RegistrationError(DeploymentError): - """Raised when function registration fails in the deployment system""" + _SDK_ERROR_NAME = "sdk.agentic_rl.RegistrationError" def __init__( self, message: str, - error_code: int = 1410, + error_code: Optional[int] = None, resource_id: Optional[str] = None, - ): + **kwargs: Any, + ) -> None: super().__init__( f"Registration failed: {message}", error_code, resource_id, + **kwargs, ) class DatasetsError(DeploymentError): - """Raised when update datasets fails in the deployment system""" + _SDK_ERROR_NAME = "sdk.agentic_rl.DatasetsError" - def __init__(self, message: str, error_code: int = 1460): - super().__init__(f"Datasets failed: {message}", error_code) + def __init__( + self, + message: str, + error_code: Optional[int] = None, + **kwargs: Any, + ) -> None: + super().__init__(f"Datasets failed: {message}", error_code, **kwargs) class FunctionLoadError(DeploymentError): - """Raised when loading a registered function into runtime fails""" + _SDK_ERROR_NAME = "sdk.agentic_rl.FunctionLoadError" def __init__( self, message: str, - error_code: int = 1420, - entity_id: str = None, + error_code: Optional[int] = None, + entity_id: Optional[str] = None, error_log: Optional[str] = None, - ): + **kwargs: Any, + ) -> None: super().__init__( f"Function load failed: {message}", error_code, entity_id, + **kwargs, ) self.entity_id = entity_id self.error_log = error_log class FunctionLayerError(DeploymentError): - """Raised when creating a layer of function fails""" + _SDK_ERROR_NAME = "sdk.agentic_rl.FunctionLayerError" def __init__( self, message: str, - error_code: int = 1450, - layer_name: str = None, + error_code: Optional[int] = None, + layer_name: Optional[str] = None, error_log: Optional[str] = None, - ): + **kwargs: Any, + ) -> None: super().__init__( f"Function layer create failed: {message}", error_code, layer_name, + **kwargs, ) self.layer_name = layer_name self.error_log = error_log class InstanceWarmupError(DeploymentError): - """Raised when function instance health check fails after deployment""" + _SDK_ERROR_NAME = "sdk.agentic_rl.InstanceWarmupError" def __init__( self, message: str, - error_code: int = 1430, - instance_url: str = None, + error_code: Optional[int] = None, + instance_url: Optional[str] = None, timeout: float = 0.0, retry_after: Optional[float] = None, - ): - super().__init__(f"Instance warmup failed: {message}", error_code) + **kwargs: Any, + ) -> None: + super().__init__( + f"Instance warmup failed: {message}", + error_code, + **kwargs, + ) self.instance_url = instance_url self.timeout = timeout self.retry_after = retry_after class InstanceQueryError(DeploymentError): - """Raised when querying function instance status fails""" + _SDK_ERROR_NAME = "sdk.agentic_rl.InstanceQueryError" def __init__( self, message: str, - error_code: int = 1440, - instance_id: str = None, + error_code: Optional[int] = None, + instance_id: Optional[str] = None, query_attempts: int = 1, - ): + **kwargs: Any, + ) -> None: super().__init__( f"Instance query failed: {message}", error_code, instance_id, + **kwargs, ) self.instance_id = instance_id self.query_attempts = query_attempts class ValidationError(AgenticRLError): - """Base class for data validation failures""" + _SDK_ERROR_NAME = "sdk.agentic_rl.ValidationError" def __init__( self, message: str, - error_code: int = 1500, + error_code: Optional[int] = None, invalid_data: Optional[Dict] = None, validation_rules: Optional[Dict] = None, - ): - super().__init__(f"Validation failed: {message}", error_code) + **kwargs: Any, + ) -> None: + super().__init__(f"Validation failed: {message}", error_code, **kwargs) self.invalid_data = invalid_data self.validation_rules = validation_rules class ConfigurationError(ValidationError): - """Raised when invalid system configuration is detected""" + _SDK_ERROR_NAME = "sdk.agentic_rl.ConfigurationError" def __init__( self, message: str, - error_code: int = 1510, + error_code: Optional[int] = None, config_path: Optional[str] = None, - ): - super().__init__(message, error_code=error_code) + **kwargs: Any, + ) -> None: + super().__init__(message, error_code=error_code, **kwargs) self.config_path = config_path class BasePermissionError(AgenticRLError): - """Raised when an operation lacks required permissions""" + _SDK_ERROR_NAME = "sdk.agentic_rl.BasePermissionError" def __init__( self, message: str, - error_code: int = 1700, - operation: str = None, - resource: str = None, - ): - super().__init__(f"Permission denied: {message}", error_code) + error_code: Optional[int] = None, + operation: Optional[str] = None, + resource: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(f"Permission denied: {message}", error_code, **kwargs) self.operation = operation self.resource = resource diff --git a/dashscope/v2/resources/_agentstudio.py b/dashscope/v2/resources/_agentstudio.py deleted file mode 100644 index fc963ae0..00000000 --- a/dashscope/v2/resources/_agentstudio.py +++ /dev/null @@ -1,385 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Transparent v2 error-alignment proxy for the mounted AgentStudio sub-SDK. - -Design doc M3 task 3.12. ``client.agentstudio`` lazily mounts the -AgentStudio sub-SDK, which keeps its own exception family rooted at -``dashscope._shared._errors.DashScopeException``. That family shares no -lineage with ``dashscope.v2.errors.DashScopeError``, so a user writing -``except dashscope.v2.errors.DashScopeError`` would miss every failure -the mounted sub-SDK raises. - -This module closes that gap at the *mount boundary only*: it wraps the -constructed AgentStudio client in a transparent proxy that catches -``AgentStudioError`` and re-raises the equivalent v2 error. The -standalone ``dashscope.agentstudio.Client`` keeps its native error -contract -- only the v2 mount is translated. - -Two-branch translation (design doc §7): - -* HTTP-layer errors (``APIStatusError`` and its subclasses) carry a real - ``status_code``; they belong to the status map, not ``SDK_ERRORS``, so - they go through :func:`~dashscope.v2.errors.error_from_status`. -* Local / stream errors that have no HTTP response behind them - (``APIConnectionError`` / ``APITimeoutError`` / ``StreamError`` / - ``StreamClosedError``) go through - :func:`~dashscope.v2.errors.error_from_sdk_exception` with family - ``sdk.agentstudio``. The registry carries exactly these four and, by - design, no ``sdk.agentstudio.Error`` family fallback, so any other - non-HTTP class degrades to a v2 ``APIConnectionError`` rather than let - the two-tier lookup raise ``KeyError`` and mask the original failure. - -The proxy is transparent (``__class__`` spoofing plus attribute -forwarding) so ``isinstance(client.agentstudio, AgentStudioClient)``, -``.transport`` access and the context-manager protocol keep working. -""" - -from __future__ import annotations - -import asyncio -import functools -from collections.abc import AsyncIterator, Iterator -from typing import Any, Dict, Optional - -import httpx - -from dashscope.v2 import errors as _v2_errors -from dashscope.v2.agentstudio import exceptions as _as_exceptions -from dashscope.v2.agentstudio.pagination import AsyncCursorPage, CursorPage - -_AGENTSTUDIO_FAMILY = "sdk.agentstudio" - -# The only AgentStudio classes registered under ``sdk.agentstudio.*`` in -# the error catalog. Everything else non-HTTP has no entry and no family -# fallback, so it must not reach ``error_from_sdk_exception``. -_SDK_ERROR_CLASSES = frozenset( - { - "APIConnectionError", - "APITimeoutError", - "StreamError", - "StreamClosedError", - }, -) - -# Resource classes (and the typed event streams) live under this package; -# attribute access recurses into them so nested method calls stay guarded. -_RESOURCE_PACKAGE = "dashscope.v2.agentstudio.resources" - -# AgentStudio raises a few HTTP-family errors locally, without a response, -# so they carry no ``status_code`` (the missing-``api_key`` guard in -# ``transport.build_headers`` is the live example). Recover the status the -# class stands for so ``error_from_status`` still picks the right v2 class -# instead of degrading to a 500. Values mirror agentstudio's own -# ``_STATUS_TO_DEFAULT``; only the class's canonical status is kept. -_STATUS_BY_CLASS: Dict[type, int] = { - _as_exceptions.InvalidRequestError: 400, - _as_exceptions.AuthenticationError: 401, - _as_exceptions.PermissionDeniedError: 403, - _as_exceptions.NotFoundError: 404, - _as_exceptions.ConflictError: 409, - _as_exceptions.RateLimitError: 429, - _as_exceptions.OverloadedError: 503, - _as_exceptions.InternalServerError: 500, -} - - -def _effective_status(exc: _as_exceptions.AgentStudioError) -> int: - """The HTTP status to map on, recovering it from the class if absent.""" - - status = getattr(exc, "status_code", None) - if status is not None: - return status - for cls in type(exc).__mro__: - if cls in _STATUS_BY_CLASS: - return _STATUS_BY_CLASS[cls] - return 500 - - -def translate_agentstudio_error( - exc: _as_exceptions.AgentStudioError, -) -> _v2_errors.DashScopeError: - """Map an AgentStudio exception into the v2 error family.""" - - status = getattr(exc, "status_code", None) - if isinstance(exc, _as_exceptions.APIStatusError) or status is not None: - # AgentStudio's own exception already resolved the compatibility - # fields and the header precedence. - request_id = getattr(exc, "request_id", None) - headers = {"x-request-id": request_id} if request_id else None - error = _v2_errors.error_from_status( - _effective_status(exc), - message=getattr(exc, "message", None) or str(exc), - headers=headers, - ) - error.code = exc.code - error.body = exc.raw - return error - - if type(exc).__name__ in _SDK_ERROR_CLASSES: - return _v2_errors.error_from_sdk_exception( - exc, - family=_AGENTSTUDIO_FAMILY, - ) - - # Unregistered non-HTTP class (in practice only the bare base class): - # surface it as a connection error instead of a masking KeyError. - return _v2_errors.APIConnectionError( - getattr(exc, "message", None) or str(exc), - request_id=getattr(exc, "request_id", None), - ) - - -def _is_resource(obj: Any) -> bool: - return type(obj).__module__.startswith(_RESOURCE_PACKAGE) - - -def _is_sync_stream(obj: Any) -> bool: - # CursorPage is iterable but not a context manager, so requiring both - # keeps pagination results out of the stream proxy. - return hasattr(obj, "__enter__") and hasattr(obj, "__iter__") - - -def _is_async_stream(obj: Any) -> bool: - return hasattr(obj, "__aenter__") and hasattr(obj, "__aiter__") - - -def _wrap_result(result: Any) -> Any: - """Guard lazy stream and pagination calls without changing model data.""" - - if isinstance(result, AsyncCursorPage): - return _AsyncPageProxy(result) - if isinstance(result, CursorPage): - return _SyncPageProxy(result) - if _is_async_stream(result): - return _AsyncStreamProxy(result) - if _is_sync_stream(result): - return _SyncStreamProxy(result) - return result - - -_STREAM_ERRORS = ( - _as_exceptions.AgentStudioError, - httpx.TimeoutException, - httpx.NetworkError, - httpx.RemoteProtocolError, -) - - -def _translate_stream_error(exc: Exception) -> _v2_errors.DashScopeError: - if isinstance(exc, _as_exceptions.AgentStudioError): - return translate_agentstudio_error(exc) - cls = ( - _as_exceptions.APITimeoutError - if isinstance(exc, httpx.TimeoutException) - else _as_exceptions.APIConnectionError - ) - return translate_agentstudio_error(cls(str(exc))) - - -def _iterate_sync(target: Any) -> Any: - try: - yield from target - except _STREAM_ERRORS as exc: - raise _translate_stream_error(exc) from exc - - -async def _iterate_async(target: Any) -> Any: - iterator = None - try: - iterator = target.__aiter__() - async for item in iterator: - yield item - except _STREAM_ERRORS as exc: - raise _translate_stream_error(exc) from exc - finally: - if hasattr(iterator, "aclose"): - await iterator.aclose() - - -def _guard_sync(func: Any) -> Any: - @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - try: - result = func(*args, **kwargs) - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - return _wrap_result(result) - - return wrapper - - -def _guard_async(func: Any) -> Any: - @functools.wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> Any: - try: - result = await func(*args, **kwargs) - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - return _wrap_result(result) - - return wrapper - - -class _SyncStreamProxy: - """Guards a sync event stream whose errors surface on enter/iterate.""" - - def __init__(self, target: Any) -> None: - object.__setattr__(self, "_target", target) - - @property # type: ignore[misc] - def __class__(self) -> type: # noqa: D105 - transparency helper - return type(object.__getattribute__(self, "_target")) - - def __getattr__(self, name: str) -> Any: - value = getattr(object.__getattribute__(self, "_target"), name) - if isinstance(value, Iterator): - return _iterate_sync(value) - if callable(value): - return _guard_sync(value) - return value - - def __setattr__(self, name: str, value: Any) -> None: - setattr(object.__getattribute__(self, "_target"), name, value) - - def __enter__(self) -> "_SyncStreamProxy": - target = object.__getattribute__(self, "_target") - try: - target.__enter__() - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - return self - - def __exit__(self, *exc_info: Any) -> Optional[bool]: - target = object.__getattribute__(self, "_target") - return target.__exit__(*exc_info) - - def __iter__(self) -> Any: - return _iterate_sync(object.__getattribute__(self, "_target")) - - -class _AsyncStreamProxy: - """Guards an async event stream whose errors surface on enter/iterate.""" - - def __init__(self, target: Any) -> None: - object.__setattr__(self, "_target", target) - - @property # type: ignore[misc] - def __class__(self) -> type: # noqa: D105 - transparency helper - return type(object.__getattribute__(self, "_target")) - - def __getattr__(self, name: str) -> Any: - value = getattr(object.__getattribute__(self, "_target"), name) - if isinstance(value, AsyncIterator): - return _iterate_async(value) - if callable(value): - return ( - _guard_async(value) - if asyncio.iscoroutinefunction(value) - else _guard_sync(value) - ) - return value - - def __setattr__(self, name: str, value: Any) -> None: - setattr(object.__getattribute__(self, "_target"), name, value) - - async def __aenter__(self) -> "_AsyncStreamProxy": - target = object.__getattribute__(self, "_target") - try: - await target.__aenter__() - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - return self - - async def __aexit__(self, *exc_info: Any) -> Optional[bool]: - target = object.__getattribute__(self, "_target") - return await target.__aexit__(*exc_info) - - def __aiter__(self) -> Any: - return _iterate_async(object.__getattribute__(self, "_target")) - - -class AgentStudioProxy: - """Transparent recursive proxy over an AgentStudio client or resource. - - Attribute access returns a guarded callable for methods, a nested - proxy for sub-resources, and the raw value for everything else (so - ``.transport`` and returned model objects pass through untouched). - """ - - def __init__(self, target: Any) -> None: - object.__setattr__(self, "_target", target) - object.__setattr__(self, "_cache", {}) - - @property # type: ignore[misc] - def __class__(self) -> type: # noqa: D105 - transparency helper - return type(object.__getattribute__(self, "_target")) - - def __getattr__(self, name: str) -> Any: - target = object.__getattribute__(self, "_target") - cache = object.__getattribute__(self, "_cache") - value = getattr(target, name) - if _is_resource(value) and not callable(value): - cached = cache.get(name) - if cached is None or cached[0] is not value: - cached = cache[name] = (value, AgentStudioProxy(value)) - return cached[1] - cache.pop(name, None) - if callable(value): - return ( - _guard_async(value) - if asyncio.iscoroutinefunction(value) - else _guard_sync(value) - ) - return value - - def __setattr__(self, name: str, value: Any) -> None: - setattr(object.__getattribute__(self, "_target"), name, value) - object.__getattribute__(self, "_cache").pop(name, None) - - def __repr__(self) -> str: # pragma: no cover - debug helper - return repr(object.__getattribute__(self, "_target")) - - def __enter__(self) -> "AgentStudioProxy": - object.__getattribute__(self, "_target").__enter__() - return self - - def __exit__(self, *exc_info: Any) -> Optional[bool]: - target = object.__getattribute__(self, "_target") - return target.__exit__(*exc_info) - - async def __aenter__(self) -> "AgentStudioProxy": - await object.__getattribute__(self, "_target").__aenter__() - return self - - async def __aexit__(self, *exc_info: Any) -> Optional[bool]: - target = object.__getattribute__(self, "_target") - return await target.__aexit__(*exc_info) - - -class _SyncPageProxy(AgentStudioProxy): - def __len__(self) -> int: - return len(object.__getattribute__(self, "_target")) - - def __iter__(self) -> Any: - try: - yield from object.__getattribute__(self, "_target") - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - - -class _AsyncPageProxy(AgentStudioProxy): - def __len__(self) -> int: - return len(object.__getattribute__(self, "_target")) - - async def __aiter__(self) -> Any: - try: - async for item in object.__getattribute__(self, "_target"): - yield item - except _as_exceptions.AgentStudioError as exc: - raise translate_agentstudio_error(exc) from exc - - -def wrap_agentstudio(client: Any) -> AgentStudioProxy: - """Wrap a constructed AgentStudio client in the v2 translation proxy.""" - - return AgentStudioProxy(client) diff --git a/dashscope/v2/resources/reinforcement_learning.py b/dashscope/v2/resources/reinforcement_learning.py index 10b868af..462809bc 100644 --- a/dashscope/v2/resources/reinforcement_learning.py +++ b/dashscope/v2/resources/reinforcement_learning.py @@ -3,11 +3,9 @@ """client.rl thin mount layer (design doc M3 3.7). The agentic-RL workflow lives in the 2.0 package -(``dashscope.v2.finetune``); this layer only mounts it on the 2.0 Client -and routes both halves through the v2 error model -- job management -through the v2 transport, and the workflow steps by translating the RL -exception family into ``SdkError`` (catalog ``code`` plus ``solution``). -One ``except DashScopeError`` therefore covers both. +(``dashscope.v2.finetune``); this layer only mounts it on the 2.0 Client. +Job management and workflow steps both raise ``dashscope.v2.errors`` +natively, so one ``except DashScopeError`` covers both. Two halves, matching the rest of the SDK: ``rl.jobs`` is the training job lifecycle (``create`` submits and never blocks, ``wait`` blocks, @@ -34,7 +32,6 @@ from __future__ import annotations import asyncio -import contextlib import time from typing import Any, Awaitable, Callable, Dict, List, Optional, Union @@ -44,7 +41,6 @@ DashScopeError, check_response, error_from_business_code, - error_from_sdk_exception, ) from dashscope.v2.resources._jobs import ( CANCELED, @@ -81,9 +77,6 @@ # workflow implementation. from dashscope.v2.finetune import agentic_rl as _agentic_rl # noqa: E402 from dashscope.v2.finetune import reinforcement as _reinforcement # noqa: E402 -from dashscope.v2.finetune.reinforcement.common import ( # noqa: E402 - errors as _rl_errors, -) class RegisteredFunctions(BaseModel): @@ -116,35 +109,6 @@ class UploadedData(BaseModel): validation_file_ids: List[str] = [] -_RL_FAMILY = "sdk.agentic_rl" - -# These three bases cover all 19 exception classes in common/errors.py: -# AgenticRLError roots 17, and the two *WithCode classes derive from -# RuntimeError / ValueError instead. Nothing wider is needed, since -# common/utils.py's request helper already funnels transport, timeout -# and unexpected failures into RuntimeErrorWithCode; catching more would -# relabel bugs in this module as SDK errors. pydantic's ValidationError, -# which is also a ValueError, stays outside on purpose. -_RL_EXCEPTIONS = ( - _rl_errors.AgenticRLError, - _rl_errors.RuntimeErrorWithCode, - _rl_errors.ValueErrorWithCode, -) - - -@contextlib.contextmanager -def _translate_rl_errors(): - """Keep agentic-RL workflow failures inside the v2 exception family. - - A plain sync context manager, so it wraps ``asyncio.run(...)`` and - ``await ...`` bodies alike. - """ - try: - yield - except _RL_EXCEPTIONS as exc: - raise error_from_sdk_exception(exc, family=_RL_FAMILY) from exc - - def _resolve_func_type(func_type: Union[str, Any]) -> Any: if isinstance(func_type, str): return _reinforcement.FunctionType[func_type.upper()] @@ -273,12 +237,11 @@ def create( ``kwargs`` override individual keys of the training config loaded from ``config_path``, which is open-ended by design. """ - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - if job_name is not None: - kwargs["job_name"] = job_name - rl.init(config_path=config_path, **kwargs) - result = asyncio.run(rl.run()) + rl = _new_agentic_rl(self._client, api_key=api_key) + if job_name is not None: + kwargs["job_name"] = job_name + rl.init(config_path=config_path, **kwargs) + result = asyncio.run(rl.run()) return _to_job(result) def fetch( @@ -451,18 +414,17 @@ def register_functions( lazy_load: bool = True, api_key: Optional[str] = None, ) -> RegisteredFunctions: - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - return asyncio.run( - _register_functions_workflow( - rl, - rollout_classpaths, - reward_classpaths, - group_reward_classpaths, - workspace_dir, - lazy_load, - ), - ) + rl = _new_agentic_rl(self._client, api_key=api_key) + return asyncio.run( + _register_functions_workflow( + rl, + rollout_classpaths, + reward_classpaths, + group_reward_classpaths, + workspace_dir, + lazy_load, + ), + ) def test_functions( self, @@ -482,19 +444,18 @@ def test_functions( parameters forward to the log-pulling step that runs after verification. """ - with _translate_rl_errors(): - return asyncio.run( - _agentic_rl.AgenticRL.test_functions( - instance_id=instance_id, - functype=_resolve_func_type(func_type), - input_data=input_data, - api_key=api_key or self._client.config.api_key or "", - pull_logs=pull_logs, - log_page_size=log_page_size, - start_time=start_time, - end_time=end_time, - ), - ) + return asyncio.run( + _agentic_rl.AgenticRL.test_functions( + instance_id=instance_id, + functype=_resolve_func_type(func_type), + input_data=input_data, + api_key=api_key or self._client.config.api_key or "", + pull_logs=pull_logs, + log_page_size=log_page_size, + start_time=start_time, + end_time=end_time, + ), + ) def upload_data( self, @@ -503,14 +464,13 @@ def upload_data( *, api_key: Optional[str] = None, ) -> UploadedData: - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - train_ids, val_ids = asyncio.run( - rl.upload_datasets( - training_files=training_files, - validation_files=validation_files, - ), - ) + rl = _new_agentic_rl(self._client, api_key=api_key) + train_ids, val_ids = asyncio.run( + rl.upload_datasets( + training_files=training_files, + validation_files=validation_files, + ), + ) return UploadedData( training_file_ids=train_ids, validation_file_ids=val_ids or [], @@ -532,18 +492,17 @@ def query_function_instance_logs( Wraps ``asyncio.run``, so it must not be called from a running event loop — use :class:`AsyncRl` there. """ - with _translate_rl_errors(): - return asyncio.run( - _agentic_rl.AgenticRL.query_function_instance_logs( - function_instance_id=function_instance_id, - page_number=page_number, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - api_key=api_key or self._client.config.api_key or "", - ), - ) + return asyncio.run( + _agentic_rl.AgenticRL.query_function_instance_logs( + function_instance_id=function_instance_id, + page_number=page_number, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + api_key=api_key or self._client.config.api_key or "", + ), + ) def query_all_function_instance_logs( self, @@ -560,17 +519,16 @@ def query_all_function_instance_logs( Wraps ``asyncio.run``, so it must not be called from a running event loop — use :class:`AsyncRl` there. """ - with _translate_rl_errors(): - return asyncio.run( - _agentic_rl.AgenticRL.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - api_key=api_key or self._client.config.api_key or "", - ), - ) + return asyncio.run( + _agentic_rl.AgenticRL.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + api_key=api_key or self._client.config.api_key or "", + ), + ) def delete_function_instance( self, @@ -583,13 +541,12 @@ def delete_function_instance( Wraps ``asyncio.run``, so it must not be called from a running event loop — use :class:`AsyncRl` there. """ - with _translate_rl_errors(): - return asyncio.run( - _agentic_rl.AgenticRL.delete_function_instance( - function_instance_id=function_instance_id, - api_key=api_key or self._client.config.api_key or "", - ), - ) + return asyncio.run( + _agentic_rl.AgenticRL.delete_function_instance( + function_instance_id=function_instance_id, + api_key=api_key or self._client.config.api_key or "", + ), + ) class AsyncRlJobs: @@ -611,12 +568,11 @@ async def create( ``kwargs`` override individual keys of the training config loaded from ``config_path``, which is open-ended by design. """ - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - if job_name is not None: - kwargs["job_name"] = job_name - rl.init(config_path=config_path, **kwargs) - result = await rl.run() + rl = _new_agentic_rl(self._client, api_key=api_key) + if job_name is not None: + kwargs["job_name"] = job_name + rl.init(config_path=config_path, **kwargs) + result = await rl.run() return _to_job(result) async def fetch( @@ -786,16 +742,15 @@ async def register_functions( lazy_load: bool = True, api_key: Optional[str] = None, ) -> RegisteredFunctions: - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - return await _register_functions_workflow( - rl, - rollout_classpaths, - reward_classpaths, - group_reward_classpaths, - workspace_dir, - lazy_load, - ) + rl = _new_agentic_rl(self._client, api_key=api_key) + return await _register_functions_workflow( + rl, + rollout_classpaths, + reward_classpaths, + group_reward_classpaths, + workspace_dir, + lazy_load, + ) async def test_functions( self, @@ -815,17 +770,16 @@ async def test_functions( parameters forward to the log-pulling step that runs after verification. """ - with _translate_rl_errors(): - return await _agentic_rl.AgenticRL.test_functions( - instance_id=instance_id, - functype=_resolve_func_type(func_type), - input_data=input_data, - api_key=api_key or self._client.config.api_key or "", - pull_logs=pull_logs, - log_page_size=log_page_size, - start_time=start_time, - end_time=end_time, - ) + return await _agentic_rl.AgenticRL.test_functions( + instance_id=instance_id, + functype=_resolve_func_type(func_type), + input_data=input_data, + api_key=api_key or self._client.config.api_key or "", + pull_logs=pull_logs, + log_page_size=log_page_size, + start_time=start_time, + end_time=end_time, + ) async def upload_data( self, @@ -834,12 +788,11 @@ async def upload_data( *, api_key: Optional[str] = None, ) -> UploadedData: - with _translate_rl_errors(): - rl = _new_agentic_rl(self._client, api_key=api_key) - train_ids, val_ids = await rl.upload_datasets( - training_files=training_files, - validation_files=validation_files, - ) + rl = _new_agentic_rl(self._client, api_key=api_key) + train_ids, val_ids = await rl.upload_datasets( + training_files=training_files, + validation_files=validation_files, + ) return UploadedData( training_file_ids=train_ids, validation_file_ids=val_ids or [], @@ -857,16 +810,15 @@ async def query_function_instance_logs( api_key: Optional[str] = None, ) -> Dict[str, Any]: """One page of function-instance logs (raw log-query API dict).""" - with _translate_rl_errors(): - return await _agentic_rl.AgenticRL.query_function_instance_logs( - function_instance_id=function_instance_id, - page_number=page_number, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - api_key=api_key or self._client.config.api_key or "", - ) + return await _agentic_rl.AgenticRL.query_function_instance_logs( + function_instance_id=function_instance_id, + page_number=page_number, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + api_key=api_key or self._client.config.api_key or "", + ) async def query_all_function_instance_logs( self, @@ -879,17 +831,14 @@ async def query_all_function_instance_logs( api_key: Optional[str] = None, ) -> List[str]: """All function-instance logs, aggregated across pages.""" - with _translate_rl_errors(): - return ( - await _agentic_rl.AgenticRL.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - api_key=api_key or self._client.config.api_key or "", - ) - ) + return await _agentic_rl.AgenticRL.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + api_key=api_key or self._client.config.api_key or "", + ) async def delete_function_instance( self, @@ -898,8 +847,7 @@ async def delete_function_instance( api_key: Optional[str] = None, ) -> Dict[str, Any]: """Delete a function-instance (raw delete API dict).""" - with _translate_rl_errors(): - return await _agentic_rl.AgenticRL.delete_function_instance( - function_instance_id=function_instance_id, - api_key=api_key or self._client.config.api_key or "", - ) + return await _agentic_rl.AgenticRL.delete_function_instance( + function_instance_id=function_instance_id, + api_key=api_key or self._client.config.api_key or "", + ) diff --git a/tests/v2/test_agentstudio_errors.py b/tests/v2/test_agentstudio_errors.py index 01840c0a..ecb1b0d2 100644 --- a/tests/v2/test_agentstudio_errors.py +++ b/tests/v2/test_agentstudio_errors.py @@ -1,15 +1,9 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Tests for the AgentStudio -> v2 error-alignment proxy (M3 3.12). +"""AgentStudio raises v2 errors natively through its public API. -Two things are pinned here: - -* :func:`translate_agentstudio_error` maps the 1.x AgentStudio family - onto the v2 family by *two* routes -- HTTP-status errors through - ``error_from_status`` and the four no-response local/stream errors - through ``error_from_sdk_exception``. -* The transparent proxy keeps ``isinstance`` / ``.transport`` working - while making every failure catchable as ``DashScopeError``. +Covers direct clients, lazy pagination, and SSE streams without a mount- +boundary translation layer. """ # Mock handlers intentionally ignore the request argument. @@ -29,11 +23,6 @@ from dashscope.agentstudio.pagination import AsyncCursorPage, CursorPage from dashscope.agentstudio.types import ServerEvent, Session from dashscope.v2 import errors as v2_errors -from dashscope.v2.resources._agentstudio import ( - _wrap_result, - translate_agentstudio_error, - wrap_agentstudio, -) BASE_URL = "https://as.test.local" @@ -50,79 +39,41 @@ def _clear_credentials(monkeypatch): # --------------------------------------------------------------------------- -# Two-branch translation, exercised directly (no network) +# Native v2 classes and catalog-backed local errors # --------------------------------------------------------------------------- -@pytest.mark.parametrize( - "as_cls,status,expected", - [ - (as_exc.InvalidRequestError, 400, v2_errors.BadRequestError), - (as_exc.AuthenticationError, 401, v2_errors.AuthenticationError), - (as_exc.PermissionDeniedError, 403, v2_errors.PermissionDeniedError), - (as_exc.NotFoundError, 404, v2_errors.NotFoundError), - (as_exc.RateLimitError, 429, v2_errors.RateLimitError), - (as_exc.InternalServerError, 500, v2_errors.InternalServerError), - (as_exc.OverloadedError, 503, v2_errors.ServiceUnavailableError), - # 409 has no catalog status entry -> generic APIStatusError, but - # still inside the v2 family and still carrying the real status. - (as_exc.ConflictError, 409, v2_errors.APIStatusError), - ], -) -def test_http_status_errors_map_through_status_map(as_cls, status, expected): - err = translate_agentstudio_error( - as_cls("boom", status_code=status, request_id="req-1"), - ) - assert isinstance(err, expected) - assert isinstance(err, v2_errors.DashScopeError) - assert err.status_code == status - assert err.request_id == "req-1" +def test_legacy_names_alias_v2_classes(): + assert as_exc.AgentStudioError is v2_errors.DashScopeError + assert as_exc.InvalidRequestError is v2_errors.BadRequestError + assert as_exc.ConflictError is v2_errors.APIStatusError + assert as_exc.OverloadedError is v2_errors.ServiceUnavailableError @pytest.mark.parametrize( "as_cls,code", [ - (as_exc.APIConnectionError, "sdk.agentstudio.APIConnectionError"), - (as_exc.APITimeoutError, "sdk.agentstudio.APITimeoutError"), + (as_exc.APIConnectionError, "sdk.ConnectionError"), + (as_exc.APITimeoutError, "sdk.Timeout"), (as_exc.StreamError, "sdk.agentstudio.StreamError"), (as_exc.StreamClosedError, "sdk.agentstudio.StreamClosedError"), ], ) -def test_local_and_stream_errors_map_to_sdk_error(as_cls, code): - err = translate_agentstudio_error(as_cls("boom")) +def test_local_and_stream_errors_use_catalog_entries(as_cls, code): + err = as_cls("boom") assert isinstance(err, v2_errors.SdkError) assert err.code == code assert err.status_code is None -def test_unregistered_base_class_does_not_keyerror(): - # The bare base class has no catalog entry and there is deliberately - # no ``sdk.agentstudio.Error`` fallback; translation must degrade - # gracefully instead of raising KeyError and masking the original. - err = translate_agentstudio_error(as_exc.AgentStudioError("boom")) - assert isinstance(err, v2_errors.DashScopeError) - assert isinstance(err, v2_errors.APIConnectionError) - - -def test_status_less_http_error_recovers_class_status(): - # The missing-api_key guard raises an HTTP-family error with no - # status_code; translation must still land on AuthenticationError - # rather than degrading to a 500 InternalServerError. - err = translate_agentstudio_error( - as_exc.AuthenticationError("api_key is required", code="auth"), - ) - assert isinstance(err, v2_errors.AuthenticationError) - assert err.status_code == 401 - - # --------------------------------------------------------------------------- -# Transparent proxy over a real (mock-transport) client +# Transparent client over a real (mock-transport) client # --------------------------------------------------------------------------- -def _make_proxy(handler, **kwargs): +def _make_client(handler, **kwargs): kwargs.setdefault("max_retries", 0) - real = AgentStudioClient( + return AgentStudioClient( api_key="sk-test", base_url=BASE_URL, http_client=httpx.Client( @@ -131,19 +82,18 @@ def _make_proxy(handler, **kwargs): ), **kwargs, ) - return wrap_agentstudio(real) -def test_proxy_is_transparent(): - proxy = _make_proxy(lambda request: httpx.Response(200, json={})) - assert isinstance(proxy, AgentStudioClient) - assert hasattr(proxy, "agents") - assert hasattr(proxy, "sessions") - assert proxy.transport.base_url == BASE_URL - proxy.close() +def test_client_exposes_native_resources(): + client = _make_client(lambda request: httpx.Response(200, json={})) + assert isinstance(client, AgentStudioClient) + assert hasattr(client, "agents") + assert hasattr(client, "sessions") + assert client.transport.base_url == BASE_URL + client.close() -def test_proxy_translates_http_error_and_is_catchable_as_v2(): +def test_client_translates_http_error_and_is_catchable_as_v2(): def handler(request): return httpx.Response( 404, @@ -157,43 +107,41 @@ def handler(request): }, ) - proxy = _make_proxy(handler) + client = _make_client(handler) with pytest.raises(v2_errors.DashScopeError) as ei: - proxy.sessions.retrieve("s-1") + client.sessions.retrieve("s-1") assert isinstance(ei.value, v2_errors.NotFoundError) assert ei.value.status_code == 404 assert ei.value.request_id == "req-404" - # The original 1.x error is preserved as the cause. - assert isinstance(ei.value.__cause__, as_exc.NotFoundError) - proxy.close() + client.close() -def test_proxy_translates_rate_limit_error(): - proxy = _make_proxy(lambda request: httpx.Response(429, json={})) +def test_client_translates_rate_limit_error(): + client = _make_client(lambda request: httpx.Response(429, json={})) with pytest.raises(v2_errors.RateLimitError): - proxy.sessions.retrieve("s-1") - proxy.close() + client.sessions.retrieve("s-1") + client.close() -def test_proxy_translates_connection_error_to_sdk_error(): +def test_client_translates_connection_error_to_sdk_error(): def handler(request): raise httpx.ConnectError("boom") - proxy = _make_proxy(handler) + client = _make_client(handler) with pytest.raises(v2_errors.SdkError) as ei: - proxy.sessions.retrieve("s-1") - assert ei.value.code == "sdk.agentstudio.APIConnectionError" - proxy.close() + client.sessions.retrieve("s-1") + assert ei.value.code == "sdk.ConnectionError" + client.close() # --------------------------------------------------------------------------- -# Async proxy +# Async client # --------------------------------------------------------------------------- -def _make_async_proxy(handler, **kwargs): +def _make_async_client(handler, **kwargs): kwargs.setdefault("max_retries", 0) - real = AsAgentStudioClient( + return AsAgentStudioClient( api_key="sk-test", base_url=BASE_URL, http_client=httpx.AsyncClient( @@ -202,11 +150,10 @@ def _make_async_proxy(handler, **kwargs): ), **kwargs, ) - return wrap_agentstudio(real) @pytest.mark.asyncio -async def test_async_proxy_translates_http_error(): +async def test_async_client_translates_http_error(): def handler(request): return httpx.Response( 429, @@ -219,44 +166,16 @@ def handler(request): }, ) - proxy = _make_async_proxy(handler) - assert isinstance(proxy, AsAgentStudioClient) + client = _make_async_client(handler) + assert isinstance(client, AsAgentStudioClient) with pytest.raises(v2_errors.RateLimitError): - await proxy.sessions.retrieve("s-1") - await proxy.aclose() - - -# --------------------------------------------------------------------------- -# Lazy stream errors surface during iteration, not at call time -# --------------------------------------------------------------------------- - - -class _FakeStream: - """CM + iterable that fails partway through, like a dropped SSE stream.""" - - def __enter__(self): - return self - - def __exit__(self, *exc): - return None - - def __iter__(self): - yield {"ok": 1} - raise as_exc.StreamClosedError("stream dropped") - - -def test_sync_stream_proxy_translates_iteration_error(): - stream = _wrap_result(_FakeStream()) - iterator = iter(stream) - assert next(iterator) == {"ok": 1} - with pytest.raises(v2_errors.SdkError) as ei: - next(iterator) - assert ei.value.code == "sdk.agentstudio.StreamClosedError" + await client.sessions.retrieve("s-1") + await client.aclose() @pytest.mark.parametrize("compat", [False, True]) @pytest.mark.parametrize("header_id", [None, "header-id"]) -def test_translation_preserves_v1_metadata(compat, header_id): +def test_from_response_preserves_metadata(compat, header_id): body = { "type": "error", "message": "top-level message must not replace the parsed message", @@ -267,105 +186,27 @@ def test_translation_preserves_v1_metadata(compat, header_id): }, } headers = {"x-request-id": header_id} if header_id else {} - original = as_exc.from_response( + err = as_exc.from_response( status_code=429, body=body, headers=headers, ) - translated = translate_agentstudio_error(original) - assert isinstance(translated, v2_errors.RateLimitError) - assert ( - translated.request_id - == original.request_id - == (header_id or "body-id") - ) - assert translated.code == original.code == "rate_limit_error" - assert translated.message == original.message == "canonical message" - assert translated.body is body + assert isinstance(err, v2_errors.RateLimitError) + assert err.request_id == (header_id or "body-id") + assert err.code == "rate_limit_error" + assert err.message == "canonical message" + assert err.body is body assert body["request_id"] == "body-id" -def test_agentstudio_code_does_not_override_http_status(): - original = as_exc.from_response( +def test_unknown_agentstudio_code_falls_back_to_http_status(): + err = as_exc.from_response( status_code=429, body={"error": {"code": "InvalidApiKey", "message": "quota"}}, ) - translated = translate_agentstudio_error(original) - assert isinstance(translated, v2_errors.RateLimitError) - assert translated.code == "InvalidApiKey" - assert translated.body is original.raw - - -@pytest.mark.parametrize("asynchronous", [False, True]) -@pytest.mark.asyncio -async def test_proxy_observes_attribute_and_method_replacements(asynchronous): - factory = _make_async_proxy if asynchronous else _make_proxy - proxy = factory(lambda request: httpx.Response(200, json={"id": "s-1"})) - target = object.__getattribute__(proxy, "_target") - http_client = target.transport._client - try: - target.labels = {"before": True} - assert proxy.labels is target.labels - proxy.labels = {"after": True} - assert proxy.labels == {"after": True} - target.labels = {"direct": True} - assert proxy.labels is target.labels - assert proxy.labels == {"direct": True} - assert "labels" not in object.__getattribute__(proxy, "_cache") - - resource = proxy.sessions - assert proxy.sessions is resource - result = resource.retrieve("s-1") - if asynchronous: - result = await result - assert type(result) is Session - assert result.id == "s-1" - assert "retrieve" not in object.__getattribute__(resource, "_cache") - - first = Session(id="replacement") - - async def async_retrieve(session_id): - return first - - replacement = async_retrieve if asynchronous else lambda _: first - resource.retrieve = replacement - result = resource.retrieve("s-1") - assert (await result if asynchronous else result) is first - - async def fail(session_id): - raise as_exc.RateLimitError("new method") - - def sync_fail(session_id): - raise as_exc.RateLimitError("new method") - - target.sessions.retrieve = fail if asynchronous else sync_fail - with pytest.raises(v2_errors.RateLimitError): - result = resource.retrieve("s-1") - if asynchronous: - await result - - target.sessions = type(target.sessions)(target) - assert proxy.sessions is not resource - assert proxy.sessions.events is proxy.sessions.events - resource = proxy.sessions - proxy.sessions = type(target.sessions)(target) - assert proxy.sessions is not resource - - transport = proxy.transport - proxy.transport = object() - assert proxy.transport is target.transport - assert proxy.transport is not transport - target.transport = transport - assert proxy.transport is transport - finally: - if asynchronous: - await proxy.aclose() - assert not http_client.is_closed - await http_client.aclose() - else: - proxy.close() - assert not http_client.is_closed - http_client.close() + assert isinstance(err, v2_errors.RateLimitError) + assert err.code == "InvalidApiKey" + assert err.body == {"error": {"code": "InvalidApiKey", "message": "quota"}} def _page_handler(requests, fail_at=None): @@ -406,10 +247,10 @@ async def test_pagination_lazily_translates_later_pages( fail_at, ): requests = [] - factory = _make_async_proxy if asynchronous else _make_proxy - proxy = factory(_page_handler(requests, fail_at)) + factory = _make_async_client if asynchronous else _make_client + client = factory(_page_handler(requests, fail_at)) try: - page = proxy.sessions.list(limit=1) + page = client.sessions.list(limit=1) if asynchronous: page = await page assert isinstance( @@ -444,12 +285,11 @@ async def test_pagination_lazily_translates_later_pages( assert caught.value.request_id == "page-header" assert caught.value.message == "slow" assert caught.value.status_code == 429 - assert isinstance(caught.value.__cause__, as_exc.RateLimitError) finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() @pytest.mark.parametrize("asynchronous", [False, True]) @@ -458,10 +298,10 @@ async def test_pagination_success_preserves_models_and_mutability( asynchronous, ): requests = [] - factory = _make_async_proxy if asynchronous else _make_proxy - proxy = factory(_page_handler(requests)) + factory = _make_async_client if asynchronous else _make_client + client = factory(_page_handler(requests)) try: - page = proxy.sessions.list(limit=1) + page = client.sessions.list(limit=1) if asynchronous: page = await page items = [item async for item in page] if asynchronous else list(page) @@ -481,9 +321,9 @@ async def test_pagination_success_preserves_models_and_mutability( assert not page finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() _SSE_EVENTS = [ @@ -546,7 +386,7 @@ def _sse_client(asynchronous, error=None): headers={"content-type": "text/event-stream"}, stream=body, ) - factory = _make_async_proxy if asynchronous else _make_proxy + factory = _make_async_client if asynchronous else _make_client return factory(lambda request: response), response, body @@ -560,17 +400,17 @@ async def _consume_stream(stream, view, asynchronous): @pytest.mark.parametrize( "error_cls,code", [ - (httpx.ReadTimeout, "sdk.agentstudio.APITimeoutError"), - (httpx.ReadError, "sdk.agentstudio.APIConnectionError"), - (httpx.RemoteProtocolError, "sdk.agentstudio.APIConnectionError"), + (httpx.ReadTimeout, "sdk.Timeout"), + (httpx.ReadError, "sdk.ConnectionError"), + (httpx.RemoteProtocolError, "sdk.ConnectionError"), ], ) @pytest.mark.asyncio async def test_real_sse_transport_errors(asynchronous, view, error_cls, code): original = error_cls("SSE transport failed") - proxy, response, body = _sse_client(asynchronous, original) + client, response, body = _sse_client(asynchronous, original) try: - stream = proxy.sessions.events.stream("s-1") + stream = client.sessions.events.stream("s-1") if asynchronous: stream = await stream assert not response.is_closed @@ -583,18 +423,18 @@ async def test_real_sse_transport_errors(asynchronous, view, error_cls, code): assert body.closed == 1 finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() @pytest.mark.parametrize("asynchronous", [False, True]) @pytest.mark.parametrize("view", ["events", "text_stream", "text_deltas"]) @pytest.mark.asyncio async def test_real_sse_closed_views(asynchronous, view): - proxy, response, body = _sse_client(asynchronous) + client, response, body = _sse_client(asynchronous) try: - stream = proxy.sessions.events.stream("s-1") + stream = client.sessions.events.stream("s-1") if asynchronous: stream = await stream await stream.aclose() @@ -607,12 +447,11 @@ async def test_real_sse_closed_views(asynchronous, view): with pytest.raises(v2_errors.SdkError) as caught: await _consume_stream(stream, view, asynchronous) assert caught.value.code == "sdk.agentstudio.StreamClosedError" - assert isinstance(caught.value.__cause__, as_exc.StreamClosedError) finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() @pytest.mark.parametrize("asynchronous", [False, True]) @@ -624,9 +463,9 @@ async def test_real_sse_normal_results_and_context_close( view, early_exit, ): - proxy, response, body = _sse_client(asynchronous) + client, response, body = _sse_client(asynchronous) try: - stream = proxy.sessions.events.stream("s-1") + stream = client.sessions.events.stream("s-1") if asynchronous: stream = await stream async with stream as entered: @@ -667,9 +506,9 @@ async def test_real_sse_normal_results_and_context_close( assert body.closed == 1 finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() @pytest.mark.parametrize("asynchronous", [False, True]) @@ -683,9 +522,9 @@ async def test_real_sse_programming_errors_are_not_translated( error_cls, ): original = error_cls("programming error") - proxy, response, body = _sse_client(asynchronous, original) + client, response, body = _sse_client(asynchronous, original) try: - stream = proxy.sessions.events.stream("s-1") + stream = client.sessions.events.stream("s-1") if asynchronous: stream = await stream with pytest.raises(error_cls) as caught: @@ -695,9 +534,9 @@ async def test_real_sse_programming_errors_are_not_translated( assert body.closed == 1 finally: if asynchronous: - await proxy.transport._client.aclose() + await client.transport._client.aclose() else: - proxy.transport._client.close() + client.transport._client.close() @pytest.mark.asyncio @@ -716,9 +555,9 @@ async def __aiter__(self): headers={"content-type": "text/event-stream"}, stream=body, ) - proxy = _make_async_proxy(lambda request: response) + client = _make_async_client(lambda request: response) try: - stream = await proxy.sessions.events.stream("s-1") + stream = await client.sessions.events.stream("s-1") async with stream: task = asyncio.create_task( _consume_stream(stream, "text_stream", True), @@ -730,28 +569,4 @@ async def __aiter__(self): assert response.is_closed assert body.closed == 1 finally: - await proxy.transport._client.aclose() - - -@pytest.mark.parametrize("asynchronous", [False, True]) -@pytest.mark.asyncio -async def test_standalone_v1_keeps_native_errors(asynchronous): - original = httpx.ReadTimeout("native read timeout") - proxy, response, body = _sse_client(asynchronous, original) - client = object.__getattribute__(proxy, "_target") - try: - stream = client.sessions.events.stream("s-1") - if asynchronous: - stream = await stream - with pytest.raises(httpx.ReadTimeout) as caught: - await _consume_stream(stream, "text_stream", asynchronous) - assert caught.value is original - with pytest.raises(as_exc.StreamClosedError): - await _consume_stream(stream, "text_stream", asynchronous) - assert response.is_closed - assert body.closed == 1 - finally: - if asynchronous: - await client.transport._client.aclose() - else: - client.transport._client.close() + await client.transport._client.aclose() diff --git a/tests/v2/test_agentstudio_shim.py b/tests/v2/test_agentstudio_shim.py index 9e3e442c..893f97df 100644 --- a/tests/v2/test_agentstudio_shim.py +++ b/tests/v2/test_agentstudio_shim.py @@ -7,10 +7,8 @@ * the old import path -- including deep submodule paths -- resolves to the very same module and class objects, so nothing downstream sees a fork; -* the pieces AgentStudio used to borrow from the 1.x ``common`` package now - come from the neutral ``dashscope._shared`` layer, with identity intact - (the exception base class and the mutable sdk-client override are both - load-bearing singletons); +* shared validation and SDK identity helpers still come from the neutral + ``dashscope._shared`` layer, while exceptions come from ``v2.errors``; * AgentStudio keeps emitting the *unmarked* ``agentstudio`` module tag, and resolves its api key without reading the 1.x ``dashscope.api_key`` global. """ @@ -29,9 +27,9 @@ from dashscope.legacy_1x.common import error as legacy_error from dashscope.legacy_1x.common import env as legacy_env from dashscope.legacy_1x.common import utils as legacy_utils +from dashscope.v2 import errors as v2_errors from dashscope.v2.agentstudio import client as as_client from dashscope.v2.agentstudio import exceptions as impl_exc -from dashscope.v2.resources._agentstudio import _is_resource, wrap_agentstudio BASE_URL = "https://as.test.local" @@ -141,20 +139,12 @@ def test_deep_import_from_syntax_resolves(): # --------------------------------------------------------------------------- -def test_exception_base_class_identity_is_preserved(): - """Both stacks must inherit from one and the same class object.""" - - from dashscope._shared._errors import DashScopeException - - assert legacy_error.DashScopeException is DashScopeException - assert issubclass( +def test_agentstudio_uses_the_v2_error_base(): + assert impl_exc.AgentStudioError is v2_errors.DashScopeError + assert not issubclass( impl_exc.AgentStudioError, legacy_error.DashScopeException, ) - assert issubclass( - legacy_error.InvalidParameter, - legacy_error.DashScopeException, - ) def test_validators_are_shared_and_still_reject_host_breakouts(): @@ -277,30 +267,3 @@ def test_missing_api_key_surfaces_at_request_time(_no_key_anywhere): uid=None, user_agent="ua", ) - - -# --------------------------------------------------------------------------- -# The v2 error-alignment proxy still recognises sub-resources -# --------------------------------------------------------------------------- - - -def test_resource_detection_survives_the_move(): - """``_RESOURCE_PACKAGE`` must track the new module path. - - If it still pointed at ``dashscope.agentstudio.resources`` the proxy - would silently stop wrapping sub-resources, and their errors would - escape ``except dashscope.v2.errors.DashScopeError``. - """ - - client = _make_client() - assert _is_resource(client.agents) - assert _is_resource(client.sessions) - assert not _is_resource(client.transport) - - -def test_wrapped_resource_is_proxied(): - wrapped = wrap_agentstudio(_make_client()) - assert wrapped.agents is not _make_client().agents - assert type(wrapped.agents).__module__.startswith( - "dashscope.v2.resources._agentstudio", - ) diff --git a/tests/v2/test_apps.py b/tests/v2/test_apps.py index f5c08a87..d0ddd6c0 100644 --- a/tests/v2/test_apps.py +++ b/tests/v2/test_apps.py @@ -190,7 +190,7 @@ def test_agentstudio_mounted_lazily(): workspace="ws-1", ) mounted = client.agentstudio - assert isinstance(mounted, AgentStudioClient) + assert type(mounted) is AgentStudioClient assert client.agentstudio is mounted assert hasattr(mounted, "agents") assert hasattr(mounted, "sessions") @@ -227,7 +227,7 @@ async def test_async_agentstudio_mount_follows_region(): region="eu-central-1", ) mounted = client.agentstudio - assert isinstance(mounted, AsAgentStudioClient) + assert type(mounted) is AsAgentStudioClient assert mounted.transport.base_url == ( "https://ws-1.eu-central-1.maas.aliyuncs.com/api/v1/agentstudio" ) diff --git a/tests/v2/test_error_registry.py b/tests/v2/test_error_registry.py index 89f2815f..0a980aeb 100644 --- a/tests/v2/test_error_registry.py +++ b/tests/v2/test_error_registry.py @@ -33,10 +33,8 @@ _PLACEHOLDER = re.compile(r"\{(\w+)\}") -# Catalog entries whose ``name`` describes an operation outcome rather -# than an exception type. 1.x signals these by raising a generic class -# with a specific message and numeric ``error_code``, so they are -# unreachable from ``type(exc).__name__`` and deliberately stay unused. +# Operation outcomes raised through ``AgenticRLError.from_catalog`` rather +# than dedicated exception classes. _CONDITION_ONLY = { "sdk.agentic_rl.FunctionRegistrationFailed", "sdk.agentic_rl.DatasetsUploadFailed", @@ -219,7 +217,7 @@ def test_format_message_substitution(): def test_agentic_rl_reachability_counts(): - """18 exact class-name hits, plus a family fallback for the rest.""" + """18 dedicated classes, plus condition-only and base-class entries.""" rl = _classes(rl_errors) entries = [e for e in SDK if e.name.startswith("sdk.agentic_rl.")] assert len(entries) == 27 @@ -228,13 +226,12 @@ def test_agentic_rl_reachability_counts(): exact = suffixes & rl assert len(exact) == 18, sorted(exact) - # The family fallback the two-tier lookup degrades to. assert "sdk.agentic_rl.Error" in _by_name(SDK) - unreachable = suffixes - rl + without_dedicated_classes = suffixes - rl expected = {name.rsplit(".", 1)[1] for name in _CONDITION_ONLY} expected.add("Error") - assert unreachable == expected + assert without_dedicated_classes == expected def test_every_1x_rl_exception_has_a_catalog_landing_point(): @@ -244,14 +241,19 @@ def test_every_1x_rl_exception_has_a_catalog_landing_point(): ``sdk.agentic_rl.Error``; everything else matches by class name. """ names = _by_name(SDK) - for cls_name in _classes(rl_errors): + local_classes = { + name + for name in _classes(rl_errors) + if getattr(rl_errors, name).__module__ == rl_errors.__name__ + } + for cls_name in local_classes: candidate = f"sdk.agentic_rl.{cls_name}" assert candidate in names or cls_name == "AgenticRLError", cls_name assert "sdk.agentic_rl.Error" in names -def test_agentstudio_transport_errors_fully_covered(): - """The four non-HTTP agentstudio errors map 1:1 onto the catalog.""" +def test_agentstudio_sdk_error_entries_have_native_classes(): + """The catalog's four AgentStudio entries all have native classes.""" expected = { "APIConnectionError", "APITimeoutError", @@ -264,12 +266,7 @@ def test_agentstudio_transport_errors_fully_covered(): def test_agentstudio_has_no_family_fallback(): - """Unlike agentic_rl, this family has no generic ``.Error`` entry. - - Pinned because the sdk-side lookup cannot degrade gracefully here; - the HTTP-layer agentstudio errors carry a real ``status_code`` and - belong to the status mapping, not to ``SDK_ERRORS``. - """ + """AgentStudio HTTP errors use statuses, not a generic SDK entry.""" assert "sdk.agentstudio.Error" not in _by_name(SDK) status_classes = { name @@ -329,9 +326,7 @@ def test_ws_family_entries(): "sdk.ws.BinaryOwnerConflict", "sdk.ws.IllegalState", } - assert { - e.name for e in SDK if e.name.startswith("sdk.ws.") - } == expected + assert {e.name for e in SDK if e.name.startswith("sdk.ws.")} == expected from dashscope.v2 import errors as v2_errors diff --git a/tests/v2/test_finetune_rl_shim.py b/tests/v2/test_finetune_rl_shim.py index e1af41cd..41e15bdd 100644 --- a/tests/v2/test_finetune_rl_shim.py +++ b/tests/v2/test_finetune_rl_shim.py @@ -37,6 +37,9 @@ from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL from dashscope.v2.finetune.reinforcement import TrainingType from dashscope.v2.finetune.reinforcement.common.errors import ( + _LEGACY_ERROR_CODES, + AgenticRLError, + RuntimeErrorWithCode, ValueErrorWithCode, ) from dashscope.v2.resources import reinforcement_learning as _rl_mount @@ -428,19 +431,53 @@ def test_the_v2_mount_points_at_the_v2_workflow_modules(): def test_the_rl_error_module_is_shared_by_both_paths(): legacy_errors = importlib.import_module(f"{LEGACY}.common.errors") - assert _rl_mount._rl_errors is legacy_errors - assert _rl_mount._rl_errors is importlib.import_module( - f"{V2}.common.errors", + v2_errors = importlib.import_module(f"{V2}.common.errors") + assert legacy_errors is v2_errors + assert legacy_errors.ValueErrorWithCode is ValueErrorWithCode + + +@pytest.mark.parametrize( + ("catalog_name", "legacy_code"), + _LEGACY_ERROR_CODES.items(), +) +def test_rl_errors_have_native_catalog_and_legacy_code_defaults( + catalog_name, + legacy_code, +): + errors = importlib.import_module(f"{V2}.common.errors") + cls_name = catalog_name.rsplit(".", 1)[1] + cls = AgenticRLError if cls_name == "Error" else getattr(errors, cls_name) + error = cls("failure") + + assert isinstance(error, SdkError) + assert error.code == catalog_name + assert error.error_code == legacy_code + assert error.status_code is None + + +def test_explicit_legacy_code_overrides_the_class_default(): + error = ValueErrorWithCode("duplicate names", error_code=3004) + + assert error.code == "sdk.agentic_rl.ValueErrorWithCode" + assert error.error_code == 3004 + assert error.message == "duplicate names" + + +def test_condition_only_catalog_entries_do_not_need_new_classes(): + error = AgenticRLError.from_catalog( + "sdk.agentic_rl.DuplicateFunctionNames", + error_code=3004, + variables={"names": "rollout"}, ) - for name in _rl_mount._RL_EXCEPTIONS: - assert getattr(legacy_errors, name.__name__) is name + assert type(error) is AgenticRLError + assert error.code == "sdk.agentic_rl.DuplicateFunctionNames" + assert error.message == "Duplicate function names detected: rollout." + assert error.error_code == 3004 + assert error.allow_retry is False + assert "unique name" in error.solution -def test_translate_rl_errors_still_maps_the_family(): - with pytest.raises(SdkError) as ei: - with _rl_mount._translate_rl_errors(): - raise ValueErrorWithCode("duplicate names", error_code=3004) - assert ei.value.code == "sdk.agentic_rl.ValueErrorWithCode" - assert "duplicate names" in ei.value.message - assert isinstance(ei.value.__cause__, ValueErrorWithCode) +def test_legacy_builtin_exception_inheritance_is_not_retained(): + assert not issubclass(RuntimeErrorWithCode, RuntimeError) + assert not issubclass(ValueErrorWithCode, ValueError) diff --git a/tests/v2/test_rl.py b/tests/v2/test_rl.py index dd982c92..619c823a 100644 --- a/tests/v2/test_rl.py +++ b/tests/v2/test_rl.py @@ -644,12 +644,12 @@ def test_rl_long_form_alias(): assert async_client.reinforcement_learning is async_client.rl -# -- RL exceptions translated into the v2 error family ------------------- +# -- RL exceptions are native members of the v2 error family -------------- @pytest.fixture(name="fail_step") def fixture_fail_step(fake_agentic_rl, monkeypatch): - """Make one named step of the 1.x fake raise ``exc``.""" + """Make one named step of the fake raise ``exc``.""" # Steps the mount layer invokes as ``AgenticRL`` classmethods. classmethod_steps = { @@ -677,24 +677,28 @@ def _sfail(first, **kwargs): return _set -def test_jobs_create_translates_1x_failure(fail_step): - fail_step("run", RuntimeErrorWithCode("submit blew up", error_code=3002)) +def test_jobs_create_propagates_the_native_failure(fail_step): + original = RuntimeErrorWithCode("submit blew up", error_code=3002) + fail_step("run", original) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: client.rl.jobs.create(config_path="init.yaml") err = ei.value + assert err is original assert err.code == "sdk.agentic_rl.RuntimeErrorWithCode" + assert err.error_code == 3002 # SDK-side failures have no HTTP semantics, so no invented status. assert err.status_code is None assert "submit blew up" in err.message - assert isinstance(err.__cause__, RuntimeErrorWithCode) -def test_translated_error_is_catchable_as_dashscope_error(fail_step): - fail_step("upload_datasets", OSSUploadError("bucket denied")) +def test_native_error_is_catchable_as_dashscope_error(fail_step): + original = OSSUploadError("bucket denied") + fail_step("upload_datasets", original) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(DashScopeError) as ei: client.rl.upload_data(training_files=["t.jsonl"]) + assert ei.value is original assert isinstance(ei.value, SdkError) @@ -709,9 +713,7 @@ def test_exact_catalog_hit_carries_its_own_solution(fail_step): assert "OSS bucket permissions" in err.solution -def test_uncatalogued_rl_class_falls_back_to_the_family_entry(fail_step): - # AgenticRLError is the RL family's base but has no catalog entry of - # its own, so it must land on the family fallback, not escape raw. +def test_rl_base_class_uses_the_family_entry(fail_step): fail_step("register_functions", AgenticRLError("something odd")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: @@ -719,7 +721,7 @@ def test_uncatalogued_rl_class_falls_back_to_the_family_entry(fail_step): assert ei.value.code == "sdk.agentic_rl.Error" -def test_test_functions_translates(fail_step): +def test_test_functions_propagates_native_error(fail_step): fail_step("test_functions", InstanceQueryError("no instance")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: @@ -731,7 +733,7 @@ def test_test_functions_translates(fail_step): assert ei.value.code == "sdk.agentic_rl.InstanceQueryError" -def test_function_instance_delete_translates(fail_step): +def test_function_instance_delete_propagates_native_error(fail_step): fail_step("delete_function_instance", InstanceQueryError("no instance")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: @@ -740,8 +742,6 @@ def test_function_instance_delete_translates(fail_step): def test_non_rl_exceptions_pass_through(fail_step): - # A bare ValueError is not part of the rl family, so relabelling it - # as an SDK error would hide bugs in the mount layer itself. fail_step("init", ValueError("plain python error")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(ValueError) as ei: @@ -750,16 +750,18 @@ def test_non_rl_exceptions_pass_through(fail_step): @pytest.mark.asyncio -async def test_async_jobs_create_translates(fail_step): - fail_step("run", RuntimeErrorWithCode("submit blew up", error_code=3002)) +async def test_async_jobs_create_propagates_native_error(fail_step): + original = RuntimeErrorWithCode("submit blew up", error_code=3002) + fail_step("run", original) client = _make_async_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: await client.rl.jobs.create(config_path="init.yaml") + assert ei.value is original assert ei.value.code == "sdk.agentic_rl.RuntimeErrorWithCode" @pytest.mark.asyncio -async def test_async_register_functions_translates(fail_step): +async def test_async_register_functions_propagates_native_error(fail_step): fail_step("register_functions", OSSUploadError("layer upload failed")) client = _make_async_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: From dea0cc221550f531b0af2055ab0bf8b3b27beffe Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 13:23:28 +0800 Subject: [PATCH 09/41] refactor(v2): promote reinforcement public import path Keep legacy reinforcement aliases and move AgentStudio coverage into the v2 suite. --- dashscope/finetune/agentic_rl.py | 8 +- dashscope/finetune/reinforcement/__init__.py | 158 ++---------------- dashscope/reinforcement/__init__.py | 135 +++++++++++++++ .../reinforcement/common/constants.py | 8 +- .../component/observability/genai/__init__.py | 10 +- .../reinforcement/component/server/server.py | 16 +- .../observability/smoke_observability.py | 18 +- .../examples/workspace/UserGuide-zh.md | 16 +- .../examples/workspace/UserGuide.md | 16 +- .../functions/reward/group_reward.py | 6 +- .../workspace/functions/reward/reward.py | 12 +- .../functions/reward/reward_decorator.py | 8 +- .../workspace/functions/rollout/rollout.py | 10 +- .../functions/rollout/rollout_only.py | 10 +- .../scripts/start_local_group_reward.sh | 2 +- .../workspace/scripts/start_local_reward.sh | 2 +- .../scripts/start_local_reward_decorator.sh | 2 +- .../workspace/scripts/start_local_rollout.sh | 2 +- .../examples/workspace/submit_job.py | 2 +- .../examples/workspace/test_functions.py | 2 +- tests/{unit => v2}/test_agentstudio_models.py | 4 +- .../{unit => v2}/test_agentstudio_protocol.py | 118 ++++++------- tests/v2/test_finetune_rl_shim.py | 96 ++++++----- 23 files changed, 334 insertions(+), 327 deletions(-) create mode 100644 dashscope/reinforcement/__init__.py rename tests/{unit => v2}/test_agentstudio_models.py (99%) rename tests/{unit => v2}/test_agentstudio_protocol.py (92%) diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 1220656b..0cc58ee1 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -32,10 +32,10 @@ # Re-exported so every name this module historically carried stays # importable from here. Both spellings resolve to the same objects: the -# reverse-reference shim under ``dashscope/finetune/reinforcement`` -# aliases each submodule to the 2.0 one. +# reverse-reference shim under ``dashscope/reinforcement`` aliases each +# submodule to the 2.0 one. # pylint: disable-next=unused-import -from dashscope.v2.finetune.reinforcement import ( # noqa: F401 +from dashscope.reinforcement import ( # noqa: F401 DASHSCOPE_HTTP_BASE_URL, AgenticRLFunctionComponent, AgenticRLTuning, @@ -59,7 +59,7 @@ ) # pylint: disable-next=unused-import -from dashscope.v2.finetune.reinforcement.common.errors import ( # noqa: F401 +from dashscope.reinforcement.common.errors import ( # noqa: F401 DatasetsError, InstanceQueryError, RegistrationError, diff --git a/dashscope/finetune/reinforcement/__init__.py b/dashscope/finetune/reinforcement/__init__.py index ed6d74fe..f7995f5e 100644 --- a/dashscope/finetune/reinforcement/__init__.py +++ b/dashscope/finetune/reinforcement/__init__.py @@ -1,164 +1,28 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility shim; the implementation moved to -``dashscope.v2.finetune.reinforcement``. +"""Legacy alias for :mod:`dashscope.reinforcement`. -The agentic-RL workflow package is owned by the 2.0 SDK. This path has to -keep resolving indefinitely, because two of its consumers resolve the -module *by name* in a process this repo does not control: - -- ``FC_SERVER_CLASSPATH`` is written into the ``start.sh`` that gets - deployed to a Function Compute sandbox, where the installed dashscope - may be any published version. -- ``examples/workspace/scripts/start_local_*.sh`` launches the reward / - rollout server with ``python3 -m - dashscope.finetune.reinforcement.component.server.server``, and - ``server.py`` hands the same dotted string to ``uvicorn.run``, which - imports it inside its worker process. - -Unlike the AgentStudio shim, aliasing here cannot be eager. Nine of the -package's modules -- ``component.server.*`` and -``component.observability.genai.*`` -- are imported lazily and need -fastapi / opentelemetry, both optional. Walking the tree up front would -either miss them or promote two optional dependencies to import-time -hard ones, so a ``sys.meta_path`` finder aliases each submodule on -demand and hands back the *same module object* the v2 path uses. - -Import from ``dashscope.v2.finetune.reinforcement`` in new code. +The public compatibility package owns the lazy module aliasing; this path +only registers its prefix so existing dotted-path consumers keep resolving +to the same v2 module objects. """ -import importlib -import importlib.util -import sys - -# Importing the package runs its own dependency guard, so a missing -# pydantic / tenacity / pyyaml still raises from this path. -from dashscope.v2.finetune import reinforcement as _impl - -# pylint: disable-next=wildcard-import,unused-wildcard-import +# Import the v2 exports directly rather than wildcard-importing the public +# shim, whose __all__ includes the deliberately lazy CLI ``app``. +from dashscope import reinforcement as _public from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 - -# The wildcard only carries the names in the implementation's __all__, -# which lists classes and helpers but not its subpackages. Binding them -# here keeps the deep imports the samples use statically resolvable for -# pylint and IDEs, to whom the meta_path finder below is invisible. At -# runtime both are already imported by the package's own eager imports. -# pylint: disable-next=unused-import from dashscope.v2.finetune.reinforcement import ( # noqa: F401 common, component, ) -_OLD = __name__ -_NEW = _impl.__name__ - -# ``app`` is the 1.x CLI's typer app and is reachable from this path -# only; the 2.0 package must not depend on the CLI. It stays lazy -# because resolving it pulls in typer and the rest of the CLI. -# pylint: disable-next=undefined-all-variable -__all__ = list(_impl.__all__) + ["app"] # noqa: F405 +__all__ = list(_public.__all__) def __getattr__(name): if name == "app": - from dashscope.legacy_1x.cli.agentic_rl import app - - return app - raise AttributeError(f"module {_OLD!r} has no attribute {name!r}") - - -def _restore_identity(module, v2_spec): - """Put the v2 identity back on a module object both paths share. - - ``importlib._bootstrap._init_module_attrs`` stamps the spec it was - handed onto whatever ``create_module`` returned, and that spec names - the legacy path. The object is the v2 module, so it must keep - reporting the v2 name: pickling, ``importlib.reload`` and anything - comparing ``module.__spec__.name`` against the ``sys.modules`` key - all read it. - """ - module.__name__ = v2_spec.name - module.__spec__ = v2_spec - module.__loader__ = v2_spec.loader - module.__package__ = v2_spec.parent - module.__file__ = v2_spec.origin - if v2_spec.submodule_search_locations is not None: - module.__path__ = v2_spec.submodule_search_locations - - -class _AliasLoader: - """Loader that hands back the already-imported v2 module. - - Not an ``importlib.abc.Loader`` subclass on purpose: the import - system resolves loaders by duck typing, and the ABC still declares - ``module_repr`` abstract on this interpreter even though 3.12 - removed it. - """ - - def __init__(self, v2_spec): - self._v2_spec = v2_spec - - def create_module(self, spec): - module = sys.modules.get(self._v2_spec.name) - if module is None: - module = importlib.import_module(self._v2_spec.name) - # Binding the attribute on the parent is what makes - # ``dashscope.finetune.reinforcement.common.utils`` work as an - # expression and not just as an import target: the import system - # skips a module it already finds in sys.modules and never sets - # the attribute itself. - parent_name, _, leaf = spec.name.rpartition(".") - parent = sys.modules.get(parent_name) - if parent is not None: - setattr(parent, leaf, module) - return module - - def exec_module(self, module): - # Deliberately does not re-run the module body. It already ran - # under the v2 name, and a second pass would build a duplicate - # FastAPI app, OpenTelemetry tracer and log handler. - _restore_identity(module, self._v2_spec) - - def get_code(self, fullname): # pylint: disable=unused-argument - # ``runpy`` -- and therefore ``python -m`` -- refuses a loader - # that yields no code object, and uvicorn resolves its import - # string through the same path. - return self._v2_spec.loader.get_code(self._v2_spec.name) - - -class _Finder: - """Resolve ``dashscope.finetune.reinforcement.*`` to the v2 modules. - - Duck-typed for the same reason as ``_AliasLoader``: ``sys.meta_path`` - entries only need a ``find_spec``. - """ - - def find_spec( # pylint: disable=unused-argument - self, - fullname, - path=None, - target=None, - ): - if not fullname.startswith(_OLD + "."): - return None - v2_name = _NEW + fullname[len(_OLD) :] - # find_spec resolves without importing the leaf, so a module that - # needs fastapi or opentelemetry stays unimported until someone - # actually asks for it. - v2_spec = importlib.util.find_spec(v2_name) - if v2_spec is None or v2_spec.loader is None: - return None - return importlib.util.spec_from_file_location( - fullname, - v2_spec.origin, - loader=_AliasLoader(v2_spec), - submodule_search_locations=v2_spec.submodule_search_locations, - ) + return _public.app + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -# Inserted ahead of PathFinder rather than appended: the alias inherits -# the v2 package's __path__, so PathFinder would find the real file -# there and execute it a *second* time under the legacy name, yielding -# two distinct class objects for the same source. -if not any(isinstance(entry, _Finder) for entry in sys.meta_path): - sys.meta_path.insert(0, _Finder()) +_public._register_alias(__name__) diff --git a/dashscope/reinforcement/__init__.py b/dashscope/reinforcement/__init__.py new file mode 100644 index 00000000..a93a843a --- /dev/null +++ b/dashscope/reinforcement/__init__.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim; the implementation lives in +``dashscope.v2.finetune.reinforcement``. + +This is the public reinforcement import path. The former +``dashscope.finetune.reinforcement`` path registers itself as an additional +alias so existing Function Compute sandboxes and local launch scripts keep +working. + +Aliasing cannot be eager. Nine of the package's modules -- +``component.server.*`` and ``component.observability.genai.*`` -- are +imported lazily and need fastapi / opentelemetry, both optional. Walking +the tree up front would either miss them or promote two optional +dependencies to import-time hard ones, so a ``sys.meta_path`` finder aliases +each submodule on demand and hands back the *same module object* the v2 path +uses. +""" + +import importlib +import importlib.util +import sys + +# Importing the package runs its own dependency guard, so a missing +# pydantic / tenacity / pyyaml still raises from this path. +from dashscope.v2.finetune import reinforcement as _impl + +# pylint: disable-next=wildcard-import,unused-wildcard-import +from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 + +# The wildcard only carries the names in the implementation's __all__, +# which lists classes and helpers but not its subpackages. Binding them +# here keeps the deep imports the samples use statically resolvable for +# pylint and IDEs, to whom the meta_path finder below is invisible. At +# runtime both are already imported by the package's own eager imports. +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement import ( # noqa: F401 + common, + component, +) + +_PUBLIC = __name__ +_IMPL = _impl.__name__ + +# ``app`` is the 1.x CLI's typer app and is reachable from the compatibility +# paths only; the 2.0 package must not depend on the CLI. It stays lazy +# because resolving it pulls in typer and the rest of the CLI. +# pylint: disable-next=undefined-all-variable +__all__ = list(_impl.__all__) + ["app"] # noqa: F405 + + +def __getattr__(name): + if name == "app": + from dashscope.legacy_1x.cli.agentic_rl import app + + return app + raise AttributeError(f"module {_PUBLIC!r} has no attribute {name!r}") + + +def _restore_identity(module, v2_spec): + """Put the v2 identity back on a module object all paths share.""" + module.__name__ = v2_spec.name + module.__spec__ = v2_spec + module.__loader__ = v2_spec.loader + module.__package__ = v2_spec.parent + module.__file__ = v2_spec.origin + if v2_spec.submodule_search_locations is not None: + module.__path__ = v2_spec.submodule_search_locations + + +class _AliasLoader: + """Hand back the already-imported v2 module without executing it again.""" + + def __init__(self, v2_spec): + self._v2_spec = v2_spec + + def create_module(self, spec): + module = sys.modules.get(self._v2_spec.name) + if module is None: + module = importlib.import_module(self._v2_spec.name) + # The import system skips a module already in sys.modules, so bind + # the leaf on the parent for attribute-expression imports. + parent_name, _, leaf = spec.name.rpartition(".") + parent = sys.modules.get(parent_name) + if parent is not None: + setattr(parent, leaf, module) + return module + + def exec_module(self, module): + _restore_identity(module, self._v2_spec) + + def get_code(self, fullname): # pylint: disable=unused-argument + # runpy (python -m) and uvicorn require a loader with a code object. + return self._v2_spec.loader.get_code(self._v2_spec.name) + + +class _Finder: + """Resolve one public package prefix to the v2 modules.""" + + def __init__(self, alias_name): + self.alias_name = alias_name + + def find_spec( # pylint: disable=unused-argument + self, + fullname, + path=None, + target=None, + ): + if not fullname.startswith(self.alias_name + "."): + return None + v2_name = _IMPL + fullname[len(self.alias_name) :] + # find_spec resolves without importing the leaf, preserving the + # optional fastapi and opentelemetry dependencies. + v2_spec = importlib.util.find_spec(v2_name) + if v2_spec is None or v2_spec.loader is None: + return None + return importlib.util.spec_from_file_location( + fullname, + v2_spec.origin, + loader=_AliasLoader(v2_spec), + submodule_search_locations=v2_spec.submodule_search_locations, + ) + + +def _register_alias(alias_name): + # Register ahead of PathFinder so it cannot execute the v2 source again + # under the compatibility name and create duplicate class objects. + if not any( + isinstance(entry, _Finder) and entry.alias_name == alias_name + for entry in sys.meta_path + ): + sys.meta_path.insert(0, _Finder(alias_name)) + + +_register_alias(_PUBLIC) diff --git a/dashscope/v2/finetune/reinforcement/common/constants.py b/dashscope/v2/finetune/reinforcement/common/constants.py index dcab4d9e..0d0a21c7 100644 --- a/dashscope/v2/finetune/reinforcement/common/constants.py +++ b/dashscope/v2/finetune/reinforcement/common/constants.py @@ -38,7 +38,7 @@ def get_int_env(env_var: str, default: int) -> int: # The logger name is an observability contract: user log filters and # existing pipelines match on it, so it keeps the published name even # though the package itself now lives under ``dashscope.v2``. -LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.finetune.reinforcement") +LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.reinforcement") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { "trigger_token", @@ -117,9 +117,9 @@ def get_int_env(env_var: str, default: int) -> int: "FC_SERVER_CLASSPATH", # Written into the start.sh deployed to the Function Compute # sandbox, where the installed dashscope may be any published - # version. The public path keeps resolving through the - # reverse-reference shim at dashscope/finetune/reinforcement. - "dashscope.finetune.reinforcement.component.server.server", + # version. Both the new public path and the old finetune path keep + # resolving through reverse-reference shims. + "dashscope.reinforcement.component.server.server", ) FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) FC_LAYER_USED = get_bool_env("FC_LAYER_USED", True) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py index 21670a57..48a15a6a 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py @@ -4,13 +4,13 @@ Submodules by responsibility: -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 +- :mod:`~dashscope.reinforcement.component.observability.genai.messages` # noqa: E501 — message and tool_calls mapping -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 +- :mod:`~dashscope.reinforcement.component.observability.genai.llm_openai` # noqa: E501 — OpenAI-compatible LLM client patching -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 +- :mod:`~dashscope.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 — DashScope Generation patching -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.tools` +- :mod:`~dashscope.reinforcement.component.observability.genai.tools` — tool tracing (decorator + patcher) Requires ``pip install loongsuite-util-genai`` and tracing env flags @@ -18,7 +18,7 @@ Optional: ``AGENTIC_RL_LOG_TRACE_ID=true`` enables ``trace_id`` diagnostic logs (default off); -see :mod:`dashscope.finetune.reinforcement.component.observability.tracing`. +see :mod:`dashscope.reinforcement.component.observability.tracing`. """ from __future__ import annotations diff --git a/dashscope/v2/finetune/reinforcement/component/server/server.py b/dashscope/v2/finetune/reinforcement/component/server/server.py index 854e2699..c1b5763d 100644 --- a/dashscope/v2/finetune/reinforcement/component/server/server.py +++ b/dashscope/v2/finetune/reinforcement/component/server/server.py @@ -10,7 +10,7 @@ FUNC_TYPE Function type, current supported values: "reward"/"rollout" (required) PROCESSOR_CLASS Full Python path to processor class, e.g.: -dashscope.agenticRL.component.demo.reward_processor_demo.DemoRewardProcessor +dashscope.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor (required, processor instantiated using this class) SERVER_PORT Server port, default 8000 ENABLE_LOGGING Enable verbose logging, "true"/"1" to enable, @@ -22,14 +22,14 @@ == Startup Methods == # Recommended: Run with python -m - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.agenticRL.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor python -m - dashscope.agenticRL.server.server + dashscope.reinforcement.component.server.server # Using uvicorn - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.agenticRL.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor uvicorn - dashscope.agenticRL.server.server:app --host 0.0.0.0 --port 8000 + dashscope.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 == Endpoints == @@ -120,7 +120,7 @@ def _resolve_func_type(raw: str) -> FuncType: if not _PROCESSOR_CLASS_ENV: raise RuntimeError( "Environment variable PROCESSOR_CLASS is required. " - "Example: PROCESSOR_CLASS=dashscope.agenticRL.component.demo." + "Example: PROCESSOR_CLASS=dashscope.reinforcement.component.demo." "reward_processor_demo.DemoRewardProcessor", ) @@ -590,7 +590,7 @@ async def health_check_endpoint() -> JSONResponse: if __name__ == "__main__": # Direct execution entry point for FastAPI server. # Alternative startup using uvicorn: - # uvicorn dashscope.agenticRL.server.server:app --host 0.0.0.0 --port 8000 + # uvicorn dashscope.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 import uvicorn import sys import argparse @@ -627,7 +627,7 @@ async def health_check_endpoint() -> JSONResponse: # uvicorn resolves this import string inside its own worker # process, so it must stay the public path used by # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. - "dashscope.finetune.reinforcement.component.server.server:app", + "dashscope.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, workers=worker_count, diff --git a/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py index d34cc349..2ebcb61e 100644 --- a/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py +++ b/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py @@ -51,33 +51,33 @@ InMemorySpanExporter, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.reinforcement.component.data.base_data_model import ( AgentOutput, ModelResource, ) -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.func_manager import FuncManager -from dashscope.finetune.reinforcement.component.observability.genai import ( +from dashscope.reinforcement.component.func_manager import FuncManager +from dashscope.reinforcement.component.observability.genai import ( observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long observe_processor, ) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md index 7d5b295e..f6788070 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md @@ -118,7 +118,7 @@ export LOG_LEVEL="CRITICAL" 生成智能体轨迹(与环境/LLM 的交互)。 ```python -from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput class DemoRolloutProcessor(AbstractRolloutProcessor): async def process(self, input: RolloutInput) -> RolloutOutput: @@ -130,7 +130,7 @@ class DemoRolloutProcessor(AbstractRolloutProcessor): 评分单个步骤或最终输出。 ```python -from dashscope.finetune.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput +from dashscope.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput class DemoRewardProcessor(AbstractRewardProcessor): def process(self, input: RewardInput) -> RewardOutput: @@ -140,7 +140,7 @@ class DemoRewardProcessor(AbstractRewardProcessor): #### 使用装饰器的高级 Reward 处理器 ```python -from dashscope.finetune.reinforcement import reward_func, sub_reward_func, aggregate_func +from dashscope.reinforcement import reward_func, sub_reward_func, aggregate_func @reward_func("SafetyProcessor") class SafetyProcessor(AbstractRewardProcessor): @@ -164,7 +164,7 @@ class SafetyProcessor(AbstractRewardProcessor): 批量评分轨迹(例如用于排名)。 ```python -from dashscope.finetune.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput +from dashscope.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): def setup(self) -> None: @@ -184,7 +184,7 @@ class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): 2. 确认未显式关闭链路跟踪(`ENABLE_TRAJECTORY=false` 会关闭)。 #### 检测装饰器 -从 `dashscope.finetune.reinforcement.component.observability` 导入。 +从 `dashscope.reinforcement.component.observability` 导入。 | 装饰器 | 用法 | 描述 | | :--- | :--- | :--- | @@ -226,9 +226,9 @@ class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): ```python import openai -from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus +from dashscope.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md index aded12a0..1c090d4a 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md @@ -117,7 +117,7 @@ Functions must inherit from the abstract base classes provided by the SDK. Generates agent trajectories (interactions with the environment/LLM). ```python -from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput class DemoRolloutProcessor(AbstractRolloutProcessor): async def process(self, input: RolloutInput) -> RolloutOutput: @@ -129,7 +129,7 @@ class DemoRolloutProcessor(AbstractRolloutProcessor): Scores individual steps or final outputs. ```python -from dashscope.finetune.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput +from dashscope.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput class DemoRewardProcessor(AbstractRewardProcessor): def process(self, input: RewardInput) -> RewardOutput: @@ -139,7 +139,7 @@ class DemoRewardProcessor(AbstractRewardProcessor): #### Advanced Reward Processor with Decorators ```python -from dashscope.finetune.reinforcement import reward_func, sub_reward_func, aggregate_func +from dashscope.reinforcement import reward_func, sub_reward_func, aggregate_func @reward_func("SafetyProcessor") class SafetyProcessor(AbstractRewardProcessor): @@ -163,7 +163,7 @@ class SafetyProcessor(AbstractRewardProcessor): Scores a batch of trajectories collectively (e.g., for ranking). ```python -from dashscope.finetune.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput +from dashscope.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): def setup(self) -> None: @@ -182,7 +182,7 @@ Enable deep visibility into your agent's execution using OpenTelemetry. Trace da 1. Add observability dependencies to `requirements.txt` (see Section 2.5, “Dependencies for Observability (Tracing)”). #### Instrumentation Decorators -Import from `dashscope.finetune.reinforcement.component.observability`. +Import from `dashscope.reinforcement.component.observability`. | Decorator | Usage | Description | | :--- | :--- | :--- | @@ -217,9 +217,9 @@ Import from `dashscope.finetune.reinforcement.component.observability`. ```python import openai -from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus +from dashscope.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py index 0aad8c64..6c197919 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py @@ -8,15 +8,15 @@ from __future__ import annotations -from dashscope.finetune.reinforcement import ( +from dashscope.reinforcement import ( GroupRewardInput, GroupRewardOutput, GroupReward, Reward, TaskStatus, ) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.reinforcement import logger +from dashscope.reinforcement.component.processor import ( AbstractGroupRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py index aef38047..9c6f42d5 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py @@ -16,20 +16,20 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.finetune.reinforcement import RewardInput, RewardOutput -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.reinforcement import RewardInput, RewardOutput +from dashscope.reinforcement import logger +from dashscope.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.reinforcement.component.data.reward_output import ( Reward, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.reinforcement.component.observability import ( observe_processor, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.reinforcement.component.processor import ( AbstractRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py index 394e7d5a..1e664137 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py @@ -12,15 +12,15 @@ import time from typing import Dict -from dashscope.finetune.reinforcement import AbstractRewardProcessor -from dashscope.finetune.reinforcement import ( +from dashscope.reinforcement import AbstractRewardProcessor +from dashscope.reinforcement import ( RewardInput, RewardOutput, Reward, TaskStatus, ) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement import ( +from dashscope.reinforcement import logger +from dashscope.reinforcement import ( reward_func, sub_reward_func, aggregate_func, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py index 12ece959..75acf817 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py @@ -29,19 +29,19 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.reinforcement import RolloutInput, RolloutOutput +from dashscope.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.reinforcement.component.processor import ( AbstractRolloutProcessor, ) @@ -438,7 +438,7 @@ def __del__(self): if __name__ == "__main__": - from dashscope.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 + from dashscope.reinforcement.component.data.base_data_model import ( # noqa: E501 ModelResource, ) import os diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py index 14cd00bb..7b838993 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py @@ -36,22 +36,22 @@ from types import SimpleNamespace from typing import Any, Dict, List -from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.reinforcement import RolloutInput, RolloutOutput +from dashscope.reinforcement import logger +from dashscope.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.reinforcement.component.observability import ( observe_processor, observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh index 04020e3c..66c28d95 100755 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh @@ -105,7 +105,7 @@ main() { # Phase 4: Service Launch log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.finetune.reinforcement.component.server.server "$@" + exec python3 -m dashscope.reinforcement.component.server.server "$@" } # ==================== Entry ======================== diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh index c4c8445a..d77826f1 100755 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh @@ -104,7 +104,7 @@ main() { # Phase 4: Service Launch log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.finetune.reinforcement.component.server.server "$@" + exec python3 -m dashscope.reinforcement.component.server.server "$@" } # ==================== Entry ======================== diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh index 31f1ac76..9fbfe5e6 100755 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh @@ -104,7 +104,7 @@ main() { # Phase 4: Service Launch log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.finetune.reinforcement.component.server.server "$@" + exec python3 -m dashscope.reinforcement.component.server.server "$@" } # ==================== Entry ======================== diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh index 49c38b12..3e606866 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh @@ -104,7 +104,7 @@ main() { # Phase 4: Service Launch log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.finetune.reinforcement.component.server.server "$@" + exec python3 -m dashscope.reinforcement.component.server.server "$@" } # ==================== Entry ======================== diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py index 7311f4ad..5f13b8e3 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py @@ -49,7 +49,7 @@ # await client.run() # Uses YAML configuration from dashscope.finetune.agentic_rl import AgenticRL -from dashscope.finetune.reinforcement import ( +from dashscope.reinforcement import ( logger, DataSourceType, TrainingDataset, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py index 56b84e7d..5645828d 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py @@ -37,7 +37,7 @@ import json -from dashscope.finetune.reinforcement import ( +from dashscope.reinforcement import ( logger, TaskStatus, FunctionType, diff --git a/tests/unit/test_agentstudio_models.py b/tests/v2/test_agentstudio_models.py similarity index 99% rename from tests/unit/test_agentstudio_models.py rename to tests/v2/test_agentstudio_models.py index 3fecdd86..c383010d 100644 --- a/tests/unit/test_agentstudio_models.py +++ b/tests/v2/test_agentstudio_models.py @@ -3,11 +3,11 @@ import pytest -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_vault, _coerce_credential, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( Agent, Credential, CredentialAuth, diff --git a/tests/unit/test_agentstudio_protocol.py b/tests/v2/test_agentstudio_protocol.py similarity index 92% rename from tests/unit/test_agentstudio_protocol.py rename to tests/v2/test_agentstudio_protocol.py index 6cf6c0e2..217b4631 100644 --- a/tests/unit/test_agentstudio_protocol.py +++ b/tests/v2/test_agentstudio_protocol.py @@ -18,9 +18,9 @@ import pytest -from dashscope.agentstudio import exceptions -from dashscope.agentstudio.transport import is_error_payload, unwrap -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio import exceptions +from dashscope.v2.agentstudio.transport import is_error_payload, unwrap +from dashscope.v2.agentstudio.types import ( user_custom_tool_result, user_define_outcome, user_interrupt, @@ -260,7 +260,7 @@ def test_agent_system_field_and_property(): """Server returns ``system`` field; SDK exposes it both as ``agent.system`` and the convenience ``agent.system_prompt`` property. """ - from dashscope.agentstudio.types import Agent + from dashscope.v2.agentstudio.types import Agent agent = Agent(id="agt_1", system="You are helpful.", name="Test") assert agent.system == "You are helpful." @@ -269,7 +269,7 @@ def test_agent_system_field_and_property(): def test_session_stats_and_usage_fields(): """Session now parses ``stats`` and ``usage`` from server response.""" - from dashscope.agentstudio.types import Session + from dashscope.v2.agentstudio.types import Session stats = {"active_seconds": 42, "duration_seconds": 5} usage = {"input_tokens": 100, "output_tokens": 200} @@ -305,7 +305,7 @@ def __init__(self): def request(self, method, path, **kwargs): self.calls.append({"method": method, "path": path, **kwargs}) - from dashscope.agentstudio.transport import APIResponse + from dashscope.v2.agentstudio.transport import APIResponse return APIResponse( data={"id": "agent_1", "version": 3, "name": "demo"}, @@ -314,7 +314,7 @@ def request(self, method, path, **kwargs): def _client_with_recording_transport(): - from dashscope.agentstudio import Client + from dashscope.v2.agentstudio import Client c = Client(api_key="test-key", base_url="http://test") c.transport = _RecordingTransport() @@ -363,7 +363,7 @@ def test_agents_update_does_not_auto_retrieve(): def test_thread_event_exposes_thread_id(): """thread_* events carry a top-level thread_id; it must be a real field.""" - from dashscope.agentstudio.types import Message + from dashscope.v2.agentstudio.types import Message ev = Message( object="message", @@ -386,7 +386,7 @@ def test_thread_event_exposes_thread_id(): def test_agent_create_body_includes_multiagent(): """multiagent roster is forwarded verbatim on create.""" - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="coordinator", @@ -411,7 +411,7 @@ def test_agent_create_body_includes_multiagent(): def test_agent_update_body_includes_multiagent(): """multiagent is forwarded on update alongside the required version.""" - from dashscope.agentstudio.types.params import AgentUpdateParams + from dashscope.v2.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams( name="coordinator", @@ -431,7 +431,7 @@ def test_agent_update_body_includes_multiagent(): # A MultiAgentConfig read off a response must be accepted back as-is, # so read-modify-write round trips work. - from dashscope.agentstudio.types import Agent + from dashscope.v2.agentstudio.types import Agent hydrated = Agent( id="agent_1", @@ -455,7 +455,7 @@ def test_agent_update_body_includes_multiagent(): def test_agent_create_body_model_id_shorthand(): """Bare model id == ModelConfig(id=...) shorthand.""" - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types.params import AgentCreateParams body = AgentCreateParams(name="a", model="qwen3-max").to_dict() assert body["model"] == {"id": "qwen3-max"} @@ -463,8 +463,8 @@ def test_agent_create_body_model_id_shorthand(): def test_agent_create_body_model_config_object(): """The model config is one object carrying id + inference params.""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -495,8 +495,8 @@ def test_agent_create_body_model_config_object(): def test_agent_create_body_keeps_falsy_params(): """False / 0.0 mean "set to this", not "unset".""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -515,7 +515,7 @@ def test_agent_create_body_keeps_falsy_params(): def test_agent_create_rejects_a_raw_dict_model(): """Request side takes the typed config; the response parser still maps.""" - from dashscope.agentstudio.types.params import ( + from dashscope.v2.agentstudio.types.params import ( AgentCreateParams, AgentUpdateParams, ) @@ -532,8 +532,8 @@ def test_agent_create_rejects_a_raw_dict_model(): def test_agent_create_body_omits_unset_model_params(): """Only the fields you set are sent; the rest keep server defaults.""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -548,9 +548,9 @@ def test_model_config_carries_the_whole_surface_no_flat_kwargs(): """Guard: inference params live on ModelConfig, never as flat kwargs.""" import inspect - from dashscope.agentstudio.resources.agents import Agents, AsyncAgents - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import ( + from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import ( AgentCreateParams, AgentUpdateParams, ) @@ -574,8 +574,8 @@ def test_model_config_carries_the_whole_surface_no_flat_kwargs(): def test_agent_create_requires_a_model_with_id(): """Missing or id-less configs fail fast.""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentCreateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentCreateParams with pytest.raises(ValueError, match="model is required"): AgentCreateParams(name="a", model=None) @@ -586,7 +586,7 @@ def test_agent_create_requires_a_model_with_id(): def test_agent_update_body_omits_model_when_unset(): """Update without model omits the field, so the remote config survives.""" - from dashscope.agentstudio.types.params import AgentUpdateParams + from dashscope.v2.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams(version=2, name="a").to_dict() assert "model" not in body @@ -596,8 +596,8 @@ def test_agent_update_body_omits_model_when_unset(): def test_agent_update_replaces_model_config_without_local_merge(): """No local merge: the SDK sends exactly the config it was given.""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentUpdateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams( version=2, @@ -617,8 +617,8 @@ def test_agent_update_replaces_model_config_without_local_merge(): def test_agent_update_partial_config_is_not_silently_dropped(): """An explicit model argument must carry an id, even when empty.""" - from dashscope.agentstudio.types import ModelConfig - from dashscope.agentstudio.types.params import AgentUpdateParams + from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types.params import AgentUpdateParams with pytest.raises(ValueError, match="model.id is required"): AgentUpdateParams(version=2, model=ModelConfig()) @@ -628,7 +628,7 @@ def test_agent_update_partial_config_is_not_silently_dropped(): def test_agents_create_sends_model_config(): """The resource method forwards the config object into the request.""" - from dashscope.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types import ModelConfig client = _client_with_recording_transport() client.agents.create( @@ -650,7 +650,7 @@ def test_agents_create_sends_model_config(): def test_agents_update_sends_model_config(): - from dashscope.agentstudio.types import Agent + from dashscope.v2.agentstudio.types import Agent client = _client_with_recording_transport() agent = Agent(id="agent_1", model={"id": "qwen3-max", "max_tokens": 4096}) @@ -676,7 +676,7 @@ def __init__(self): async def request(self, method, path, **kwargs): self.calls.append({"method": method, "path": path, **kwargs}) - from dashscope.agentstudio.transport import APIResponse + from dashscope.v2.agentstudio.transport import APIResponse return APIResponse( data={"id": "agent_1", "version": 3, "name": "demo"}, @@ -685,7 +685,7 @@ async def request(self, method, path, **kwargs): def _async_client_with_recording_transport(): - from dashscope.agentstudio import AsyncClient + from dashscope.v2.agentstudio import AsyncClient c = AsyncClient(api_key="test-key", base_url="http://test") c.transport = _AsyncRecordingTransport() @@ -696,7 +696,7 @@ def test_agents_sync_and_async_mirror_each_other(): """AsyncAgents must mirror Agents: same kwargs, same docstrings.""" import inspect - from dashscope.agentstudio.resources.agents import Agents, AsyncAgents + from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents for method in ( "create", @@ -720,7 +720,7 @@ def test_agents_sync_and_async_mirror_each_other(): @pytest.mark.asyncio async def test_async_agents_create_sends_model_config(): - from dashscope.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types import ModelConfig client = _async_client_with_recording_transport() await client.agents.create( @@ -742,7 +742,7 @@ async def test_async_agents_create_sends_model_config(): @pytest.mark.asyncio async def test_async_agents_update_sends_model_config(): - from dashscope.agentstudio.types import ModelConfig + from dashscope.v2.agentstudio.types import ModelConfig client = _async_client_with_recording_transport() await client.agents.update( @@ -758,7 +758,7 @@ async def test_async_agents_update_sends_model_config(): def test_agent_model_hydrates_multiagent(): """Agent response hydrates the multiagent dict into typed models.""" - from dashscope.agentstudio.types import ( + from dashscope.v2.agentstudio.types import ( Agent, MultiAgentConfig, MultiAgentRosterEntry, @@ -785,7 +785,7 @@ def test_agent_model_hydrates_multiagent(): def _approval_request_msg(): - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message return parse_message( { @@ -811,7 +811,7 @@ def _approval_request_msg(): def test_tool_approval_request_accessor(): - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message m = _approval_request_msg() ap = m.tool_approval_request @@ -826,8 +826,8 @@ def test_tool_approval_request_accessor(): def test_stop_reason_status_and_enum(): - from dashscope.agentstudio.types import StopReason - from dashscope.agentstudio.constants import ( + from dashscope.v2.agentstudio.types import StopReason + from dashscope.v2.agentstudio.constants import ( SSEEventType, SessionStatus, StopReasonType, @@ -853,7 +853,7 @@ def test_stop_reason_status_and_enum(): def test_permission_policy_validation(): - from dashscope.agentstudio.types import PermissionPolicy + from dashscope.v2.agentstudio.types import PermissionPolicy assert PermissionPolicy(type="always_ask").type == "always_ask" assert PermissionPolicy().type == "always_allow" # default @@ -868,7 +868,7 @@ def test_permission_policy_validation(): def test_approval_errors_map_by_http_status(): """Approval error codes are not enumerated as separate exception types; they map by HTTP status, with the reason in .code/.message.""" - from dashscope.agentstudio.exceptions import ( + from dashscope.v2.agentstudio.exceptions import ( InternalServerError, InvalidRequestError, OverloadedError, @@ -896,7 +896,7 @@ def test_event_list_types_repeated_query(): """Multi-type filter serializes as repeated types[]= keys, not a single comma-joined string (which the server parses as one bogus value).""" import httpx - from dashscope.agentstudio.types.params import SessionEventListParams + from dashscope.v2.agentstudio.types.params import SessionEventListParams params = SessionEventListParams( types=["message", "tool_call", "tool_approval_request"], @@ -947,10 +947,10 @@ def _delta_payloads(): def test_delta_frames_and_text_deltas(): - from dashscope.agentstudio.resources.session_events import ( + from dashscope.v2.agentstudio.resources.session_events import ( _TypedEventStream, ) - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message msgs = [parse_message(p) for p in _delta_payloads()] assert msgs[0].event_start == {"id": "sevt_a", "type": "message"} @@ -1003,7 +1003,7 @@ def _chunk(eid, text): def _stream_of(frames): - from dashscope.agentstudio.resources.session_events import ( + from dashscope.v2.agentstudio.resources.session_events import ( _TypedEventStream, ) @@ -1011,7 +1011,7 @@ def _stream_of(frames): def test_second_pass_on_one_stream_raises_instead_of_returning_nothing(): - from dashscope.agentstudio.resources.session_events import ( + from dashscope.v2.agentstudio.resources.session_events import ( _AioTypedEventStream, ) @@ -1050,8 +1050,8 @@ async def collect(): def test_session_resource_model_and_add_params(): - from dashscope.agentstudio import SessionResource - from dashscope.agentstudio.types.params import SessionResourceAddParams + from dashscope.v2.agentstudio import SessionResource + from dashscope.v2.agentstudio.types.params import SessionResourceAddParams r = SessionResource( id="sesrsc_1", @@ -1076,7 +1076,7 @@ def test_session_resource_model_and_add_params(): def test_session_thread_model_fields(): - from dashscope.agentstudio.types import SessionThread + from dashscope.v2.agentstudio.types import SessionThread th = SessionThread( id="sthr_01", @@ -1098,7 +1098,7 @@ def test_session_create_params_agent_string_and_object(): """createSession ``agent`` accepts the Agent ID string (forward-compat) or an override object; ``environment_variables`` / ``mcp_configs`` are new top-level fields. Omitted fields are not sent (no null leak).""" - from dashscope.agentstudio.types.params import SessionCreateParams + from dashscope.v2.agentstudio.types.params import SessionCreateParams # String agent — backward compatible. assert SessionCreateParams(agent="agent_01", title="x").to_dict() == { @@ -1149,7 +1149,7 @@ def test_session_update_params_patch_and_clear_semantics(): an explicit ``None`` sub-field serializes to ``null`` (revoke override), an omitted sub-field stays absent (keep), ``[]`` / ``""`` override empty. """ - from dashscope.agentstudio.types.params import SessionUpdateParams + from dashscope.v2.agentstudio.types.params import SessionUpdateParams # Only title — everything else omitted, no null uploaded. assert SessionUpdateParams(title="x").to_dict() == {"title": "x"} @@ -1214,7 +1214,7 @@ def test_session_update_params_patch_and_clear_semantics(): def test_session_model_hydrates_override_agent_and_new_fields(): """Session response parses the agent override object and the new ``environment_variables`` / ``mcp_configs`` fields.""" - from dashscope.agentstudio.types import Agent, Session + from dashscope.v2.agentstudio.types import Agent, Session s = Session( id="sesn_1", @@ -1240,7 +1240,7 @@ def test_session_model_hydrates_override_agent_and_new_fields(): def test_message_error_and_pending_signal(): - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message ev = parse_message( { @@ -1283,7 +1283,7 @@ def test_message_error_and_pending_signal(): def test_message_data_accessors(): - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message # model_request_end mre = parse_message( @@ -1383,7 +1383,7 @@ def test_message_data_accessors(): def test_thread_message_routing_accessor(): - from dashscope.agentstudio.types import parse_message + from dashscope.v2.agentstudio.types import parse_message sent = parse_message( { @@ -1420,7 +1420,7 @@ def test_multiagent_config_normalization_and_roster_fields(): """The SDK normalizes multiagent config (type default, self cleanup) but does not reject server data — the 1-20 / at-most-one-self limits are the server's to enforce, so parsing a valid agent never fails.""" - from dashscope.agentstudio.types import ( + from dashscope.v2.agentstudio.types import ( MultiAgentConfig, MultiAgentRosterEntry, ) @@ -1452,7 +1452,7 @@ def test_text_stream_stops_on_session_status_idle(): """Regression: text_stream/text_deltas must not crash on session_status events. RESCHEDULING was deleted from constants but stop-set tuples still referenced it, causing AttributeError on every stream's normal end path.""" - from dashscope.agentstudio.resources.session_events import ( + from dashscope.v2.agentstudio.resources.session_events import ( _TypedEventStream, ) diff --git a/tests/v2/test_finetune_rl_shim.py b/tests/v2/test_finetune_rl_shim.py index 41e15bdd..361a666f 100644 --- a/tests/v2/test_finetune_rl_shim.py +++ b/tests/v2/test_finetune_rl_shim.py @@ -2,13 +2,13 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Reverse-reference shims and the submit transport for the RL move. -``dashscope.finetune.reinforcement`` and ``dashscope.finetune.agentic_rl`` -are now compatibility surfaces over ``dashscope.v2.finetune``. The old -paths have to keep resolving *by name* in processes this repo does not -control -- a Function Compute sandbox running ``FC_SERVER_CLASSPATH``, and -``uvicorn`` / ``python -m`` resolving the reward-server import string -- -so what is pinned here is object identity, laziness and the wire format, -not just the happy path. +``dashscope.reinforcement`` is the public compatibility package, with +``dashscope.finetune.reinforcement`` retained as its legacy alias. Both +resolve over ``dashscope.v2.finetune``. The paths have to keep resolving +*by name* in processes this repo does not control -- a Function Compute +sandbox running ``FC_SERVER_CLASSPATH``, and ``uvicorn`` / ``python -m`` +resolving the reward-server import string -- so what is pinned here is +object identity, laziness and the wire format, not just the happy path. """ from __future__ import annotations @@ -44,10 +44,12 @@ ) from dashscope.v2.resources import reinforcement_learning as _rl_mount +PUBLIC = "dashscope.reinforcement" LEGACY = "dashscope.finetune.reinforcement" V2 = "dashscope.v2.finetune.reinforcement" +ALIASES = [PUBLIC, LEGACY] -# The modules the package imports eagerly, so both spellings are already +# The modules the package imports eagerly, so all spellings are already # in sys.modules before anything asks for them. The nine that are *not* # here (component.server.* and component.observability.genai.*) are the # reason the shim is a meta_path finder rather than an eager walk. @@ -66,8 +68,6 @@ "component.processor.abstract_reward_processor", ] -SUBMIT_PATH = f"{LEGACY}.component.server.server" - ENVELOPE = { "status_code": 200, "request_id": "req-1", @@ -131,8 +131,8 @@ def fixture_post_calls(monkeypatch): calls = [] # ``_SDK_CLIENT`` is process-wide mutable state, and importing the CLI - # (which ``test_app_is_reachable_only_from_the_legacy_path`` does) - # marks the whole process ``python-cli``. Pinning it here keeps these + # (which the compatibility-path ``app`` test does) marks the whole + # process ``python-cli``. Pinning it here keeps these # assertions independent of test order; monkeypatch restores whatever # the ambient value was. monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-sdk") @@ -162,13 +162,14 @@ def post(self, url, data=None, headers=None, timeout=None): # -- identity ------------------------------------------------------------- -def test_every_exported_name_is_the_same_object(): - legacy = importlib.import_module(LEGACY) +@pytest.mark.parametrize("alias", ALIASES) +def test_every_exported_name_is_the_same_object(alias): + compatible = importlib.import_module(alias) v2 = importlib.import_module(V2) assert v2.__all__ - assert legacy.__all__ == list(v2.__all__) + ["app"] + assert compatible.__all__ == list(v2.__all__) + ["app"] for name in v2.__all__: - assert getattr(legacy, name) is getattr(v2, name), name + assert getattr(compatible, name) is getattr(v2, name), name def test_legacy_agentic_rl_subclasses_the_v2_one(): @@ -183,42 +184,48 @@ def test_only_the_1x_surface_keeps_the_job_verbs(): assert verb in LegacyAgenticRL.__dict__, verb +@pytest.mark.parametrize("alias", ALIASES) @pytest.mark.parametrize("submodule", EAGER_SUBMODULES) -def test_eager_deep_imports_alias_to_the_same_module(submodule): - legacy = importlib.import_module(f"{LEGACY}.{submodule}") +def test_eager_deep_imports_alias_to_the_same_module(alias, submodule): + compatible = importlib.import_module(f"{alias}.{submodule}") v2 = importlib.import_module(f"{V2}.{submodule}") - assert legacy is v2 + assert compatible is v2 # The shared object keeps reporting the v2 name, so pickling and # reload resolve against the sys.modules key that owns it. - assert legacy.__name__ == f"{V2}.{submodule}" - assert sys.modules[f"{LEGACY}.{submodule}"] is v2 + assert compatible.__name__ == f"{V2}.{submodule}" + assert sys.modules[f"{alias}.{submodule}"] is v2 -def test_deep_import_works_as_an_attribute_expression(): - legacy = importlib.import_module(LEGACY) +@pytest.mark.parametrize("alias", ALIASES) +def test_deep_import_works_as_an_attribute_expression(alias): + compatible = importlib.import_module(alias) v2_utils = importlib.import_module(f"{V2}.common.utils") - assert legacy.common.utils.set_api_key is v2_utils.set_api_key + assert compatible.common.utils.set_api_key is v2_utils.set_api_key # -- laziness ------------------------------------------------------------- -def test_server_module_resolves_without_importing_fastapi(): - spec = importlib.util.find_spec(SUBMIT_PATH) +@pytest.mark.parametrize("alias", ALIASES) +def test_server_module_resolves_without_importing_fastapi(alias): + submit_path = f"{alias}.component.server.server" + spec = importlib.util.find_spec(submit_path) assert spec is not None assert "dashscope/v2/finetune/reinforcement" in spec.origin.replace( "\\", "/", ) - assert spec.loader.get_code(SUBMIT_PATH) is not None + assert spec.loader.get_code(submit_path) is not None assert "fastapi" not in sys.modules -def test_python_dash_m_resolves_the_legacy_server_path(): +@pytest.mark.parametrize("alias", ALIASES) +def test_python_dash_m_resolves_the_compatibility_server_path(alias): # ``runpy`` refuses a loader that yields no code object, which is how # examples/workspace/scripts/start_local_*.sh launches the server. - name, _, code = runpy._get_module_details(SUBMIT_PATH) - assert name == SUBMIT_PATH + submit_path = f"{alias}.component.server.server" + name, _, code = runpy._get_module_details(submit_path) + assert name == submit_path assert code is not None assert "fastapi" not in sys.modules @@ -227,26 +234,25 @@ def test_python_dash_m_resolves_the_legacy_server_path(): importlib.util.find_spec("opentelemetry") is not None, reason="opentelemetry is installed, so the guard cannot be observed", ) -def test_genai_stays_behind_its_optional_dependency_on_both_paths(): +def test_genai_stays_behind_its_optional_dependency_on_all_paths(): # ``genai/__init__`` imports ``llm_dashscope`` eagerly, which needs - # opentelemetry. Both spellings must fail identically rather than one + # opentelemetry. All spellings must fail identically rather than one # quietly succeeding through the alias. - for name in ( - f"{LEGACY}.component.observability.genai.llm_dashscope", - f"{V2}.component.observability.genai.llm_dashscope", - ): + for alias in ALIASES + [V2]: + name = f"{alias}.component.observability.genai.llm_dashscope" with pytest.raises(ModuleNotFoundError) as ei: importlib.import_module(name) assert "opentelemetry" in str(ei.value) -def test_app_is_reachable_only_from_the_legacy_path(): +def test_app_is_reachable_only_from_the_compatibility_paths(): v2 = importlib.import_module(V2) assert not hasattr(v2, "app") from dashscope.legacy_1x.cli.agentic_rl import app as cli_app - assert importlib.import_module(LEGACY).app is cli_app + for alias in ALIASES: + assert importlib.import_module(alias).app is cli_app # -- the submit POST ------------------------------------------------------ @@ -416,9 +422,10 @@ def test_a_failed_submit_is_an_envelope_not_an_exception(): # -- single implementation ------------------------------------------------ -def test_the_legacy_package_directory_holds_only_the_shim(): - legacy = importlib.import_module(LEGACY) - package_dir = pathlib.Path(legacy.__file__).parent +@pytest.mark.parametrize("alias", ALIASES) +def test_compatibility_package_directories_hold_only_shims(alias): + compatible = importlib.import_module(alias) + package_dir = pathlib.Path(compatible.__file__).parent assert [p.name for p in package_dir.rglob("*.py")] == ["__init__.py"] @@ -429,11 +436,12 @@ def test_the_v2_mount_points_at_the_v2_workflow_modules(): assert _rl_mount._reinforcement is importlib.import_module(V2) -def test_the_rl_error_module_is_shared_by_both_paths(): +def test_the_rl_error_module_is_shared_by_all_paths(): + public_errors = importlib.import_module(f"{PUBLIC}.common.errors") legacy_errors = importlib.import_module(f"{LEGACY}.common.errors") v2_errors = importlib.import_module(f"{V2}.common.errors") - assert legacy_errors is v2_errors - assert legacy_errors.ValueErrorWithCode is ValueErrorWithCode + assert public_errors is legacy_errors is v2_errors + assert public_errors.ValueErrorWithCode is ValueErrorWithCode @pytest.mark.parametrize( From 0bf8a7fe8820dbcc7bbb55cc2e416c88a7b4bbcb Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 17:25:26 +0800 Subject: [PATCH 10/41] refactor(v2): promote SDK implementation to package root Finalize the 2.0 package layout, move 1.x APIs under legacy_1x, and align tests, packaging, CI, examples, and documentation with the new import paths. --- .github/workflows/unit_test.yml | 52 +- .gitignore | 2 +- .pre-commit-config.yaml | 37 +- MANIFEST.in | 2 +- README.md | 28 +- README_zh.md | 25 +- dashscope/__init__.py | 20 +- dashscope/__init__.pyi | 21 +- dashscope/{v2 => }/_config.py | 4 +- dashscope/{v2 => }/_error_registry.py | 0 dashscope/{v2 => }/_logging.py | 2 +- dashscope/{v2 => }/_quickstart.py | 4 +- dashscope/_shared/_sdk_ident.py | 4 +- .../dashscope-sdk-expert/.acli/skills/cli.md | 2 +- .../.acli/skills/fine-tuning.md | 2 +- .../.acli/system-prompt.md | 4 +- dashscope/agentstudio/__init__.py | 204 ++++- dashscope/{v2 => }/agentstudio/client.py | 28 +- dashscope/{v2 => }/agentstudio/constants.py | 0 dashscope/{v2 => }/agentstudio/exceptions.py | 10 +- dashscope/{v2 => }/agentstudio/pagination.py | 0 .../agentstudio/resources/__init__.py | 20 +- .../agentstudio/resources/_helpers.py | 2 +- .../{v2 => }/agentstudio/resources/agents.py | 8 +- .../agentstudio/resources/deployments.py | 8 +- .../agentstudio/resources/environments.py | 8 +- .../{v2 => }/agentstudio/resources/files.py | 8 +- .../agentstudio/resources/memory_stores.py | 8 +- .../agentstudio/resources/security.py | 4 +- .../agentstudio/resources/session_events.py | 12 +- .../resources/session_resources.py | 8 +- .../agentstudio/resources/session_threads.py | 8 +- .../agentstudio/resources/sessions.py | 14 +- .../{v2 => }/agentstudio/resources/skills.py | 8 +- .../{v2 => }/agentstudio/resources/vaults.py | 8 +- .../resources/webhook_endpoints.py | 8 +- dashscope/{v2 => }/agentstudio/streaming.py | 2 +- dashscope/{v2 => }/agentstudio/transport.py | 4 +- .../{v2 => }/agentstudio/types/__init__.py | 6 +- .../{v2 => }/agentstudio/types/models.py | 2 +- .../{v2 => }/agentstudio/types/params.py | 2 +- dashscope/aigc/__init__.py | 11 - dashscope/api_entities/__init__.py | 12 - dashscope/app/__init__.py | 11 - dashscope/assistants/__init__.py | 11 - dashscope/audio/__init__.py | 11 - dashscope/cli/__init__.py | 11 - dashscope/{v2 => }/client.py | 50 +- dashscope/common/__init__.py | 11 - dashscope/embeddings/__init__.py | 11 - dashscope/{v2 => }/errors.py | 9 +- dashscope/files.py | 11 - dashscope/{v2 => }/finetune/_submit.py | 2 +- dashscope/finetune/agentic_rl.py | 837 ++++++++++++++---- dashscope/finetune/reinforcement/__init__.py | 276 +++++- .../reinforcement/common}/__init__.py | 0 .../reinforcement/common/constants.py | 6 +- .../finetune/reinforcement/common/errors.py | 2 +- .../finetune/reinforcement/common/log.py | 2 +- .../finetune/reinforcement/common/model.py | 12 +- .../reinforcement/common/model_types.py | 0 .../finetune/reinforcement/common/utils.py | 8 +- .../reinforcement/component/__init__.py | 28 +- .../reinforcement/component/data/__init__.py | 14 +- .../component/data/base_data_model.py | 2 +- .../component/data/group_reward_input.py | 4 +- .../component/data/group_reward_output.py | 4 +- .../component/data/reward_input.py | 4 +- .../component/data/reward_output.py | 2 +- .../component/data/rollout_input.py | 4 +- .../component/data/rollout_output.py | 2 +- .../reinforcement/component/demo/__init__.py | 19 + .../demo/group_reward_processor_demo.py | 12 +- .../component/demo/reward_processor_demo.py | 10 +- .../component/demo/rollout_processor_demo.py | 10 +- .../reinforcement/component/func_decorator.py | 4 +- .../reinforcement/component/func_manager.py | 18 +- .../component/observability/__init__.py | 8 +- .../component/observability/genai/__init__.py | 20 +- .../component/observability/genai/_core.py | 0 .../observability/genai/llm_dashscope.py | 8 +- .../observability/genai/llm_openai.py | 8 +- .../component/observability/genai/llm_span.py | 6 +- .../component/observability/genai/messages.py | 6 +- .../component/observability/genai/tools.py | 2 +- .../component/observability/processor_span.py | 8 +- .../component/observability/tracing.py | 8 +- .../component/parser/__init__.py | 20 + .../component/parser/base_parser.py | 2 +- .../component/parser/group_reward_parser.py | 6 +- .../component/parser/reward_parser.py | 6 +- .../component/parser/rollout_parser.py | 6 +- .../component/processor/__init__.py | 20 + .../abstract_group_reward_processor.py | 6 +- .../component/processor/abstract_processor.py | 2 +- .../processor/abstract_reward_processor.py | 8 +- .../processor/abstract_rollout_processor.py | 6 +- .../component/server}/__init__.py | 0 .../reinforcement/component/server/server.py | 28 +- .../examples/observability}/__init__.py | 0 .../observability/smoke_observability.py | 18 +- .../examples/workspace/README-zh.md | 0 .../examples/workspace/README.md | 0 .../examples/workspace/UserGuide-zh.md | 16 +- .../examples/workspace/UserGuide.md | 16 +- .../reinforcement/examples/workspace/cli.sh | 0 .../workspace/data/calc_train_min.jsonl | 0 .../workspace/data/calc_validation_min.jsonl | 0 .../examples/workspace/functions}/__init__.py | 0 .../functions/reward/group_reward.py | 6 +- .../workspace/functions/reward/reward.py | 12 +- .../functions/reward/reward_decorator.py | 8 +- .../workspace/functions/rollout/rollout.py | 10 +- .../functions/rollout/rollout_only.py | 10 +- .../examples/workspace/opd-job.yaml | 0 .../examples/workspace/requirements.txt | 0 .../resources/group_reward_input.json | 0 .../resources/reward_decorator_input.json | 0 .../workspace/resources/reward_input.json | 0 .../workspace/resources/rollout_input.json | 0 .../examples/workspace/rl-job.yaml | 0 .../examples/workspace/submit_job.py | 2 +- .../examples/workspace/test_functions.py | 2 +- dashscope/io/__init__.py | 11 - .../legacy_1x/audio/asr/asr_phrase_manager.py | 2 +- dashscope/legacy_1x/cli/agentic_rl.py | 2 +- .../server => legacy_1x/finetune}/__init__.py | 0 dashscope/legacy_1x/finetune/agentic_rl.py | 222 +++++ .../finetune/customize_types.py | 0 .../{ => legacy_1x}/finetune/deployments.py | 2 +- .../{ => legacy_1x}/finetune/finetunes.py | 2 +- dashscope/model.py | 11 - dashscope/models.py | 11 - dashscope/multimodal/__init__.py | 11 - dashscope/nlp/__init__.py | 11 - dashscope/protocol/__init__.py | 11 - dashscope/{v2 => }/protocols/__init__.py | 10 +- dashscope/{v2 => }/protocols/anthropic.py | 6 +- dashscope/{v2 => }/protocols/base.py | 2 +- dashscope/{v2 => }/protocols/native.py | 4 +- dashscope/{v2 => }/protocols/openai.py | 6 +- dashscope/reinforcement/__init__.py | 135 --- dashscope/rerank/__init__.py | 11 - dashscope/{v2 => }/resources/__init__.py | 0 dashscope/{v2 => }/resources/_jobs.py | 4 +- dashscope/{v2 => }/resources/_oss.py | 4 +- dashscope/{v2 => }/resources/_oss_upload.py | 6 +- dashscope/{v2 => }/resources/_sse.py | 2 +- dashscope/{v2 => }/resources/apps.py | 4 +- .../{v2 => }/resources/audio/__init__.py | 12 +- .../{v2 => }/resources/audio/_callbacks.py | 2 +- dashscope/{v2 => }/resources/audio/asr.py | 8 +- .../{v2 => }/resources/audio/customization.py | 2 +- .../{v2 => }/resources/audio/http_tts.py | 6 +- dashscope/{v2 => }/resources/audio/phrases.py | 2 +- .../{v2 => }/resources/audio/realtime.py | 10 +- dashscope/{v2 => }/resources/audio/tts.py | 6 +- dashscope/{v2 => }/resources/chat/__init__.py | 6 +- dashscope/{v2 => }/resources/chat/_params.py | 0 .../{v2 => }/resources/chat/completions.py | 16 +- dashscope/{v2 => }/resources/decision.py | 6 +- dashscope/{v2 => }/resources/deployments.py | 6 +- dashscope/{v2 => }/resources/embeddings.py | 6 +- dashscope/{v2 => }/resources/files.py | 4 +- dashscope/{v2 => }/resources/fine_tunes.py | 8 +- dashscope/{v2 => }/resources/images.py | 8 +- dashscope/{v2 => }/resources/models.py | 4 +- .../{v2 => }/resources/multimodal/__init__.py | 4 +- .../{v2 => }/resources/multimodal/chat.py | 6 +- .../resources/multimodal/embeddings.py | 8 +- .../resources/reinforcement_learning.py | 18 +- dashscope/{v2 => }/resources/rerank.py | 4 +- dashscope/{v2 => }/resources/tingwu.py | 8 +- dashscope/{v2 => }/resources/tokenization.py | 6 +- dashscope/{v2 => }/resources/videos.py | 4 +- dashscope/threads/__init__.py | 11 - dashscope/tokenizers/__init__.py | 12 +- dashscope/{v2 => }/tokenizers/qwen.tiktoken | 0 dashscope/tokenizers/qwen_tokenizer.py | 198 ++++- dashscope/tokenizers/tokenizer.py | 72 +- dashscope/tokenizers/tokenizer_base.py | 33 +- dashscope/{v2 => }/transport/__init__.py | 2 +- dashscope/{v2 => }/transport/_ident.py | 0 dashscope/{v2 => }/transport/_retry.py | 0 dashscope/{v2 => }/transport/http.py | 12 +- dashscope/{v2 => }/transport/sse.py | 2 +- dashscope/{v2 => }/transport/ws.py | 8 +- dashscope/{v2 => }/transport/ws_connection.py | 6 +- dashscope/{v2 => }/types/__init__.py | 6 +- dashscope/{v2 => }/types/common.py | 2 +- dashscope/{v2 => }/types/completions.py | 4 +- dashscope/{v2 => }/types/messages.py | 0 dashscope/utils/__init__.py | 11 - dashscope/v2/__init__.py | 32 - dashscope/v2/agentstudio/__init__.py | 164 ---- dashscope/v2/finetune/__init__.py | 16 - dashscope/v2/finetune/agentic_rl.py | 762 ---------------- .../v2/finetune/reinforcement/__init__.py | 257 ------ .../reinforcement/component/demo/__init__.py | 19 - .../component/parser/__init__.py | 20 - .../component/processor/__init__.py | 20 - .../examples/workspace/functions/__init__.py | 0 .../examples/workspace/scripts/build.sh | 162 ---- .../scripts/query_local_group_reward.sh | 24 - .../workspace/scripts/query_local_reward.sh | 24 - .../scripts/query_local_reward_decorator.sh | 39 - .../workspace/scripts/query_local_rollout.sh | 47 - .../workspace/scripts/query_online_tuning.sh | 41 - .../scripts/start_local_group_reward.sh | 112 --- .../workspace/scripts/start_local_reward.sh | 111 --- .../scripts/start_local_reward_decorator.sh | 111 --- .../workspace/scripts/start_local_rollout.sh | 111 --- dashscope/v2/tokenizers/__init__.py | 17 - dashscope/v2/tokenizers/qwen_tokenizer.py | 167 ---- dashscope/v2/tokenizers/tokenizer.py | 43 - dashscope/v2/tokenizers/tokenizer_base.py | 31 - dev.md | 16 + docs/source/api/dashscope.aigc.rst | 8 +- ...shscope.aigc.conversation.Conversation.rst | 2 +- .../dashscope.aigc.conversation.History.rst | 2 +- ...ashscope.aigc.conversation.HistoryItem.rst | 2 +- docs/source/develop.md | 14 + .../01_atomic_capabilities/audio/asr_file.py | 2 +- .../audio/asr_http_stream.py | 2 +- .../audio/direct_call/asr_file.py | 2 +- .../audio/direct_call/asr_http_stream.py | 2 +- .../audio/direct_call/realtime_omni.py | 2 +- .../audio/direct_call/realtime_qwen_audio.py | 2 +- .../audio/direct_call/realtime_translation.py | 2 +- .../audio/direct_call/tts_websocket.py | 2 +- .../audio/direct_call/voice_clone.py | 2 +- .../audio/realtime_omni.py | 2 +- .../audio/realtime_qwen_audio.py | 2 +- .../audio/realtime_translation.py | 2 +- .../audio/tts_websocket.py | 2 +- .../audio/voice_clone.py | 2 +- .../decision/decision.py | 2 +- .../circuit_breaker_retry.py | 4 +- .../local_mock_debugging.py | 2 +- examples/03_agent_recipes/agentstudio.py | 5 +- examples/04_real_world_scenarios/README.md | 10 +- examples/04_real_world_scenarios/README_zh.md | 10 +- .../production_ready_rag_harness.py | 4 +- .../realtime_audio_meeting_harness.py | 8 +- .../ticket_triage_harness.py | 4 +- examples/README.md | 2 +- examples/README_zh.md | 2 +- requirements-test.txt | 2 +- setup.py | 8 +- specs/chat.yaml | 6 +- tests/legacy_1x/__init__.py | 1 + .../integration/test_large_utf8_payload.py | 4 +- tests/{ => legacy_1x}/legacy/conftest.py | 2 +- .../{ => legacy_1x}/legacy/test_async_api.py | 2 +- .../{ => legacy_1x}/legacy/test_encryption.py | 0 .../legacy/test_image_synthesis.py | 2 +- .../legacy/test_speech_recognizer.py | 2 +- .../legacy/test_speech_synthesis.py | 2 +- .../legacy/test_speech_transcription.py | 2 +- .../legacy/test_video_synthesis.py | 2 +- .../legacy/test_websocket_async_api.py | 6 +- .../legacy/test_websocket_parameters.py | 6 +- .../legacy/test_websocket_sync_api.py | 6 +- .../legacy_1x/unit}/__init__.py | 0 tests/{ => legacy_1x}/unit/base_test.py | 0 tests/{ => legacy_1x}/unit/conftest.py | 2 +- tests/{ => legacy_1x}/unit/constants.py | 0 .../unit/handle_deployment_request.py | 2 +- .../unit/handle_fine_tune_request.py | 2 +- tests/{ => legacy_1x}/unit/helper.py | 0 .../{ => legacy_1x}/unit/http_task_request.py | 0 .../{ => legacy_1x}/unit/mock_request_base.py | 2 +- tests/{ => legacy_1x}/unit/mock_server.py | 10 +- tests/{ => legacy_1x}/unit/mock_sse.py | 0 .../unit/test_acli_acceptance.py | 0 .../unit/test_acli_command_timeout.py | 0 .../unit/test_acli_command_trust.py | 0 .../unit/test_acli_provider_retries.py | 0 .../unit/test_acli_reflection.py | 0 .../unit/test_acli_sdk_contract.py | 42 +- .../unit/test_acli_skill_render.py | 0 .../unit/test_acli_usage_report.py | 0 .../unit/test_acli_vision_tool.py | 0 .../unit/test_add_resources.py | 6 +- .../unit/test_agentic_rl_cli_opd.py | 4 +- .../unit/test_agentic_rl_components.py | 0 .../unit/test_agentic_rl_model.py | 0 .../unit/test_agentic_rl_submit_job.py | 78 +- .../unit/test_agentic_rl_utils.py | 0 .../unit/test_agentstudio_base_url.py | 0 .../unit/test_agentstudio_deployments.py | 0 .../unit/test_agentstudio_files.py | 0 .../unit/test_agentstudio_memory_stores.py | 0 .../unit/test_agentstudio_pagination.py | 0 .../unit/test_agentstudio_skills.py | 0 .../{ => legacy_1x}/unit/test_aio_session.py | 0 .../unit/test_aio_session_cleanup.py | 0 .../{ => legacy_1x}/unit/test_application.py | 4 +- .../{ => legacy_1x}/unit/test_asr_phrases.py | 4 +- .../unit/test_assistant_files.py | 4 +- tests/{ => legacy_1x}/unit/test_assistants.py | 4 +- .../unit/test_async_custom_session.py | 0 .../{ => legacy_1x}/unit/test_banner_guide.py | 0 .../unit/test_cli_agent_route.py | 0 .../unit/test_cli_application.py | 0 tests/{ => legacy_1x}/unit/test_cli_auth.py | 0 .../unit/test_cli_code_generation.py | 0 tests/{ => legacy_1x}/unit/test_cli_common.py | 0 .../unit/test_cli_embeddings.py | 0 .../unit/test_cli_image_generation.py | 0 .../unit/test_cli_image_synthesis.py | 0 tests/{ => legacy_1x}/unit/test_cli_main.py | 4 +- tests/{ => legacy_1x}/unit/test_cli_models.py | 0 .../unit/test_cli_multimodal_conversation.py | 0 .../unit/test_cli_multimodal_embedding.py | 0 tests/{ => legacy_1x}/unit/test_cli_rerank.py | 0 .../unit/test_cli_speech_synthesis.py | 0 .../unit/test_cli_tokenization.py | 0 .../unit/test_cli_transcription.py | 0 .../unit/test_cli_understanding.py | 0 .../unit/test_cli_video_synthesis.py | 0 .../unit/test_code_generation.py | 4 +- .../{ => legacy_1x}/unit/test_conversation.py | 4 +- .../unit/test_dashscope_response.py | 0 .../unit/test_handlers_key_docs.py | 0 tests/{ => legacy_1x}/unit/test_http_api.py | 6 +- .../unit/test_http_deployments_api.py | 4 +- .../unit/test_http_files_api.py | 2 +- .../unit/test_http_fine_tunes_api.py | 6 +- .../unit/test_http_models_api.py | 4 +- .../unit/test_http_speech_synthesizer.py | 0 .../unit/test_import_deprecation.py | 2 +- .../unit/test_layer_decoupling.py | 142 +-- .../{ => legacy_1x}/unit/test_maas_region.py | 0 tests/{ => legacy_1x}/unit/test_messages.py | 4 +- .../unit/test_multimodal_dialog.py | 2 +- tests/{ => legacy_1x}/unit/test_opd.py | 24 +- tests/{ => legacy_1x}/unit/test_oss_utils.py | 0 .../unit/test_packaging_metadata.py | 2 +- .../unit/test_recognition_explicit_params.py | 0 tests/{ => legacy_1x}/unit/test_rerank.py | 4 +- tests/{ => legacy_1x}/unit/test_runs.py | 4 +- .../{ => legacy_1x}/unit/test_sdk_headers.py | 4 +- tests/{ => legacy_1x}/unit/test_set_region.py | 0 .../unit/test_sketch_image_synthesis.py | 4 +- .../unit/test_speech_synthesis_v2.py | 2 +- .../unit/test_sync_custom_session.py | 0 .../unit/test_text_embedding.py | 2 +- tests/{ => legacy_1x}/unit/test_threads.py | 4 +- .../{ => legacy_1x}/unit/test_tokenization.py | 4 +- .../unit/test_tokenization_explicit_params.py | 0 tests/{ => legacy_1x}/unit/test_tokenizer.py | 94 +- .../unit/test_tongyi_native.py | 0 .../unit/test_translation_recognizer.py | 2 +- ..._translation_recognizer_explicit_params.py | 0 .../unit/test_understanding.py | 4 +- .../unit/test_utf8_encoding.py | 0 .../unit/test_video_synthesis_params.py | 4 +- .../websocket_mock_server_task_handler.py | 0 .../unit/websocket_task_request.py | 0 .../protocols/test_anthropic_conformance.py | 8 +- .../protocols/test_integration_endpoints.py | 6 +- .../protocols/test_openai_conformance.py | 2 +- tests/{v2 => }/smoke/test_examples.py | 2 +- tests/{v2 => }/smoke/test_full_surface.py | 2 +- tests/{v2 => }/test_agentstudio_errors.py | 2 +- tests/{v2 => }/test_agentstudio_models.py | 4 +- tests/{v2 => }/test_agentstudio_protocol.py | 118 +-- tests/{v2 => }/test_agentstudio_shim.py | 72 +- tests/{v2 => }/test_apps.py | 4 +- tests/{v2 => }/test_audio.py | 16 +- tests/{v2 => }/test_audio_connection.py | 4 +- tests/{v2 => }/test_audio_management.py | 4 +- tests/{v2 => }/test_audio_omni_session.py | 8 +- tests/{v2 => }/test_audio_samples.py | 2 +- .../test_audio_samples_direct_call.py | 2 +- tests/{v2 => }/test_audio_tts_http.py | 6 +- tests/{v2 => }/test_audio_tts_realtime.py | 6 +- tests/{v2 => }/test_audio_tts_session.py | 14 +- tests/{v2 => }/test_audio_ws_integration.py | 6 +- tests/{v2 => }/test_base_url_routing.py | 4 +- tests/{v2 => }/test_chat.py | 8 +- tests/{v2 => }/test_chat_params.py | 2 +- tests/{v2 => }/test_config.py | 14 +- tests/{v2 => }/test_decision.py | 10 +- tests/{v2 => }/test_error_registry.py | 12 +- tests/{v2 => }/test_errors.py | 22 +- tests/{v2 => }/test_files_models.py | 6 +- tests/{v2 => }/test_finetune_deployments.py | 6 +- tests/{v2 => }/test_finetune_rl_shim.py | 143 +-- tests/{v2 => }/test_http_pool.py | 4 +- tests/{v2 => }/test_images_videos.py | 4 +- tests/{v2 => }/test_jobs.py | 14 +- tests/{v2 => }/test_logging.py | 46 +- tests/{v2 => }/test_multimodal.py | 2 +- tests/{v2 => }/test_oss_upload.py | 6 +- tests/{v2 => }/test_package_root.py | 81 +- tests/{v2 => }/test_protocols.py | 8 +- tests/{v2 => }/test_rl.py | 8 +- tests/{v2 => }/test_settings.py | 8 +- tests/{v2 => }/test_sse.py | 10 +- .../{v2 => }/test_sse_comprehensive_verify.py | 30 +- tests/{v2 => }/test_tingwu.py | 4 +- tests/{v2 => }/test_tools_resources.py | 4 +- tests/{v2 => }/test_transport.py | 22 +- tests/{v2 => }/test_types_messages.py | 4 +- tests/{v2 => }/test_ws_connection.py | 6 +- tests/unit/__init__.py | 0 408 files changed, 2959 insertions(+), 4194 deletions(-) rename dashscope/{v2 => }/_config.py (99%) rename dashscope/{v2 => }/_error_registry.py (100%) rename dashscope/{v2 => }/_logging.py (99%) rename dashscope/{v2 => }/_quickstart.py (89%) rename dashscope/{v2 => }/agentstudio/client.py (89%) rename dashscope/{v2 => }/agentstudio/constants.py (100%) rename dashscope/{v2 => }/agentstudio/exceptions.py (95%) rename dashscope/{v2 => }/agentstudio/pagination.py (100%) rename dashscope/{v2 => }/agentstudio/resources/__init__.py (72%) rename dashscope/{v2 => }/agentstudio/resources/_helpers.py (99%) rename dashscope/{v2 => }/agentstudio/resources/agents.py (97%) rename dashscope/{v2 => }/agentstudio/resources/deployments.py (98%) rename dashscope/{v2 => }/agentstudio/resources/environments.py (96%) rename dashscope/{v2 => }/agentstudio/resources/files.py (97%) rename dashscope/{v2 => }/agentstudio/resources/memory_stores.py (99%) rename dashscope/{v2 => }/agentstudio/resources/security.py (96%) rename dashscope/{v2 => }/agentstudio/resources/session_events.py (97%) rename dashscope/{v2 => }/agentstudio/resources/session_resources.py (95%) rename dashscope/{v2 => }/agentstudio/resources/session_threads.py (97%) rename dashscope/{v2 => }/agentstudio/resources/sessions.py (95%) rename dashscope/{v2 => }/agentstudio/resources/skills.py (97%) rename dashscope/{v2 => }/agentstudio/resources/vaults.py (98%) rename dashscope/{v2 => }/agentstudio/resources/webhook_endpoints.py (97%) rename dashscope/{v2 => }/agentstudio/streaming.py (99%) rename dashscope/{v2 => }/agentstudio/transport.py (99%) rename dashscope/{v2 => }/agentstudio/types/__init__.py (93%) rename dashscope/{v2 => }/agentstudio/types/models.py (99%) rename dashscope/{v2 => }/agentstudio/types/params.py (99%) delete mode 100644 dashscope/aigc/__init__.py delete mode 100644 dashscope/api_entities/__init__.py delete mode 100644 dashscope/app/__init__.py delete mode 100644 dashscope/assistants/__init__.py delete mode 100644 dashscope/audio/__init__.py delete mode 100644 dashscope/cli/__init__.py rename dashscope/{v2 => }/client.py (90%) delete mode 100644 dashscope/common/__init__.py delete mode 100644 dashscope/embeddings/__init__.py rename dashscope/{v2 => }/errors.py (98%) delete mode 100644 dashscope/files.py rename dashscope/{v2 => }/finetune/_submit.py (99%) rename dashscope/{client => finetune/reinforcement/common}/__init__.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/common/constants.py (95%) rename dashscope/{v2 => }/finetune/reinforcement/common/errors.py (99%) rename dashscope/{v2 => }/finetune/reinforcement/common/log.py (91%) rename dashscope/{v2 => }/finetune/reinforcement/common/model.py (99%) rename dashscope/{v2 => }/finetune/reinforcement/common/model_types.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/common/utils.py (99%) rename dashscope/{v2 => }/finetune/reinforcement/component/__init__.py (74%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/__init__.py (70%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/base_data_model.py (98%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/group_reward_input.py (90%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/group_reward_output.py (90%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/reward_input.py (89%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/reward_output.py (96%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/rollout_input.py (93%) rename dashscope/{v2 => }/finetune/reinforcement/component/data/rollout_output.py (95%) create mode 100644 dashscope/finetune/reinforcement/component/demo/__init__.py rename dashscope/{v2 => }/finetune/reinforcement/component/demo/group_reward_processor_demo.py (85%) rename dashscope/{v2 => }/finetune/reinforcement/component/demo/reward_processor_demo.py (85%) rename dashscope/{v2 => }/finetune/reinforcement/component/demo/rollout_processor_demo.py (85%) rename dashscope/{v2 => }/finetune/reinforcement/component/func_decorator.py (98%) rename dashscope/{v2 => }/finetune/reinforcement/component/func_manager.py (93%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/__init__.py (91%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/__init__.py (79%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/_core.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/llm_dashscope.py (93%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/llm_openai.py (98%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/llm_span.py (95%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/messages.py (98%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/genai/tools.py (99%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/processor_span.py (97%) rename dashscope/{v2 => }/finetune/reinforcement/component/observability/tracing.py (99%) create mode 100644 dashscope/finetune/reinforcement/component/parser/__init__.py rename dashscope/{v2 => }/finetune/reinforcement/component/parser/base_parser.py (90%) rename dashscope/{v2 => }/finetune/reinforcement/component/parser/group_reward_parser.py (81%) rename dashscope/{v2 => }/finetune/reinforcement/component/parser/reward_parser.py (79%) rename dashscope/{v2 => }/finetune/reinforcement/component/parser/rollout_parser.py (79%) create mode 100644 dashscope/finetune/reinforcement/component/processor/__init__.py rename dashscope/{v2 => }/finetune/reinforcement/component/processor/abstract_group_reward_processor.py (90%) rename dashscope/{v2 => }/finetune/reinforcement/component/processor/abstract_processor.py (94%) rename dashscope/{v2 => }/finetune/reinforcement/component/processor/abstract_reward_processor.py (95%) rename dashscope/{v2 => }/finetune/reinforcement/component/processor/abstract_rollout_processor.py (88%) rename dashscope/{threads/messages => finetune/reinforcement/component/server}/__init__.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/component/server/server.py (94%) rename dashscope/{threads/runs => finetune/reinforcement/examples/observability}/__init__.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/observability/smoke_observability.py (92%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/README-zh.md (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/README.md (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/UserGuide-zh.md (98%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/UserGuide.md (98%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/cli.sh (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl (100%) rename dashscope/{v2/finetune/reinforcement/common => finetune/reinforcement/examples/workspace/functions}/__init__.py (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py (95%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/functions/reward/reward.py (90%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py (93%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py (97%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py (98%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/opd-job.yaml (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/requirements.txt (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/resources/group_reward_input.json (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/resources/reward_input.json (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/resources/rollout_input.json (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/rl-job.yaml (100%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/submit_job.py (99%) rename dashscope/{v2 => }/finetune/reinforcement/examples/workspace/test_functions.py (99%) delete mode 100644 dashscope/io/__init__.py rename dashscope/{v2/finetune/reinforcement/component/server => legacy_1x/finetune}/__init__.py (100%) create mode 100644 dashscope/legacy_1x/finetune/agentic_rl.py rename dashscope/{ => legacy_1x}/finetune/customize_types.py (100%) rename dashscope/{ => legacy_1x}/finetune/deployments.py (98%) rename dashscope/{ => legacy_1x}/finetune/finetunes.py (99%) delete mode 100644 dashscope/model.py delete mode 100644 dashscope/models.py delete mode 100644 dashscope/multimodal/__init__.py delete mode 100644 dashscope/nlp/__init__.py delete mode 100644 dashscope/protocol/__init__.py rename dashscope/{v2 => }/protocols/__init__.py (79%) rename dashscope/{v2 => }/protocols/anthropic.py (99%) rename dashscope/{v2 => }/protocols/base.py (98%) rename dashscope/{v2 => }/protocols/native.py (98%) rename dashscope/{v2 => }/protocols/openai.py (98%) delete mode 100644 dashscope/reinforcement/__init__.py delete mode 100644 dashscope/rerank/__init__.py rename dashscope/{v2 => }/resources/__init__.py (100%) rename dashscope/{v2 => }/resources/_jobs.py (99%) rename dashscope/{v2 => }/resources/_oss.py (98%) rename dashscope/{v2 => }/resources/_oss_upload.py (97%) rename dashscope/{v2 => }/resources/_sse.py (98%) rename dashscope/{v2 => }/resources/apps.py (99%) rename dashscope/{v2 => }/resources/audio/__init__.py (86%) rename dashscope/{v2 => }/resources/audio/_callbacks.py (97%) rename dashscope/{v2 => }/resources/audio/asr.py (98%) rename dashscope/{v2 => }/resources/audio/customization.py (99%) rename dashscope/{v2 => }/resources/audio/http_tts.py (98%) rename dashscope/{v2 => }/resources/audio/phrases.py (99%) rename dashscope/{v2 => }/resources/audio/realtime.py (99%) rename dashscope/{v2 => }/resources/audio/tts.py (98%) rename dashscope/{v2 => }/resources/chat/__init__.py (82%) rename dashscope/{v2 => }/resources/chat/_params.py (100%) rename dashscope/{v2 => }/resources/chat/completions.py (98%) rename dashscope/{v2 => }/resources/decision.py (98%) rename dashscope/{v2 => }/resources/deployments.py (99%) rename dashscope/{v2 => }/resources/embeddings.py (98%) rename dashscope/{v2 => }/resources/files.py (99%) rename dashscope/{v2 => }/resources/fine_tunes.py (99%) rename dashscope/{v2 => }/resources/images.py (99%) rename dashscope/{v2 => }/resources/models.py (98%) rename dashscope/{v2 => }/resources/multimodal/__init__.py (85%) rename dashscope/{v2 => }/resources/multimodal/chat.py (98%) rename dashscope/{v2 => }/resources/multimodal/embeddings.py (97%) rename dashscope/{v2 => }/resources/reinforcement_learning.py (98%) rename dashscope/{v2 => }/resources/rerank.py (98%) rename dashscope/{v2 => }/resources/tingwu.py (97%) rename dashscope/{v2 => }/resources/tokenization.py (95%) rename dashscope/{v2 => }/resources/videos.py (99%) delete mode 100644 dashscope/threads/__init__.py rename dashscope/{v2 => }/tokenizers/qwen.tiktoken (100%) rename dashscope/{v2 => }/transport/__init__.py (86%) rename dashscope/{v2 => }/transport/_ident.py (100%) rename dashscope/{v2 => }/transport/_retry.py (100%) rename dashscope/{v2 => }/transport/http.py (97%) rename dashscope/{v2 => }/transport/sse.py (98%) rename dashscope/{v2 => }/transport/ws.py (98%) rename dashscope/{v2 => }/transport/ws_connection.py (99%) rename dashscope/{v2 => }/types/__init__.py (84%) rename dashscope/{v2 => }/types/common.py (98%) rename dashscope/{v2 => }/types/completions.py (97%) rename dashscope/{v2 => }/types/messages.py (100%) delete mode 100644 dashscope/utils/__init__.py delete mode 100644 dashscope/v2/__init__.py delete mode 100644 dashscope/v2/agentstudio/__init__.py delete mode 100644 dashscope/v2/finetune/__init__.py delete mode 100644 dashscope/v2/finetune/agentic_rl.py delete mode 100644 dashscope/v2/finetune/reinforcement/__init__.py delete mode 100644 dashscope/v2/finetune/reinforcement/component/demo/__init__.py delete mode 100644 dashscope/v2/finetune/reinforcement/component/parser/__init__.py delete mode 100644 dashscope/v2/finetune/reinforcement/component/processor/__init__.py delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh delete mode 100755 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh delete mode 100755 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh delete mode 100755 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh delete mode 100644 dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh delete mode 100644 dashscope/v2/tokenizers/__init__.py delete mode 100644 dashscope/v2/tokenizers/qwen_tokenizer.py delete mode 100644 dashscope/v2/tokenizers/tokenizer.py delete mode 100644 dashscope/v2/tokenizers/tokenizer_base.py create mode 100644 tests/legacy_1x/__init__.py rename tests/{ => legacy_1x}/integration/test_large_utf8_payload.py (97%) rename tests/{ => legacy_1x}/legacy/conftest.py (95%) rename tests/{ => legacy_1x}/legacy/test_async_api.py (98%) rename tests/{ => legacy_1x}/legacy/test_encryption.py (100%) rename tests/{ => legacy_1x}/legacy/test_image_synthesis.py (98%) rename tests/{ => legacy_1x}/legacy/test_speech_recognizer.py (99%) rename tests/{ => legacy_1x}/legacy/test_speech_synthesis.py (99%) rename tests/{ => legacy_1x}/legacy/test_speech_transcription.py (99%) rename tests/{ => legacy_1x}/legacy/test_video_synthesis.py (97%) rename tests/{ => legacy_1x}/legacy/test_websocket_async_api.py (98%) rename tests/{ => legacy_1x}/legacy/test_websocket_parameters.py (94%) rename tests/{ => legacy_1x}/legacy/test_websocket_sync_api.py (98%) rename {dashscope/v2/finetune/reinforcement/examples/observability => tests/legacy_1x/unit}/__init__.py (100%) rename tests/{ => legacy_1x}/unit/base_test.py (100%) rename tests/{ => legacy_1x}/unit/conftest.py (96%) rename tests/{ => legacy_1x}/unit/constants.py (100%) rename tests/{ => legacy_1x}/unit/handle_deployment_request.py (98%) rename tests/{ => legacy_1x}/unit/handle_fine_tune_request.py (98%) rename tests/{ => legacy_1x}/unit/helper.py (100%) rename tests/{ => legacy_1x}/unit/http_task_request.py (100%) rename tests/{ => legacy_1x}/unit/mock_request_base.py (92%) rename tests/{ => legacy_1x}/unit/mock_server.py (99%) rename tests/{ => legacy_1x}/unit/mock_sse.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_acceptance.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_command_timeout.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_command_trust.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_provider_retries.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_reflection.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_sdk_contract.py (92%) rename tests/{ => legacy_1x}/unit/test_acli_skill_render.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_usage_report.py (100%) rename tests/{ => legacy_1x}/unit/test_acli_vision_tool.py (100%) rename tests/{ => legacy_1x}/unit/test_add_resources.py (96%) rename tests/{ => legacy_1x}/unit/test_agentic_rl_cli_opd.py (94%) rename tests/{ => legacy_1x}/unit/test_agentic_rl_components.py (100%) rename tests/{ => legacy_1x}/unit/test_agentic_rl_model.py (100%) rename tests/{ => legacy_1x}/unit/test_agentic_rl_submit_job.py (91%) rename tests/{ => legacy_1x}/unit/test_agentic_rl_utils.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_base_url.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_deployments.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_files.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_memory_stores.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_pagination.py (100%) rename tests/{ => legacy_1x}/unit/test_agentstudio_skills.py (100%) rename tests/{ => legacy_1x}/unit/test_aio_session.py (100%) rename tests/{ => legacy_1x}/unit/test_aio_session_cleanup.py (100%) rename tests/{ => legacy_1x}/unit/test_application.py (98%) rename tests/{ => legacy_1x}/unit/test_asr_phrases.py (98%) rename tests/{ => legacy_1x}/unit/test_assistant_files.py (96%) rename tests/{ => legacy_1x}/unit/test_assistants.py (98%) rename tests/{ => legacy_1x}/unit/test_async_custom_session.py (100%) rename tests/{ => legacy_1x}/unit/test_banner_guide.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_agent_route.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_application.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_auth.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_code_generation.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_common.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_embeddings.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_image_generation.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_image_synthesis.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_main.py (99%) rename tests/{ => legacy_1x}/unit/test_cli_models.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_multimodal_conversation.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_multimodal_embedding.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_rerank.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_speech_synthesis.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_tokenization.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_transcription.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_understanding.py (100%) rename tests/{ => legacy_1x}/unit/test_cli_video_synthesis.py (100%) rename tests/{ => legacy_1x}/unit/test_code_generation.py (99%) rename tests/{ => legacy_1x}/unit/test_conversation.py (98%) rename tests/{ => legacy_1x}/unit/test_dashscope_response.py (100%) rename tests/{ => legacy_1x}/unit/test_handlers_key_docs.py (100%) rename tests/{ => legacy_1x}/unit/test_http_api.py (95%) rename tests/{ => legacy_1x}/unit/test_http_deployments_api.py (93%) rename tests/{ => legacy_1x}/unit/test_http_files_api.py (95%) rename tests/{ => legacy_1x}/unit/test_http_fine_tunes_api.py (97%) rename tests/{ => legacy_1x}/unit/test_http_models_api.py (82%) rename tests/{ => legacy_1x}/unit/test_http_speech_synthesizer.py (100%) rename tests/{ => legacy_1x}/unit/test_import_deprecation.py (98%) rename tests/{ => legacy_1x}/unit/test_layer_decoupling.py (69%) rename tests/{ => legacy_1x}/unit/test_maas_region.py (100%) rename tests/{ => legacy_1x}/unit/test_messages.py (97%) rename tests/{ => legacy_1x}/unit/test_multimodal_dialog.py (98%) rename tests/{ => legacy_1x}/unit/test_opd.py (91%) rename tests/{ => legacy_1x}/unit/test_oss_utils.py (100%) rename tests/{ => legacy_1x}/unit/test_packaging_metadata.py (99%) rename tests/{ => legacy_1x}/unit/test_recognition_explicit_params.py (100%) rename tests/{ => legacy_1x}/unit/test_rerank.py (97%) rename tests/{ => legacy_1x}/unit/test_runs.py (99%) rename tests/{ => legacy_1x}/unit/test_sdk_headers.py (99%) rename tests/{ => legacy_1x}/unit/test_set_region.py (100%) rename tests/{ => legacy_1x}/unit/test_sketch_image_synthesis.py (95%) rename tests/{ => legacy_1x}/unit/test_speech_synthesis_v2.py (99%) rename tests/{ => legacy_1x}/unit/test_sync_custom_session.py (100%) rename tests/{ => legacy_1x}/unit/test_text_embedding.py (94%) rename tests/{ => legacy_1x}/unit/test_threads.py (96%) rename tests/{ => legacy_1x}/unit/test_tokenization.py (92%) rename tests/{ => legacy_1x}/unit/test_tokenization_explicit_params.py (100%) rename tests/{ => legacy_1x}/unit/test_tokenizer.py (55%) rename tests/{ => legacy_1x}/unit/test_tongyi_native.py (100%) rename tests/{ => legacy_1x}/unit/test_translation_recognizer.py (98%) rename tests/{ => legacy_1x}/unit/test_translation_recognizer_explicit_params.py (100%) rename tests/{ => legacy_1x}/unit/test_understanding.py (93%) rename tests/{ => legacy_1x}/unit/test_utf8_encoding.py (100%) rename tests/{ => legacy_1x}/unit/test_video_synthesis_params.py (98%) rename tests/{ => legacy_1x}/unit/websocket_mock_server_task_handler.py (100%) rename tests/{ => legacy_1x}/unit/websocket_task_request.py (100%) rename tests/{v2 => }/protocols/test_anthropic_conformance.py (97%) rename tests/{v2 => }/protocols/test_integration_endpoints.py (95%) rename tests/{v2 => }/protocols/test_openai_conformance.py (98%) rename tests/{v2 => }/smoke/test_examples.py (97%) rename tests/{v2 => }/smoke/test_full_surface.py (99%) rename tests/{v2 => }/test_agentstudio_errors.py (99%) rename tests/{v2 => }/test_agentstudio_models.py (99%) rename tests/{v2 => }/test_agentstudio_protocol.py (92%) rename tests/{v2 => }/test_agentstudio_shim.py (74%) rename tests/{v2 => }/test_apps.py (99%) rename tests/{v2 => }/test_audio.py (99%) rename tests/{v2 => }/test_audio_connection.py (99%) rename tests/{v2 => }/test_audio_management.py (99%) rename tests/{v2 => }/test_audio_omni_session.py (99%) rename tests/{v2 => }/test_audio_samples.py (99%) rename tests/{v2 => }/test_audio_samples_direct_call.py (99%) rename tests/{v2 => }/test_audio_tts_http.py (99%) rename tests/{v2 => }/test_audio_tts_realtime.py (99%) rename tests/{v2 => }/test_audio_tts_session.py (98%) rename tests/{v2 => }/test_audio_ws_integration.py (99%) rename tests/{v2 => }/test_base_url_routing.py (98%) rename tests/{v2 => }/test_chat.py (99%) rename tests/{v2 => }/test_chat_params.py (96%) rename tests/{v2 => }/test_config.py (98%) rename tests/{v2 => }/test_decision.py (98%) rename tests/{v2 => }/test_error_registry.py (96%) rename tests/{v2 => }/test_errors.py (98%) rename tests/{v2 => }/test_files_models.py (98%) rename tests/{v2 => }/test_finetune_deployments.py (99%) rename tests/{v2 => }/test_finetune_rl_shim.py (72%) rename tests/{v2 => }/test_http_pool.py (97%) rename tests/{v2 => }/test_images_videos.py (99%) rename tests/{v2 => }/test_jobs.py (98%) rename tests/{v2 => }/test_logging.py (79%) rename tests/{v2 => }/test_multimodal.py (99%) rename tests/{v2 => }/test_oss_upload.py (98%) rename tests/{v2 => }/test_package_root.py (58%) rename tests/{v2 => }/test_protocols.py (99%) rename tests/{v2 => }/test_rl.py (99%) rename tests/{v2 => }/test_settings.py (97%) rename tests/{v2 => }/test_sse.py (98%) rename tests/{v2 => }/test_sse_comprehensive_verify.py (93%) rename tests/{v2 => }/test_tingwu.py (99%) rename tests/{v2 => }/test_tools_resources.py (99%) rename tests/{v2 => }/test_transport.py (97%) rename tests/{v2 => }/test_types_messages.py (99%) rename tests/{v2 => }/test_ws_connection.py (99%) delete mode 100644 tests/unit/__init__.py diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index e207745c..63823931 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -9,7 +9,7 @@ on: - 'main' jobs: - test: + current: if: ${{ !(github.event_name == 'pull_request' && startsWith(github.event.pull_request.title, 'docs:')) }} runs-on: ${{ matrix.os }} @@ -18,7 +18,6 @@ jobs: os: [ ubuntu-latest ] python-version: [ '3.9', '3.12', '3.13', '3.14' ] - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -55,10 +54,55 @@ jobs: run: | python -m pip list --format=columns - - name: Run tests with coverage + - name: Run current tests with coverage run: | - coverage run -m pytest tests/unit + coverage run -m pytest tests --ignore=tests/legacy_1x - name: Generate coverage report run: | coverage report -m + + legacy: + if: ${{ !(github.event_name == 'pull_request' && startsWith(github.event.pull_request.title, 'docs:')) }} + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: [ '3.9', '3.12', '3.13', '3.14' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Update setuptools + run: | + pip install --upgrade pip + pip install setuptools==68.2.2 wheel==0.41.2 + + - name: Set PYTHONPATH + run: | + echo "PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/src" >> $GITHUB_ENV + + - name: Install dependencies + run: | + export PIP_DEFAULT_TIMEOUT=300 + pip install -r requirements.txt + pip install -r requirements-test.txt + + - name: Run archived legacy tests + run: | + python -m pytest tests/legacy_1x diff --git a/.gitignore b/.gitignore index c8a620a1..0fbbae98 100644 --- a/.gitignore +++ b/.gitignore @@ -144,5 +144,5 @@ cython_debug/ # Local design/analysis docs — kept out of the public repo /md/ -# Local PyPI investigation tooling; the CI guard lives in tests/unit/. +# Local PyPI investigation tooling; the CI guard lives in tests/legacy_1x/unit/. /scripts/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index efb06421..bebb2bb5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,36 +3,36 @@ repos: rev: v4.3.0 hooks: - id: check-ast - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: sort-simple-yaml - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: check-yaml exclude: | (?x)^( meta.yaml - | tests/legacy/ + | tests/legacy_1x/legacy/ )$ - id: check-xml - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: check-toml - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: check-docstring-first - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: check-json - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: fix-encoding-pragma - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: detect-private-key - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - id: trailing-whitespace - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: - id: add-trailing-comma # 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)/ + exclude: ^(tests/legacy_1x/legacy|examples/01_atomic_capabilities/audio)/ - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.7.0 hooks: @@ -45,7 +45,7 @@ repos: pb2\.py$ | grpc\.py$ | ^docs - | ^tests/legacy/ + | ^tests/legacy_1x/legacy/ | ^examples/01_atomic_capabilities/audio/ | \.html$ | \.pyi$ @@ -67,13 +67,13 @@ repos: hooks: - id: black args: [ --line-length=79 ] - exclude: ^(tests/legacy|examples/01_atomic_capabilities/audio)/ + exclude: ^(tests/legacy_1x/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|examples/01_atomic_capabilities/audio)/ + exclude: ^(tests/legacy_1x/legacy|examples/01_atomic_capabilities/audio)/ - repo: https://github.com/pylint-dev/pylint rev: v3.0.2 hooks: @@ -122,6 +122,7 @@ repos: --disable=C0114, # Disable missing module docstring for quick dev --disable=C0115, # Disable missing class docstring for quick dev --disable=C0116, # Disable missing function or method docstring for quick dev + --disable=E0603, # Absolute self-imports and lazy exports in reinforcement ] # Tests get their own entry (pylint 3.0.2 discovers rcfiles from the # invocation CWD, not per linted-file directory, so a tests/pylintrc @@ -132,7 +133,7 @@ repos: - id: pylint name: pylint (tests) files: ^tests/ - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ args: [ --disable=W0511, --disable=W0718, @@ -190,14 +191,14 @@ repos: hooks: - id: eslint files: \.(js|jsx)$ - exclude: '(.*js_third_party.*|^tests/legacy/)' + exclude: '(.*js_third_party.*|^tests/legacy_1x/legacy/)' args: [ '--fix' ] - repo: https://github.com/thibaudcolas/pre-commit-stylelint rev: v14.4.0 hooks: - id: stylelint files: \.(css)$ - exclude: '(.*css_third_party.*|^tests/legacy/)' + exclude: '(.*css_third_party.*|^tests/legacy_1x/legacy/)' args: [ '--fix' ] - repo: https://github.com/pre-commit/mirrors-prettier rev: 'v3.0.0' @@ -205,4 +206,4 @@ repos: - id: prettier additional_dependencies: [ 'prettier@3.0.0' ] files: \.(tsx?)$ - exclude: ^tests/legacy/ + exclude: ^tests/legacy_1x/legacy/ diff --git a/MANIFEST.in b/MANIFEST.in index cbf2d6b6..8cedf45a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include dashscope/v2/tokenizers/qwen.tiktoken +include dashscope/tokenizers/qwen.tiktoken include dashscope/py.typed include dashscope/__init__.pyi include NOTICE diff --git a/README.md b/README.md index 4a6f7b84..a0481f4e 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ By default each frame is incremental. Pass `incremental_output=False` to receive Failures raise a typed `DashScopeError` by default (both non-2xx HTTP statuses and HTTP-200-with-a-business-error land here): ```python -from dashscope.v2 import errors +from dashscope import errors try: client.chat.completions.create(model="qwen-plus", messages=["hi"]) @@ -447,6 +447,32 @@ The 1.x module-level API is unchanged and still supported — you can adopt 2.0 | `dashscope.set_region(region, workspace_id)` | `dashscope.Client(region=..., workspace=...)` | | OpenAI-compatible via `dashscope.aigc.chat_completion.Completions` | `dashscope.Client(protocol="openai")` | +### Package layout and compatibility + +The 2.0 implementation is directly under `dashscope`; the former +`dashscope.v2` namespace has been removed. Existing root 1.x exports remain +available. Old deep modules such as `dashscope.aigc` and `dashscope.client` +have moved under `dashscope.legacy_1x`. The 2.0 Agentic RL workflow lives in +`dashscope.finetune`; its 1.x job-management surface is under +`dashscope.legacy_1x.finetune`. + +`dashscope.save_api_key` now comes from `dashscope._config`. It accepts an +optional key-file path, expands `~`, creates missing parent directories, and +writes UTF-8. Logs from the current client use the independent +`dashscope._logging` logger hierarchy and do not reconfigure the shared +`dashscope` root logger. + +### Development tests + +```bash +python -m pytest tests --ignore=tests/legacy_1x -q # current +python -m pytest tests/legacy_1x -q # archived compatibility +python -m pytest tests -q # everything +``` + +For current coverage, run `coverage run -m pytest tests +--ignore=tests/legacy_1x`, then `coverage report -m`. + ## License This project is licensed under the Apache License (Version 2.0). diff --git a/README_zh.md b/README_zh.md index eb6ad822..a1529e69 100644 --- a/README_zh.md +++ b/README_zh.md @@ -194,7 +194,7 @@ for text in stream.text_stream: # 内容增量 失败默认抛出类型化的 `DashScopeError`(非 2xx 的 HTTP 状态码,以及 HTTP 200 但业务报错,都会归入这里): ```python -from dashscope.v2 import errors +from dashscope import errors try: client.chat.completions.create(model="qwen-plus", messages=["hi"]) @@ -447,6 +447,29 @@ examples/ | `dashscope.set_region(region, workspace_id)` | `dashscope.Client(region=..., workspace=...)` | | OpenAI 兼容通过 `dashscope.aigc.chat_completion.Completions` | `dashscope.Client(protocol="openai")` | +### 包布局与兼容策略 + +2.0 实现直接位于 `dashscope` 根包,原 `dashscope.v2` 命名空间已移除。 +包根已有的 1.x 导出继续可用;`dashscope.aigc`、`dashscope.client` 等旧 +deep import 已移至 `dashscope.legacy_1x` 下。2.0 Agentic RL 工作流位于 +`dashscope.finetune`,1.x 任务管理接口位于 `dashscope.legacy_1x.finetune`。 + +`dashscope.save_api_key` 现在来自 `dashscope._config`,仍接受可选的 key +文件路径,并会展开 `~`、创建缺失的父目录、按 UTF-8 写入。当前客户端使用 +独立的 `dashscope._logging` logger 层级,不会重新配置共享的 `dashscope` +根 logger。 + +### 开发测试 + +```bash +python -m pytest tests --ignore=tests/legacy_1x -q # 当前实现 +python -m pytest tests/legacy_1x -q # 归档兼容套件 +python -m pytest tests -q # 全量 +``` + +当前实现的 coverage 使用 `coverage run -m pytest tests +--ignore=tests/legacy_1x`,随后执行 `coverage report -m`。 + ## 许可证 本项目采用 Apache License (Version 2.0) 许可证。 diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 4b174aa5..c084ffd4 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -41,7 +41,6 @@ from dashscope.legacy_1x.api_entities.http_request import ( close_shared_sync_session, ) -from dashscope.legacy_1x.common.api_key import save_api_key from dashscope.legacy_1x.common.env import ( MAAS_REGIONS, api_key, @@ -51,8 +50,8 @@ base_websocket_api_url, validate_workspace_id, ) -from dashscope.finetune.deployments import Deployments -from dashscope.finetune.finetunes import FineTunes +from dashscope.legacy_1x.finetune.deployments import Deployments +from dashscope.legacy_1x.finetune.finetunes import FineTunes from dashscope.legacy_1x.embeddings.batch_text_embedding import ( BatchTextEmbedding, ) @@ -78,8 +77,13 @@ list_tokenizers, ) -# dashscope 2.0 public surface (implementation lives in dashscope/v2/). -from dashscope.v2 import AsyncClient, Client, WsConnection, chat, settings +# dashscope 2.0 public surface. +from dashscope import errors +from dashscope._config import Config, Settings, save_api_key, settings +from dashscope._quickstart import chat +from dashscope.client import AsyncClient, Client +from dashscope.errors import DashScopeError, ErrorCode +from dashscope.transport.ws_connection import WsConnection, WsTask # cn-beijing defaults to the legacy dashscope.aliyuncs.com endpoints; # its MaaS URLs are only reachable via an explicit set_region() call. @@ -229,8 +233,14 @@ def __dir__(): # dashscope 2.0 "AsyncClient", "Client", + "Config", + "DashScopeError", + "ErrorCode", + "Settings", "WsConnection", + "WsTask", "chat", + "errors", "set_region", ] diff --git a/dashscope/__init__.pyi b/dashscope/__init__.pyi index 538a6e22..cb36deef 100644 --- a/dashscope/__init__.pyi +++ b/dashscope/__init__.pyi @@ -14,11 +14,18 @@ from typing import ( # --------------------------------------------------------------------------- # 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 +from dashscope import errors as errors +from dashscope._config import Config as Config +from dashscope._config import Settings as Settings +from dashscope._config import save_api_key as save_api_key +from dashscope._config import settings as settings +from dashscope._quickstart import chat as chat +from dashscope.client import AsyncClient as AsyncClient +from dashscope.client import Client as Client +from dashscope.errors import DashScopeError as DashScopeError +from dashscope.errors import ErrorCode as ErrorCode +from dashscope.transport.ws_connection import WsConnection as WsConnection +from dashscope.transport.ws_connection import WsTask as WsTask # --------------------------------------------------------------------------- # Config @@ -29,7 +36,9 @@ base_compatible_api_url: str base_http_api_url: str base_websocket_api_url: str -def save_api_key(api_key: str, file_path: Optional[str] = ...) -> None: ... +def save_api_key( + api_key: str, api_key_file_path: Optional[str] = ... +) -> None: ... def close_shared_aio_session() -> None: ... def close_shared_sync_session() -> None: ... def set_region(region: str, workspace_id: Optional[str] = ...) -> None: ... diff --git a/dashscope/v2/_config.py b/dashscope/_config.py similarity index 99% rename from dashscope/v2/_config.py rename to dashscope/_config.py index a727faeb..dbec16c6 100644 --- a/dashscope/v2/_config.py +++ b/dashscope/_config.py @@ -321,7 +321,7 @@ def clear(self) -> None: self._explicit.clear() if had_log_level: # pylint: disable=import-outside-toplevel - from dashscope.v2._logging import reset_log_level + from dashscope._logging import reset_log_level reset_log_level() @@ -496,7 +496,7 @@ def log_level(self) -> Optional[str]: @log_level.setter def log_level(self, value: str) -> None: # pylint: disable=import-outside-toplevel - from dashscope.v2._logging import set_log_level + from dashscope._logging import set_log_level set_log_level(value) with self._lock: diff --git a/dashscope/v2/_error_registry.py b/dashscope/_error_registry.py similarity index 100% rename from dashscope/v2/_error_registry.py rename to dashscope/_error_registry.py diff --git a/dashscope/v2/_logging.py b/dashscope/_logging.py similarity index 99% rename from dashscope/v2/_logging.py rename to dashscope/_logging.py index bfb39d54..1ecfc2e8 100644 --- a/dashscope/v2/_logging.py +++ b/dashscope/_logging.py @@ -15,7 +15,7 @@ from typing import Any, Mapping, Optional LOG_ENV = "DASHSCOPE_LOG" -ROOT_NAME = "dashscope.v2" +ROOT_NAME = "dashscope._logging" _LEVELS = { "debug": logging.DEBUG, diff --git a/dashscope/v2/_quickstart.py b/dashscope/_quickstart.py similarity index 89% rename from dashscope/v2/_quickstart.py rename to dashscope/_quickstart.py index 78f46647..602adcda 100644 --- a/dashscope/v2/_quickstart.py +++ b/dashscope/_quickstart.py @@ -7,8 +7,8 @@ import threading from typing import Any, Optional -from dashscope.v2.client import Client -from dashscope.v2.types.completions import ChatCompletion +from dashscope.client import Client +from dashscope.types.completions import ChatCompletion __all__ = ["chat"] diff --git a/dashscope/_shared/_sdk_ident.py b/dashscope/_shared/_sdk_ident.py index 45cf03b9..83fa7de5 100644 --- a/dashscope/_shared/_sdk_ident.py +++ b/dashscope/_shared/_sdk_ident.py @@ -14,7 +14,7 @@ truth, same reason as the session id in ``_shared._sdk_session``. The 2.0 transport builds its own header in -``dashscope.v2.transport._ident`` because it appends a trailing +``dashscope.transport._ident`` because it appends a trailing generation marker (``aigc`` -> ``aigc2``) that this unmarked 1.x shape must not carry. Sub-SDKs that predate the marker -- AgentStudio, whose ``agentstudio`` tag the backend already reports on -- keep using this @@ -25,7 +25,7 @@ ``get_sdk_headers`` on the version segment. It is the *1.x shape* (``dashscope/; python/...; platform/...; processor/...``) and is deliberately not shared with the 2.0 transport's own -``dashscope.v2.transport.http.default_user_agent()`` +``dashscope.transport.http.default_user_agent()`` (``dashscope-v2-python/``). Those are two distinct backend contracts, not two spellings of one value, so they must not be merged. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md index d10a6277..20aa30ff 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md @@ -29,7 +29,7 @@ This file is authoritative for answering `dashscope` CLI usage questions (based | multimodal-embedding | create | Multimodal embeddings (text/image/audio) | dashscope.MultiModalEmbedding | | transcription | create | Audio file transcription (async task) | dashscope.Transcription | | speech-synthesis | create | Speech synthesis TTS | dashscope.HttpSpeechSynthesizer | -| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | dashscope.finetune.agentic_rl.AgenticRL | +| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | dashscope.legacy_1x.finetune.agentic_rl.AgenticRL | Note: code-generation and understanding still exist in the source but are deprecated/discontinued and are not registered in the CLI. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md index 0495b090..d1ef55f0 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md @@ -13,7 +13,7 @@ description: Quick reference for fine-tuning and deployment (FineTunes/Files/Dep | --- | --- | --- | | SFT supervised fine-tuning | `FineTunes.call(..., mode="sft")` | `mode` is written into the request body as `training_type`; the source docstring lists values `sft` / `efficient_sft` | | CPT continued pre-training / DPO preference optimization | `FineTunes.call` | The SDK does not enumerate CPT/DPO literals; pass them through `mode`/`hyper_parameters` per the server-side docs | -| AgenticRL reinforcement fine-tuning (GSPO) | `dashscope.finetune.agentic_rl.AgenticRL` | `training_type="reinforcement"`, `hyper_parameters["algorithm"]="gspo"` | +| AgenticRL reinforcement fine-tuning (GSPO) | `dashscope.legacy_1x.finetune.agentic_rl.AgenticRL` | `training_type="reinforcement"`, `hyper_parameters["algorithm"]="gspo"` | ## Standard Workflow (upload file → create fine-tune job → poll status → deploy → invoke) diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md index 0fcc0482..34cfa831 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md @@ -75,8 +75,8 @@ The Python SDK repo ships runnable cookbook scripts under `examples/`, organized In the Python SDK repo the 2.0 API is isolated by directory. Only these paths are V2: -- `dashscope/v2/` — the 2.0 client and resources -- `tests/v2/` — V2 tests +- `dashscope/` — the 2.0 client and resources at the package root +- `tests/` — current tests; transitional suites live in `tests/legacy_1x/` - `examples/v2/` — V2 cookbook Everything else (`dashscope/` top-level modules, the rest of `tests/`, `examples/v1/`) is v1.x code. Match the tree to the user's API generation before reading source or quoting examples: resource-style `dashscope.Client` calls → V2 paths; module-level `dashscope.Generation.call(...)` style → v1.x paths. diff --git a/dashscope/agentstudio/__init__.py b/dashscope/agentstudio/__init__.py index 5152e1fc..edd58186 100644 --- a/dashscope/agentstudio/__init__.py +++ b/dashscope/agentstudio/__init__.py @@ -1,58 +1,164 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility shim; the implementation moved to ``dashscope.v2.agentstudio``. +"""AgentStudio sub-SDK for the dashscope Python client. -The AgentStudio sub-SDK is owned by the 2.0 package. This module keeps the -historical import path working: every public name is re-exported, and every -submodule is aliased in ``sys.modules`` so deep imports such as -``from dashscope.agentstudio.resources.agents import Agents`` resolve to the -very same module object -- and therefore the very same classes -- as the -``dashscope.v2.agentstudio`` path. +The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP +API. All resource classes are accessed through a client instance:: -Import from ``dashscope.v2.agentstudio`` in new code. -""" - -import pkgutil -import sys + from dashscope.agentstudio import Client + from dashscope.agentstudio.types import ( + user_message, user_tool_result, + ) -# Importing the package runs its own eager imports, so every submodule is -# already in sys.modules by the time the walk below starts. -from dashscope.v2 import agentstudio as _impl +Example +------- +>>> from dashscope.agentstudio import Client +>>> from dashscope.agentstudio.types import user_message +>>> client = Client(api_key="sk-xxx") +>>> agent = client.agents.create(name="demo", model="qwen-plus") +>>> session = client.sessions.create(agent=agent.id) +>>> client.sessions.events.send(session.id, [user_message("Hello!")]) +>>> with client.sessions.events.stream(session.id) as stream: +... for event in stream: +... print(event.type, event.to_dict()) +... if event.type == "session_status": +... break +""" -# pylint: disable-next=wildcard-import,unused-wildcard-import -from dashscope.v2.agentstudio import * # noqa: F401,F403 +# yapf: disable -# The wildcard only carries the names in the implementation's __all__, which -# lists classes and helpers but not its submodules. Naming them here keeps -# ``from dashscope.agentstudio import exceptions`` resolvable statically; at -# runtime they are already imported by the package's own eager imports. -# pylint: disable-next=unused-import -from dashscope.v2.agentstudio import ( # noqa: F401 - client, - constants, - exceptions, - pagination, - resources, - streaming, - transport, - types, +from .client import Client, AsyncClient +from .transport import APIResponse, RequestOptions +from . import types +from .exceptions import ( + AgentStudioError, + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + ConflictError, + InternalServerError, + InvalidRequestError, + NotFoundError, + OverloadedError, + PermissionDeniedError, + RateLimitError, + StreamClosedError, + StreamError, +) +from .pagination import ( + AsyncCursorPage, + CursorPage, +) +from .types import ( + Memory, + MemoryStore, + MemoryVersion, + Message, + PermissionPolicy, + SecurityAlert, + SecurityAlertList, + SecurityOverview, + ServerEvent, + SessionResource, + StopReason, + user_define_outcome, + user_interrupt, + user_message, + user_tool_approval_response, + user_custom_tool_result, + user_tool_result, +) +from .types.params import ( + AgentCreateParams, + AgentUpdateParams, + SessionCreateParams, + SessionUpdateParams, + EnvironmentCreateParams, + EnvironmentUpdateParams, + DeploymentCreateParams, + DeploymentUpdateParams, + SessionEventSendParams, + WebhookEndpointCreateParams, + WebhookEndpointUpdateParams, + WebhookEventListParams, +) +from .constants import ( + PermissionPolicyType, + SSEEventType, + SessionStatus, + StopReasonType, + WebhookDeliveryStatus, + WebhookDisabledReason, + WebhookEventType, + WebhookStatus, ) -_V2_PACKAGE = _impl.__name__ - -__all__ = list(_impl.__all__) - -# Alias each submodule under the old dotted path. Registering in -# sys.modules alone is not enough: the import system skips a module it -# finds there, so the attribute on the parent would never be set and -# ``dashscope.agentstudio.resources`` would raise AttributeError. -for _info in pkgutil.walk_packages(_impl.__path__, prefix=_V2_PACKAGE + "."): - _module = sys.modules.get(_info.name) - if _module is None: # pragma: no cover - walk_packages imports as it goes - continue - _alias = __name__ + _info.name[len(_V2_PACKAGE) :] - sys.modules.setdefault(_alias, _module) - _parent_name, _, _leaf = _alias.rpartition(".") - _parent = sys.modules.get(_parent_name) - if _parent is not None: - setattr(_parent, _leaf, _module) +__all__ = [ + # client + "Client", + "AsyncClient", + # base types + "RequestOptions", + "APIResponse", + # exceptions + "AgentStudioError", + "APIConnectionError", + "APIStatusError", + "APITimeoutError", + "AuthenticationError", + "ConflictError", + "InternalServerError", + "InvalidRequestError", + "NotFoundError", + "OverloadedError", + "PermissionDeniedError", + "RateLimitError", + "StreamError", + "StreamClosedError", + # pagination + "CursorPage", + "AsyncCursorPage", + # unified message type + "Memory", + "MemoryStore", + "MemoryVersion", + "Message", + "PermissionPolicy", + "SecurityAlert", + "SecurityAlertList", + "SecurityOverview", + "ServerEvent", + "SessionResource", + "StopReason", + # client event helpers (re-exported for convenience) + "user_message", + "user_interrupt", + "user_tool_approval_response", + "user_custom_tool_result", + "user_tool_result", + "user_define_outcome", + # typing module + "types", + # params (commonly used request-body types) + "AgentCreateParams", + "AgentUpdateParams", + "SessionCreateParams", + "SessionUpdateParams", + "EnvironmentCreateParams", + "EnvironmentUpdateParams", + "DeploymentCreateParams", + "DeploymentUpdateParams", + "SessionEventSendParams", + "WebhookEndpointCreateParams", + "WebhookEndpointUpdateParams", + "WebhookEventListParams", + "WebhookStatus", + "WebhookDisabledReason", + "WebhookDeliveryStatus", + "WebhookEventType", + "SSEEventType", + "SessionStatus", + "StopReasonType", + "PermissionPolicyType", +] diff --git a/dashscope/v2/agentstudio/client.py b/dashscope/agentstudio/client.py similarity index 89% rename from dashscope/v2/agentstudio/client.py rename to dashscope/agentstudio/client.py index 0834d3a9..19e870e9 100644 --- a/dashscope/v2/agentstudio/client.py +++ b/dashscope/agentstudio/client.py @@ -13,38 +13,38 @@ validate_region, validate_workspace_id, ) -from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.v2.agentstudio.resources.deployments import ( +from dashscope.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.v2.agentstudio.resources.environments import ( +from dashscope.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles -from dashscope.v2.agentstudio.resources.memory_stores import ( +from dashscope.agentstudio.resources.files import Files, AsyncFiles +from dashscope.agentstudio.resources.memory_stores import ( MemoryStores, AsyncMemoryStores, ) -from dashscope.v2.agentstudio.resources.security import Security, AsyncSecurity -from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.v2.agentstudio.resources.skills import Skills, AsyncSkills -from dashscope.v2.agentstudio.resources.vaults import Vaults, AsyncVaults -from dashscope.v2.agentstudio.resources.webhook_endpoints import ( +from dashscope.agentstudio.resources.security import Security, AsyncSecurity +from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.agentstudio.resources.skills import Skills, AsyncSkills +from dashscope.agentstudio.resources.vaults import Vaults, AsyncVaults +from dashscope.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) -from dashscope.v2.agentstudio.constants import ( +from dashscope.agentstudio.constants import ( AGENTSTUDIO_BASE_URL_TEMPLATE, AGENTSTUDIO_DEFAULT_REGION, AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.v2.agentstudio.transport import SyncTransport, AsyncTransport -from dashscope.v2._config import _api_key_from_file, settings +from dashscope.agentstudio.transport import SyncTransport, AsyncTransport +from dashscope._config import _api_key_from_file, settings def _default_api_key() -> Optional[str]: @@ -58,7 +58,7 @@ def _default_api_key() -> Optional[str]: names the AgentStudio entry points. This deliberately does not read the 1.x ``dashscope.api_key`` module - global; set ``dashscope.v2.settings.api_key`` or the environment + global; set ``dashscope.settings.api_key`` or the environment variable instead. """ return settings.api_key or _api_key_from_file() diff --git a/dashscope/v2/agentstudio/constants.py b/dashscope/agentstudio/constants.py similarity index 100% rename from dashscope/v2/agentstudio/constants.py rename to dashscope/agentstudio/constants.py diff --git a/dashscope/v2/agentstudio/exceptions.py b/dashscope/agentstudio/exceptions.py similarity index 95% rename from dashscope/v2/agentstudio/exceptions.py rename to dashscope/agentstudio/exceptions.py index 6ebb62ab..641ce461 100644 --- a/dashscope/v2/agentstudio/exceptions.py +++ b/dashscope/agentstudio/exceptions.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""AgentStudio errors: the ``dashscope.v2.errors`` family, no second hierarchy. +"""AgentStudio errors: the ``dashscope.errors`` family, no second hierarchy. The sub-SDK raises v2 errors natively, so the v2 mount needs no translation layer. Old class names stay importable as aliases of the -v2 classes they map to; new code should use ``dashscope.v2.errors``. +v2 classes they map to; new code should use ``dashscope.errors``. Mapping notes: @@ -35,9 +35,9 @@ from typing import Any, Dict, Mapping, Optional -from dashscope.v2 import _error_registry as _registry -from dashscope.v2 import errors as _v2_errors -from dashscope.v2.errors import ( # noqa: F401 +from dashscope import _error_registry as _registry +from dashscope import errors as _v2_errors +from dashscope.errors import ( # noqa: F401 APIConnectionError, APIStatusError, APITimeoutError, diff --git a/dashscope/v2/agentstudio/pagination.py b/dashscope/agentstudio/pagination.py similarity index 100% rename from dashscope/v2/agentstudio/pagination.py rename to dashscope/agentstudio/pagination.py diff --git a/dashscope/v2/agentstudio/resources/__init__.py b/dashscope/agentstudio/resources/__init__.py similarity index 72% rename from dashscope/v2/agentstudio/resources/__init__.py rename to dashscope/agentstudio/resources/__init__.py index aeeacbc8..76c65123 100644 --- a/dashscope/v2/agentstudio/resources/__init__.py +++ b/dashscope/agentstudio/resources/__init__.py @@ -15,19 +15,19 @@ client.sessions.events.send(session.id, [user_message("hello")]) """ -from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.v2.agentstudio.resources.deployments import ( +from dashscope.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.v2.agentstudio.resources.environments import ( +from dashscope.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles -from dashscope.v2.agentstudio.resources.memory_stores import ( +from dashscope.agentstudio.resources.files import Files, AsyncFiles +from dashscope.agentstudio.resources.memory_stores import ( AsyncMemories, AsyncMemoryStores, AsyncMemoryVersions, @@ -35,26 +35,26 @@ MemoryStores, MemoryVersions, ) -from dashscope.v2.agentstudio.resources.session_events import ( +from dashscope.agentstudio.resources.session_events import ( AsyncSessionEvents, SessionEvents, _AioTypedEventStream, _TypedEventStream, ) -from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.v2.agentstudio.resources.skills import ( +from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.agentstudio.resources.skills import ( AsyncSkillVersions, AsyncSkills, SkillVersions, Skills, ) -from dashscope.v2.agentstudio.resources.vaults import ( +from dashscope.agentstudio.resources.vaults import ( AsyncCredentials, AsyncVaults, Credentials, Vaults, ) -from dashscope.v2.agentstudio.resources.webhook_endpoints import ( +from dashscope.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) diff --git a/dashscope/v2/agentstudio/resources/_helpers.py b/dashscope/agentstudio/resources/_helpers.py similarity index 99% rename from dashscope/v2/agentstudio/resources/_helpers.py rename to dashscope/agentstudio/resources/_helpers.py index 0978b1ab..f47d6d94 100644 --- a/dashscope/v2/agentstudio/resources/_helpers.py +++ b/dashscope/agentstudio/resources/_helpers.py @@ -6,7 +6,7 @@ from typing import Any, Mapping -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio.types import ( Agent, AgentVersion, Credential, diff --git a/dashscope/v2/agentstudio/resources/agents.py b/dashscope/agentstudio/resources/agents.py similarity index 97% rename from dashscope/v2/agentstudio/resources/agents.py rename to dashscope/agentstudio/resources/agents.py index 29d6f04a..53712811 100644 --- a/dashscope/v2/agentstudio/resources/agents.py +++ b/dashscope/agentstudio/resources/agents.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence, Union -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_agent, _coerce_agent_version, ) -from dashscope.v2.agentstudio.types import Agent, AgentVersion, ModelConfig -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import Agent, AgentVersion, ModelConfig +from dashscope.agentstudio.types.params import ( AgentCreateParams, AgentListParams, AgentUpdateParams, diff --git a/dashscope/v2/agentstudio/resources/deployments.py b/dashscope/agentstudio/resources/deployments.py similarity index 98% rename from dashscope/v2/agentstudio/resources/deployments.py rename to dashscope/agentstudio/resources/deployments.py index 4e93b234..149b64f2 100644 --- a/dashscope/v2/agentstudio/resources/deployments.py +++ b/dashscope/agentstudio/resources/deployments.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_deployment, _coerce_deployment_run, ) -from dashscope.v2.agentstudio.types import Deployment, DeploymentRun -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import Deployment, DeploymentRun +from dashscope.agentstudio.types.params import ( _NOT_GIVEN, DeploymentCreateParams, DeploymentListParams, diff --git a/dashscope/v2/agentstudio/resources/environments.py b/dashscope/agentstudio/resources/environments.py similarity index 96% rename from dashscope/v2/agentstudio/resources/environments.py rename to dashscope/agentstudio/resources/environments.py index 26a4ab38..8aed2eeb 100644 --- a/dashscope/v2/agentstudio/resources/environments.py +++ b/dashscope/agentstudio/resources/environments.py @@ -6,16 +6,16 @@ from typing import Any, Mapping, Optional -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_env, ) -from dashscope.v2.agentstudio.types import DeleteResponse, Environment -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import DeleteResponse, Environment +from dashscope.agentstudio.types.params import ( EnvironmentCreateParams, EnvironmentListParams, EnvironmentUpdateParams, diff --git a/dashscope/v2/agentstudio/resources/files.py b/dashscope/agentstudio/resources/files.py similarity index 97% rename from dashscope/v2/agentstudio/resources/files.py rename to dashscope/agentstudio/resources/files.py index a2c9936e..e05c7be8 100644 --- a/dashscope/v2/agentstudio/resources/files.py +++ b/dashscope/agentstudio/resources/files.py @@ -18,16 +18,16 @@ Union, ) -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_file, ) -from dashscope.v2.agentstudio.types import DeleteResponse, File -from dashscope.v2.agentstudio.types.params import FileListParams +from dashscope.agentstudio.types import DeleteResponse, File +from dashscope.agentstudio.types.params import FileListParams _PATH_FILES = "/files" diff --git a/dashscope/v2/agentstudio/resources/memory_stores.py b/dashscope/agentstudio/resources/memory_stores.py similarity index 99% rename from dashscope/v2/agentstudio/resources/memory_stores.py rename to dashscope/agentstudio/resources/memory_stores.py index 1308a20e..573084a0 100644 --- a/dashscope/v2/agentstudio/resources/memory_stores.py +++ b/dashscope/agentstudio/resources/memory_stores.py @@ -41,12 +41,12 @@ from typing import Any, List, Mapping, Optional -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_memory, _coerce_memory_store, _coerce_memory_version, @@ -59,13 +59,13 @@ _memory_version_redact_path, _memory_versions_path, ) -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio.types import ( DeleteResponse, Memory, MemoryStore, MemoryVersion, ) -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types.params import ( MemoryCreateParams, MemoryListParams, MemoryStoreCreateParams, diff --git a/dashscope/v2/agentstudio/resources/security.py b/dashscope/agentstudio/resources/security.py similarity index 96% rename from dashscope/v2/agentstudio/resources/security.py rename to dashscope/agentstudio/resources/security.py index 8e5af669..a6ebc723 100644 --- a/dashscope/v2/agentstudio/resources/security.py +++ b/dashscope/agentstudio/resources/security.py @@ -12,8 +12,8 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.v2.agentstudio.types import SecurityAlertList, SecurityOverview -from dashscope.v2.agentstudio.types.params import SecurityListAgentLogsParams +from dashscope.agentstudio.types import SecurityAlertList, SecurityOverview +from dashscope.agentstudio.types.params import SecurityListAgentLogsParams _PATH_SECURITY_OVERVIEW = "/security/overview" _PATH_SECURITY_AGENT_LOGS = "/security/agent_logs" diff --git a/dashscope/v2/agentstudio/resources/session_events.py b/dashscope/agentstudio/resources/session_events.py similarity index 97% rename from dashscope/v2/agentstudio/resources/session_events.py rename to dashscope/agentstudio/resources/session_events.py index 1e9f2e80..aa272597 100644 --- a/dashscope/v2/agentstudio/resources/session_events.py +++ b/dashscope/agentstudio/resources/session_events.py @@ -14,23 +14,23 @@ Sequence, ) -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_event, _events_path, _stream_path, ) -from dashscope.v2.agentstudio.streaming import AsyncEventStream, EventStream -from dashscope.v2.agentstudio.types import ServerEvent -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.streaming import AsyncEventStream, EventStream +from dashscope.agentstudio.types import ServerEvent +from dashscope.agentstudio.types.params import ( SessionEventListParams, SessionEventSendParams, ) -from dashscope.v2.agentstudio.constants import ( +from dashscope.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, BlockType, SessionStatus, diff --git a/dashscope/v2/agentstudio/resources/session_resources.py b/dashscope/agentstudio/resources/session_resources.py similarity index 95% rename from dashscope/v2/agentstudio/resources/session_resources.py rename to dashscope/agentstudio/resources/session_resources.py index fc2e4b37..1691a654 100644 --- a/dashscope/v2/agentstudio/resources/session_resources.py +++ b/dashscope/agentstudio/resources/session_resources.py @@ -10,18 +10,18 @@ from typing import Optional -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_session_resource, _session_resource_item_path, _session_resources_path, ) -from dashscope.v2.agentstudio.types import DeleteResponse, SessionResource -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import DeleteResponse, SessionResource +from dashscope.agentstudio.types.params import ( SessionResourceAddParams, SessionResourceListParams, ) diff --git a/dashscope/v2/agentstudio/resources/session_threads.py b/dashscope/agentstudio/resources/session_threads.py similarity index 97% rename from dashscope/v2/agentstudio/resources/session_threads.py rename to dashscope/agentstudio/resources/session_threads.py index 6c8164c1..e2f20313 100644 --- a/dashscope/v2/agentstudio/resources/session_threads.py +++ b/dashscope/agentstudio/resources/session_threads.py @@ -10,12 +10,12 @@ from typing import Optional, Sequence -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_event, _coerce_session_thread, _session_thread_archive_path, @@ -23,8 +23,8 @@ _session_thread_item_path, _session_threads_path, ) -from dashscope.v2.agentstudio.types import ServerEvent, SessionThread -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import ServerEvent, SessionThread +from dashscope.agentstudio.types.params import ( SessionEventListParams, SessionThreadListParams, ) diff --git a/dashscope/v2/agentstudio/resources/sessions.py b/dashscope/agentstudio/resources/sessions.py similarity index 95% rename from dashscope/v2/agentstudio/resources/sessions.py rename to dashscope/agentstudio/resources/sessions.py index e5be7178..4bb68f15 100644 --- a/dashscope/v2/agentstudio/resources/sessions.py +++ b/dashscope/agentstudio/resources/sessions.py @@ -6,28 +6,28 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_session, ) -from dashscope.v2.agentstudio.resources.session_events import ( +from dashscope.agentstudio.resources.session_events import ( SessionEvents, AsyncSessionEvents, ) -from dashscope.v2.agentstudio.resources.session_resources import ( +from dashscope.agentstudio.resources.session_resources import ( SessionResources, AsyncSessionResources, ) -from dashscope.v2.agentstudio.resources.session_threads import ( +from dashscope.agentstudio.resources.session_threads import ( SessionThreads, AsyncSessionThreads, ) -from dashscope.v2.agentstudio.types import DeleteResponse, Session -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import DeleteResponse, Session +from dashscope.agentstudio.types.params import ( SessionCreateParams, SessionListParams, SessionUpdateParams, diff --git a/dashscope/v2/agentstudio/resources/skills.py b/dashscope/agentstudio/resources/skills.py similarity index 97% rename from dashscope/v2/agentstudio/resources/skills.py rename to dashscope/agentstudio/resources/skills.py index 3ad70cf8..c9d9833e 100644 --- a/dashscope/v2/agentstudio/resources/skills.py +++ b/dashscope/agentstudio/resources/skills.py @@ -7,17 +7,17 @@ import os # pylint: disable=unused-import # noqa: F401 from typing import Any, BinaryIO, Dict, Optional, Tuple, Union -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_skill, _coerce_skill_version, ) -from dashscope.v2.agentstudio.types import DeleteResponse, Skill, SkillVersion -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types import DeleteResponse, Skill, SkillVersion +from dashscope.agentstudio.types.params import ( SkillCreateParams, SkillListParams, SkillVersionCreateParams, diff --git a/dashscope/v2/agentstudio/resources/vaults.py b/dashscope/agentstudio/resources/vaults.py similarity index 98% rename from dashscope/v2/agentstudio/resources/vaults.py rename to dashscope/agentstudio/resources/vaults.py index 7440ec69..c68e4922 100644 --- a/dashscope/v2/agentstudio/resources/vaults.py +++ b/dashscope/agentstudio/resources/vaults.py @@ -6,21 +6,21 @@ from typing import Any, Mapping, Optional -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_credential, _coerce_vault, ) -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio.types import ( Credential, DeleteResponse, Vault, ) -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types.params import ( CredentialCreateParams, CredentialListParams, CredentialUpdateParams, diff --git a/dashscope/v2/agentstudio/resources/webhook_endpoints.py b/dashscope/agentstudio/resources/webhook_endpoints.py similarity index 97% rename from dashscope/v2/agentstudio/resources/webhook_endpoints.py rename to dashscope/agentstudio/resources/webhook_endpoints.py index c20ee2c4..fc1a4485 100644 --- a/dashscope/v2/agentstudio/resources/webhook_endpoints.py +++ b/dashscope/agentstudio/resources/webhook_endpoints.py @@ -6,23 +6,23 @@ from typing import Any, Dict, Optional, Sequence -from dashscope.v2.agentstudio.pagination import ( +from dashscope.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_webhook_endpoint, _coerce_webhook_event, _coerce_webhook_secret_reset, ) -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio.types import ( WebhookEndpoint, WebhookEndpointList, WebhookEvent, WebhookSecretReset, ) -from dashscope.v2.agentstudio.types.params import ( +from dashscope.agentstudio.types.params import ( WebhookEndpointCreateParams, WebhookEndpointUpdateParams, WebhookEventListParams, diff --git a/dashscope/v2/agentstudio/streaming.py b/dashscope/agentstudio/streaming.py similarity index 99% rename from dashscope/v2/agentstudio/streaming.py rename to dashscope/agentstudio/streaming.py index 414f61c9..23aac095 100644 --- a/dashscope/v2/agentstudio/streaming.py +++ b/dashscope/agentstudio/streaming.py @@ -39,7 +39,7 @@ import httpx from httpx_sse import EventSource -from dashscope.v2.agentstudio import exceptions +from dashscope.agentstudio import exceptions # A child of the "dashscope" logger, so the level and handler installed by # dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via diff --git a/dashscope/v2/agentstudio/transport.py b/dashscope/agentstudio/transport.py similarity index 99% rename from dashscope/v2/agentstudio/transport.py rename to dashscope/agentstudio/transport.py index 0a4e1e19..6068b066 100644 --- a/dashscope/v2/agentstudio/transport.py +++ b/dashscope/agentstudio/transport.py @@ -28,8 +28,8 @@ import httpx from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.v2.agentstudio import exceptions -from dashscope.v2.agentstudio.constants import ( +from dashscope.agentstudio import exceptions +from dashscope.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) diff --git a/dashscope/v2/agentstudio/types/__init__.py b/dashscope/agentstudio/types/__init__.py similarity index 93% rename from dashscope/v2/agentstudio/types/__init__.py rename to dashscope/agentstudio/types/__init__.py index 1f220487..2bb98d1b 100644 --- a/dashscope/v2/agentstudio/types/__init__.py +++ b/dashscope/agentstudio/types/__init__.py @@ -4,8 +4,8 @@ Re-exports all public symbols from sub-modules for backward compatibility: from dashscope.agentstudio.types import Agent, Message, user_message """ -from dashscope.v2.agentstudio.types.models import * # noqa: F401,F403 -from dashscope.v2.agentstudio.types.models import ( # noqa: F401 +from dashscope.agentstudio.types.models import * # noqa: F401,F403 +from dashscope.agentstudio.types.models import ( # noqa: F401 BaseModel, Metadata, Scope, @@ -94,7 +94,7 @@ WebhookEventData, WebhookSecretReset, ) -from dashscope.v2.agentstudio.types.params import ( # noqa: F401 +from dashscope.agentstudio.types.params import ( # noqa: F401 AgentCreateParams, AgentUpdateParams, AgentListParams, diff --git a/dashscope/v2/agentstudio/types/models.py b/dashscope/agentstudio/types/models.py similarity index 99% rename from dashscope/v2/agentstudio/types/models.py rename to dashscope/agentstudio/types/models.py index f04e1bbe..dac21783 100644 --- a/dashscope/v2/agentstudio/types/models.py +++ b/dashscope/agentstudio/types/models.py @@ -27,7 +27,7 @@ Union, ) -from dashscope.v2.agentstudio.constants import ( +from dashscope.agentstudio.constants import ( SSEEventType, MessageRole, BlockType, diff --git a/dashscope/v2/agentstudio/types/params.py b/dashscope/agentstudio/types/params.py similarity index 99% rename from dashscope/v2/agentstudio/types/params.py rename to dashscope/agentstudio/types/params.py index a3065381..6742e94b 100644 --- a/dashscope/v2/agentstudio/types/params.py +++ b/dashscope/agentstudio/types/params.py @@ -18,7 +18,7 @@ from typing import Any, Dict, Mapping, Optional, Sequence, Union -from dashscope.v2.agentstudio.types.models import BaseModel, ModelConfig +from dashscope.agentstudio.types.models import BaseModel, ModelConfig _NOT_GIVEN = object() diff --git a/dashscope/aigc/__init__.py b/dashscope/aigc/__init__.py deleted file mode 100644 index eba54d24..00000000 --- a/dashscope/aigc/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.aigc``. - -Import from ``dashscope.legacy_1x.aigc`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.aigc") diff --git a/dashscope/api_entities/__init__.py b/dashscope/api_entities/__init__.py deleted file mode 100644 index 3d4c9c3d..00000000 --- a/dashscope/api_entities/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to -``dashscope.legacy_1x.api_entities``. - -Import from ``dashscope.legacy_1x.api_entities`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.api_entities") diff --git a/dashscope/app/__init__.py b/dashscope/app/__init__.py deleted file mode 100644 index 5b018c18..00000000 --- a/dashscope/app/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.app``. - -Import from ``dashscope.legacy_1x.app`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.app") diff --git a/dashscope/assistants/__init__.py b/dashscope/assistants/__init__.py deleted file mode 100644 index 292369d2..00000000 --- a/dashscope/assistants/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.assistants``. - -Import from ``dashscope.legacy_1x.assistants`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.assistants") diff --git a/dashscope/audio/__init__.py b/dashscope/audio/__init__.py deleted file mode 100644 index f4bd5f18..00000000 --- a/dashscope/audio/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.audio``. - -Import from ``dashscope.legacy_1x.audio`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.audio") diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py deleted file mode 100644 index cd5ca981..00000000 --- a/dashscope/cli/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.cli``. - -Import from ``dashscope.legacy_1x.cli`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.cli") diff --git a/dashscope/v2/client.py b/dashscope/client.py similarity index 90% rename from dashscope/v2/client.py rename to dashscope/client.py index 2b34c3ec..4e454289 100644 --- a/dashscope/v2/client.py +++ b/dashscope/client.py @@ -14,31 +14,31 @@ import httpx -from dashscope.v2._config import Config, resolve_config -from dashscope.v2.protocols import ChatProtocol, get_protocol -from dashscope.v2.resources.apps import Apps, AsyncApps -from dashscope.v2.resources.audio import AsyncAudio, Audio -from dashscope.v2.resources.chat import AsyncChat, Chat -from dashscope.v2.resources.decision import AsyncDecision, Decision -from dashscope.v2.resources.deployments import AsyncDeployments, Deployments -from dashscope.v2.resources.embeddings import AsyncEmbeddings, Embeddings -from dashscope.v2.resources.files import AsyncFiles, Files -from dashscope.v2.resources.fine_tunes import AsyncFineTunes, FineTunes -from dashscope.v2.resources.images import AsyncImages, Images -from dashscope.v2.resources.models import AsyncModels, Models -from dashscope.v2.resources.multimodal import AsyncMultimodal, Multimodal -from dashscope.v2.resources.rerank import AsyncRerank, Rerank -from dashscope.v2.resources.reinforcement_learning import AsyncRl, Rl -from dashscope.v2.resources.tingwu import AsyncTingWu, TingWu -from dashscope.v2.resources.tokenization import AsyncTokenization, Tokenization -from dashscope.v2.resources.videos import AsyncVideos, Videos -from dashscope.v2.transport import ( +from dashscope._config import Config, resolve_config +from dashscope.protocols import ChatProtocol, get_protocol +from dashscope.resources.apps import Apps, AsyncApps +from dashscope.resources.audio import AsyncAudio, Audio +from dashscope.resources.chat import AsyncChat, Chat +from dashscope.resources.decision import AsyncDecision, Decision +from dashscope.resources.deployments import AsyncDeployments, Deployments +from dashscope.resources.embeddings import AsyncEmbeddings, Embeddings +from dashscope.resources.files import AsyncFiles, Files +from dashscope.resources.fine_tunes import AsyncFineTunes, FineTunes +from dashscope.resources.images import AsyncImages, Images +from dashscope.resources.models import AsyncModels, Models +from dashscope.resources.multimodal import AsyncMultimodal, Multimodal +from dashscope.resources.rerank import AsyncRerank, Rerank +from dashscope.resources.reinforcement_learning import AsyncRl, Rl +from dashscope.resources.tingwu import AsyncTingWu, TingWu +from dashscope.resources.tokenization import AsyncTokenization, Tokenization +from dashscope.resources.videos import AsyncVideos, Videos +from dashscope.transport import ( AsyncTransport, SyncTransport, default_user_agent, ) -from dashscope.v2.transport.ws import Connector -from dashscope.v2.transport.ws_connection import WsConnection +from dashscope.transport.ws import Connector +from dashscope.transport.ws_connection import WsConnection def _http_limits(config: Config) -> httpx.Limits: @@ -153,7 +153,7 @@ def reinforcement_learning(self) -> "Rl": def agentstudio(self): """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). - AgentStudio raises ``dashscope.v2.errors`` natively. It ships its + AgentStudio raises ``dashscope.errors`` natively. It ships its own transport rather than riding this client's, so its requests keep the unmarked module tag (``agentstudio``) instead of the v2 ``2`` marker -- the backend already reports on that tag, and @@ -161,7 +161,7 @@ def agentstudio(self): """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.v2.agentstudio import Client as AgentStudioClient + from dashscope.agentstudio import Client as AgentStudioClient # agentstudio resolves its own base_url from workspace + # region (the v2 base_url does not propagate: the agentstudio @@ -386,12 +386,12 @@ def reinforcement_learning(self) -> "AsyncRl": def agentstudio(self): """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). - See :attr:`Client.agentstudio`; errors use the ``dashscope.v2`` + See :attr:`Client.agentstudio`; errors use the ``dashscope`` exception family natively. """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.v2.agentstudio import AsyncClient as AsClient + from dashscope.agentstudio import AsyncClient as AsClient # See Client.agentstudio: agentstudio builds its own base_url # from workspace + region, so forward both. diff --git a/dashscope/common/__init__.py b/dashscope/common/__init__.py deleted file mode 100644 index 105fba9c..00000000 --- a/dashscope/common/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.common``. - -Import from ``dashscope.legacy_1x.common`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.common") diff --git a/dashscope/embeddings/__init__.py b/dashscope/embeddings/__init__.py deleted file mode 100644 index e421da08..00000000 --- a/dashscope/embeddings/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.embeddings``. - -Import from ``dashscope.legacy_1x.embeddings`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.embeddings") diff --git a/dashscope/v2/errors.py b/dashscope/errors.py similarity index 98% rename from dashscope/v2/errors.py rename to dashscope/errors.py index f9b8a710..41ed4231 100644 --- a/dashscope/v2/errors.py +++ b/dashscope/errors.py @@ -3,7 +3,7 @@ """Exception hierarchy and error codes for the dashscope 2.0 SDK. Class names and HTTP statuses are read out of the generated error -catalog (``dashscope/v2/_error_registry.py``) instead of being spelled +catalog (``dashscope/_error_registry.py``) instead of being spelled here: every ``PUBLIC_ERRORS.error_code`` names exactly one exception class, and that class takes its ``status_code`` from the same entry. @@ -37,8 +37,8 @@ import httpx -from dashscope.v2 import _error_registry as _registry -from dashscope.v2._error_registry import PublicError, SdkErrorDef +from dashscope import _error_registry as _registry +from dashscope._error_registry import PublicError, SdkErrorDef class ErrorCode(str, Enum): @@ -298,7 +298,8 @@ class ContentFilterError(BadRequestError): _STATUS_MAP: Dict[int, Type[APIStatusError]] = {} for _entry_ in _registry.PUBLIC_ERRORS: _STATUS_MAP.setdefault( - _entry_.status_code, _CATALOG_CLASSES[_entry_.error_code] + _entry_.status_code, + _CATALOG_CLASSES[_entry_.error_code], ) del _entry_ diff --git a/dashscope/files.py b/dashscope/files.py deleted file mode 100644 index 55ede462..00000000 --- a/dashscope/files.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.files``. - -Import from ``dashscope.legacy_1x.files`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.files") diff --git a/dashscope/v2/finetune/_submit.py b/dashscope/finetune/_submit.py similarity index 99% rename from dashscope/v2/finetune/_submit.py rename to dashscope/finetune/_submit.py index a698173c..873a3434 100644 --- a/dashscope/v2/finetune/_submit.py +++ b/dashscope/finetune/_submit.py @@ -4,7 +4,7 @@ ``AgenticRL.submit_job`` used to inherit ``CreateMixin`` from ``dashscope.legacy_1x.client.base_api`` for one non-streaming POST. Keeping that -would have left the last import of the 1.x client inside ``dashscope.v2``, +would have left the last import of the 1.x client inside ``dashscope``, so the call is spelled out here instead: a few dozen lines, against pulling the 1.x request/response stack (``default_headers``, ``join_url``, ``_handle_http_stream_response``) into the 2.0 package. diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 0cc58ee1..6abb90bf 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -1,96 +1,357 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility shim plus the 1.x surface; the implementation moved to -``dashscope.v2.finetune.agentic_rl``. - -The agentic-RL workflow is owned by the 2.0 SDK, but this import path has -to keep resolving: the ``dashscope`` CLI, the samples under -``reinforcement/examples/workspace`` and the acli skill docs all name -``dashscope.finetune.agentic_rl.AgenticRL``. - -What stays here is the half of the surface that belongs to 1.x -- the -five job-management verbs, which delegate to ``dashscope.FineTunes`` (a -1.x root public API, alongside ``Deployments``), and the ``FineTune`` -dataclasses they return. ``submit_job`` therefore returns a ``FineTune`` -from this path and a ``SubmitResult`` from the 2.0 one; the payload and -the POST are built once, in the 2.0 class. - -Import from ``dashscope.v2.finetune.agentic_rl`` in new code. +"""The agentic-RL workflow driver, owned by the 2.0 package. + +``Client(...).rl`` is built on this class. The 1.x job-management surface +-- ``FineTunes`` / ``Deployments`` and the ``FineTune`` dataclasses -- +stays at ``dashscope.legacy_1x.finetune``; the thin subclass there re-adds +those five verbs and swaps ``SubmitResult`` back for ``FineTune``. + +Two deliberate differences from the 1.x class this was split out of: + +- ``submit_job`` returns ``SubmitResult`` instead of a 1.x ``FineTune`` + dataclass, and posts through ``dashscope.finetune._submit`` instead + of inheriting ``CreateMixin``. +- The api key is the constructor argument or ``DASHSCOPE_API_KEY``, never + the ``dashscope.api_key`` root global. ``set_api_key`` already writes + the argument into that env var and raises when it cannot, so resolution + here is unambiguous -- reading the root global could silently disagree + with the key this instance was built with. """ -from typing import Dict, List, Optional, Union +from __future__ import annotations -from dashscope.finetune.customize_types import ( - FineTune, - FineTuneCancel, - FineTuneDelete, - FineTuneList, -) -from dashscope.finetune.finetunes import FineTunes -from dashscope.v2.finetune.agentic_rl import AgenticRL as _AgenticRLBase -from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult - -# Re-exported so every name this module historically carried stays -# importable from here. Both spellings resolve to the same objects: the -# reverse-reference shim under ``dashscope/reinforcement`` aliases each -# submodule to the 2.0 one. -# pylint: disable-next=unused-import -from dashscope.reinforcement import ( # noqa: F401 - DASHSCOPE_HTTP_BASE_URL, +import os +import time +from typing import Union, List, Optional, ClassVar, Dict, Any +from typing_extensions import Self + +from dashscope.finetune import _submit +from dashscope.finetune.reinforcement import ( AgenticRLFunctionComponent, - AgenticRLTuning, + RolloutFunctionComponent, + RewardFunctionComponent, Dataset, - DatasetsType, + TrainingDataset, + ValidationDataset, +) +from dashscope.finetune.reinforcement import AgenticRLTuning, TuningModel +from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.finetune.reinforcement import ( FunctionType, - GroupRewardInput, - RewardFunctionComponent, + DatasetsType, + TrainingType, +) +from dashscope.finetune.reinforcement import ( RewardInput, - RolloutFunctionComponent, RolloutInput, - TrainingDataset, - TrainingType, - TuningModel, - ValidationDataset, - deep_remove_none, + GroupRewardInput, +) +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement import ( + set_api_key, generate_random_id, get_func_type_id, - logger, - set_api_key, + deep_remove_none, ) - -# pylint: disable-next=unused-import -from dashscope.reinforcement.common.errors import ( # noqa: F401 - DatasetsError, - InstanceQueryError, +from dashscope.finetune.reinforcement.common.errors import ( RegistrationError, - RuntimeErrorWithCode, ValidationError, + InstanceQueryError, + RuntimeErrorWithCode, ValueErrorWithCode, + DatasetsError, ) -__all__ = [ - "AgenticRL", - "DASHSCOPE_HTTP_BASE_URL", - "FineTune", - "FineTuneCancel", - "FineTuneDelete", - "FineTuneList", - "FineTunes", - "SubmitOutput", - "SubmitResult", -] - - -class AgenticRL(_AgenticRLBase): - """The 1.x surface: ``FineTune`` returns plus the ``FineTunes`` verbs. - - The five verbs below are 1.x-only. ``Client(...).rl.jobs`` already - implements the same lifecycle natively on the 2.0 transport, so - duplicating them into the 2.0 class would have given the SDK two - copies of every job verb. +__all__ = ["AgenticRL", "SubmitOutput", "SubmitResult"] + + +def _inner_code(exc: Exception) -> str: + return getattr(exc, "code", None) or type(exc).__name__ + + +class SubmitOutput: + """The ``output`` payload nested in a submit response. + + A plain attribute container on purpose: the server's job payload is + open-ended, and ``reinforcement_learning._to_job`` flattens it with + ``vars(output)``, which needs a ``__dict__``. + """ + + def __init__(self, **kwargs: Any): + self.__dict__.update(kwargs) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.__dict__!r})" + + +class SubmitResult: + """The envelope ``AgenticRL.submit_job`` returns. + + Carries the same seven fields as 1.x's ``DashScopeAPIResponse``, so + ``FineTune(**envelope)`` and ``SubmitResult(**envelope)`` accept the + same dict. A failed submit is a non-200 ``status_code`` here rather + than an exception, exactly as it was under ``FineTune``. """ - def submit_job( # type: ignore[override] + def __init__( + self, + status_code: int = None, + request_id: str = "", + code: str = None, + message: str = "", + output: Any = None, + usage: Any = None, + headers: Dict[str, Any] = None, + **kwargs: Any, + ): + self.__dict__.update(kwargs) + self.status_code = status_code + self.request_id = request_id + self.code = code + self.message = message + self.output = ( + SubmitOutput(**output) if isinstance(output, dict) else output + ) + self.usage = usage + self.headers = headers or {} + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(status_code={self.status_code!r}, " + f"request_id={self.request_id!r}, code={self.code!r}, " + f"message={self.message!r}, output={self.output!r}, " + f"usage={self.usage!r})" + ) + + +class AgenticRL(AgenticRLTuning): + SUB_PATH: ClassVar[str] = "fine-tunes" + + def __init__(self, api_key: str = None): + super().__init__() + + try: + set_api_key(api_key) + except Exception as e: + raise ValueErrorWithCode.from_catalog( + "sdk.InvalidApiKey", + error_code=3001, + ) from e + + self._api_key = api_key + + def _resolve_api_key(self) -> str: + """The key to submit with; see the module docstring.""" + return self._api_key or os.environ.get("DASHSCOPE_API_KEY", "") + + def init(self, config_path: Optional[str] = None, **kwargs) -> Self: + """ + Initialize an AgenticRL instance from a YAML configuration file. + """ + self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) + + return self + + async def register_functions( + self, + functions: Optional[ + Union[ + List[Union[RolloutFunctionComponent, RewardFunctionComponent]], + RolloutFunctionComponent, + RewardFunctionComponent, + ] + ] = None, + lazy_load: Optional[bool] = True, + ) -> tuple[ + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + ]: + """Register function components and return entity/instance IDs.""" + if functions: + self.tuning.functions = functions + + try: + ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) = await self.tuning.register_functions( + lazy_load=lazy_load, + ) + logger.info("Function components registered") + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RegistrationError.from_catalog( + "sdk.agentic_rl.FunctionRegistrationFailed", + error_code=3002, + variables={"inner_code": _inner_code(e)}, + ) from e + + return ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) + + async def upload_datasets( + self, + datasets: Optional[List[Dataset]] = None, + training_files: Optional[Union[List[str], str]] = None, + validation_files: Optional[Union[List[str], str]] = None, + ) -> tuple[List[str], List[str]]: + if datasets: + self.tuning.datasets = datasets + + try: + ( + uploaded_training_ids, + uploaded_validation_ids, + ) = await self.tuning.upload_datasets( + training_files=training_files or [], + validation_files=validation_files or [], + ) + logger.info("Datasets uploaded") + except Exception as e: + raise DatasetsError.from_catalog( + "sdk.agentic_rl.DatasetsUploadFailed", + error_code=3003, + variables={"inner_code": _inner_code(e)}, + ) from e + + return uploaded_training_ids, uploaded_validation_ids + + def _build_submit_request( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + ) -> Dict[str, Any]: + """Assemble the job payload, folding the arguments into ``tuning``. + + Split out of ``submit_job`` so the 1.x surface can reuse it + without touching the transport. + """ + teacher_model = teacher_model or self.tuning.teacher_model + + # Resolve job name (fallback to class default) + if job_name: + self.tuning.name = job_name + job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" + + # Model name + if model: + self.tuning.model.name = model + + # rollouts/rewards + if functions: + self.tuning.functions = functions + rollouts = self.tuning.combine_ids_runtimes( + functype=FunctionType.ROLLOUT, + ) + rewards = self.tuning.combine_ids_runtimes( + functype=FunctionType.REWARD, + ) + rewards.extend( + self.tuning.combine_ids_runtimes( + functype=FunctionType.GROUP_REWARD, + id_str=get_func_type_id(FunctionType.REWARD), + ), + ) + # names of functions + if not self.tuning.check_function_names(): + names = [ + getattr(fc, "name", "") + for fc in self.tuning.functions + ] + duplicates = sorted( + {name for name in names if names.count(name) > 1}, + ) + raise ValueErrorWithCode.from_catalog( + "sdk.agentic_rl.DuplicateFunctionNames", + error_code=3004, + variables={"names": ", ".join(duplicates or names)}, + ) + + # datasets + if datasets: + self.tuning.datasets = datasets + training_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.TRAINING + ] + validation_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.VALIDATION + ] + + # hyper_parameters + if hyper_parameters: + self.tuning.training.hyper_parameters = hyper_parameters + + # resources + if resources: + self.tuning.training.resources = resources + + training_type = self.tuning.training.type + if teacher_model: + training_type = TrainingType.PG_OPD + elif training_type == TrainingType.PG_OPD: + raise ValueErrorWithCode( + f"teacher_model is required when training.type is {training_type}", + error_code=3006, + ) + + request = { + "model": self.tuning.model.name, + "training_datasets": [ds.model_dump() for ds in training_datasets], + "validation_datasets": [ + ds.model_dump() for ds in validation_datasets + ], + "rollout": rollouts[0] if rollouts else None, + "rewards": rewards, + "hyper_parameters": self.tuning.training.hyper_parameters, + "resource_config": self.tuning.training.resources, + "training_type": str(training_type), + "teacher_model": teacher_model, + "job_name": job_name_with_suffix, + } + request = deep_remove_none(request) + logger.info(f"agentic_rl submit_job request: {request}") + + return request + + def _submit_envelope( self, model: Optional[str] = None, datasets: Optional[List[Dataset]] = None, @@ -113,11 +374,66 @@ def submit_job( # type: ignore[override] job_name: Optional[str] = None, teacher_model: Optional[str] = None, **kwargs, - ) -> FineTune: + ) -> Dict[str, Any]: + """Build the payload, POST it, and return the raw envelope. + + Shared by both surfaces: this class wraps the result in + ``SubmitResult``, the 1.x subclass in a ``FineTune`` dataclass. + ``kwargs`` are forwarded to ``post_submit`` (``path``, + ``timeout``, and overrides for ``api_key`` / ``base_url``). + """ + request = self._build_submit_request( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + ) + + kwargs.setdefault("api_key", self._resolve_api_key()) + kwargs.setdefault("base_url", DASHSCOPE_HTTP_BASE_URL) + kwargs.setdefault("path", self.SUB_PATH) + try: + return _submit.post_submit(request, **kwargs) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode.from_catalog( + "sdk.agentic_rl.JobSubmissionFailed", + error_code=3005, + variables={"inner_code": _inner_code(e)}, + ) from e + + def submit_job( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: """ Submit RL tuning job to the platform. """ - return FineTune( + return SubmitResult( **self._submit_envelope( model=model, datasets=datasets, @@ -130,98 +446,317 @@ def submit_job( # type: ignore[override] ), ) + async def run( + self, + model: Optional[str] = None, + # Datasets parameters + training_datasets: Optional[List[TrainingDataset]] = None, + validation_datasets: Optional[List[ValidationDataset]] = None, + # Path-driven parameters (auto-register & upload) + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + # Common parameters + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: + """ + Execute RL tuning workflow. + """ + try: + logger.info( + "Path-Driven mode: Registering functions & uploading " + "datasets...", + ) + await self.register_functions( + functions=functions, + lazy_load=True, + ) + + datasets = list(training_datasets or []) + list( + validation_datasets or [], + ) + await self.upload_datasets( + datasets=datasets, + ) + + return self.submit_job( + model=model, + datasets=datasets, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode.from_catalog( + "sdk.agentic_rl.WorkflowFailed", + error_code=3006, + variables={"inner_code": _inner_code(e)}, + ) from e + @classmethod - def cancel( + async def test_functions( cls, - job_id: str, + instance_id: str, + functype: FunctionType, + input_data: Dict[str, Any], api_key: str = None, - workspace: str = None, - **kwargs, - ) -> FineTuneCancel: - """Cancel a running fine-tune job.""" - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - - return FineTunes.cancel( - job_id, - api_key=api_key, - workspace=workspace, - **kwargs, - ) + pull_logs: bool = False, + log_page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ): + """Test a deployed function instance with custom input data. + + Args: + instance_id: Target function instance ID. + functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). + input_data: Test input payload. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + pull_logs: If True, pull all logs of the function instance + (with pagination) after verification and print them + between separator markers. + log_page_size: Page size used when pulling logs. + start_time: Optional start time filter (in seconds) for log + pulling. Defaults to 24 hours before ``end_time``. + end_time: Optional end time filter (in seconds) for log + pulling. Defaults to the current time (now). + """ + try: + set_api_key(api_key) + + if functype == FunctionType.ROLLOUT: + value = RolloutInput.model_validate(input_data) + elif functype == FunctionType.REWARD: + value = RewardInput.model_validate(input_data) + elif functype == FunctionType.GROUP_REWARD: + value = GroupRewardInput.model_validate(input_data) + else: + raise ValueErrorWithCode.from_catalog( + "sdk.agentic_rl.UnsupportedFunctionType", + error_code=3007, + variables={"functype": str(functype)}, + ) + + logger.info( + f"Starting {str(functype)} verification", + extra={ + "instance_id": instance_id, + "input_params": value.model_dump(exclude={"api_key"}), + }, + ) + + result = await AgenticRLFunctionComponent.verify_function( + value, + instance_id, + ) + + if pull_logs: + await cls._pull_function_instance_logs( + function_instance_id=instance_id, + page_size=log_page_size, + start_time=start_time, + end_time=end_time, + ) + + return result + + except Exception as e: + raise ValidationError.from_catalog( + "sdk.agentic_rl.FunctionTestFailed", + error_code=3008, + variables={"inner_code": _inner_code(e)}, + ) from e @classmethod - def list( + async def query_function_instance_logs( cls, - page_no=1, - page_size=10, + function_instance_id: str, + page_number: int = 1, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, api_key: str = None, - workspace: str = None, - **kwargs, - ) -> FineTuneList: - """List fine-tune jobs.""" - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - - return FineTunes.list( - page_no=page_no, - page_size=page_size, - api_key=api_key, - workspace=workspace, - **kwargs, - ) + ) -> Dict[str, Any]: + """Query one page of logs for a function (faas) instance. + + Args: + function_instance_id: Target function instance ID. + page_number: Page number, starting from 1. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the log query API. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_function_instance_logs( + function_instance_id=function_instance_id, + page_number=page_number, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3009, + ) from e @classmethod - def get( + async def query_all_function_instance_logs( cls, - job_id: str, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, api_key: str = None, - workspace: str = None, - **kwargs, - ) -> FineTune: - """Get fine-tune job information.""" - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - - return FineTunes.get( - job_id, - api_key=api_key, - workspace=workspace, - **kwargs, - ) + ) -> List[str]: + """Fetch all logs of a function (faas) instance. + + Pages are fetched one by one via the paginated log query until + all entries are collected. + + Args: + function_instance_id: Target function instance ID. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Aggregated list of log messages across all pages. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3010, + ) from e @classmethod - def delete( + async def delete_function_instance( cls, - job_id: str, + function_instance_id: str, api_key: str = None, - workspace: str = None, - **kwargs, - ) -> FineTuneDelete: - """Delete a fine-tune job.""" - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - - return FineTunes.delete( - job_id, - api_key=api_key, - workspace=workspace, - **kwargs, - ) + ) -> Dict[str, Any]: + """Delete a function (faas) runtime instance. + + Args: + function_instance_id: Target function instance ID. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the delete API, containing + ``data.status`` (e.g. "deleted") and + ``data.sandbox_code``. + + Raises: + InstanceQueryError: If the deletion fails. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.delete_function_instance( + function_instance_id=function_instance_id, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance deletion failed", + error_code=3011, + ) from e @classmethod - def logs( + async def _pull_function_instance_logs( cls, - job_id: str, - offset: int = 1, - lines: int = 1000, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> FineTune: - """Get job logs.""" - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - - return FineTunes.logs( - job_id, - offset=offset, - line=lines, - api_key=api_key, - workspace=workspace, - **kwargs, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ) -> None: + """Pull all logs of a function instance and print them between + separator markers (best effort, never raises). + + When ``start_time``/``end_time`` are not provided, defaults to + pulling logs from 24 hours ago until now. + """ + if end_time is None: + end_time = int(time.time()) + if start_time is None: + start_time = end_time - 24 * 60 * 60 + logger.info( + "************start query log " + f"(function_instance_id={function_instance_id})*******", ) + try: + logs = await cls.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + ) + for entry in logs: + logger.info(f"[function instance log] {entry}") + logger.info( + f"Pulled {len(logs)} log entries for function instance " + f"{function_instance_id}", + ) + except Exception as e: + logger.warning( + f"Failed to pull logs for function instance " + f"{function_instance_id}: {e}", + ) + finally: + logger.info("************end query log *******") diff --git a/dashscope/finetune/reinforcement/__init__.py b/dashscope/finetune/reinforcement/__init__.py index f7995f5e..2f6f74da 100644 --- a/dashscope/finetune/reinforcement/__init__.py +++ b/dashscope/finetune/reinforcement/__init__.py @@ -1,28 +1,266 @@ # -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Legacy alias for :mod:`dashscope.reinforcement`. +try: + import pydantic # noqa: F401 + import tenacity # noqa: F401 + import yaml # noqa: F401 +except ImportError as _rl_dep_err: + raise ImportError( + "Agentic RL fine-tuning dependencies are missing from this " + "install. Reinstall with: pip install -U dashscope", + ) from _rl_dep_err -The public compatibility package owns the lazy module aliasing; this path -only registers its prefix so existing dotted-path consumers keep resolving -to the same v2 module objects. -""" - -# Import the v2 exports directly rather than wildcard-importing the public -# shim, whose __all__ includes the deliberately lazy CLI ``app``. -from dashscope import reinforcement as _public -from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 -from dashscope.v2.finetune.reinforcement import ( # noqa: F401 - common, - component, +from .common.constants import ( + BAILIAN_FILE_API, + BAILIAN_FILE_TIMEOUT, + DASHSCOPE_API_KEY, + DASHSCOPE_HTTP_BASE_URL, + LOG_LEVEL, + FC_API_KEY, + FC_DELETE_API, + FC_FILES_START, + FC_LOAD_API, + FC_PYPI_LIB, + FC_PYPI_REPO, + FC_QUERY_API, + FC_QUERY_LOG_API, + FC_REGISTER_REWARD_API, + FC_REGISTER_ROLLOUT_API, + FC_REGISTER_GROUP_REWARD_API, + FC_SERVER_CLASSPATH, + FC_UPLOAD_OSS_API, + FC_WORKERS_COUNT, + FC_LAYER_CREATE_API, + FC_LAYER_QUERY_API, + FC_LAYER_USED, + FC_LAYER_NAME, + FC_REQUIREMENTS_FILE, + FC_ZIP_EXCLUDE_PATTERNS, + FC_OSS_FILE_SIZE_WARNING, + DATASETS_FILE_SIZE_WARNING, + HTTP_REQUEST_TIMEOUT, + LOGGER_FILTER_FIELDS, +) +from .common.errors import ( + AgenticRLError, + InputError, + OutputError, + BaseConnectionError, + OSSConnectionError, + OSSUploadError, + DeploymentError, + RegistrationError, + FunctionLoadError, + InstanceWarmupError, + InstanceQueryError, + FunctionLayerError, + ValidationError, + ConfigurationError, + BasePermissionError, + IOErrorWithCode, + RuntimeErrorWithCode, + ValueErrorWithCode, +) +from .common.log import logger +from .common.model import ( + FunctionComponentModel, + TuningModel, + FoundationModel, + FunctionComponentRuntime, + AgenticRLFunctionComponent, + RolloutFunctionComponent, + RewardFunctionComponent, + GroupRewardFunctionComponent, + Datasets, + Dataset, + TrainingDataset, + ValidationDataset, + MountStorage, + Training, + Observability, + AgenticRLTuning, + Models, +) +from .common.model_types import ( + FileSpec, + FunctionType, + DatasetsType, + DataSourceType, + RequestFC, + ResponseFC, + Status, + StatusType, + TrainingType, +) +from .common.utils import ( + check_file, + client_fc, + create_deployment_files, + deep_mask, + generate_random_id, + get_filepath_classname, + upload_zip_to_oss_and_by_signed_url, + set_api_key, + to_bailian_data, + zip_dir, + get_func_type_id, + deep_remove_none, + extract_reward_weights, + get_weights_from_file, + serialize_for_output, +) +from .component import ( + reward_func, + sub_reward_func, + aggregate_func, +) +from .component.data import ( + AgentOutput, + Reward, + RewardInput, + RewardOutput, + RolloutInput, + RolloutOutput, + GroupReward, + GroupRewardInput, + GroupRewardOutput, + TaskStatus, +) +from .component.func_decorator import ( + RewardProcessorMeta, ) +from .component.processor import ( + AbstractRolloutProcessor, +) +from .component.processor import ( + AbstractRewardProcessor, +) +from .component.processor import ( + AbstractGroupRewardProcessor, +) + -__all__ = list(_public.__all__) +__all__ = [ + # Constants + "BAILIAN_FILE_API", + "BAILIAN_FILE_TIMEOUT", + "DASHSCOPE_API_KEY", + "DASHSCOPE_HTTP_BASE_URL", + "LOG_LEVEL", + "FC_API_KEY", + "FC_DELETE_API", + "FC_FILES_START", + "FC_LOAD_API", + "FC_PYPI_LIB", + "FC_PYPI_REPO", + "FC_QUERY_API", + "FC_QUERY_LOG_API", + "FC_REGISTER_REWARD_API", + "FC_REGISTER_ROLLOUT_API", + "FC_REGISTER_GROUP_REWARD_API", + "FC_SERVER_CLASSPATH", + "FC_UPLOAD_OSS_API", + "FC_WORKERS_COUNT", + "FC_LAYER_CREATE_API", + "FC_LAYER_QUERY_API", + "FC_LAYER_USED", + "FC_LAYER_NAME", + "FC_REQUIREMENTS_FILE", + "FC_ZIP_EXCLUDE_PATTERNS", + "FC_OSS_FILE_SIZE_WARNING", + "DATASETS_FILE_SIZE_WARNING", + "HTTP_REQUEST_TIMEOUT", + "LOGGER_FILTER_FIELDS", + # Errors + "AgenticRLError", + "InputError", + "OutputError", + "BaseConnectionError", + "OSSConnectionError", + "OSSUploadError", + "DeploymentError", + "RegistrationError", + "FunctionLoadError", + "FunctionLayerError", + "InstanceWarmupError", + "InstanceQueryError", + "ValidationError", + "ConfigurationError", + "BasePermissionError", + "IOErrorWithCode", + "RuntimeErrorWithCode", + "ValueErrorWithCode", + # Logging + "logger", + # Utilities + "check_file", + "client_fc", + "create_deployment_files", + "deep_mask", + "generate_random_id", + "get_filepath_classname", + "set_api_key", + "to_bailian_data", + "upload_zip_to_oss_and_by_signed_url", + "zip_dir", + "get_func_type_id", + "deep_remove_none", + "extract_reward_weights", + "get_weights_from_file", + "serialize_for_output", + # Model Types + "FileSpec", + "FunctionType", + "DatasetsType", + "DataSourceType", + "RequestFC", + "ResponseFC", + "Status", + "StatusType", + "TrainingType", + # Data models + "RolloutInput", + "RolloutOutput", + "RewardInput", + "RewardOutput", + "GroupRewardInput", + "GroupRewardOutput", + "GroupReward", + "Reward", + "AgentOutput", + "TaskStatus", + # Func Manager + "reward_func", + "sub_reward_func", + "aggregate_func", + "RewardProcessorMeta", + "AbstractRolloutProcessor", + "AbstractRewardProcessor", + "AbstractGroupRewardProcessor", + # Core Models + "FunctionComponentModel", + "TuningModel", + "FoundationModel", + "FunctionComponentRuntime", + "AgenticRLFunctionComponent", + "RolloutFunctionComponent", + "RewardFunctionComponent", + "GroupRewardFunctionComponent", + "Datasets", + "Dataset", + "TrainingDataset", + "ValidationDataset", + "MountStorage", + "Training", + "Observability", + "AgenticRLTuning", + "Models", + "app", +] def __getattr__(name): if name == "app": - return _public.app - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - + from dashscope.legacy_1x.cli.agentic_rl import app -_public._register_alias(__name__) + return app + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/dashscope/client/__init__.py b/dashscope/finetune/reinforcement/common/__init__.py similarity index 100% rename from dashscope/client/__init__.py rename to dashscope/finetune/reinforcement/common/__init__.py diff --git a/dashscope/v2/finetune/reinforcement/common/constants.py b/dashscope/finetune/reinforcement/common/constants.py similarity index 95% rename from dashscope/v2/finetune/reinforcement/common/constants.py rename to dashscope/finetune/reinforcement/common/constants.py index 0d0a21c7..dbbaf119 100644 --- a/dashscope/v2/finetune/reinforcement/common/constants.py +++ b/dashscope/finetune/reinforcement/common/constants.py @@ -37,8 +37,8 @@ def get_int_env(env_var: str, default: int) -> int: # -------------------------- # The logger name is an observability contract: user log filters and # existing pipelines match on it, so it keeps the published name even -# though the package itself now lives under ``dashscope.v2``. -LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.reinforcement") +# though the package itself now lives under ``dashscope``. +LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.finetune.reinforcement") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { "trigger_token", @@ -119,7 +119,7 @@ def get_int_env(env_var: str, default: int) -> int: # sandbox, where the installed dashscope may be any published # version. Both the new public path and the old finetune path keep # resolving through reverse-reference shims. - "dashscope.reinforcement.component.server.server", + "dashscope.finetune.reinforcement.component.server.server", ) FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) FC_LAYER_USED = get_bool_env("FC_LAYER_USED", True) diff --git a/dashscope/v2/finetune/reinforcement/common/errors.py b/dashscope/finetune/reinforcement/common/errors.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/common/errors.py rename to dashscope/finetune/reinforcement/common/errors.py index a80a2d0a..58991360 100644 --- a/dashscope/v2/finetune/reinforcement/common/errors.py +++ b/dashscope/finetune/reinforcement/common/errors.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Any, Dict, Optional -from dashscope.v2.errors import _CatalogSdkError, sdk_error +from dashscope.errors import _CatalogSdkError, sdk_error _LEGACY_ERROR_CODES = { "sdk.agentic_rl.Error": 1000, diff --git a/dashscope/v2/finetune/reinforcement/common/log.py b/dashscope/finetune/reinforcement/common/log.py similarity index 91% rename from dashscope/v2/finetune/reinforcement/common/log.py rename to dashscope/finetune/reinforcement/common/log.py index 0dd13fb0..3db44a58 100644 --- a/dashscope/v2/finetune/reinforcement/common/log.py +++ b/dashscope/finetune/reinforcement/common/log.py @@ -2,7 +2,7 @@ import logging import sys -from dashscope.v2.finetune.reinforcement.common.constants import ( +from dashscope.finetune.reinforcement.common.constants import ( LOGGER_NAME, LOG_LEVEL, ) diff --git a/dashscope/v2/finetune/reinforcement/common/model.py b/dashscope/finetune/reinforcement/common/model.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/common/model.py rename to dashscope/finetune/reinforcement/common/model.py index 8baf5eb3..d0eb5a25 100644 --- a/dashscope/v2/finetune/reinforcement/common/model.py +++ b/dashscope/finetune/reinforcement/common/model.py @@ -23,7 +23,7 @@ import yaml # Local Application -from dashscope.v2.finetune.reinforcement.common.constants import ( +from dashscope.finetune.reinforcement.common.constants import ( FC_API_KEY, FC_DELETE_API, FC_LOAD_API, @@ -40,7 +40,7 @@ FC_LAYER_QUERY_API, LOG_LEVEL, ) -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, DatasetsType, @@ -51,7 +51,7 @@ StatusType, TrainingType, ) -from dashscope.v2.finetune.reinforcement.component.data import ( +from dashscope.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, RolloutInput, @@ -59,7 +59,7 @@ GroupRewardInput, GroupRewardOutput, ) -from dashscope.v2.finetune.reinforcement.common.utils import ( +from dashscope.finetune.reinforcement.common.utils import ( check_file, client_fc, create_deployment_files, @@ -73,8 +73,8 @@ get_weights_from_file, deep_remove_none, ) -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.errors import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.errors import ( InputError, OutputError, OSSConnectionError, diff --git a/dashscope/v2/finetune/reinforcement/common/model_types.py b/dashscope/finetune/reinforcement/common/model_types.py similarity index 100% rename from dashscope/v2/finetune/reinforcement/common/model_types.py rename to dashscope/finetune/reinforcement/common/model_types.py diff --git a/dashscope/v2/finetune/reinforcement/common/utils.py b/dashscope/finetune/reinforcement/common/utils.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/common/utils.py rename to dashscope/finetune/reinforcement/common/utils.py index 64a583d1..a46015f9 100644 --- a/dashscope/v2/finetune/reinforcement/common/utils.py +++ b/dashscope/finetune/reinforcement/common/utils.py @@ -21,8 +21,8 @@ ) from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.v2.finetune.reinforcement import logger -from dashscope.v2.finetune.reinforcement.common.errors import ( +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.common.errors import ( InputError, OutputError, ConfigurationError, @@ -30,7 +30,7 @@ RuntimeErrorWithCode, OSSUploadError, ) -from dashscope.v2.finetune.reinforcement import ( +from dashscope.finetune.reinforcement import ( LOG_LEVEL, DASHSCOPE_API_KEY, BAILIAN_FILE_API, @@ -47,7 +47,7 @@ LOGGER_FILTER_FIELDS, FC_WORKERS_COUNT, ) -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, ) diff --git a/dashscope/v2/finetune/reinforcement/component/__init__.py b/dashscope/finetune/reinforcement/component/__init__.py similarity index 74% rename from dashscope/v2/finetune/reinforcement/component/__init__.py rename to dashscope/finetune/reinforcement/component/__init__.py index bc2a7334..93d768ba 100644 --- a/dashscope/v2/finetune/reinforcement/component/__init__.py +++ b/dashscope/finetune/reinforcement/component/__init__.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- # # Base Components # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -17,28 +17,28 @@ # -------------------------------------------------------------------------- # # Data Models # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( Reward, RewardOutput, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) # -------------------------------------------------------------------------- # # Demos # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.demo import ( +from dashscope.finetune.reinforcement.component.demo import ( DemoRewardProcessor, DemoRolloutProcessor, ) -from dashscope.v2.finetune.reinforcement.component.func_decorator import ( +from dashscope.finetune.reinforcement.component.func_decorator import ( reward_func, sub_reward_func, aggregate_func, @@ -47,7 +47,7 @@ # -------------------------------------------------------------------------- # # Func Manager # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.func_manager import ( +from dashscope.finetune.reinforcement.component.func_manager import ( FuncManager, ) @@ -55,30 +55,30 @@ # Observability # # -------------------------------------------------------------------------- # # pylint: disable=no-name-in-module -from dashscope.v2.finetune.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) # -------------------------------------------------------------------------- # # Parsers # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( +from dashscope.finetune.reinforcement.component.parser.reward_parser import ( RewardRequestParser, ) -from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( +from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( RolloutRequestParser, ) # -------------------------------------------------------------------------- # # Processors # # -------------------------------------------------------------------------- # -from dashscope.v2.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) -from dashscope.v2.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/data/__init__.py b/dashscope/finetune/reinforcement/component/data/__init__.py similarity index 70% rename from dashscope/v2/finetune/reinforcement/component/data/__init__.py rename to dashscope/finetune/reinforcement/component/data/__init__.py index 51727288..6277aa74 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/__init__.py +++ b/dashscope/finetune/reinforcement/component/data/__init__.py @@ -2,7 +2,7 @@ # ========================================================================== # # Base Components # # ========================================================================== # -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -12,10 +12,10 @@ RequestMetadata, AgentOutput, ) -from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) @@ -23,21 +23,21 @@ # ========================================================================== # # Input Models # # ========================================================================== # -from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) # ========================================================================== # # Output Models # # ========================================================================== # -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) diff --git a/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py b/dashscope/finetune/reinforcement/component/data/base_data_model.py similarity index 98% rename from dashscope/v2/finetune/reinforcement/component/data/base_data_model.py rename to dashscope/finetune/reinforcement/component/data/base_data_model.py index 1b59e38b..c1970c53 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/finetune/reinforcement/component/data/base_data_model.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) diff --git a/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/finetune/reinforcement/component/data/group_reward_input.py similarity index 90% rename from dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py rename to dashscope/finetune/reinforcement/component/data/group_reward_input.py index dc03f39d..acd897f6 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/finetune/reinforcement/component/data/group_reward_input.py @@ -9,10 +9,10 @@ from typing import Any, List, Optional from pydantic import ConfigDict, Field -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py b/dashscope/finetune/reinforcement/component/data/group_reward_output.py similarity index 90% rename from dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py rename to dashscope/finetune/reinforcement/component/data/group_reward_output.py index a1fdd8bc..07208c84 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py +++ b/dashscope/finetune/reinforcement/component/data/group_reward_output.py @@ -8,10 +8,10 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( Reward, ) diff --git a/dashscope/v2/finetune/reinforcement/component/data/reward_input.py b/dashscope/finetune/reinforcement/component/data/reward_input.py similarity index 89% rename from dashscope/v2/finetune/reinforcement/component/data/reward_input.py rename to dashscope/finetune/reinforcement/component/data/reward_input.py index ef7435a7..f21bd71e 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/finetune/reinforcement/component/data/reward_input.py @@ -9,10 +9,10 @@ from typing import Any, Optional from pydantic import ConfigDict, Field -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/v2/finetune/reinforcement/component/data/reward_output.py b/dashscope/finetune/reinforcement/component/data/reward_output.py similarity index 96% rename from dashscope/v2/finetune/reinforcement/component/data/reward_output.py rename to dashscope/finetune/reinforcement/component/data/reward_output.py index 1264d780..93c3061c 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/reward_output.py +++ b/dashscope/finetune/reinforcement/component/data/reward_output.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, _normalize_error_code, ) diff --git a/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py b/dashscope/finetune/reinforcement/component/data/rollout_input.py similarity index 93% rename from dashscope/v2/finetune/reinforcement/component/data/rollout_input.py rename to dashscope/finetune/reinforcement/component/data/rollout_input.py index 485da9ef..ea7bfa1a 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/finetune/reinforcement/component/data/rollout_input.py @@ -9,10 +9,10 @@ from typing import Any, Dict, List, Optional from pydantic import ConfigDict, Field -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ModelResource, RequestMetadata, diff --git a/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py b/dashscope/finetune/reinforcement/component/data/rollout_output.py similarity index 95% rename from dashscope/v2/finetune/reinforcement/component/data/rollout_output.py rename to dashscope/finetune/reinforcement/component/data/rollout_output.py index a2518126..1b923558 100644 --- a/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py +++ b/dashscope/finetune/reinforcement/component/data/rollout_output.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, AgentOutput, _normalize_error_code, diff --git a/dashscope/finetune/reinforcement/component/demo/__init__.py b/dashscope/finetune/reinforcement/component/demo/__init__.py new file mode 100644 index 00000000..6100812f --- /dev/null +++ b/dashscope/finetune/reinforcement/component/demo/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from dashscope.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoGroupRewardProcessor, +) +from dashscope.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRewardProcessor, +) +from dashscope.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRolloutProcessor, +) + +__all__ = [ + "DemoRewardProcessor", + # Inherits AbstractRewardProcessor, returns RewardOutput + "DemoRolloutProcessor", + # Inherits AbstractRolloutProcessor, returns RolloutOutput + "DemoGroupRewardProcessor", + # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput +] diff --git a/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py similarity index 85% rename from dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py rename to dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py index dd3476d9..a43688d6 100644 --- a/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -6,21 +6,21 @@ Demonstrates rule-based scoring for multiple Agent outputs in a group. """ -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( Reward, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py similarity index 85% rename from dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py rename to dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py index 2ac8bc01..d99ad89a 100644 --- a/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -6,18 +6,18 @@ Demonstrates rule-based scoring for Agent outputs. """ -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py similarity index 85% rename from dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py rename to dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py index 51a5f74f..8099cc18 100644 --- a/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -8,18 +8,18 @@ import time -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/func_decorator.py b/dashscope/finetune/reinforcement/component/func_decorator.py similarity index 98% rename from dashscope/v2/finetune/reinforcement/component/func_decorator.py rename to dashscope/finetune/reinforcement/component/func_decorator.py index 26a99c23..e10d9464 100644 --- a/dashscope/v2/finetune/reinforcement/component/func_decorator.py +++ b/dashscope/finetune/reinforcement/component/func_decorator.py @@ -4,8 +4,8 @@ from functools import wraps from typing import Callable, Dict, Optional, Type -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, Reward, diff --git a/dashscope/v2/finetune/reinforcement/component/func_manager.py b/dashscope/finetune/reinforcement/component/func_manager.py similarity index 93% rename from dashscope/v2/finetune/reinforcement/component/func_manager.py rename to dashscope/finetune/reinforcement/component/func_manager.py index d135ff43..048b5f13 100644 --- a/dashscope/v2/finetune/reinforcement/component/func_manager.py +++ b/dashscope/finetune/reinforcement/component/func_manager.py @@ -20,31 +20,31 @@ from functools import partial from typing import Any, Dict, Optional, Type -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( +from dashscope.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, trace_processor_process, async_trace_processor_process, ) -from dashscope.v2.finetune.reinforcement.component.parser import ( +from dashscope.finetune.reinforcement.component.parser import ( BaseRequestParser, ) -from dashscope.v2.finetune.reinforcement.component.parser import ( +from dashscope.finetune.reinforcement.component.parser import ( GroupRewardRequestParser, ) -from dashscope.v2.finetune.reinforcement.component.parser import ( +from dashscope.finetune.reinforcement.component.parser import ( RewardRequestParser, ) -from dashscope.v2.finetune.reinforcement.component.parser import ( +from dashscope.finetune.reinforcement.component.parser import ( RolloutRequestParser, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/__init__.py b/dashscope/finetune/reinforcement/component/observability/__init__.py similarity index 91% rename from dashscope/v2/finetune/reinforcement/component/observability/__init__.py rename to dashscope/finetune/reinforcement/component/observability/__init__.py index 5701ec1e..96a00c34 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/__init__.py +++ b/dashscope/finetune/reinforcement/component/observability/__init__.py @@ -4,7 +4,7 @@ This module provides a *stable public surface* while keeping heavy observability dependencies lazily imported. User-facing imports such as:: - from dashscope.v2.finetune.reinforcement.component.observability import ( + from dashscope.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, @@ -81,7 +81,7 @@ def __getattr__(name: str) -> Any: "trace_client", "trace_tool", }: - from dashscope.v2.finetune.reinforcement.component.observability import ( + from dashscope.finetune.reinforcement.component.observability import ( genai as _genai, ) @@ -89,7 +89,7 @@ def __getattr__(name: str) -> Any: # Processor-span helpers if name in {"observe_processor", "trace_processor_span"}: - from dashscope.v2.finetune.reinforcement.component.observability import ( + from dashscope.finetune.reinforcement.component.observability import ( processor_span as _processor_span, ) @@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any: "span_payload_preview", "trace_processor_process", }: - from dashscope.v2.finetune.reinforcement.component.observability import ( + from dashscope.finetune.reinforcement.component.observability import ( tracing as _tracing, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/finetune/reinforcement/component/observability/genai/__init__.py similarity index 79% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py rename to dashscope/finetune/reinforcement/component/observability/genai/__init__.py index 48a15a6a..2beb244e 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/__init__.py @@ -4,13 +4,13 @@ Submodules by responsibility: -- :mod:`~dashscope.reinforcement.component.observability.genai.messages` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 — message and tool_calls mapping -- :mod:`~dashscope.reinforcement.component.observability.genai.llm_openai` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 — OpenAI-compatible LLM client patching -- :mod:`~dashscope.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 — DashScope Generation patching -- :mod:`~dashscope.reinforcement.component.observability.genai.tools` +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.tools` — tool tracing (decorator + patcher) Requires ``pip install loongsuite-util-genai`` and tracing env flags @@ -18,30 +18,30 @@ Optional: ``AGENTIC_RL_LOG_TRACE_ID=true`` enables ``trace_id`` diagnostic logs (default off); -see :mod:`dashscope.reinforcement.component.observability.tracing`. +see :mod:`dashscope.finetune.reinforcement.component.observability.tracing`. """ from __future__ import annotations from typing import Any -from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long instrument_dashscope_generation_call, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long instrument_openai_chat_completions, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long genai_llm_span, observe_llm, run_with_genai_llm_span, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long observe_tool, trace_tool, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py b/dashscope/finetune/reinforcement/component/observability/genai/_core.py similarity index 100% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py rename to dashscope/finetune/reinforcement/component/observability/genai/_core.py diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py similarity index 93% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py rename to dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py index b72c7d40..19232401 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -7,16 +7,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py similarity index 98% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py rename to dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py index 0a0dd205..ecc6882b 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -16,17 +16,17 @@ from opentelemetry import trace as otel_trace from opentelemetry.trace.status import Status, StatusCode -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long openai_chat_messages_to_input_messages, openai_completion_to_output_messages, unwrap_openai_completion, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( +from dashscope.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py b/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py similarity index 95% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py rename to dashscope/finetune/reinforcement/component/observability/genai/llm_span.py index 645a39d0..635725a9 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py @@ -12,16 +12,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( +from dashscope.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/finetune/reinforcement/component/observability/genai/messages.py similarity index 98% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py rename to dashscope/finetune/reinforcement/component/observability/genai/messages.py index e1824654..44abe0f5 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/messages.py @@ -27,9 +27,9 @@ import os from typing import Any, Dict, List, Optional -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.utils import deep_mask -from dashscope.v2.finetune.reinforcement.component.observability import genai +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.utils import deep_mask +from dashscope.finetune.reinforcement.component.observability import genai # pylint: disable=protected-access _core = genai._core diff --git a/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py b/dashscope/finetune/reinforcement/component/observability/genai/tools.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py rename to dashscope/finetune/reinforcement/component/observability/genai/tools.py index c6c260e5..c0d48b85 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py +++ b/dashscope/finetune/reinforcement/component/observability/genai/tools.py @@ -50,7 +50,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.v2.finetune.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( genai, tracing, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py b/dashscope/finetune/reinforcement/component/observability/processor_span.py similarity index 97% rename from dashscope/v2/finetune/reinforcement/component/observability/processor_span.py rename to dashscope/finetune/reinforcement/component/observability/processor_span.py index 3014ab13..65fc3824 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/finetune/reinforcement/component/observability/processor_span.py @@ -34,11 +34,11 @@ from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar, cast -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( +from dashscope.finetune.reinforcement.component.observability.tracing import ( _REWARD_SPAN_PREFIX, _ROLLOUT_SPAN_PREFIX, apply_processor_span_attributes_after, @@ -233,7 +233,7 @@ def observe_processor( Replaces the explicit ``@trace_processor_span(logical_kind=...)`` style. """ - from dashscope.v2.finetune.reinforcement.component.processor import ( + from dashscope.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/observability/tracing.py b/dashscope/finetune/reinforcement/component/observability/tracing.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/component/observability/tracing.py rename to dashscope/finetune/reinforcement/component/observability/tracing.py index 447109e4..288de269 100644 --- a/dashscope/v2/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/finetune/reinforcement/component/observability/tracing.py @@ -36,11 +36,11 @@ from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) @@ -66,7 +66,7 @@ # OpenTelemetry instrumentation scope name. Renaming it would split # already-exported traces across two scopes, so it keeps the published -# name even though the package now lives under ``dashscope.v2``. +# name even though the package now lives under ``dashscope``. _TRACER_NAME = "dashscope.finetune.reinforcement" # I/O preview limits for span attributes. _IO_MAX_LEN = 128 * 1024 # 128KB per input/output preview diff --git a/dashscope/finetune/reinforcement/component/parser/__init__.py b/dashscope/finetune/reinforcement/component/parser/__init__.py new file mode 100644 index 00000000..6f5dedd0 --- /dev/null +++ b/dashscope/finetune/reinforcement/component/parser/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.finetune.reinforcement.component.parser.base_parser import ( + BaseRequestParser, +) +from dashscope.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 + GroupRewardRequestParser, +) +from dashscope.finetune.reinforcement.component.parser.reward_parser import ( + RewardRequestParser, +) +from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( + RolloutRequestParser, +) + +__all__ = [ + "BaseRequestParser", + "RewardRequestParser", + "RolloutRequestParser", + "GroupRewardRequestParser", +] diff --git a/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py b/dashscope/finetune/reinforcement/component/parser/base_parser.py similarity index 90% rename from dashscope/v2/finetune/reinforcement/component/parser/base_parser.py rename to dashscope/finetune/reinforcement/component/parser/base_parser.py index 0d7d3b6f..fac27693 100644 --- a/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py +++ b/dashscope/finetune/reinforcement/component/parser/base_parser.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py b/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py similarity index 81% rename from dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py rename to dashscope/finetune/reinforcement/component/parser/group_reward_parser.py index e03c850e..aceb78d7 100644 --- a/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py +++ b/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py b/dashscope/finetune/reinforcement/component/parser/reward_parser.py similarity index 79% rename from dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py rename to dashscope/finetune/reinforcement/component/parser/reward_parser.py index c8d36b51..0dff8195 100644 --- a/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py +++ b/dashscope/finetune/reinforcement/component/parser/reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py b/dashscope/finetune/reinforcement/component/parser/rollout_parser.py similarity index 79% rename from dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py rename to dashscope/finetune/reinforcement/component/parser/rollout_parser.py index 738b6104..0aaed03b 100644 --- a/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py +++ b/dashscope/finetune/reinforcement/component/parser/rollout_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/processor/__init__.py b/dashscope/finetune/reinforcement/component/processor/__init__.py new file mode 100644 index 00000000..9246fa56 --- /dev/null +++ b/dashscope/finetune/reinforcement/component/processor/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractGroupRewardProcessor, +) +from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractProcessor, +) +from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRewardProcessor, +) +from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRolloutProcessor, +) + +__all__ = [ + "AbstractProcessor", + "AbstractRewardProcessor", + "AbstractRolloutProcessor", + "AbstractGroupRewardProcessor", +] diff --git a/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py b/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py similarity index 90% rename from dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py rename to dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py index df618859..3c166be8 100644 --- a/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py +++ b/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.v2.finetune.reinforcement.component.data import ( +from dashscope.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data import ( +from dashscope.finetune.reinforcement.component.data import ( GroupRewardOutput, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py b/dashscope/finetune/reinforcement/component/processor/abstract_processor.py similarity index 94% rename from dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py rename to dashscope/finetune/reinforcement/component/processor/abstract_processor.py index f4fd4f5a..8f13afd3 100644 --- a/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py +++ b/dashscope/finetune/reinforcement/component/processor/abstract_processor.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py b/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py similarity index 95% rename from dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py rename to dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py index c9b4aec6..0b64262f 100644 --- a/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py +++ b/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py @@ -14,18 +14,18 @@ from concurrent.futures import ThreadPoolExecutor, Executor from typing import Optional, Dict -from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( RewardOutput, ) -from dashscope.v2.finetune.reinforcement.component.func_decorator import ( +from dashscope.finetune.reinforcement.component.func_decorator import ( RewardProcessorMeta, SubRewardFunction, AggregateFunction, ) -from dashscope.v2.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py b/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py similarity index 88% rename from dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py rename to dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py index bace5d53..9c6dace9 100644 --- a/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py +++ b/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/threads/messages/__init__.py b/dashscope/finetune/reinforcement/component/server/__init__.py similarity index 100% rename from dashscope/threads/messages/__init__.py rename to dashscope/finetune/reinforcement/component/server/__init__.py diff --git a/dashscope/v2/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py similarity index 94% rename from dashscope/v2/finetune/reinforcement/component/server/server.py rename to dashscope/finetune/reinforcement/component/server/server.py index c1b5763d..947727d9 100644 --- a/dashscope/v2/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -10,7 +10,7 @@ FUNC_TYPE Function type, current supported values: "reward"/"rollout" (required) PROCESSOR_CLASS Full Python path to processor class, e.g.: -dashscope.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor +dashscope.finetune.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor (required, processor instantiated using this class) SERVER_PORT Server port, default 8000 ENABLE_LOGGING Enable verbose logging, "true"/"1" to enable, @@ -22,14 +22,14 @@ == Startup Methods == # Recommended: Run with python -m - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor python -m - dashscope.reinforcement.component.server.server + dashscope.finetune.reinforcement.component.server.server # Using uvicorn - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor uvicorn - dashscope.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + dashscope.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 == Endpoints == @@ -47,19 +47,19 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.utils import ( +from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.utils import ( get_fc_request_id, get_business_summary, ) -from dashscope.v2.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.v2.finetune.reinforcement.component import BaseDataModel -from dashscope.v2.finetune.reinforcement.component.func_manager import ( +from dashscope.finetune.reinforcement.component import BaseDataModel +from dashscope.finetune.reinforcement.component.func_manager import ( FuncManager, ) -from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( +from dashscope.finetune.reinforcement.component.observability.tracing import ( ensure_agentic_rl_baggage_span_processor, is_tracing_enabled, maybe_force_flush_async, @@ -120,7 +120,7 @@ def _resolve_func_type(raw: str) -> FuncType: if not _PROCESSOR_CLASS_ENV: raise RuntimeError( "Environment variable PROCESSOR_CLASS is required. " - "Example: PROCESSOR_CLASS=dashscope.reinforcement.component.demo." + "Example: PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo." "reward_processor_demo.DemoRewardProcessor", ) @@ -590,7 +590,7 @@ async def health_check_endpoint() -> JSONResponse: if __name__ == "__main__": # Direct execution entry point for FastAPI server. # Alternative startup using uvicorn: - # uvicorn dashscope.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + # uvicorn dashscope.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 import uvicorn import sys import argparse @@ -627,7 +627,7 @@ async def health_check_endpoint() -> JSONResponse: # uvicorn resolves this import string inside its own worker # process, so it must stay the public path used by # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. - "dashscope.reinforcement.component.server.server:app", + "dashscope.finetune.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, workers=worker_count, diff --git a/dashscope/threads/runs/__init__.py b/dashscope/finetune/reinforcement/examples/observability/__init__.py similarity index 100% rename from dashscope/threads/runs/__init__.py rename to dashscope/finetune/reinforcement/examples/observability/__init__.py diff --git a/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py similarity index 92% rename from dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py rename to dashscope/finetune/reinforcement/examples/observability/smoke_observability.py index 2ebcb61e..d34cc349 100644 --- a/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py +++ b/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py @@ -51,33 +51,33 @@ InMemorySpanExporter, ) -from dashscope.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, ModelResource, ) -from dashscope.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.reinforcement.component.func_manager import FuncManager -from dashscope.reinforcement.component.observability.genai import ( +from dashscope.finetune.reinforcement.component.func_manager import FuncManager +from dashscope.finetune.reinforcement.component.observability.genai import ( observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long observe_processor, ) -from dashscope.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) -from dashscope.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md b/dashscope/finetune/reinforcement/examples/workspace/README-zh.md similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md rename to dashscope/finetune/reinforcement/examples/workspace/README-zh.md diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/README.md b/dashscope/finetune/reinforcement/examples/workspace/README.md similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/README.md rename to dashscope/finetune/reinforcement/examples/workspace/README.md diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md b/dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md similarity index 98% rename from dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md rename to dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md index f6788070..7d5b295e 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md +++ b/dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md @@ -118,7 +118,7 @@ export LOG_LEVEL="CRITICAL" 生成智能体轨迹(与环境/LLM 的交互)。 ```python -from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput class DemoRolloutProcessor(AbstractRolloutProcessor): async def process(self, input: RolloutInput) -> RolloutOutput: @@ -130,7 +130,7 @@ class DemoRolloutProcessor(AbstractRolloutProcessor): 评分单个步骤或最终输出。 ```python -from dashscope.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput +from dashscope.finetune.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput class DemoRewardProcessor(AbstractRewardProcessor): def process(self, input: RewardInput) -> RewardOutput: @@ -140,7 +140,7 @@ class DemoRewardProcessor(AbstractRewardProcessor): #### 使用装饰器的高级 Reward 处理器 ```python -from dashscope.reinforcement import reward_func, sub_reward_func, aggregate_func +from dashscope.finetune.reinforcement import reward_func, sub_reward_func, aggregate_func @reward_func("SafetyProcessor") class SafetyProcessor(AbstractRewardProcessor): @@ -164,7 +164,7 @@ class SafetyProcessor(AbstractRewardProcessor): 批量评分轨迹(例如用于排名)。 ```python -from dashscope.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput +from dashscope.finetune.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): def setup(self) -> None: @@ -184,7 +184,7 @@ class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): 2. 确认未显式关闭链路跟踪(`ENABLE_TRAJECTORY=false` 会关闭)。 #### 检测装饰器 -从 `dashscope.reinforcement.component.observability` 导入。 +从 `dashscope.finetune.reinforcement.component.observability` 导入。 | 装饰器 | 用法 | 描述 | | :--- | :--- | :--- | @@ -226,9 +226,9 @@ class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): ```python import openai -from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput -from dashscope.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus -from dashscope.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md b/dashscope/finetune/reinforcement/examples/workspace/UserGuide.md similarity index 98% rename from dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md rename to dashscope/finetune/reinforcement/examples/workspace/UserGuide.md index 1c090d4a..aded12a0 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md +++ b/dashscope/finetune/reinforcement/examples/workspace/UserGuide.md @@ -117,7 +117,7 @@ Functions must inherit from the abstract base classes provided by the SDK. Generates agent trajectories (interactions with the environment/LLM). ```python -from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput class DemoRolloutProcessor(AbstractRolloutProcessor): async def process(self, input: RolloutInput) -> RolloutOutput: @@ -129,7 +129,7 @@ class DemoRolloutProcessor(AbstractRolloutProcessor): Scores individual steps or final outputs. ```python -from dashscope.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput +from dashscope.finetune.reinforcement import AbstractRewardProcessor, RewardInput, RewardOutput class DemoRewardProcessor(AbstractRewardProcessor): def process(self, input: RewardInput) -> RewardOutput: @@ -139,7 +139,7 @@ class DemoRewardProcessor(AbstractRewardProcessor): #### Advanced Reward Processor with Decorators ```python -from dashscope.reinforcement import reward_func, sub_reward_func, aggregate_func +from dashscope.finetune.reinforcement import reward_func, sub_reward_func, aggregate_func @reward_func("SafetyProcessor") class SafetyProcessor(AbstractRewardProcessor): @@ -163,7 +163,7 @@ class SafetyProcessor(AbstractRewardProcessor): Scores a batch of trajectories collectively (e.g., for ranking). ```python -from dashscope.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput +from dashscope.finetune.reinforcement import AbstractGroupRewardProcessor, GroupRewardInput, GroupRewardOutput class DemoGroupRewardProcessor(AbstractGroupRewardProcessor): def setup(self) -> None: @@ -182,7 +182,7 @@ Enable deep visibility into your agent's execution using OpenTelemetry. Trace da 1. Add observability dependencies to `requirements.txt` (see Section 2.5, “Dependencies for Observability (Tracing)”). #### Instrumentation Decorators -Import from `dashscope.reinforcement.component.observability`. +Import from `dashscope.finetune.reinforcement.component.observability`. | Decorator | Usage | Description | | :--- | :--- | :--- | @@ -217,9 +217,9 @@ Import from `dashscope.reinforcement.component.observability`. ```python import openai -from dashscope.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput -from dashscope.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus -from dashscope.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement import AbstractRolloutProcessor, RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement.component.data.base_data_model import AgentOutput, TaskStatus +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh b/dashscope/finetune/reinforcement/examples/workspace/cli.sh similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh rename to dashscope/finetune/reinforcement/examples/workspace/cli.sh diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl b/dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl rename to dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl b/dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl rename to dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl diff --git a/dashscope/v2/finetune/reinforcement/common/__init__.py b/dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py similarity index 100% rename from dashscope/v2/finetune/reinforcement/common/__init__.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py similarity index 95% rename from dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py index 6c197919..0aad8c64 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py +++ b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py @@ -8,15 +8,15 @@ from __future__ import annotations -from dashscope.reinforcement import ( +from dashscope.finetune.reinforcement import ( GroupRewardInput, GroupRewardOutput, GroupReward, Reward, TaskStatus, ) -from dashscope.reinforcement import logger -from dashscope.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.processor import ( AbstractGroupRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py similarity index 90% rename from dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py index 9c6f42d5..aef38047 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py +++ b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py @@ -16,20 +16,20 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.reinforcement import RewardInput, RewardOutput -from dashscope.reinforcement import logger -from dashscope.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import RewardInput, RewardOutput +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( Reward, ) # pylint: disable=no-name-in-module -from dashscope.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py similarity index 93% rename from dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py index 1e664137..394e7d5a 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py +++ b/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py @@ -12,15 +12,15 @@ import time from typing import Dict -from dashscope.reinforcement import AbstractRewardProcessor -from dashscope.reinforcement import ( +from dashscope.finetune.reinforcement import AbstractRewardProcessor +from dashscope.finetune.reinforcement import ( RewardInput, RewardOutput, Reward, TaskStatus, ) -from dashscope.reinforcement import logger -from dashscope.reinforcement import ( +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement import ( reward_func, sub_reward_func, aggregate_func, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py similarity index 97% rename from dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py index 75acf817..12ece959 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py +++ b/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py @@ -29,19 +29,19 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.reinforcement import RolloutInput, RolloutOutput -from dashscope.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, ) -from dashscope.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) @@ -438,7 +438,7 @@ def __del__(self): if __name__ == "__main__": - from dashscope.reinforcement.component.data.base_data_model import ( # noqa: E501 + from dashscope.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 ModelResource, ) import os diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py similarity index 98% rename from dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py rename to dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py index 7b838993..14cd00bb 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py +++ b/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py @@ -36,22 +36,22 @@ from types import SimpleNamespace from typing import Any, Dict, List -from dashscope.reinforcement import RolloutInput, RolloutOutput -from dashscope.reinforcement import logger -from dashscope.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml b/dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml rename to dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt b/dashscope/finetune/reinforcement/examples/workspace/requirements.txt similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt rename to dashscope/finetune/reinforcement/examples/workspace/requirements.txt diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json b/dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json rename to dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json b/dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json rename to dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json b/dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json rename to dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json b/dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json rename to dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml b/dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml rename to dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/finetune/reinforcement/examples/workspace/submit_job.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py rename to dashscope/finetune/reinforcement/examples/workspace/submit_job.py index 5f13b8e3..7311f4ad 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/finetune/reinforcement/examples/workspace/submit_job.py @@ -49,7 +49,7 @@ # await client.run() # Uses YAML configuration from dashscope.finetune.agentic_rl import AgenticRL -from dashscope.reinforcement import ( +from dashscope.finetune.reinforcement import ( logger, DataSourceType, TrainingDataset, diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/finetune/reinforcement/examples/workspace/test_functions.py similarity index 99% rename from dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py rename to dashscope/finetune/reinforcement/examples/workspace/test_functions.py index 5645828d..56b84e7d 100644 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py +++ b/dashscope/finetune/reinforcement/examples/workspace/test_functions.py @@ -37,7 +37,7 @@ import json -from dashscope.reinforcement import ( +from dashscope.finetune.reinforcement import ( logger, TaskStatus, FunctionType, diff --git a/dashscope/io/__init__.py b/dashscope/io/__init__.py deleted file mode 100644 index 75c5b903..00000000 --- a/dashscope/io/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.io``. - -Import from ``dashscope.legacy_1x.io`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.io") diff --git a/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py index d493a41e..ab833bf1 100644 --- a/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py +++ b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py @@ -10,7 +10,7 @@ from dashscope.legacy_1x.client.base_api import BaseAsyncApi from dashscope.legacy_1x.common.error import InvalidParameter from dashscope.legacy_1x.common.logging import logger -from dashscope.finetune.finetunes import FineTunes +from dashscope.legacy_1x.finetune.finetunes import FineTunes class AsrPhraseManager(BaseAsyncApi): diff --git a/dashscope/legacy_1x/cli/agentic_rl.py b/dashscope/legacy_1x/cli/agentic_rl.py index f6b8fbdd..827c86ac 100644 --- a/dashscope/legacy_1x/cli/agentic_rl.py +++ b/dashscope/legacy_1x/cli/agentic_rl.py @@ -17,7 +17,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table -from dashscope.finetune.agentic_rl import AgenticRL, SubmitResult +from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL, SubmitResult from dashscope.finetune.reinforcement import ( serialize_for_output, FunctionType, diff --git a/dashscope/v2/finetune/reinforcement/component/server/__init__.py b/dashscope/legacy_1x/finetune/__init__.py similarity index 100% rename from dashscope/v2/finetune/reinforcement/component/server/__init__.py rename to dashscope/legacy_1x/finetune/__init__.py diff --git a/dashscope/legacy_1x/finetune/agentic_rl.py b/dashscope/legacy_1x/finetune/agentic_rl.py new file mode 100644 index 00000000..804adb43 --- /dev/null +++ b/dashscope/legacy_1x/finetune/agentic_rl.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim plus the 1.x surface; the implementation moved to +``dashscope.finetune.agentic_rl``. + +The agentic-RL workflow is owned by the 2.0 SDK, while the ``dashscope`` +CLI and acli skill docs use this 1.x import path. + +What stays here is the half of the surface that belongs to 1.x -- the +five job-management verbs, which delegate to ``dashscope.FineTunes`` (a +1.x root public API, alongside ``Deployments``), and the ``FineTune`` +dataclasses they return. ``submit_job`` therefore returns a ``FineTune`` +from this path and a ``SubmitResult`` from the 2.0 one; the payload and +the POST are built once, in the 2.0 class. + +Import from ``dashscope.finetune.agentic_rl`` in new code. +""" + +from typing import Dict, List, Optional, Union + +from dashscope.legacy_1x.finetune.customize_types import ( + FineTune, + FineTuneCancel, + FineTuneDelete, + FineTuneList, +) +from dashscope.legacy_1x.finetune.finetunes import FineTunes +from dashscope.finetune.agentic_rl import AgenticRL as _AgenticRLBase +from dashscope.finetune.agentic_rl import SubmitOutput, SubmitResult + +# Re-export every name this module historically carried. +# pylint: disable-next=unused-import +from dashscope.finetune.reinforcement import ( # noqa: F401 + DASHSCOPE_HTTP_BASE_URL, + AgenticRLFunctionComponent, + AgenticRLTuning, + Dataset, + DatasetsType, + FunctionType, + GroupRewardInput, + RewardFunctionComponent, + RewardInput, + RolloutFunctionComponent, + RolloutInput, + TrainingDataset, + TrainingType, + TuningModel, + ValidationDataset, + deep_remove_none, + generate_random_id, + get_func_type_id, + logger, + set_api_key, +) + +# pylint: disable-next=unused-import +from dashscope.finetune.reinforcement.common.errors import ( # noqa: F401 + DatasetsError, + InstanceQueryError, + RegistrationError, + RuntimeErrorWithCode, + ValidationError, + ValueErrorWithCode, +) + +__all__ = [ + "AgenticRL", + "DASHSCOPE_HTTP_BASE_URL", + "FineTune", + "FineTuneCancel", + "FineTuneDelete", + "FineTuneList", + "FineTunes", + "SubmitOutput", + "SubmitResult", +] + + +class AgenticRL(_AgenticRLBase): + """The 1.x surface: ``FineTune`` returns plus the ``FineTunes`` verbs. + + The five verbs below are 1.x-only. ``Client(...).rl.jobs`` already + implements the same lifecycle natively on the 2.0 transport, so + duplicating them into the 2.0 class would have given the SDK two + copies of every job verb. + """ + + def submit_job( # type: ignore[override] + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> FineTune: + """ + Submit RL tuning job to the platform. + """ + return FineTune( + **self._submit_envelope( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ), + ) + + @classmethod + def cancel( + cls, + job_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> FineTuneCancel: + """Cancel a running fine-tune job.""" + kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL + + return FineTunes.cancel( + job_id, + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def list( + cls, + page_no=1, + page_size=10, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> FineTuneList: + """List fine-tune jobs.""" + kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL + + return FineTunes.list( + page_no=page_no, + page_size=page_size, + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def get( + cls, + job_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> FineTune: + """Get fine-tune job information.""" + kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL + + return FineTunes.get( + job_id, + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def delete( + cls, + job_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> FineTuneDelete: + """Delete a fine-tune job.""" + kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL + + return FineTunes.delete( + job_id, + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def logs( + cls, + job_id: str, + offset: int = 1, + lines: int = 1000, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> FineTune: + """Get job logs.""" + kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL + + return FineTunes.logs( + job_id, + offset=offset, + line=lines, + api_key=api_key, + workspace=workspace, + **kwargs, + ) diff --git a/dashscope/finetune/customize_types.py b/dashscope/legacy_1x/finetune/customize_types.py similarity index 100% rename from dashscope/finetune/customize_types.py rename to dashscope/legacy_1x/finetune/customize_types.py diff --git a/dashscope/finetune/deployments.py b/dashscope/legacy_1x/finetune/deployments.py similarity index 98% rename from dashscope/finetune/deployments.py rename to dashscope/legacy_1x/finetune/deployments.py index 0a06ed46..dcf0eff3 100644 --- a/dashscope/finetune/deployments.py +++ b/dashscope/legacy_1x/finetune/deployments.py @@ -10,7 +10,7 @@ PutMixin, StreamEventMixin, ) -from dashscope.finetune.customize_types import ( +from dashscope.legacy_1x.finetune.customize_types import ( Deployment, DeploymentDelete, DeploymentList, diff --git a/dashscope/finetune/finetunes.py b/dashscope/legacy_1x/finetune/finetunes.py similarity index 99% rename from dashscope/finetune/finetunes.py rename to dashscope/legacy_1x/finetune/finetunes.py index 83745783..49f959da 100644 --- a/dashscope/finetune/finetunes.py +++ b/dashscope/legacy_1x/finetune/finetunes.py @@ -15,7 +15,7 @@ StreamEventMixin, ) from dashscope.legacy_1x.common.constants import TaskStatus -from dashscope.finetune.customize_types import ( +from dashscope.legacy_1x.finetune.customize_types import ( FineTune, FineTuneCancel, FineTuneDelete, diff --git a/dashscope/model.py b/dashscope/model.py deleted file mode 100644 index b979ec12..00000000 --- a/dashscope/model.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.model``. - -Import from ``dashscope.legacy_1x.model`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.model") diff --git a/dashscope/models.py b/dashscope/models.py deleted file mode 100644 index 9f97c04f..00000000 --- a/dashscope/models.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.models``. - -Import from ``dashscope.legacy_1x.models`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.models") diff --git a/dashscope/multimodal/__init__.py b/dashscope/multimodal/__init__.py deleted file mode 100644 index 61f1c31b..00000000 --- a/dashscope/multimodal/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.multimodal``. - -Import from ``dashscope.legacy_1x.multimodal`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.multimodal") diff --git a/dashscope/nlp/__init__.py b/dashscope/nlp/__init__.py deleted file mode 100644 index 48fa8237..00000000 --- a/dashscope/nlp/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.nlp``. - -Import from ``dashscope.legacy_1x.nlp`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.nlp") diff --git a/dashscope/protocol/__init__.py b/dashscope/protocol/__init__.py deleted file mode 100644 index 4f4774b0..00000000 --- a/dashscope/protocol/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.protocol``. - -Import from ``dashscope.legacy_1x.protocol`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.protocol") diff --git a/dashscope/v2/protocols/__init__.py b/dashscope/protocols/__init__.py similarity index 79% rename from dashscope/v2/protocols/__init__.py rename to dashscope/protocols/__init__.py index 78178b78..d0fe4a7b 100644 --- a/dashscope/v2/protocols/__init__.py +++ b/dashscope/protocols/__init__.py @@ -7,11 +7,11 @@ from typing import Any, Dict, Optional -from dashscope.v2._config import VALID_PROTOCOLS -from dashscope.v2.protocols.anthropic import AnthropicProtocol -from dashscope.v2.protocols.base import ChatProtocol, ChatRequest -from dashscope.v2.protocols.native import NativeProtocol -from dashscope.v2.protocols.openai import OpenAIProtocol +from dashscope._config import VALID_PROTOCOLS +from dashscope.protocols.anthropic import AnthropicProtocol +from dashscope.protocols.base import ChatProtocol, ChatRequest +from dashscope.protocols.native import NativeProtocol +from dashscope.protocols.openai import OpenAIProtocol __all__ = [ "AnthropicProtocol", diff --git a/dashscope/v2/protocols/anthropic.py b/dashscope/protocols/anthropic.py similarity index 99% rename from dashscope/v2/protocols/anthropic.py rename to dashscope/protocols/anthropic.py index 6b7dc49e..913ddde6 100644 --- a/dashscope/v2/protocols/anthropic.py +++ b/dashscope/protocols/anthropic.py @@ -31,9 +31,9 @@ import json from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from dashscope.v2.errors import BadRequestError, DashScopeError, ErrorCode -from dashscope.v2.protocols.base import ChatProtocol, ChatRequest -from dashscope.v2.types.completions import ( +from dashscope.errors import BadRequestError, DashScopeError, ErrorCode +from dashscope.protocols.base import ChatProtocol, ChatRequest +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ) diff --git a/dashscope/v2/protocols/base.py b/dashscope/protocols/base.py similarity index 98% rename from dashscope/v2/protocols/base.py rename to dashscope/protocols/base.py index 5e6a09d6..05ebdd85 100644 --- a/dashscope/v2/protocols/base.py +++ b/dashscope/protocols/base.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: # Annotation-only: avoid importing resources at runtime, which would # cycle back through completions.py into this partially-loaded module. - from dashscope.v2.types.completions import ( + from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ) diff --git a/dashscope/v2/protocols/native.py b/dashscope/protocols/native.py similarity index 98% rename from dashscope/v2/protocols/native.py rename to dashscope/protocols/native.py index 59976a33..f3135316 100644 --- a/dashscope/v2/protocols/native.py +++ b/dashscope/protocols/native.py @@ -6,8 +6,8 @@ from typing import Any, Dict, List, Optional, Union -from dashscope.v2.protocols.base import ChatProtocol, ChatRequest -from dashscope.v2.types.completions import ( +from dashscope.protocols.base import ChatProtocol, ChatRequest +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, parse_chunk, diff --git a/dashscope/v2/protocols/openai.py b/dashscope/protocols/openai.py similarity index 98% rename from dashscope/v2/protocols/openai.py rename to dashscope/protocols/openai.py index 4a2f1a68..049ad6a9 100644 --- a/dashscope/v2/protocols/openai.py +++ b/dashscope/protocols/openai.py @@ -13,9 +13,9 @@ from typing import Any, Dict, List, Optional, Union -from dashscope.v2.errors import DashScopeError -from dashscope.v2.protocols.base import ChatProtocol, ChatRequest -from dashscope.v2.types.completions import ( +from dashscope.errors import DashScopeError +from dashscope.protocols.base import ChatProtocol, ChatRequest +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ) diff --git a/dashscope/reinforcement/__init__.py b/dashscope/reinforcement/__init__.py deleted file mode 100644 index a93a843a..00000000 --- a/dashscope/reinforcement/__init__.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility shim; the implementation lives in -``dashscope.v2.finetune.reinforcement``. - -This is the public reinforcement import path. The former -``dashscope.finetune.reinforcement`` path registers itself as an additional -alias so existing Function Compute sandboxes and local launch scripts keep -working. - -Aliasing cannot be eager. Nine of the package's modules -- -``component.server.*`` and ``component.observability.genai.*`` -- are -imported lazily and need fastapi / opentelemetry, both optional. Walking -the tree up front would either miss them or promote two optional -dependencies to import-time hard ones, so a ``sys.meta_path`` finder aliases -each submodule on demand and hands back the *same module object* the v2 path -uses. -""" - -import importlib -import importlib.util -import sys - -# Importing the package runs its own dependency guard, so a missing -# pydantic / tenacity / pyyaml still raises from this path. -from dashscope.v2.finetune import reinforcement as _impl - -# pylint: disable-next=wildcard-import,unused-wildcard-import -from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 - -# The wildcard only carries the names in the implementation's __all__, -# which lists classes and helpers but not its subpackages. Binding them -# here keeps the deep imports the samples use statically resolvable for -# pylint and IDEs, to whom the meta_path finder below is invisible. At -# runtime both are already imported by the package's own eager imports. -# pylint: disable-next=unused-import -from dashscope.v2.finetune.reinforcement import ( # noqa: F401 - common, - component, -) - -_PUBLIC = __name__ -_IMPL = _impl.__name__ - -# ``app`` is the 1.x CLI's typer app and is reachable from the compatibility -# paths only; the 2.0 package must not depend on the CLI. It stays lazy -# because resolving it pulls in typer and the rest of the CLI. -# pylint: disable-next=undefined-all-variable -__all__ = list(_impl.__all__) + ["app"] # noqa: F405 - - -def __getattr__(name): - if name == "app": - from dashscope.legacy_1x.cli.agentic_rl import app - - return app - raise AttributeError(f"module {_PUBLIC!r} has no attribute {name!r}") - - -def _restore_identity(module, v2_spec): - """Put the v2 identity back on a module object all paths share.""" - module.__name__ = v2_spec.name - module.__spec__ = v2_spec - module.__loader__ = v2_spec.loader - module.__package__ = v2_spec.parent - module.__file__ = v2_spec.origin - if v2_spec.submodule_search_locations is not None: - module.__path__ = v2_spec.submodule_search_locations - - -class _AliasLoader: - """Hand back the already-imported v2 module without executing it again.""" - - def __init__(self, v2_spec): - self._v2_spec = v2_spec - - def create_module(self, spec): - module = sys.modules.get(self._v2_spec.name) - if module is None: - module = importlib.import_module(self._v2_spec.name) - # The import system skips a module already in sys.modules, so bind - # the leaf on the parent for attribute-expression imports. - parent_name, _, leaf = spec.name.rpartition(".") - parent = sys.modules.get(parent_name) - if parent is not None: - setattr(parent, leaf, module) - return module - - def exec_module(self, module): - _restore_identity(module, self._v2_spec) - - def get_code(self, fullname): # pylint: disable=unused-argument - # runpy (python -m) and uvicorn require a loader with a code object. - return self._v2_spec.loader.get_code(self._v2_spec.name) - - -class _Finder: - """Resolve one public package prefix to the v2 modules.""" - - def __init__(self, alias_name): - self.alias_name = alias_name - - def find_spec( # pylint: disable=unused-argument - self, - fullname, - path=None, - target=None, - ): - if not fullname.startswith(self.alias_name + "."): - return None - v2_name = _IMPL + fullname[len(self.alias_name) :] - # find_spec resolves without importing the leaf, preserving the - # optional fastapi and opentelemetry dependencies. - v2_spec = importlib.util.find_spec(v2_name) - if v2_spec is None or v2_spec.loader is None: - return None - return importlib.util.spec_from_file_location( - fullname, - v2_spec.origin, - loader=_AliasLoader(v2_spec), - submodule_search_locations=v2_spec.submodule_search_locations, - ) - - -def _register_alias(alias_name): - # Register ahead of PathFinder so it cannot execute the v2 source again - # under the compatibility name and create duplicate class objects. - if not any( - isinstance(entry, _Finder) and entry.alias_name == alias_name - for entry in sys.meta_path - ): - sys.meta_path.insert(0, _Finder(alias_name)) - - -_register_alias(_PUBLIC) diff --git a/dashscope/rerank/__init__.py b/dashscope/rerank/__init__.py deleted file mode 100644 index dc612489..00000000 --- a/dashscope/rerank/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.rerank``. - -Import from ``dashscope.legacy_1x.rerank`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.rerank") diff --git a/dashscope/v2/resources/__init__.py b/dashscope/resources/__init__.py similarity index 100% rename from dashscope/v2/resources/__init__.py rename to dashscope/resources/__init__.py diff --git a/dashscope/v2/resources/_jobs.py b/dashscope/resources/_jobs.py similarity index 99% rename from dashscope/v2/resources/_jobs.py rename to dashscope/resources/_jobs.py index 61b42547..23d22878 100644 --- a/dashscope/v2/resources/_jobs.py +++ b/dashscope/resources/_jobs.py @@ -17,12 +17,12 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field -from dashscope.v2.errors import ( +from dashscope.errors import ( DashScopeError, check_response, error_from_business_code, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( ResponseBase, Usage, flatten_envelope, diff --git a/dashscope/v2/resources/_oss.py b/dashscope/resources/_oss.py similarity index 98% rename from dashscope/v2/resources/_oss.py rename to dashscope/resources/_oss.py index 42999805..004f5978 100644 --- a/dashscope/v2/resources/_oss.py +++ b/dashscope/resources/_oss.py @@ -22,8 +22,8 @@ from typing import Any, Dict, List, Optional, Tuple -from dashscope.v2.resources._oss_upload import check_and_upload_local -from dashscope.v2.types.messages import ( +from dashscope.resources._oss_upload import check_and_upload_local +from dashscope.types.messages import ( AudioPart, ImagePart, Message, diff --git a/dashscope/v2/resources/_oss_upload.py b/dashscope/resources/_oss_upload.py similarity index 97% rename from dashscope/v2/resources/_oss_upload.py rename to dashscope/resources/_oss_upload.py index fb1eebe2..caa4713c 100644 --- a/dashscope/v2/resources/_oss_upload.py +++ b/dashscope/resources/_oss_upload.py @@ -25,9 +25,9 @@ import httpx -from dashscope.v2._logging import get_logger -from dashscope.v2.errors import check_response, error_from_status -from dashscope.v2.transport.http import default_user_agent +from dashscope._logging import get_logger +from dashscope.errors import check_response, error_from_status +from dashscope.transport.http import default_user_agent __all__ = [ "check_and_upload_local", diff --git a/dashscope/v2/resources/_sse.py b/dashscope/resources/_sse.py similarity index 98% rename from dashscope/v2/resources/_sse.py rename to dashscope/resources/_sse.py index d30104ee..2d4f1de3 100644 --- a/dashscope/v2/resources/_sse.py +++ b/dashscope/resources/_sse.py @@ -13,7 +13,7 @@ import json from typing import Any, AsyncIterator, Iterator, Optional -from dashscope.v2.errors import ( +from dashscope.errors import ( DashScopeError, error_from_business_code, error_from_status, diff --git a/dashscope/v2/resources/apps.py b/dashscope/resources/apps.py similarity index 99% rename from dashscope/v2/resources/apps.py rename to dashscope/resources/apps.py index 58025941..b1eefcad 100644 --- a/dashscope/v2/resources/apps.py +++ b/dashscope/resources/apps.py @@ -23,11 +23,11 @@ from pydantic import BaseModel, ConfigDict -from dashscope.v2.errors import ( +from dashscope.errors import ( check_response, error_from_business_code, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( ResponseBase, Usage, flatten_envelope, diff --git a/dashscope/v2/resources/audio/__init__.py b/dashscope/resources/audio/__init__.py similarity index 86% rename from dashscope/v2/resources/audio/__init__.py rename to dashscope/resources/audio/__init__.py index 300bff25..9b62ef94 100644 --- a/dashscope/v2/resources/audio/__init__.py +++ b/dashscope/resources/audio/__init__.py @@ -10,12 +10,12 @@ ``voices`` / ``voice_design``. """ -from dashscope.v2.resources.audio.asr import ( +from dashscope.resources.audio.asr import ( Asr, AsyncAsr, Recognition, ) -from dashscope.v2.resources.audio.customization import ( +from dashscope.resources.audio.customization import ( DesignedVoice, DesignedVoiceList, Vocabularies, @@ -26,19 +26,19 @@ VoiceList, Voices, ) -from dashscope.v2.resources.audio.http_tts import ( +from dashscope.resources.audio.http_tts import ( HttpSynthesis, HttpSynthesisChunk, HttpSynthesisStream, HttpTts, ) -from dashscope.v2.resources.audio.phrases import ( +from dashscope.resources.audio.phrases import ( Phrase, PhraseList, Phrases, ) -from dashscope.v2.resources.audio.realtime import Realtime, RealtimeSession -from dashscope.v2.resources.audio.tts import SynthesisSession, Tts +from dashscope.resources.audio.realtime import Realtime, RealtimeSession +from dashscope.resources.audio.tts import SynthesisSession, Tts __all__ = [ "Asr", diff --git a/dashscope/v2/resources/audio/_callbacks.py b/dashscope/resources/audio/_callbacks.py similarity index 97% rename from dashscope/v2/resources/audio/_callbacks.py rename to dashscope/resources/audio/_callbacks.py index aea8fc1f..145f76c9 100644 --- a/dashscope/v2/resources/audio/_callbacks.py +++ b/dashscope/resources/audio/_callbacks.py @@ -13,7 +13,7 @@ from typing import Any, Iterable -from dashscope.v2.transport.ws import EVENT_FINISHED +from dashscope.transport.ws import EVENT_FINISHED __all__ = [ "run_with_callback", diff --git a/dashscope/v2/resources/audio/asr.py b/dashscope/resources/audio/asr.py similarity index 98% rename from dashscope/v2/resources/audio/asr.py rename to dashscope/resources/audio/asr.py index 6e49078f..81f00822 100644 --- a/dashscope/v2/resources/audio/asr.py +++ b/dashscope/resources/audio/asr.py @@ -28,8 +28,8 @@ from pydantic import BaseModel, ConfigDict, Field -from dashscope.v2.errors import DashScopeError, error_from_business_code -from dashscope.v2.resources._jobs import ( +from dashscope.errors import DashScopeError, error_from_business_code +from dashscope.resources._jobs import ( CANCELED, DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, @@ -39,8 +39,8 @@ Job, Jobs, ) -from dashscope.v2.transport.ws import Connector, EVENT_GENERATED, WsSession -from dashscope.v2.transport.ws_connection import WsConnection, WsTask +from dashscope.transport.ws import Connector, EVENT_GENERATED, WsSession +from dashscope.transport.ws_connection import WsConnection, WsTask __all__ = [ "Asr", diff --git a/dashscope/v2/resources/audio/customization.py b/dashscope/resources/audio/customization.py similarity index 99% rename from dashscope/v2/resources/audio/customization.py rename to dashscope/resources/audio/customization.py index f96de9f3..8d38aba2 100644 --- a/dashscope/v2/resources/audio/customization.py +++ b/dashscope/resources/audio/customization.py @@ -19,7 +19,7 @@ from pydantic import AliasChoices, Field -from dashscope.v2.types.common import ( +from dashscope.types.common import ( JobStatus, ResponseBase, flatten_envelope, diff --git a/dashscope/v2/resources/audio/http_tts.py b/dashscope/resources/audio/http_tts.py similarity index 98% rename from dashscope/v2/resources/audio/http_tts.py rename to dashscope/resources/audio/http_tts.py index cc0edadc..867c012f 100644 --- a/dashscope/v2/resources/audio/http_tts.py +++ b/dashscope/resources/audio/http_tts.py @@ -29,14 +29,14 @@ Tuple, ) -from dashscope.v2.errors import ( +from dashscope.errors import ( APIStatusError, DashScopeError, error_from_business_code, error_from_status, ) -from dashscope.v2.resources._sse import _try_json -from dashscope.v2.types.common import ResponseBase, response_body, with_headers +from dashscope.resources._sse import _try_json +from dashscope.types.common import ResponseBase, response_body, with_headers __all__ = [ "HttpSynthesis", diff --git a/dashscope/v2/resources/audio/phrases.py b/dashscope/resources/audio/phrases.py similarity index 99% rename from dashscope/v2/resources/audio/phrases.py rename to dashscope/resources/audio/phrases.py index 6af26877..d6b8f74d 100644 --- a/dashscope/v2/resources/audio/phrases.py +++ b/dashscope/resources/audio/phrases.py @@ -17,7 +17,7 @@ from pydantic import AliasChoices, Field -from dashscope.v2.types.common import ( +from dashscope.types.common import ( JobStatus, ResponseBase, flatten_envelope, diff --git a/dashscope/v2/resources/audio/realtime.py b/dashscope/resources/audio/realtime.py similarity index 99% rename from dashscope/v2/resources/audio/realtime.py rename to dashscope/resources/audio/realtime.py index a45fca85..2e34ca36 100644 --- a/dashscope/v2/resources/audio/realtime.py +++ b/dashscope/resources/audio/realtime.py @@ -12,11 +12,11 @@ from typing import Any, Dict, Iterator, List, Mapping, Optional, Union, cast from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit -from dashscope.v2._config import WS_REALTIME_PATH, workspace_ws_url -from dashscope.v2.errors import APIConnectionError, error_from_business_code -from dashscope.v2.transport import default_user_agent -from dashscope.v2.transport._ident import sdk_headers -from dashscope.v2.transport.ws import Connector, _close_websocket +from dashscope._config import WS_REALTIME_PATH, workspace_ws_url +from dashscope.errors import APIConnectionError, error_from_business_code +from dashscope.transport import default_user_agent +from dashscope.transport._ident import sdk_headers +from dashscope.transport.ws import Connector, _close_websocket __all__ = [ "Realtime", diff --git a/dashscope/v2/resources/audio/tts.py b/dashscope/resources/audio/tts.py similarity index 98% rename from dashscope/v2/resources/audio/tts.py rename to dashscope/resources/audio/tts.py index cf8c5321..383585f0 100644 --- a/dashscope/v2/resources/audio/tts.py +++ b/dashscope/resources/audio/tts.py @@ -28,9 +28,9 @@ Union, ) -from dashscope.v2.resources.audio.http_tts import HttpTts -from dashscope.v2.transport.ws import Connector, WsSession -from dashscope.v2.transport.ws_connection import WsConnection, WsTask +from dashscope.resources.audio.http_tts import HttpTts +from dashscope.transport.ws import Connector, WsSession +from dashscope.transport.ws_connection import WsConnection, WsTask __all__ = [ "SynthesisSession", diff --git a/dashscope/v2/resources/chat/__init__.py b/dashscope/resources/chat/__init__.py similarity index 82% rename from dashscope/v2/resources/chat/__init__.py rename to dashscope/resources/chat/__init__.py index 60af2252..539f3ad3 100644 --- a/dashscope/v2/resources/chat/__init__.py +++ b/dashscope/resources/chat/__init__.py @@ -2,12 +2,12 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """client.chat resource.""" -from dashscope.v2.resources.chat.completions import ( +from dashscope.resources.chat.completions import ( AsyncCompletions, Completions, ) -from dashscope.v2.types.common import Usage -from dashscope.v2.types.completions import ( +from dashscope.types.common import Usage +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ChatCompletionMessage, diff --git a/dashscope/v2/resources/chat/_params.py b/dashscope/resources/chat/_params.py similarity index 100% rename from dashscope/v2/resources/chat/_params.py rename to dashscope/resources/chat/_params.py diff --git a/dashscope/v2/resources/chat/completions.py b/dashscope/resources/chat/completions.py similarity index 98% rename from dashscope/v2/resources/chat/completions.py rename to dashscope/resources/chat/completions.py index 258f9024..69966704 100644 --- a/dashscope/v2/resources/chat/completions.py +++ b/dashscope/resources/chat/completions.py @@ -14,17 +14,17 @@ from typing_extensions import Literal -from dashscope.v2.errors import response_error -from dashscope.v2.protocols.base import ChatRequest -from dashscope.v2.resources._oss import ( +from dashscope.errors import response_error +from dashscope.protocols.base import ChatRequest +from dashscope.resources._oss import ( OSS_RESOLVE_HEADERS, upload_message_media, ) -from dashscope.v2.resources.chat._params import ( +from dashscope.resources.chat._params import ( should_modify_incremental_output, ) -from dashscope.v2.types.common import Usage, with_headers -from dashscope.v2.types.completions import ( +from dashscope.types.common import Usage, with_headers +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ChatCompletionMessage, @@ -32,8 +32,8 @@ SearchInfo, StreamChoice, ) -from dashscope.v2.transport.sse import AsyncStream, Stream -from dashscope.v2.types.messages import ( +from dashscope.transport.sse import AsyncStream, Stream +from dashscope.types.messages import ( Message, MessageInput, ToolCall, diff --git a/dashscope/v2/resources/decision.py b/dashscope/resources/decision.py similarity index 98% rename from dashscope/v2/resources/decision.py rename to dashscope/resources/decision.py index a89f9519..c81fc60a 100644 --- a/dashscope/v2/resources/decision.py +++ b/dashscope/resources/decision.py @@ -24,9 +24,9 @@ from pydantic import BaseModel, ConfigDict, Field -from dashscope.v2._config import Config, compatible_mode_base_url -from dashscope.v2.errors import response_error -from dashscope.v2.types.common import ResponseBase, Usage, with_headers +from dashscope._config import Config, compatible_mode_base_url +from dashscope.errors import response_error +from dashscope.types.common import ResponseBase, Usage, with_headers _PATH = "/systemone" # Functional-area tag for x-dashscope-sdk-client. The decision model is diff --git a/dashscope/v2/resources/deployments.py b/dashscope/resources/deployments.py similarity index 99% rename from dashscope/v2/resources/deployments.py rename to dashscope/resources/deployments.py index ba68dfa3..0841c312 100644 --- a/dashscope/v2/resources/deployments.py +++ b/dashscope/resources/deployments.py @@ -22,14 +22,14 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field -from dashscope.v2.errors import check_response -from dashscope.v2.resources._sse import ( +from dashscope.errors import check_response +from dashscope.resources._sse import ( _aiter_sse, _iter_sse, _stream_error, _try_json, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( JobStatus, ResponseBase, flatten_envelope, diff --git a/dashscope/v2/resources/embeddings.py b/dashscope/resources/embeddings.py similarity index 98% rename from dashscope/v2/resources/embeddings.py rename to dashscope/resources/embeddings.py index f5614de5..0c917a46 100644 --- a/dashscope/v2/resources/embeddings.py +++ b/dashscope/resources/embeddings.py @@ -14,8 +14,8 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field -from dashscope.v2.errors import check_response -from dashscope.v2.resources._jobs import ( +from dashscope.errors import check_response +from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_WAIT_TIMEOUT, @@ -24,7 +24,7 @@ JobList, Jobs, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( ResponseBase, Usage, response_envelope, diff --git a/dashscope/v2/resources/files.py b/dashscope/resources/files.py similarity index 99% rename from dashscope/v2/resources/files.py rename to dashscope/resources/files.py index 23654c6e..46b16d57 100644 --- a/dashscope/v2/resources/files.py +++ b/dashscope/resources/files.py @@ -11,14 +11,14 @@ from pydantic import AliasChoices, Field -from dashscope.v2.errors import ( +from dashscope.errors import ( BadRequestError, NotFoundError, check_response, response_error, sdk_error, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( ResponseBase, response_envelope, with_headers, diff --git a/dashscope/v2/resources/fine_tunes.py b/dashscope/resources/fine_tunes.py similarity index 99% rename from dashscope/v2/resources/fine_tunes.py rename to dashscope/resources/fine_tunes.py index e9551869..bd03d773 100644 --- a/dashscope/v2/resources/fine_tunes.py +++ b/dashscope/resources/fine_tunes.py @@ -35,12 +35,12 @@ field_validator, ) -from dashscope.v2.errors import ( +from dashscope.errors import ( DashScopeError, check_response, error_from_business_code, ) -from dashscope.v2.resources._jobs import ( +from dashscope.resources._jobs import ( CANCELED, DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, @@ -48,13 +48,13 @@ FAILED, SUCCEEDED, ) -from dashscope.v2.resources._sse import ( +from dashscope.resources._sse import ( _aiter_sse, _iter_sse, _stream_error, _try_json, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( JobStatus, ResponseBase, Usage, diff --git a/dashscope/v2/resources/images.py b/dashscope/resources/images.py similarity index 99% rename from dashscope/v2/resources/images.py rename to dashscope/resources/images.py index 7821121b..b9caa6bf 100644 --- a/dashscope/v2/resources/images.py +++ b/dashscope/resources/images.py @@ -25,8 +25,8 @@ import asyncio from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union -from dashscope.v2.errors import check_response -from dashscope.v2.resources._jobs import ( +from dashscope.errors import check_response +from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_WAIT_TIMEOUT, @@ -36,11 +36,11 @@ JobList, Jobs, ) -from dashscope.v2.resources._oss import ( +from dashscope.resources._oss import ( OSS_RESOLVE_HEADERS, upload_media_value, ) -from dashscope.v2.types.common import with_headers +from dashscope.types.common import with_headers __all__ = [ "AsyncImages", diff --git a/dashscope/v2/resources/models.py b/dashscope/resources/models.py similarity index 98% rename from dashscope/v2/resources/models.py rename to dashscope/resources/models.py index 066e10e6..334ebbd6 100644 --- a/dashscope/v2/resources/models.py +++ b/dashscope/resources/models.py @@ -8,12 +8,12 @@ from pydantic import AliasChoices, Field -from dashscope.v2.errors import ( +from dashscope.errors import ( ErrorCode, check_response, error_from_business_code, ) -from dashscope.v2.types.common import ( +from dashscope.types.common import ( ResponseBase, response_envelope, with_headers, diff --git a/dashscope/v2/resources/multimodal/__init__.py b/dashscope/resources/multimodal/__init__.py similarity index 85% rename from dashscope/v2/resources/multimodal/__init__.py rename to dashscope/resources/multimodal/__init__.py index 4569217d..07f7c3b3 100644 --- a/dashscope/v2/resources/multimodal/__init__.py +++ b/dashscope/resources/multimodal/__init__.py @@ -2,11 +2,11 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """client.multimodal resource.""" -from dashscope.v2.resources.multimodal.chat import ( +from dashscope.resources.multimodal.chat import ( AsyncMultimodalChat, MultimodalChat, ) -from dashscope.v2.resources.multimodal.embeddings import ( +from dashscope.resources.multimodal.embeddings import ( AsyncMultimodalEmbeddings, MultimodalEmbeddings, ) diff --git a/dashscope/v2/resources/multimodal/chat.py b/dashscope/resources/multimodal/chat.py similarity index 98% rename from dashscope/v2/resources/multimodal/chat.py rename to dashscope/resources/multimodal/chat.py index 2490f086..d086434d 100644 --- a/dashscope/v2/resources/multimodal/chat.py +++ b/dashscope/resources/multimodal/chat.py @@ -20,15 +20,15 @@ from typing_extensions import Literal -from dashscope.v2.resources.chat.completions import ( +from dashscope.resources.chat.completions import ( AsyncChatStream, ChatStream, _asend, _build_request, _send, ) -from dashscope.v2.types.completions import ChatCompletion -from dashscope.v2.types.messages import MessageInput +from dashscope.types.completions import ChatCompletion +from dashscope.types.messages import MessageInput __all__ = [ "AsyncMultimodalChat", diff --git a/dashscope/v2/resources/multimodal/embeddings.py b/dashscope/resources/multimodal/embeddings.py similarity index 97% rename from dashscope/v2/resources/multimodal/embeddings.py rename to dashscope/resources/multimodal/embeddings.py index f214c01d..fdbfdb00 100644 --- a/dashscope/v2/resources/multimodal/embeddings.py +++ b/dashscope/resources/multimodal/embeddings.py @@ -12,13 +12,13 @@ from pydantic import BaseModel, ConfigDict -from dashscope.v2.errors import check_response -from dashscope.v2.resources._oss import upload_embedding_media -from dashscope.v2.resources.embeddings import ( +from dashscope.errors import check_response +from dashscope.resources._oss import upload_embedding_media +from dashscope.resources.embeddings import ( EmbeddingResponse, parse_embedding_response, ) -from dashscope.v2.types.common import with_headers +from dashscope.types.common import with_headers __all__ = [ "AsyncMultimodalEmbeddings", diff --git a/dashscope/v2/resources/reinforcement_learning.py b/dashscope/resources/reinforcement_learning.py similarity index 98% rename from dashscope/v2/resources/reinforcement_learning.py rename to dashscope/resources/reinforcement_learning.py index 462809bc..fc31fea8 100644 --- a/dashscope/v2/resources/reinforcement_learning.py +++ b/dashscope/resources/reinforcement_learning.py @@ -3,8 +3,8 @@ """client.rl thin mount layer (design doc M3 3.7). The agentic-RL workflow lives in the 2.0 package -(``dashscope.v2.finetune``); this layer only mounts it on the 2.0 Client. -Job management and workflow steps both raise ``dashscope.v2.errors`` +(``dashscope.finetune``); this layer only mounts it on the 2.0 Client. +Job management and workflow steps both raise ``dashscope.errors`` natively, so one ``except DashScopeError`` covers both. Two halves, matching the rest of the SDK: ``rl.jobs`` is the training @@ -37,19 +37,19 @@ from pydantic import BaseModel, ConfigDict -from dashscope.v2.errors import ( +from dashscope.errors import ( DashScopeError, check_response, error_from_business_code, ) -from dashscope.v2.resources._jobs import ( +from dashscope.resources._jobs import ( CANCELED, DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_WAIT_TIMEOUT, FAILED, ) -from dashscope.v2.resources.fine_tunes import ( +from dashscope.resources.fine_tunes import ( MODULE, FineTune, FineTuneList, @@ -61,7 +61,7 @@ _parse_logs, _parse_status, ) -from dashscope.v2.types.common import JobStatus, with_headers +from dashscope.types.common import JobStatus, with_headers __all__ = [ "AsyncRl", @@ -73,10 +73,10 @@ ] # Imported below via the module object so tests can monkeypatch -# ``dashscope.v2.finetune.agentic_rl.AgenticRL`` without touching the +# ``dashscope.finetune.agentic_rl.AgenticRL`` without touching the # workflow implementation. -from dashscope.v2.finetune import agentic_rl as _agentic_rl # noqa: E402 -from dashscope.v2.finetune import reinforcement as _reinforcement # noqa: E402 +from dashscope.finetune import agentic_rl as _agentic_rl # noqa: E402 +from dashscope.finetune import reinforcement as _reinforcement # noqa: E402 class RegisteredFunctions(BaseModel): diff --git a/dashscope/v2/resources/rerank.py b/dashscope/resources/rerank.py similarity index 98% rename from dashscope/v2/resources/rerank.py rename to dashscope/resources/rerank.py index cb862984..5edae131 100644 --- a/dashscope/v2/resources/rerank.py +++ b/dashscope/resources/rerank.py @@ -8,8 +8,8 @@ from pydantic import BaseModel, ConfigDict -from dashscope.v2.errors import check_response -from dashscope.v2.types.common import ( +from dashscope.errors import check_response +from dashscope.types.common import ( ResponseBase, Usage, response_envelope, diff --git a/dashscope/v2/resources/tingwu.py b/dashscope/resources/tingwu.py similarity index 97% rename from dashscope/v2/resources/tingwu.py rename to dashscope/resources/tingwu.py index 198f0412..0f8fe615 100644 --- a/dashscope/v2/resources/tingwu.py +++ b/dashscope/resources/tingwu.py @@ -17,10 +17,10 @@ from pydantic import ConfigDict, Field -from dashscope.v2.errors import check_response, error_from_business_code -from dashscope.v2.transport.ws import Connector, WsSession -from dashscope.v2.transport.ws_connection import WsConnection, WsTask -from dashscope.v2.types.common import ( +from dashscope.errors import check_response, error_from_business_code +from dashscope.transport.ws import Connector, WsSession +from dashscope.transport.ws_connection import WsConnection, WsTask +from dashscope.types.common import ( ResponseBase, Usage, response_envelope, diff --git a/dashscope/v2/resources/tokenization.py b/dashscope/resources/tokenization.py similarity index 95% rename from dashscope/v2/resources/tokenization.py rename to dashscope/resources/tokenization.py index d71c47a5..c0fcd592 100644 --- a/dashscope/v2/resources/tokenization.py +++ b/dashscope/resources/tokenization.py @@ -6,14 +6,14 @@ from typing import Any, Dict, List, Optional, Union -from dashscope.v2.errors import check_response -from dashscope.v2.types.common import ( +from dashscope.errors import check_response +from dashscope.types.common import ( ResponseBase, Usage, response_envelope, with_headers, ) -from dashscope.v2.types.messages import MessageInput, normalize_messages +from dashscope.types.messages import MessageInput, normalize_messages _PATH = "/tokenizer" # Functional-area tag for x-dashscope-sdk-client (matches 1.x ``tokenizers``). diff --git a/dashscope/v2/resources/videos.py b/dashscope/resources/videos.py similarity index 99% rename from dashscope/v2/resources/videos.py rename to dashscope/resources/videos.py index b88c5db1..466efe1a 100644 --- a/dashscope/v2/resources/videos.py +++ b/dashscope/resources/videos.py @@ -22,7 +22,7 @@ import asyncio from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union -from dashscope.v2.resources._jobs import ( +from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_WAIT_TIMEOUT, @@ -31,7 +31,7 @@ JobList, Jobs, ) -from dashscope.v2.resources._oss import ( +from dashscope.resources._oss import ( OSS_RESOLVE_HEADERS, upload_media_value, ) diff --git a/dashscope/threads/__init__.py b/dashscope/threads/__init__.py deleted file mode 100644 index f0f9f935..00000000 --- a/dashscope/threads/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.threads``. - -Import from ``dashscope.legacy_1x.threads`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.threads") diff --git a/dashscope/tokenizers/__init__.py b/dashscope/tokenizers/__init__.py index bf30f999..e43a8af3 100644 --- a/dashscope/tokenizers/__init__.py +++ b/dashscope/tokenizers/__init__.py @@ -2,16 +2,8 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from .tokenization import Tokenization - -# The offline tokenizer implementation lives in the 2.0 package; both stacks -# share it so there is one copy of the code and of the bundled vocab. It is -# imported second on purpose: the 1.x chain above must finish loading before -# the v2 chain starts pulling modules in. -from dashscope.v2.tokenizers import ( # pylint: disable=wrong-import-order - Tokenizer, - get_tokenizer, - list_tokenizers, -) +from .tokenizer import get_tokenizer, list_tokenizers +from .tokenizer_base import Tokenizer __all__ = [ "Tokenization", diff --git a/dashscope/v2/tokenizers/qwen.tiktoken b/dashscope/tokenizers/qwen.tiktoken similarity index 100% rename from dashscope/v2/tokenizers/qwen.tiktoken rename to dashscope/tokenizers/qwen.tiktoken diff --git a/dashscope/tokenizers/qwen_tokenizer.py b/dashscope/tokenizers/qwen_tokenizer.py index 2b2baa57..9a538f95 100644 --- a/dashscope/tokenizers/qwen_tokenizer.py +++ b/dashscope/tokenizers/qwen_tokenizer.py @@ -1,39 +1,167 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility re-export; the implementation moved to the 2.0 package. - -The Qwen tiktoken tokenizer and its special-token tables live only in -``dashscope.v2.tokenizers.qwen_tokenizer``, next to the bundled vocab, so -both stacks share one copy. Import from there in new code. - -``Tokenizer`` is re-exported because the old module imported it into its -own namespace, making ``from dashscope.tokenizers.qwen_tokenizer import -Tokenizer`` a working (if incidental) path. -""" - -# pylint: disable-next=unused-import -from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 - ENDOFTEXT, - EXTRAS, - IMEND, - IMSTART, - PAT_STR, - SPECIAL_START_ID, - SPECIAL_TOKENS, - SPECIAL_TOKENS_SET, - QwenTokenizer, - Tokenizer, +# mypy: disable-error-code="annotation-unchecked" + +import base64 +import unicodedata +from typing import Collection, Dict, List, Set, Union + +from .tokenizer_base import Tokenizer + +PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long +ENDOFTEXT = "<|endoftext|>" +IMSTART = "<|im_start|>" +IMEND = "<|im_end|>" +# as the default behavior is changed to allow special tokens in +# regular texts, the surface forms of special tokens need to be +# as different as possible to minimize the impact +EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) +# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 +SPECIAL_START_ID = 151643 +SPECIAL_TOKENS = tuple( + enumerate( + ( + ( + ENDOFTEXT, + IMSTART, + IMEND, + ) + + EXTRAS + ), + start=SPECIAL_START_ID, + ), ) +SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) + +# tiktoken's BPE merges tokens recursively in Rust, which can overflow the +# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). +# Split text into chunks below this threshold before encoding. +_CHUNK_SIZE = 100_000 + + +class QwenTokenizer(Tokenizer): + @staticmethod + def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: + with open(tiktoken_bpe_file, "rb") as f: + contents = f.read() + return { + base64.b64decode(token): int(rank) + for token, rank in ( + line.split() for line in contents.splitlines() if line + ) + } + + def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): + self._errors = errors + self._vocab_file = vocab_file + self._extra_vocab_file = extra_vocab_file + + self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( + vocab_file, + ) # type: Dict[bytes, int] + self._special_tokens = { + token: index for index, token in SPECIAL_TOKENS + } + + # try load extra vocab from file + if extra_vocab_file is not None: + used_ids = set(self._mergeable_ranks.values()) | set( + self._special_tokens.values(), + ) + extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) + for token, index in extra_mergeable_ranks.items(): + if token in self._mergeable_ranks: + continue + if index in used_ids: + continue + self._mergeable_ranks[token] = index + # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long + import tiktoken + + enc = tiktoken.Encoding( + "Qwen", + pat_str=PAT_STR, + mergeable_ranks=self._mergeable_ranks, + special_tokens=self._special_tokens, + ) + assert ( + len(self._mergeable_ranks) + len(self._special_tokens) + == enc.n_vocab + ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long + + self.decoder = { + v: k for k, v in self._mergeable_ranks.items() + } # type: dict[int, bytes|str] + self.decoder.update({v: k for k, v in self._special_tokens.items()}) + + self._tokenizer = enc # type: tiktoken.Encoding + + self.eod_id = self._tokenizer.eot_token + self.im_start_id = self._special_tokens[IMSTART] + self.im_end_id = self._special_tokens[IMEND] + + def encode( # type: ignore[override] + self, + text: str, + allowed_special: Union[Set, str] = "all", + disallowed_special: Union[Collection, str] = (), + ) -> Union[List[List], List]: + text = unicodedata.normalize("NFC", text) + if len(text) <= _CHUNK_SIZE: + return self._tokenizer.encode( + text, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ) + + result = [] + for chunk in self._split_text(text): + result.extend( + self._tokenizer.encode( + chunk, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ), + ) + return result + + @staticmethod + def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: + """Split text into chunks at safe tokenization boundaries.""" + parts: List[str] = [] + for i, line in enumerate(text.split("\n")): + piece = line if i == 0 else "\n" + line + if len(piece) <= chunk_size: + parts.append(piece) + else: + for j in range(0, len(piece), chunk_size): + parts.append(piece[j : j + chunk_size]) + + chunks: List[str] = [] + current_chunk: List[str] = [] + current_len = 0 + for part in parts: + if current_len + len(part) <= chunk_size: + current_chunk.append(part) + current_len += len(part) + else: + if current_chunk: + chunks.append("".join(current_chunk)) + current_chunk = [part] + current_len = len(part) + if current_chunk: + chunks.append("".join(current_chunk)) + return chunks -__all__ = [ - "ENDOFTEXT", - "EXTRAS", - "IMEND", - "IMSTART", - "PAT_STR", - "SPECIAL_START_ID", - "SPECIAL_TOKENS", - "SPECIAL_TOKENS_SET", - "QwenTokenizer", - "Tokenizer", -] + def decode( + self, + token_ids: Union[int, List[int]], + skip_special_tokens: bool = False, + errors: str = None, + **kwargs, + ) -> str: + if isinstance(token_ids, int): + token_ids = [token_ids] + if skip_special_tokens: + token_ids = [i for i in token_ids if i < self.eod_id] + return self._tokenizer.decode(token_ids, errors=errors or self._errors) diff --git a/dashscope/tokenizers/tokenizer.py b/dashscope/tokenizers/tokenizer.py index b3218769..e8625bd7 100644 --- a/dashscope/tokenizers/tokenizer.py +++ b/dashscope/tokenizers/tokenizer.py @@ -1,41 +1,43 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility re-export; the implementation moved to the 2.0 package. - -The offline tokenizer factory lives only in -``dashscope.v2.tokenizers.tokenizer``, next to the bundled vocab, so both -stacks share one copy of the code and of the vocab. Import from there in -new code. - -Note that :func:`get_tokenizer` raises ``ValueError`` for an unknown model; -it used to raise ``dashscope.legacy_1x.common.error.UnsupportedModel``, which the 2.0 -leaf must not depend on. - -``Tokenizer`` and ``QwenTokenizer`` are re-exported because the old module -imported them into its own namespace. The old module-level ``current_path`` -and ``root_path`` are deliberately not restored: they only located the -vocab, which no longer ships next to this file. -""" - -# pylint: disable-next=unused-import -from dashscope.v2.tokenizers.tokenizer import ( # noqa: F401 - QWEN_SERIALS, - get_tokenizer, - list_tokenizers, -) -# pylint: disable-next=unused-import -from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 - QwenTokenizer, +import os +from typing import List + +from .qwen_tokenizer import QwenTokenizer +from .tokenizer_base import Tokenizer + +QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] +# The vocab ships next to this module, so the leaf stays relocatable. +_VOCAB_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "qwen.tiktoken", ) -# pylint: disable-next=unused-import -from dashscope.v2.tokenizers.tokenizer_base import Tokenizer # noqa: F401 -__all__ = [ - "QWEN_SERIALS", - "QwenTokenizer", - "Tokenizer", - "get_tokenizer", - "list_tokenizers", -] +def get_tokenizer(model: str) -> Tokenizer: + """Get a tokenizer based on model name. + + Args: + model (str): The model name. + + Raises: + ValueError: Not support model + + Returns: + Tokenizer: The `Tokenizer` of the model. + """ + if model.startswith("qwen"): + return QwenTokenizer(_VOCAB_PATH) + raise ValueError( + f"Not support model: {model}, currently only support qwen models.", + ) + + +def list_tokenizers() -> List[str]: + """List support models + + Returns: + List[str]: The model list. + """ + return QWEN_SERIALS diff --git a/dashscope/tokenizers/tokenizer_base.py b/dashscope/tokenizers/tokenizer_base.py index 89520f92..2e385bf8 100644 --- a/dashscope/tokenizers/tokenizer_base.py +++ b/dashscope/tokenizers/tokenizer_base.py @@ -1,12 +1,31 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Compatibility re-export; the implementation moved to the 2.0 package. -The offline tokenizer base class lives only in -``dashscope.v2.tokenizers.tokenizer_base`` so both stacks share one copy. -Import from there in new code. -""" +from typing import List -from dashscope.v2.tokenizers.tokenizer_base import Tokenizer -__all__ = ["Tokenizer"] +class Tokenizer: + """Base tokenizer interface for local tokenizers.""" + + def __init__(self): + pass + + def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 + """Encode input text string to token ids. + + Args: + text (str): The string to be encoded. + + Returns: + List[int]: The token ids. + """ + + def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 + """Decode token ids to string. + + Args: + token_ids (List[int]): The input token ids. + + Returns: + str: The string of the token ids. + """ diff --git a/dashscope/v2/transport/__init__.py b/dashscope/transport/__init__.py similarity index 86% rename from dashscope/v2/transport/__init__.py rename to dashscope/transport/__init__.py index ecc95dac..d552f13f 100644 --- a/dashscope/v2/transport/__init__.py +++ b/dashscope/transport/__init__.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Transport layer for the dashscope 2.0 SDK.""" -from dashscope.v2.transport.http import ( +from dashscope.transport.http import ( AsyncTransport, SyncTransport, default_user_agent, diff --git a/dashscope/v2/transport/_ident.py b/dashscope/transport/_ident.py similarity index 100% rename from dashscope/v2/transport/_ident.py rename to dashscope/transport/_ident.py diff --git a/dashscope/v2/transport/_retry.py b/dashscope/transport/_retry.py similarity index 100% rename from dashscope/v2/transport/_retry.py rename to dashscope/transport/_retry.py diff --git a/dashscope/v2/transport/http.py b/dashscope/transport/http.py similarity index 97% rename from dashscope/v2/transport/http.py rename to dashscope/transport/http.py index 7bb805e1..2b1cd269 100644 --- a/dashscope/v2/transport/http.py +++ b/dashscope/transport/http.py @@ -10,16 +10,16 @@ import httpx -from dashscope.v2._config import DEFAULT_TIMEOUT -from dashscope.v2._logging import get_logger -from dashscope.v2.errors import ( +from dashscope._config import DEFAULT_TIMEOUT +from dashscope._logging import get_logger +from dashscope.errors import ( APIConnectionError, APITimeoutError, DashScopeError, check_response, ) -from dashscope.v2.transport._ident import sdk_headers -from dashscope.v2.transport._retry import backoff_delay, should_retry +from dashscope.transport._ident import sdk_headers +from dashscope.transport._retry import backoff_delay, should_retry logger = get_logger("transport") @@ -259,7 +259,7 @@ def open_stream( ) -> httpx.Response: # Streaming responses surface errors inside the event stream; # only the initial status is checked here, no retries. This - # no-retry contract is pinned by tests/v2/test_transport.py and + # no-retry contract is pinned by tests/test_transport.py and # relied on by examples/04_real_world_scenarios/ # production_ready_rag_harness.py, which layers its own stream # backoff on top -- update that example if retries are added. diff --git a/dashscope/v2/transport/sse.py b/dashscope/transport/sse.py similarity index 98% rename from dashscope/v2/transport/sse.py rename to dashscope/transport/sse.py index 7efccf7a..1dfb6245 100644 --- a/dashscope/v2/transport/sse.py +++ b/dashscope/transport/sse.py @@ -32,7 +32,7 @@ import httpx from httpx_sse import EventSource -from dashscope.v2.errors import DashScopeError, error_from_business_code +from dashscope.errors import DashScopeError, error_from_business_code T = TypeVar("T") diff --git a/dashscope/v2/transport/ws.py b/dashscope/transport/ws.py similarity index 98% rename from dashscope/v2/transport/ws.py rename to dashscope/transport/ws.py index e5f073f9..ed497477 100644 --- a/dashscope/v2/transport/ws.py +++ b/dashscope/transport/ws.py @@ -11,19 +11,19 @@ import uuid from typing import Any, Callable, Dict, List, Mapping, Optional, Union -from dashscope.v2._config import ( +from dashscope._config import ( WS_INFERENCE_PATH, settings, workspace_ws_url, ) -from dashscope.v2.errors import ( +from dashscope.errors import ( APIConnectionError, WsEventError, WsStateError, error_from_business_code, ) -from dashscope.v2.transport import default_user_agent -from dashscope.v2.transport._ident import sdk_headers +from dashscope.transport import default_user_agent +from dashscope.transport._ident import sdk_headers __all__ = [ "Connector", diff --git a/dashscope/v2/transport/ws_connection.py b/dashscope/transport/ws_connection.py similarity index 99% rename from dashscope/v2/transport/ws_connection.py rename to dashscope/transport/ws_connection.py index 27165983..41628c89 100644 --- a/dashscope/v2/transport/ws_connection.py +++ b/dashscope/transport/ws_connection.py @@ -35,17 +35,17 @@ import uuid from typing import Any, Dict, List, Mapping, Optional, Tuple, Union -from dashscope.v2._config import ( +from dashscope._config import ( WS_INFERENCE_PATH, settings, workspace_ws_url, ) -from dashscope.v2.errors import ( +from dashscope.errors import ( APIConnectionError, WsBinaryOwnerConflictError, WsStateError, ) -from dashscope.v2.transport.ws import ( +from dashscope.transport.ws import ( EVENT_FAILED, EVENT_FINISHED, EVENT_STARTED, diff --git a/dashscope/v2/types/__init__.py b/dashscope/types/__init__.py similarity index 84% rename from dashscope/v2/types/__init__.py rename to dashscope/types/__init__.py index b9f5a59a..e6b5a0d7 100644 --- a/dashscope/v2/types/__init__.py +++ b/dashscope/types/__init__.py @@ -2,8 +2,8 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Shared typed models for the dashscope 2.0 SDK.""" -from dashscope.v2.types.common import JobStatus, ResponseBase, Usage -from dashscope.v2.types.completions import ( +from dashscope.types.common import JobStatus, ResponseBase, Usage +from dashscope.types.completions import ( ChatCompletion, ChatCompletionChunk, ChatCompletionMessage, @@ -13,7 +13,7 @@ parse_chunk, parse_completion, ) -from dashscope.v2.types.messages import ( +from dashscope.types.messages import ( AudioPart, ImagePart, Message, diff --git a/dashscope/v2/types/common.py b/dashscope/types/common.py similarity index 98% rename from dashscope/v2/types/common.py rename to dashscope/types/common.py index 5f159042..ff5595d1 100644 --- a/dashscope/v2/types/common.py +++ b/dashscope/types/common.py @@ -9,7 +9,7 @@ import httpx from pydantic import BaseModel, ConfigDict, Field, model_validator -from dashscope.v2.errors import error_from_business_code, error_from_status +from dashscope.errors import error_from_business_code, error_from_status __all__ = [ "JobStatus", diff --git a/dashscope/v2/types/completions.py b/dashscope/types/completions.py similarity index 97% rename from dashscope/v2/types/completions.py rename to dashscope/types/completions.py index 2d5dc086..9e579472 100644 --- a/dashscope/v2/types/completions.py +++ b/dashscope/types/completions.py @@ -8,8 +8,8 @@ from pydantic import BaseModel, ConfigDict -from dashscope.v2.types.common import ResponseBase, Usage -from dashscope.v2.types.messages import ToolCall +from dashscope.types.common import ResponseBase, Usage +from dashscope.types.messages import ToolCall __all__ = [ "ChatCompletion", diff --git a/dashscope/v2/types/messages.py b/dashscope/types/messages.py similarity index 100% rename from dashscope/v2/types/messages.py rename to dashscope/types/messages.py diff --git a/dashscope/utils/__init__.py b/dashscope/utils/__init__.py deleted file mode 100644 index 5f7f9be0..00000000 --- a/dashscope/utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.utils``. - -Import from ``dashscope.legacy_1x.utils`` (or the public ``dashscope`` -root re-exports) in new code. -""" - -from dashscope.legacy_1x._shim import install as _install - -_install(__name__, "dashscope.legacy_1x.utils") diff --git a/dashscope/v2/__init__.py b/dashscope/v2/__init__.py deleted file mode 100644 index d6a03811..00000000 --- a/dashscope/v2/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""dashscope 2.0 SDK implementation package (internal). - -Public API is re-exported at the package root (dashscope.Client etc.); -``dashscope.v2`` is only the internal implementation location. -""" - -from dashscope.v2 import errors -from dashscope.v2._config import Config, Settings, save_api_key, settings -from dashscope.v2._quickstart import chat -from dashscope.v2.client import AsyncClient, Client -from dashscope.v2.errors import DashScopeError, ErrorCode -from dashscope.v2.tokenizers import get_tokenizer, list_tokenizers -from dashscope.v2.transport.ws_connection import WsConnection, WsTask - -__all__ = [ - "AsyncClient", - "Client", - "Config", - "DashScopeError", - "ErrorCode", - "Settings", - "WsConnection", - "WsTask", - "chat", - "errors", - "get_tokenizer", - "list_tokenizers", - "save_api_key", - "settings", -] diff --git a/dashscope/v2/agentstudio/__init__.py b/dashscope/v2/agentstudio/__init__.py deleted file mode 100644 index edd58186..00000000 --- a/dashscope/v2/agentstudio/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""AgentStudio sub-SDK for the dashscope Python client. - -The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP -API. All resource classes are accessed through a client instance:: - - from dashscope.agentstudio import Client - from dashscope.agentstudio.types import ( - user_message, user_tool_result, - ) - -Example -------- ->>> from dashscope.agentstudio import Client ->>> from dashscope.agentstudio.types import user_message ->>> client = Client(api_key="sk-xxx") ->>> agent = client.agents.create(name="demo", model="qwen-plus") ->>> session = client.sessions.create(agent=agent.id) ->>> client.sessions.events.send(session.id, [user_message("Hello!")]) ->>> with client.sessions.events.stream(session.id) as stream: -... for event in stream: -... print(event.type, event.to_dict()) -... if event.type == "session_status": -... break -""" - -# yapf: disable - -from .client import Client, AsyncClient -from .transport import APIResponse, RequestOptions -from . import types -from .exceptions import ( - AgentStudioError, - APIConnectionError, - APIStatusError, - APITimeoutError, - AuthenticationError, - ConflictError, - InternalServerError, - InvalidRequestError, - NotFoundError, - OverloadedError, - PermissionDeniedError, - RateLimitError, - StreamClosedError, - StreamError, -) -from .pagination import ( - AsyncCursorPage, - CursorPage, -) -from .types import ( - Memory, - MemoryStore, - MemoryVersion, - Message, - PermissionPolicy, - SecurityAlert, - SecurityAlertList, - SecurityOverview, - ServerEvent, - SessionResource, - StopReason, - user_define_outcome, - user_interrupt, - user_message, - user_tool_approval_response, - user_custom_tool_result, - user_tool_result, -) -from .types.params import ( - AgentCreateParams, - AgentUpdateParams, - SessionCreateParams, - SessionUpdateParams, - EnvironmentCreateParams, - EnvironmentUpdateParams, - DeploymentCreateParams, - DeploymentUpdateParams, - SessionEventSendParams, - WebhookEndpointCreateParams, - WebhookEndpointUpdateParams, - WebhookEventListParams, -) -from .constants import ( - PermissionPolicyType, - SSEEventType, - SessionStatus, - StopReasonType, - WebhookDeliveryStatus, - WebhookDisabledReason, - WebhookEventType, - WebhookStatus, -) - -__all__ = [ - # client - "Client", - "AsyncClient", - # base types - "RequestOptions", - "APIResponse", - # exceptions - "AgentStudioError", - "APIConnectionError", - "APIStatusError", - "APITimeoutError", - "AuthenticationError", - "ConflictError", - "InternalServerError", - "InvalidRequestError", - "NotFoundError", - "OverloadedError", - "PermissionDeniedError", - "RateLimitError", - "StreamError", - "StreamClosedError", - # pagination - "CursorPage", - "AsyncCursorPage", - # unified message type - "Memory", - "MemoryStore", - "MemoryVersion", - "Message", - "PermissionPolicy", - "SecurityAlert", - "SecurityAlertList", - "SecurityOverview", - "ServerEvent", - "SessionResource", - "StopReason", - # client event helpers (re-exported for convenience) - "user_message", - "user_interrupt", - "user_tool_approval_response", - "user_custom_tool_result", - "user_tool_result", - "user_define_outcome", - # typing module - "types", - # params (commonly used request-body types) - "AgentCreateParams", - "AgentUpdateParams", - "SessionCreateParams", - "SessionUpdateParams", - "EnvironmentCreateParams", - "EnvironmentUpdateParams", - "DeploymentCreateParams", - "DeploymentUpdateParams", - "SessionEventSendParams", - "WebhookEndpointCreateParams", - "WebhookEndpointUpdateParams", - "WebhookEventListParams", - "WebhookStatus", - "WebhookDisabledReason", - "WebhookDeliveryStatus", - "WebhookEventType", - "SSEEventType", - "SessionStatus", - "StopReasonType", - "PermissionPolicyType", -] diff --git a/dashscope/v2/finetune/__init__.py b/dashscope/v2/finetune/__init__.py deleted file mode 100644 index ed3aa25f..00000000 --- a/dashscope/v2/finetune/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Agentic fine-tuning, owned by the 2.0 package. - -Holds the agentic-RL workflow package (``reinforcement``) and the -``AgenticRL`` driver that ``Client(...).rl`` is built on. - -The 1.x job-management surface -- ``dashscope.FineTunes`` and -``dashscope.Deployments`` -- stays at ``dashscope.finetune``; only the -modules listed in the decoupling plan moved here, and each keeps a -reverse-reference shim at its old import path. - -Deliberately imports nothing at module level: ``reinforcement`` requires -pydantic / tenacity / pyyaml, and pulling those into ``import -dashscope.v2`` would make the 2.0 client depend on them. -""" diff --git a/dashscope/v2/finetune/agentic_rl.py b/dashscope/v2/finetune/agentic_rl.py deleted file mode 100644 index 003ab06e..00000000 --- a/dashscope/v2/finetune/agentic_rl.py +++ /dev/null @@ -1,762 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""The agentic-RL workflow driver, owned by the 2.0 package. - -``Client(...).rl`` is built on this class. The 1.x job-management surface --- ``FineTunes`` / ``Deployments`` and the ``FineTune`` dataclasses -- -stays at ``dashscope.finetune``; the thin subclass there re-adds those -five verbs and swaps ``SubmitResult`` back for ``FineTune``. - -Two deliberate differences from the 1.x class this was split out of: - -- ``submit_job`` returns ``SubmitResult`` instead of a 1.x ``FineTune`` - dataclass, and posts through ``dashscope.v2.finetune._submit`` instead - of inheriting ``CreateMixin``. -- The api key is the constructor argument or ``DASHSCOPE_API_KEY``, never - the ``dashscope.api_key`` root global. ``set_api_key`` already writes - the argument into that env var and raises when it cannot, so resolution - here is unambiguous -- reading the root global could silently disagree - with the key this instance was built with. -""" - -from __future__ import annotations - -import os -import time -from typing import Union, List, Optional, ClassVar, Dict, Any -from typing_extensions import Self - -from dashscope.v2.finetune import _submit -from dashscope.v2.finetune.reinforcement import ( - AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, - Dataset, - TrainingDataset, - ValidationDataset, -) -from dashscope.v2.finetune.reinforcement import AgenticRLTuning, TuningModel -from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.v2.finetune.reinforcement import ( - FunctionType, - DatasetsType, - TrainingType, -) -from dashscope.v2.finetune.reinforcement import ( - RewardInput, - RolloutInput, - GroupRewardInput, -) -from dashscope.v2.finetune.reinforcement import logger -from dashscope.v2.finetune.reinforcement import ( - set_api_key, - generate_random_id, - get_func_type_id, - deep_remove_none, -) -from dashscope.v2.finetune.reinforcement.common.errors import ( - RegistrationError, - ValidationError, - InstanceQueryError, - RuntimeErrorWithCode, - ValueErrorWithCode, - DatasetsError, -) - -__all__ = ["AgenticRL", "SubmitOutput", "SubmitResult"] - - -def _inner_code(exc: Exception) -> str: - return getattr(exc, "code", None) or type(exc).__name__ - - -class SubmitOutput: - """The ``output`` payload nested in a submit response. - - A plain attribute container on purpose: the server's job payload is - open-ended, and ``reinforcement_learning._to_job`` flattens it with - ``vars(output)``, which needs a ``__dict__``. - """ - - def __init__(self, **kwargs: Any): - self.__dict__.update(kwargs) - - def __repr__(self) -> str: - return f"{type(self).__name__}({self.__dict__!r})" - - -class SubmitResult: - """The envelope ``AgenticRL.submit_job`` returns. - - Carries the same seven fields as 1.x's ``DashScopeAPIResponse``, so - ``FineTune(**envelope)`` and ``SubmitResult(**envelope)`` accept the - same dict. A failed submit is a non-200 ``status_code`` here rather - than an exception, exactly as it was under ``FineTune``. - """ - - def __init__( - self, - status_code: int = None, - request_id: str = "", - code: str = None, - message: str = "", - output: Any = None, - usage: Any = None, - headers: Dict[str, Any] = None, - **kwargs: Any, - ): - self.__dict__.update(kwargs) - self.status_code = status_code - self.request_id = request_id - self.code = code - self.message = message - self.output = ( - SubmitOutput(**output) if isinstance(output, dict) else output - ) - self.usage = usage - self.headers = headers or {} - - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(status_code={self.status_code!r}, " - f"request_id={self.request_id!r}, code={self.code!r}, " - f"message={self.message!r}, output={self.output!r}, " - f"usage={self.usage!r})" - ) - - -class AgenticRL(AgenticRLTuning): - SUB_PATH: ClassVar[str] = "fine-tunes" - - def __init__(self, api_key: str = None): - super().__init__() - - try: - set_api_key(api_key) - except Exception as e: - raise ValueErrorWithCode.from_catalog( - "sdk.InvalidApiKey", - error_code=3001, - ) from e - - self._api_key = api_key - - def _resolve_api_key(self) -> str: - """The key to submit with; see the module docstring.""" - return self._api_key or os.environ.get("DASHSCOPE_API_KEY", "") - - def init(self, config_path: Optional[str] = None, **kwargs) -> Self: - """ - Initialize an AgenticRL instance from a YAML configuration file. - """ - self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) - - return self - - async def register_functions( - self, - functions: Optional[ - Union[ - List[Union[RolloutFunctionComponent, RewardFunctionComponent]], - RolloutFunctionComponent, - RewardFunctionComponent, - ] - ] = None, - lazy_load: Optional[bool] = True, - ) -> tuple[ - List[str], - List[str], - List[str], - List[str], - List[str], - List[str], - ]: - """Register function components and return entity/instance IDs.""" - if functions: - self.tuning.functions = functions - - try: - ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) = await self.tuning.register_functions( - lazy_load=lazy_load, - ) - logger.info("Function components registered") - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RegistrationError.from_catalog( - "sdk.agentic_rl.FunctionRegistrationFailed", - error_code=3002, - variables={"inner_code": _inner_code(e)}, - ) from e - - return ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) - - async def upload_datasets( - self, - datasets: Optional[List[Dataset]] = None, - training_files: Optional[Union[List[str], str]] = None, - validation_files: Optional[Union[List[str], str]] = None, - ) -> tuple[List[str], List[str]]: - if datasets: - self.tuning.datasets = datasets - - try: - ( - uploaded_training_ids, - uploaded_validation_ids, - ) = await self.tuning.upload_datasets( - training_files=training_files or [], - validation_files=validation_files or [], - ) - logger.info("Datasets uploaded") - except Exception as e: - raise DatasetsError.from_catalog( - "sdk.agentic_rl.DatasetsUploadFailed", - error_code=3003, - variables={"inner_code": _inner_code(e)}, - ) from e - - return uploaded_training_ids, uploaded_validation_ids - - def _build_submit_request( - self, - model: Optional[str] = None, - datasets: Optional[List[Dataset]] = None, - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - ) -> Dict[str, Any]: - """Assemble the job payload, folding the arguments into ``tuning``. - - Split out of ``submit_job`` so the 1.x surface can reuse it - without touching the transport. - """ - teacher_model = teacher_model or self.tuning.teacher_model - - # Resolve job name (fallback to class default) - if job_name: - self.tuning.name = job_name - job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" - - # Model name - if model: - self.tuning.model.name = model - - # rollouts/rewards - if functions: - self.tuning.functions = functions - rollouts = self.tuning.combine_ids_runtimes( - functype=FunctionType.ROLLOUT, - ) - rewards = self.tuning.combine_ids_runtimes( - functype=FunctionType.REWARD, - ) - rewards.extend( - self.tuning.combine_ids_runtimes( - functype=FunctionType.GROUP_REWARD, - id_str=get_func_type_id(FunctionType.REWARD), - ), - ) - # names of functions - if not self.tuning.check_function_names(): - names = [ - getattr(fc, "name", "") - for fc in self.tuning.functions - ] - duplicates = sorted( - {name for name in names if names.count(name) > 1}, - ) - raise ValueErrorWithCode.from_catalog( - "sdk.agentic_rl.DuplicateFunctionNames", - error_code=3004, - variables={"names": ", ".join(duplicates or names)}, - ) - - # datasets - if datasets: - self.tuning.datasets = datasets - training_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.TRAINING - ] - validation_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.VALIDATION - ] - - # hyper_parameters - if hyper_parameters: - self.tuning.training.hyper_parameters = hyper_parameters - - # resources - if resources: - self.tuning.training.resources = resources - - training_type = self.tuning.training.type - if teacher_model: - training_type = TrainingType.PG_OPD - elif training_type == TrainingType.PG_OPD: - raise ValueErrorWithCode( - f"teacher_model is required when training.type is {training_type}", - error_code=3006, - ) - - request = { - "model": self.tuning.model.name, - "training_datasets": [ds.model_dump() for ds in training_datasets], - "validation_datasets": [ - ds.model_dump() for ds in validation_datasets - ], - "rollout": rollouts[0] if rollouts else None, - "rewards": rewards, - "hyper_parameters": self.tuning.training.hyper_parameters, - "resource_config": self.tuning.training.resources, - "training_type": str(training_type), - "teacher_model": teacher_model, - "job_name": job_name_with_suffix, - } - request = deep_remove_none(request) - logger.info(f"agentic_rl submit_job request: {request}") - - return request - - def _submit_envelope( - self, - model: Optional[str] = None, - datasets: Optional[List[Dataset]] = None, - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - **kwargs, - ) -> Dict[str, Any]: - """Build the payload, POST it, and return the raw envelope. - - Shared by both surfaces: this class wraps the result in - ``SubmitResult``, the 1.x subclass in a ``FineTune`` dataclass. - ``kwargs`` are forwarded to ``post_submit`` (``path``, - ``timeout``, and overrides for ``api_key`` / ``base_url``). - """ - request = self._build_submit_request( - model=model, - datasets=datasets, - functions=functions, - hyper_parameters=hyper_parameters, - resources=resources, - job_name=job_name, - teacher_model=teacher_model, - ) - - kwargs.setdefault("api_key", self._resolve_api_key()) - kwargs.setdefault("base_url", DASHSCOPE_HTTP_BASE_URL) - kwargs.setdefault("path", self.SUB_PATH) - try: - return _submit.post_submit(request, **kwargs) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode.from_catalog( - "sdk.agentic_rl.JobSubmissionFailed", - error_code=3005, - variables={"inner_code": _inner_code(e)}, - ) from e - - def submit_job( - self, - model: Optional[str] = None, - datasets: Optional[List[Dataset]] = None, - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - **kwargs, - ) -> SubmitResult: - """ - Submit RL tuning job to the platform. - """ - return SubmitResult( - **self._submit_envelope( - model=model, - datasets=datasets, - functions=functions, - hyper_parameters=hyper_parameters, - resources=resources, - job_name=job_name, - teacher_model=teacher_model, - **kwargs, - ), - ) - - async def run( - self, - model: Optional[str] = None, - # Datasets parameters - training_datasets: Optional[List[TrainingDataset]] = None, - validation_datasets: Optional[List[ValidationDataset]] = None, - # Path-driven parameters (auto-register & upload) - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - # Common parameters - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - **kwargs, - ) -> SubmitResult: - """ - Execute RL tuning workflow. - """ - try: - logger.info( - "Path-Driven mode: Registering functions & uploading " - "datasets...", - ) - await self.register_functions( - functions=functions, - lazy_load=True, - ) - - datasets = list(training_datasets or []) + list( - validation_datasets or [], - ) - await self.upload_datasets( - datasets=datasets, - ) - - return self.submit_job( - model=model, - datasets=datasets, - hyper_parameters=hyper_parameters, - resources=resources, - job_name=job_name, - teacher_model=teacher_model, - **kwargs, - ) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode.from_catalog( - "sdk.agentic_rl.WorkflowFailed", - error_code=3006, - variables={"inner_code": _inner_code(e)}, - ) from e - - @classmethod - async def test_functions( - cls, - instance_id: str, - functype: FunctionType, - input_data: Dict[str, Any], - api_key: str = None, - pull_logs: bool = False, - log_page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ): - """Test a deployed function instance with custom input data. - - Args: - instance_id: Target function instance ID. - functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). - input_data: Test input payload. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - pull_logs: If True, pull all logs of the function instance - (with pagination) after verification and print them - between separator markers. - log_page_size: Page size used when pulling logs. - start_time: Optional start time filter (in seconds) for log - pulling. Defaults to 24 hours before ``end_time``. - end_time: Optional end time filter (in seconds) for log - pulling. Defaults to the current time (now). - """ - try: - set_api_key(api_key) - - if functype == FunctionType.ROLLOUT: - value = RolloutInput.model_validate(input_data) - elif functype == FunctionType.REWARD: - value = RewardInput.model_validate(input_data) - elif functype == FunctionType.GROUP_REWARD: - value = GroupRewardInput.model_validate(input_data) - else: - raise ValueErrorWithCode.from_catalog( - "sdk.agentic_rl.UnsupportedFunctionType", - error_code=3007, - variables={"functype": str(functype)}, - ) - - logger.info( - f"Starting {str(functype)} verification", - extra={ - "instance_id": instance_id, - "input_params": value.model_dump(exclude={"api_key"}), - }, - ) - - result = await AgenticRLFunctionComponent.verify_function( - value, - instance_id, - ) - - if pull_logs: - await cls._pull_function_instance_logs( - function_instance_id=instance_id, - page_size=log_page_size, - start_time=start_time, - end_time=end_time, - ) - - return result - - except Exception as e: - raise ValidationError.from_catalog( - "sdk.agentic_rl.FunctionTestFailed", - error_code=3008, - variables={"inner_code": _inner_code(e)}, - ) from e - - @classmethod - async def query_function_instance_logs( - cls, - function_instance_id: str, - page_number: int = 1, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> Dict[str, Any]: - """Query one page of logs for a function (faas) instance. - - Args: - function_instance_id: Target function instance ID. - page_number: Page number, starting from 1. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the log query API. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_function_instance_logs( - function_instance_id=function_instance_id, - page_number=page_number, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3009, - ) from e - - @classmethod - async def query_all_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> List[str]: - """Fetch all logs of a function (faas) instance. - - Pages are fetched one by one via the paginated log query until - all entries are collected. - - Args: - function_instance_id: Target function instance ID. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Aggregated list of log messages across all pages. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3010, - ) from e - - @classmethod - async def delete_function_instance( - cls, - function_instance_id: str, - api_key: str = None, - ) -> Dict[str, Any]: - """Delete a function (faas) runtime instance. - - Args: - function_instance_id: Target function instance ID. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the delete API, containing - ``data.status`` (e.g. "deleted") and - ``data.sandbox_code``. - - Raises: - InstanceQueryError: If the deletion fails. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.delete_function_instance( - function_instance_id=function_instance_id, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance deletion failed", - error_code=3011, - ) from e - - @classmethod - async def _pull_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ) -> None: - """Pull all logs of a function instance and print them between - separator markers (best effort, never raises). - - When ``start_time``/``end_time`` are not provided, defaults to - pulling logs from 24 hours ago until now. - """ - if end_time is None: - end_time = int(time.time()) - if start_time is None: - start_time = end_time - 24 * 60 * 60 - logger.info( - "************start query log " - f"(function_instance_id={function_instance_id})*******", - ) - try: - logs = await cls.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - ) - for entry in logs: - logger.info(f"[function instance log] {entry}") - logger.info( - f"Pulled {len(logs)} log entries for function instance " - f"{function_instance_id}", - ) - except Exception as e: - logger.warning( - f"Failed to pull logs for function instance " - f"{function_instance_id}: {e}", - ) - finally: - logger.info("************end query log *******") diff --git a/dashscope/v2/finetune/reinforcement/__init__.py b/dashscope/v2/finetune/reinforcement/__init__.py deleted file mode 100644 index 391e24fd..00000000 --- a/dashscope/v2/finetune/reinforcement/__init__.py +++ /dev/null @@ -1,257 +0,0 @@ -# -*- coding: utf-8 -*- -try: - import pydantic # noqa: F401 - import tenacity # noqa: F401 - import yaml # noqa: F401 -except ImportError as _rl_dep_err: - raise ImportError( - "Agentic RL fine-tuning dependencies are missing from this " - "install. Reinstall with: pip install -U dashscope", - ) from _rl_dep_err - -from dashscope.v2.finetune.reinforcement.common.constants import ( - BAILIAN_FILE_API, - BAILIAN_FILE_TIMEOUT, - DASHSCOPE_API_KEY, - DASHSCOPE_HTTP_BASE_URL, - LOG_LEVEL, - FC_API_KEY, - FC_DELETE_API, - FC_FILES_START, - FC_LOAD_API, - FC_PYPI_LIB, - FC_PYPI_REPO, - FC_QUERY_API, - FC_QUERY_LOG_API, - FC_REGISTER_REWARD_API, - FC_REGISTER_ROLLOUT_API, - FC_REGISTER_GROUP_REWARD_API, - FC_SERVER_CLASSPATH, - FC_UPLOAD_OSS_API, - FC_WORKERS_COUNT, - FC_LAYER_CREATE_API, - FC_LAYER_QUERY_API, - FC_LAYER_USED, - FC_LAYER_NAME, - FC_REQUIREMENTS_FILE, - FC_ZIP_EXCLUDE_PATTERNS, - FC_OSS_FILE_SIZE_WARNING, - DATASETS_FILE_SIZE_WARNING, - HTTP_REQUEST_TIMEOUT, - LOGGER_FILTER_FIELDS, -) -from dashscope.v2.finetune.reinforcement.common.errors import ( - AgenticRLError, - InputError, - OutputError, - BaseConnectionError, - OSSConnectionError, - OSSUploadError, - DeploymentError, - RegistrationError, - FunctionLoadError, - InstanceWarmupError, - InstanceQueryError, - FunctionLayerError, - ValidationError, - ConfigurationError, - BasePermissionError, - IOErrorWithCode, - RuntimeErrorWithCode, - ValueErrorWithCode, -) -from dashscope.v2.finetune.reinforcement.common.log import logger -from dashscope.v2.finetune.reinforcement.common.model import ( - FunctionComponentModel, - TuningModel, - FoundationModel, - FunctionComponentRuntime, - AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, - GroupRewardFunctionComponent, - Datasets, - Dataset, - TrainingDataset, - ValidationDataset, - MountStorage, - Training, - Observability, - AgenticRLTuning, - Models, -) -from dashscope.v2.finetune.reinforcement.common.model_types import ( - FileSpec, - FunctionType, - DatasetsType, - DataSourceType, - RequestFC, - ResponseFC, - Status, - StatusType, - TrainingType, -) -from dashscope.v2.finetune.reinforcement.common.utils import ( - check_file, - client_fc, - create_deployment_files, - deep_mask, - generate_random_id, - get_filepath_classname, - upload_zip_to_oss_and_by_signed_url, - set_api_key, - to_bailian_data, - zip_dir, - get_func_type_id, - deep_remove_none, - extract_reward_weights, - get_weights_from_file, - serialize_for_output, -) -from dashscope.v2.finetune.reinforcement.component import ( - reward_func, - sub_reward_func, - aggregate_func, -) -from dashscope.v2.finetune.reinforcement.component.data import ( - AgentOutput, - Reward, - RewardInput, - RewardOutput, - RolloutInput, - RolloutOutput, - GroupReward, - GroupRewardInput, - GroupRewardOutput, - TaskStatus, -) -from dashscope.v2.finetune.reinforcement.component.func_decorator import ( - RewardProcessorMeta, -) -from dashscope.v2.finetune.reinforcement.component.processor import ( - AbstractRolloutProcessor, -) -from dashscope.v2.finetune.reinforcement.component.processor import ( - AbstractRewardProcessor, -) -from dashscope.v2.finetune.reinforcement.component.processor import ( - AbstractGroupRewardProcessor, -) - - -__all__ = [ - # Constants - "BAILIAN_FILE_API", - "BAILIAN_FILE_TIMEOUT", - "DASHSCOPE_API_KEY", - "DASHSCOPE_HTTP_BASE_URL", - "LOG_LEVEL", - "FC_API_KEY", - "FC_DELETE_API", - "FC_FILES_START", - "FC_LOAD_API", - "FC_PYPI_LIB", - "FC_PYPI_REPO", - "FC_QUERY_API", - "FC_QUERY_LOG_API", - "FC_REGISTER_REWARD_API", - "FC_REGISTER_ROLLOUT_API", - "FC_REGISTER_GROUP_REWARD_API", - "FC_SERVER_CLASSPATH", - "FC_UPLOAD_OSS_API", - "FC_WORKERS_COUNT", - "FC_LAYER_CREATE_API", - "FC_LAYER_QUERY_API", - "FC_LAYER_USED", - "FC_LAYER_NAME", - "FC_REQUIREMENTS_FILE", - "FC_ZIP_EXCLUDE_PATTERNS", - "FC_OSS_FILE_SIZE_WARNING", - "DATASETS_FILE_SIZE_WARNING", - "HTTP_REQUEST_TIMEOUT", - "LOGGER_FILTER_FIELDS", - # Errors - "AgenticRLError", - "InputError", - "OutputError", - "BaseConnectionError", - "OSSConnectionError", - "OSSUploadError", - "DeploymentError", - "RegistrationError", - "FunctionLoadError", - "FunctionLayerError", - "InstanceWarmupError", - "InstanceQueryError", - "ValidationError", - "ConfigurationError", - "BasePermissionError", - "IOErrorWithCode", - "RuntimeErrorWithCode", - "ValueErrorWithCode", - # Logging - "logger", - # Utilities - "check_file", - "client_fc", - "create_deployment_files", - "deep_mask", - "generate_random_id", - "get_filepath_classname", - "set_api_key", - "to_bailian_data", - "upload_zip_to_oss_and_by_signed_url", - "zip_dir", - "get_func_type_id", - "deep_remove_none", - "extract_reward_weights", - "get_weights_from_file", - "serialize_for_output", - # Model Types - "FileSpec", - "FunctionType", - "DatasetsType", - "DataSourceType", - "RequestFC", - "ResponseFC", - "Status", - "StatusType", - "TrainingType", - # Data models - "RolloutInput", - "RolloutOutput", - "RewardInput", - "RewardOutput", - "GroupRewardInput", - "GroupRewardOutput", - "GroupReward", - "Reward", - "AgentOutput", - "TaskStatus", - # Func Manager - "reward_func", - "sub_reward_func", - "aggregate_func", - "RewardProcessorMeta", - "AbstractRolloutProcessor", - "AbstractRewardProcessor", - "AbstractGroupRewardProcessor", - # Core Models - "FunctionComponentModel", - "TuningModel", - "FoundationModel", - "FunctionComponentRuntime", - "AgenticRLFunctionComponent", - "RolloutFunctionComponent", - "RewardFunctionComponent", - "GroupRewardFunctionComponent", - "Datasets", - "Dataset", - "TrainingDataset", - "ValidationDataset", - "MountStorage", - "Training", - "Observability", - "AgenticRLTuning", - "Models", -] diff --git a/dashscope/v2/finetune/reinforcement/component/demo/__init__.py b/dashscope/v2/finetune/reinforcement/component/demo/__init__.py deleted file mode 100644 index f43bc02a..00000000 --- a/dashscope/v2/finetune/reinforcement/component/demo/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.v2.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoGroupRewardProcessor, -) -from dashscope.v2.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRewardProcessor, -) -from dashscope.v2.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRolloutProcessor, -) - -__all__ = [ - "DemoRewardProcessor", - # Inherits AbstractRewardProcessor, returns RewardOutput - "DemoRolloutProcessor", - # Inherits AbstractRolloutProcessor, returns RolloutOutput - "DemoGroupRewardProcessor", - # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput -] diff --git a/dashscope/v2/finetune/reinforcement/component/parser/__init__.py b/dashscope/v2/finetune/reinforcement/component/parser/__init__.py deleted file mode 100644 index bb442998..00000000 --- a/dashscope/v2/finetune/reinforcement/component/parser/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( - BaseRequestParser, -) -from dashscope.v2.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 - GroupRewardRequestParser, -) -from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( - RewardRequestParser, -) -from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( - RolloutRequestParser, -) - -__all__ = [ - "BaseRequestParser", - "RewardRequestParser", - "RolloutRequestParser", - "GroupRewardRequestParser", -] diff --git a/dashscope/v2/finetune/reinforcement/component/processor/__init__.py b/dashscope/v2/finetune/reinforcement/component/processor/__init__.py deleted file mode 100644 index b579f7a4..00000000 --- a/dashscope/v2/finetune/reinforcement/component/processor/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractGroupRewardProcessor, -) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractProcessor, -) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRewardProcessor, -) -from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRolloutProcessor, -) - -__all__ = [ - "AbstractProcessor", - "AbstractRewardProcessor", - "AbstractRolloutProcessor", - "AbstractGroupRewardProcessor", -] diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh deleted file mode 100644 index b5454eef..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -#****************************************************************# -# ScriptName: build.sh -# Create Date: 2026-03-04 17:22 -# Modify Date: 2026-03-25 14:27 -# Function: -# Build and optionally install DashScope SDK for Agentic RL development -# Default behavior: Build wheel only, no installation -# Set INSTALL_LOCAL=1 to install local build -# Automatically detects the latest built wheel file -#***************************************************************# - -# Configuration -WORK_DIR='dashscope/v2/finetune/reinforcement/examples/workspace/' - -# Parse command line arguments -INSTALL_FLAG=0 -while [[ $# -gt 0 ]]; do - case "$1" in - -i|--install) - INSTALL_FLAG=1 - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -# Find project root directory (where setup.py is located) -find_project_root() { - # Start from current directory - local current_dir="$PWD" - - # Look for setup.py in current and parent directories - while [[ "$current_dir" != "/" ]]; do - if [[ -f "$current_dir/setup.py" ]]; then - echo "$current_dir" - return 0 - fi - current_dir=$(dirname "$current_dir") - done - - echo "Error: Could not find project root (setup.py)" >&2 - return 1 -} - -# Build SDK wheel function -build_sdk_wheel() { - echo "Building SDK wheel..." - if ! python3 -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1; then - echo "Error: Failed to upgrade build tools" >&2 - return 1 - fi - - if ! python3 setup.py sdist bdist_wheel >/dev/null 2>&1; then - echo "Error: Failed to build wheel package" >&2 - return 1 - fi - - rm -rf build - return 0 -} - -# Find the latest wheel file -find_latest_wheel() { - wheels=($(find dist -name "dashscope-*.whl" 2>/dev/null)) - - if [ ${#wheels[@]} -eq 0 ]; then - echo "Error: No wheel files found in dist directory" >&2 - return 1 - fi - - # Sort by modification time (newest first) - latest=$(ls -t dist/*.whl | head -n1 2>/dev/null) - - if [ -z "$latest" ]; then - echo "Error: Failed to determine latest wheel" >&2 - return 1 - fi - - echo "$latest" - return 0 -} - -# Install local wheel function -install_local_wheel() { - echo "Installing local wheel..." - if ! pip3 uninstall -y dashscope >/dev/null 2>&1; then - echo "Warning: Failed to uninstall existing dashscope" >&2 - fi - - if ! pip3 install -U "$1" -i https://mirrors.aliyun.com/pypi/simple/ >/dev/null 2>&1; then - echo "Error: Failed to install local wheel" >&2 - return 1 - fi - - return 0 -} - -# Main script execution -main() { - # Find project root - PROJECT_ROOT=$(find_project_root) || exit 1 - echo "Project root: $PROJECT_ROOT" - - # Navigate to project root - pushd "$PROJECT_ROOT" >/dev/null || exit 1 - - # Build SDK wheel - if ! build_sdk_wheel; then - echo "Build failed" >&2 - popd >/dev/null - exit 1 - fi - - # Find the latest wheel file - LATEST_WHL=$(find_latest_wheel) - if [ $? -ne 0 ]; then - echo "Error: Failed to find wheel file" >&2 - popd >/dev/null - exit 1 - fi - - # Extract the wheel filename - WHL_FILENAME=$(basename "$LATEST_WHL") - echo "Latest wheel file: $WHL_FILENAME" - - # Create workspace directory if needed - WORKSPACE_PATH="${PROJECT_ROOT}/${WORK_DIR}" - if [ ! -d "$WORKSPACE_PATH" ]; then - mkdir -p "$WORKSPACE_PATH" - fi - - # Copy wheel file to workspace - echo "Copying wheel ($WHL_FILENAME) to workspace..." - if ! cp -v "$LATEST_WHL" "${WORKSPACE_PATH}/"; then - echo "Error: Failed to copy wheel to workspace" >&2 - popd >/dev/null - exit 1 - fi - - # Install local wheel if requested - if [ "$INSTALL_FLAG" = "1" ]; then - if ! install_local_wheel "${WORKSPACE_PATH}/${WHL_FILENAME}"; then - echo "Installation failed" >&2 - popd >/dev/null - exit 1 - fi - echo "Successfully installed local build" - else - echo "Local build created but not installed (use --install to install)" - fi - - # Return to original directory - popd >/dev/null - exit 0 -} - -# Execute main function -main diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh deleted file mode 100644 index 227618a7..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -#****************************************************************# -# ScriptName: query_group_reward.sh -# Create Date: 2026-03-16 11:44 -# Modify Date: 2026-03-16 11:44 -# Function: -#***************************************************************# -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "group_reward", - "ground_truth": "0.5", - "request_metadata": null, - "agent_outputs": [{ - "messages": [{"role": "user", "content": "Test question"}], - "rollout_extra": { - "content": "This is a test content that exceeds 50 characters in length and should receive a reward score of 0.5" - }, - "rollout_metrics": {"accuracy": 0.95}, - "reward_score": null - }], - "rollout_id": "ro-ins-e8e92e94-b5c3-4546-b7b1-9832b8b948f2" -}' diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh deleted file mode 100644 index da04f084..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -#****************************************************************# -# ScriptName: query_reward.sh -# Create Date: 2026-03-16 11:44 -# Modify Date: 2026-03-16 11:44 -# Function: -#***************************************************************# -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "reward", - "ground_truth": "0.5", - "request_metadata": null, - "agent_output": { - "messages": [{"role": "user", "content": "Test question"}], - "rollout_extra": { - "content": "This is a test content that exceeds 50 characters in length and should receive a reward score of 0.5" - }, - "rollout_metrics": {"accuracy": 0.95}, - "reward_score": null - }, - "rollout_id": "ro-ins-e8e92e94-b5c3-4546-b7b1-9832b8b948f2" -}' diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh deleted file mode 100644 index 9a459b1f..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh -#****************************************************************# -# ScriptName: query_local_reward_decorator.sh -# Create Date: 2026-03-16 11:44 -# Modify Date: 2026-03-16 11:44 -# Function: -#***************************************************************# -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "reward", - "ground_truth": "0.5", - "agent_output": { - "messages": [{"role": "user", "content": "I cannot help you with that"}] - } -}' & - -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "reward", - "ground_truth": "0.5", - "agent_output": { - "messages": [{"role": "user", "content": "I attack you with that"}] - } -}' & - -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "reward", - "ground_truth": "0.5", - "agent_output": { - "messages": [{"role": "user", "content": "I walk with you"}] - } -}' & diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh deleted file mode 100644 index 23c44c84..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -#****************************************************************# -# ScriptName: 1.sh -# Create Date: 2026-03-16 11:30 -# Modify Date: 2026-03-16 11:30 -# Function: -#***************************************************************# -curl -X POST http://localhost:8000/api/v1 \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer xxx" \ - -d '{ - "func_type": "rollout", - "messages": [ - { - "role": "user", - "content": "Hello, please introduce yourself" - } - ], - "tools": null, - "ground_truth": "", - "rollout_extra": { - "rollout_id": "ro-ins-e8e92e94-b5c3-4546-b7b1-9832b8b948f2", - "training_state": { - "task_id": "task-001" - } - }, - "sampling_params": { - "temperature": 0.7, - "max_tokens": 1024, - "max_turns": 25, - "timeout": 60.0 - }, - "model_resource": { - "model_name": "qwen3.5-35b-a3b", - "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "api_key": "your-api-key-here" - }, - "request_metadata": { - "protocol": "openai", - "system_prompt": "", - "job_id": "dummy-job-id", - "sample_id": "dummy-sample-id", - "rollout_id": "dummy-rollout-id", - "attempt_id": "dummy-attempt-id" - }, - "additional_field": "any extra data you want" -}' diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh deleted file mode 100644 index 70e616b1..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh -#****************************************************************# -# ScriptName: 1.sh -# Create Date: 2026-03-16 11:30 -# Modify Date: 2026-03-16 11:30 -# Function: -#***************************************************************# -curl -X POST https://dashscope.aliyuncs.com/api/v1/fine-tunes \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer sk-xxx" \ - -d '{ - 'model': 'qwen3-32b', - 'rollout': { - 'rollout_id': 'ro-50ada8b9-e027-4e43-9b6d-c0c875a8f06b', - 'cpu': 2, - 'memory_size': 4096, - 'disk_size': 10240, - 'concurrency': 2 - }, - 'rewards': [{ - 'reward_id': 'rw-4092eb99-8252-46a7-9919-3819df9484b1', - 'cpu': 2, - 'memory_size': 4096, - 'disk_size': 10240, - 'concurrency': 10 - }, { - 'reward_id': 'rw-38d37050-6c07-4b63-b014-fbb048c2a1c0', - 'cpu': 2, - 'memory_size': 4096, - 'disk_size': 10240, - 'concurrency': 5 - }], - 'training_file_ids': ['c5c39255-04d5-4d2f-9df5-c4db06107046'], - 'validation_file_ids': ['ec24290d-67bd-4378-93bf-9c23a27d4b79'], - 'hyper_parameters': { - 'batch_size': '256' - }, - 'training_type': 'reinforcement', - 'job_name': 'agentic-rl-3773091b', - 'model_name': '' -}' diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh deleted file mode 100755 index 66c28d95..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -#****************************************************************# -# ScriptName: start_reward_server.sh -# Create Date: 2026-03-17 10:05 -# Modify Date: 2026-03-24 17:54 -# Function: -#***************************************************************# -set -euo pipefail # Strict error handling - -cd ../ - -# ================= Configuration ================== -#VERSION="1.25.16" -SERVICE_TYPE="group_reward" # group_reward/reward|rollout -PROCESSOR_CLASS="functions.reward.group_reward.DemoGroupRewardProcessor" # Full class path -PYPI_REPO="https://mirrors.aliyun.com/pypi/simple/" -#SDK_PACKAGE="dashscope-${VERSION}-py3-none-any.whl fastapi pyyaml uvicorn" -REQUIREMENTS_FILE="./requirements.txt" -LOG_DIR="/tmp/log/agentic_rl" -MAX_RETRIES=3 - -# ================ Helper Functions ================ -init_logging() { - mkdir -p "${LOG_DIR}" - local log_file="${LOG_DIR}/service_$(date +%Y%m%d).log" - exec 3>&1 4>&2 - trap 'exec 1>&3 2>&4' EXIT - # 关键修复:使用双引号、修正进程替换语法 - exec > >(tee -a "$log_file") 2>&1 - echo -e "\n\n=== Service Start: $(date) ===" -} - -log() { - printf "[%s] %s\n" "$(date +'%Y-%m-%d %H:%M:%S')" "$*" -} - -cleanup() { - log "Cleaning temporary resources..." - rm -rf "${TMPDIR:-/tmp}/pip*" - find . -name "__pycache__" -exec rm -rf {} + 2>/dev/null -} - -validate_environment() { - log "Validating runtime environment..." - - # Python check - if ! command -v python3 &>/dev/null; then - log "ERROR: Python3 not found in PATH" - exit 101 - fi -} - -# ============== Dependency Management ============== -install_with_retry() { - local packages=("$@") - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - log "Installing ${packages[*]} (attempt $((retry_count+1))/${MAX_RETRIES})" - if python3 -m pip install -U "${packages[@]}" \ - --index-url "${PYPI_REPO}" \ - --no-cache-dir \ - --compile; then - return 0 - fi - retry_count=$((retry_count+1)) - sleep $((retry_count * 10)) - done - - log "FATAL: Failed to install ${packages[*]} after ${MAX_RETRIES} attempts" - return 1 -} - -# ================ Main Execution =================== -main() { - # Phase 1: Initialization - init_logging - trap cleanup EXIT - validate_environment - - # Phase 2: Dependency Setup - log "Starting dependency installation" - -# local packages=($SDK_PACKAGE) -# for pkg in "${packages[@]}"; do -# if ! install_with_retry "$pkg"; then -# exit 201 -# fi -# done - - if [ -f "${REQUIREMENTS_FILE}" ]; then - log "Installing additional requirements" - if ! install_with_retry -r "${REQUIREMENTS_FILE}"; then - exit 202 - fi - fi - - # Phase 3: Environment Configuration - export FUNC_TYPE="${SERVICE_TYPE}" - export PROCESSOR_CLASS="${PROCESSOR_CLASS}" - export PYTHONPATH=".:${PYTHONPATH:-}" - - log "Final Environment:" - env | grep -E 'FUNC_TYPE|PROCESSOR_CLASS|PYTHONPATH' - - # Phase 4: Service Launch - log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.reinforcement.component.server.server "$@" -} - -# ==================== Entry ======================== -main "$@" diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh deleted file mode 100755 index d77826f1..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -#****************************************************************# -# ScriptName: start_reward_server.sh -# Create Date: 2026-03-17 10:05 -# Modify Date: 2026-03-24 17:54 -# Function: -#***************************************************************# -set -euo pipefail # Strict error handling - -cd ../ - -# ================= Configuration ================== -#VERSION="1.25.16" -SERVICE_TYPE="reward" # reward|rollout -PROCESSOR_CLASS="functions.reward.reward.DemoRewardProcessor" # Full class path -PYPI_REPO="https://mirrors.aliyun.com/pypi/simple/" -#SDK_PACKAGE="dashscope-${VERSION}-py3-none-any.whl fastapi pyyaml uvicorn" -REQUIREMENTS_FILE="./requirements.txt" -LOG_DIR="/tmp/log/agentic_rl" -MAX_RETRIES=3 - -# ================ Helper Functions ================ -init_logging() { - mkdir -p "${LOG_DIR}" - local log_file="${LOG_DIR}/service_$(date +%Y%m%d).log" - exec 3>&1 4>&2 - trap 'exec 1>&3 2>&4' EXIT - exec > >(tee -a "$log_file") 2>&1 - echo -e "\n\n=== Service Start: $(date) ===" -} - -log() { - printf "[%s] %s\n" "$(date +'%Y-%m-%d %H:%M:%S')" "$*" -} - -cleanup() { - log "Cleaning temporary resources..." - rm -rf "${TMPDIR:-/tmp}/pip*" - find . -name "__pycache__" -exec rm -rf {} + 2>/dev/null -} - -validate_environment() { - log "Validating runtime environment..." - - # Python check - if ! command -v python3 &>/dev/null; then - log "ERROR: Python3 not found in PATH" - exit 101 - fi -} - -# ============== Dependency Management ============== -install_with_retry() { - local packages=("$@") - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - log "Installing ${packages[*]} (attempt $((retry_count+1))/${MAX_RETRIES})" - if python3 -m pip install -U "${packages[@]}" \ - --index-url "${PYPI_REPO}" \ - --no-cache-dir \ - --compile; then - return 0 - fi - retry_count=$((retry_count+1)) - sleep $((retry_count * 10)) - done - - log "FATAL: Failed to install ${packages[*]} after ${MAX_RETRIES} attempts" - return 1 -} - -# ================ Main Execution =================== -main() { - # Phase 1: Initialization - init_logging - trap cleanup EXIT - validate_environment - - # Phase 2: Dependency Setup - log "Starting dependency installation" - -# local packages=($SDK_PACKAGE) -# for pkg in "${packages[@]}"; do -# if ! install_with_retry "$pkg"; then -# exit 201 -# fi -# done - - if [ -f "${REQUIREMENTS_FILE}" ]; then - log "Installing additional requirements" - if ! install_with_retry -r "${REQUIREMENTS_FILE}"; then - exit 202 - fi - fi - - # Phase 3: Environment Configuration - export FUNC_TYPE="${SERVICE_TYPE}" - export PROCESSOR_CLASS="${PROCESSOR_CLASS}" - export PYTHONPATH=".:${PYTHONPATH:-}" - - log "Final Environment:" - env | grep -E 'FUNC_TYPE|PROCESSOR_CLASS|PYTHONPATH' - - # Phase 4: Service Launch - log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.reinforcement.component.server.server "$@" -} - -# ==================== Entry ======================== -main "$@" diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh deleted file mode 100755 index 9fbfe5e6..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -#****************************************************************# -# ScriptName: start_local_reward_decorator.sh -# Create Date: 2026-03-17 10:05 -# Modify Date: 2026-03-24 17:54 -# Function: -#***************************************************************# -set -euo pipefail # Strict error handling - -cd ../ - -# ================= Configuration ================== -#VERSION="1.25.16" -SERVICE_TYPE="reward" # reward|rollout -PROCESSOR_CLASS="functions.reward.reward_decorator.SafetyProcessor" # Full class path -PYPI_REPO="https://mirrors.aliyun.com/pypi/simple/" -#SDK_PACKAGE="dashscope-${VERSION}-py3-none-any.whl fastapi pyyaml uvicorn" -REQUIREMENTS_FILE="./requirements.txt" -LOG_DIR="/tmp/log/agentic_rl" -MAX_RETRIES=3 - -# ================ Helper Functions ================ -init_logging() { - mkdir -p "${LOG_DIR}" - local log_file="${LOG_DIR}/service_$(date +%Y%m%d).log" - exec 3>&1 4>&2 - trap 'exec 1>&3 2>&4' EXIT - exec > >(tee -a "$log_file") 2>&1 - echo -e "\n\n=== Service Start: $(date) ===" -} - -log() { - printf "[%s] %s\n" "$(date +'%Y-%m-%d %H:%M:%S')" "$*" -} - -cleanup() { - log "Cleaning temporary resources..." - rm -rf "${TMPDIR:-/tmp}/pip*" - find . -name "__pycache__" -exec rm -rf {} + 2>/dev/null -} - -validate_environment() { - log "Validating runtime environment..." - - # Python check - if ! command -v python3 &>/dev/null; then - log "ERROR: Python3 not found in PATH" - exit 101 - fi -} - -# ============== Dependency Management ============== -install_with_retry() { - local packages=("$@") - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - log "Installing ${packages[*]} (attempt $((retry_count+1))/${MAX_RETRIES})" - if python3 -m pip install -U "${packages[@]}" \ - --index-url "${PYPI_REPO}" \ - --no-cache-dir \ - --compile; then - return 0 - fi - retry_count=$((retry_count+1)) - sleep $((retry_count * 10)) - done - - log "FATAL: Failed to install ${packages[*]} after ${MAX_RETRIES} attempts" - return 1 -} - -# ================ Main Execution =================== -main() { - # Phase 1: Initialization - init_logging - trap cleanup EXIT - validate_environment - - # Phase 2: Dependency Setup - log "Starting dependency installation" - -# local packages=($SDK_PACKAGE) -# for pkg in "${packages[@]}"; do -# if ! install_with_retry "$pkg"; then -# exit 201 -# fi -# done - - if [ -f "${REQUIREMENTS_FILE}" ]; then - log "Installing additional requirements" - if ! install_with_retry -r "${REQUIREMENTS_FILE}"; then - exit 202 - fi - fi - - # Phase 3: Environment Configuration - export FUNC_TYPE="${SERVICE_TYPE}" - export PROCESSOR_CLASS="${PROCESSOR_CLASS}" - export PYTHONPATH=".:${PYTHONPATH:-}" - - log "Final Environment:" - env | grep -E 'FUNC_TYPE|PROCESSOR_CLASS|PYTHONPATH' - - # Phase 4: Service Launch - log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.reinforcement.component.server.server "$@" -} - -# ==================== Entry ======================== -main "$@" diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh deleted file mode 100644 index 3e606866..00000000 --- a/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -#****************************************************************# -# ScriptName: start_reward_server.sh -# Create Date: 2026-03-17 10:05 -# Modify Date: 2026-03-17 10:05 -# Function: -#***************************************************************# -set -euo pipefail # Strict error handling - -cd ../ - -# ================= Configuration ================== -#VERSION="1.25.16" -SERVICE_TYPE="rollout" # reward|rollout -PROCESSOR_CLASS="functions.rollout.rollout2.DemoRolloutProcessor" # Full class path -PYPI_REPO="https://mirrors.aliyun.com/pypi/simple/" -#SDK_PACKAGE="dashscope-${VERSION}-py3-none-any.whl fastapi pyyaml uvicorn" -REQUIREMENTS_FILE="./requirements.txt" -LOG_DIR="/tmp/log/agentic_rl" -MAX_RETRIES=3 - -# ================ Helper Functions ================ -init_logging() { - mkdir -p "${LOG_DIR}" - local log_file="${LOG_DIR}/service_$(date +%Y%m%d).log" - exec 3>&1 4>&2 - trap 'exec 1>&3 2>&4' EXIT - exec > >(tee -a "$log_file") 2>&1 - echo -e "\n\n=== Service Start: $(date) ===" -} - -log() { - printf "[%s] %s\n" "$(date +'%Y-%m-%d %H:%M:%S')" "$*" -} - -cleanup() { - log "Cleaning temporary resources..." - rm -rf "${TMPDIR:-/tmp}/pip*" - find . -name "__pycache__" -exec rm -rf {} + 2>/dev/null -} - -validate_environment() { - log "Validating runtime environment..." - - # Python check - if ! command -v python3 &>/dev/null; then - log "ERROR: Python3 not found in PATH" - exit 101 - fi -} - -# ============== Dependency Management ============== -install_with_retry() { - local packages=("$@") - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - log "Installing ${packages[*]} (attempt $((retry_count+1))/${MAX_RETRIES})" - if python3 -m pip install -U "${packages[@]}" \ - --index-url "${PYPI_REPO}" \ - --no-cache-dir \ - --compile; then - return 0 - fi - retry_count=$((retry_count+1)) - sleep $((retry_count * 10)) - done - - log "FATAL: Failed to install ${packages[*]} after ${MAX_RETRIES} attempts" - return 1 -} - -# ================ Main Execution =================== -main() { - # Phase 1: Initialization - init_logging - trap cleanup EXIT - validate_environment - - # Phase 2: Dependency Setup - log "Starting dependency installation" - -# local packages=($SDK_PACKAGE) -# for pkg in "${packages[@]}"; do -# if ! install_with_retry "$pkg"; then -# exit 201 -# fi -# done - - if [ -f "${REQUIREMENTS_FILE}" ]; then - log "Installing additional requirements" - if ! install_with_retry -r "${REQUIREMENTS_FILE}"; then - exit 202 - fi - fi - - # Phase 3: Environment Configuration - export FUNC_TYPE="${SERVICE_TYPE}" - export PROCESSOR_CLASS="${PROCESSOR_CLASS}" - export PYTHONPATH=".:${PYTHONPATH:-}" - - log "Final Environment:" - env | grep -E 'FUNC_TYPE|PROCESSOR_CLASS|PYTHONPATH' - - # Phase 4: Service Launch - log "Starting ${SERVICE_TYPE} service" - exec python3 -m dashscope.reinforcement.component.server.server "$@" -} - -# ==================== Entry ======================== -main "$@" diff --git a/dashscope/v2/tokenizers/__init__.py b/dashscope/v2/tokenizers/__init__.py deleted file mode 100644 index 4e340f37..00000000 --- a/dashscope/v2/tokenizers/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -"""Offline tokenizers owned by the 2.0 SDK. - -Pure local counting over the bundled tiktoken vocab -- no network, no -dependency on the 1.x package. ``dashscope.tokenizers`` re-exports these -so both stacks share one implementation and one copy of the vocab. -""" - -from .tokenizer import get_tokenizer, list_tokenizers -from .tokenizer_base import Tokenizer - -__all__ = [ - "Tokenizer", - "get_tokenizer", - "list_tokenizers", -] diff --git a/dashscope/v2/tokenizers/qwen_tokenizer.py b/dashscope/v2/tokenizers/qwen_tokenizer.py deleted file mode 100644 index 9a538f95..00000000 --- a/dashscope/v2/tokenizers/qwen_tokenizer.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. -# mypy: disable-error-code="annotation-unchecked" - -import base64 -import unicodedata -from typing import Collection, Dict, List, Set, Union - -from .tokenizer_base import Tokenizer - -PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long -ENDOFTEXT = "<|endoftext|>" -IMSTART = "<|im_start|>" -IMEND = "<|im_end|>" -# as the default behavior is changed to allow special tokens in -# regular texts, the surface forms of special tokens need to be -# as different as possible to minimize the impact -EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) -# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 -SPECIAL_START_ID = 151643 -SPECIAL_TOKENS = tuple( - enumerate( - ( - ( - ENDOFTEXT, - IMSTART, - IMEND, - ) - + EXTRAS - ), - start=SPECIAL_START_ID, - ), -) -SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) - -# tiktoken's BPE merges tokens recursively in Rust, which can overflow the -# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). -# Split text into chunks below this threshold before encoding. -_CHUNK_SIZE = 100_000 - - -class QwenTokenizer(Tokenizer): - @staticmethod - def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: - with open(tiktoken_bpe_file, "rb") as f: - contents = f.read() - return { - base64.b64decode(token): int(rank) - for token, rank in ( - line.split() for line in contents.splitlines() if line - ) - } - - def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): - self._errors = errors - self._vocab_file = vocab_file - self._extra_vocab_file = extra_vocab_file - - self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( - vocab_file, - ) # type: Dict[bytes, int] - self._special_tokens = { - token: index for index, token in SPECIAL_TOKENS - } - - # try load extra vocab from file - if extra_vocab_file is not None: - used_ids = set(self._mergeable_ranks.values()) | set( - self._special_tokens.values(), - ) - extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) - for token, index in extra_mergeable_ranks.items(): - if token in self._mergeable_ranks: - continue - if index in used_ids: - continue - self._mergeable_ranks[token] = index - # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long - import tiktoken - - enc = tiktoken.Encoding( - "Qwen", - pat_str=PAT_STR, - mergeable_ranks=self._mergeable_ranks, - special_tokens=self._special_tokens, - ) - assert ( - len(self._mergeable_ranks) + len(self._special_tokens) - == enc.n_vocab - ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long - - self.decoder = { - v: k for k, v in self._mergeable_ranks.items() - } # type: dict[int, bytes|str] - self.decoder.update({v: k for k, v in self._special_tokens.items()}) - - self._tokenizer = enc # type: tiktoken.Encoding - - self.eod_id = self._tokenizer.eot_token - self.im_start_id = self._special_tokens[IMSTART] - self.im_end_id = self._special_tokens[IMEND] - - def encode( # type: ignore[override] - self, - text: str, - allowed_special: Union[Set, str] = "all", - disallowed_special: Union[Collection, str] = (), - ) -> Union[List[List], List]: - text = unicodedata.normalize("NFC", text) - if len(text) <= _CHUNK_SIZE: - return self._tokenizer.encode( - text, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ) - - result = [] - for chunk in self._split_text(text): - result.extend( - self._tokenizer.encode( - chunk, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ), - ) - return result - - @staticmethod - def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: - """Split text into chunks at safe tokenization boundaries.""" - parts: List[str] = [] - for i, line in enumerate(text.split("\n")): - piece = line if i == 0 else "\n" + line - if len(piece) <= chunk_size: - parts.append(piece) - else: - for j in range(0, len(piece), chunk_size): - parts.append(piece[j : j + chunk_size]) - - chunks: List[str] = [] - current_chunk: List[str] = [] - current_len = 0 - for part in parts: - if current_len + len(part) <= chunk_size: - current_chunk.append(part) - current_len += len(part) - else: - if current_chunk: - chunks.append("".join(current_chunk)) - current_chunk = [part] - current_len = len(part) - if current_chunk: - chunks.append("".join(current_chunk)) - return chunks - - def decode( - self, - token_ids: Union[int, List[int]], - skip_special_tokens: bool = False, - errors: str = None, - **kwargs, - ) -> str: - if isinstance(token_ids, int): - token_ids = [token_ids] - if skip_special_tokens: - token_ids = [i for i in token_ids if i < self.eod_id] - return self._tokenizer.decode(token_ids, errors=errors or self._errors) diff --git a/dashscope/v2/tokenizers/tokenizer.py b/dashscope/v2/tokenizers/tokenizer.py deleted file mode 100644 index e8625bd7..00000000 --- a/dashscope/v2/tokenizers/tokenizer.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -import os -from typing import List - -from .qwen_tokenizer import QwenTokenizer -from .tokenizer_base import Tokenizer - -QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] -# The vocab ships next to this module, so the leaf stays relocatable. -_VOCAB_PATH = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "qwen.tiktoken", -) - - -def get_tokenizer(model: str) -> Tokenizer: - """Get a tokenizer based on model name. - - Args: - model (str): The model name. - - Raises: - ValueError: Not support model - - Returns: - Tokenizer: The `Tokenizer` of the model. - """ - if model.startswith("qwen"): - return QwenTokenizer(_VOCAB_PATH) - raise ValueError( - f"Not support model: {model}, currently only support qwen models.", - ) - - -def list_tokenizers() -> List[str]: - """List support models - - Returns: - List[str]: The model list. - """ - return QWEN_SERIALS diff --git a/dashscope/v2/tokenizers/tokenizer_base.py b/dashscope/v2/tokenizers/tokenizer_base.py deleted file mode 100644 index 2e385bf8..00000000 --- a/dashscope/v2/tokenizers/tokenizer_base.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from typing import List - - -class Tokenizer: - """Base tokenizer interface for local tokenizers.""" - - def __init__(self): - pass - - def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 - """Encode input text string to token ids. - - Args: - text (str): The string to be encoded. - - Returns: - List[int]: The token ids. - """ - - def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 - """Decode token ids to string. - - Args: - token_ids (List[int]): The input token ids. - - Returns: - str: The string of the token ids. - """ diff --git a/dev.md b/dev.md index b600396a..9d4fb9bd 100644 --- a/dev.md +++ b/dev.md @@ -41,3 +41,19 @@ pre-commit run --all-files `.github/workflows/pre-commit.yml` runs `pre-commit run --all-files` on every push and pull request, so code that bypasses the local hooks is still blocked in CI. + +## 2. Tests + +The current implementation lives at the package root. Archived +transitional suites are separated from current coverage: + +```bash +python -m pytest tests --ignore=tests/legacy_1x -q +coverage run -m pytest tests --ignore=tests/legacy_1x +coverage report -m +python -m pytest tests/legacy_1x -q +python -m pytest tests -q +``` + +Plain `pytest` runs both groups. The current and legacy groups also run as +separate jobs in `.github/workflows/unit_test.yml`. diff --git a/docs/source/api/dashscope.aigc.rst b/docs/source/api/dashscope.aigc.rst index e2a1c48a..7af21a3f 100644 --- a/docs/source/api/dashscope.aigc.rst +++ b/docs/source/api/dashscope.aigc.rst @@ -1,9 +1,9 @@ -dashscope.aigc -============== +dashscope.legacy_1x.aigc +======================== -.. automodule:: dashscope.aigc +.. automodule:: dashscope.legacy_1x.aigc -.. currentmodule:: dashscope.aigc +.. currentmodule:: dashscope.legacy_1x.aigc .. autosummary:: :toctree: generated diff --git a/docs/source/api/generated/dashscope.aigc.conversation.Conversation.rst b/docs/source/api/generated/dashscope.aigc.conversation.Conversation.rst index b2b16b2f..b418068b 100644 --- a/docs/source/api/generated/dashscope.aigc.conversation.Conversation.rst +++ b/docs/source/api/generated/dashscope.aigc.conversation.Conversation.rst @@ -1,4 +1,4 @@ -.. currentmodule:: dashscope.aigc.conversation +.. currentmodule:: dashscope.legacy_1x.aigc.conversation Conversation diff --git a/docs/source/api/generated/dashscope.aigc.conversation.History.rst b/docs/source/api/generated/dashscope.aigc.conversation.History.rst index 160dfda2..819bb62e 100644 --- a/docs/source/api/generated/dashscope.aigc.conversation.History.rst +++ b/docs/source/api/generated/dashscope.aigc.conversation.History.rst @@ -1,4 +1,4 @@ -.. currentmodule:: dashscope.aigc.conversation +.. currentmodule:: dashscope.legacy_1x.aigc.conversation History diff --git a/docs/source/api/generated/dashscope.aigc.conversation.HistoryItem.rst b/docs/source/api/generated/dashscope.aigc.conversation.HistoryItem.rst index 50daf956..edfa7fb4 100644 --- a/docs/source/api/generated/dashscope.aigc.conversation.HistoryItem.rst +++ b/docs/source/api/generated/dashscope.aigc.conversation.HistoryItem.rst @@ -1,4 +1,4 @@ -.. currentmodule:: dashscope.aigc.conversation +.. currentmodule:: dashscope.legacy_1x.aigc.conversation HistoryItem diff --git a/docs/source/develop.md b/docs/source/develop.md index ceafeea6..45ddc7c6 100644 --- a/docs/source/develop.md +++ b/docs/source/develop.md @@ -32,3 +32,17 @@ If you only want to format and lint your code, you can run ```bash make linter ``` + +## 2. Tests + +Run the current root implementation, archived compatibility suites, or the +complete suite with: + +```bash +python -m pytest tests --ignore=tests/legacy_1x -q +python -m pytest tests/legacy_1x -q +python -m pytest tests -q +``` + +Current coverage uses `coverage run -m pytest tests +--ignore=tests/legacy_1x`, followed by `coverage report -m`. diff --git a/examples/01_atomic_capabilities/audio/asr_file.py b/examples/01_atomic_capabilities/audio/asr_file.py index ac66cc89..7bdb13ca 100644 --- a/examples/01_atomic_capabilities/audio/asr_file.py +++ b/examples/01_atomic_capabilities/audio/asr_file.py @@ -10,7 +10,7 @@ import httpx from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import positive_seconds diff --git a/examples/01_atomic_capabilities/audio/asr_http_stream.py b/examples/01_atomic_capabilities/audio/asr_http_stream.py index f625ebf0..08b4517d 100644 --- a/examples/01_atomic_capabilities/audio/asr_http_stream.py +++ b/examples/01_atomic_capabilities/audio/asr_http_stream.py @@ -11,7 +11,7 @@ from httpx_sse import EventSource from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import open_wav, positive_seconds from .asr_file import https_url diff --git a/examples/01_atomic_capabilities/audio/direct_call/asr_file.py b/examples/01_atomic_capabilities/audio/direct_call/asr_file.py index 7394aefe..c771b940 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/asr_file.py +++ b/examples/01_atomic_capabilities/audio/direct_call/asr_file.py @@ -10,7 +10,7 @@ import httpx from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import positive_seconds diff --git a/examples/01_atomic_capabilities/audio/direct_call/asr_http_stream.py b/examples/01_atomic_capabilities/audio/direct_call/asr_http_stream.py index 1f5c26d0..16950024 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/asr_http_stream.py +++ b/examples/01_atomic_capabilities/audio/direct_call/asr_http_stream.py @@ -10,7 +10,7 @@ from httpx_sse import EventSource from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import open_wav, positive_seconds from .asr_file import https_url diff --git a/examples/01_atomic_capabilities/audio/direct_call/realtime_omni.py b/examples/01_atomic_capabilities/audio/direct_call/realtime_omni.py index 9651e402..bec52226 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/realtime_omni.py +++ b/examples/01_atomic_capabilities/audio/direct_call/realtime_omni.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py index 97be0ef5..87da80b6 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/direct_call/realtime_translation.py b/examples/01_atomic_capabilities/audio/direct_call/realtime_translation.py index 4db13ed7..2f5a9720 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/realtime_translation.py +++ b/examples/01_atomic_capabilities/audio/direct_call/realtime_translation.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/direct_call/tts_websocket.py b/examples/01_atomic_capabilities/audio/direct_call/tts_websocket.py index aa89a8ea..84d99b8d 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/tts_websocket.py +++ b/examples/01_atomic_capabilities/audio/direct_call/tts_websocket.py @@ -9,7 +9,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( play_wav, player_command, diff --git a/examples/01_atomic_capabilities/audio/direct_call/voice_clone.py b/examples/01_atomic_capabilities/audio/direct_call/voice_clone.py index a979bd95..6e1c80ca 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/voice_clone.py +++ b/examples/01_atomic_capabilities/audio/direct_call/voice_clone.py @@ -13,7 +13,7 @@ import httpx from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import positive_seconds diff --git a/examples/01_atomic_capabilities/audio/realtime_omni.py b/examples/01_atomic_capabilities/audio/realtime_omni.py index 9d7b72e4..e372c6dc 100644 --- a/examples/01_atomic_capabilities/audio/realtime_omni.py +++ b/examples/01_atomic_capabilities/audio/realtime_omni.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py index ff1ad099..9a6df18e 100644 --- a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/realtime_translation.py b/examples/01_atomic_capabilities/audio/realtime_translation.py index 186f9df2..46b016b1 100644 --- a/examples/01_atomic_capabilities/audio/realtime_translation.py +++ b/examples/01_atomic_capabilities/audio/realtime_translation.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( input_device, load_sounddevice, diff --git a/examples/01_atomic_capabilities/audio/tts_websocket.py b/examples/01_atomic_capabilities/audio/tts_websocket.py index b2a82593..49dffa08 100644 --- a/examples/01_atomic_capabilities/audio/tts_websocket.py +++ b/examples/01_atomic_capabilities/audio/tts_websocket.py @@ -10,7 +10,7 @@ from pathlib import Path from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import ( play_wav, player_command, diff --git a/examples/01_atomic_capabilities/audio/voice_clone.py b/examples/01_atomic_capabilities/audio/voice_clone.py index d6ab2b56..f7b2fbd8 100644 --- a/examples/01_atomic_capabilities/audio/voice_clone.py +++ b/examples/01_atomic_capabilities/audio/voice_clone.py @@ -13,7 +13,7 @@ import httpx from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError from ._common import positive_seconds diff --git a/examples/01_atomic_capabilities/decision/decision.py b/examples/01_atomic_capabilities/decision/decision.py index bd8af485..e0c3628a 100644 --- a/examples/01_atomic_capabilities/decision/decision.py +++ b/examples/01_atomic_capabilities/decision/decision.py @@ -34,7 +34,7 @@ print("----- decision request -----") # Question values may also be the typed models ChoiceQuestion / - # NoulQuestion / ScoreQuestion from dashscope.v2.resources.decision; + # NoulQuestion / ScoreQuestion from dashscope.resources.decision; # raw dicts below show the exact wire shape (and pass future fields # through untouched). resp = client.decision.create( diff --git a/examples/02_advanced_engineering/circuit_breaker_retry.py b/examples/02_advanced_engineering/circuit_breaker_retry.py index a4b97612..d76e82ff 100644 --- a/examples/02_advanced_engineering/circuit_breaker_retry.py +++ b/examples/02_advanced_engineering/circuit_breaker_retry.py @@ -7,7 +7,7 @@ 1. WITHIN a region the SDK already retries transient 429 / 5xx with Retry-After-aware exponential backoff -- see ``Client(max_retries=)`` - and ``dashscope/v2/transport/_retry.py``. You do not reimplement it. + and ``dashscope/transport/_retry.py``. You do not reimplement it. 2. ACROSS regions this recipe adds a per-region circuit breaker and fails over to the next healthy region when one keeps erroring, so a whole-region outage does not take your application down. @@ -23,7 +23,7 @@ import httpx import dashscope -from dashscope.v2 import errors +from dashscope import errors # Fail over across these regions, in order. Both have a dedicated # shared gateway, so no workspace is required for the demo. diff --git a/examples/02_advanced_engineering/local_mock_debugging.py b/examples/02_advanced_engineering/local_mock_debugging.py index 99526c93..f64ed379 100644 --- a/examples/02_advanced_engineering/local_mock_debugging.py +++ b/examples/02_advanced_engineering/local_mock_debugging.py @@ -16,7 +16,7 @@ import httpx import dashscope -from dashscope.v2 import errors +from dashscope import errors # A fake gateway URL. MockTransport intercepts every request, so no # traffic ever leaves this process and the host is never resolved. diff --git a/examples/03_agent_recipes/agentstudio.py b/examples/03_agent_recipes/agentstudio.py index 682855c7..68ef206f 100644 --- a/examples/03_agent_recipes/agentstudio.py +++ b/examples/03_agent_recipes/agentstudio.py @@ -8,7 +8,7 @@ AgentStudio is shared by the 1.x and 2.0 SDK generations: the 2.0 client mounts it as ``client.agentstudio`` (workspace and region forward from the client config) and translates its errors into the -``dashscope.v2`` exception family, so one ``client`` covers both the +``dashscope`` exception family, so one ``client`` covers both the 2.0 resources and AgentStudio. `memory_stores_demo` covers the memory store flow (create store -> @@ -22,7 +22,8 @@ import os import dashscope -from dashscope.agentstudio import ModelConfig, user_message +from dashscope.agentstudio import user_message +from dashscope.agentstudio.types import ModelConfig def make_client(): diff --git a/examples/04_real_world_scenarios/README.md b/examples/04_real_world_scenarios/README.md index 2636abe7..45c942fe 100644 --- a/examples/04_real_world_scenarios/README.md +++ b/examples/04_real_world_scenarios/README.md @@ -81,7 +81,7 @@ The demo runs a three-turn dialogue whose second turn — "can this order be ref ### A real pit you will step into without reading the source -> **Streaming requests do not go through the SDK's auto-retry.** `open_stream` in `dashscope/v2/transport/http.py` checks the initial status once and returns (source comment: "only the initial status is checked here, no retries"), while the plain `request` runs the `max_retries` backoff loop. So a streaming RAG that does not handle 429/503 itself throws the exception straight at the user. The harness's `retry_with_backoff` exists precisely for this gap. +> **Streaming requests do not go through the SDK's auto-retry.** `open_stream` in `dashscope/transport/http.py` checks the initial status once and returns (source comment: "only the initial status is checked here, no retries"), while the plain `request` runs the `max_retries` backoff loop. So a streaming RAG that does not handle 429/503 itself throws the exception straight at the user. The harness's `retry_with_backoff` exists precisely for this gap. Likewise, a stream is a **one-shot iterator**: consuming `text_stream` and then calling `get_final_message()` yields an empty tail, while the usage counters live in the **last chunk**, not the first. `_consume_stream` therefore iterates exactly once, printing the deltas and folding in usage at the same time. @@ -113,7 +113,7 @@ The offline demo runs in order: ## Case: high-concurrency realtime meeting transcription base -`realtime_audio_meeting_harness.py` rebuilds the legacy TingWu realtime script (`legacy_1x/tingwu_realtime.py`, a test-flavored style of callbacks + `time.sleep` polling) into a **meeting-platform-grade concurrency supervisor**: transcribe N meeting rooms in realtime at the same time, each room with its own state machine and transcript, and no single room's disconnect or moderation block can drag the others down. It also runs offline by default — a scripted duplex socket is injected through TingWu realtime's `connector=` (the same seam used by `tests/v2/test_tingwu.py`), so without a microphone or a key you can watch concurrency, reconnects and degradation actually happen. +`realtime_audio_meeting_harness.py` rebuilds the legacy TingWu realtime script (`legacy_1x/tingwu_realtime.py`, a test-flavored style of callbacks + `time.sleep` polling) into a **meeting-platform-grade concurrency supervisor**: transcribe N meeting rooms in realtime at the same time, each room with its own state machine and transcript, and no single room's disconnect or moderation block can drag the others down. It also runs offline by default — a scripted duplex socket is injected through TingWu realtime's `connector=` (the same seam used by `tests/test_tingwu.py`), so without a microphone or a key you can watch concurrency, reconnects and degradation actually happen. ### The four production dimensions @@ -126,7 +126,7 @@ The offline demo runs in order: ### A real pit you will step into without reading the source -> **The realtime family cannot be multiplexed.** The header comment of `dashscope/v2/transport/ws_connection.py` states it: the realtime protocol has no task id — one socket is one session, closed when the session ends — so it always opens a dedicated connection per session. (The multiplexable `WsConnection` only serves the JSON-framed asr/tts/translation tasks, and binary uplink is single-owner.) Therefore the correct shape of "high-concurrency realtime audio" is **one dedicated connection per meeting plus a supervising thread pool**, not stuffing multiple audio streams into one socket — the protocol simply cannot do that. The harness's `MeetingSupervisor` uses a bounded thread pool to tie "concurrency" to "active sockets / blast radius" as admission control. +> **The realtime family cannot be multiplexed.** The header comment of `dashscope/transport/ws_connection.py` states it: the realtime protocol has no task id — one socket is one session, closed when the session ends — so it always opens a dedicated connection per session. (The multiplexable `WsConnection` only serves the JSON-framed asr/tts/translation tasks, and binary uplink is single-owner.) Therefore the correct shape of "high-concurrency realtime audio" is **one dedicated connection per meeting plus a supervising thread pool**, not stuffing multiple audio streams into one socket — the protocol simply cannot do that. The harness's `MeetingSupervisor` uses a bounded thread pool to tie "concurrency" to "active sockets / blast radius" as admission control. Likewise, when a socket drops mid-stream, `WsSession.recv` raises `APIConnectionError` ("closed before task-finished"), while a `task-failed` event is mapped through the error catalog into a business exception with `status_code=200` (e.g. `ContentFilterError`). `_is_reconnectable` distinguishes exactly on this: the former reconnects, the latter degrades — the offline demo's `room-gamma` (after a drop-and-reconnect, the half sentence `今天我们讨论` is completed into `今天我们讨论第三季度的路线图。`) and `room-delta` (a content-moderation `task-failed` that degrades immediately) demonstrate these two paths respectively. @@ -166,11 +166,11 @@ The offline demo runs 4 rooms concurrently, reports `completed` / `degraded` sep ### A real pit you will step into without reading the source -> **DashScope can report a failure with HTTP 200.** `ResponseBase` in `dashscope/v2/types/common.py` carries `code` / `message` / `is_error` on every response, and with `raise_on_error=False` the SDK fills that envelope instead of raising. A triage loop that only wraps `try/except` will treat a failed call as a successful one with empty answers — and auto-route the ticket on nothing. The harness therefore checks `response.is_error` even in the default raising mode: belt and suspenders, because the day someone flips `raise_on_error=False` the guard must already be in place. +> **DashScope can report a failure with HTTP 200.** `ResponseBase` in `dashscope/types/common.py` carries `code` / `message` / `is_error` on every response, and with `raise_on_error=False` the SDK fills that envelope instead of raising. A triage loop that only wraps `try/except` will treat a failed call as a successful one with empty answers — and auto-route the ticket on nothing. The harness therefore checks `response.is_error` even in the default raising mode: belt and suspenders, because the day someone flips `raise_on_error=False` the guard must already be in place. Likewise, **`score` answers are probability-weighted level indices, not integer classes** — `2.3` means "between level 2 and 3, leaning 2", and `legend` maps an index back to your level text. Cut lines like `P1_SCORE = 2.5` are therefore thresholds on a continuous axis, which is exactly what severity bands want. -And a routing note: the System One endpoint lives under the **compatible-mode path on every gateway** (derived by `compatible_mode_base_url` in `dashscope/v2/_config.py`), so changing the client's chat `protocol` does not move it. +And a routing note: the System One endpoint lives under the **compatible-mode path on every gateway** (derived by `compatible_mode_base_url` in `dashscope/_config.py`), so changing the client's chat `protocol` does not move it. ### Run diff --git a/examples/04_real_world_scenarios/README_zh.md b/examples/04_real_world_scenarios/README_zh.md index 59c5dedc..f6ccc078 100644 --- a/examples/04_real_world_scenarios/README_zh.md +++ b/examples/04_real_world_scenarios/README_zh.md @@ -81,7 +81,7 @@ python3 examples/04_real_world_scenarios/customer_service_assistant.py ### 不读源码一定会踩的坑 -> **流式请求不走 SDK 的自动重试。** `dashscope/v2/transport/http.py` 里的 `open_stream` 只检查一次初始状态就返回(源码注释:"only the initial status is checked here, no retries"),而普通 `request` 会跑 `max_retries` 退避循环。所以一个不自己处理 429/503 的流式 RAG,会把异常直接甩到用户脸上。harness 的 `retry_with_backoff` 正是为这个缺口而存在。 +> **流式请求不走 SDK 的自动重试。** `dashscope/transport/http.py` 里的 `open_stream` 只检查一次初始状态就返回(源码注释:"only the initial status is checked here, no retries"),而普通 `request` 会跑 `max_retries` 退避循环。所以一个不自己处理 429/503 的流式 RAG,会把异常直接甩到用户脸上。harness 的 `retry_with_backoff` 正是为这个缺口而存在。 同样,流是**一次性迭代器**:先消费 `text_stream` 再调 `get_final_message()` 只能拿到空尾巴,而 usage 计数在**最后一个 chunk** 而不是第一个。因此 `_consume_stream` 恰好迭代一次,边打印增量边把 usage 折叠进来。 @@ -113,7 +113,7 @@ RAG_HARNESS_LIVE=1 python3 examples/04_real_world_scenarios/production_ready_rag ## 案例:高并发实时会议转写底座 -`realtime_audio_meeting_harness.py` 把 legacy 听悟 realtime 脚本(`legacy_1x/tingwu_realtime.py`,回调 + `time.sleep` 轮询的测试风格)重建成**会议平台级并发监管器**:同时实时转写 N 个会议室,每个房间有自己的状态机和转写稿,任何一个房间的断线或审核拦截都不能拖垮其他房间。它同样默认离线运行——通过听悟 realtime 的 `connector=` 注入一条脚本化双工 socket(与 `tests/v2/test_tingwu.py` 用的是同一个接缝),没有麦克风、没有密钥也能看到并发、重连和降级真实发生。 +`realtime_audio_meeting_harness.py` 把 legacy 听悟 realtime 脚本(`legacy_1x/tingwu_realtime.py`,回调 + `time.sleep` 轮询的测试风格)重建成**会议平台级并发监管器**:同时实时转写 N 个会议室,每个房间有自己的状态机和转写稿,任何一个房间的断线或审核拦截都不能拖垮其他房间。它同样默认离线运行——通过听悟 realtime 的 `connector=` 注入一条脚本化双工 socket(与 `tests/test_tingwu.py` 用的是同一个接缝),没有麦克风、没有密钥也能看到并发、重连和降级真实发生。 ### 四个生产维度 @@ -126,7 +126,7 @@ RAG_HARNESS_LIVE=1 python3 examples/04_real_world_scenarios/production_ready_rag ### 不读源码一定会踩的坑 -> **realtime 家族无法复用连接。** `dashscope/v2/transport/ws_connection.py` 的头注释写明了:realtime 协议没有 task id——一条 socket 就是一个会话,会话结束即关闭——所以它总是为每个会话开一条独立连接。(可复用的 `WsConnection` 只服务 JSON 帧的 asr/tts/translation 任务,且二进制上行是单属主的。)因此"高并发实时音频"的正确形态是**每场会议一条独立连接 + 一个监管线程池**,而不是把多路音频塞进一条 socket——协议做不到。harness 的 `MeetingSupervisor` 用有界线程池把"并发"绑定到"活跃 socket / 爆炸半径"上,作为准入控制。 +> **realtime 家族无法复用连接。** `dashscope/transport/ws_connection.py` 的头注释写明了:realtime 协议没有 task id——一条 socket 就是一个会话,会话结束即关闭——所以它总是为每个会话开一条独立连接。(可复用的 `WsConnection` 只服务 JSON 帧的 asr/tts/translation 任务,且二进制上行是单属主的。)因此"高并发实时音频"的正确形态是**每场会议一条独立连接 + 一个监管线程池**,而不是把多路音频塞进一条 socket——协议做不到。harness 的 `MeetingSupervisor` 用有界线程池把"并发"绑定到"活跃 socket / 爆炸半径"上,作为准入控制。 同样,当 socket 在中途断开时,`WsSession.recv` 抛 `APIConnectionError`("closed before task-finished"),而 `task-failed` 事件会经错误目录映射成 `status_code=200` 的业务异常(如 `ContentFilterError`)。`_is_reconnectable` 正是据此区分:前者重连,后者降级——离线演示里的 `room-gamma`(断线重连后,半句 `今天我们讨论` 被补全为 `今天我们讨论第三季度的路线图。`)和 `room-delta`(内容审核 `task-failed` 立即降级)分别演示了这两条路径。 @@ -166,11 +166,11 @@ MEETING_HARNESS_LIVE=1 python3 examples/04_real_world_scenarios/realtime_audio_m ### 不读源码一定会踩的坑 -> **DashScope 可能用 HTTP 200 报告失败。** `dashscope/v2/types/common.py` 的 `ResponseBase` 在每个响应上都带 `code` / `message` / `is_error`,而 `raise_on_error=False` 时 SDK 会填充这个信封而不是抛异常。一个只包 `try/except` 的分诊循环会把失败的调用当成答案为空的成功调用——然后基于"什么都没有"自动路由工单。因此 harness 即使在默认 raising 模式下也检查 `response.is_error`:这是双保险,因为当某天有人把 `raise_on_error=False` 打开时,这道防线必须已经就位。 +> **DashScope 可能用 HTTP 200 报告失败。** `dashscope/types/common.py` 的 `ResponseBase` 在每个响应上都带 `code` / `message` / `is_error`,而 `raise_on_error=False` 时 SDK 会填充这个信封而不是抛异常。一个只包 `try/except` 的分诊循环会把失败的调用当成答案为空的成功调用——然后基于"什么都没有"自动路由工单。因此 harness 即使在默认 raising 模式下也检查 `response.is_error`:这是双保险,因为当某天有人把 `raise_on_error=False` 打开时,这道防线必须已经就位。 同样,**`score` 答案是概率加权等级索引,不是整数类别**——`2.3` 表示"介于等级 2 和 3 之间,偏向 2",`legend` 把索引映射回你给的等级文本。像 `P1_SCORE = 2.5` 这样的分数线因此是连续轴上的阈值,这正是严重度分档想要的东西。 -还有一条路由说明:System One 端点在**每个网关的 compatible-mode 路径下**(由 `dashscope/v2/_config.py` 的 `compatible_mode_base_url` 推导),所以改变客户端的聊天 `protocol` 不会移动它。 +还有一条路由说明:System One 端点在**每个网关的 compatible-mode 路径下**(由 `dashscope/_config.py` 的 `compatible_mode_base_url` 推导),所以改变客户端的聊天 `protocol` 不会移动它。 ### 运行 diff --git a/examples/04_real_world_scenarios/production_ready_rag_harness.py b/examples/04_real_world_scenarios/production_ready_rag_harness.py index 5e3a0878..34b8751b 100644 --- a/examples/04_real_world_scenarios/production_ready_rag_harness.py +++ b/examples/04_real_world_scenarios/production_ready_rag_harness.py @@ -15,7 +15,7 @@ 3. **Rigid resilience** -- the SDK retries transient 429/5xx *within* a region for normal calls (``Client(max_retries=)``), but a STREAMING call is NOT retried by the SDK (``transport.open_stream`` checks the - status once and returns -- see dashscope/v2/transport/http.py). So the + status once and returns -- see dashscope/transport/http.py). So the harness owns exponential backoff for the streaming generate step, and layers a per-region circuit breaker + cross-region failover on top. 4. **Graceful degradation** -- 429 / content-filter / total-outage are @@ -39,7 +39,7 @@ import httpx import dashscope -from dashscope.v2 import errors +from dashscope import errors # ---- configuration (override via env) ---- # EMBED_MODEL = os.environ.get("DASHSCOPE_EMBED_MODEL", "text-embedding-v4") diff --git a/examples/04_real_world_scenarios/realtime_audio_meeting_harness.py b/examples/04_real_world_scenarios/realtime_audio_meeting_harness.py index 13ce5c36..56896024 100644 --- a/examples/04_real_world_scenarios/realtime_audio_meeting_harness.py +++ b/examples/04_real_world_scenarios/realtime_audio_meeting_harness.py @@ -12,7 +12,7 @@ 1. **Concurrency that respects the protocol.** Realtime TingWu is NOT multiplexable: its wire protocol carries no task id, so one websocket is exactly one session and closes when the session ends (see the - module note in dashscope/v2/transport/ws_connection.py). Concurrency + module note in dashscope/transport/ws_connection.py). Concurrency therefore means one *dedicated* connection + worker per meeting, and the pool size is the admission control that caps how many live sockets -- and how much blast radius -- you hold open at once. @@ -33,7 +33,7 @@ It runs **fully offline and deterministically by default** by injecting a scripted duplex socket through TingWu's ``connector=`` seam (the same -seam tests/v2/test_tingwu.py uses), so you can *see* the concurrency, +seam tests/test_tingwu.py uses), so you can *see* the concurrency, reconnect and degradation without a microphone or a token. Set ``MEETING_HARNESS_LIVE=1`` to point the exact same engine at the real gateway with ``DASHSCOPE_API_KEY`` + ``DASHSCOPE_TINGWU_APP_ID``. @@ -45,7 +45,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed import dashscope -from dashscope.v2 import errors +from dashscope import errors # ---- configuration (override via env) ---- # MODEL = os.environ.get("DASHSCOPE_TINGWU_MODEL", "tingwu-turbo") @@ -307,7 +307,7 @@ def _recognize(text, sentence_id, end): class FakeDuplexSocket: """A scripted duplex websocket that replays a canned event inbox. - Mirrors tests/v2/test_tingwu.py's FakeWebSocket. ``recv`` returns the + Mirrors tests/test_tingwu.py's FakeWebSocket. ``recv`` returns the next JSON frame, or ``None`` once the inbox is exhausted -- which the SDK maps to ``APIConnectionError`` ("closed before task-finished"), so an inbox that stops short of ``speech-end`` simulates a mid-stream diff --git a/examples/04_real_world_scenarios/ticket_triage_harness.py b/examples/04_real_world_scenarios/ticket_triage_harness.py index 38ba4cc7..b74afbd9 100644 --- a/examples/04_real_world_scenarios/ticket_triage_harness.py +++ b/examples/04_real_world_scenarios/ticket_triage_harness.py @@ -36,7 +36,7 @@ import os import dashscope -from dashscope.v2 import errors +from dashscope import errors # ---- configuration (override via env) ---- # DECISION_MODEL = os.environ.get("DASHSCOPE_MODEL", "decision-model-preview") @@ -60,7 +60,7 @@ # One decision round asks every question at once; ``answers`` come back # keyed by these same question ids. Raw dicts show the wire shape; the # typed ChoiceQuestion / NoulQuestion / ScoreQuestion models in -# dashscope.v2.resources.decision add local validation if wanted. +# dashscope.resources.decision add local validation if wanted. QUESTIONS = { "department": { "type": "choice", diff --git a/examples/README.md b/examples/README.md index 751647f3..e10f90e7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,7 +2,7 @@ > **English** | [中文](README_zh.md) -This is not an "API call reference". In the age of AI-assisted development, anyone can generate a one-line `call(model="xxx")` in seconds — the value of this directory is that it **writes those few hundred lines of "grubby code" needed to push a Model Studio application to production, correctly, in advance**: every advanced file is a miniature production-grade harness blueprint with rigid engineering defenses, targeting a specific development or business scenario. Everything is built on the 2.0 resource-style client `dashscope.Client()` (consistent with `dashscope.v2` inside the package); legacy 1.x examples are preserved intact under `legacy_1x/`. +This is not an "API call reference". In the age of AI-assisted development, anyone can generate a one-line `call(model="xxx")` in seconds — the value of this directory is that it **writes those few hundred lines of "grubby code" needed to push a Model Studio application to production, correctly, in advance**: every advanced file is a miniature production-grade harness blueprint with rigid engineering defenses, targeting a specific development or business scenario. Everything is built on the 2.0 resource-style client `dashscope.Client()` (consistent with `dashscope` inside the package); legacy 1.x examples are preserved intact under `legacy_1x/`. A qualified best-practice case must demonstrate three dimensions at once: diff --git a/examples/README_zh.md b/examples/README_zh.md index 37d00573..63d736bc 100644 --- a/examples/README_zh.md +++ b/examples/README_zh.md @@ -2,7 +2,7 @@ > [English](README.md) | **中文** -这不是一份"API 调用参考"。在 AI 辅助开发的时代,任何人都能几秒钟生成一行 `call(model="xxx")`——本目录的价值在于**提前、正确地把一个百炼应用推向生产所需的那几百行"脏活代码"写好**:每个进阶文件都是一个微型的生产级 harness 蓝图,带有严格的工程防御,面向一个具体的开发或业务场景。全部示例基于 2.0 资源风格客户端 `dashscope.Client()`(与包内 `dashscope.v2` 一致);旧版 1.x 示例完整归档在 `legacy_1x/` 下。 +这不是一份"API 调用参考"。在 AI 辅助开发的时代,任何人都能几秒钟生成一行 `call(model="xxx")`——本目录的价值在于**提前、正确地把一个百炼应用推向生产所需的那几百行"脏活代码"写好**:每个进阶文件都是一个微型的生产级 harness 蓝图,带有严格的工程防御,面向一个具体的开发或业务场景。全部示例基于 2.0 资源风格客户端 `dashscope.Client()`(与包内 `dashscope` 一致);旧版 1.x 示例完整归档在 `legacy_1x/` 下。 一个合格的最佳实践案例必须同时展现三个维度: diff --git a/requirements-test.txt b/requirements-test.txt index e034ab94..3e725519 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -9,7 +9,7 @@ coverage pydantic>=2.0,<3.0 tenacity typing_extensions -# Optional extras exercised by tests/unit (cli, acli) +# Optional extras exercised by tests/legacy_1x/unit (cli, acli) typer>=0.9.0 rich>=13.0.0 prompt-toolkit>=3.0 diff --git a/setup.py b/setup.py index 12345c1b..1cf5da24 100644 --- a/setup.py +++ b/setup.py @@ -65,8 +65,8 @@ def run(self): # Hoisted to a module-level name (like ``name``/``url`` above) so the -# static packaging audit in tests/unit/test_packaging_metadata.py keeps -# seeing only literal-evaluable setup() kwargs. +# static packaging audit in tests/legacy_1x/unit/test_packaging_metadata.py +# keeps seeing only literal-evaluable setup() kwargs. cmdclass = {"build_py": build_py} @@ -126,5 +126,7 @@ def run(self): }, zip_safe=False, cmdclass=cmdclass, - entry_points={"console_scripts": ["dashscope = dashscope.cli:main"]}, + entry_points={ + "console_scripts": ["dashscope = dashscope.legacy_1x.cli:main"], + }, ) diff --git a/specs/chat.yaml b/specs/chat.yaml index dbc9776e..fef65b1c 100644 --- a/specs/chat.yaml +++ b/specs/chat.yaml @@ -235,7 +235,7 @@ components: $ref: "#/components/schemas/Usage" x-python: class: ChatCompletion - module: dashscope.v2.types.completions + module: dashscope.types.completions convenience: output_text: output.choices[0].message.content @@ -283,7 +283,7 @@ components: $ref: "#/components/schemas/Usage" x-python: class: ChatCompletionChunk - module: dashscope.v2.types.completions + module: dashscope.types.completions ErrorResponse: type: object @@ -311,4 +311,4 @@ components: request_id: type: string x-python: - maps_to: dashscope.v2.errors.ErrorCode + maps_to: dashscope.errors.ErrorCode diff --git a/tests/legacy_1x/__init__.py b/tests/legacy_1x/__init__.py new file mode 100644 index 00000000..e28d78e7 --- /dev/null +++ b/tests/legacy_1x/__init__.py @@ -0,0 +1 @@ +"""Transitional legacy test suites.""" diff --git a/tests/integration/test_large_utf8_payload.py b/tests/legacy_1x/integration/test_large_utf8_payload.py similarity index 97% rename from tests/integration/test_large_utf8_payload.py rename to tests/legacy_1x/integration/test_large_utf8_payload.py index ef84ae99..5da6bfe8 100644 --- a/tests/integration/test_large_utf8_payload.py +++ b/tests/legacy_1x/integration/test_large_utf8_payload.py @@ -3,7 +3,7 @@ Usage: export DASHSCOPE_API_KEY="sk-xxx" - python3 tests/integration/test_websearch_utf8.py + python3 tests/legacy_1x/integration/test_large_utf8_payload.py """ import asyncio @@ -16,7 +16,7 @@ from dashscope.legacy_1x.aigc.generation import Generation, AioGeneration WEBSEARCH_JSON = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "websearch.json", ) diff --git a/tests/legacy/conftest.py b/tests/legacy_1x/legacy/conftest.py similarity index 95% rename from tests/legacy/conftest.py rename to tests/legacy_1x/legacy/conftest.py index b2e886ed..675bf3a8 100644 --- a/tests/legacy/conftest.py +++ b/tests/legacy_1x/legacy/conftest.py @@ -8,7 +8,7 @@ import dashscope from dashscope.legacy_1x.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV -from tests.unit.mock_server import create_mock_server, run_server +from tests.legacy_1x.unit.mock_server import create_mock_server, run_server @pytest.fixture(scope="session", autouse=True) diff --git a/tests/legacy/test_async_api.py b/tests/legacy_1x/legacy/test_async_api.py similarity index 98% rename from tests/legacy/test_async_api.py rename to tests/legacy_1x/legacy/test_async_api.py index 4729f220..848bc237 100644 --- a/tests/legacy/test_async_api.py +++ b/tests/legacy_1x/legacy/test_async_api.py @@ -9,7 +9,7 @@ from dashscope.legacy_1x.api_entities.dashscope_response import DashScopeAPIResponse from dashscope.legacy_1x.client.base_api import BaseAsyncApi from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod, TaskStatus -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment class AsyncRequest(BaseAsyncApi): diff --git a/tests/legacy/test_encryption.py b/tests/legacy_1x/legacy/test_encryption.py similarity index 100% rename from tests/legacy/test_encryption.py rename to tests/legacy_1x/legacy/test_encryption.py diff --git a/tests/legacy/test_image_synthesis.py b/tests/legacy_1x/legacy/test_image_synthesis.py similarity index 98% rename from tests/legacy/test_image_synthesis.py rename to tests/legacy_1x/legacy/test_image_synthesis.py index ff39b3ba..8a9c0d0b 100644 --- a/tests/legacy/test_image_synthesis.py +++ b/tests/legacy_1x/legacy/test_image_synthesis.py @@ -7,7 +7,7 @@ from dashscope import ImageSynthesis from dashscope.legacy_1x.common.constants import TaskStatus -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment @pytest.mark.skip(reason="Legacy integration test, requires real DashScope API") diff --git a/tests/legacy/test_speech_recognizer.py b/tests/legacy_1x/legacy/test_speech_recognizer.py similarity index 99% rename from tests/legacy/test_speech_recognizer.py rename to tests/legacy_1x/legacy/test_speech_recognizer.py index 90d89c20..cdebb815 100644 --- a/tests/legacy/test_speech_recognizer.py +++ b/tests/legacy_1x/legacy/test_speech_recognizer.py @@ -16,7 +16,7 @@ RecognitionCallback, RecognitionResult, ) -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment logger = logging.getLogger("dashscope") logger.setLevel(logging.DEBUG) diff --git a/tests/legacy/test_speech_synthesis.py b/tests/legacy_1x/legacy/test_speech_synthesis.py similarity index 99% rename from tests/legacy/test_speech_synthesis.py rename to tests/legacy_1x/legacy/test_speech_synthesis.py index 28e51fbb..a95b917b 100644 --- a/tests/legacy/test_speech_synthesis.py +++ b/tests/legacy_1x/legacy/test_speech_synthesis.py @@ -16,7 +16,7 @@ SpeechSynthesisResult, SpeechSynthesizer, ) -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment logger = logging.getLogger("dashscope") logger.setLevel(logging.DEBUG) diff --git a/tests/legacy/test_speech_transcription.py b/tests/legacy_1x/legacy/test_speech_transcription.py similarity index 99% rename from tests/legacy/test_speech_transcription.py rename to tests/legacy_1x/legacy/test_speech_transcription.py index 293cc610..d083b378 100644 --- a/tests/legacy/test_speech_transcription.py +++ b/tests/legacy_1x/legacy/test_speech_transcription.py @@ -12,7 +12,7 @@ import dashscope from dashscope.legacy_1x.audio.asr import Transcription from dashscope.legacy_1x.common.constants import TaskStatus -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment HTTPS_16K_CH1_WAV = "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example.wav" # noqa: * HTTPS_16K_CH2_WAV = "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_2ch.wav" # noqa: * diff --git a/tests/legacy/test_video_synthesis.py b/tests/legacy_1x/legacy/test_video_synthesis.py similarity index 97% rename from tests/legacy/test_video_synthesis.py rename to tests/legacy_1x/legacy/test_video_synthesis.py index 678e2097..cfdec0ca 100644 --- a/tests/legacy/test_video_synthesis.py +++ b/tests/legacy_1x/legacy/test_video_synthesis.py @@ -7,7 +7,7 @@ from dashscope import VideoSynthesis from dashscope.legacy_1x.common.constants import TaskStatus -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment @pytest.mark.skip(reason="Legacy integration test, requires real DashScope API") diff --git a/tests/legacy/test_websocket_async_api.py b/tests/legacy_1x/legacy/test_websocket_async_api.py similarity index 98% rename from tests/legacy/test_websocket_async_api.py rename to tests/legacy_1x/legacy/test_websocket_async_api.py index 4015481a..ad32cdc8 100644 --- a/tests/legacy/test_websocket_async_api.py +++ b/tests/legacy_1x/legacy/test_websocket_async_api.py @@ -5,9 +5,9 @@ import dashscope from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode -from tests.unit.base_test import BaseTestEnvironment -from tests.unit.constants import TestTasks -from tests.unit.websocket_task_request import WebSocketRequest +from tests.legacy_1x.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.constants import TestTasks +from tests.legacy_1x.unit.websocket_task_request import WebSocketRequest # set mock server url. base_websocket_api_url = "ws://localhost:8080/ws/aigc/v1" diff --git a/tests/legacy/test_websocket_parameters.py b/tests/legacy_1x/legacy/test_websocket_parameters.py similarity index 94% rename from tests/legacy/test_websocket_parameters.py rename to tests/legacy_1x/legacy/test_websocket_parameters.py index 4e240b00..b5381ab5 100644 --- a/tests/legacy/test_websocket_parameters.py +++ b/tests/legacy_1x/legacy/test_websocket_parameters.py @@ -3,13 +3,13 @@ import dashscope from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode -from tests.unit.base_test import BaseTestEnvironment -from tests.unit.constants import ( +from tests.legacy_1x.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, TEST_ENABLE_DATA_INSPECTION_REQUEST_ID, TestTasks, ) -from tests.unit.websocket_task_request import WebSocketRequest +from tests.legacy_1x.unit.websocket_task_request import WebSocketRequest def pytest_generate_tests(metafunc): diff --git a/tests/legacy/test_websocket_sync_api.py b/tests/legacy_1x/legacy/test_websocket_sync_api.py similarity index 98% rename from tests/legacy/test_websocket_sync_api.py rename to tests/legacy_1x/legacy/test_websocket_sync_api.py index c61cdfac..a94703ed 100644 --- a/tests/legacy/test_websocket_sync_api.py +++ b/tests/legacy_1x/legacy/test_websocket_sync_api.py @@ -3,9 +3,9 @@ import dashscope from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode -from tests.unit.base_test import BaseTestEnvironment -from tests.unit.constants import TestTasks -from tests.unit.websocket_task_request import WebSocketRequest +from tests.legacy_1x.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.constants import TestTasks +from tests.legacy_1x.unit.websocket_task_request import WebSocketRequest def pytest_generate_tests(metafunc): diff --git a/dashscope/v2/finetune/reinforcement/examples/observability/__init__.py b/tests/legacy_1x/unit/__init__.py similarity index 100% rename from dashscope/v2/finetune/reinforcement/examples/observability/__init__.py rename to tests/legacy_1x/unit/__init__.py diff --git a/tests/unit/base_test.py b/tests/legacy_1x/unit/base_test.py similarity index 100% rename from tests/unit/base_test.py rename to tests/legacy_1x/unit/base_test.py diff --git a/tests/unit/conftest.py b/tests/legacy_1x/unit/conftest.py similarity index 96% rename from tests/unit/conftest.py rename to tests/legacy_1x/unit/conftest.py index fdcb6efc..644dc3be 100644 --- a/tests/unit/conftest.py +++ b/tests/legacy_1x/unit/conftest.py @@ -9,7 +9,7 @@ from dashscope.legacy_1x.common.constants import ( DASHSCOPE_DISABLE_DATA_INSPECTION_ENV, ) -from tests.unit.mock_server import create_mock_server, run_server +from tests.legacy_1x.unit.mock_server import create_mock_server, run_server @pytest.fixture(autouse=True) diff --git a/tests/unit/constants.py b/tests/legacy_1x/unit/constants.py similarity index 100% rename from tests/unit/constants.py rename to tests/legacy_1x/unit/constants.py diff --git a/tests/unit/handle_deployment_request.py b/tests/legacy_1x/unit/handle_deployment_request.py similarity index 98% rename from tests/unit/handle_deployment_request.py rename to tests/legacy_1x/unit/handle_deployment_request.py index 1930d02c..e3c4adbf 100644 --- a/tests/unit/handle_deployment_request.py +++ b/tests/legacy_1x/unit/handle_deployment_request.py @@ -6,7 +6,7 @@ import aiohttp from aiohttp import web -from tests.unit.constants import TEST_JOB_ID +from tests.legacy_1x.unit.constants import TEST_JOB_ID async def create_deployment_handler(request: aiohttp.request): diff --git a/tests/unit/handle_fine_tune_request.py b/tests/legacy_1x/unit/handle_fine_tune_request.py similarity index 98% rename from tests/unit/handle_fine_tune_request.py rename to tests/legacy_1x/unit/handle_fine_tune_request.py index c9bf07a1..404678a5 100644 --- a/tests/unit/handle_fine_tune_request.py +++ b/tests/legacy_1x/unit/handle_fine_tune_request.py @@ -7,7 +7,7 @@ import aiohttp from aiohttp import web -from tests.unit.mock_sse import sse_response +from tests.legacy_1x.unit.mock_sse import sse_response from .constants import TEST_JOB_ID # pylint: disable=relative-beyond-top-level diff --git a/tests/unit/helper.py b/tests/legacy_1x/unit/helper.py similarity index 100% rename from tests/unit/helper.py rename to tests/legacy_1x/unit/helper.py diff --git a/tests/unit/http_task_request.py b/tests/legacy_1x/unit/http_task_request.py similarity index 100% rename from tests/unit/http_task_request.py rename to tests/legacy_1x/unit/http_task_request.py diff --git a/tests/unit/mock_request_base.py b/tests/legacy_1x/unit/mock_request_base.py similarity index 92% rename from tests/unit/mock_request_base.py rename to tests/legacy_1x/unit/mock_request_base.py index c5c016b0..57d441ae 100644 --- a/tests/unit/mock_request_base.py +++ b/tests/legacy_1x/unit/mock_request_base.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import dashscope -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment class MockRequestBase(BaseTestEnvironment): diff --git a/tests/unit/mock_server.py b/tests/legacy_1x/unit/mock_server.py similarity index 99% rename from tests/unit/mock_server.py rename to tests/legacy_1x/unit/mock_server.py index cd2e578f..c6b91104 100644 --- a/tests/unit/mock_server.py +++ b/tests/legacy_1x/unit/mock_server.py @@ -12,19 +12,19 @@ from aiohttp import web from dashscope.legacy_1x.protocol.websocket import ActionType -from tests.unit.constants import ( +from tests.legacy_1x.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, TEST_ENABLE_DATA_INSPECTION_REQUEST_ID, TEST_JOB_ID, ) -from tests.unit.handle_deployment_request import ( +from tests.legacy_1x.unit.handle_deployment_request import ( create_deployment_handler, delete_deployment_handler, events_deployment_handler, get_deployment_handler, list_deployment_handler, ) -from tests.unit.handle_fine_tune_request import ( +from tests.legacy_1x.unit.handle_fine_tune_request import ( cancel_fine_tune_handler, create_fine_tune_handler, delete_fine_tune_handler, @@ -32,8 +32,8 @@ get_fine_tune_handler, list_fine_tune_handler, ) -from tests.unit.mock_sse import sse_response -from tests.unit.websocket_mock_server_task_handler import ( +from tests.legacy_1x.unit.mock_sse import sse_response +from tests.legacy_1x.unit.websocket_mock_server_task_handler import ( WebSocketTaskProcessor, ) diff --git a/tests/unit/mock_sse.py b/tests/legacy_1x/unit/mock_sse.py similarity index 100% rename from tests/unit/mock_sse.py rename to tests/legacy_1x/unit/mock_sse.py diff --git a/tests/unit/test_acli_acceptance.py b/tests/legacy_1x/unit/test_acli_acceptance.py similarity index 100% rename from tests/unit/test_acli_acceptance.py rename to tests/legacy_1x/unit/test_acli_acceptance.py diff --git a/tests/unit/test_acli_command_timeout.py b/tests/legacy_1x/unit/test_acli_command_timeout.py similarity index 100% rename from tests/unit/test_acli_command_timeout.py rename to tests/legacy_1x/unit/test_acli_command_timeout.py diff --git a/tests/unit/test_acli_command_trust.py b/tests/legacy_1x/unit/test_acli_command_trust.py similarity index 100% rename from tests/unit/test_acli_command_trust.py rename to tests/legacy_1x/unit/test_acli_command_trust.py diff --git a/tests/unit/test_acli_provider_retries.py b/tests/legacy_1x/unit/test_acli_provider_retries.py similarity index 100% rename from tests/unit/test_acli_provider_retries.py rename to tests/legacy_1x/unit/test_acli_provider_retries.py diff --git a/tests/unit/test_acli_reflection.py b/tests/legacy_1x/unit/test_acli_reflection.py similarity index 100% rename from tests/unit/test_acli_reflection.py rename to tests/legacy_1x/unit/test_acli_reflection.py diff --git a/tests/unit/test_acli_sdk_contract.py b/tests/legacy_1x/unit/test_acli_sdk_contract.py similarity index 92% rename from tests/unit/test_acli_sdk_contract.py rename to tests/legacy_1x/unit/test_acli_sdk_contract.py index d8a32936..7a44f428 100644 --- a/tests/unit/test_acli_sdk_contract.py +++ b/tests/legacy_1x/unit/test_acli_sdk_contract.py @@ -23,7 +23,7 @@ reporting True and defers the failure to the end user. acli's deliberately separate session id is covered by -``tests/unit/test_sdk_headers.py``; it is not repeated here. +``tests/legacy_1x/unit/test_sdk_headers.py``; it is not repeated here. """ from __future__ import annotations @@ -36,9 +36,18 @@ import dashscope -_REPO = pathlib.Path(__file__).resolve().parents[2] +_REPO = pathlib.Path(__file__).resolve().parents[3] _ACLI = _REPO / "dashscope" / "acli" -_V2 = _REPO / "dashscope" / "v2" +_CURRENT = ( + _REPO / "dashscope" / "client.py", + _REPO / "dashscope" / "errors.py", + _REPO / "dashscope" / "protocols", + _REPO / "dashscope" / "resources", + _REPO / "dashscope" / "transport", + _REPO / "dashscope" / "types", + _REPO / "dashscope" / "agentstudio", + _REPO / "dashscope" / "finetune", +) _ROOT_INIT = _REPO / "dashscope" / "__init__.py" # What acli is allowed to couple to. The bare ``dashscope`` entry is here @@ -76,7 +85,9 @@ def _parse(root: pathlib.Path): A syntax error is a failure, not a skip: silently skipping would let an unparseable file drop out of the survey and hollow out the guard. """ + assert root.exists(), f"scan target does not exist: {root}" paths = [root] if root.is_file() else sorted(root.rglob("*.py")) + assert paths, f"scan target contains no Python modules: {root}" parsed = [] for path in paths: source = path.read_text(encoding="utf-8") @@ -296,13 +307,17 @@ def test_acli_also_sets_the_env_var(): # --------------------------------------------------------------------------- -def test_v2_package_does_not_import_acli(): - hits = { - m: loc - for m, loc in _imports_under(str(_V2)).items() - if _is_acli_internal(m) - } - assert not hits, f"2.0 must not depend on acli: {hits}" +def test_current_implementation_does_not_import_acli(): + hits = {} + for root in _CURRENT: + hits.update( + { + module: locations + for module, locations in _imports_under(str(root)).items() + if _is_acli_internal(module) + }, + ) + assert not hits, f"the current implementation must not depend on acli: {hits}" def test_package_root_does_not_import_acli(): @@ -315,11 +330,10 @@ def test_package_root_does_not_import_acli(): def test_acli_lives_at_the_top_level(): - """acli is a neutral surface: neither the 2.0 nor the legacy bucket.""" assert _ACLI.is_dir(), "dashscope/acli/ must stay at the top level" - moved_into_v2 = (_REPO / "dashscope" / "v2" / "acli").exists() - moved_into_legacy = (_REPO / "dashscope" / "legacy" / "acli").exists() - assert not moved_into_v2, "acli must not be folded into 2.0" + moved_into_legacy = ( + _REPO / "dashscope" / "legacy_1x" / "acli" + ).exists() assert not moved_into_legacy, "acli must not land in the legacy bucket" diff --git a/tests/unit/test_acli_skill_render.py b/tests/legacy_1x/unit/test_acli_skill_render.py similarity index 100% rename from tests/unit/test_acli_skill_render.py rename to tests/legacy_1x/unit/test_acli_skill_render.py diff --git a/tests/unit/test_acli_usage_report.py b/tests/legacy_1x/unit/test_acli_usage_report.py similarity index 100% rename from tests/unit/test_acli_usage_report.py rename to tests/legacy_1x/unit/test_acli_usage_report.py diff --git a/tests/unit/test_acli_vision_tool.py b/tests/legacy_1x/unit/test_acli_vision_tool.py similarity index 100% rename from tests/unit/test_acli_vision_tool.py rename to tests/legacy_1x/unit/test_acli_vision_tool.py diff --git a/tests/unit/test_add_resources.py b/tests/legacy_1x/unit/test_add_resources.py similarity index 96% rename from tests/unit/test_add_resources.py rename to tests/legacy_1x/unit/test_add_resources.py index b06165b2..ea180ff7 100644 --- a/tests/unit/test_add_resources.py +++ b/tests/legacy_1x/unit/test_add_resources.py @@ -5,9 +5,9 @@ from http import HTTPStatus from dashscope import Generation -from tests.unit.http_task_request import HttpRequest -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.http_task_request import HttpRequest +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer model = Generation.Models.qwen_turbo diff --git a/tests/unit/test_agentic_rl_cli_opd.py b/tests/legacy_1x/unit/test_agentic_rl_cli_opd.py similarity index 94% rename from tests/unit/test_agentic_rl_cli_opd.py rename to tests/legacy_1x/unit/test_agentic_rl_cli_opd.py index 59620f36..1140ab98 100644 --- a/tests/unit/test_agentic_rl_cli_opd.py +++ b/tests/legacy_1x/unit/test_agentic_rl_cli_opd.py @@ -9,7 +9,7 @@ from typer.testing import CliRunner from dashscope.legacy_1x.cli.agentic_rl import _run_workflow_async, app -from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL def test_run_help_explains_conditional_function_requirements(): @@ -54,7 +54,7 @@ async def inspect_config(client): with ( patch( - "dashscope.v2.finetune.agentic_rl.set_api_key", + "dashscope.finetune.agentic_rl.set_api_key", ), patch.object( AgenticRL, diff --git a/tests/unit/test_agentic_rl_components.py b/tests/legacy_1x/unit/test_agentic_rl_components.py similarity index 100% rename from tests/unit/test_agentic_rl_components.py rename to tests/legacy_1x/unit/test_agentic_rl_components.py diff --git a/tests/unit/test_agentic_rl_model.py b/tests/legacy_1x/unit/test_agentic_rl_model.py similarity index 100% rename from tests/unit/test_agentic_rl_model.py rename to tests/legacy_1x/unit/test_agentic_rl_model.py diff --git a/tests/unit/test_agentic_rl_submit_job.py b/tests/legacy_1x/unit/test_agentic_rl_submit_job.py similarity index 91% rename from tests/unit/test_agentic_rl_submit_job.py rename to tests/legacy_1x/unit/test_agentic_rl_submit_job.py index 0da6ed38..113dfa96 100644 --- a/tests/unit/test_agentic_rl_submit_job.py +++ b/tests/legacy_1x/unit/test_agentic_rl_submit_job.py @@ -287,7 +287,7 @@ async def test_main_workflow_success( "dashscope.finetune.reinforcement.common.utils.set_api_key", ), patch.object( __import__( - "dashscope.finetune.agentic_rl", + "dashscope.legacy_1x.finetune.agentic_rl", fromlist=["AgenticRL"], ), "AgenticRL", @@ -296,7 +296,7 @@ async def test_main_workflow_success( instance.run = AsyncMock(return_value=mock_success_run_result) MockAgenticRL.get = MagicMock(return_value=mock_get_result) - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL client = AgenticRL() result = await client.run( @@ -347,7 +347,7 @@ async def test_main_workflow_success( @pytest.mark.asyncio async def test_main_workflow_submit_failure(self, mock_failure_run_result): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch.object( AgenticRL, @@ -380,7 +380,7 @@ async def test_main_workflow_submit_failure(self, mock_failure_run_result): @pytest.mark.asyncio async def test_main_workflow_run_exception(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch.object( AgenticRL, @@ -420,7 +420,7 @@ async def test_yaml_workflow_success( mock_success_run_result, mock_get_result, ): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import TuningModel with patch.object(AgenticRL, "init") as mock_init, patch.object( @@ -459,7 +459,7 @@ async def test_yaml_workflow_submit_failure( self, mock_failure_run_result, ): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch.object(AgenticRL, "init"), patch.object( AgenticRL, @@ -480,7 +480,7 @@ async def test_yaml_workflow_submit_failure( @pytest.mark.asyncio async def test_yaml_workflow_to_yaml_export(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import TuningModel client = AgenticRL() @@ -499,48 +499,48 @@ async def test_yaml_workflow_to_yaml_export(self): class TestAgenticRLClient: def test_init_with_valid_api_key(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch( - "dashscope.v2.finetune.agentic_rl.set_api_key", + "dashscope.finetune.agentic_rl.set_api_key", ) as mock_set_key: client = AgenticRL(api_key="sk-test-key") mock_set_key.assert_called_once_with("sk-test-key") assert isinstance(client, AgenticRL) def test_init_with_invalid_api_key(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( ValueErrorWithCode, ) with patch( - "dashscope.v2.finetune.agentic_rl.set_api_key", + "dashscope.finetune.agentic_rl.set_api_key", side_effect=Exception("Invalid key"), ): with pytest.raises(ValueErrorWithCode): AgenticRL(api_key="invalid") def test_init_without_api_key(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch( - "dashscope.v2.finetune.agentic_rl.set_api_key", + "dashscope.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL() mock_set_key.assert_called_once_with(None) def test_init_with_api_key(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch( - "dashscope.v2.finetune.agentic_rl.set_api_key", + "dashscope.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL(api_key="your_api_key_here") mock_set_key.assert_called_once_with("your_api_key_here") def test_init_from_yaml(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import TuningModel mock_tuning = MagicMock(spec=TuningModel) @@ -562,7 +562,7 @@ def test_init_from_yaml(self): assert result is client def test_init_from_yaml_default_path(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import TuningModel mock_tuning = MagicMock(spec=TuningModel) @@ -577,7 +577,7 @@ def test_init_from_yaml_default_path(self): @pytest.mark.asyncio async def test_register_functions_success(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL client = AgenticRL() client.tuning = MagicMock() @@ -605,7 +605,7 @@ async def test_register_functions_success(self): @pytest.mark.asyncio async def test_register_functions_failure(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( RegistrationError, ) @@ -622,7 +622,7 @@ async def test_register_functions_failure(self): @pytest.mark.asyncio async def test_upload_datasets_success(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL client = AgenticRL() client.tuning = MagicMock() @@ -636,7 +636,7 @@ async def test_upload_datasets_success(self): @pytest.mark.asyncio async def test_upload_datasets_failure(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( DatasetsError, ) @@ -659,10 +659,10 @@ async def test_upload_datasets_failure(self): class TestSubmitJob: def test_submit_job_builds_correct_request(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL with patch( - "dashscope.v2.finetune.agentic_rl.generate_random_id", + "dashscope.finetune.agentic_rl.generate_random_id", return_value="abcdef1234567890", ): client = AgenticRL() @@ -694,7 +694,7 @@ def test_submit_job_builds_correct_request(self): ) def test_submit_job_duplicate_function_names(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( ValueErrorWithCode, ) @@ -725,12 +725,12 @@ def test_submit_job_duplicate_function_names(self): class TestClassMethods: def test_get_job(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_result = MagicMock() mock_result.status_code = 200 with patch( - "dashscope.finetune.agentic_rl.FineTunes.get", + "dashscope.legacy_1x.finetune.agentic_rl.FineTunes.get", return_value=mock_result, ) as mock_get: result = AgenticRL.get(job_id="ft-job-123") @@ -738,12 +738,12 @@ def test_get_job(self): mock_get.assert_called_once() def test_cancel_job(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_result = MagicMock() mock_result.status_code = 200 with patch( - "dashscope.finetune.agentic_rl.FineTunes.cancel", + "dashscope.legacy_1x.finetune.agentic_rl.FineTunes.cancel", return_value=mock_result, ) as mock_cancel: result = AgenticRL.cancel(job_id="ft-job-123") @@ -751,12 +751,12 @@ def test_cancel_job(self): mock_cancel.assert_called_once() def test_list_jobs(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_result = MagicMock() mock_result.status_code = 200 with patch( - "dashscope.finetune.agentic_rl.FineTunes.list", + "dashscope.legacy_1x.finetune.agentic_rl.FineTunes.list", return_value=mock_result, ) as mock_list: result = AgenticRL.list(page_no=1, page_size=10) @@ -764,12 +764,12 @@ def test_list_jobs(self): mock_list.assert_called_once() def test_delete_job(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_result = MagicMock() mock_result.status_code = 200 with patch( - "dashscope.finetune.agentic_rl.FineTunes.delete", + "dashscope.legacy_1x.finetune.agentic_rl.FineTunes.delete", return_value=mock_result, ) as mock_delete: result = AgenticRL.delete(job_id="ft-job-123") @@ -777,12 +777,12 @@ def test_delete_job(self): mock_delete.assert_called_once() def test_logs_job(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_result = MagicMock() mock_result.status_code = 200 with patch( - "dashscope.finetune.agentic_rl.FineTunes.logs", + "dashscope.legacy_1x.finetune.agentic_rl.FineTunes.logs", return_value=mock_result, ) as mock_logs: result = AgenticRL.logs( @@ -810,7 +810,7 @@ async def test_run_full_workflow( hyper_parameters, resources, ): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_finetune = MagicMock() mock_finetune.status_code = 200 @@ -848,7 +848,7 @@ async def test_run_full_workflow( @pytest.mark.asyncio async def test_run_register_failure_raises(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( RuntimeErrorWithCode, ) @@ -867,7 +867,7 @@ async def test_run_register_failure_raises(self): @pytest.mark.asyncio async def test_run_upload_failure_raises(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( RuntimeErrorWithCode, ) @@ -890,7 +890,7 @@ async def test_run_upload_failure_raises(self): @pytest.mark.asyncio async def test_run_submit_failure_raises(self): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement.common.errors import ( RuntimeErrorWithCode, ) @@ -916,7 +916,7 @@ async def test_run_submit_failure_raises(self): @pytest.mark.asyncio async def test_run_with_no_validation_datasets(self, training_datasets): - from dashscope.finetune.agentic_rl import AgenticRL + from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL mock_finetune = MagicMock() mock_finetune.status_code = 200 diff --git a/tests/unit/test_agentic_rl_utils.py b/tests/legacy_1x/unit/test_agentic_rl_utils.py similarity index 100% rename from tests/unit/test_agentic_rl_utils.py rename to tests/legacy_1x/unit/test_agentic_rl_utils.py diff --git a/tests/unit/test_agentstudio_base_url.py b/tests/legacy_1x/unit/test_agentstudio_base_url.py similarity index 100% rename from tests/unit/test_agentstudio_base_url.py rename to tests/legacy_1x/unit/test_agentstudio_base_url.py diff --git a/tests/unit/test_agentstudio_deployments.py b/tests/legacy_1x/unit/test_agentstudio_deployments.py similarity index 100% rename from tests/unit/test_agentstudio_deployments.py rename to tests/legacy_1x/unit/test_agentstudio_deployments.py diff --git a/tests/unit/test_agentstudio_files.py b/tests/legacy_1x/unit/test_agentstudio_files.py similarity index 100% rename from tests/unit/test_agentstudio_files.py rename to tests/legacy_1x/unit/test_agentstudio_files.py diff --git a/tests/unit/test_agentstudio_memory_stores.py b/tests/legacy_1x/unit/test_agentstudio_memory_stores.py similarity index 100% rename from tests/unit/test_agentstudio_memory_stores.py rename to tests/legacy_1x/unit/test_agentstudio_memory_stores.py diff --git a/tests/unit/test_agentstudio_pagination.py b/tests/legacy_1x/unit/test_agentstudio_pagination.py similarity index 100% rename from tests/unit/test_agentstudio_pagination.py rename to tests/legacy_1x/unit/test_agentstudio_pagination.py diff --git a/tests/unit/test_agentstudio_skills.py b/tests/legacy_1x/unit/test_agentstudio_skills.py similarity index 100% rename from tests/unit/test_agentstudio_skills.py rename to tests/legacy_1x/unit/test_agentstudio_skills.py diff --git a/tests/unit/test_aio_session.py b/tests/legacy_1x/unit/test_aio_session.py similarity index 100% rename from tests/unit/test_aio_session.py rename to tests/legacy_1x/unit/test_aio_session.py diff --git a/tests/unit/test_aio_session_cleanup.py b/tests/legacy_1x/unit/test_aio_session_cleanup.py similarity index 100% rename from tests/unit/test_aio_session_cleanup.py rename to tests/legacy_1x/unit/test_aio_session_cleanup.py diff --git a/tests/unit/test_application.py b/tests/legacy_1x/unit/test_application.py similarity index 98% rename from tests/unit/test_application.py rename to tests/legacy_1x/unit/test_application.py index ead3b65f..a6fa53c3 100644 --- a/tests/unit/test_application.py +++ b/tests/legacy_1x/unit/test_application.py @@ -11,8 +11,8 @@ from dashscope import Application from dashscope.legacy_1x.app.application_response import ApplicationResponse -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestCompletion(MockServerBase): diff --git a/tests/unit/test_asr_phrases.py b/tests/legacy_1x/unit/test_asr_phrases.py similarity index 98% rename from tests/unit/test_asr_phrases.py rename to tests/legacy_1x/unit/test_asr_phrases.py index 29d2f75c..768924a3 100644 --- a/tests/unit/test_asr_phrases.py +++ b/tests/legacy_1x/unit/test_asr_phrases.py @@ -11,8 +11,8 @@ import dashscope from dashscope.legacy_1x.audio.asr import AsrPhraseManager -from tests.unit.constants import TEST_JOB_ID -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.constants import TEST_JOB_ID +from tests.legacy_1x.unit.mock_request_base import MockRequestBase logger = logging.getLogger("dashscope") logger.setLevel(logging.DEBUG) diff --git a/tests/unit/test_assistant_files.py b/tests/legacy_1x/unit/test_assistant_files.py similarity index 96% rename from tests/unit/test_assistant_files.py rename to tests/legacy_1x/unit/test_assistant_files.py index b6c22365..84522e83 100644 --- a/tests/unit/test_assistant_files.py +++ b/tests/legacy_1x/unit/test_assistant_files.py @@ -5,8 +5,8 @@ import uuid from dashscope.legacy_1x.assistants.files import Files -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestAssistantFiles(MockServerBase): diff --git a/tests/unit/test_assistants.py b/tests/legacy_1x/unit/test_assistants.py similarity index 98% rename from tests/unit/test_assistants.py rename to tests/legacy_1x/unit/test_assistants.py index 883d053d..3f117779 100644 --- a/tests/unit/test_assistants.py +++ b/tests/legacy_1x/unit/test_assistants.py @@ -5,8 +5,8 @@ import uuid from dashscope import Assistants # pylint: disable=no-name-in-module -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestAssistants(MockServerBase): diff --git a/tests/unit/test_async_custom_session.py b/tests/legacy_1x/unit/test_async_custom_session.py similarity index 100% rename from tests/unit/test_async_custom_session.py rename to tests/legacy_1x/unit/test_async_custom_session.py diff --git a/tests/unit/test_banner_guide.py b/tests/legacy_1x/unit/test_banner_guide.py similarity index 100% rename from tests/unit/test_banner_guide.py rename to tests/legacy_1x/unit/test_banner_guide.py diff --git a/tests/unit/test_cli_agent_route.py b/tests/legacy_1x/unit/test_cli_agent_route.py similarity index 100% rename from tests/unit/test_cli_agent_route.py rename to tests/legacy_1x/unit/test_cli_agent_route.py diff --git a/tests/unit/test_cli_application.py b/tests/legacy_1x/unit/test_cli_application.py similarity index 100% rename from tests/unit/test_cli_application.py rename to tests/legacy_1x/unit/test_cli_application.py diff --git a/tests/unit/test_cli_auth.py b/tests/legacy_1x/unit/test_cli_auth.py similarity index 100% rename from tests/unit/test_cli_auth.py rename to tests/legacy_1x/unit/test_cli_auth.py diff --git a/tests/unit/test_cli_code_generation.py b/tests/legacy_1x/unit/test_cli_code_generation.py similarity index 100% rename from tests/unit/test_cli_code_generation.py rename to tests/legacy_1x/unit/test_cli_code_generation.py diff --git a/tests/unit/test_cli_common.py b/tests/legacy_1x/unit/test_cli_common.py similarity index 100% rename from tests/unit/test_cli_common.py rename to tests/legacy_1x/unit/test_cli_common.py diff --git a/tests/unit/test_cli_embeddings.py b/tests/legacy_1x/unit/test_cli_embeddings.py similarity index 100% rename from tests/unit/test_cli_embeddings.py rename to tests/legacy_1x/unit/test_cli_embeddings.py diff --git a/tests/unit/test_cli_image_generation.py b/tests/legacy_1x/unit/test_cli_image_generation.py similarity index 100% rename from tests/unit/test_cli_image_generation.py rename to tests/legacy_1x/unit/test_cli_image_generation.py diff --git a/tests/unit/test_cli_image_synthesis.py b/tests/legacy_1x/unit/test_cli_image_synthesis.py similarity index 100% rename from tests/unit/test_cli_image_synthesis.py rename to tests/legacy_1x/unit/test_cli_image_synthesis.py diff --git a/tests/unit/test_cli_main.py b/tests/legacy_1x/unit/test_cli_main.py similarity index 99% rename from tests/unit/test_cli_main.py rename to tests/legacy_1x/unit/test_cli_main.py index f013171b..203067df 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/legacy_1x/unit/test_cli_main.py @@ -515,7 +515,7 @@ def mock_files_list(**kwargs): def mock_fine_tunes_list(**kwargs): captured_requests["fine_tunes"] = kwargs - from dashscope.finetune.customize_types import ( + from dashscope.legacy_1x.finetune.customize_types import ( FineTuneList, FineTuneListOutput, ) @@ -526,7 +526,7 @@ def mock_fine_tunes_list(**kwargs): def mock_deployments_list(**kwargs): captured_requests["deployments"] = kwargs - from dashscope.finetune.customize_types import ( + from dashscope.legacy_1x.finetune.customize_types import ( DeploymentList, DeploymentListOutput, ) diff --git a/tests/unit/test_cli_models.py b/tests/legacy_1x/unit/test_cli_models.py similarity index 100% rename from tests/unit/test_cli_models.py rename to tests/legacy_1x/unit/test_cli_models.py diff --git a/tests/unit/test_cli_multimodal_conversation.py b/tests/legacy_1x/unit/test_cli_multimodal_conversation.py similarity index 100% rename from tests/unit/test_cli_multimodal_conversation.py rename to tests/legacy_1x/unit/test_cli_multimodal_conversation.py diff --git a/tests/unit/test_cli_multimodal_embedding.py b/tests/legacy_1x/unit/test_cli_multimodal_embedding.py similarity index 100% rename from tests/unit/test_cli_multimodal_embedding.py rename to tests/legacy_1x/unit/test_cli_multimodal_embedding.py diff --git a/tests/unit/test_cli_rerank.py b/tests/legacy_1x/unit/test_cli_rerank.py similarity index 100% rename from tests/unit/test_cli_rerank.py rename to tests/legacy_1x/unit/test_cli_rerank.py diff --git a/tests/unit/test_cli_speech_synthesis.py b/tests/legacy_1x/unit/test_cli_speech_synthesis.py similarity index 100% rename from tests/unit/test_cli_speech_synthesis.py rename to tests/legacy_1x/unit/test_cli_speech_synthesis.py diff --git a/tests/unit/test_cli_tokenization.py b/tests/legacy_1x/unit/test_cli_tokenization.py similarity index 100% rename from tests/unit/test_cli_tokenization.py rename to tests/legacy_1x/unit/test_cli_tokenization.py diff --git a/tests/unit/test_cli_transcription.py b/tests/legacy_1x/unit/test_cli_transcription.py similarity index 100% rename from tests/unit/test_cli_transcription.py rename to tests/legacy_1x/unit/test_cli_transcription.py diff --git a/tests/unit/test_cli_understanding.py b/tests/legacy_1x/unit/test_cli_understanding.py similarity index 100% rename from tests/unit/test_cli_understanding.py rename to tests/legacy_1x/unit/test_cli_understanding.py diff --git a/tests/unit/test_cli_video_synthesis.py b/tests/legacy_1x/unit/test_cli_video_synthesis.py similarity index 100% rename from tests/unit/test_cli_video_synthesis.py rename to tests/legacy_1x/unit/test_cli_video_synthesis.py diff --git a/tests/unit/test_code_generation.py b/tests/legacy_1x/unit/test_code_generation.py similarity index 99% rename from tests/unit/test_code_generation.py rename to tests/legacy_1x/unit/test_code_generation.py index f378ad81..74e2e764 100644 --- a/tests/unit/test_code_generation.py +++ b/tests/legacy_1x/unit/test_code_generation.py @@ -9,8 +9,8 @@ AttachmentRoleMessageParam, UserRoleMessageParam, ) -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer model = CodeGeneration.Models.tongyi_lingma_v1 diff --git a/tests/unit/test_conversation.py b/tests/legacy_1x/unit/test_conversation.py similarity index 98% rename from tests/unit/test_conversation.py rename to tests/legacy_1x/unit/test_conversation.py index c1dc3265..e40f60c6 100644 --- a/tests/unit/test_conversation.py +++ b/tests/legacy_1x/unit/test_conversation.py @@ -16,8 +16,8 @@ Role, ) from dashscope.legacy_1x.common.message_manager import MessageManager -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer def request_generator(): diff --git a/tests/unit/test_dashscope_response.py b/tests/legacy_1x/unit/test_dashscope_response.py similarity index 100% rename from tests/unit/test_dashscope_response.py rename to tests/legacy_1x/unit/test_dashscope_response.py diff --git a/tests/unit/test_handlers_key_docs.py b/tests/legacy_1x/unit/test_handlers_key_docs.py similarity index 100% rename from tests/unit/test_handlers_key_docs.py rename to tests/legacy_1x/unit/test_handlers_key_docs.py diff --git a/tests/unit/test_http_api.py b/tests/legacy_1x/unit/test_http_api.py similarity index 95% rename from tests/unit/test_http_api.py rename to tests/legacy_1x/unit/test_http_api.py index 15ada150..57f2b45c 100644 --- a/tests/unit/test_http_api.py +++ b/tests/legacy_1x/unit/test_http_api.py @@ -4,12 +4,12 @@ from http import HTTPStatus from dashscope import Generation -from tests.unit.constants import ( +from tests.legacy_1x.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, TEST_ENABLE_DATA_INSPECTION_REQUEST_ID, ) -from tests.unit.http_task_request import HttpRequest -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.http_task_request import HttpRequest +from tests.legacy_1x.unit.mock_request_base import MockRequestBase def request_generator(): diff --git a/tests/unit/test_http_deployments_api.py b/tests/legacy_1x/unit/test_http_deployments_api.py similarity index 93% rename from tests/unit/test_http_deployments_api.py rename to tests/legacy_1x/unit/test_http_deployments_api.py index 3119344e..e22bcb7c 100644 --- a/tests/unit/test_http_deployments_api.py +++ b/tests/legacy_1x/unit/test_http_deployments_api.py @@ -4,8 +4,8 @@ from http import HTTPStatus from dashscope import Deployments -from tests.unit.constants import TEST_JOB_ID -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.constants import TEST_JOB_ID +from tests.legacy_1x.unit.mock_request_base import MockRequestBase class TestDeploymentRequest(MockRequestBase): diff --git a/tests/unit/test_http_files_api.py b/tests/legacy_1x/unit/test_http_files_api.py similarity index 95% rename from tests/unit/test_http_files_api.py rename to tests/legacy_1x/unit/test_http_files_api.py index 950ff954..5e7938a7 100644 --- a/tests/unit/test_http_files_api.py +++ b/tests/legacy_1x/unit/test_http_files_api.py @@ -4,7 +4,7 @@ from http import HTTPStatus from dashscope import Files -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.mock_request_base import MockRequestBase class TestFileRequest(MockRequestBase): diff --git a/tests/unit/test_http_fine_tunes_api.py b/tests/legacy_1x/unit/test_http_fine_tunes_api.py similarity index 97% rename from tests/unit/test_http_fine_tunes_api.py rename to tests/legacy_1x/unit/test_http_fine_tunes_api.py index 3ea7e573..35457f52 100644 --- a/tests/unit/test_http_fine_tunes_api.py +++ b/tests/legacy_1x/unit/test_http_fine_tunes_api.py @@ -6,9 +6,9 @@ from http import HTTPStatus from dashscope import FineTunes -from tests.unit.constants import TEST_JOB_ID -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.constants import TEST_JOB_ID +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer # yapf: disable diff --git a/tests/unit/test_http_models_api.py b/tests/legacy_1x/unit/test_http_models_api.py similarity index 82% rename from tests/unit/test_http_models_api.py rename to tests/legacy_1x/unit/test_http_models_api.py index 53a01ff8..3d32672b 100644 --- a/tests/unit/test_http_models_api.py +++ b/tests/legacy_1x/unit/test_http_models_api.py @@ -4,8 +4,8 @@ from http import HTTPStatus from dashscope import Models -from tests.unit.constants import TEST_JOB_ID -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.constants import TEST_JOB_ID +from tests.legacy_1x.unit.mock_request_base import MockRequestBase class TestModelRequest(MockRequestBase): diff --git a/tests/unit/test_http_speech_synthesizer.py b/tests/legacy_1x/unit/test_http_speech_synthesizer.py similarity index 100% rename from tests/unit/test_http_speech_synthesizer.py rename to tests/legacy_1x/unit/test_http_speech_synthesizer.py diff --git a/tests/unit/test_import_deprecation.py b/tests/legacy_1x/unit/test_import_deprecation.py similarity index 98% rename from tests/unit/test_import_deprecation.py rename to tests/legacy_1x/unit/test_import_deprecation.py index 86fe722c..cde7beab 100644 --- a/tests/unit/test_import_deprecation.py +++ b/tests/legacy_1x/unit/test_import_deprecation.py @@ -13,7 +13,7 @@ import textwrap from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[2] +REPO_ROOT = Path(__file__).resolve().parents[3] def _run(snippet, *, error_on_deprecation): diff --git a/tests/unit/test_layer_decoupling.py b/tests/legacy_1x/unit/test_layer_decoupling.py similarity index 69% rename from tests/unit/test_layer_decoupling.py rename to tests/legacy_1x/unit/test_layer_decoupling.py index a7ef2350..5ce04de8 100644 --- a/tests/unit/test_layer_decoupling.py +++ b/tests/legacy_1x/unit/test_layer_decoupling.py @@ -1,18 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""The layer boundaries the 1.x / 2.0 decoupling rests on. - -``dashscope/_shared`` is the neutral layer both stacks import from, and -the 2.0 package must not reach into the 1.x *business* surface at all. -Neither rule is enforced by the interpreter: both are import-graph -properties that a well-meaning edit undoes silently, and both only fail -at the moment the 1.x tree moves to ``legacy_1x/`` and 2.0 is promoted to -the top level. So they are surveyed here as AST rather than left as -prose. - -``test_v2_does_not_import_the_1x_business_surface`` is where the design -doc's "hits must decrease monotonically" principle lands: zero. That -zero is the precondition for the physical move, not a consequence of it. +"""Import-graph boundaries for the root implementation and legacy APIs. + +``dashscope/_shared`` is the neutral layer both stacks import from. The +current implementation must not reach into the 1.x business surface, apart +from explicitly pinned compatibility adapters. These are import-graph +properties, so they are surveyed as AST rather than left as prose. """ from __future__ import annotations @@ -21,30 +14,68 @@ import importlib import pathlib -_REPO = pathlib.Path(__file__).resolve().parents[2] +_REPO = pathlib.Path(__file__).resolve().parents[3] _DASHSCOPE = _REPO / "dashscope" _SHARED = _DASHSCOPE / "_shared" -_V2 = _DASHSCOPE / "v2" +_CURRENT = ( + _DASHSCOPE / "_config.py", + _DASHSCOPE / "_error_registry.py", + _DASHSCOPE / "_logging.py", + _DASHSCOPE / "_quickstart.py", + _DASHSCOPE / "finetune" / "agentic_rl.py", + _DASHSCOPE / "client.py", + _DASHSCOPE / "errors.py", + _DASHSCOPE / "finetune" / "_submit.py", + _DASHSCOPE / "protocols", + _DASHSCOPE / "resources", + _DASHSCOPE / "transport", + _DASHSCOPE / "types", + _DASHSCOPE / "agentstudio", + _DASHSCOPE / "finetune" / "reinforcement", + _DASHSCOPE / "tokenizers" / "tokenizer.py", + _DASHSCOPE / "tokenizers" / "tokenizer_base.py", + _DASHSCOPE / "tokenizers" / "qwen_tokenizer.py", +) _VERSION_FILE = "dashscope/version.py" -# Second segment of a ``dashscope.*`` module that is not 1.x surface. -# ``acli`` serves both stacks and stays at the top level; ``_shared`` is -# the neutral layer itself. -_NEUTRAL_PACKAGES = frozenset({"v2", "_shared", "acli"}) - -# ``dashscope.version`` is distribution metadata, not 1.x API surface: it -# is a bare constant module with no imports of its own, so it does not -# move to ``legacy_1x/`` and importing it cannot close a cycle. It is read by -# the neutral layer and by the three v2 identity builders. -_NEUTRAL_LEAVES = frozenset({"dashscope.version"}) - -# Reward-server payloads that live under v2 but are deliberately not -# packaged. They import the old deep paths on purpose: once the layout -# move makes those paths canonical again the shims disappear and these -# files are correct without being edited. +_NEUTRAL_PACKAGES = frozenset( + { + "_shared", + "acli", + "agentstudio", + "finetune", + "protocols", + "resources", + "transport", + "types", + "tokenizers", + }, +) + +_NEUTRAL_LEAVES = frozenset( + { + "dashscope", + "dashscope.version", + "dashscope._config", + "dashscope._error_registry", + "dashscope._logging", + "dashscope._quickstart", + "dashscope.client", + "dashscope.errors", + "dashscope.finetune._submit", + }, +) + _SKIP_PARTS = frozenset({"examples"}) +_COMPATIBILITY_IMPORTS = { + ( + "dashscope/finetune/reinforcement/__init__.py", + "dashscope.legacy_1x.cli.agentic_rl", + ), +} + # ``__version__`` bindings that are not the distribution version. _OTHER_VERSION_OWNERS = frozenset( { @@ -52,7 +83,7 @@ # agenticCLI repo, so it must stay independent. "dashscope/acli/__init__.py", # The version of the vendored error-registry data. - "dashscope/v2/_error_registry.py", + "dashscope/_error_registry.py", }, ) @@ -69,12 +100,16 @@ def _parse(path: pathlib.Path) -> ast.AST: def _modules(root: pathlib.Path): - """Every module under ``root``, minus the excluded subtrees.""" - return [ + assert root.exists(), f"scan target does not exist: {root}" + if root.is_file(): + return [root] + paths = [ path for path in sorted(root.rglob("*.py")) if not _SKIP_PARTS.intersection(path.parts) ] + assert paths, f"scan target contains no Python modules: {root}" + return paths def _dashscope_imports(tree: ast.AST): @@ -103,9 +138,9 @@ def _is_neutral(module: str) -> bool: def _module_level_version_owners(tree: ast.AST): """Line numbers of module-scope ``__version__ =`` assignments. - Function-scope bindings are deliberately excluded. Two of the four - version readers -- ``v2/transport/http.default_user_agent`` and - ``v2/agentstudio/client._user_agent`` -- degrade with a local + Function-scope bindings are deliberately excluded. Two readers -- + ``transport/http.default_user_agent`` and + ``agentstudio/client._user_agent`` -- degrade with a local ``except Exception: __version__ = "0.0.0"``. That rebinds a name inside one function; it does not define the distribution version. """ @@ -125,22 +160,23 @@ def _rel(path: pathlib.Path) -> str: return path.relative_to(_REPO).as_posix() -def test_v2_does_not_import_the_1x_business_surface(): - """2.0 depends on the neutral layer and on nothing else in 1.x. - - This is the decreasing gate the decoupling is measured by, and the - precondition for moving 1.x to ``legacy_1x/``: every hit here is a deep - path that stops resolving the moment the tree it points at moves. - """ +def test_current_implementation_does_not_import_1x_business_surface(): offenders = [] - for path in _modules(_V2): - for module, lineno in _dashscope_imports(_parse(path)): - if _is_neutral(module): - continue - offenders.append(f"{_rel(path)}:{lineno} -> {module}") + scanned = [] + for root in _CURRENT: + for path in _modules(root): + scanned.append(path) + for module, lineno in _dashscope_imports(_parse(path)): + if _is_neutral(module): + continue + if (_rel(path), module) in _COMPATIBILITY_IMPORTS: + continue + offenders.append(f"{_rel(path)}:{lineno} -> {module}") + assert scanned, "the current implementation scan must not be empty" assert not offenders, ( - "2.0 must not import the 1.x business surface; these would break " - "the moment 1.x moves to legacy_1x/:\n " + "\n ".join(offenders) + "the current implementation must not import the 1.x business " + "surface outside explicit compatibility adapters:\n " + + "\n ".join(offenders) ) @@ -232,13 +268,13 @@ def test_one_version_reaches_every_identity_string(monkeypatch): assert headers["x-dashscope-sdk-client"].split("/")[1] == version assert ident.get_user_agent().startswith(f"dashscope/{version};") - v2_ident = importlib.import_module("dashscope.v2.transport._ident") + v2_ident = importlib.import_module("dashscope.transport._ident") assert v2_ident.SDK_CLIENT_VALUE.split("/")[1] == version - http = importlib.import_module("dashscope.v2.transport.http") + http = importlib.import_module("dashscope.transport.http") assert http.default_user_agent() == f"dashscope-v2-python/{version}" - studio = importlib.import_module("dashscope.v2.agentstudio.client") + studio = importlib.import_module("dashscope.agentstudio.client") agent = studio._user_agent("https://agentstudio.invalid") assert agent == ( f"dashscope-agentstudio-python/{version} " diff --git a/tests/unit/test_maas_region.py b/tests/legacy_1x/unit/test_maas_region.py similarity index 100% rename from tests/unit/test_maas_region.py rename to tests/legacy_1x/unit/test_maas_region.py diff --git a/tests/unit/test_messages.py b/tests/legacy_1x/unit/test_messages.py similarity index 97% rename from tests/unit/test_messages.py rename to tests/legacy_1x/unit/test_messages.py index 8cbf0213..d1a3d0b3 100644 --- a/tests/unit/test_messages.py +++ b/tests/legacy_1x/unit/test_messages.py @@ -6,8 +6,8 @@ from dashscope import Messages # pylint: disable=no-name-in-module from dashscope.legacy_1x.threads.messages.files import Files -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestMessages(MockServerBase): diff --git a/tests/unit/test_multimodal_dialog.py b/tests/legacy_1x/unit/test_multimodal_dialog.py similarity index 98% rename from tests/unit/test_multimodal_dialog.py rename to tests/legacy_1x/unit/test_multimodal_dialog.py index 710c08ac..b3d3d1bb 100644 --- a/tests/unit/test_multimodal_dialog.py +++ b/tests/legacy_1x/unit/test_multimodal_dialog.py @@ -17,7 +17,7 @@ RequestParameters, Device, ) -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment logger = logging.getLogger("dashscope") logger.setLevel(logging.DEBUG) diff --git a/tests/unit/test_opd.py b/tests/legacy_1x/unit/test_opd.py similarity index 91% rename from tests/unit/test_opd.py rename to tests/legacy_1x/unit/test_opd.py index ac600e59..b49995d7 100644 --- a/tests/unit/test_opd.py +++ b/tests/legacy_1x/unit/test_opd.py @@ -7,7 +7,7 @@ import pytest -from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import ( DataSourceType, RewardFunctionComponent, @@ -22,7 +22,7 @@ def rl_client(): # ``set_api_key`` is looked up in the module that defines __init__, # which is the 2.0 one; patching the 1.x shim's copy would miss. - with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): + with patch("dashscope.finetune.agentic_rl.set_api_key"): return AgenticRL(api_key="test-key") @@ -71,7 +71,7 @@ def test_teacher_model_enables_opd( reward_count, ): with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job( @@ -97,7 +97,7 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): rl_client.init(config_path=str(config)) with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) @@ -109,15 +109,15 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): def test_opd_example_reuses_supported_rl_configuration(rl_client): config = ( - Path(__file__).parents[2] - / "dashscope/v2/finetune/reinforcement/examples/workspace" + Path(__file__).parents[3] + / "dashscope/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) config_text = config.read_text(encoding="utf-8") rl_client.init(config_path=str(config)) with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job() @@ -157,8 +157,8 @@ def test_opd_example_reuses_supported_rl_configuration(rl_client): @pytest.mark.asyncio async def test_opd_example_uploads_local_datasets(rl_client): config = ( - Path(__file__).parents[2] - / "dashscope/v2/finetune/reinforcement/examples/workspace" + Path(__file__).parents[3] + / "dashscope/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) rl_client.init(config_path=str(config)) @@ -179,8 +179,8 @@ async def test_opd_example_uploads_local_datasets(rl_client): def test_regular_rl_example_keeps_resources_and_rl_hyperparameters(rl_client): config = ( - Path(__file__).parents[2] - / "dashscope/v2/finetune/reinforcement/examples/workspace" + Path(__file__).parents[3] + / "dashscope/finetune/reinforcement/examples/workspace" / "rl-job.yaml" ) rl_client.init(config_path=str(config)) @@ -211,7 +211,7 @@ def test_pg_opd_requires_teacher_model(rl_client): def test_regular_reinforcement_is_unchanged(rl_client): with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) diff --git a/tests/unit/test_oss_utils.py b/tests/legacy_1x/unit/test_oss_utils.py similarity index 100% rename from tests/unit/test_oss_utils.py rename to tests/legacy_1x/unit/test_oss_utils.py diff --git a/tests/unit/test_packaging_metadata.py b/tests/legacy_1x/unit/test_packaging_metadata.py similarity index 99% rename from tests/unit/test_packaging_metadata.py rename to tests/legacy_1x/unit/test_packaging_metadata.py index 2056482b..3dfb76cf 100644 --- a/tests/unit/test_packaging_metadata.py +++ b/tests/legacy_1x/unit/test_packaging_metadata.py @@ -21,7 +21,7 @@ import pytest -ROOT = Path(__file__).resolve().parents[2] +ROOT = Path(__file__).resolve().parents[3] SETUP_PY = ROOT / "setup.py" WORKFLOW = ROOT / ".github" / "workflows" / "unit_test.yml" PYPISTATS_URL = ( diff --git a/tests/unit/test_recognition_explicit_params.py b/tests/legacy_1x/unit/test_recognition_explicit_params.py similarity index 100% rename from tests/unit/test_recognition_explicit_params.py rename to tests/legacy_1x/unit/test_recognition_explicit_params.py diff --git a/tests/unit/test_rerank.py b/tests/legacy_1x/unit/test_rerank.py similarity index 97% rename from tests/unit/test_rerank.py rename to tests/legacy_1x/unit/test_rerank.py index fcccd575..357fe88d 100644 --- a/tests/unit/test_rerank.py +++ b/tests/legacy_1x/unit/test_rerank.py @@ -6,8 +6,8 @@ import uuid from dashscope import AioTextReRank, TextReRank -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestReRank(MockServerBase): diff --git a/tests/unit/test_runs.py b/tests/legacy_1x/unit/test_runs.py similarity index 99% rename from tests/unit/test_runs.py rename to tests/legacy_1x/unit/test_runs.py index fe6830dd..86283a4b 100644 --- a/tests/unit/test_runs.py +++ b/tests/legacy_1x/unit/test_runs.py @@ -5,8 +5,8 @@ import uuid from dashscope import Runs, Steps # pylint: disable=no-name-in-module -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestRuns(MockServerBase): diff --git a/tests/unit/test_sdk_headers.py b/tests/legacy_1x/unit/test_sdk_headers.py similarity index 99% rename from tests/unit/test_sdk_headers.py rename to tests/legacy_1x/unit/test_sdk_headers.py index af497df8..21ac656c 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/legacy_1x/unit/test_sdk_headers.py @@ -71,7 +71,7 @@ def test_session_id_has_one_source_across_stacks(): from dashscope.legacy_1x.common._sdk_session import ( sdk_session_id as via_1x, ) - from dashscope.v2.transport._ident import sdk_session_id as via_v2 + from dashscope.transport._ident import sdk_session_id as via_v2 assert via_1x is leaf assert via_v2 is leaf @@ -218,7 +218,7 @@ def test_get_api_module_mapping(): "dashscope.legacy_1x.threads.runs.runs": "assistants", "dashscope.legacy_1x.models": "models", "dashscope.legacy_1x.files": "files", - "dashscope.finetune.finetunes": "finetune", + "dashscope.legacy_1x.finetune.finetunes": "finetune", "dashscope.legacy_1x.multimodal.tingwu.tingwu": "multimodal", # non-API classes never reach this "dashscope.legacy_1x.common.utils": "common", diff --git a/tests/unit/test_set_region.py b/tests/legacy_1x/unit/test_set_region.py similarity index 100% rename from tests/unit/test_set_region.py rename to tests/legacy_1x/unit/test_set_region.py diff --git a/tests/unit/test_sketch_image_synthesis.py b/tests/legacy_1x/unit/test_sketch_image_synthesis.py similarity index 95% rename from tests/unit/test_sketch_image_synthesis.py rename to tests/legacy_1x/unit/test_sketch_image_synthesis.py index 70462079..4706f457 100644 --- a/tests/unit/test_sketch_image_synthesis.py +++ b/tests/legacy_1x/unit/test_sketch_image_synthesis.py @@ -5,8 +5,8 @@ from http import HTTPStatus from dashscope import ImageSynthesis -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestSketchImageSynthesis(MockServerBase): diff --git a/tests/unit/test_speech_synthesis_v2.py b/tests/legacy_1x/unit/test_speech_synthesis_v2.py similarity index 99% rename from tests/unit/test_speech_synthesis_v2.py rename to tests/legacy_1x/unit/test_speech_synthesis_v2.py index 52dee3d3..50594263 100644 --- a/tests/unit/test_speech_synthesis_v2.py +++ b/tests/legacy_1x/unit/test_speech_synthesis_v2.py @@ -9,7 +9,7 @@ from dashscope.legacy_1x.audio.tts_v2 import ResultCallback, SpeechSynthesizer from dashscope.legacy_1x.common.error import RequestFailure from dashscope.legacy_1x.protocol.websocket import ActionType, EventType -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment class TestCallback(ResultCallback): diff --git a/tests/unit/test_sync_custom_session.py b/tests/legacy_1x/unit/test_sync_custom_session.py similarity index 100% rename from tests/unit/test_sync_custom_session.py rename to tests/legacy_1x/unit/test_sync_custom_session.py diff --git a/tests/unit/test_text_embedding.py b/tests/legacy_1x/unit/test_text_embedding.py similarity index 94% rename from tests/unit/test_text_embedding.py rename to tests/legacy_1x/unit/test_text_embedding.py index 9aaf76a7..13f80355 100644 --- a/tests/unit/test_text_embedding.py +++ b/tests/legacy_1x/unit/test_text_embedding.py @@ -4,7 +4,7 @@ from http import HTTPStatus from dashscope import TextEmbedding -from tests.unit.mock_request_base import MockRequestBase +from tests.legacy_1x.unit.mock_request_base import MockRequestBase class TestTextEmbeddingRequest(MockRequestBase): diff --git a/tests/unit/test_threads.py b/tests/legacy_1x/unit/test_threads.py similarity index 96% rename from tests/unit/test_threads.py rename to tests/legacy_1x/unit/test_threads.py index bbd780a1..51fadf1c 100644 --- a/tests/unit/test_threads.py +++ b/tests/legacy_1x/unit/test_threads.py @@ -5,8 +5,8 @@ import uuid from dashscope import Threads # pylint: disable=no-name-in-module -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestThreads(MockServerBase): diff --git a/tests/unit/test_tokenization.py b/tests/legacy_1x/unit/test_tokenization.py similarity index 92% rename from tests/unit/test_tokenization.py rename to tests/legacy_1x/unit/test_tokenization.py index 7728fe78..0c7730b9 100644 --- a/tests/unit/test_tokenization.py +++ b/tests/legacy_1x/unit/test_tokenization.py @@ -5,8 +5,8 @@ from http import HTTPStatus from dashscope import Tokenization -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestTokenization(MockServerBase): diff --git a/tests/unit/test_tokenization_explicit_params.py b/tests/legacy_1x/unit/test_tokenization_explicit_params.py similarity index 100% rename from tests/unit/test_tokenization_explicit_params.py rename to tests/legacy_1x/unit/test_tokenization_explicit_params.py diff --git a/tests/unit/test_tokenizer.py b/tests/legacy_1x/unit/test_tokenizer.py similarity index 55% rename from tests/unit/test_tokenizer.py rename to tests/legacy_1x/unit/test_tokenizer.py index 4742d56f..106cd0ee 100644 --- a/tests/unit/test_tokenizer.py +++ b/tests/legacy_1x/unit/test_tokenizer.py @@ -6,8 +6,8 @@ import pytest -from dashscope.v2.tokenizers.tokenizer import get_tokenizer -from dashscope.v2.tokenizers.qwen_tokenizer import _CHUNK_SIZE +from dashscope.tokenizers.tokenizer import get_tokenizer +from dashscope.tokenizers.qwen_tokenizer import _CHUNK_SIZE class TestTokenization: @@ -83,71 +83,47 @@ def test_encode_chunk_size_exceed(self): assert decoded_with_special == long_text_with_special -class TestLegacyTokenizerPaths: - """The published ``dashscope.tokenizers.*`` deep paths must keep working. - - The implementation moved to ``dashscope.v2.tokenizers``; the 1.x paths - are thin re-export shims, so they have to resolve to the very same - objects rather than to a second copy. - """ +class TestTokenizerPaths: + """The root package owns one implementation and its published exports.""" @pytest.mark.parametrize( - "legacy_path,v2_path,name", + "module_path,name", [ - ( - "dashscope.tokenizers.tokenizer", - "dashscope.v2.tokenizers.tokenizer", - "get_tokenizer", - ), - ( - "dashscope.tokenizers.tokenizer", - "dashscope.v2.tokenizers.tokenizer", - "list_tokenizers", - ), - ( - "dashscope.tokenizers.tokenizer", - "dashscope.v2.tokenizers.tokenizer", - "QWEN_SERIALS", - ), - ( - "dashscope.tokenizers.qwen_tokenizer", - "dashscope.v2.tokenizers.qwen_tokenizer", - "QwenTokenizer", - ), - ( - "dashscope.tokenizers.qwen_tokenizer", - "dashscope.v2.tokenizers.qwen_tokenizer", - "SPECIAL_TOKENS_SET", - ), - ( - "dashscope.tokenizers.tokenizer_base", - "dashscope.v2.tokenizers.tokenizer_base", - "Tokenizer", - ), + ("dashscope.tokenizers.tokenizer", "get_tokenizer"), + ("dashscope.tokenizers.tokenizer", "list_tokenizers"), + ("dashscope.tokenizers.tokenizer", "QWEN_SERIALS"), + ("dashscope.tokenizers.qwen_tokenizer", "QwenTokenizer"), + ("dashscope.tokenizers.qwen_tokenizer", "SPECIAL_TOKENS_SET"), + ("dashscope.tokenizers.tokenizer_base", "Tokenizer"), ], ) - def test_deep_imports_resolve_to_the_v2_objects( - self, - legacy_path, - v2_path, - name, - ): - legacy = importlib.import_module(legacy_path) - v2 = importlib.import_module(v2_path) - - assert getattr(legacy, name) is getattr(v2, name) - - def test_package_level_reexports_are_untouched(self): - import dashscope.tokenizers as legacy_pkg - import dashscope.v2.tokenizers as v2_pkg + def test_deep_imports_resolve(self, module_path, name): + module = importlib.import_module(module_path) - for name in v2_pkg.__all__: - assert getattr(legacy_pkg, name) is getattr(v2_pkg, name) + assert getattr(module, name) is not None - def test_legacy_path_returns_a_working_tokenizer(self): - from dashscope.tokenizers.tokenizer import get_tokenizer as legacy + def test_package_level_reexports(self): + import dashscope.tokenizers as tokenizers + from dashscope.tokenizers.tokenization import Tokenization + from dashscope.tokenizers.tokenizer import ( + get_tokenizer, + list_tokenizers, + ) + from dashscope.tokenizers.tokenizer_base import Tokenizer - tokenizer = legacy("qwen-turbo") + assert tokenizers.__all__ == [ + "Tokenization", + "Tokenizer", + "get_tokenizer", + "list_tokenizers", + ] + assert tokenizers.Tokenization is Tokenization + assert tokenizers.Tokenizer is Tokenizer + assert tokenizers.get_tokenizer is get_tokenizer + assert tokenizers.list_tokenizers is list_tokenizers + + def test_public_path_returns_a_working_tokenizer(self): + tokenizer = get_tokenizer("qwen-turbo") tokens = tokenizer.encode("hello world") assert isinstance(tokens, list) diff --git a/tests/unit/test_tongyi_native.py b/tests/legacy_1x/unit/test_tongyi_native.py similarity index 100% rename from tests/unit/test_tongyi_native.py rename to tests/legacy_1x/unit/test_tongyi_native.py diff --git a/tests/unit/test_translation_recognizer.py b/tests/legacy_1x/unit/test_translation_recognizer.py similarity index 98% rename from tests/unit/test_translation_recognizer.py rename to tests/legacy_1x/unit/test_translation_recognizer.py index 84fc7bb9..6b642d61 100644 --- a/tests/unit/test_translation_recognizer.py +++ b/tests/legacy_1x/unit/test_translation_recognizer.py @@ -12,7 +12,7 @@ TranslationRecognizerRealtime, TranslationResult, ) -from tests.unit.base_test import BaseTestEnvironment +from tests.legacy_1x.unit.base_test import BaseTestEnvironment class Callback(TranslationRecognizerCallback): diff --git a/tests/unit/test_translation_recognizer_explicit_params.py b/tests/legacy_1x/unit/test_translation_recognizer_explicit_params.py similarity index 100% rename from tests/unit/test_translation_recognizer_explicit_params.py rename to tests/legacy_1x/unit/test_translation_recognizer_explicit_params.py diff --git a/tests/unit/test_understanding.py b/tests/legacy_1x/unit/test_understanding.py similarity index 93% rename from tests/unit/test_understanding.py rename to tests/legacy_1x/unit/test_understanding.py index 6e2756c8..63eaf3d4 100644 --- a/tests/unit/test_understanding.py +++ b/tests/legacy_1x/unit/test_understanding.py @@ -5,8 +5,8 @@ from http import HTTPStatus from dashscope import Understanding -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer model = Understanding.Models.opennlu_v1 diff --git a/tests/unit/test_utf8_encoding.py b/tests/legacy_1x/unit/test_utf8_encoding.py similarity index 100% rename from tests/unit/test_utf8_encoding.py rename to tests/legacy_1x/unit/test_utf8_encoding.py diff --git a/tests/unit/test_video_synthesis_params.py b/tests/legacy_1x/unit/test_video_synthesis_params.py similarity index 98% rename from tests/unit/test_video_synthesis_params.py rename to tests/legacy_1x/unit/test_video_synthesis_params.py index a23f0a4a..9be7c779 100644 --- a/tests/unit/test_video_synthesis_params.py +++ b/tests/legacy_1x/unit/test_video_synthesis_params.py @@ -5,8 +5,8 @@ from http import HTTPStatus from dashscope import VideoSynthesis -from tests.unit.mock_request_base import MockServerBase -from tests.unit.mock_server import MockServer +from tests.legacy_1x.unit.mock_request_base import MockServerBase +from tests.legacy_1x.unit.mock_server import MockServer class TestVideoSynthesis(MockServerBase): diff --git a/tests/unit/websocket_mock_server_task_handler.py b/tests/legacy_1x/unit/websocket_mock_server_task_handler.py similarity index 100% rename from tests/unit/websocket_mock_server_task_handler.py rename to tests/legacy_1x/unit/websocket_mock_server_task_handler.py diff --git a/tests/unit/websocket_task_request.py b/tests/legacy_1x/unit/websocket_task_request.py similarity index 100% rename from tests/unit/websocket_task_request.py rename to tests/legacy_1x/unit/websocket_task_request.py diff --git a/tests/v2/protocols/test_anthropic_conformance.py b/tests/protocols/test_anthropic_conformance.py similarity index 97% rename from tests/v2/protocols/test_anthropic_conformance.py rename to tests/protocols/test_anthropic_conformance.py index 4d86b073..666c9fa9 100644 --- a/tests/v2/protocols/test_anthropic_conformance.py +++ b/tests/protocols/test_anthropic_conformance.py @@ -28,8 +28,8 @@ import pytest from dashscope.acli.providers.anthropic import AnthropicProvider -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.protocols import AnthropicProtocol +from dashscope import AsyncClient, Client +from dashscope.protocols import AnthropicProtocol MODEL = "qwen3-max" @@ -253,9 +253,9 @@ async def test_public_tool_choice_translation_and_null_compatibility( tool_choice, expected, ): - monkeypatch.setattr("dashscope.v2._config._load_config_file", lambda _: {}) + monkeypatch.setattr("dashscope._config._load_config_file", lambda _: {}) monkeypatch.setattr( - "dashscope.v2._config._api_key_from_file", + "dashscope._config._api_key_from_file", lambda: None, ) via_parameters = entry_point != "named" diff --git a/tests/v2/protocols/test_integration_endpoints.py b/tests/protocols/test_integration_endpoints.py similarity index 95% rename from tests/v2/protocols/test_integration_endpoints.py rename to tests/protocols/test_integration_endpoints.py index c5aaa3b4..84405257 100644 --- a/tests/v2/protocols/test_integration_endpoints.py +++ b/tests/protocols/test_integration_endpoints.py @@ -11,9 +11,9 @@ gateway case targets any OpenAI-compatible endpoint via the explicit ``base_url`` escape hatch:: - DASHSCOPE_API_KEY=... DASHSCOPE_WORKSPACE=... pytest tests/v2/protocols/ + DASHSCOPE_API_KEY=... DASHSCOPE_WORKSPACE=... pytest tests/protocols/ GATEWAY_API_KEY=... GATEWAY_BASE_URL=... GATEWAY_MODEL=... \\ - pytest tests/v2/protocols/test_integration_endpoints.py + pytest tests/protocols/test_integration_endpoints.py """ from __future__ import annotations @@ -22,7 +22,7 @@ import pytest -from dashscope.v2 import Client +from dashscope import Client def _make_client(api_key, base_url): diff --git a/tests/v2/protocols/test_openai_conformance.py b/tests/protocols/test_openai_conformance.py similarity index 98% rename from tests/v2/protocols/test_openai_conformance.py rename to tests/protocols/test_openai_conformance.py index be0ea359..d6ccd6ca 100644 --- a/tests/v2/protocols/test_openai_conformance.py +++ b/tests/protocols/test_openai_conformance.py @@ -16,7 +16,7 @@ # pylint: disable=protected-access from dashscope.acli.providers.openai import OpenAIProvider -from dashscope.v2.protocols import OpenAIProtocol +from dashscope.protocols import OpenAIProtocol MODEL = "qwen-plus" diff --git a/tests/v2/smoke/test_examples.py b/tests/smoke/test_examples.py similarity index 97% rename from tests/v2/smoke/test_examples.py rename to tests/smoke/test_examples.py index af10f89f..af280e61 100644 --- a/tests/v2/smoke/test_examples.py +++ b/tests/smoke/test_examples.py @@ -18,7 +18,7 @@ import pytest -ROOT = Path(__file__).resolve().parents[3] +ROOT = Path(__file__).resolve().parents[2] EXAMPLES = ROOT / "examples" # Examples that run fully offline (mock transport / local subprocess), diff --git a/tests/v2/smoke/test_full_surface.py b/tests/smoke/test_full_surface.py similarity index 99% rename from tests/v2/smoke/test_full_surface.py rename to tests/smoke/test_full_surface.py index 7b69cbbe..350fa665 100644 --- a/tests/v2/smoke/test_full_surface.py +++ b/tests/smoke/test_full_surface.py @@ -16,7 +16,7 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client +from dashscope import AsyncClient, Client BASE_URL = "https://api.test.local/api/v1" diff --git a/tests/v2/test_agentstudio_errors.py b/tests/test_agentstudio_errors.py similarity index 99% rename from tests/v2/test_agentstudio_errors.py rename to tests/test_agentstudio_errors.py index ecb1b0d2..be555d6b 100644 --- a/tests/v2/test_agentstudio_errors.py +++ b/tests/test_agentstudio_errors.py @@ -22,7 +22,7 @@ from dashscope.agentstudio import exceptions as as_exc from dashscope.agentstudio.pagination import AsyncCursorPage, CursorPage from dashscope.agentstudio.types import ServerEvent, Session -from dashscope.v2 import errors as v2_errors +from dashscope import errors as v2_errors BASE_URL = "https://as.test.local" diff --git a/tests/v2/test_agentstudio_models.py b/tests/test_agentstudio_models.py similarity index 99% rename from tests/v2/test_agentstudio_models.py rename to tests/test_agentstudio_models.py index c383010d..3fecdd86 100644 --- a/tests/v2/test_agentstudio_models.py +++ b/tests/test_agentstudio_models.py @@ -3,11 +3,11 @@ import pytest -from dashscope.v2.agentstudio.resources._helpers import ( +from dashscope.agentstudio.resources._helpers import ( _coerce_vault, _coerce_credential, ) -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio.types import ( Agent, Credential, CredentialAuth, diff --git a/tests/v2/test_agentstudio_protocol.py b/tests/test_agentstudio_protocol.py similarity index 92% rename from tests/v2/test_agentstudio_protocol.py rename to tests/test_agentstudio_protocol.py index 217b4631..6cf6c0e2 100644 --- a/tests/v2/test_agentstudio_protocol.py +++ b/tests/test_agentstudio_protocol.py @@ -18,9 +18,9 @@ import pytest -from dashscope.v2.agentstudio import exceptions -from dashscope.v2.agentstudio.transport import is_error_payload, unwrap -from dashscope.v2.agentstudio.types import ( +from dashscope.agentstudio import exceptions +from dashscope.agentstudio.transport import is_error_payload, unwrap +from dashscope.agentstudio.types import ( user_custom_tool_result, user_define_outcome, user_interrupt, @@ -260,7 +260,7 @@ def test_agent_system_field_and_property(): """Server returns ``system`` field; SDK exposes it both as ``agent.system`` and the convenience ``agent.system_prompt`` property. """ - from dashscope.v2.agentstudio.types import Agent + from dashscope.agentstudio.types import Agent agent = Agent(id="agt_1", system="You are helpful.", name="Test") assert agent.system == "You are helpful." @@ -269,7 +269,7 @@ def test_agent_system_field_and_property(): def test_session_stats_and_usage_fields(): """Session now parses ``stats`` and ``usage`` from server response.""" - from dashscope.v2.agentstudio.types import Session + from dashscope.agentstudio.types import Session stats = {"active_seconds": 42, "duration_seconds": 5} usage = {"input_tokens": 100, "output_tokens": 200} @@ -305,7 +305,7 @@ def __init__(self): def request(self, method, path, **kwargs): self.calls.append({"method": method, "path": path, **kwargs}) - from dashscope.v2.agentstudio.transport import APIResponse + from dashscope.agentstudio.transport import APIResponse return APIResponse( data={"id": "agent_1", "version": 3, "name": "demo"}, @@ -314,7 +314,7 @@ def request(self, method, path, **kwargs): def _client_with_recording_transport(): - from dashscope.v2.agentstudio import Client + from dashscope.agentstudio import Client c = Client(api_key="test-key", base_url="http://test") c.transport = _RecordingTransport() @@ -363,7 +363,7 @@ def test_agents_update_does_not_auto_retrieve(): def test_thread_event_exposes_thread_id(): """thread_* events carry a top-level thread_id; it must be a real field.""" - from dashscope.v2.agentstudio.types import Message + from dashscope.agentstudio.types import Message ev = Message( object="message", @@ -386,7 +386,7 @@ def test_thread_event_exposes_thread_id(): def test_agent_create_body_includes_multiagent(): """multiagent roster is forwarded verbatim on create.""" - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="coordinator", @@ -411,7 +411,7 @@ def test_agent_create_body_includes_multiagent(): def test_agent_update_body_includes_multiagent(): """multiagent is forwarded on update alongside the required version.""" - from dashscope.v2.agentstudio.types.params import AgentUpdateParams + from dashscope.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams( name="coordinator", @@ -431,7 +431,7 @@ def test_agent_update_body_includes_multiagent(): # A MultiAgentConfig read off a response must be accepted back as-is, # so read-modify-write round trips work. - from dashscope.v2.agentstudio.types import Agent + from dashscope.agentstudio.types import Agent hydrated = Agent( id="agent_1", @@ -455,7 +455,7 @@ def test_agent_update_body_includes_multiagent(): def test_agent_create_body_model_id_shorthand(): """Bare model id == ModelConfig(id=...) shorthand.""" - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types.params import AgentCreateParams body = AgentCreateParams(name="a", model="qwen3-max").to_dict() assert body["model"] == {"id": "qwen3-max"} @@ -463,8 +463,8 @@ def test_agent_create_body_model_id_shorthand(): def test_agent_create_body_model_config_object(): """The model config is one object carrying id + inference params.""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -495,8 +495,8 @@ def test_agent_create_body_model_config_object(): def test_agent_create_body_keeps_falsy_params(): """False / 0.0 mean "set to this", not "unset".""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -515,7 +515,7 @@ def test_agent_create_body_keeps_falsy_params(): def test_agent_create_rejects_a_raw_dict_model(): """Request side takes the typed config; the response parser still maps.""" - from dashscope.v2.agentstudio.types.params import ( + from dashscope.agentstudio.types.params import ( AgentCreateParams, AgentUpdateParams, ) @@ -532,8 +532,8 @@ def test_agent_create_rejects_a_raw_dict_model(): def test_agent_create_body_omits_unset_model_params(): """Only the fields you set are sent; the rest keep server defaults.""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentCreateParams body = AgentCreateParams( name="a", @@ -548,9 +548,9 @@ def test_model_config_carries_the_whole_surface_no_flat_kwargs(): """Guard: inference params live on ModelConfig, never as flat kwargs.""" import inspect - from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import ( + from dashscope.agentstudio.resources.agents import Agents, AsyncAgents + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import ( AgentCreateParams, AgentUpdateParams, ) @@ -574,8 +574,8 @@ def test_model_config_carries_the_whole_surface_no_flat_kwargs(): def test_agent_create_requires_a_model_with_id(): """Missing or id-less configs fail fast.""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentCreateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentCreateParams with pytest.raises(ValueError, match="model is required"): AgentCreateParams(name="a", model=None) @@ -586,7 +586,7 @@ def test_agent_create_requires_a_model_with_id(): def test_agent_update_body_omits_model_when_unset(): """Update without model omits the field, so the remote config survives.""" - from dashscope.v2.agentstudio.types.params import AgentUpdateParams + from dashscope.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams(version=2, name="a").to_dict() assert "model" not in body @@ -596,8 +596,8 @@ def test_agent_update_body_omits_model_when_unset(): def test_agent_update_replaces_model_config_without_local_merge(): """No local merge: the SDK sends exactly the config it was given.""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentUpdateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentUpdateParams body = AgentUpdateParams( version=2, @@ -617,8 +617,8 @@ def test_agent_update_replaces_model_config_without_local_merge(): def test_agent_update_partial_config_is_not_silently_dropped(): """An explicit model argument must carry an id, even when empty.""" - from dashscope.v2.agentstudio.types import ModelConfig - from dashscope.v2.agentstudio.types.params import AgentUpdateParams + from dashscope.agentstudio.types import ModelConfig + from dashscope.agentstudio.types.params import AgentUpdateParams with pytest.raises(ValueError, match="model.id is required"): AgentUpdateParams(version=2, model=ModelConfig()) @@ -628,7 +628,7 @@ def test_agent_update_partial_config_is_not_silently_dropped(): def test_agents_create_sends_model_config(): """The resource method forwards the config object into the request.""" - from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.agentstudio.types import ModelConfig client = _client_with_recording_transport() client.agents.create( @@ -650,7 +650,7 @@ def test_agents_create_sends_model_config(): def test_agents_update_sends_model_config(): - from dashscope.v2.agentstudio.types import Agent + from dashscope.agentstudio.types import Agent client = _client_with_recording_transport() agent = Agent(id="agent_1", model={"id": "qwen3-max", "max_tokens": 4096}) @@ -676,7 +676,7 @@ def __init__(self): async def request(self, method, path, **kwargs): self.calls.append({"method": method, "path": path, **kwargs}) - from dashscope.v2.agentstudio.transport import APIResponse + from dashscope.agentstudio.transport import APIResponse return APIResponse( data={"id": "agent_1", "version": 3, "name": "demo"}, @@ -685,7 +685,7 @@ async def request(self, method, path, **kwargs): def _async_client_with_recording_transport(): - from dashscope.v2.agentstudio import AsyncClient + from dashscope.agentstudio import AsyncClient c = AsyncClient(api_key="test-key", base_url="http://test") c.transport = _AsyncRecordingTransport() @@ -696,7 +696,7 @@ def test_agents_sync_and_async_mirror_each_other(): """AsyncAgents must mirror Agents: same kwargs, same docstrings.""" import inspect - from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents + from dashscope.agentstudio.resources.agents import Agents, AsyncAgents for method in ( "create", @@ -720,7 +720,7 @@ def test_agents_sync_and_async_mirror_each_other(): @pytest.mark.asyncio async def test_async_agents_create_sends_model_config(): - from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.agentstudio.types import ModelConfig client = _async_client_with_recording_transport() await client.agents.create( @@ -742,7 +742,7 @@ async def test_async_agents_create_sends_model_config(): @pytest.mark.asyncio async def test_async_agents_update_sends_model_config(): - from dashscope.v2.agentstudio.types import ModelConfig + from dashscope.agentstudio.types import ModelConfig client = _async_client_with_recording_transport() await client.agents.update( @@ -758,7 +758,7 @@ async def test_async_agents_update_sends_model_config(): def test_agent_model_hydrates_multiagent(): """Agent response hydrates the multiagent dict into typed models.""" - from dashscope.v2.agentstudio.types import ( + from dashscope.agentstudio.types import ( Agent, MultiAgentConfig, MultiAgentRosterEntry, @@ -785,7 +785,7 @@ def test_agent_model_hydrates_multiagent(): def _approval_request_msg(): - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message return parse_message( { @@ -811,7 +811,7 @@ def _approval_request_msg(): def test_tool_approval_request_accessor(): - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message m = _approval_request_msg() ap = m.tool_approval_request @@ -826,8 +826,8 @@ def test_tool_approval_request_accessor(): def test_stop_reason_status_and_enum(): - from dashscope.v2.agentstudio.types import StopReason - from dashscope.v2.agentstudio.constants import ( + from dashscope.agentstudio.types import StopReason + from dashscope.agentstudio.constants import ( SSEEventType, SessionStatus, StopReasonType, @@ -853,7 +853,7 @@ def test_stop_reason_status_and_enum(): def test_permission_policy_validation(): - from dashscope.v2.agentstudio.types import PermissionPolicy + from dashscope.agentstudio.types import PermissionPolicy assert PermissionPolicy(type="always_ask").type == "always_ask" assert PermissionPolicy().type == "always_allow" # default @@ -868,7 +868,7 @@ def test_permission_policy_validation(): def test_approval_errors_map_by_http_status(): """Approval error codes are not enumerated as separate exception types; they map by HTTP status, with the reason in .code/.message.""" - from dashscope.v2.agentstudio.exceptions import ( + from dashscope.agentstudio.exceptions import ( InternalServerError, InvalidRequestError, OverloadedError, @@ -896,7 +896,7 @@ def test_event_list_types_repeated_query(): """Multi-type filter serializes as repeated types[]= keys, not a single comma-joined string (which the server parses as one bogus value).""" import httpx - from dashscope.v2.agentstudio.types.params import SessionEventListParams + from dashscope.agentstudio.types.params import SessionEventListParams params = SessionEventListParams( types=["message", "tool_call", "tool_approval_request"], @@ -947,10 +947,10 @@ def _delta_payloads(): def test_delta_frames_and_text_deltas(): - from dashscope.v2.agentstudio.resources.session_events import ( + from dashscope.agentstudio.resources.session_events import ( _TypedEventStream, ) - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message msgs = [parse_message(p) for p in _delta_payloads()] assert msgs[0].event_start == {"id": "sevt_a", "type": "message"} @@ -1003,7 +1003,7 @@ def _chunk(eid, text): def _stream_of(frames): - from dashscope.v2.agentstudio.resources.session_events import ( + from dashscope.agentstudio.resources.session_events import ( _TypedEventStream, ) @@ -1011,7 +1011,7 @@ def _stream_of(frames): def test_second_pass_on_one_stream_raises_instead_of_returning_nothing(): - from dashscope.v2.agentstudio.resources.session_events import ( + from dashscope.agentstudio.resources.session_events import ( _AioTypedEventStream, ) @@ -1050,8 +1050,8 @@ async def collect(): def test_session_resource_model_and_add_params(): - from dashscope.v2.agentstudio import SessionResource - from dashscope.v2.agentstudio.types.params import SessionResourceAddParams + from dashscope.agentstudio import SessionResource + from dashscope.agentstudio.types.params import SessionResourceAddParams r = SessionResource( id="sesrsc_1", @@ -1076,7 +1076,7 @@ def test_session_resource_model_and_add_params(): def test_session_thread_model_fields(): - from dashscope.v2.agentstudio.types import SessionThread + from dashscope.agentstudio.types import SessionThread th = SessionThread( id="sthr_01", @@ -1098,7 +1098,7 @@ def test_session_create_params_agent_string_and_object(): """createSession ``agent`` accepts the Agent ID string (forward-compat) or an override object; ``environment_variables`` / ``mcp_configs`` are new top-level fields. Omitted fields are not sent (no null leak).""" - from dashscope.v2.agentstudio.types.params import SessionCreateParams + from dashscope.agentstudio.types.params import SessionCreateParams # String agent — backward compatible. assert SessionCreateParams(agent="agent_01", title="x").to_dict() == { @@ -1149,7 +1149,7 @@ def test_session_update_params_patch_and_clear_semantics(): an explicit ``None`` sub-field serializes to ``null`` (revoke override), an omitted sub-field stays absent (keep), ``[]`` / ``""`` override empty. """ - from dashscope.v2.agentstudio.types.params import SessionUpdateParams + from dashscope.agentstudio.types.params import SessionUpdateParams # Only title — everything else omitted, no null uploaded. assert SessionUpdateParams(title="x").to_dict() == {"title": "x"} @@ -1214,7 +1214,7 @@ def test_session_update_params_patch_and_clear_semantics(): def test_session_model_hydrates_override_agent_and_new_fields(): """Session response parses the agent override object and the new ``environment_variables`` / ``mcp_configs`` fields.""" - from dashscope.v2.agentstudio.types import Agent, Session + from dashscope.agentstudio.types import Agent, Session s = Session( id="sesn_1", @@ -1240,7 +1240,7 @@ def test_session_model_hydrates_override_agent_and_new_fields(): def test_message_error_and_pending_signal(): - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message ev = parse_message( { @@ -1283,7 +1283,7 @@ def test_message_error_and_pending_signal(): def test_message_data_accessors(): - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message # model_request_end mre = parse_message( @@ -1383,7 +1383,7 @@ def test_message_data_accessors(): def test_thread_message_routing_accessor(): - from dashscope.v2.agentstudio.types import parse_message + from dashscope.agentstudio.types import parse_message sent = parse_message( { @@ -1420,7 +1420,7 @@ def test_multiagent_config_normalization_and_roster_fields(): """The SDK normalizes multiagent config (type default, self cleanup) but does not reject server data — the 1-20 / at-most-one-self limits are the server's to enforce, so parsing a valid agent never fails.""" - from dashscope.v2.agentstudio.types import ( + from dashscope.agentstudio.types import ( MultiAgentConfig, MultiAgentRosterEntry, ) @@ -1452,7 +1452,7 @@ def test_text_stream_stops_on_session_status_idle(): """Regression: text_stream/text_deltas must not crash on session_status events. RESCHEDULING was deleted from constants but stop-set tuples still referenced it, causing AttributeError on every stream's normal end path.""" - from dashscope.v2.agentstudio.resources.session_events import ( + from dashscope.agentstudio.resources.session_events import ( _TypedEventStream, ) diff --git a/tests/v2/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py similarity index 74% rename from tests/v2/test_agentstudio_shim.py rename to tests/test_agentstudio_shim.py index 893f97df..6c6b397b 100644 --- a/tests/v2/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -1,16 +1,9 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Tests for the AgentStudio relocation into the 2.0 package (D7). +"""Tests for AgentStudio's root implementation and public contracts. -The sub-SDK moved to ``dashscope.v2.agentstudio``; ``dashscope.agentstudio`` -is now a shim. Three contracts are pinned here: - -* the old import path -- including deep submodule paths -- resolves to the - very same module and class objects, so nothing downstream sees a fork; -* shared validation and SDK identity helpers still come from the neutral - ``dashscope._shared`` layer, while exceptions come from ``v2.errors``; -* AgentStudio keeps emitting the *unmarked* ``agentstudio`` module tag, and - resolves its api key without reading the 1.x ``dashscope.api_key`` global. +The public package owns the only implementation. Deep imports, shared +validation, unified errors and SDK identity must keep their existing behavior. """ from __future__ import annotations @@ -19,17 +12,15 @@ import pytest -import dashscope.agentstudio as shim -import dashscope.v2.agentstudio as impl +import dashscope.agentstudio as impl +from dashscope import errors as sdk_errors from dashscope._shared import _sdk_ident, _validation -from dashscope.agentstudio import exceptions as shim_exc from dashscope.agentstudio.transport import build_headers from dashscope.legacy_1x.common import error as legacy_error from dashscope.legacy_1x.common import env as legacy_env from dashscope.legacy_1x.common import utils as legacy_utils -from dashscope.v2 import errors as v2_errors -from dashscope.v2.agentstudio import client as as_client -from dashscope.v2.agentstudio import exceptions as impl_exc +from dashscope.agentstudio import client as as_client +from dashscope.agentstudio import exceptions as impl_exc BASE_URL = "https://as.test.local" @@ -82,35 +73,32 @@ def _clean_env(monkeypatch): # --------------------------------------------------------------------------- -# The shim resolves to one and the same implementation +# The public package owns the only implementation # --------------------------------------------------------------------------- -def test_public_names_are_the_same_objects(): - assert shim.__all__ == impl.__all__ +def test_public_names_are_exported(): + assert impl.__all__ for name in impl.__all__: - assert getattr(shim, name) is getattr(impl, name), name + assert getattr(impl, name) is not None, name -def test_client_classes_are_the_same_objects(): - assert shim.Client is impl.Client - assert shim.AsyncClient is impl.AsyncClient - assert shim.Client.__module__ == "dashscope.v2.agentstudio.client" +def test_client_classes_have_root_module_ownership(): + assert impl.Client.__module__ == "dashscope.agentstudio.client" + assert impl.AsyncClient.__module__ == "dashscope.agentstudio.client" -def test_exception_classes_are_the_same_objects(): - assert shim_exc.AgentStudioError is impl_exc.AgentStudioError - assert shim_exc.AuthenticationError is impl_exc.AuthenticationError - assert shim_exc.NotFoundError is impl_exc.NotFoundError +def test_exception_aliases_use_root_errors(): + assert impl_exc.AgentStudioError is sdk_errors.DashScopeError + assert impl_exc.AuthenticationError is sdk_errors.AuthenticationError + assert impl_exc.NotFoundError is sdk_errors.NotFoundError @pytest.mark.parametrize("name", SUBMODULES) -def test_deep_imports_alias_the_v2_module(name): - __import__(f"dashscope.agentstudio.{name}") - assert ( - sys.modules[f"dashscope.agentstudio.{name}"] - is sys.modules[f"dashscope.v2.agentstudio.{name}"] - ) +def test_deep_imports_resolve_to_root_modules(name): + module_name = f"dashscope.agentstudio.{name}" + __import__(module_name) + assert sys.modules[module_name].__name__ == module_name @pytest.mark.parametrize("name", SUBMODULES) @@ -118,20 +106,18 @@ def test_submodules_are_reachable_as_attributes(name): """sys.modules alone is not enough -- attribute access must work too.""" __import__("dashscope.agentstudio") - obj = shim + obj = impl for part in name.split("."): obj = getattr(obj, part) - assert obj is sys.modules[f"dashscope.v2.agentstudio.{name}"] + assert obj is sys.modules[f"dashscope.agentstudio.{name}"] def test_deep_import_from_syntax_resolves(): from dashscope.agentstudio.resources.agents import Agents from dashscope.agentstudio.types.models import Agent - from dashscope.v2.agentstudio.resources.agents import Agents as V2Agents - from dashscope.v2.agentstudio.types.models import Agent as V2Agent - assert Agents is V2Agents - assert Agent is V2Agent + assert Agents.__module__ == "dashscope.agentstudio.resources.agents" + assert Agent.__module__ == "dashscope.agentstudio.types.models" # --------------------------------------------------------------------------- @@ -139,8 +125,8 @@ def test_deep_import_from_syntax_resolves(): # --------------------------------------------------------------------------- -def test_agentstudio_uses_the_v2_error_base(): - assert impl_exc.AgentStudioError is v2_errors.DashScopeError +def test_agentstudio_uses_the_unified_error_base(): + assert impl_exc.AgentStudioError is sdk_errors.DashScopeError assert not issubclass( impl_exc.AgentStudioError, legacy_error.DashScopeException, @@ -215,7 +201,7 @@ def test_cli_sdk_client_override_reaches_agentstudio(monkeypatch): def test_session_id_is_shared_with_the_v2_transport(): - from dashscope.v2.transport._ident import sdk_headers + from dashscope.transport._ident import sdk_headers assert ( build_headers( diff --git a/tests/v2/test_apps.py b/tests/test_apps.py similarity index 99% rename from tests/v2/test_apps.py rename to tests/test_apps.py index d0ddd6c0..587e6566 100644 --- a/tests/v2/test_apps.py +++ b/tests/test_apps.py @@ -12,8 +12,8 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import RateLimitError +from dashscope import AsyncClient, Client +from dashscope.errors import RateLimitError BASE_URL = "https://api.test.local/api/v1" diff --git a/tests/v2/test_audio.py b/tests/test_audio.py similarity index 99% rename from tests/v2/test_audio.py rename to tests/test_audio.py index 3a9ea729..6102b1ae 100644 --- a/tests/v2/test_audio.py +++ b/tests/test_audio.py @@ -12,12 +12,16 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import APIConnectionError, DashScopeError, WsStateError -from dashscope.v2.resources import audio as audio_resources -from dashscope.v2.resources.audio._callbacks import run_with_callback -from dashscope.v2.resources.audio.asr import RecognitionResult -from dashscope.v2.transport.ws import WsEventError +from dashscope import AsyncClient, Client +from dashscope.errors import ( + APIConnectionError, + DashScopeError, + WsStateError, +) +from dashscope.resources import audio as audio_resources +from dashscope.resources.audio._callbacks import run_with_callback +from dashscope.resources.audio.asr import RecognitionResult +from dashscope.transport.ws import WsEventError BASE_URL = "https://api.test.local/api/v1" TRANSCRIPTION_PATH = "/api/v1/services/audio/asr/transcription" diff --git a/tests/v2/test_audio_connection.py b/tests/test_audio_connection.py similarity index 99% rename from tests/v2/test_audio_connection.py rename to tests/test_audio_connection.py index 29d8194c..5f8c7003 100644 --- a/tests/v2/test_audio_connection.py +++ b/tests/test_audio_connection.py @@ -9,8 +9,8 @@ import pytest -from dashscope.v2.client import Client -from dashscope.v2.errors import DashScopeError, WsBinaryOwnerConflictError +from dashscope.client import Client +from dashscope.errors import DashScopeError, WsBinaryOwnerConflictError BASE_URL = "https://api.test.local/api/v1" WS_URL = "wss://ws.test.local/api-ws/v1/inference" diff --git a/tests/v2/test_audio_management.py b/tests/test_audio_management.py similarity index 99% rename from tests/v2/test_audio_management.py rename to tests/test_audio_management.py index 3d4700b8..54106a93 100644 --- a/tests/v2/test_audio_management.py +++ b/tests/test_audio_management.py @@ -17,8 +17,8 @@ import httpx import pytest -from dashscope.v2 import Client -from dashscope.v2.errors import ( +from dashscope import Client +from dashscope.errors import ( APIConnectionError, APIStatusError, APITimeoutError, diff --git a/tests/v2/test_audio_omni_session.py b/tests/test_audio_omni_session.py similarity index 99% rename from tests/v2/test_audio_omni_session.py rename to tests/test_audio_omni_session.py index 380c46b2..a49fdf73 100644 --- a/tests/v2/test_audio_omni_session.py +++ b/tests/test_audio_omni_session.py @@ -15,14 +15,14 @@ WebSocketTimeoutException, ) -from dashscope.v2 import Client -from dashscope.v2.errors import ( +from dashscope import Client +from dashscope.errors import ( APIConnectionError, BadRequestError, RateLimitError, ) -from dashscope.v2.resources.audio import realtime -from dashscope.v2.transport._ident import sdk_headers +from dashscope.resources.audio import realtime +from dashscope.transport._ident import sdk_headers class FakeWebSocket: diff --git a/tests/v2/test_audio_samples.py b/tests/test_audio_samples.py similarity index 99% rename from tests/v2/test_audio_samples.py rename to tests/test_audio_samples.py index d4d30f21..d4f7f419 100644 --- a/tests/v2/test_audio_samples.py +++ b/tests/test_audio_samples.py @@ -16,7 +16,7 @@ import pytest from dashscope import Client -from dashscope.v2.errors import DashScopeError +from dashscope.errors import DashScopeError def _sample(name): diff --git a/tests/v2/test_audio_samples_direct_call.py b/tests/test_audio_samples_direct_call.py similarity index 99% rename from tests/v2/test_audio_samples_direct_call.py rename to tests/test_audio_samples_direct_call.py index 88485eb9..f813d4fd 100644 --- a/tests/v2/test_audio_samples_direct_call.py +++ b/tests/test_audio_samples_direct_call.py @@ -35,7 +35,7 @@ def _direct(name): SAMPLES = ( - Path(__file__).resolve().parents[2] + Path(__file__).resolve().parents[1] / "examples" / "01_atomic_capabilities" / "audio" diff --git a/tests/v2/test_audio_tts_http.py b/tests/test_audio_tts_http.py similarity index 99% rename from tests/v2/test_audio_tts_http.py rename to tests/test_audio_tts_http.py index 8737a3ea..4628fd2c 100644 --- a/tests/v2/test_audio_tts_http.py +++ b/tests/test_audio_tts_http.py @@ -13,8 +13,8 @@ import httpx import pytest -from dashscope.v2 import Client -from dashscope.v2.errors import ( +from dashscope import Client +from dashscope.errors import ( APIConnectionError, APIStatusError, APITimeoutError, @@ -731,7 +731,7 @@ def test_http_stream_close_before_first_chunk_releases_response( def test_http_stream_close_failure_still_releases_response(): - from dashscope.v2.resources.audio.http_tts import ( + from dashscope.resources.audio.http_tts import ( HttpSynthesisChunk, HttpSynthesisStream, ) diff --git a/tests/v2/test_audio_tts_realtime.py b/tests/test_audio_tts_realtime.py similarity index 99% rename from tests/v2/test_audio_tts_realtime.py rename to tests/test_audio_tts_realtime.py index fa29637a..3955a55b 100644 --- a/tests/v2/test_audio_tts_realtime.py +++ b/tests/test_audio_tts_realtime.py @@ -15,13 +15,13 @@ WebSocketTimeoutException, ) -from dashscope.v2 import Client -from dashscope.v2.errors import ( +from dashscope import Client +from dashscope.errors import ( BadRequestError, DashScopeError, RateLimitError, ) -from dashscope.v2.resources.audio._callbacks import run_with_callback +from dashscope.resources.audio._callbacks import run_with_callback def _make_client(**kwargs): diff --git a/tests/v2/test_audio_tts_session.py b/tests/test_audio_tts_session.py similarity index 98% rename from tests/v2/test_audio_tts_session.py rename to tests/test_audio_tts_session.py index 34b0875b..5ae253a7 100644 --- a/tests/v2/test_audio_tts_session.py +++ b/tests/test_audio_tts_session.py @@ -14,9 +14,13 @@ import pytest -from dashscope.v2 import Client -from dashscope.v2.errors import APIConnectionError, BadRequestError, WsStateError -from dashscope.v2.resources.audio.tts import SynthesisSession +from dashscope import Client +from dashscope.errors import ( + APIConnectionError, + BadRequestError, + WsStateError, +) +from dashscope.resources.audio.tts import SynthesisSession def _event(name, payload=None, task_id="t"): @@ -99,7 +103,7 @@ def test_multiple_text_flush_stop_and_raw_events(client, monkeypatch): ws = FakeWebSocket([started, result, b"first", b"second", finished]) clock = iter([10.0, 10.25]) monkeypatch.setattr( - "dashscope.v2.resources.audio.tts.time.monotonic", + "dashscope.resources.audio.tts.time.monotonic", lambda: next(clock), ) session = _session(client, ws) @@ -468,7 +472,7 @@ def interrupted_run(*args, **kwargs): raise error monkeypatch.setattr( - "dashscope.v2.resources.audio.tts.WsSession.run_task", + "dashscope.resources.audio.tts.WsSession.run_task", interrupted_run, ) try: diff --git a/tests/v2/test_audio_ws_integration.py b/tests/test_audio_ws_integration.py similarity index 99% rename from tests/v2/test_audio_ws_integration.py rename to tests/test_audio_ws_integration.py index fcdf9fda..881e1ac8 100644 --- a/tests/v2/test_audio_ws_integration.py +++ b/tests/test_audio_ws_integration.py @@ -17,13 +17,13 @@ from aiohttp import WSMsgType, web from websocket import WebSocketConnectionClosedException -from dashscope.v2 import Client -from dashscope.v2.errors import ( +from dashscope import Client +from dashscope.errors import ( APIConnectionError, BadRequestError, RateLimitError, ) -from dashscope.v2.transport.ws import WsEventError +from dashscope.transport.ws import WsEventError TIMEOUT = 2 diff --git a/tests/v2/test_base_url_routing.py b/tests/test_base_url_routing.py similarity index 98% rename from tests/v2/test_base_url_routing.py rename to tests/test_base_url_routing.py index c656c043..bd730e67 100644 --- a/tests/v2/test_base_url_routing.py +++ b/tests/test_base_url_routing.py @@ -18,7 +18,7 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client +from dashscope import AsyncClient, Client # Default routes use the workspace-scoped MaaS gateway when a workspace # is configured; without one (and no explicit base_url) they fall back to @@ -71,7 +71,7 @@ @pytest.fixture def clean_env(monkeypatch): - from dashscope.v2._config import settings + from dashscope._config import settings for name in ( "DASHSCOPE_API_KEY", diff --git a/tests/v2/test_chat.py b/tests/test_chat.py similarity index 99% rename from tests/v2/test_chat.py rename to tests/test_chat.py index 0bc51ed4..dfec3460 100644 --- a/tests/v2/test_chat.py +++ b/tests/test_chat.py @@ -12,9 +12,9 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import ContentFilterError, RateLimitError -from dashscope.v2.types.completions import parse_completion +from dashscope import AsyncClient, Client +from dashscope.errors import ContentFilterError, RateLimitError +from dashscope.types.completions import parse_completion BASE_URL = "https://api.test.local/api/v1" CHAT_PATH = "/api/v1/services/aigc/text-generation/generation" @@ -1543,7 +1543,7 @@ def fake_upload(client, model, content, **kwargs): return True, "oss://bucket/cat.png", {} monkeypatch.setattr( - "dashscope.v2.resources._oss.check_and_upload_local", + "dashscope.resources._oss.check_and_upload_local", fake_upload, ) captured = {} diff --git a/tests/v2/test_chat_params.py b/tests/test_chat_params.py similarity index 96% rename from tests/v2/test_chat_params.py rename to tests/test_chat_params.py index 9f85ce6e..687a9a10 100644 --- a/tests/v2/test_chat_params.py +++ b/tests/test_chat_params.py @@ -11,7 +11,7 @@ import pytest -from dashscope.v2.resources.chat._params import ( +from dashscope.resources.chat._params import ( should_modify_incremental_output, ) diff --git a/tests/v2/test_config.py b/tests/test_config.py similarity index 98% rename from tests/v2/test_config.py rename to tests/test_config.py index b1df7548..d4a33645 100644 --- a/tests/v2/test_config.py +++ b/tests/test_config.py @@ -9,8 +9,8 @@ import pytest -from dashscope.v2 import AsyncClient, Client, Config -from dashscope.v2._config import ( +from dashscope import AsyncClient, Client, Config +from dashscope._config import ( DEFAULT_MAX_RETRIES, DEFAULT_PROTOCOL, DEFAULT_REGION, @@ -36,7 +36,7 @@ @pytest.fixture def clean_env(monkeypatch, tmp_path): - from dashscope.v2._config import settings + from dashscope._config import settings for name in ( "DASHSCOPE_API_KEY", @@ -452,7 +452,7 @@ def test_api_key_env_and_config_beat_key_file( def test_save_api_key_roundtrip(clean_env, tmp_path, monkeypatch): - from dashscope.v2 import save_api_key + from dashscope import save_api_key key_file = tmp_path / "nested" / "api_key" save_api_key("sk-saved", str(key_file)) @@ -467,7 +467,7 @@ def test_save_api_key_writes_the_default_path( tmp_path, monkeypatch, ): - from dashscope.v2 import _config + from dashscope import _config default = tmp_path / "home" / ".dashscope" / "api_key" monkeypatch.setattr(_config, "_DEFAULT_API_KEY_FILE_PATH", str(default)) @@ -487,7 +487,7 @@ def test_save_api_key_accepts_a_bare_filename( tmp_path, monkeypatch, ): - from dashscope.v2 import save_api_key + from dashscope import save_api_key # 1.x does os.makedirs(os.path.dirname(path)), which raises when the # path has no directory component; the v2 writer resolves against cwd. @@ -585,7 +585,7 @@ def test_invalid_numeric_env(clean_env, monkeypatch): def test_settings_read_env_lazily(clean_env, monkeypatch): - from dashscope.v2._config import settings + from dashscope._config import settings monkeypatch.setenv("DASHSCOPE_PROTOCOL", "anthropic") monkeypatch.setenv("DASHSCOPE_TIMEOUT", "9") diff --git a/tests/v2/test_decision.py b/tests/test_decision.py similarity index 98% rename from tests/v2/test_decision.py rename to tests/test_decision.py index e5f87ab1..1a59de5a 100644 --- a/tests/v2/test_decision.py +++ b/tests/test_decision.py @@ -19,14 +19,14 @@ import pytest from pydantic import ValidationError -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import BadRequestError, RateLimitError -from dashscope.v2.resources.decision import ( +from dashscope import AsyncClient, Client +from dashscope.errors import BadRequestError, RateLimitError +from dashscope.resources.decision import ( ChoiceQuestion, NoulQuestion, ScoreQuestion, ) -from dashscope.v2.transport._ident import sdk_client_value +from dashscope.transport._ident import sdk_client_value BASE_URL = "https://api.test.local/api/v1" _WS_HOST = "https://ws-123.cn-beijing.maas.aliyuncs.com" @@ -83,7 +83,7 @@ def _make_async_client(handler, **kwargs): @pytest.fixture def clean_env(monkeypatch): - from dashscope.v2._config import settings + from dashscope._config import settings for name in ( "DASHSCOPE_API_KEY", diff --git a/tests/v2/test_error_registry.py b/tests/test_error_registry.py similarity index 96% rename from tests/v2/test_error_registry.py rename to tests/test_error_registry.py index 0a980aeb..6e773d79 100644 --- a/tests/v2/test_error_registry.py +++ b/tests/test_error_registry.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Integrity tests for the vendored error catalog. -``dashscope/v2/_error_registry.py`` is a generated artifact from the +``dashscope/_error_registry.py`` is a generated artifact from the separate ``dashscope-error-registry`` repo (``tools/generate.py`` over ``data/*.textproto``), vendored at ``__version__ == "1.0.11"``. Only the ``PublicError`` and ``SdkErrorDef`` tables are carried; see @@ -12,8 +12,8 @@ upstream regeneration surfaces as a test failure instead of silent drift. -Nothing here asserts how ``dashscope/v2/errors.py`` maps the catalog; -that coupling lives in ``tests/v2/test_errors.py``, so this file keeps +Nothing here asserts how ``dashscope/errors.py`` maps the catalog; +that coupling lives in ``tests/test_errors.py``, so this file keeps depending on the generated artifact alone. """ @@ -26,7 +26,7 @@ from dashscope.agentstudio import exceptions as agentstudio_exceptions from dashscope.finetune.reinforcement.common import errors as rl_errors -from dashscope.v2 import _error_registry as registry +from dashscope import _error_registry as registry PUBLIC = registry.PUBLIC_ERRORS SDK = registry.SDK_ERRORS @@ -298,7 +298,7 @@ def test_unfamily_entries_are_the_generic_sdk_ones(): e.name for e in SDK if not e.name.startswith( - ("sdk.agentic_rl.", "sdk.agentstudio.", "sdk.ws.") + ("sdk.agentic_rl.", "sdk.agentstudio.", "sdk.ws."), ) ] assert others == [ @@ -328,7 +328,7 @@ def test_ws_family_entries(): } assert {e.name for e in SDK if e.name.startswith("sdk.ws.")} == expected - from dashscope.v2 import errors as v2_errors + from dashscope import errors as v2_errors wired = { "sdk.ws.ProtocolError": "WsEventError", diff --git a/tests/v2/test_errors.py b/tests/test_errors.py similarity index 98% rename from tests/v2/test_errors.py rename to tests/test_errors.py index 4568e6a8..3a8cf71c 100644 --- a/tests/v2/test_errors.py +++ b/tests/test_errors.py @@ -5,7 +5,7 @@ Two things are pinned here. First, the runtime behaviour of the mapping functions (which class a given status / wire code produces). Second, the coupling to the generated catalog in -``dashscope/v2/_error_registry.py``: exception class names, their +``dashscope/_error_registry.py``: exception class names, their ``status_code``, and the wire-code crosswalk must all be derivable from it, so an upstream regeneration shows up as a test failure instead of silent drift. @@ -18,10 +18,10 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2 import _error_registry as registry -from dashscope.v2 import errors -from dashscope.v2.errors import ( +from dashscope import AsyncClient, Client +from dashscope import _error_registry as registry +from dashscope import errors +from dashscope.errors import ( APIConnectionError, APIStatusError, APITimeoutError, @@ -47,14 +47,14 @@ error_from_sdk_exception, sdk_error, ) -from dashscope.v2.errors import ( +from dashscope.errors import ( _CODE_MAP, _REFINED_CLASS, _SDK_ERRORS, _STATUS_MAP, _WIRE_TO_PUBLIC, ) -from dashscope.v2.types.common import JobStatus, ResponseBase, response_body +from dashscope.types.common import JobStatus, ResponseBase, response_body def _resp(status, body=None, headers=None): @@ -543,9 +543,9 @@ async def test_public_http_error_propagates_and_closes( text, payload, ): - monkeypatch.setattr("dashscope.v2._config._load_config_file", lambda _: {}) + monkeypatch.setattr("dashscope._config._load_config_file", lambda _: {}) monkeypatch.setattr( - "dashscope.v2._config._api_key_from_file", + "dashscope._config._api_key_from_file", lambda: None, ) wire = _ErrorBodyStream(text) @@ -699,9 +699,7 @@ def test_response_error_is_sdk_side_only(): assert issubclass(ResponseError, APIStatusError) assert ResponseError.status_code == 500 assert ResponseError not in _STATUS_MAP.values() - assert ResponseError not in { - cls for cls in _CODE_MAP.values() - } + assert ResponseError not in {cls for cls in _CODE_MAP.values()} @pytest.mark.parametrize( diff --git a/tests/v2/test_files_models.py b/tests/test_files_models.py similarity index 98% rename from tests/v2/test_files_models.py rename to tests/test_files_models.py index ee76334d..a1e14bbf 100644 --- a/tests/v2/test_files_models.py +++ b/tests/test_files_models.py @@ -7,8 +7,8 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import BadRequestError, NotFoundError, SdkError +from dashscope import AsyncClient, Client +from dashscope.errors import BadRequestError, NotFoundError, SdkError BASE_URL = "https://api.test.local/api/v1" @@ -112,7 +112,7 @@ def handler(request): }, ) - from dashscope.v2.transport import http as transport_http + from dashscope.transport import http as transport_http monkeypatch.setattr(transport_http, "_sleep", lambda seconds: None) file_path = tmp_path / "t.jsonl" diff --git a/tests/v2/test_finetune_deployments.py b/tests/test_finetune_deployments.py similarity index 99% rename from tests/v2/test_finetune_deployments.py rename to tests/test_finetune_deployments.py index afc3238f..bb08d7dd 100644 --- a/tests/v2/test_finetune_deployments.py +++ b/tests/test_finetune_deployments.py @@ -12,13 +12,13 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import ( +from dashscope import AsyncClient, Client +from dashscope.errors import ( BadRequestError, DashScopeError, RateLimitError, ) -from dashscope.v2.resources.fine_tunes import FineTune +from dashscope.resources.fine_tunes import FineTune BASE_URL = "https://api.test.local/api/v1" diff --git a/tests/v2/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py similarity index 72% rename from tests/v2/test_finetune_rl_shim.py rename to tests/test_finetune_rl_shim.py index 361a666f..bc68339b 100644 --- a/tests/v2/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Reverse-reference shims and the submit transport for the RL move. - -``dashscope.reinforcement`` is the public compatibility package, with -``dashscope.finetune.reinforcement`` retained as its legacy alias. Both -resolve over ``dashscope.v2.finetune``. The paths have to keep resolving -*by name* in processes this repo does not control -- a Function Compute -sandbox running ``FC_SERVER_CLASSPATH``, and ``uvicorn`` / ``python -m`` -resolving the reward-server import string -- so what is pinned here is -object identity, laziness and the wire format, not just the happy path. +"""The finetune RL implementation and its 1.x compatibility surface. + +``dashscope.finetune.reinforcement`` and ``dashscope.finetune.agentic_rl`` +own the 2.0 workflow. ``dashscope.legacy_1x.finetune.agentic_rl`` keeps the +1.x return types and job verbs. The tests also pin Function Compute, +``uvicorn`` / ``python -m``, lazy optional dependencies and the submit wire +format. """ from __future__ import annotations @@ -28,45 +26,25 @@ import dashscope from dashscope._shared import _sdk_ident from dashscope._shared._sdk_ident import set_sdk_client -from dashscope.finetune.agentic_rl import AgenticRL as LegacyAgenticRL +from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL as LegacyAgenticRL from dashscope.version import __version__ -from dashscope.v2.errors import SdkError -from dashscope.v2.finetune import _submit -from dashscope.v2.finetune.agentic_rl import AgenticRL as V2AgenticRL -from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult -from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.v2.finetune.reinforcement import TrainingType -from dashscope.v2.finetune.reinforcement.common.errors import ( +from dashscope.errors import SdkError +from dashscope.finetune import _submit +from dashscope.finetune.agentic_rl import AgenticRL as V2AgenticRL +from dashscope.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.finetune.reinforcement import TrainingType +from dashscope.finetune.reinforcement.common.errors import ( _LEGACY_ERROR_CODES, AgenticRLError, RuntimeErrorWithCode, ValueErrorWithCode, ) -from dashscope.v2.resources import reinforcement_learning as _rl_mount - -PUBLIC = "dashscope.reinforcement" -LEGACY = "dashscope.finetune.reinforcement" -V2 = "dashscope.v2.finetune.reinforcement" -ALIASES = [PUBLIC, LEGACY] - -# The modules the package imports eagerly, so all spellings are already -# in sys.modules before anything asks for them. The nine that are *not* -# here (component.server.* and component.observability.genai.*) are the -# reason the shim is a meta_path finder rather than an eager walk. -EAGER_SUBMODULES = [ - "common.constants", - "common.errors", - "common.log", - "common.model", - "common.model_types", - "common.utils", - "component.data.base_data_model", - "component.func_decorator", - "component.func_manager", - "component.observability.tracing", - "component.parser.reward_parser", - "component.processor.abstract_reward_processor", -] +from dashscope.resources import reinforcement_learning as _rl_mount + +PUBLIC = "dashscope.finetune.reinforcement" +V2 = PUBLIC +PATHS = [PUBLIC] ENVELOPE = { "status_code": 200, @@ -109,7 +87,7 @@ def check_function_names(self): def _stub(cls, api_key="sk-submit"): # ``set_api_key`` writes DASHSCOPE_API_KEY for real; these tests are # about the submit call, not about credential discovery. - with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): + with patch("dashscope.finetune.agentic_rl.set_api_key"): instance = cls(api_key=api_key) instance.tuning = _StubTuning() return instance @@ -159,17 +137,7 @@ def post(self, url, data=None, headers=None, timeout=None): return calls -# -- identity ------------------------------------------------------------- - - -@pytest.mark.parametrize("alias", ALIASES) -def test_every_exported_name_is_the_same_object(alias): - compatible = importlib.import_module(alias) - v2 = importlib.import_module(V2) - assert v2.__all__ - assert compatible.__all__ == list(v2.__all__) + ["app"] - for name in v2.__all__: - assert getattr(compatible, name) is getattr(v2, name), name +# -- the two AgenticRL surfaces ------------------------------------------- def test_legacy_agentic_rl_subclasses_the_v2_one(): @@ -184,34 +152,15 @@ def test_only_the_1x_surface_keeps_the_job_verbs(): assert verb in LegacyAgenticRL.__dict__, verb -@pytest.mark.parametrize("alias", ALIASES) -@pytest.mark.parametrize("submodule", EAGER_SUBMODULES) -def test_eager_deep_imports_alias_to_the_same_module(alias, submodule): - compatible = importlib.import_module(f"{alias}.{submodule}") - v2 = importlib.import_module(f"{V2}.{submodule}") - assert compatible is v2 - # The shared object keeps reporting the v2 name, so pickling and - # reload resolve against the sys.modules key that owns it. - assert compatible.__name__ == f"{V2}.{submodule}" - assert sys.modules[f"{alias}.{submodule}"] is v2 - - -@pytest.mark.parametrize("alias", ALIASES) -def test_deep_import_works_as_an_attribute_expression(alias): - compatible = importlib.import_module(alias) - v2_utils = importlib.import_module(f"{V2}.common.utils") - assert compatible.common.utils.set_api_key is v2_utils.set_api_key - - # -- laziness ------------------------------------------------------------- -@pytest.mark.parametrize("alias", ALIASES) +@pytest.mark.parametrize("alias", PATHS) def test_server_module_resolves_without_importing_fastapi(alias): submit_path = f"{alias}.component.server.server" spec = importlib.util.find_spec(submit_path) assert spec is not None - assert "dashscope/v2/finetune/reinforcement" in spec.origin.replace( + assert "dashscope/finetune/reinforcement" in spec.origin.replace( "\\", "/", ) @@ -219,7 +168,7 @@ def test_server_module_resolves_without_importing_fastapi(alias): assert "fastapi" not in sys.modules -@pytest.mark.parametrize("alias", ALIASES) +@pytest.mark.parametrize("alias", PATHS) def test_python_dash_m_resolves_the_compatibility_server_path(alias): # ``runpy`` refuses a loader that yields no code object, which is how # examples/workspace/scripts/start_local_*.sh launches the server. @@ -238,20 +187,17 @@ def test_genai_stays_behind_its_optional_dependency_on_all_paths(): # ``genai/__init__`` imports ``llm_dashscope`` eagerly, which needs # opentelemetry. All spellings must fail identically rather than one # quietly succeeding through the alias. - for alias in ALIASES + [V2]: + for alias in [V2]: name = f"{alias}.component.observability.genai.llm_dashscope" with pytest.raises(ModuleNotFoundError) as ei: importlib.import_module(name) assert "opentelemetry" in str(ei.value) -def test_app_is_reachable_only_from_the_compatibility_paths(): - v2 = importlib.import_module(V2) - assert not hasattr(v2, "app") - +def test_app_is_reachable_lazily_from_the_public_path(): from dashscope.legacy_1x.cli.agentic_rl import app as cli_app - for alias in ALIASES: + for alias in PATHS: assert importlib.import_module(alias).app is cli_app @@ -327,7 +273,7 @@ def test_sdk_header_kill_switch_still_applies(monkeypatch, post_calls): def test_session_id_is_one_value_across_both_stacks(): from dashscope.legacy_1x.common.utils import get_sdk_headers - from dashscope.v2.transport._ident import sdk_headers + from dashscope.transport._ident import sdk_headers legacy = get_sdk_headers("finetune")["x-dashscope-sdk-session-id"] v2 = sdk_headers("finetune")["x-dashscope-sdk-session-id"] @@ -361,9 +307,9 @@ def test_submit_falls_back_to_the_environment(monkeypatch, post_calls): # -- the two return types ------------------------------------------------- -def test_v2_submit_returns_submit_result_and_feeds_to_job(): +def test_current_submit_returns_submit_result_and_feeds_to_job(): with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=dict(ENVELOPE), ): result = _stub(V2AgenticRL).submit_job() @@ -384,10 +330,10 @@ def test_v2_submit_returns_submit_result_and_feeds_to_job(): def test_legacy_submit_returns_the_finetune_dataclass(): - from dashscope.finetune.customize_types import FineTune + from dashscope.legacy_1x.finetune.customize_types import FineTune with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=dict(ENVELOPE), ): result = _stub(LegacyAgenticRL).submit_job() @@ -409,7 +355,7 @@ def test_a_failed_submit_is_an_envelope_not_an_exception(): "headers": {}, } with patch( - "dashscope.v2.finetune._submit.post_submit", + "dashscope.finetune._submit.post_submit", return_value=dict(failure), ): result = _stub(V2AgenticRL).submit_job() @@ -422,25 +368,26 @@ def test_a_failed_submit_is_an_envelope_not_an_exception(): # -- single implementation ------------------------------------------------ -@pytest.mark.parametrize("alias", ALIASES) -def test_compatibility_package_directories_hold_only_shims(alias): - compatible = importlib.import_module(alias) - package_dir = pathlib.Path(compatible.__file__).parent - assert [p.name for p in package_dir.rglob("*.py")] == ["__init__.py"] +def test_public_package_contains_the_unique_implementation(): + public = importlib.import_module(PUBLIC) + package_dir = pathlib.Path(public.__file__).parent + assert package_dir.name == "reinforcement" + assert package_dir.parent.name == "finetune" + assert len(list(package_dir.rglob("*.py"))) > 1 + assert importlib.util.find_spec("dashscope.reinforcement") is None + assert not (package_dir.parent.parent / "agentic_rl.py").exists() def test_the_v2_mount_points_at_the_v2_workflow_modules(): assert _rl_mount._agentic_rl is importlib.import_module( - "dashscope.v2.finetune.agentic_rl", + "dashscope.finetune.agentic_rl", ) assert _rl_mount._reinforcement is importlib.import_module(V2) -def test_the_rl_error_module_is_shared_by_all_paths(): +def test_the_rl_error_module_uses_the_public_path(): public_errors = importlib.import_module(f"{PUBLIC}.common.errors") - legacy_errors = importlib.import_module(f"{LEGACY}.common.errors") - v2_errors = importlib.import_module(f"{V2}.common.errors") - assert public_errors is legacy_errors is v2_errors + assert public_errors.__name__ == f"{PUBLIC}.common.errors" assert public_errors.ValueErrorWithCode is ValueErrorWithCode diff --git a/tests/v2/test_http_pool.py b/tests/test_http_pool.py similarity index 97% rename from tests/v2/test_http_pool.py rename to tests/test_http_pool.py index f666031b..9b3dc8ad 100644 --- a/tests/v2/test_http_pool.py +++ b/tests/test_http_pool.py @@ -7,8 +7,8 @@ import pytest -from dashscope.v2._config import resolve_config, settings -from dashscope.v2.client import Client +from dashscope._config import resolve_config, settings +from dashscope.client import Client @pytest.fixture diff --git a/tests/v2/test_images_videos.py b/tests/test_images_videos.py similarity index 99% rename from tests/v2/test_images_videos.py rename to tests/test_images_videos.py index a7dc46eb..00bcba7e 100644 --- a/tests/v2/test_images_videos.py +++ b/tests/test_images_videos.py @@ -12,7 +12,7 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client +from dashscope import AsyncClient, Client BASE_URL = "https://api.test.local/api/v1" IMAGE_PATH = "/api/v1/services/aigc/text2image/image-synthesis" @@ -671,7 +671,7 @@ def fake_upload(client, model, content, **kwargs): return True, "oss://bucket/ref.png", {} monkeypatch.setattr( - "dashscope.v2.resources._oss.check_and_upload_local", + "dashscope.resources._oss.check_and_upload_local", fake_upload, ) captured = {} diff --git a/tests/v2/test_jobs.py b/tests/test_jobs.py similarity index 98% rename from tests/v2/test_jobs.py rename to tests/test_jobs.py index bdf2c536..a720070d 100644 --- a/tests/v2/test_jobs.py +++ b/tests/test_jobs.py @@ -13,9 +13,9 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import DashScopeError -from dashscope.v2.resources._jobs import AsyncJobs, Job, JobList, Jobs +from dashscope import AsyncClient, Client +from dashscope.errors import DashScopeError +from dashscope.resources._jobs import AsyncJobs, Job, JobList, Jobs BASE_URL = "https://api.test.local/api/v1" @@ -236,7 +236,7 @@ def test_wait_times_out(monkeypatch, wait_clock): def handler(request): return httpx.Response(200, json=_job_body("RUNNING")) - monkeypatch.setattr("dashscope.v2.resources._jobs.time", wait_clock) + monkeypatch.setattr("dashscope.resources._jobs.time", wait_clock) client = _make_client(handler) jobs = Jobs(client, "aigc") with pytest.raises(DashScopeError, match="timed out"): @@ -666,7 +666,7 @@ def handler(request): assert wait_clock.monotonic() == 10.5 return httpx.Response(200, json=_job_body("SUCCEEDED")) - monkeypatch.setattr("dashscope.v2.resources._jobs.time", wait_clock) + monkeypatch.setattr("dashscope.resources._jobs.time", wait_clock) client = ( _make_async_client(handler, raise_on_error=raise_on_error) if asynchronous @@ -722,7 +722,7 @@ def handler(request): clock_module = "audio.asr" if modality == "asr" else "_jobs" monkeypatch.setattr( - f"dashscope.v2.resources.{clock_module}.time", + f"dashscope.resources.{clock_module}.time", wait_clock, ) client = ( @@ -777,7 +777,7 @@ def handler(request): requests.append(request) return httpx.Response(200, json=_job_body("SUCCEEDED")) - monkeypatch.setattr("dashscope.v2.resources.audio.asr.time", wait_clock) + monkeypatch.setattr("dashscope.resources.audio.asr.time", wait_clock) client = ( _make_async_client(handler, raise_on_error=raise_on_error) if asynchronous diff --git a/tests/v2/test_logging.py b/tests/test_logging.py similarity index 79% rename from tests/v2/test_logging.py rename to tests/test_logging.py index 38b67040..f3b88ae8 100644 --- a/tests/v2/test_logging.py +++ b/tests/test_logging.py @@ -13,20 +13,20 @@ import httpx import pytest -from dashscope.v2 import _logging as logging_mod -from dashscope.v2._logging import ( +from dashscope import _logging as logging_mod +from dashscope._logging import ( REDACTED, SensitiveHeadersFilter, get_logger, redact_headers, redact_text, ) -from dashscope.v2.transport import SyncTransport +from dashscope.transport import SyncTransport @pytest.fixture def reset_logging(monkeypatch): - root = logging.getLogger("dashscope.v2") + root = logging.getLogger(logging_mod.ROOT_NAME) monkeypatch.setattr(logging_mod, "_configured", False) yield root.filters.clear() @@ -81,7 +81,7 @@ def test_filter_redacts_record(reset_logging): def test_env_enables_debug_logging(reset_logging, monkeypatch): monkeypatch.setenv("DASHSCOPE_LOG", "debug") logger = get_logger("transport") - root = logging.getLogger("dashscope.v2") + root = logging.getLogger(logging_mod.ROOT_NAME) assert root.level == logging.DEBUG assert len(root.handlers) == 1 assert isinstance( @@ -89,13 +89,13 @@ def test_env_enables_debug_logging(reset_logging, monkeypatch): SensitiveHeadersFilter, ) assert isinstance(logger.filters[0], SensitiveHeadersFilter) - assert logger.name == "dashscope.v2.transport" + assert logger.name == f"{logging_mod.ROOT_NAME}.transport" def test_env_invalid_level_no_handler(reset_logging, monkeypatch): monkeypatch.setenv("DASHSCOPE_LOG", "verbose") logger = get_logger("transport") - root = logging.getLogger("dashscope.v2") + root = logging.getLogger(logging_mod.ROOT_NAME) assert root.level == logging.NOTSET assert root.handlers == [] assert root.filters == [] @@ -107,20 +107,38 @@ def test_configured_once(reset_logging, monkeypatch): get_logger("a") get_logger("a") get_logger("b") - root = logging.getLogger("dashscope.v2") + root = logging.getLogger(logging_mod.ROOT_NAME) assert len(root.handlers) == 1 - for name in ("dashscope.v2.a", "dashscope.v2.b"): - child = logging.getLogger(name) + for name in ("a", "b"): + child = logging.getLogger(f"{logging_mod.ROOT_NAME}.{name}") assert len(child.filters) == 1 +def test_configuration_does_not_mutate_legacy_root_logger( + reset_logging, + monkeypatch, +): + legacy_root = logging.getLogger("dashscope") + level = legacy_root.level + handlers = list(legacy_root.handlers) + filters = list(legacy_root.filters) + + monkeypatch.setenv("DASHSCOPE_LOG", "debug") + get_logger("transport") + + assert logging_mod.ROOT_NAME != "dashscope" + assert legacy_root.level == level + assert legacy_root.handlers == handlers + assert legacy_root.filters == filters + + def test_transport_logs_retry_and_request_id( reset_logging, monkeypatch, caplog, ): monkeypatch.setattr( - "dashscope.v2.transport.http._sleep", + "dashscope.transport.http._sleep", lambda s: None, ) calls = {"n": 0} @@ -143,7 +161,7 @@ def handler(request): base_url="https://api.test.local", http_client=http_client, ) - with caplog.at_level(logging.DEBUG, logger="dashscope.v2"): + with caplog.at_level(logging.DEBUG, logger=logging_mod.ROOT_NAME): resp = t.request("POST", "/chat") assert resp.status_code == 200 messages = [r.getMessage() for r in caplog.records] @@ -158,7 +176,7 @@ def test_child_logger_output_redacted_via_sdk_handler( ): monkeypatch.setenv("DASHSCOPE_LOG", "debug") logger = get_logger("transport") - root = logging.getLogger("dashscope.v2") + root = logging.getLogger(logging_mod.ROOT_NAME) stream = io.StringIO() root.handlers[0].stream = stream logger.info("token: Bearer sk-abcdef1234567890") @@ -174,7 +192,7 @@ def test_child_logger_output_redacted_via_user_handler( # No DASHSCOPE_LOG: records propagate to the user's own handlers # (e.g. logging.basicConfig) and must still be redacted. logger = get_logger("transport") - with caplog.at_level(logging.DEBUG, logger="dashscope.v2"): + with caplog.at_level(logging.DEBUG, logger=logging_mod.ROOT_NAME): logger.info("token: Bearer sk-abcdef1234567890") messages = [r.getMessage() for r in caplog.records] assert any("Bearer " + REDACTED in m for m in messages) diff --git a/tests/v2/test_multimodal.py b/tests/test_multimodal.py similarity index 99% rename from tests/v2/test_multimodal.py rename to tests/test_multimodal.py index 54cb41e4..80c8c404 100644 --- a/tests/v2/test_multimodal.py +++ b/tests/test_multimodal.py @@ -12,7 +12,7 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client +from dashscope import AsyncClient, Client BASE_URL = "https://api.test.local/api/v1" MULTIMODAL_PATH = "/api/v1/services/aigc/multimodal-generation/generation" diff --git a/tests/v2/test_oss_upload.py b/tests/test_oss_upload.py similarity index 98% rename from tests/v2/test_oss_upload.py rename to tests/test_oss_upload.py index b75c190a..5007ed15 100644 --- a/tests/v2/test_oss_upload.py +++ b/tests/test_oss_upload.py @@ -12,9 +12,9 @@ import httpx import pytest -from dashscope.v2 import Client -from dashscope.v2.errors import AuthenticationError, PermissionDeniedError -from dashscope.v2.resources import _oss_upload +from dashscope import Client +from dashscope.errors import AuthenticationError, PermissionDeniedError +from dashscope.resources import _oss_upload BASE_URL = "https://api.test.local/api/v1" OSS_HOST = "https://dashscope-oss.oss-cn-beijing.aliyuncs.com" diff --git a/tests/v2/test_package_root.py b/tests/test_package_root.py similarity index 58% rename from tests/v2/test_package_root.py rename to tests/test_package_root.py index 7cd65572..005ea28c 100644 --- a/tests/v2/test_package_root.py +++ b/tests/test_package_root.py @@ -13,9 +13,9 @@ import pytest import dashscope -from dashscope.v2 import _quickstart -from dashscope.v2 import AsyncClient as V2AsyncClient -from dashscope.v2 import Client as V2Client +from dashscope import _config, _quickstart +from dashscope.client import AsyncClient as RootAsyncClient +from dashscope.client import Client as RootClient BASE_URL = "https://api.test.local/api/v1" @@ -33,12 +33,27 @@ } -def test_root_exports_are_v2(): - assert dashscope.Client is V2Client - assert dashscope.AsyncClient is V2AsyncClient +def test_root_exports_current_implementation(): + assert dashscope.Client is RootClient + assert dashscope.AsyncClient is RootAsyncClient + assert dashscope.Client.__module__ == "dashscope.client" assert callable(dashscope.chat) - for name in ("AsyncClient", "Client", "chat"): - assert name in dashscope.__all__ + for name in ( + "AsyncClient", + "Client", + "Config", + "DashScopeError", + "ErrorCode", + "Settings", + "WsConnection", + "WsTask", + "chat", + "errors", + "save_api_key", + "settings", + ): + assert hasattr(dashscope, name), name + assert name in dashscope.__all__, name def test_legacy_exports_untouched(): @@ -50,44 +65,44 @@ def test_legacy_exports_untouched(): assert hasattr(dashscope, "save_api_key") -def test_tokenizers_are_shared_between_stacks(): - # The local tiktoken wrapper is owned by 2.0; 1.x and the package root - # re-export the same objects, so there is one implementation and one - # copy of the bundled vocab. - from dashscope import v2 - from dashscope.tokenizers import get_tokenizer as get_1x - from dashscope.tokenizers import list_tokenizers as list_1x +def test_tokenizers_use_one_root_implementation(): + from dashscope.tokenizers import get_tokenizer, list_tokenizers - assert v2.get_tokenizer.__module__.startswith("dashscope.v2.tokenizers") - assert v2.get_tokenizer is get_1x - assert v2.list_tokenizers is list_1x - assert dashscope.get_tokenizer is v2.get_tokenizer + assert get_tokenizer.__module__ == "dashscope.tokenizers.tokenizer" + assert dashscope.get_tokenizer is get_tokenizer + assert dashscope.list_tokenizers is list_tokenizers for name in ("get_tokenizer", "list_tokenizers"): - assert name in v2.__all__ + assert name in dashscope.__all__ -def test_v2_owns_save_api_key(): - # The key-file writer is v2-native, so reaching for it does not pull in - # the 1.x package root; the package root keeps its own 1.x writer. - from dashscope import v2 +def test_root_uses_current_save_api_key(): from dashscope.legacy_1x.common.api_key import save_api_key as save_1x - assert v2.save_api_key.__module__ == "dashscope.v2._config" - assert v2.save_api_key is not save_1x - assert dashscope.save_api_key is save_1x - assert "save_api_key" in v2.__all__ + assert dashscope.save_api_key is _config.save_api_key + assert dashscope.save_api_key is not save_1x + assert dashscope.save_api_key.__module__ == "dashscope._config" + +def test_root_save_api_key_roundtrip(tmp_path): + target = tmp_path / "nested" / "api_key" + dashscope.save_api_key("sk-roundtrip", str(target)) + assert target.read_text(encoding="utf-8") == "sk-roundtrip" -def test_v2_get_tokenizer_encodes_locally(): - from dashscope import v2 - tokenizer = v2.get_tokenizer("qwen-turbo") +def test_root_get_tokenizer_encodes_locally(): + tokenizer = dashscope.get_tokenizer("qwen-turbo") ids = tokenizer.encode("hello world") assert ids and all(isinstance(i, int) for i in ids) assert tokenizer.decode(ids) == "hello world" - assert "qwen-turbo" in v2.list_tokenizers() + assert "qwen-turbo" in dashscope.list_tokenizers() with pytest.raises(ValueError): - v2.get_tokenizer("not-a-qwen-model") + dashscope.get_tokenizer("not-a-qwen-model") + + +def test_v2_namespace_is_removed(): + assert not hasattr(dashscope, "v2") + with pytest.raises(ModuleNotFoundError): + __import__("dashscope.v2") def test_chat_quickstart_uses_singleton_client(monkeypatch): diff --git a/tests/v2/test_protocols.py b/tests/test_protocols.py similarity index 99% rename from tests/v2/test_protocols.py rename to tests/test_protocols.py index 469af0cf..8aeec92e 100644 --- a/tests/v2/test_protocols.py +++ b/tests/test_protocols.py @@ -7,15 +7,15 @@ import httpx import pytest -from dashscope.v2 import Client -from dashscope.v2.errors import BadRequestError, DashScopeError -from dashscope.v2.protocols import ( +from dashscope import Client +from dashscope.errors import BadRequestError, DashScopeError +from dashscope.protocols import ( AnthropicProtocol, NativeProtocol, OpenAIProtocol, get_protocol, ) -from dashscope.v2.protocols.base import ChatRequest +from dashscope.protocols.base import ChatRequest BASE_URL = "https://api.test.local/api/v1" CHAT_PATH = "/api/v1/services/aigc/text-generation/generation" diff --git a/tests/v2/test_rl.py b/tests/test_rl.py similarity index 99% rename from tests/v2/test_rl.py rename to tests/test_rl.py index 619c823a..6a86b91d 100644 --- a/tests/v2/test_rl.py +++ b/tests/test_rl.py @@ -10,16 +10,16 @@ import httpx import pytest -from dashscope.v2.finetune import agentic_rl as agentic_rl_module +from dashscope.finetune import agentic_rl as agentic_rl_module from dashscope.finetune.reinforcement.common.errors import ( AgenticRLError, InstanceQueryError, OSSUploadError, RuntimeErrorWithCode, ) -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import DashScopeError, SdkError -from dashscope.v2.types.common import JobStatus +from dashscope import AsyncClient, Client +from dashscope.errors import DashScopeError, SdkError +from dashscope.types.common import JobStatus BASE_URL = "https://api.test.local/api/v1" diff --git a/tests/v2/test_settings.py b/tests/test_settings.py similarity index 97% rename from tests/v2/test_settings.py rename to tests/test_settings.py index 2fe0e55a..d0b6ddf7 100644 --- a/tests/v2/test_settings.py +++ b/tests/test_settings.py @@ -11,13 +11,13 @@ import pytest -from dashscope.v2 import settings -from dashscope.v2._config import ( +from dashscope import settings +from dashscope._config import ( DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, resolve_config, ) -from dashscope.v2._logging import ROOT_NAME +from dashscope._logging import ROOT_NAME @pytest.fixture @@ -187,7 +187,7 @@ def test_websocket_base_url_lazy_env(clean): def test_ws_session_uses_settings_url(clean): - from dashscope.v2.transport.ws import WsSession + from dashscope.transport.ws import WsSession captured = {} diff --git a/tests/v2/test_sse.py b/tests/test_sse.py similarity index 98% rename from tests/v2/test_sse.py rename to tests/test_sse.py index b57fdc41..7bd9f7b9 100644 --- a/tests/v2/test_sse.py +++ b/tests/test_sse.py @@ -12,18 +12,18 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import ( +from dashscope import AsyncClient, Client +from dashscope.errors import ( ContentFilterError, DashScopeError, RateLimitError, ) -from dashscope.v2.resources._sse import _aiter_sse, _iter_sse, _stream_error -from dashscope.v2.resources.audio.http_tts import ( +from dashscope.resources._sse import _aiter_sse, _iter_sse, _stream_error +from dashscope.resources.audio.http_tts import ( _audio_stream_error, _iter_audio_sse, ) -from dashscope.v2.transport.sse import ( +from dashscope.transport.sse import ( parse_event_data, stream_error, ) diff --git a/tests/v2/test_sse_comprehensive_verify.py b/tests/test_sse_comprehensive_verify.py similarity index 93% rename from tests/v2/test_sse_comprehensive_verify.py rename to tests/test_sse_comprehensive_verify.py index 404b39b6..7a923af6 100644 --- a/tests/v2/test_sse_comprehensive_verify.py +++ b/tests/test_sse_comprehensive_verify.py @@ -11,8 +11,8 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import ( +from dashscope import AsyncClient, Client +from dashscope.errors import ( DashScopeError, RateLimitError, ) @@ -410,28 +410,10 @@ def handler(request): assert err.code == "overloaded_error" -# ── Finding: package root missing errors export ───────────────────────────── - - -def test_finding_package_root_errors_not_exported(): - """dashscope.errors is NOT directly accessible from the package root. - - Design doc §2.2 says ``dashscope.errors.ErrorCode`` should work, - but currently only ``dashscope.v2.errors`` is populated. - """ +def test_package_root_exports_errors(): import dashscope + from dashscope.errors import ErrorCode - # v2 subpackage has errors — this works - assert hasattr(dashscope.v2, "errors") - assert hasattr(dashscope.v2.errors, "ErrorCode") - - # Package root does NOT export errors directly - assert not hasattr(dashscope, "errors"), ( - "dashscope.errors unexpectedly exists — " - "the finding may already be fixed" - ) - - # But ErrorCode IS available via v2.errors - from dashscope.v2.errors import ErrorCode - + assert dashscope.errors.ErrorCode is ErrorCode + assert "errors" in dashscope.__all__ assert ErrorCode.INVALID_API_KEY.value == "InvalidApiKey" diff --git a/tests/v2/test_tingwu.py b/tests/test_tingwu.py similarity index 99% rename from tests/v2/test_tingwu.py rename to tests/test_tingwu.py index 8d6c2e14..e4f42e1b 100644 --- a/tests/v2/test_tingwu.py +++ b/tests/test_tingwu.py @@ -12,8 +12,8 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import DashScopeError +from dashscope import AsyncClient, Client +from dashscope.errors import DashScopeError BASE_URL = "https://api.test.local/api/v1" CALL_PATH = "/api/v1/services/aigc/multimodal-generation/generation" diff --git a/tests/v2/test_tools_resources.py b/tests/test_tools_resources.py similarity index 99% rename from tests/v2/test_tools_resources.py rename to tests/test_tools_resources.py index 94e33102..50e45959 100644 --- a/tests/v2/test_tools_resources.py +++ b/tests/test_tools_resources.py @@ -9,8 +9,8 @@ import httpx import pytest -from dashscope.v2 import AsyncClient, Client -from dashscope.v2.resources.embeddings import parse_embedding_response +from dashscope import AsyncClient, Client +from dashscope.resources.embeddings import parse_embedding_response BASE_URL = "https://api.test.local/api/v1" diff --git a/tests/v2/test_transport.py b/tests/test_transport.py similarity index 97% rename from tests/v2/test_transport.py rename to tests/test_transport.py index f4820014..8e1c595b 100644 --- a/tests/v2/test_transport.py +++ b/tests/test_transport.py @@ -10,17 +10,17 @@ import httpx import pytest -from dashscope.v2.errors import ( +from dashscope.errors import ( APIConnectionError, APIStatusError, APITimeoutError, BadRequestError, response_error, ) -from dashscope.v2.transport import AsyncTransport, SyncTransport -from dashscope.v2.transport import http as transport_http -from dashscope.v2.transport._ident import SDK_CLIENT_VALUE -from dashscope.v2.transport._retry import backoff_delay, retry_after_seconds +from dashscope.transport import AsyncTransport, SyncTransport +from dashscope.transport import http as transport_http +from dashscope.transport._ident import SDK_CLIENT_VALUE +from dashscope.transport._retry import backoff_delay, retry_after_seconds BASE_URL = "https://api.test.local" @@ -248,7 +248,7 @@ def test_default_headers(no_sleep): def test_build_headers_and_timeout(): - from dashscope.v2.transport.http import _build_headers, _resolve_timeout + from dashscope.transport.http import _build_headers, _resolve_timeout headers = _build_headers("sk-1", "ws-1", "ua") assert headers == { @@ -353,8 +353,8 @@ def test_realtime_session_sends_sdk_headers(monkeypatch): # the websocket handshake; the kill switch applies here too. from types import SimpleNamespace - from dashscope.v2.resources.audio.realtime import RealtimeSession - from dashscope.v2.transport._ident import ( + from dashscope.resources.audio.realtime import RealtimeSession + from dashscope.transport._ident import ( sdk_client_value, sdk_session_id, ) @@ -436,7 +436,7 @@ async def test_async_sdk_identification_headers(no_sleep): def test_sdk_session_id_cached_per_process(): from dashscope._shared import _sdk_session - from dashscope.v2.transport import _ident + from dashscope.transport import _ident original = _sdk_session._session_id try: @@ -452,8 +452,8 @@ def test_sdk_session_id_cached_per_process(): def test_ws_session_sends_sdk_headers(): - from dashscope.v2.transport._ident import sdk_session_id - from dashscope.v2.transport.ws import WsSession + from dashscope.transport._ident import sdk_session_id + from dashscope.transport.ws import WsSession captured = {} diff --git a/tests/v2/test_types_messages.py b/tests/test_types_messages.py similarity index 99% rename from tests/v2/test_types_messages.py rename to tests/test_types_messages.py index e28c0cc1..fca794f8 100644 --- a/tests/v2/test_types_messages.py +++ b/tests/test_types_messages.py @@ -7,7 +7,7 @@ import pytest from pydantic import ValidationError -from dashscope.v2.types import ( +from dashscope.types import ( AudioPart, ImagePart, Message, @@ -16,7 +16,7 @@ extract_text, normalize_messages, ) -from dashscope.v2.types.messages import has_media_parts +from dashscope.types.messages import has_media_parts def test_plain_string_message(): diff --git a/tests/v2/test_ws_connection.py b/tests/test_ws_connection.py similarity index 99% rename from tests/v2/test_ws_connection.py rename to tests/test_ws_connection.py index f7235866..daab3a09 100644 --- a/tests/v2/test_ws_connection.py +++ b/tests/test_ws_connection.py @@ -10,14 +10,14 @@ import pytest -from dashscope.v2.errors import ( +from dashscope.errors import ( APIConnectionError, DashScopeError, WsBinaryOwnerConflictError, WsStateError, ) -from dashscope.v2.transport.ws import WsEventError, WsSession -from dashscope.v2.transport.ws_connection import WsConnection +from dashscope.transport.ws import WsEventError, WsSession +from dashscope.transport.ws_connection import WsConnection URL = "wss://ws.test.local/api-ws/v1/inference" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 From 712eaa4c004086942ad9f3af815f1d660e725dce Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 20:01:48 +0800 Subject: [PATCH 11/41] refactor(v2): group applications and preserve legacy imports --- README.md | 11 +- dashscope/__init__.py | 43 ++++- .../.acli/skills/fine-tuning.md | 8 +- dashscope/applications/__init__.py | 2 + .../agentstudio/__init__.py | 8 +- .../{ => applications}/agentstudio/client.py | 28 ++-- .../agentstudio/constants.py | 0 .../agentstudio/exceptions.py | 0 .../agentstudio/pagination.py | 0 .../agentstudio/resources/__init__.py | 22 +-- .../agentstudio/resources/_helpers.py | 2 +- .../agentstudio/resources/agents.py | 8 +- .../agentstudio/resources/deployments.py | 8 +- .../agentstudio/resources/environments.py | 8 +- .../agentstudio/resources/files.py | 8 +- .../agentstudio/resources/memory_stores.py | 8 +- .../agentstudio/resources/security.py | 4 +- .../agentstudio/resources/session_events.py | 12 +- .../resources/session_resources.py | 8 +- .../agentstudio/resources/session_threads.py | 8 +- .../agentstudio/resources/sessions.py | 14 +- .../agentstudio/resources/skills.py | 8 +- .../agentstudio/resources/vaults.py | 8 +- .../resources/webhook_endpoints.py | 8 +- .../agentstudio/streaming.py | 4 +- .../agentstudio/transport.py | 6 +- .../agentstudio/types/__init__.py | 8 +- .../agentstudio/types/models.py | 2 +- .../agentstudio/types/params.py | 4 +- .../{ => applications}/finetune/__init__.py | 0 .../{ => applications}/finetune/_submit.py | 0 .../{ => applications}/finetune/agentic_rl.py | 20 +-- .../finetune/reinforcement/__init__.py | 0 .../finetune/reinforcement/common/__init__.py | 0 .../reinforcement/common/constants.py | 4 +- .../finetune/reinforcement/common/errors.py | 0 .../finetune/reinforcement/common/log.py | 2 +- .../finetune/reinforcement/common/model.py | 12 +- .../reinforcement/common/model_types.py | 0 .../finetune/reinforcement/common/utils.py | 8 +- .../reinforcement/component/__init__.py | 28 ++-- .../reinforcement/component/data/__init__.py | 14 +- .../component/data/base_data_model.py | 2 +- .../component/data/group_reward_input.py | 4 +- .../component/data/group_reward_output.py | 4 +- .../component/data/reward_input.py | 4 +- .../component/data/reward_output.py | 2 +- .../component/data/rollout_input.py | 4 +- .../component/data/rollout_output.py | 2 +- .../reinforcement/component/demo/__init__.py | 19 +++ .../demo/group_reward_processor_demo.py | 12 +- .../component/demo/reward_processor_demo.py | 10 +- .../component/demo/rollout_processor_demo.py | 10 +- .../reinforcement/component/func_decorator.py | 4 +- .../reinforcement/component/func_manager.py | 18 +- .../component/observability/__init__.py | 8 +- .../component/observability/genai/__init__.py | 20 +-- .../component/observability/genai/_core.py | 0 .../observability/genai/llm_dashscope.py | 8 +- .../observability/genai/llm_openai.py | 8 +- .../component/observability/genai/llm_span.py | 6 +- .../component/observability/genai/messages.py | 6 +- .../component/observability/genai/tools.py | 2 +- .../component/observability/processor_span.py | 8 +- .../component/observability/tracing.py | 8 +- .../component/parser/__init__.py | 20 +++ .../component/parser/base_parser.py | 2 +- .../component/parser/group_reward_parser.py | 6 +- .../component/parser/reward_parser.py | 6 +- .../component/parser/rollout_parser.py | 6 +- .../component/processor/__init__.py | 20 +++ .../abstract_group_reward_processor.py | 6 +- .../component/processor/abstract_processor.py | 2 +- .../processor/abstract_reward_processor.py | 8 +- .../processor/abstract_rollout_processor.py | 6 +- .../component/server/__init__.py | 0 .../reinforcement/component/server/server.py | 28 ++-- .../examples/observability/__init__.py | 0 .../observability/smoke_observability.py | 18 +- .../examples/workspace/README-zh.md | 0 .../examples/workspace/README.md | 0 .../examples/workspace/UserGuide-zh.md | 0 .../examples/workspace/UserGuide.md | 0 .../reinforcement/examples/workspace/cli.sh | 0 .../workspace/data/calc_train_min.jsonl | 0 .../workspace/data/calc_validation_min.jsonl | 0 .../examples/workspace/functions/__init__.py | 0 .../functions/reward/group_reward.py | 6 +- .../workspace/functions/reward/reward.py | 12 +- .../functions/reward/reward_decorator.py | 8 +- .../workspace/functions/rollout/rollout.py | 10 +- .../functions/rollout/rollout_only.py | 10 +- .../examples/workspace/opd-job.yaml | 0 .../examples/workspace/requirements.txt | 0 .../resources/group_reward_input.json | 0 .../resources/reward_decorator_input.json | 0 .../workspace/resources/reward_input.json | 0 .../workspace/resources/rollout_input.json | 0 .../examples/workspace/rl-job.yaml | 0 .../examples/workspace/submit_job.py | 4 +- .../examples/workspace/test_functions.py | 4 +- dashscope/client.py | 3 + .../reinforcement/component/demo/__init__.py | 19 --- .../component/parser/__init__.py | 20 --- .../component/processor/__init__.py | 20 --- dashscope/legacy_1x/_shim.py | 156 ++++++++++++------ .../legacy_1x/unit/test_acli_sdk_contract.py | 4 +- tests/legacy_1x/unit/test_layer_decoupling.py | 11 +- tests/legacy_1x/unit/test_opd.py | 6 +- tests/test_agentstudio_shim.py | 19 ++- tests/test_finetune_rl_shim.py | 7 +- tests/test_legacy_import_aliases.py | 155 +++++++++++++++++ 112 files changed, 696 insertions(+), 421 deletions(-) create mode 100644 dashscope/applications/__init__.py rename dashscope/{ => applications}/agentstudio/__init__.py (94%) rename dashscope/{ => applications}/agentstudio/client.py (87%) rename dashscope/{ => applications}/agentstudio/constants.py (100%) rename dashscope/{ => applications}/agentstudio/exceptions.py (100%) rename dashscope/{ => applications}/agentstudio/pagination.py (100%) rename dashscope/{ => applications}/agentstudio/resources/__init__.py (67%) rename dashscope/{ => applications}/agentstudio/resources/_helpers.py (98%) rename dashscope/{ => applications}/agentstudio/resources/agents.py (97%) rename dashscope/{ => applications}/agentstudio/resources/deployments.py (98%) rename dashscope/{ => applications}/agentstudio/resources/environments.py (96%) rename dashscope/{ => applications}/agentstudio/resources/files.py (97%) rename dashscope/{ => applications}/agentstudio/resources/memory_stores.py (99%) rename dashscope/{ => applications}/agentstudio/resources/security.py (96%) rename dashscope/{ => applications}/agentstudio/resources/session_events.py (96%) rename dashscope/{ => applications}/agentstudio/resources/session_resources.py (94%) rename dashscope/{ => applications}/agentstudio/resources/session_threads.py (96%) rename dashscope/{ => applications}/agentstudio/resources/sessions.py (95%) rename dashscope/{ => applications}/agentstudio/resources/skills.py (97%) rename dashscope/{ => applications}/agentstudio/resources/vaults.py (98%) rename dashscope/{ => applications}/agentstudio/resources/webhook_endpoints.py (97%) rename dashscope/{ => applications}/agentstudio/streaming.py (98%) rename dashscope/{ => applications}/agentstudio/transport.py (99%) rename dashscope/{ => applications}/agentstudio/types/__init__.py (90%) rename dashscope/{ => applications}/agentstudio/types/models.py (99%) rename dashscope/{ => applications}/agentstudio/types/params.py (99%) rename dashscope/{ => applications}/finetune/__init__.py (100%) rename dashscope/{ => applications}/finetune/_submit.py (100%) rename dashscope/{ => applications}/finetune/agentic_rl.py (97%) rename dashscope/{ => applications}/finetune/reinforcement/__init__.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/common/__init__.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/common/constants.py (96%) rename dashscope/{ => applications}/finetune/reinforcement/common/errors.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/common/log.py (90%) rename dashscope/{ => applications}/finetune/reinforcement/common/model.py (99%) rename dashscope/{ => applications}/finetune/reinforcement/common/model_types.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/common/utils.py (99%) rename dashscope/{ => applications}/finetune/reinforcement/component/__init__.py (71%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/__init__.py (68%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/base_data_model.py (98%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/group_reward_input.py (88%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/group_reward_output.py (89%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/reward_input.py (88%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/reward_output.py (95%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/rollout_input.py (92%) rename dashscope/{ => applications}/finetune/reinforcement/component/data/rollout_output.py (94%) create mode 100644 dashscope/applications/finetune/reinforcement/component/demo/__init__.py rename dashscope/{ => applications}/finetune/reinforcement/component/demo/group_reward_processor_demo.py (84%) rename dashscope/{ => applications}/finetune/reinforcement/component/demo/reward_processor_demo.py (84%) rename dashscope/{ => applications}/finetune/reinforcement/component/demo/rollout_processor_demo.py (84%) rename dashscope/{ => applications}/finetune/reinforcement/component/func_decorator.py (97%) rename dashscope/{ => applications}/finetune/reinforcement/component/func_manager.py (92%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/__init__.py (91%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/__init__.py (77%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/_core.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/llm_dashscope.py (93%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/llm_openai.py (98%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/llm_span.py (95%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/messages.py (98%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/genai/tools.py (99%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/processor_span.py (97%) rename dashscope/{ => applications}/finetune/reinforcement/component/observability/tracing.py (98%) create mode 100644 dashscope/applications/finetune/reinforcement/component/parser/__init__.py rename dashscope/{ => applications}/finetune/reinforcement/component/parser/base_parser.py (89%) rename dashscope/{ => applications}/finetune/reinforcement/component/parser/group_reward_parser.py (79%) rename dashscope/{ => applications}/finetune/reinforcement/component/parser/reward_parser.py (77%) rename dashscope/{ => applications}/finetune/reinforcement/component/parser/rollout_parser.py (77%) create mode 100644 dashscope/applications/finetune/reinforcement/component/processor/__init__.py rename dashscope/{ => applications}/finetune/reinforcement/component/processor/abstract_group_reward_processor.py (89%) rename dashscope/{ => applications}/finetune/reinforcement/component/processor/abstract_processor.py (94%) rename dashscope/{ => applications}/finetune/reinforcement/component/processor/abstract_reward_processor.py (95%) rename dashscope/{ => applications}/finetune/reinforcement/component/processor/abstract_rollout_processor.py (87%) rename dashscope/{ => applications}/finetune/reinforcement/component/server/__init__.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/component/server/server.py (93%) rename dashscope/{ => applications}/finetune/reinforcement/examples/observability/__init__.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/observability/smoke_observability.py (91%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/README-zh.md (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/README.md (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/UserGuide-zh.md (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/UserGuide.md (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/cli.sh (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/__init__.py (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py (94%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/reward/reward.py (88%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py (92%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py (97%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py (98%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/opd-job.yaml (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/requirements.txt (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/resources/group_reward_input.json (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/resources/reward_input.json (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/resources/rollout_input.json (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/rl-job.yaml (100%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/submit_job.py (98%) rename dashscope/{ => applications}/finetune/reinforcement/examples/workspace/test_functions.py (98%) delete mode 100644 dashscope/finetune/reinforcement/component/demo/__init__.py delete mode 100644 dashscope/finetune/reinforcement/component/parser/__init__.py delete mode 100644 dashscope/finetune/reinforcement/component/processor/__init__.py create mode 100644 tests/test_legacy_import_aliases.py diff --git a/README.md b/README.md index a0481f4e..c7bb464b 100644 --- a/README.md +++ b/README.md @@ -450,10 +450,13 @@ The 1.x module-level API is unchanged and still supported — you can adopt 2.0 ### Package layout and compatibility The 2.0 implementation is directly under `dashscope`; the former -`dashscope.v2` namespace has been removed. Existing root 1.x exports remain -available. Old deep modules such as `dashscope.aigc` and `dashscope.client` -have moved under `dashscope.legacy_1x`. The 2.0 Agentic RL workflow lives in -`dashscope.finetune`; its 1.x job-management surface is under +`dashscope.v2` namespace has been removed. AgentStudio and FineTune/Agentic RL +implementations are grouped under `dashscope.applications`, while the public +`dashscope.agentstudio` and `dashscope.finetune` import paths remain available +through lazy aliases. Existing root 1.x exports remain available. Historical +deep modules such as `dashscope.aigc`, `dashscope.client.base_api`, and +`dashscope.files` also remain available through lazy aliases to +`dashscope.legacy_1x`. The 1.x Agentic RL job-management surface is under `dashscope.legacy_1x.finetune`. `dashscope.save_api_key` now comes from `dashscope._config`. It accepts an diff --git a/dashscope/__init__.py b/dashscope/__init__.py index c084ffd4..32fae212 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -15,6 +15,10 @@ category=Warning, ) +from dashscope.legacy_1x._shim import install as _install_legacy_aliases + +_install_legacy_aliases() + from dashscope.version import __version__ from dashscope.legacy_1x.aigc.code_generation import CodeGeneration from dashscope.legacy_1x.aigc.conversation import ( @@ -153,20 +157,53 @@ def set_region(region: str, workspace_id: str = None): "Threads": "dashscope.legacy_1x.threads", } +_LEGACY_LAZY_MODULES = frozenset( + { + "aigc", + "agentstudio", + "api_entities", + "app", + "applications", + "assistants", + "audio", + "cli", + "common", + "embeddings", + "files", + "finetune", + "io", + "model", + "models", + "multimodal", + "nlp", + "protocol", + "rerank", + "threads", + "utils", + }, +) + def __getattr__(name): module_name = _DEPRECATED_LAZY_IMPORTS.get(name) - if module_name is None: + if module_name is not None: + value = getattr(importlib.import_module(module_name), name) + elif name in _LEGACY_LAZY_MODULES: + value = importlib.import_module(f"dashscope.{name}") + else: raise AttributeError( f"module {__name__!r} has no attribute {name!r}", ) - value = getattr(importlib.import_module(module_name), name) globals()[name] = value return value def __dir__(): - return sorted(set(globals()) | set(_DEPRECATED_LAZY_IMPORTS)) + return sorted( + set(globals()) + | set(_DEPRECATED_LAZY_IMPORTS) + | _LEGACY_LAZY_MODULES, + ) __all__ = [ diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md index d1ef55f0..dd2310ad 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md @@ -5,7 +5,7 @@ description: Quick reference for fine-tuning and deployment (FineTunes/Files/Dep # Fine-tuning and Deployment -> Quick-reference note: all parameter names and return fields on this page follow the SDK source code (`dashscope/finetune/`, `dashscope/files.py`, `dashscope/common/`). Check the signatures on this page before answering; if unsure, verify on the spot with `inspect.signature` instead of relying on memory. +> Quick-reference note: all parameter names and return fields on this page follow the SDK source code (`dashscope/applications/finetune/`, `dashscope/legacy_1x/finetune/`, `dashscope/legacy_1x/files.py`, `dashscope/legacy_1x/common/`). Check the signatures on this page before answering; if unsure, verify on the spot with `inspect.signature` instead of relying on memory. ## Use Cases and Training Modes (SFT/CPT/DPO/AgenticRL) @@ -26,7 +26,7 @@ description: Quick reference for fine-tuning and deployment (FineTunes/Files/Dep ## SDK APIs (source of truth: the source code) -### FineTunes (`dashscope/finetune/finetunes.py`, SUB_PATH="fine-tunes") +### FineTunes (`dashscope/legacy_1x/finetune/finetunes.py`, SUB_PATH="fine-tunes") The creation method is named `call` (the SDK has no `create` method): `FineTunes.call(model, training_file_ids, validation_file_ids=None, mode=None, hyper_parameters={}, api_key=None, workspace=None, **kwargs) -> FineTune` @@ -42,7 +42,7 @@ The creation method is named `call` (the SDK has no `create` method): Other methods: `get(job_id) -> FineTune`; `list(page_no=1, page_size=10) -> FineTuneList`; `cancel(job_id) -> FineTuneCancel`; `delete(job_id) -> FineTuneDelete`; `wait(job_id)` polls until `SUCCEEDED/FAILED/CANCELED`. -### Deployments (`dashscope/finetune/deployments.py`, SUB_PATH="deployments") +### Deployments (`dashscope/legacy_1x/finetune/deployments.py`, SUB_PATH="deployments") - `Deployments.call(model, capacity, version=None, suffix=None, ...) -> Deployment`: request body keys are `model_name/capacity/model_version/suffix`; fine-tuned models do not need `version`; with `suffix` set, the deployment name is `model_suffix` - `get(deployed_model)`, `list(page_no=1, page_size=10)`, `delete(deployed_model)`, `scale(deployed_model, capacity)` @@ -52,7 +52,7 @@ Other methods: `get(job_id) -> FineTune`; `list(page_no=1, page_size=10) -> Fine - `Files.upload(file_path, purpose="fine_tune", description=None, ...) -> DashScopeAPIResponse`: see `FilePurpose` for `purpose` values (`fine_tune`/`assistants`); for `fine_tune`, the JSONL is validated locally first and invalid files raise `InvalidFileFormat` directly - `list(page=1, page_size=10)`, `get(file_id)`, `delete(file_id)`; all return `DashScopeAPIResponse` -### AgenticRL (`dashscope/finetune/agentic_rl.py`, brief) +### AgenticRL (`dashscope/applications/finetune/agentic_rl.py`, brief) - Usage: `client = AgenticRL(api_key=None)` → optionally `client.init(config_path="job.yaml")` (YAML-driven) → `await client.run(...)` completes "register functions → upload datasets → submit job" in one step; or call `register_functions()` / `upload_datasets()` / `submit_job()` step by step - Key concepts: `RolloutFunctionComponent`/`RewardFunctionComponent` function components; `TrainingDataset`/`ValidationDataset` (`data_source_type`: `file_id`/`download_url`/`oss_mount`); `hyper_parameters` (`algorithm="gspo"`, `n_rollouts`, `kl_loss_coef`, `learning_rate`, etc.); `resources` (e.g. `mtu_spec_code`) diff --git a/dashscope/applications/__init__.py b/dashscope/applications/__init__.py new file mode 100644 index 00000000..cc26a06f --- /dev/null +++ b/dashscope/applications/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. diff --git a/dashscope/agentstudio/__init__.py b/dashscope/applications/agentstudio/__init__.py similarity index 94% rename from dashscope/agentstudio/__init__.py rename to dashscope/applications/agentstudio/__init__.py index edd58186..c03c6658 100644 --- a/dashscope/agentstudio/__init__.py +++ b/dashscope/applications/agentstudio/__init__.py @@ -5,15 +5,15 @@ The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP API. All resource classes are accessed through a client instance:: - from dashscope.agentstudio import Client - from dashscope.agentstudio.types import ( + from dashscope.applications.agentstudio import Client + from dashscope.applications.agentstudio.types import ( user_message, user_tool_result, ) Example ------- ->>> from dashscope.agentstudio import Client ->>> from dashscope.agentstudio.types import user_message +>>> from dashscope.applications.agentstudio import Client +>>> from dashscope.applications.agentstudio.types import user_message >>> client = Client(api_key="sk-xxx") >>> agent = client.agents.create(name="demo", model="qwen-plus") >>> session = client.sessions.create(agent=agent.id) diff --git a/dashscope/agentstudio/client.py b/dashscope/applications/agentstudio/client.py similarity index 87% rename from dashscope/agentstudio/client.py rename to dashscope/applications/agentstudio/client.py index 19e870e9..62fc8a85 100644 --- a/dashscope/agentstudio/client.py +++ b/dashscope/applications/agentstudio/client.py @@ -13,37 +13,37 @@ validate_region, validate_workspace_id, ) -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope.applications.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.applications.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.applications.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.applications.agentstudio.resources.files import Files, AsyncFiles +from dashscope.applications.agentstudio.resources.memory_stores import ( MemoryStores, AsyncMemoryStores, ) -from dashscope.agentstudio.resources.security import Security, AsyncSecurity -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import Skills, AsyncSkills -from dashscope.agentstudio.resources.vaults import Vaults, AsyncVaults -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.applications.agentstudio.resources.security import Security, AsyncSecurity +from dashscope.applications.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.applications.agentstudio.resources.skills import Skills, AsyncSkills +from dashscope.applications.agentstudio.resources.vaults import Vaults, AsyncVaults +from dashscope.applications.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) -from dashscope.agentstudio.constants import ( +from dashscope.applications.agentstudio.constants import ( AGENTSTUDIO_BASE_URL_TEMPLATE, AGENTSTUDIO_DEFAULT_REGION, AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.agentstudio.transport import SyncTransport, AsyncTransport +from dashscope.applications.agentstudio.transport import SyncTransport, AsyncTransport from dashscope._config import _api_key_from_file, settings @@ -120,7 +120,7 @@ class Client: Usage:: - from dashscope.agentstudio import Client, ModelConfig + from dashscope.applications.agentstudio import Client, ModelConfig client = Client(api_key="sk-xxx", workspace="my-ws") agent = client.agents.create( @@ -201,7 +201,7 @@ class AsyncClient: Usage:: - from dashscope.agentstudio import AsyncClient, ModelConfig + from dashscope.applications.agentstudio import AsyncClient, ModelConfig client = AsyncClient(api_key="sk-xxx", workspace="my-ws") agent = await client.agents.create( diff --git a/dashscope/agentstudio/constants.py b/dashscope/applications/agentstudio/constants.py similarity index 100% rename from dashscope/agentstudio/constants.py rename to dashscope/applications/agentstudio/constants.py diff --git a/dashscope/agentstudio/exceptions.py b/dashscope/applications/agentstudio/exceptions.py similarity index 100% rename from dashscope/agentstudio/exceptions.py rename to dashscope/applications/agentstudio/exceptions.py diff --git a/dashscope/agentstudio/pagination.py b/dashscope/applications/agentstudio/pagination.py similarity index 100% rename from dashscope/agentstudio/pagination.py rename to dashscope/applications/agentstudio/pagination.py diff --git a/dashscope/agentstudio/resources/__init__.py b/dashscope/applications/agentstudio/resources/__init__.py similarity index 67% rename from dashscope/agentstudio/resources/__init__.py rename to dashscope/applications/agentstudio/resources/__init__.py index 76c65123..722179cf 100644 --- a/dashscope/agentstudio/resources/__init__.py +++ b/dashscope/applications/agentstudio/resources/__init__.py @@ -7,7 +7,7 @@ Usage:: - from dashscope.agentstudio import Client + from dashscope.applications.agentstudio import Client client = Client(api_key="sk-...") agent = client.agents.create(name="demo", model="qwen-plus") @@ -15,19 +15,19 @@ client.sessions.events.send(session.id, [user_message("hello")]) """ -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope.applications.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.applications.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.applications.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.applications.agentstudio.resources.files import Files, AsyncFiles +from dashscope.applications.agentstudio.resources.memory_stores import ( AsyncMemories, AsyncMemoryStores, AsyncMemoryVersions, @@ -35,26 +35,26 @@ MemoryStores, MemoryVersions, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.applications.agentstudio.resources.session_events import ( AsyncSessionEvents, SessionEvents, _AioTypedEventStream, _TypedEventStream, ) -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import ( +from dashscope.applications.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.applications.agentstudio.resources.skills import ( AsyncSkillVersions, AsyncSkills, SkillVersions, Skills, ) -from dashscope.agentstudio.resources.vaults import ( +from dashscope.applications.agentstudio.resources.vaults import ( AsyncCredentials, AsyncVaults, Credentials, Vaults, ) -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.applications.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) diff --git a/dashscope/agentstudio/resources/_helpers.py b/dashscope/applications/agentstudio/resources/_helpers.py similarity index 98% rename from dashscope/agentstudio/resources/_helpers.py rename to dashscope/applications/agentstudio/resources/_helpers.py index f47d6d94..653c2ebb 100644 --- a/dashscope/agentstudio/resources/_helpers.py +++ b/dashscope/applications/agentstudio/resources/_helpers.py @@ -6,7 +6,7 @@ from typing import Any, Mapping -from dashscope.agentstudio.types import ( +from dashscope.applications.agentstudio.types import ( Agent, AgentVersion, Credential, diff --git a/dashscope/agentstudio/resources/agents.py b/dashscope/applications/agentstudio/resources/agents.py similarity index 97% rename from dashscope/agentstudio/resources/agents.py rename to dashscope/applications/agentstudio/resources/agents.py index 53712811..c372fa60 100644 --- a/dashscope/agentstudio/resources/agents.py +++ b/dashscope/applications/agentstudio/resources/agents.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence, Union -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_agent, _coerce_agent_version, ) -from dashscope.agentstudio.types import Agent, AgentVersion, ModelConfig -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import Agent, AgentVersion, ModelConfig +from dashscope.applications.agentstudio.types.params import ( AgentCreateParams, AgentListParams, AgentUpdateParams, diff --git a/dashscope/agentstudio/resources/deployments.py b/dashscope/applications/agentstudio/resources/deployments.py similarity index 98% rename from dashscope/agentstudio/resources/deployments.py rename to dashscope/applications/agentstudio/resources/deployments.py index 149b64f2..c3f49ebc 100644 --- a/dashscope/agentstudio/resources/deployments.py +++ b/dashscope/applications/agentstudio/resources/deployments.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_deployment, _coerce_deployment_run, ) -from dashscope.agentstudio.types import Deployment, DeploymentRun -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import Deployment, DeploymentRun +from dashscope.applications.agentstudio.types.params import ( _NOT_GIVEN, DeploymentCreateParams, DeploymentListParams, diff --git a/dashscope/agentstudio/resources/environments.py b/dashscope/applications/agentstudio/resources/environments.py similarity index 96% rename from dashscope/agentstudio/resources/environments.py rename to dashscope/applications/agentstudio/resources/environments.py index 8aed2eeb..5615c9fa 100644 --- a/dashscope/agentstudio/resources/environments.py +++ b/dashscope/applications/agentstudio/resources/environments.py @@ -6,16 +6,16 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_env, ) -from dashscope.agentstudio.types import DeleteResponse, Environment -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import DeleteResponse, Environment +from dashscope.applications.agentstudio.types.params import ( EnvironmentCreateParams, EnvironmentListParams, EnvironmentUpdateParams, diff --git a/dashscope/agentstudio/resources/files.py b/dashscope/applications/agentstudio/resources/files.py similarity index 97% rename from dashscope/agentstudio/resources/files.py rename to dashscope/applications/agentstudio/resources/files.py index e05c7be8..5afd489d 100644 --- a/dashscope/agentstudio/resources/files.py +++ b/dashscope/applications/agentstudio/resources/files.py @@ -18,16 +18,16 @@ Union, ) -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_file, ) -from dashscope.agentstudio.types import DeleteResponse, File -from dashscope.agentstudio.types.params import FileListParams +from dashscope.applications.agentstudio.types import DeleteResponse, File +from dashscope.applications.agentstudio.types.params import FileListParams _PATH_FILES = "/files" diff --git a/dashscope/agentstudio/resources/memory_stores.py b/dashscope/applications/agentstudio/resources/memory_stores.py similarity index 99% rename from dashscope/agentstudio/resources/memory_stores.py rename to dashscope/applications/agentstudio/resources/memory_stores.py index 573084a0..a08b153b 100644 --- a/dashscope/agentstudio/resources/memory_stores.py +++ b/dashscope/applications/agentstudio/resources/memory_stores.py @@ -41,12 +41,12 @@ from typing import Any, List, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_memory, _coerce_memory_store, _coerce_memory_version, @@ -59,13 +59,13 @@ _memory_version_redact_path, _memory_versions_path, ) -from dashscope.agentstudio.types import ( +from dashscope.applications.agentstudio.types import ( DeleteResponse, Memory, MemoryStore, MemoryVersion, ) -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types.params import ( MemoryCreateParams, MemoryListParams, MemoryStoreCreateParams, diff --git a/dashscope/agentstudio/resources/security.py b/dashscope/applications/agentstudio/resources/security.py similarity index 96% rename from dashscope/agentstudio/resources/security.py rename to dashscope/applications/agentstudio/resources/security.py index a6ebc723..78055a19 100644 --- a/dashscope/agentstudio/resources/security.py +++ b/dashscope/applications/agentstudio/resources/security.py @@ -12,8 +12,8 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.types import SecurityAlertList, SecurityOverview -from dashscope.agentstudio.types.params import SecurityListAgentLogsParams +from dashscope.applications.agentstudio.types import SecurityAlertList, SecurityOverview +from dashscope.applications.agentstudio.types.params import SecurityListAgentLogsParams _PATH_SECURITY_OVERVIEW = "/security/overview" _PATH_SECURITY_AGENT_LOGS = "/security/agent_logs" diff --git a/dashscope/agentstudio/resources/session_events.py b/dashscope/applications/agentstudio/resources/session_events.py similarity index 96% rename from dashscope/agentstudio/resources/session_events.py rename to dashscope/applications/agentstudio/resources/session_events.py index aa272597..504bef5b 100644 --- a/dashscope/agentstudio/resources/session_events.py +++ b/dashscope/applications/agentstudio/resources/session_events.py @@ -14,23 +14,23 @@ Sequence, ) -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_event, _events_path, _stream_path, ) -from dashscope.agentstudio.streaming import AsyncEventStream, EventStream -from dashscope.agentstudio.types import ServerEvent -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.streaming import AsyncEventStream, EventStream +from dashscope.applications.agentstudio.types import ServerEvent +from dashscope.applications.agentstudio.types.params import ( SessionEventListParams, SessionEventSendParams, ) -from dashscope.agentstudio.constants import ( +from dashscope.applications.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, BlockType, SessionStatus, diff --git a/dashscope/agentstudio/resources/session_resources.py b/dashscope/applications/agentstudio/resources/session_resources.py similarity index 94% rename from dashscope/agentstudio/resources/session_resources.py rename to dashscope/applications/agentstudio/resources/session_resources.py index 1691a654..4571126a 100644 --- a/dashscope/agentstudio/resources/session_resources.py +++ b/dashscope/applications/agentstudio/resources/session_resources.py @@ -10,18 +10,18 @@ from typing import Optional -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_session_resource, _session_resource_item_path, _session_resources_path, ) -from dashscope.agentstudio.types import DeleteResponse, SessionResource -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import DeleteResponse, SessionResource +from dashscope.applications.agentstudio.types.params import ( SessionResourceAddParams, SessionResourceListParams, ) diff --git a/dashscope/agentstudio/resources/session_threads.py b/dashscope/applications/agentstudio/resources/session_threads.py similarity index 96% rename from dashscope/agentstudio/resources/session_threads.py rename to dashscope/applications/agentstudio/resources/session_threads.py index e2f20313..ad70ed8f 100644 --- a/dashscope/agentstudio/resources/session_threads.py +++ b/dashscope/applications/agentstudio/resources/session_threads.py @@ -10,12 +10,12 @@ from typing import Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_event, _coerce_session_thread, _session_thread_archive_path, @@ -23,8 +23,8 @@ _session_thread_item_path, _session_threads_path, ) -from dashscope.agentstudio.types import ServerEvent, SessionThread -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import ServerEvent, SessionThread +from dashscope.applications.agentstudio.types.params import ( SessionEventListParams, SessionThreadListParams, ) diff --git a/dashscope/agentstudio/resources/sessions.py b/dashscope/applications/agentstudio/resources/sessions.py similarity index 95% rename from dashscope/agentstudio/resources/sessions.py rename to dashscope/applications/agentstudio/resources/sessions.py index 4bb68f15..b03b3a64 100644 --- a/dashscope/agentstudio/resources/sessions.py +++ b/dashscope/applications/agentstudio/resources/sessions.py @@ -6,28 +6,28 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_session, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.applications.agentstudio.resources.session_events import ( SessionEvents, AsyncSessionEvents, ) -from dashscope.agentstudio.resources.session_resources import ( +from dashscope.applications.agentstudio.resources.session_resources import ( SessionResources, AsyncSessionResources, ) -from dashscope.agentstudio.resources.session_threads import ( +from dashscope.applications.agentstudio.resources.session_threads import ( SessionThreads, AsyncSessionThreads, ) -from dashscope.agentstudio.types import DeleteResponse, Session -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import DeleteResponse, Session +from dashscope.applications.agentstudio.types.params import ( SessionCreateParams, SessionListParams, SessionUpdateParams, diff --git a/dashscope/agentstudio/resources/skills.py b/dashscope/applications/agentstudio/resources/skills.py similarity index 97% rename from dashscope/agentstudio/resources/skills.py rename to dashscope/applications/agentstudio/resources/skills.py index c9d9833e..cd8ecda9 100644 --- a/dashscope/agentstudio/resources/skills.py +++ b/dashscope/applications/agentstudio/resources/skills.py @@ -7,17 +7,17 @@ import os # pylint: disable=unused-import # noqa: F401 from typing import Any, BinaryIO, Dict, Optional, Tuple, Union -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_skill, _coerce_skill_version, ) -from dashscope.agentstudio.types import DeleteResponse, Skill, SkillVersion -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types import DeleteResponse, Skill, SkillVersion +from dashscope.applications.agentstudio.types.params import ( SkillCreateParams, SkillListParams, SkillVersionCreateParams, diff --git a/dashscope/agentstudio/resources/vaults.py b/dashscope/applications/agentstudio/resources/vaults.py similarity index 98% rename from dashscope/agentstudio/resources/vaults.py rename to dashscope/applications/agentstudio/resources/vaults.py index c68e4922..2698ebc7 100644 --- a/dashscope/agentstudio/resources/vaults.py +++ b/dashscope/applications/agentstudio/resources/vaults.py @@ -6,21 +6,21 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_credential, _coerce_vault, ) -from dashscope.agentstudio.types import ( +from dashscope.applications.agentstudio.types import ( Credential, DeleteResponse, Vault, ) -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types.params import ( CredentialCreateParams, CredentialListParams, CredentialUpdateParams, diff --git a/dashscope/agentstudio/resources/webhook_endpoints.py b/dashscope/applications/agentstudio/resources/webhook_endpoints.py similarity index 97% rename from dashscope/agentstudio/resources/webhook_endpoints.py rename to dashscope/applications/agentstudio/resources/webhook_endpoints.py index fc1a4485..422e5869 100644 --- a/dashscope/agentstudio/resources/webhook_endpoints.py +++ b/dashscope/applications/agentstudio/resources/webhook_endpoints.py @@ -6,23 +6,23 @@ from typing import Any, Dict, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.applications.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.applications.agentstudio.resources._helpers import ( _coerce_webhook_endpoint, _coerce_webhook_event, _coerce_webhook_secret_reset, ) -from dashscope.agentstudio.types import ( +from dashscope.applications.agentstudio.types import ( WebhookEndpoint, WebhookEndpointList, WebhookEvent, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( +from dashscope.applications.agentstudio.types.params import ( WebhookEndpointCreateParams, WebhookEndpointUpdateParams, WebhookEventListParams, diff --git a/dashscope/agentstudio/streaming.py b/dashscope/applications/agentstudio/streaming.py similarity index 98% rename from dashscope/agentstudio/streaming.py rename to dashscope/applications/agentstudio/streaming.py index 23aac095..f6e524d4 100644 --- a/dashscope/agentstudio/streaming.py +++ b/dashscope/applications/agentstudio/streaming.py @@ -39,13 +39,13 @@ import httpx from httpx_sse import EventSource -from dashscope.agentstudio import exceptions +from dashscope.applications.agentstudio import exceptions # A child of the "dashscope" logger, so the level and handler installed by # dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via # propagation -- without importing that module (and its import-time side # effects) just to name a logger. -logger = logging.getLogger("dashscope.agentstudio.streaming") +logger = logging.getLogger("dashscope.applications.agentstudio.streaming") _DEFAULT_IDLE_TIMEOUT = 120.0 diff --git a/dashscope/agentstudio/transport.py b/dashscope/applications/agentstudio/transport.py similarity index 99% rename from dashscope/agentstudio/transport.py rename to dashscope/applications/agentstudio/transport.py index 6068b066..db6a08c5 100644 --- a/dashscope/agentstudio/transport.py +++ b/dashscope/applications/agentstudio/transport.py @@ -28,13 +28,13 @@ import httpx from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.agentstudio import exceptions -from dashscope.agentstudio.constants import ( +from dashscope.applications.agentstudio import exceptions +from dashscope.applications.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -logger = logging.getLogger("dashscope.agentstudio") +logger = logging.getLogger("dashscope.applications.agentstudio") # --------------------------------------------------------------------------- # Base data classes (merged from _base.py) diff --git a/dashscope/agentstudio/types/__init__.py b/dashscope/applications/agentstudio/types/__init__.py similarity index 90% rename from dashscope/agentstudio/types/__init__.py rename to dashscope/applications/agentstudio/types/__init__.py index 2bb98d1b..0f922387 100644 --- a/dashscope/agentstudio/types/__init__.py +++ b/dashscope/applications/agentstudio/types/__init__.py @@ -2,10 +2,10 @@ """AgentStudio type definitions. Re-exports all public symbols from sub-modules for backward compatibility: - from dashscope.agentstudio.types import Agent, Message, user_message + from dashscope.applications.agentstudio.types import Agent, Message, user_message """ -from dashscope.agentstudio.types.models import * # noqa: F401,F403 -from dashscope.agentstudio.types.models import ( # noqa: F401 +from dashscope.applications.agentstudio.types.models import * # noqa: F401,F403 +from dashscope.applications.agentstudio.types.models import ( # noqa: F401 BaseModel, Metadata, Scope, @@ -94,7 +94,7 @@ WebhookEventData, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( # noqa: F401 +from dashscope.applications.agentstudio.types.params import ( # noqa: F401 AgentCreateParams, AgentUpdateParams, AgentListParams, diff --git a/dashscope/agentstudio/types/models.py b/dashscope/applications/agentstudio/types/models.py similarity index 99% rename from dashscope/agentstudio/types/models.py rename to dashscope/applications/agentstudio/types/models.py index dac21783..18fccf46 100644 --- a/dashscope/agentstudio/types/models.py +++ b/dashscope/applications/agentstudio/types/models.py @@ -27,7 +27,7 @@ Union, ) -from dashscope.agentstudio.constants import ( +from dashscope.applications.agentstudio.constants import ( SSEEventType, MessageRole, BlockType, diff --git a/dashscope/agentstudio/types/params.py b/dashscope/applications/agentstudio/types/params.py similarity index 99% rename from dashscope/agentstudio/types/params.py rename to dashscope/applications/agentstudio/types/params.py index 6742e94b..3ba6439a 100644 --- a/dashscope/agentstudio/types/params.py +++ b/dashscope/applications/agentstudio/types/params.py @@ -4,7 +4,7 @@ Each resource group (Agents, Environments, Sessions, …) defines the request-body structures for its write endpoints. All classes inherit -from :class:`~dashscope.agentstudio.types.models.BaseModel` so that +from :class:`~dashscope.applications.agentstudio.types.models.BaseModel` so that ``.to_dict()`` produces a ready-to-send wire-format dict. The ``__init__`` signature uses **user-friendly** parameter names @@ -18,7 +18,7 @@ from typing import Any, Dict, Mapping, Optional, Sequence, Union -from dashscope.agentstudio.types.models import BaseModel, ModelConfig +from dashscope.applications.agentstudio.types.models import BaseModel, ModelConfig _NOT_GIVEN = object() diff --git a/dashscope/finetune/__init__.py b/dashscope/applications/finetune/__init__.py similarity index 100% rename from dashscope/finetune/__init__.py rename to dashscope/applications/finetune/__init__.py diff --git a/dashscope/finetune/_submit.py b/dashscope/applications/finetune/_submit.py similarity index 100% rename from dashscope/finetune/_submit.py rename to dashscope/applications/finetune/_submit.py diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/applications/finetune/agentic_rl.py similarity index 97% rename from dashscope/finetune/agentic_rl.py rename to dashscope/applications/finetune/agentic_rl.py index 6abb90bf..5aaf9f7f 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/applications/finetune/agentic_rl.py @@ -10,7 +10,7 @@ Two deliberate differences from the 1.x class this was split out of: - ``submit_job`` returns ``SubmitResult`` instead of a 1.x ``FineTune`` - dataclass, and posts through ``dashscope.finetune._submit`` instead + dataclass, and posts through ``dashscope.applications.finetune._submit`` instead of inheriting ``CreateMixin``. - The api key is the constructor argument or ``DASHSCOPE_API_KEY``, never the ``dashscope.api_key`` root global. ``set_api_key`` already writes @@ -26,8 +26,8 @@ from typing import Union, List, Optional, ClassVar, Dict, Any from typing_extensions import Self -from dashscope.finetune import _submit -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune import _submit +from dashscope.applications.finetune.reinforcement import ( AgenticRLFunctionComponent, RolloutFunctionComponent, RewardFunctionComponent, @@ -35,26 +35,26 @@ TrainingDataset, ValidationDataset, ) -from dashscope.finetune.reinforcement import AgenticRLTuning, TuningModel -from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import AgenticRLTuning, TuningModel +from dashscope.applications.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.applications.finetune.reinforcement import ( FunctionType, DatasetsType, TrainingType, ) -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import ( RewardInput, RolloutInput, GroupRewardInput, ) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement import ( set_api_key, generate_random_id, get_func_type_id, deep_remove_none, ) -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.applications.finetune.reinforcement.common.errors import ( RegistrationError, ValidationError, InstanceQueryError, diff --git a/dashscope/finetune/reinforcement/__init__.py b/dashscope/applications/finetune/reinforcement/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/__init__.py rename to dashscope/applications/finetune/reinforcement/__init__.py diff --git a/dashscope/finetune/reinforcement/common/__init__.py b/dashscope/applications/finetune/reinforcement/common/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/common/__init__.py rename to dashscope/applications/finetune/reinforcement/common/__init__.py diff --git a/dashscope/finetune/reinforcement/common/constants.py b/dashscope/applications/finetune/reinforcement/common/constants.py similarity index 96% rename from dashscope/finetune/reinforcement/common/constants.py rename to dashscope/applications/finetune/reinforcement/common/constants.py index dbbaf119..a7cfdcfb 100644 --- a/dashscope/finetune/reinforcement/common/constants.py +++ b/dashscope/applications/finetune/reinforcement/common/constants.py @@ -38,7 +38,7 @@ def get_int_env(env_var: str, default: int) -> int: # The logger name is an observability contract: user log filters and # existing pipelines match on it, so it keeps the published name even # though the package itself now lives under ``dashscope``. -LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.finetune.reinforcement") +LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.applications.finetune.reinforcement") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { "trigger_token", @@ -119,7 +119,7 @@ def get_int_env(env_var: str, default: int) -> int: # sandbox, where the installed dashscope may be any published # version. Both the new public path and the old finetune path keep # resolving through reverse-reference shims. - "dashscope.finetune.reinforcement.component.server.server", + "dashscope.applications.finetune.reinforcement.component.server.server", ) FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) FC_LAYER_USED = get_bool_env("FC_LAYER_USED", True) diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py similarity index 100% rename from dashscope/finetune/reinforcement/common/errors.py rename to dashscope/applications/finetune/reinforcement/common/errors.py diff --git a/dashscope/finetune/reinforcement/common/log.py b/dashscope/applications/finetune/reinforcement/common/log.py similarity index 90% rename from dashscope/finetune/reinforcement/common/log.py rename to dashscope/applications/finetune/reinforcement/common/log.py index 3db44a58..e352ae98 100644 --- a/dashscope/finetune/reinforcement/common/log.py +++ b/dashscope/applications/finetune/reinforcement/common/log.py @@ -2,7 +2,7 @@ import logging import sys -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.applications.finetune.reinforcement.common.constants import ( LOGGER_NAME, LOG_LEVEL, ) diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/applications/finetune/reinforcement/common/model.py similarity index 99% rename from dashscope/finetune/reinforcement/common/model.py rename to dashscope/applications/finetune/reinforcement/common/model.py index d0eb5a25..3964bffc 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/applications/finetune/reinforcement/common/model.py @@ -23,7 +23,7 @@ import yaml # Local Application -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.applications.finetune.reinforcement.common.constants import ( FC_API_KEY, FC_DELETE_API, FC_LOAD_API, @@ -40,7 +40,7 @@ FC_LAYER_QUERY_API, LOG_LEVEL, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, DatasetsType, @@ -51,7 +51,7 @@ StatusType, TrainingType, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.applications.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, RolloutInput, @@ -59,7 +59,7 @@ GroupRewardInput, GroupRewardOutput, ) -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.applications.finetune.reinforcement.common.utils import ( check_file, client_fc, create_deployment_files, @@ -73,8 +73,8 @@ get_weights_from_file, deep_remove_none, ) -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.common.errors import ( InputError, OutputError, OSSConnectionError, diff --git a/dashscope/finetune/reinforcement/common/model_types.py b/dashscope/applications/finetune/reinforcement/common/model_types.py similarity index 100% rename from dashscope/finetune/reinforcement/common/model_types.py rename to dashscope/applications/finetune/reinforcement/common/model_types.py diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/applications/finetune/reinforcement/common/utils.py similarity index 99% rename from dashscope/finetune/reinforcement/common/utils.py rename to dashscope/applications/finetune/reinforcement/common/utils.py index a46015f9..9b803ce6 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/applications/finetune/reinforcement/common/utils.py @@ -21,8 +21,8 @@ ) from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement.common.errors import ( InputError, OutputError, ConfigurationError, @@ -30,7 +30,7 @@ RuntimeErrorWithCode, OSSUploadError, ) -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import ( LOG_LEVEL, DASHSCOPE_API_KEY, BAILIAN_FILE_API, @@ -47,7 +47,7 @@ LOGGER_FILTER_FIELDS, FC_WORKERS_COUNT, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, ) diff --git a/dashscope/finetune/reinforcement/component/__init__.py b/dashscope/applications/finetune/reinforcement/component/__init__.py similarity index 71% rename from dashscope/finetune/reinforcement/component/__init__.py rename to dashscope/applications/finetune/reinforcement/component/__init__.py index 93d768ba..969ef7e0 100644 --- a/dashscope/finetune/reinforcement/component/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/__init__.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- # # Base Components # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -17,28 +17,28 @@ # -------------------------------------------------------------------------- # # Data Models # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( Reward, RewardOutput, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) # -------------------------------------------------------------------------- # # Demos # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.demo import ( +from dashscope.applications.finetune.reinforcement.component.demo import ( DemoRewardProcessor, DemoRolloutProcessor, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.applications.finetune.reinforcement.component.func_decorator import ( reward_func, sub_reward_func, aggregate_func, @@ -47,7 +47,7 @@ # -------------------------------------------------------------------------- # # Func Manager # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.func_manager import ( +from dashscope.applications.finetune.reinforcement.component.func_manager import ( FuncManager, ) @@ -55,30 +55,30 @@ # Observability # # -------------------------------------------------------------------------- # # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.applications.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) # -------------------------------------------------------------------------- # # Parsers # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.reward_parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.rollout_parser import ( RolloutRequestParser, ) # -------------------------------------------------------------------------- # # Processors # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/data/__init__.py b/dashscope/applications/finetune/reinforcement/component/data/__init__.py similarity index 68% rename from dashscope/finetune/reinforcement/component/data/__init__.py rename to dashscope/applications/finetune/reinforcement/component/data/__init__.py index 6277aa74..f5d081bd 100644 --- a/dashscope/finetune/reinforcement/component/data/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/data/__init__.py @@ -2,7 +2,7 @@ # ========================================================================== # # Base Components # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -12,10 +12,10 @@ RequestMetadata, AgentOutput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) @@ -23,21 +23,21 @@ # ========================================================================== # # Input Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) # ========================================================================== # # Output Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) diff --git a/dashscope/finetune/reinforcement/component/data/base_data_model.py b/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py similarity index 98% rename from dashscope/finetune/reinforcement/component/data/base_data_model.py rename to dashscope/applications/finetune/reinforcement/component/data/base_data_model.py index c1970c53..26d09cb1 100644 --- a/dashscope/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py similarity index 88% rename from dashscope/finetune/reinforcement/component/data/group_reward_input.py rename to dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py index acd897f6..0c233040 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py @@ -9,10 +9,10 @@ from typing import Any, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_output.py b/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py similarity index 89% rename from dashscope/finetune/reinforcement/component/data/group_reward_output.py rename to dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py index 07208c84..259031e5 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py @@ -8,10 +8,10 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( Reward, ) diff --git a/dashscope/finetune/reinforcement/component/data/reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py similarity index 88% rename from dashscope/finetune/reinforcement/component/data/reward_input.py rename to dashscope/applications/finetune/reinforcement/component/data/reward_input.py index f21bd71e..993c2073 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py @@ -9,10 +9,10 @@ from typing import Any, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/reward_output.py b/dashscope/applications/finetune/reinforcement/component/data/reward_output.py similarity index 95% rename from dashscope/finetune/reinforcement/component/data/reward_output.py rename to dashscope/applications/finetune/reinforcement/component/data/reward_output.py index 93c3061c..1ad57457 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/reward_output.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, _normalize_error_code, ) diff --git a/dashscope/finetune/reinforcement/component/data/rollout_input.py b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py similarity index 92% rename from dashscope/finetune/reinforcement/component/data/rollout_input.py rename to dashscope/applications/finetune/reinforcement/component/data/rollout_input.py index ea7bfa1a..c91620cc 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py @@ -9,10 +9,10 @@ from typing import Any, Dict, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ModelResource, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/rollout_output.py b/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py similarity index 94% rename from dashscope/finetune/reinforcement/component/data/rollout_output.py rename to dashscope/applications/finetune/reinforcement/component/data/rollout_output.py index 1b923558..899faf23 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, AgentOutput, _normalize_error_code, diff --git a/dashscope/applications/finetune/reinforcement/component/demo/__init__.py b/dashscope/applications/finetune/reinforcement/component/demo/__init__.py new file mode 100644 index 00000000..44fb0350 --- /dev/null +++ b/dashscope/applications/finetune/reinforcement/component/demo/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from dashscope.applications.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoGroupRewardProcessor, +) +from dashscope.applications.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRewardProcessor, +) +from dashscope.applications.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRolloutProcessor, +) + +__all__ = [ + "DemoRewardProcessor", + # Inherits AbstractRewardProcessor, returns RewardOutput + "DemoRolloutProcessor", + # Inherits AbstractRolloutProcessor, returns RolloutOutput + "DemoGroupRewardProcessor", + # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput +] diff --git a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py similarity index 84% rename from dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py rename to dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py index a43688d6..ad06010a 100644 --- a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -6,21 +6,21 @@ Demonstrates rule-based scoring for multiple Agent outputs in a group. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py similarity index 84% rename from dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py rename to dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py index d99ad89a..4950ac2c 100644 --- a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -6,18 +6,18 @@ Demonstrates rule-based scoring for Agent outputs. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py similarity index 84% rename from dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py rename to dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py index 8099cc18..930880bd 100644 --- a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -8,18 +8,18 @@ import time -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/func_decorator.py b/dashscope/applications/finetune/reinforcement/component/func_decorator.py similarity index 97% rename from dashscope/finetune/reinforcement/component/func_decorator.py rename to dashscope/applications/finetune/reinforcement/component/func_decorator.py index e10d9464..56d23363 100644 --- a/dashscope/finetune/reinforcement/component/func_decorator.py +++ b/dashscope/applications/finetune/reinforcement/component/func_decorator.py @@ -4,8 +4,8 @@ from functools import wraps from typing import Callable, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, Reward, diff --git a/dashscope/finetune/reinforcement/component/func_manager.py b/dashscope/applications/finetune/reinforcement/component/func_manager.py similarity index 92% rename from dashscope/finetune/reinforcement/component/func_manager.py rename to dashscope/applications/finetune/reinforcement/component/func_manager.py index 048b5f13..0e60971c 100644 --- a/dashscope/finetune/reinforcement/component/func_manager.py +++ b/dashscope/applications/finetune/reinforcement/component/func_manager.py @@ -20,31 +20,31 @@ from functools import partial from typing import Any, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, trace_processor_process, async_trace_processor_process, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.applications.finetune.reinforcement.component.parser import ( BaseRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.applications.finetune.reinforcement.component.parser import ( GroupRewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.applications.finetune.reinforcement.component.parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.applications.finetune.reinforcement.component.parser import ( RolloutRequestParser, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/__init__.py b/dashscope/applications/finetune/reinforcement/component/observability/__init__.py similarity index 91% rename from dashscope/finetune/reinforcement/component/observability/__init__.py rename to dashscope/applications/finetune/reinforcement/component/observability/__init__.py index 96a00c34..4c3d1659 100644 --- a/dashscope/finetune/reinforcement/component/observability/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/__init__.py @@ -4,7 +4,7 @@ This module provides a *stable public surface* while keeping heavy observability dependencies lazily imported. User-facing imports such as:: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.applications.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, @@ -81,7 +81,7 @@ def __getattr__(name: str) -> Any: "trace_client", "trace_tool", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.applications.finetune.reinforcement.component.observability import ( genai as _genai, ) @@ -89,7 +89,7 @@ def __getattr__(name: str) -> Any: # Processor-span helpers if name in {"observe_processor", "trace_processor_span"}: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.applications.finetune.reinforcement.component.observability import ( processor_span as _processor_span, ) @@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any: "span_payload_preview", "trace_processor_process", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.applications.finetune.reinforcement.component.observability import ( tracing as _tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py similarity index 77% rename from dashscope/finetune/reinforcement/component/observability/genai/__init__.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py index 2beb244e..7fb50b3a 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py @@ -4,13 +4,13 @@ Submodules by responsibility: -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 +- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 — message and tool_calls mapping -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 +- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 — OpenAI-compatible LLM client patching -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 +- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 — DashScope Generation patching -- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.tools` +- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.tools` — tool tracing (decorator + patcher) Requires ``pip install loongsuite-util-genai`` and tracing env flags @@ -18,30 +18,30 @@ Optional: ``AGENTIC_RL_LOG_TRACE_ID=true`` enables ``trace_id`` diagnostic logs (default off); -see :mod:`dashscope.finetune.reinforcement.component.observability.tracing`. +see :mod:`dashscope.applications.finetune.reinforcement.component.observability.tracing`. """ from __future__ import annotations from typing import Any -from dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long instrument_dashscope_generation_call, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long instrument_openai_chat_completions, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long genai_llm_span, observe_llm, run_with_genai_llm_span, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long observe_tool, trace_tool, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/_core.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/_core.py similarity index 100% rename from dashscope/finetune/reinforcement/component/observability/genai/_core.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/_core.py diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py similarity index 93% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py index 19232401..4d045526 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -7,16 +7,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py index ecc6882b..d46a8959 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -16,17 +16,17 @@ from opentelemetry import trace as otel_trace from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long openai_chat_messages_to_input_messages, openai_completion_to_output_messages, unwrap_openai_completion, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py similarity index 95% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_span.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py index 635725a9..adc99419 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py @@ -12,16 +12,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/messages.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py index 44abe0f5..aabf282e 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py @@ -27,9 +27,9 @@ import os from typing import Any, Dict, List, Optional -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import deep_mask -from dashscope.finetune.reinforcement.component.observability import genai +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.common.utils import deep_mask +from dashscope.applications.finetune.reinforcement.component.observability import genai # pylint: disable=protected-access _core = genai._core diff --git a/dashscope/finetune/reinforcement/component/observability/genai/tools.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py similarity index 99% rename from dashscope/finetune/reinforcement/component/observability/genai/tools.py rename to dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py index c0d48b85..fec579f0 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/tools.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py @@ -50,7 +50,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.applications.finetune.reinforcement.component.observability import ( genai, tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/processor_span.py b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py similarity index 97% rename from dashscope/finetune/reinforcement/component/observability/processor_span.py rename to dashscope/applications/finetune/reinforcement/component/observability/processor_span.py index 65fc3824..fa589b52 100644 --- a/dashscope/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py @@ -34,11 +34,11 @@ from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar, cast -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( _REWARD_SPAN_PREFIX, _ROLLOUT_SPAN_PREFIX, apply_processor_span_attributes_after, @@ -233,7 +233,7 @@ def observe_processor( Replaces the explicit ``@trace_processor_span(logical_kind=...)`` style. """ - from dashscope.finetune.reinforcement.component.processor import ( + from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/tracing.py b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/tracing.py rename to dashscope/applications/finetune/reinforcement/component/observability/tracing.py index 288de269..ed0c9d69 100644 --- a/dashscope/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py @@ -36,11 +36,11 @@ from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) @@ -67,7 +67,7 @@ # OpenTelemetry instrumentation scope name. Renaming it would split # already-exported traces across two scopes, so it keeps the published # name even though the package now lives under ``dashscope``. -_TRACER_NAME = "dashscope.finetune.reinforcement" +_TRACER_NAME = "dashscope.applications.finetune.reinforcement" # I/O preview limits for span attributes. _IO_MAX_LEN = 128 * 1024 # 128KB per input/output preview # Per-string limit applied during JSON-friendly conversion (before diff --git a/dashscope/applications/finetune/reinforcement/component/parser/__init__.py b/dashscope/applications/finetune/reinforcement/component/parser/__init__.py new file mode 100644 index 00000000..9ee8e21a --- /dev/null +++ b/dashscope/applications/finetune/reinforcement/component/parser/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( + BaseRequestParser, +) +from dashscope.applications.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 + GroupRewardRequestParser, +) +from dashscope.applications.finetune.reinforcement.component.parser.reward_parser import ( + RewardRequestParser, +) +from dashscope.applications.finetune.reinforcement.component.parser.rollout_parser import ( + RolloutRequestParser, +) + +__all__ = [ + "BaseRequestParser", + "RewardRequestParser", + "RolloutRequestParser", + "GroupRewardRequestParser", +] diff --git a/dashscope/finetune/reinforcement/component/parser/base_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py similarity index 89% rename from dashscope/finetune/reinforcement/component/parser/base_parser.py rename to dashscope/applications/finetune/reinforcement/component/parser/base_parser.py index fac27693..8cc368ea 100644 --- a/dashscope/finetune/reinforcement/component/parser/base_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py similarity index 79% rename from dashscope/finetune/reinforcement/component/parser/group_reward_parser.py rename to dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py index aceb78d7..fa7dca9b 100644 --- a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/reward_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py similarity index 77% rename from dashscope/finetune/reinforcement/component/parser/reward_parser.py rename to dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py index 0dff8195..75503241 100644 --- a/dashscope/finetune/reinforcement/component/parser/reward_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py similarity index 77% rename from dashscope/finetune/reinforcement/component/parser/rollout_parser.py rename to dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py index 0aaed03b..1a83e1b7 100644 --- a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/__init__.py b/dashscope/applications/finetune/reinforcement/component/processor/__init__.py new file mode 100644 index 00000000..bc66518c --- /dev/null +++ b/dashscope/applications/finetune/reinforcement/component/processor/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.applications.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractGroupRewardProcessor, +) +from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractProcessor, +) +from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRewardProcessor, +) +from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRolloutProcessor, +) + +__all__ = [ + "AbstractProcessor", + "AbstractRewardProcessor", + "AbstractRolloutProcessor", + "AbstractGroupRewardProcessor", +] diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py similarity index 89% rename from dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py rename to dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py index 3c166be8..9c39dbc5 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.applications.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.applications.finetune.reinforcement.component.data import ( GroupRewardOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py similarity index 94% rename from dashscope/finetune/reinforcement/component/processor/abstract_processor.py rename to dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py index 8f13afd3..e954cf0e 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py similarity index 95% rename from dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py rename to dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py index 0b64262f..68843806 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py @@ -14,18 +14,18 @@ from concurrent.futures import ThreadPoolExecutor, Executor from typing import Optional, Dict -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( RewardOutput, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.applications.finetune.reinforcement.component.func_decorator import ( RewardProcessorMeta, SubRewardFunction, AggregateFunction, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py similarity index 87% rename from dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py rename to dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py index 9c6dace9..1e62f84c 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/server/__init__.py b/dashscope/applications/finetune/reinforcement/component/server/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/component/server/__init__.py rename to dashscope/applications/finetune/reinforcement/component/server/__init__.py diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/applications/finetune/reinforcement/component/server/server.py similarity index 93% rename from dashscope/finetune/reinforcement/component/server/server.py rename to dashscope/applications/finetune/reinforcement/component/server/server.py index 947727d9..e85c1f8f 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/applications/finetune/reinforcement/component/server/server.py @@ -10,7 +10,7 @@ FUNC_TYPE Function type, current supported values: "reward"/"rollout" (required) PROCESSOR_CLASS Full Python path to processor class, e.g.: -dashscope.finetune.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor +dashscope.applications.finetune.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor (required, processor instantiated using this class) SERVER_PORT Server port, default 8000 ENABLE_LOGGING Enable verbose logging, "true"/"1" to enable, @@ -22,14 +22,14 @@ == Startup Methods == # Recommended: Run with python -m - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor python -m - dashscope.finetune.reinforcement.component.server.server + dashscope.applications.finetune.reinforcement.component.server.server # Using uvicorn - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor uvicorn - dashscope.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + dashscope.applications.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 == Endpoints == @@ -47,19 +47,19 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.applications.finetune.reinforcement.common.log import logger +from dashscope.applications.finetune.reinforcement.common.utils import ( get_fc_request_id, get_business_summary, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component import BaseDataModel -from dashscope.finetune.reinforcement.component.func_manager import ( +from dashscope.applications.finetune.reinforcement.component import BaseDataModel +from dashscope.applications.finetune.reinforcement.component.func_manager import ( FuncManager, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( ensure_agentic_rl_baggage_span_processor, is_tracing_enabled, maybe_force_flush_async, @@ -120,7 +120,7 @@ def _resolve_func_type(raw: str) -> FuncType: if not _PROCESSOR_CLASS_ENV: raise RuntimeError( "Environment variable PROCESSOR_CLASS is required. " - "Example: PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo." + "Example: PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo." "reward_processor_demo.DemoRewardProcessor", ) @@ -590,7 +590,7 @@ async def health_check_endpoint() -> JSONResponse: if __name__ == "__main__": # Direct execution entry point for FastAPI server. # Alternative startup using uvicorn: - # uvicorn dashscope.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + # uvicorn dashscope.applications.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 import uvicorn import sys import argparse @@ -627,7 +627,7 @@ async def health_check_endpoint() -> JSONResponse: # uvicorn resolves this import string inside its own worker # process, so it must stay the public path used by # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. - "dashscope.finetune.reinforcement.component.server.server:app", + "dashscope.applications.finetune.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, workers=worker_count, diff --git a/dashscope/finetune/reinforcement/examples/observability/__init__.py b/dashscope/applications/finetune/reinforcement/examples/observability/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/observability/__init__.py rename to dashscope/applications/finetune/reinforcement/examples/observability/__init__.py diff --git a/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py similarity index 91% rename from dashscope/finetune/reinforcement/examples/observability/smoke_observability.py rename to dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py index d34cc349..5f509095 100644 --- a/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py +++ b/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py @@ -51,33 +51,33 @@ InMemorySpanExporter, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, ModelResource, ) -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.func_manager import FuncManager -from dashscope.finetune.reinforcement.component.observability.genai import ( +from dashscope.applications.finetune.reinforcement.component.func_manager import FuncManager +from dashscope.applications.finetune.reinforcement.component.observability.genai import ( observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long observe_processor, ) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/examples/workspace/README-zh.md b/dashscope/applications/finetune/reinforcement/examples/workspace/README-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README-zh.md rename to dashscope/applications/finetune/reinforcement/examples/workspace/README-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/README.md b/dashscope/applications/finetune/reinforcement/examples/workspace/README.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README.md rename to dashscope/applications/finetune/reinforcement/examples/workspace/README.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md b/dashscope/applications/finetune/reinforcement/examples/workspace/UserGuide-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md rename to dashscope/applications/finetune/reinforcement/examples/workspace/UserGuide-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide.md b/dashscope/applications/finetune/reinforcement/examples/workspace/UserGuide.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide.md rename to dashscope/applications/finetune/reinforcement/examples/workspace/UserGuide.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/cli.sh b/dashscope/applications/finetune/reinforcement/examples/workspace/cli.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/cli.sh rename to dashscope/applications/finetune/reinforcement/examples/workspace/cli.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl b/dashscope/applications/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl rename to dashscope/applications/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl b/dashscope/applications/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl rename to dashscope/applications/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/__init__.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py similarity index 94% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py index 0aad8c64..66c38ca0 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py @@ -8,15 +8,15 @@ from __future__ import annotations -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import ( GroupRewardInput, GroupRewardOutput, GroupReward, Reward, TaskStatus, ) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractGroupRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py similarity index 88% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py index aef38047..cf459190 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py @@ -16,20 +16,20 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.finetune.reinforcement import RewardInput, RewardOutput -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement import RewardInput, RewardOutput +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( Reward, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.applications.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py similarity index 92% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py index 394e7d5a..ead2d3f2 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py @@ -12,15 +12,15 @@ import time from typing import Dict -from dashscope.finetune.reinforcement import AbstractRewardProcessor -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import AbstractRewardProcessor +from dashscope.applications.finetune.reinforcement import ( RewardInput, RewardOutput, Reward, TaskStatus, ) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement import ( reward_func, sub_reward_func, aggregate_func, diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py similarity index 97% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py index 12ece959..502f8f4e 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py @@ -29,19 +29,19 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement import RolloutInput, RolloutOutput +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.applications.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) @@ -438,7 +438,7 @@ def __del__(self): if __name__ == "__main__": - from dashscope.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 + from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 ModelResource, ) import os diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py index 14cd00bb..4ca55bdd 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py @@ -36,22 +36,22 @@ from types import SimpleNamespace from typing import Any, Dict, List -from dashscope.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.applications.finetune.reinforcement import RolloutInput, RolloutOutput +from dashscope.applications.finetune.reinforcement import logger +from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.applications.finetune.reinforcement.component.observability import ( observe_processor, observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.applications.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml b/dashscope/applications/finetune/reinforcement/examples/workspace/opd-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml rename to dashscope/applications/finetune/reinforcement/examples/workspace/opd-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/requirements.txt b/dashscope/applications/finetune/reinforcement/examples/workspace/requirements.txt similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/requirements.txt rename to dashscope/applications/finetune/reinforcement/examples/workspace/requirements.txt diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json b/dashscope/applications/finetune/reinforcement/examples/workspace/resources/group_reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json rename to dashscope/applications/finetune/reinforcement/examples/workspace/resources/group_reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json b/dashscope/applications/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json rename to dashscope/applications/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json b/dashscope/applications/finetune/reinforcement/examples/workspace/resources/reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json rename to dashscope/applications/finetune/reinforcement/examples/workspace/resources/reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json b/dashscope/applications/finetune/reinforcement/examples/workspace/resources/rollout_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json rename to dashscope/applications/finetune/reinforcement/examples/workspace/resources/rollout_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml b/dashscope/applications/finetune/reinforcement/examples/workspace/rl-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml rename to dashscope/applications/finetune/reinforcement/examples/workspace/rl-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/submit_job.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py index 7311f4ad..37a7f9a9 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py @@ -48,8 +48,8 @@ # client.init(config_path="rl-job.yaml", job_name="agentic-rl") # await client.run() # Uses YAML configuration -from dashscope.finetune.agentic_rl import AgenticRL -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.agentic_rl import AgenticRL +from dashscope.applications.finetune.reinforcement import ( logger, DataSourceType, TrainingDataset, diff --git a/dashscope/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/test_functions.py rename to dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py index 56b84e7d..2ad94d97 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/test_functions.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py @@ -37,14 +37,14 @@ import json -from dashscope.finetune.reinforcement import ( +from dashscope.applications.finetune.reinforcement import ( logger, TaskStatus, FunctionType, AgenticRLFunctionComponent, FunctionComponentModel, ) -from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.applications.finetune.agentic_rl import AgenticRL async def main_functions(): diff --git a/dashscope/client.py b/dashscope/client.py index 4e454289..cdd01d88 100644 --- a/dashscope/client.py +++ b/dashscope/client.py @@ -40,6 +40,9 @@ from dashscope.transport.ws import Connector from dashscope.transport.ws_connection import WsConnection +# Historical dashscope.client.* children resolve through the 1.x alias finder. +__path__ = [] + def _http_limits(config: Config) -> httpx.Limits: """Connection-pool sizes for the client's own httpx client.""" diff --git a/dashscope/finetune/reinforcement/component/demo/__init__.py b/dashscope/finetune/reinforcement/component/demo/__init__.py deleted file mode 100644 index 6100812f..00000000 --- a/dashscope/finetune/reinforcement/component/demo/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRolloutProcessor, -) - -__all__ = [ - "DemoRewardProcessor", - # Inherits AbstractRewardProcessor, returns RewardOutput - "DemoRolloutProcessor", - # Inherits AbstractRolloutProcessor, returns RolloutOutput - "DemoGroupRewardProcessor", - # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput -] diff --git a/dashscope/finetune/reinforcement/component/parser/__init__.py b/dashscope/finetune/reinforcement/component/parser/__init__.py deleted file mode 100644 index 6f5dedd0..00000000 --- a/dashscope/finetune/reinforcement/component/parser/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.parser.base_parser import ( - BaseRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 - GroupRewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( - RewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( - RolloutRequestParser, -) - -__all__ = [ - "BaseRequestParser", - "RewardRequestParser", - "RolloutRequestParser", - "GroupRewardRequestParser", -] diff --git a/dashscope/finetune/reinforcement/component/processor/__init__.py b/dashscope/finetune/reinforcement/component/processor/__init__.py deleted file mode 100644 index 9246fa56..00000000 --- a/dashscope/finetune/reinforcement/component/processor/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRolloutProcessor, -) - -__all__ = [ - "AbstractProcessor", - "AbstractRewardProcessor", - "AbstractRolloutProcessor", - "AbstractGroupRewardProcessor", -] diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index 2ce6ec93..b32b0317 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -1,55 +1,119 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Backward-compat aliasing for the historical top-level 1.x import paths. - -The 1.x business surface now lives under ``dashscope.legacy_1x``. The thin -shim packages left at the old top-level names (``dashscope/aigc/__init__.py`` -and friends) call :func:`install` so that ``import dashscope.aigc`` and -``from dashscope.aigc.generation import Generation`` keep resolving. - -The shim re-exports the implementation's public names and points its -``__path__`` at the implementation package, so a submodule import loads the -real file. Submodules reached through the old spelling are therefore distinct -module objects from their ``dashscope.legacy_1x.*`` twins; nothing inside this -repository relies on that (all internal code, tests and examples use the -canonical ``dashscope.legacy_1x`` path, and ``import dashscope`` re-exports the -public names directly), so the aliasing is best-effort back-compat for external -callers only. It is deliberately lazy -- it never imports submodules itself -- -so importing a shim cannot trigger import-time side effects in the 1.x tree -(the ``dashscope.legacy_1x.cli`` package has some). -""" +"""Lazy aliases for the historical 1.x import paths.""" from __future__ import annotations import importlib +import importlib.abc +import importlib.util import sys -def install(shim_name: str, impl_name: str): - """Alias the package/module ``shim_name`` onto ``impl_name``. - - Returns the implementation module so a shim can also do - ``_impl = install(__name__, "dashscope.legacy_1x.aigc")`` when it wants - to reference it. - """ - impl = importlib.import_module(impl_name) - shim = sys.modules[shim_name] - - # Some 1.x packages declare a malformed ``__all__`` (e.g. ``audio`` - # lists module objects rather than names), so union the string entries - # of ``__all__`` with every public attribute instead of trusting it. - declared = getattr(impl, "__all__", None) or [] - names = {name for name in declared if isinstance(name, str)} - names.update(name for name in dir(impl) if not name.startswith("_")) - for name in names: - try: - setattr(shim, name, getattr(impl, name)) - except AttributeError: # pragma: no cover - name vanished mid-walk - continue - - if hasattr(impl, "__path__"): - # Let ``import dashscope..`` find the implementation's - # files without eagerly importing them here. - shim.__path__ = impl.__path__ - - return impl +_APPLICATION_ROOTS = frozenset({"agentstudio", "finetune"}) + +_PACKAGE_ROOTS = frozenset( + { + "aigc", + "api_entities", + "app", + "assistants", + "audio", + "cli", + "common", + "embeddings", + "io", + "multimodal", + "nlp", + "protocol", + "rerank", + "threads", + "utils", + }, +) + +_EXACT_MODULES = { + "dashscope.files": "dashscope.legacy_1x.files", + "dashscope.model": "dashscope.legacy_1x.model", + "dashscope.models": "dashscope.legacy_1x.models", + "dashscope.finetune.customize_types": ( + "dashscope.legacy_1x.finetune.customize_types" + ), + "dashscope.finetune.deployments": ( + "dashscope.legacy_1x.finetune.deployments" + ), + "dashscope.finetune.finetunes": "dashscope.legacy_1x.finetune.finetunes", +} + +_FINDER_MARKER = "_dashscope_legacy_1x_alias_finder" + + +def _canonical_name(name): + if name in _EXACT_MODULES: + return _EXACT_MODULES[name] + if name.startswith("dashscope.client."): + return "dashscope.legacy_1x.client." + name[len("dashscope.client.") :] + parts = name.split(".") + if len(parts) > 1 and parts[0] == "dashscope": + if parts[1] in _APPLICATION_ROOTS: + return "dashscope.applications." + ".".join(parts[1:]) + if parts[1] in _PACKAGE_ROOTS: + return "dashscope.legacy_1x." + ".".join(parts[1:]) + return None + + +class _AliasLoader(importlib.abc.Loader): + def __init__(self, alias_name, canonical_name, canonical_spec): + self._alias_name = alias_name + self._canonical_name = canonical_name + self._canonical_spec = canonical_spec + + def create_module(self, spec): + return None + + def exec_module(self, module): + sys.modules[self._alias_name] = importlib.import_module( + self._canonical_name, + ) + + def get_code(self, fullname): + loader = self._canonical_spec.loader + if loader is None or not hasattr(loader, "get_code"): + return None + return loader.get_code(self._canonical_name) + + +class _AliasFinder(importlib.abc.MetaPathFinder): + _dashscope_legacy_1x_alias_finder = True + + def find_spec(self, fullname, path=None, target=None): + canonical_name = _canonical_name(fullname) + if canonical_name is None: + return None + canonical_spec = importlib.util.find_spec(canonical_name) + if canonical_spec is None: + return None + loader = _AliasLoader(fullname, canonical_name, canonical_spec) + is_package = canonical_spec.submodule_search_locations is not None + spec = importlib.util.spec_from_loader( + fullname, + loader, + origin=canonical_spec.origin, + is_package=is_package, + ) + if is_package: + spec.submodule_search_locations = ( + canonical_spec.submodule_search_locations + ) + return spec + + +def install(): + """Install the process-wide lazy aliases without importing business modules.""" + for finder in sys.meta_path: + if getattr(finder, _FINDER_MARKER, False): + return + sys.meta_path.insert(0, _AliasFinder()) + + +__all__ = ["install"] diff --git a/tests/legacy_1x/unit/test_acli_sdk_contract.py b/tests/legacy_1x/unit/test_acli_sdk_contract.py index 7a44f428..75ca41dc 100644 --- a/tests/legacy_1x/unit/test_acli_sdk_contract.py +++ b/tests/legacy_1x/unit/test_acli_sdk_contract.py @@ -45,8 +45,8 @@ _REPO / "dashscope" / "resources", _REPO / "dashscope" / "transport", _REPO / "dashscope" / "types", - _REPO / "dashscope" / "agentstudio", - _REPO / "dashscope" / "finetune", + _REPO / "dashscope" / "applications" / "agentstudio", + _REPO / "dashscope" / "applications" / "finetune", ) _ROOT_INIT = _REPO / "dashscope" / "__init__.py" diff --git a/tests/legacy_1x/unit/test_layer_decoupling.py b/tests/legacy_1x/unit/test_layer_decoupling.py index 5ce04de8..dcb59e75 100644 --- a/tests/legacy_1x/unit/test_layer_decoupling.py +++ b/tests/legacy_1x/unit/test_layer_decoupling.py @@ -22,16 +22,16 @@ _DASHSCOPE / "_error_registry.py", _DASHSCOPE / "_logging.py", _DASHSCOPE / "_quickstart.py", - _DASHSCOPE / "finetune" / "agentic_rl.py", + _DASHSCOPE / "applications" / "finetune" / "agentic_rl.py", _DASHSCOPE / "client.py", _DASHSCOPE / "errors.py", - _DASHSCOPE / "finetune" / "_submit.py", + _DASHSCOPE / "applications" / "finetune" / "_submit.py", _DASHSCOPE / "protocols", _DASHSCOPE / "resources", _DASHSCOPE / "transport", _DASHSCOPE / "types", - _DASHSCOPE / "agentstudio", - _DASHSCOPE / "finetune" / "reinforcement", + _DASHSCOPE / "applications" / "agentstudio", + _DASHSCOPE / "applications" / "finetune" / "reinforcement", _DASHSCOPE / "tokenizers" / "tokenizer.py", _DASHSCOPE / "tokenizers" / "tokenizer_base.py", _DASHSCOPE / "tokenizers" / "qwen_tokenizer.py", @@ -44,6 +44,7 @@ "_shared", "acli", "agentstudio", + "applications", "finetune", "protocols", "resources", @@ -71,7 +72,7 @@ _COMPATIBILITY_IMPORTS = { ( - "dashscope/finetune/reinforcement/__init__.py", + "dashscope/applications/finetune/reinforcement/__init__.py", "dashscope.legacy_1x.cli.agentic_rl", ), } diff --git a/tests/legacy_1x/unit/test_opd.py b/tests/legacy_1x/unit/test_opd.py index b49995d7..dc0e916f 100644 --- a/tests/legacy_1x/unit/test_opd.py +++ b/tests/legacy_1x/unit/test_opd.py @@ -110,7 +110,7 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): def test_opd_example_reuses_supported_rl_configuration(rl_client): config = ( Path(__file__).parents[3] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/applications/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) config_text = config.read_text(encoding="utf-8") @@ -158,7 +158,7 @@ def test_opd_example_reuses_supported_rl_configuration(rl_client): async def test_opd_example_uploads_local_datasets(rl_client): config = ( Path(__file__).parents[3] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/applications/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) rl_client.init(config_path=str(config)) @@ -180,7 +180,7 @@ async def test_opd_example_uploads_local_datasets(rl_client): def test_regular_rl_example_keeps_resources_and_rl_hyperparameters(rl_client): config = ( Path(__file__).parents[3] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/applications/finetune/reinforcement/examples/workspace" / "rl-job.yaml" ) rl_client.init(config_path=str(config)) diff --git a/tests/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py index 6c6b397b..5cd1791e 100644 --- a/tests/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -8,6 +8,7 @@ from __future__ import annotations +import importlib import sys import pytest @@ -84,8 +85,10 @@ def test_public_names_are_exported(): def test_client_classes_have_root_module_ownership(): - assert impl.Client.__module__ == "dashscope.agentstudio.client" - assert impl.AsyncClient.__module__ == "dashscope.agentstudio.client" + assert impl.Client.__module__ == "dashscope.applications.agentstudio.client" + assert impl.AsyncClient.__module__ == ( + "dashscope.applications.agentstudio.client" + ) def test_exception_aliases_use_root_errors(): @@ -97,8 +100,10 @@ def test_exception_aliases_use_root_errors(): @pytest.mark.parametrize("name", SUBMODULES) def test_deep_imports_resolve_to_root_modules(name): module_name = f"dashscope.agentstudio.{name}" - __import__(module_name) - assert sys.modules[module_name].__name__ == module_name + canonical_name = f"dashscope.applications.agentstudio.{name}" + assert importlib.import_module(module_name) is importlib.import_module( + canonical_name, + ) @pytest.mark.parametrize("name", SUBMODULES) @@ -116,8 +121,10 @@ def test_deep_import_from_syntax_resolves(): from dashscope.agentstudio.resources.agents import Agents from dashscope.agentstudio.types.models import Agent - assert Agents.__module__ == "dashscope.agentstudio.resources.agents" - assert Agent.__module__ == "dashscope.agentstudio.types.models" + assert Agents.__module__ == ( + "dashscope.applications.agentstudio.resources.agents" + ) + assert Agent.__module__ == "dashscope.applications.agentstudio.types.models" # --------------------------------------------------------------------------- diff --git a/tests/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py index bc68339b..837ecbfd 100644 --- a/tests/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -160,7 +160,7 @@ def test_server_module_resolves_without_importing_fastapi(alias): submit_path = f"{alias}.component.server.server" spec = importlib.util.find_spec(submit_path) assert spec is not None - assert "dashscope/finetune/reinforcement" in spec.origin.replace( + assert "dashscope/applications/finetune/reinforcement" in spec.origin.replace( "\\", "/", ) @@ -387,7 +387,10 @@ def test_the_v2_mount_points_at_the_v2_workflow_modules(): def test_the_rl_error_module_uses_the_public_path(): public_errors = importlib.import_module(f"{PUBLIC}.common.errors") - assert public_errors.__name__ == f"{PUBLIC}.common.errors" + canonical_errors = importlib.import_module( + "dashscope.applications.finetune.reinforcement.common.errors", + ) + assert public_errors is canonical_errors assert public_errors.ValueErrorWithCode is ValueErrorWithCode diff --git a/tests/test_legacy_import_aliases.py b/tests/test_legacy_import_aliases.py new file mode 100644 index 00000000..f51eda3a --- /dev/null +++ b/tests/test_legacy_import_aliases.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from __future__ import annotations + +import importlib +import os +import pathlib +import subprocess +import sys +from unittest.mock import patch + +import pytest + +import dashscope +from dashscope.aigc.multimodal_conversation import MultiModalConversation +from dashscope.audio.qwen_asr import QwenTranscription +from dashscope.finetune.finetunes import FineTunes + + +_REPO = pathlib.Path(__file__).resolve().parents[1] + + +def _run_python(*args): + env = os.environ.copy() + env["PYTHONDONTWRITEBYTECODE"] = "1" + result = subprocess.run( + [sys.executable, *args], + cwd=_REPO, + env=env, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_all_legacy_module_aliases_in_clean_process(): + _run_python( + "-c", + """ +import importlib +import dashscope + +packages = ( + 'aigc', 'api_entities', 'app', 'assistants', 'audio', 'cli', + 'common', 'embeddings', 'io', 'multimodal', 'nlp', 'protocol', + 'rerank', 'threads', 'utils', +) +modules = ('files', 'model', 'models') +for name in packages + modules: + old = importlib.import_module(f'dashscope.{name}') + canonical = importlib.import_module(f'dashscope.legacy_1x.{name}') + assert old is canonical, name + assert getattr(dashscope, name) is old, name + +for name in ('customize_types', 'deployments', 'finetunes'): + old = importlib.import_module(f'dashscope.finetune.{name}') + canonical = importlib.import_module( + f'dashscope.legacy_1x.finetune.{name}', + ) + assert old is canonical, name +""", + ) + + +def test_target_legacy_classes_keep_root_identity(): + assert dashscope.FineTunes is FineTunes + assert dashscope.MultiModalConversation is MultiModalConversation + + +@pytest.mark.parametrize( + ("patch_target", "api"), + [ + ("dashscope.finetune.finetunes.FineTunes.call", FineTunes), + ( + "dashscope.aigc.multimodal_conversation.MultiModalConversation.call", + MultiModalConversation, + ), + ( + "dashscope.audio.qwen_asr.qwen_transcription.QwenTranscription.call", + QwenTranscription, + ), + ], +) +def test_target_legacy_calls_accept_old_patch_paths(patch_target, api): + sentinel = object() + with patch(patch_target, return_value=sentinel) as call: + result = api.call(model="test-model") + assert result is sentinel + call.assert_called_once_with(model="test-model") + + +def test_client_and_legacy_base_api_coexist(): + from dashscope.client import AsyncClient, Client + from dashscope.client.base_api import BaseApi + + canonical = importlib.import_module("dashscope.legacy_1x.client.base_api") + assert importlib.import_module("dashscope.client.base_api") is canonical + assert BaseApi is canonical.BaseApi + assert Client is dashscope.Client + assert AsyncClient is dashscope.AsyncClient + assert Client.__module__ == "dashscope.client" + + +@pytest.mark.parametrize( + ("old_name", "canonical_name"), + [ + ("dashscope.agentstudio", "dashscope.applications.agentstudio"), + ( + "dashscope.agentstudio.types.models", + "dashscope.applications.agentstudio.types.models", + ), + ( + "dashscope.finetune.agentic_rl", + "dashscope.applications.finetune.agentic_rl", + ), + ( + "dashscope.finetune.reinforcement.common.errors", + "dashscope.applications.finetune.reinforcement.common.errors", + ), + ], +) +def test_application_public_paths_are_aliases(old_name, canonical_name): + assert importlib.import_module(old_name) is importlib.import_module( + canonical_name, + ) + + +def test_agentic_rl_current_contract_is_unchanged(): + module = importlib.import_module("dashscope.finetune.agentic_rl") + assert not hasattr(module.AgenticRL, "list") + assert module.SubmitResult is not None + + +def test_plain_import_preserves_cli_and_assistants_laziness(): + _run_python( + "-c", + """ +import sys +import dashscope + +names = ( + 'dashscope.cli', + 'dashscope.legacy_1x.cli', + 'dashscope.assistants', + 'dashscope.legacy_1x.assistants', +) +assert not any(name in sys.modules for name in names) +""", + ) + + +def test_historical_cli_module_entrypoint(): + _run_python("-m", "dashscope.cli", "--help") From 39163feba046d30749b5925933e30a69a81aa486 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 20:20:38 +0800 Subject: [PATCH 12/41] chore(release): prepare 2.0.3 --- .pre-commit-config.yaml | 2 ++ dashscope/__init__.py | 4 +-- dashscope/__init__.pyi | 23 ++++++++++++ dashscope/applications/agentstudio/client.py | 35 +++++++++++++++---- .../applications/agentstudio/exceptions.py | 2 ++ .../agentstudio/resources/__init__.py | 15 ++++++-- .../agentstudio/resources/agents.py | 6 +++- .../agentstudio/resources/environments.py | 5 ++- .../agentstudio/resources/security.py | 9 +++-- .../agentstudio/resources/session_events.py | 5 ++- .../resources/session_resources.py | 5 ++- .../agentstudio/resources/skills.py | 6 +++- .../applications/agentstudio/types/params.py | 5 ++- dashscope/applications/finetune/agentic_rl.py | 9 +++-- .../reinforcement/common/constants.py | 5 ++- .../finetune/reinforcement/common/errors.py | 1 + .../reinforcement/component/__init__.py | 28 +++++++-------- .../reinforcement/component/data/__init__.py | 14 ++++---- .../component/data/group_reward_input.py | 2 +- .../component/data/group_reward_output.py | 4 +-- .../component/data/reward_input.py | 2 +- .../component/data/reward_output.py | 2 +- .../component/data/rollout_input.py | 2 +- .../component/data/rollout_output.py | 2 +- .../demo/group_reward_processor_demo.py | 10 +++--- .../component/demo/reward_processor_demo.py | 8 ++--- .../component/demo/rollout_processor_demo.py | 8 ++--- .../reinforcement/component/func_manager.py | 14 ++++---- .../component/observability/__init__.py | 8 ++--- .../component/observability/genai/__init__.py | 20 +++++------ .../observability/genai/llm_dashscope.py | 6 ++-- .../observability/genai/llm_openai.py | 6 ++-- .../component/observability/genai/llm_span.py | 6 ++-- .../component/observability/genai/messages.py | 8 +++-- .../component/observability/processor_span.py | 6 ++-- .../component/observability/tracing.py | 2 +- .../component/parser/__init__.py | 8 ++--- .../component/parser/base_parser.py | 2 +- .../component/processor/__init__.py | 8 ++--- .../component/processor/abstract_processor.py | 2 +- .../processor/abstract_rollout_processor.py | 6 ++-- .../reinforcement/component/server/server.py | 26 ++++++++------ .../observability/smoke_observability.py | 20 ++++++----- .../functions/reward/group_reward.py | 6 ++-- .../workspace/functions/reward/reward.py | 15 ++++---- .../functions/reward/reward_decorator.py | 10 +++--- .../workspace/functions/rollout/rollout.py | 13 ++++--- .../functions/rollout/rollout_only.py | 13 ++++--- .../examples/workspace/submit_job.py | 4 +-- .../examples/workspace/test_functions.py | 4 +-- dashscope/legacy_1x/_shim.py | 2 ++ dashscope/version.py | 2 +- tests/legacy_1x/__init__.py | 1 + .../integration/test_large_utf8_payload.py | 4 ++- .../legacy_1x/unit/test_acli_sdk_contract.py | 8 ++--- tests/legacy_1x/unit/test_tokenizer.py | 7 ++-- tests/test_agentstudio_shim.py | 8 +++-- tests/test_errors.py | 2 +- tests/test_finetune_rl_shim.py | 13 ++++--- 59 files changed, 300 insertions(+), 179 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bebb2bb5..a9e4c914 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,6 +97,7 @@ repos: --disable=R0913, --disable=E0401, --disable=E1101, + --disable=E0611, # Runtime meta-path aliases are invisible to astroid --disable=C0415, --disable=W0603, --disable=R1705, @@ -142,6 +143,7 @@ repos: --disable=R0913, --disable=E0401, --disable=E1101, + --disable=E0611, # Runtime meta-path aliases are invisible to astroid --disable=C0415, --disable=W0603, --disable=R1705, diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 32fae212..c44c4676 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -200,9 +200,7 @@ def __getattr__(name): def __dir__(): return sorted( - set(globals()) - | set(_DEPRECATED_LAZY_IMPORTS) - | _LEGACY_LAZY_MODULES, + set(globals()) | set(_DEPRECATED_LAZY_IMPORTS) | _LEGACY_LAZY_MODULES, ) diff --git a/dashscope/__init__.pyi b/dashscope/__init__.pyi index cb36deef..3dd4d843 100644 --- a/dashscope/__init__.pyi +++ b/dashscope/__init__.pyi @@ -43,6 +43,29 @@ def close_shared_aio_session() -> None: ... def close_shared_sync_session() -> None: ... def set_region(region: str, workspace_id: Optional[str] = ...) -> None: ... +# Lazily resolved compatibility modules. +aigc: Any +agentstudio: Any +api_entities: Any +app: Any +applications: Any +assistants: Any +audio: Any +cli: Any +common: Any +embeddings: Any +files: Any +finetune: Any +io: Any +model: Any +models: Any +multimodal: Any +nlp: Any +protocol: Any +rerank: Any +threads: Any +utils: Any + # --------------------------------------------------------------------------- # Response types # --------------------------------------------------------------------------- diff --git a/dashscope/applications/agentstudio/client.py b/dashscope/applications/agentstudio/client.py index 62fc8a85..55d543f0 100644 --- a/dashscope/applications/agentstudio/client.py +++ b/dashscope/applications/agentstudio/client.py @@ -13,7 +13,10 @@ validate_region, validate_workspace_id, ) -from dashscope.applications.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.applications.agentstudio.resources.agents import ( + Agents, + AsyncAgents, +) from dashscope.applications.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, @@ -24,15 +27,30 @@ Environments, AsyncEnvironments, ) -from dashscope.applications.agentstudio.resources.files import Files, AsyncFiles +from dashscope.applications.agentstudio.resources.files import ( + Files, + AsyncFiles, +) from dashscope.applications.agentstudio.resources.memory_stores import ( MemoryStores, AsyncMemoryStores, ) -from dashscope.applications.agentstudio.resources.security import Security, AsyncSecurity -from dashscope.applications.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.applications.agentstudio.resources.skills import Skills, AsyncSkills -from dashscope.applications.agentstudio.resources.vaults import Vaults, AsyncVaults +from dashscope.applications.agentstudio.resources.security import ( + Security, + AsyncSecurity, +) +from dashscope.applications.agentstudio.resources.sessions import ( + Sessions, + AsyncSessions, +) +from dashscope.applications.agentstudio.resources.skills import ( + Skills, + AsyncSkills, +) +from dashscope.applications.agentstudio.resources.vaults import ( + Vaults, + AsyncVaults, +) from dashscope.applications.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, @@ -43,7 +61,10 @@ AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.applications.agentstudio.transport import SyncTransport, AsyncTransport +from dashscope.applications.agentstudio.transport import ( + SyncTransport, + AsyncTransport, +) from dashscope._config import _api_key_from_file, settings diff --git a/dashscope/applications/agentstudio/exceptions.py b/dashscope/applications/agentstudio/exceptions.py index 641ce461..a1920b56 100644 --- a/dashscope/applications/agentstudio/exceptions.py +++ b/dashscope/applications/agentstudio/exceptions.py @@ -37,6 +37,8 @@ from dashscope import _error_registry as _registry from dashscope import errors as _v2_errors + +# pylint: disable-next=unused-import from dashscope.errors import ( # noqa: F401 APIConnectionError, APIStatusError, diff --git a/dashscope/applications/agentstudio/resources/__init__.py b/dashscope/applications/agentstudio/resources/__init__.py index 722179cf..54eb828b 100644 --- a/dashscope/applications/agentstudio/resources/__init__.py +++ b/dashscope/applications/agentstudio/resources/__init__.py @@ -15,7 +15,10 @@ client.sessions.events.send(session.id, [user_message("hello")]) """ -from dashscope.applications.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.applications.agentstudio.resources.agents import ( + Agents, + AsyncAgents, +) from dashscope.applications.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, @@ -26,7 +29,10 @@ Environments, AsyncEnvironments, ) -from dashscope.applications.agentstudio.resources.files import Files, AsyncFiles +from dashscope.applications.agentstudio.resources.files import ( + Files, + AsyncFiles, +) from dashscope.applications.agentstudio.resources.memory_stores import ( AsyncMemories, AsyncMemoryStores, @@ -41,7 +47,10 @@ _AioTypedEventStream, _TypedEventStream, ) -from dashscope.applications.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.applications.agentstudio.resources.sessions import ( + Sessions, + AsyncSessions, +) from dashscope.applications.agentstudio.resources.skills import ( AsyncSkillVersions, AsyncSkills, diff --git a/dashscope/applications/agentstudio/resources/agents.py b/dashscope/applications/agentstudio/resources/agents.py index c372fa60..d6040ed0 100644 --- a/dashscope/applications/agentstudio/resources/agents.py +++ b/dashscope/applications/agentstudio/resources/agents.py @@ -15,7 +15,11 @@ _coerce_agent, _coerce_agent_version, ) -from dashscope.applications.agentstudio.types import Agent, AgentVersion, ModelConfig +from dashscope.applications.agentstudio.types import ( + Agent, + AgentVersion, + ModelConfig, +) from dashscope.applications.agentstudio.types.params import ( AgentCreateParams, AgentListParams, diff --git a/dashscope/applications/agentstudio/resources/environments.py b/dashscope/applications/agentstudio/resources/environments.py index 5615c9fa..475ff309 100644 --- a/dashscope/applications/agentstudio/resources/environments.py +++ b/dashscope/applications/agentstudio/resources/environments.py @@ -14,7 +14,10 @@ from dashscope.applications.agentstudio.resources._helpers import ( _coerce_env, ) -from dashscope.applications.agentstudio.types import DeleteResponse, Environment +from dashscope.applications.agentstudio.types import ( + DeleteResponse, + Environment, +) from dashscope.applications.agentstudio.types.params import ( EnvironmentCreateParams, EnvironmentListParams, diff --git a/dashscope/applications/agentstudio/resources/security.py b/dashscope/applications/agentstudio/resources/security.py index 78055a19..62c3d686 100644 --- a/dashscope/applications/agentstudio/resources/security.py +++ b/dashscope/applications/agentstudio/resources/security.py @@ -12,8 +12,13 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.applications.agentstudio.types import SecurityAlertList, SecurityOverview -from dashscope.applications.agentstudio.types.params import SecurityListAgentLogsParams +from dashscope.applications.agentstudio.types import ( + SecurityAlertList, + SecurityOverview, +) +from dashscope.applications.agentstudio.types.params import ( + SecurityListAgentLogsParams, +) _PATH_SECURITY_OVERVIEW = "/security/overview" _PATH_SECURITY_AGENT_LOGS = "/security/agent_logs" diff --git a/dashscope/applications/agentstudio/resources/session_events.py b/dashscope/applications/agentstudio/resources/session_events.py index 504bef5b..42499b9c 100644 --- a/dashscope/applications/agentstudio/resources/session_events.py +++ b/dashscope/applications/agentstudio/resources/session_events.py @@ -24,7 +24,10 @@ _events_path, _stream_path, ) -from dashscope.applications.agentstudio.streaming import AsyncEventStream, EventStream +from dashscope.applications.agentstudio.streaming import ( + AsyncEventStream, + EventStream, +) from dashscope.applications.agentstudio.types import ServerEvent from dashscope.applications.agentstudio.types.params import ( SessionEventListParams, diff --git a/dashscope/applications/agentstudio/resources/session_resources.py b/dashscope/applications/agentstudio/resources/session_resources.py index 4571126a..10478b6a 100644 --- a/dashscope/applications/agentstudio/resources/session_resources.py +++ b/dashscope/applications/agentstudio/resources/session_resources.py @@ -20,7 +20,10 @@ _session_resource_item_path, _session_resources_path, ) -from dashscope.applications.agentstudio.types import DeleteResponse, SessionResource +from dashscope.applications.agentstudio.types import ( + DeleteResponse, + SessionResource, +) from dashscope.applications.agentstudio.types.params import ( SessionResourceAddParams, SessionResourceListParams, diff --git a/dashscope/applications/agentstudio/resources/skills.py b/dashscope/applications/agentstudio/resources/skills.py index cd8ecda9..926cfa51 100644 --- a/dashscope/applications/agentstudio/resources/skills.py +++ b/dashscope/applications/agentstudio/resources/skills.py @@ -16,7 +16,11 @@ _coerce_skill, _coerce_skill_version, ) -from dashscope.applications.agentstudio.types import DeleteResponse, Skill, SkillVersion +from dashscope.applications.agentstudio.types import ( + DeleteResponse, + Skill, + SkillVersion, +) from dashscope.applications.agentstudio.types.params import ( SkillCreateParams, SkillListParams, diff --git a/dashscope/applications/agentstudio/types/params.py b/dashscope/applications/agentstudio/types/params.py index 3ba6439a..7b76d08e 100644 --- a/dashscope/applications/agentstudio/types/params.py +++ b/dashscope/applications/agentstudio/types/params.py @@ -18,7 +18,10 @@ from typing import Any, Dict, Mapping, Optional, Sequence, Union -from dashscope.applications.agentstudio.types.models import BaseModel, ModelConfig +from dashscope.applications.agentstudio.types.models import ( + BaseModel, + ModelConfig, +) _NOT_GIVEN = object() diff --git a/dashscope/applications/finetune/agentic_rl.py b/dashscope/applications/finetune/agentic_rl.py index 5aaf9f7f..30bb9992 100644 --- a/dashscope/applications/finetune/agentic_rl.py +++ b/dashscope/applications/finetune/agentic_rl.py @@ -35,8 +35,13 @@ TrainingDataset, ValidationDataset, ) -from dashscope.applications.finetune.reinforcement import AgenticRLTuning, TuningModel -from dashscope.applications.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.applications.finetune.reinforcement import ( + AgenticRLTuning, + TuningModel, +) +from dashscope.applications.finetune.reinforcement import ( + DASHSCOPE_HTTP_BASE_URL, +) from dashscope.applications.finetune.reinforcement import ( FunctionType, DatasetsType, diff --git a/dashscope/applications/finetune/reinforcement/common/constants.py b/dashscope/applications/finetune/reinforcement/common/constants.py index a7cfdcfb..23071fa1 100644 --- a/dashscope/applications/finetune/reinforcement/common/constants.py +++ b/dashscope/applications/finetune/reinforcement/common/constants.py @@ -38,7 +38,10 @@ def get_int_env(env_var: str, default: int) -> int: # The logger name is an observability contract: user log filters and # existing pipelines match on it, so it keeps the published name even # though the package itself now lives under ``dashscope``. -LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.applications.finetune.reinforcement") +LOGGER_NAME = os.environ.get( + "LOGGER_NAME", + "dashscope.applications.finetune.reinforcement", +) LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { "trigger_token", diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index 58991360..b0c18fe7 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=too-many-ancestors """Native v2 errors for AgenticRL, with legacy numeric-code compatibility.""" from datetime import datetime diff --git a/dashscope/applications/finetune/reinforcement/component/__init__.py b/dashscope/applications/finetune/reinforcement/component/__init__.py index 969ef7e0..5abcee5f 100644 --- a/dashscope/applications/finetune/reinforcement/component/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/__init__.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- # # Base Components # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -17,28 +17,28 @@ # -------------------------------------------------------------------------- # # Data Models # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from .data.reward_input import ( RewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from .data.reward_output import ( Reward, RewardOutput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from .data.rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( +from .data.rollout_output import ( RolloutOutput, ) # -------------------------------------------------------------------------- # # Demos # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.demo import ( +from .demo import ( DemoRewardProcessor, DemoRolloutProcessor, ) -from dashscope.applications.finetune.reinforcement.component.func_decorator import ( +from .func_decorator import ( reward_func, sub_reward_func, aggregate_func, @@ -47,7 +47,7 @@ # -------------------------------------------------------------------------- # # Func Manager # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.func_manager import ( +from .func_manager import ( FuncManager, ) @@ -55,30 +55,30 @@ # Observability # # -------------------------------------------------------------------------- # # pylint: disable=no-name-in-module -from dashscope.applications.finetune.reinforcement.component.observability import ( +from .observability import ( observe_processor, ) -from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( +from .parser.base_parser import ( BaseRequestParser, ) # -------------------------------------------------------------------------- # # Parsers # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.parser.reward_parser import ( +from .parser.reward_parser import ( RewardRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser.rollout_parser import ( +from .parser.rollout_parser import ( RolloutRequestParser, ) # -------------------------------------------------------------------------- # # Processors # # -------------------------------------------------------------------------- # -from dashscope.applications.finetune.reinforcement.component.processor import ( +from .processor import ( AbstractRewardProcessor, ) -from dashscope.applications.finetune.reinforcement.component.processor import ( +from .processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/__init__.py b/dashscope/applications/finetune/reinforcement/component/data/__init__.py index f5d081bd..b754af3c 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/data/__init__.py @@ -2,7 +2,7 @@ # ========================================================================== # # Base Components # # ========================================================================== # -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -12,10 +12,10 @@ RequestMetadata, AgentOutput, ) -from dashscope.applications.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from .group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from .group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) @@ -23,21 +23,21 @@ # ========================================================================== # # Input Models # # ========================================================================== # -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from .reward_input import ( RewardInput, ) # ========================================================================== # # Output Models # # ========================================================================== # -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from .reward_output import ( RewardOutput, Reward, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from .rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( +from .rollout_output import ( RolloutOutput, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py index 0c233040..51808635 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py @@ -12,7 +12,7 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py b/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py index 259031e5..f1375798 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/group_reward_output.py @@ -8,10 +8,10 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( TaskStatus, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from .reward_output import ( Reward, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py index 993c2073..7e1801cf 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py @@ -12,7 +12,7 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/applications/finetune/reinforcement/component/data/reward_output.py b/dashscope/applications/finetune/reinforcement/component/data/reward_output.py index 1ad57457..fc88122c 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/reward_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/reward_output.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( TaskStatus, _normalize_error_code, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py index c91620cc..f5007cd1 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py @@ -12,7 +12,7 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( BaseDataModel, ModelResource, RequestMetadata, diff --git a/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py b/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py index 899faf23..92515f80 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py +++ b/dashscope/applications/finetune/reinforcement/component/data/rollout_output.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .base_data_model import ( TaskStatus, AgentOutput, _normalize_error_code, diff --git a/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py index ad06010a..163c3442 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -7,20 +7,20 @@ """ from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( TaskStatus, ) -from dashscope.applications.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from ..data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from ..data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from ..data.reward_output import ( Reward, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py index 4950ac2c..350b3090 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -7,17 +7,17 @@ """ from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( TaskStatus, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from ..data.reward_input import ( RewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from ..data.reward_output import ( RewardOutput, Reward, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py index 930880bd..b1fbd92a 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -9,17 +9,17 @@ import time from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( AgentOutput, TaskStatus, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from ..data.rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( +from ..data.rollout_output import ( RolloutOutput, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/func_manager.py b/dashscope/applications/finetune/reinforcement/component/func_manager.py index 0e60971c..f11aad06 100644 --- a/dashscope/applications/finetune/reinforcement/component/func_manager.py +++ b/dashscope/applications/finetune/reinforcement/component/func_manager.py @@ -24,27 +24,27 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from .data.base_data_model import ( BaseDataModel, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( +from .observability.tracing import ( is_tracing_enabled, trace_processor_process, async_trace_processor_process, ) -from dashscope.applications.finetune.reinforcement.component.parser import ( +from .parser import ( BaseRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser import ( +from .parser import ( GroupRewardRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser import ( +from .parser import ( RewardRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser import ( +from .parser import ( RolloutRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from .processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/__init__.py b/dashscope/applications/finetune/reinforcement/component/observability/__init__.py index 4c3d1659..b836df88 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/__init__.py @@ -4,7 +4,7 @@ This module provides a *stable public surface* while keeping heavy observability dependencies lazily imported. User-facing imports such as:: - from dashscope.applications.finetune.reinforcement.component.observability import ( + from . import ( observe_processor, trace_client, trace_tool, @@ -81,7 +81,7 @@ def __getattr__(name: str) -> Any: "trace_client", "trace_tool", }: - from dashscope.applications.finetune.reinforcement.component.observability import ( + from . import ( genai as _genai, ) @@ -89,7 +89,7 @@ def __getattr__(name: str) -> Any: # Processor-span helpers if name in {"observe_processor", "trace_processor_span"}: - from dashscope.applications.finetune.reinforcement.component.observability import ( + from . import ( processor_span as _processor_span, ) @@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any: "span_payload_preview", "trace_processor_process", }: - from dashscope.applications.finetune.reinforcement.component.observability import ( + from . import ( tracing as _tracing, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py index 7fb50b3a..887dd769 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/__init__.py @@ -4,13 +4,13 @@ Submodules by responsibility: -- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.messages` # noqa: E501 — message and tool_calls mapping -- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_openai` # noqa: E501 — OpenAI-compatible LLM client patching -- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope` # noqa: E501 — DashScope Generation patching -- :mod:`~dashscope.applications.finetune.reinforcement.component.observability.genai.tools` +- :mod:`~dashscope.finetune.reinforcement.component.observability.genai.tools` — tool tracing (decorator + patcher) Requires ``pip install loongsuite-util-genai`` and tracing env flags @@ -18,30 +18,30 @@ Optional: ``AGENTIC_RL_LOG_TRACE_ID=true`` enables ``trace_id`` diagnostic logs (default off); -see :mod:`dashscope.applications.finetune.reinforcement.component.observability.tracing`. +see :mod:`dashscope.finetune.reinforcement.component.observability.tracing`. """ from __future__ import annotations from typing import Any -from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long instrument_dashscope_generation_call, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long instrument_openai_chat_completions, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long genai_llm_span, observe_llm, run_with_genai_llm_span, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.tools import ( # noqa: E501 # pylint: disable=line-too-long observe_tool, trace_tool, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py index 4d045526..b65443c8 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -8,15 +8,15 @@ from opentelemetry.trace.status import Status, StatusCode from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from ..genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 +from ..tracing import ( # noqa: E501 is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py index d46a8959..c5ac9c45 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -17,16 +17,16 @@ from opentelemetry.trace.status import Status, StatusCode from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from ..genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.messages import ( # noqa: E501 # pylint: disable=line-too-long openai_chat_messages_to_input_messages, openai_completion_to_output_messages, unwrap_openai_completion, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( +from ..tracing import ( is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py index adc99419..061dedef 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_span.py @@ -12,16 +12,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.applications.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from ..genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.applications.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from ..genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( +from ..tracing import ( is_tracing_enabled, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py index aabf282e..84d6f193 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py @@ -28,8 +28,12 @@ from typing import Any, Dict, List, Optional from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.utils import deep_mask -from dashscope.applications.finetune.reinforcement.component.observability import genai +from dashscope.applications.finetune.reinforcement.common.utils import ( + deep_mask, +) +from dashscope.applications.finetune.reinforcement.component.observability import ( + genai, +) # pylint: disable=protected-access _core = genai._core diff --git a/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py index fa589b52..bd3ad8f9 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py @@ -35,10 +35,10 @@ from typing import Any, Callable, Dict, Optional, TypeVar, cast from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( BaseDataModel, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( +from ..observability.tracing import ( _REWARD_SPAN_PREFIX, _ROLLOUT_SPAN_PREFIX, apply_processor_span_attributes_after, @@ -233,7 +233,7 @@ def observe_processor( Replaces the explicit ``@trace_processor_span(logical_kind=...)`` style. """ - from dashscope.applications.finetune.reinforcement.component.processor import ( + from ..processor import ( AbstractRewardProcessor, AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/tracing.py b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py index ed0c9d69..c4488942 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py @@ -40,7 +40,7 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/applications/finetune/reinforcement/component/parser/__init__.py b/dashscope/applications/finetune/reinforcement/component/parser/__init__.py index 9ee8e21a..87a1ba44 100644 --- a/dashscope/applications/finetune/reinforcement/component/parser/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/__init__.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( +from ..parser.base_parser import ( BaseRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 +from ..parser.group_reward_parser import ( # noqa: E501 GroupRewardRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser.reward_parser import ( +from ..parser.reward_parser import ( RewardRequestParser, ) -from dashscope.applications.finetune.reinforcement.component.parser.rollout_parser import ( +from ..parser.rollout_parser import ( RolloutRequestParser, ) diff --git a/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py index 8cc368ea..837eb15c 100644 --- a/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/base_parser.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/__init__.py b/dashscope/applications/finetune/reinforcement/component/processor/__init__.py index bc66518c..47f50056 100644 --- a/dashscope/applications/finetune/reinforcement/component/processor/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/__init__.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -from dashscope.applications.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py index e954cf0e..ec716143 100644 --- a/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_processor.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from ..data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py index 1e62f84c..2696f4d3 100644 --- a/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_rollout_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from ..data.rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_output import ( +from ..data.rollout_output import ( RolloutOutput, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from ..processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/server/server.py b/dashscope/applications/finetune/reinforcement/component/server/server.py index e85c1f8f..9985fefb 100644 --- a/dashscope/applications/finetune/reinforcement/component/server/server.py +++ b/dashscope/applications/finetune/reinforcement/component/server/server.py @@ -10,7 +10,7 @@ FUNC_TYPE Function type, current supported values: "reward"/"rollout" (required) PROCESSOR_CLASS Full Python path to processor class, e.g.: -dashscope.applications.finetune.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor +dashscope.finetune.reinforcement.component.demo.reward_processor_demo.DemoRewardProcessor (required, processor instantiated using this class) SERVER_PORT Server port, default 8000 ENABLE_LOGGING Enable verbose logging, "true"/"1" to enable, @@ -22,14 +22,15 @@ == Startup Methods == # Recommended: Run with python -m - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor python -m - dashscope.applications.finetune.reinforcement.component.server.server + dashscope.finetune.reinforcement.component.server.server # Using uvicorn - FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo. + FUNC_TYPE=reward PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo. reward_processor_demo.DemoRewardProcessor uvicorn - dashscope.applications.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + dashscope.finetune.reinforcement.component.server.server:app \ + --host 0.0.0.0 --port 8000 == Endpoints == @@ -55,11 +56,13 @@ from dashscope.applications.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component import BaseDataModel -from dashscope.applications.finetune.reinforcement.component.func_manager import ( +from dashscope.applications.finetune.reinforcement.component import ( + BaseDataModel, +) +from ..func_manager import ( FuncManager, ) -from dashscope.applications.finetune.reinforcement.component.observability.tracing import ( +from ..observability.tracing import ( ensure_agentic_rl_baggage_span_processor, is_tracing_enabled, maybe_force_flush_async, @@ -120,7 +123,7 @@ def _resolve_func_type(raw: str) -> FuncType: if not _PROCESSOR_CLASS_ENV: raise RuntimeError( "Environment variable PROCESSOR_CLASS is required. " - "Example: PROCESSOR_CLASS=dashscope.applications.finetune.reinforcement.component.demo." + "Example: PROCESSOR_CLASS=dashscope.finetune.reinforcement.component.demo." "reward_processor_demo.DemoRewardProcessor", ) @@ -590,7 +593,8 @@ async def health_check_endpoint() -> JSONResponse: if __name__ == "__main__": # Direct execution entry point for FastAPI server. # Alternative startup using uvicorn: - # uvicorn dashscope.applications.finetune.reinforcement.component.server.server:app --host 0.0.0.0 --port 8000 + # uvicorn dashscope.finetune.reinforcement.component.server.server:app \ + # --host 0.0.0.0 --port 8000 import uvicorn import sys import argparse @@ -627,7 +631,7 @@ async def health_check_endpoint() -> JSONResponse: # uvicorn resolves this import string inside its own worker # process, so it must stay the public path used by # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. - "dashscope.applications.finetune.reinforcement.component.server.server:app", + "dashscope.finetune.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, workers=worker_count, diff --git a/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py index 5f509095..e54d1c16 100644 --- a/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py +++ b/dashscope/applications/finetune/reinforcement/examples/observability/smoke_observability.py @@ -51,33 +51,35 @@ InMemorySpanExporter, ) -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, ModelResource, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from dashscope.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.func_manager import FuncManager -from dashscope.applications.finetune.reinforcement.component.observability.genai import ( +from dashscope.finetune.reinforcement.component.func_manager import ( + FuncManager, +) +from dashscope.finetune.reinforcement.component.observability.genai import ( observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.applications.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.observability.processor_span import ( # noqa: E501 # pylint: disable=line-too-long observe_processor, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py index 66c38ca0..0aad8c64 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py @@ -8,15 +8,15 @@ from __future__ import annotations -from dashscope.applications.finetune.reinforcement import ( +from dashscope.finetune.reinforcement import ( GroupRewardInput, GroupRewardOutput, GroupReward, Reward, TaskStatus, ) -from dashscope.applications.finetune.reinforcement import logger -from dashscope.applications.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.processor import ( AbstractGroupRewardProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py index cf459190..b0c7b7af 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward.py @@ -16,20 +16,23 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.applications.finetune.reinforcement import RewardInput, RewardOutput -from dashscope.applications.finetune.reinforcement import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import ( + RewardInput, + RewardOutput, +) +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from dashscope.finetune.reinforcement.component.data.reward_output import ( Reward, ) # pylint: disable=no-name-in-module -from dashscope.applications.finetune.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.applications.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py index ead2d3f2..e46a15e1 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py @@ -12,15 +12,17 @@ import time from typing import Dict -from dashscope.applications.finetune.reinforcement import AbstractRewardProcessor -from dashscope.applications.finetune.reinforcement import ( +from dashscope.finetune.reinforcement import ( + AbstractRewardProcessor, +) +from dashscope.finetune.reinforcement import ( RewardInput, RewardOutput, Reward, TaskStatus, ) -from dashscope.applications.finetune.reinforcement import logger -from dashscope.applications.finetune.reinforcement import ( +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement import ( reward_func, sub_reward_func, aggregate_func, diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py index 502f8f4e..515c9b87 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py @@ -29,19 +29,22 @@ # Public names are provided lazily via __getattr__; __all__ entries are not # bindings. # pylint: disable=undefined-all-variable -from dashscope.applications.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import ( + RolloutInput, + RolloutOutput, +) +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.applications.finetune.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, ) -from dashscope.applications.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) @@ -438,7 +441,7 @@ def __del__(self): if __name__ == "__main__": - from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 + from dashscope.finetune.reinforcement.component.data.base_data_model import ( # noqa: E501 # pylint: disable=line-too-long ModelResource, ) import os diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py index 4ca55bdd..36a4b6a9 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py @@ -36,22 +36,25 @@ from types import SimpleNamespace from typing import Any, Dict, List -from dashscope.applications.finetune.reinforcement import RolloutInput, RolloutOutput -from dashscope.applications.finetune.reinforcement import logger -from dashscope.applications.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.finetune.reinforcement import ( + RolloutInput, + RolloutOutput, +) +from dashscope.finetune.reinforcement import logger +from dashscope.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) # pylint: disable=no-name-in-module -from dashscope.applications.finetune.reinforcement.component.observability import ( +from dashscope.finetune.reinforcement.component.observability import ( observe_processor, observe_llm, observe_tool, trace_client, trace_tool, ) -from dashscope.applications.finetune.reinforcement.component.processor import ( +from dashscope.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py index 37a7f9a9..7311f4ad 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/submit_job.py @@ -48,8 +48,8 @@ # client.init(config_path="rl-job.yaml", job_name="agentic-rl") # await client.run() # Uses YAML configuration -from dashscope.applications.finetune.agentic_rl import AgenticRL -from dashscope.applications.finetune.reinforcement import ( +from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.finetune.reinforcement import ( logger, DataSourceType, TrainingDataset, diff --git a/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py index 2ad94d97..56b84e7d 100644 --- a/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py +++ b/dashscope/applications/finetune/reinforcement/examples/workspace/test_functions.py @@ -37,14 +37,14 @@ import json -from dashscope.applications.finetune.reinforcement import ( +from dashscope.finetune.reinforcement import ( logger, TaskStatus, FunctionType, AgenticRLFunctionComponent, FunctionComponentModel, ) -from dashscope.applications.finetune.agentic_rl import AgenticRL +from dashscope.finetune.agentic_rl import AgenticRL async def main_functions(): diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index b32b0317..6ea23dc9 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -62,6 +62,7 @@ def _canonical_name(name): return None +# pylint: disable=abstract-method,unused-argument class _AliasLoader(importlib.abc.Loader): def __init__(self, alias_name, canonical_name, canonical_spec): self._alias_name = alias_name @@ -83,6 +84,7 @@ def get_code(self, fullname): return loader.get_code(self._canonical_name) +# pylint: disable=unused-argument class _AliasFinder(importlib.abc.MetaPathFinder): _dashscope_legacy_1x_alias_finder = True diff --git a/dashscope/version.py b/dashscope/version.py index 31f70e79..a7866f56 100644 --- a/dashscope/version.py +++ b/dashscope/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -__version__ = "2.0.1" +__version__ = "2.0.3" diff --git a/tests/legacy_1x/__init__.py b/tests/legacy_1x/__init__.py index e28d78e7..5ca34947 100644 --- a/tests/legacy_1x/__init__.py +++ b/tests/legacy_1x/__init__.py @@ -1 +1,2 @@ +# -*- coding: utf-8 -*- """Transitional legacy test suites.""" diff --git a/tests/legacy_1x/integration/test_large_utf8_payload.py b/tests/legacy_1x/integration/test_large_utf8_payload.py index 5da6bfe8..2fdd2988 100644 --- a/tests/legacy_1x/integration/test_large_utf8_payload.py +++ b/tests/legacy_1x/integration/test_large_utf8_payload.py @@ -16,7 +16,9 @@ from dashscope.legacy_1x.aigc.generation import Generation, AioGeneration WEBSEARCH_JSON = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + ), "websearch.json", ) diff --git a/tests/legacy_1x/unit/test_acli_sdk_contract.py b/tests/legacy_1x/unit/test_acli_sdk_contract.py index 75ca41dc..cfe8f2e4 100644 --- a/tests/legacy_1x/unit/test_acli_sdk_contract.py +++ b/tests/legacy_1x/unit/test_acli_sdk_contract.py @@ -317,7 +317,9 @@ def test_current_implementation_does_not_import_acli(): if _is_acli_internal(module) }, ) - assert not hits, f"the current implementation must not depend on acli: {hits}" + assert ( + not hits + ), f"the current implementation must not depend on acli: {hits}" def test_package_root_does_not_import_acli(): @@ -331,9 +333,7 @@ def test_package_root_does_not_import_acli(): def test_acli_lives_at_the_top_level(): assert _ACLI.is_dir(), "dashscope/acli/ must stay at the top level" - moved_into_legacy = ( - _REPO / "dashscope" / "legacy_1x" / "acli" - ).exists() + moved_into_legacy = (_REPO / "dashscope" / "legacy_1x" / "acli").exists() assert not moved_into_legacy, "acli must not land in the legacy bucket" diff --git a/tests/legacy_1x/unit/test_tokenizer.py b/tests/legacy_1x/unit/test_tokenizer.py index 106cd0ee..e9e703f6 100644 --- a/tests/legacy_1x/unit/test_tokenizer.py +++ b/tests/legacy_1x/unit/test_tokenizer.py @@ -103,12 +103,9 @@ def test_deep_imports_resolve(self, module_path, name): assert getattr(module, name) is not None def test_package_level_reexports(self): - import dashscope.tokenizers as tokenizers + from dashscope import tokenizers from dashscope.tokenizers.tokenization import Tokenization - from dashscope.tokenizers.tokenizer import ( - get_tokenizer, - list_tokenizers, - ) + from dashscope.tokenizers.tokenizer import list_tokenizers from dashscope.tokenizers.tokenizer_base import Tokenizer assert tokenizers.__all__ == [ diff --git a/tests/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py index 5cd1791e..30ba3f06 100644 --- a/tests/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -85,7 +85,9 @@ def test_public_names_are_exported(): def test_client_classes_have_root_module_ownership(): - assert impl.Client.__module__ == "dashscope.applications.agentstudio.client" + assert ( + impl.Client.__module__ == "dashscope.applications.agentstudio.client" + ) assert impl.AsyncClient.__module__ == ( "dashscope.applications.agentstudio.client" ) @@ -124,7 +126,9 @@ def test_deep_import_from_syntax_resolves(): assert Agents.__module__ == ( "dashscope.applications.agentstudio.resources.agents" ) - assert Agent.__module__ == "dashscope.applications.agentstudio.types.models" + assert ( + Agent.__module__ == "dashscope.applications.agentstudio.types.models" + ) # --------------------------------------------------------------------------- diff --git a/tests/test_errors.py b/tests/test_errors.py index 3a8cf71c..bbc63223 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -699,7 +699,7 @@ def test_response_error_is_sdk_side_only(): assert issubclass(ResponseError, APIStatusError) assert ResponseError.status_code == 500 assert ResponseError not in _STATUS_MAP.values() - assert ResponseError not in {cls for cls in _CODE_MAP.values()} + assert ResponseError not in set(_CODE_MAP.values()) @pytest.mark.parametrize( diff --git a/tests/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py index 837ecbfd..86ccdda0 100644 --- a/tests/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -26,7 +26,9 @@ import dashscope from dashscope._shared import _sdk_ident from dashscope._shared._sdk_ident import set_sdk_client -from dashscope.legacy_1x.finetune.agentic_rl import AgenticRL as LegacyAgenticRL +from dashscope.legacy_1x.finetune.agentic_rl import ( + AgenticRL as LegacyAgenticRL, +) from dashscope.version import __version__ from dashscope.errors import SdkError from dashscope.finetune import _submit @@ -160,9 +162,12 @@ def test_server_module_resolves_without_importing_fastapi(alias): submit_path = f"{alias}.component.server.server" spec = importlib.util.find_spec(submit_path) assert spec is not None - assert "dashscope/applications/finetune/reinforcement" in spec.origin.replace( - "\\", - "/", + assert ( + "dashscope/applications/finetune/reinforcement" + in spec.origin.replace( + "\\", + "/", + ) ) assert spec.loader.get_code(submit_path) is not None assert "fastapi" not in sys.modules From fb11b89ea3a266a869f53ccf7576108c201ae9ec Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 26 Oct 2026 20:22:00 +0800 Subject: [PATCH 13/41] docs: add internal SDK documentation --- md/aco-memory-research.md | 210 ++ md/agentic-rl.md | 215 ++ md/api_entities_review2_evaluation.md | 80 + md/api_entities_review_evaluation.md | 39 + md/ark-vs-dashscope-v2-comparison.md | 75 + ...omparison-dashscope-vs-openai-vs-claude.md | 743 +++++++ md/cli-source-level-comparison-2026-08.md | 259 +++ md/conclusion-sdk-architecture-stainless.md | 57 + md/dashscope-2.0-1x-dependency-decoupling.md | 146 ++ md/dashscope-2.0-advantages-report.md | 569 +++++ ...ope-2.0-agentstudio-agenticrl-migration.md | 140 ++ md/dashscope-2.0-asr-sdk.md | 339 +++ md/dashscope-2.0-audio-management-sdk.md | 157 ++ md/dashscope-2.0-connection-reuse-sdk.md | 163 ++ md/dashscope-2.0-decision-sdk.md | 288 +++ md/dashscope-2.0-design.md | 596 ++++++ md/dashscope-2.0-design.v1.bak.md | 230 ++ md/dashscope-2.0-errors-registry-plan.md | 882 ++++++++ md/dashscope-2.0-interfaces-examples.md | 1862 +++++++++++++++++ md/dashscope-2.0-interfaces-mapping.md | 159 ++ md/dashscope-2.0-m1-m3-review.md | 182 ++ md/dashscope-2.0-migration-guide.md | 179 ++ md/dashscope-2.0-migration.md | 344 +++ md/dashscope-2.0-naming-review.md | 661 ++++++ md/dashscope-2.0-realtime-sdk.md | 449 ++++ md/dashscope-2.0-translation-sdk.md | 160 ++ md/dashscope-2.0-tts-sdk.md | 373 ++++ md/dashscope-2.0-vs-1.x-alignment-review.md | 452 ++++ md/dashscope-2.0.3-python-sdk.md | 675 ++++++ md/dashscope-interfaces.md | 222 ++ md/dashscope-sdk-expert-intro.md | 92 + md/dashscope-v2-refactor-tasks.md | 195 ++ md/error.md | 356 ++++ md/experiences.jsonl | 8 + md/m0-1x-cli-hotfix.md | 24 + md/research-openai-anthropic-references.md | 78 + md/sdk-comparison-anthropic-vs-dashscope.md | 307 +++ md/sdk-comparison-openai-vs-dashscope.md | 206 ++ ...p-comparison-openai-anthropic-dashscope.md | 1126 ++++++++++ md/sdk-source-level-comparison-part2.md | 397 ++++ md/self-diagnostic-exception.md | 75 + md/vendor-sdk-research-compat.md | 63 + md/zero-cost-local-sandbox.md | 76 + 43 files changed, 13909 insertions(+) create mode 100644 md/aco-memory-research.md create mode 100644 md/agentic-rl.md create mode 100644 md/api_entities_review2_evaluation.md create mode 100644 md/api_entities_review_evaluation.md create mode 100644 md/ark-vs-dashscope-v2-comparison.md create mode 100644 md/cli-comparison-dashscope-vs-openai-vs-claude.md create mode 100644 md/cli-source-level-comparison-2026-08.md create mode 100644 md/conclusion-sdk-architecture-stainless.md create mode 100644 md/dashscope-2.0-1x-dependency-decoupling.md create mode 100644 md/dashscope-2.0-advantages-report.md create mode 100644 md/dashscope-2.0-agentstudio-agenticrl-migration.md create mode 100644 md/dashscope-2.0-asr-sdk.md create mode 100644 md/dashscope-2.0-audio-management-sdk.md create mode 100644 md/dashscope-2.0-connection-reuse-sdk.md create mode 100644 md/dashscope-2.0-decision-sdk.md create mode 100644 md/dashscope-2.0-design.md create mode 100644 md/dashscope-2.0-design.v1.bak.md create mode 100644 md/dashscope-2.0-errors-registry-plan.md create mode 100644 md/dashscope-2.0-interfaces-examples.md create mode 100644 md/dashscope-2.0-interfaces-mapping.md create mode 100644 md/dashscope-2.0-m1-m3-review.md create mode 100644 md/dashscope-2.0-migration-guide.md create mode 100644 md/dashscope-2.0-migration.md create mode 100644 md/dashscope-2.0-naming-review.md create mode 100644 md/dashscope-2.0-realtime-sdk.md create mode 100644 md/dashscope-2.0-translation-sdk.md create mode 100644 md/dashscope-2.0-tts-sdk.md create mode 100644 md/dashscope-2.0-vs-1.x-alignment-review.md create mode 100644 md/dashscope-2.0.3-python-sdk.md create mode 100644 md/dashscope-interfaces.md create mode 100644 md/dashscope-sdk-expert-intro.md create mode 100644 md/dashscope-v2-refactor-tasks.md create mode 100644 md/error.md create mode 100644 md/experiences.jsonl create mode 100644 md/m0-1x-cli-hotfix.md create mode 100644 md/research-openai-anthropic-references.md create mode 100644 md/sdk-comparison-anthropic-vs-dashscope.md create mode 100644 md/sdk-comparison-openai-vs-dashscope.md create mode 100644 md/sdk-deep-comparison-openai-anthropic-dashscope.md create mode 100644 md/sdk-source-level-comparison-part2.md create mode 100644 md/self-diagnostic-exception.md create mode 100644 md/vendor-sdk-research-compat.md create mode 100644 md/zero-cost-local-sandbox.md diff --git a/md/aco-memory-research.md b/md/aco-memory-research.md new file mode 100644 index 00000000..bf1e36d7 --- /dev/null +++ b/md/aco-memory-research.md @@ -0,0 +1,210 @@ +# 蚁群算法(ACO)应用于智能体记忆检索 —— 定向调研 + +> ⚠️ 保密:含未申请专利的技术方案,申请递交前勿公开、勿提交任何 git 仓库。 +> 来源:deep-research 工作流(runId: wf_13fb4357-cbf,5 角度、22 来源、94 条 claim 抽取、25 条对抗验证**全部确认**、0 驳回)。ACO 教科书内容经 Dorigo & Stützle 原书 PDF(MIT Press, 321 页)逐字核对。 +> 证据强度标注:【教科书】= 经典定义性材料;【benchmark】= 公开基准实测;【宣称】= 论文/项目方自报;【代码核实】= 开源仓库逐行验证。 + +## 0. 一页结论 + +1. **直接先例存在(关键问题的直接回答)**:开源系统 [sentAInce](https://github.com/dcnconsult/sentAInce)(Apache-2.0,2026-07 创建、2026-09 活跃)公开实现了"agent 行为在持久记忆图上沉积信息素、落点分类后信息素引导游走取 top-k 上下文注入"。**存在性证据强(代码级核实),有效性证据为零**(单人项目、3 stars、无同行评审、自身对照实验未达显著性门槛)。 +2. **学术上显式信息素路线基本空白**:2026-01 版 GraphRAG 功能综述全文无 pheromone/ant colony/stigmergy(缺席证据);最接近的 agent 记忆加权工作 HAGE(2026-05)用 RL 而非信息素、全文无相关术语。 +3. **机制工具箱全部有成熟参照**:经典教科书给出转移/挥发/MMAS 公式与参数;落点定位与 τ/η 融合有 NAACL 2015 独立 benchmark 支撑(融合后 91.77%,单信号显著更差——信号互补"缺一不可"有实证)。 +4. **风险闭环完整**:马太效应有小规模实证观察,MMAS 界/挥发/聚合约束/精英强化缓解手段齐备;**对抗性污染防御在所有已核实来源中完全空白**(既是风险也是专利空位)。 +5. **专利含义**:宽泛框架(行为沉积信息素 + 落点 + 游走取上下文)已被 sentAInce 覆盖,独权必须收窄到具体区别特征组合;窗口在快速收窄(sentAInce 两个月前才创建),要申请必须快。详见第 7 节。 + +## 1. 目标场景与设计约束 + +智能体(终端编程助手为代表)在持久化记忆图谱上运行。给定一个 query:落点定位 → 信息素引导的邻域游走 → top-k 紧凑子图(~10^3 tokens)注入上下文。约束:检索代价与库规模解耦;图谱伴随会话自动构建、在线演化;冷启动可用;防高频路径垄断。 + +## 2. ACO 经典机制【教科书,经原书逐字核实】 + +来源:Dorigo & Stützle《Ant Colony Optimization》(MIT Press)原书 PDF + Scholarpedia。 + +### 2.1 核心公式 + +- **随机比例转移规则**(式 3.2):p_ij^k = τ_ij^α · η_ij^β / Σ_l τ_il^α · η_il^β。α=0 退化为随机贪婪;β=0 导致纯信息素自放大停滞——**α/β 都不可缺**。 +- **蒸发规则**(式 3.3):τ ← (1−ρ)·τ,0<ρ≤1。使未被选择的边指数衰减,防止无限累积、实现"遗忘坏决策"。 + +### 2.2 经典参数区间(原书 Box 3.1,TSP 无局部搜索) + +| 参数 | AS/EAS | ASrank | MMAS | ACS | +|---|---|---|---|---| +| α | 1 | 1 | 1 | 1 | +| β | 2~5 | 2~5 | 2~5 | 2~5 | +| ρ | 0.5 | 0.1 | **0.02** | 0.1 | +| m(蚂蚁数) | n | n | n | 10 | +| 其他 | EAS: e=n | w=6 | —— | q0=0.9, ξ=0.1 | + +### 2.3 MMAS(防早熟收敛的关键变体) + +- 每边信息素钳制在 [τmin, τmax];τmax = 1/(ρ·C^bs) 随最优解更新,τmin = τmax/a; +- **实验证明 τmin 比 τmax 更关键**(Stützle 1999)——τmin 保证任何边始终保留非零被探索概率,这正是防马太效应的理论机制; +- 接近停滞时触发**信息素重初始化**(重置到 τmax); +- 初始化约定:MMAS 将信息素**初始化到 τmax**(促进早期探索),与 AS 的低常数初始化相反——冷启动设计应采 MMAS 约定。 + +### 2.4 概念映射表(ACO → 记忆图谱) + +| ACO 概念 | 本方案对应 | +|---|---| +| 蚂蚁 | 一次 query 的上下文提取游走 | +| 节点 | 知识实体(代码符号/文档/知识点/事项) | +| 边信息素 τ | 行为边权:共现检索频次 × 采纳率 | +| 启发值 η | 静态结构边强 × 与当前 query 的语义相似度 | +| 路径质量 | 任务完成信号(资格迹/桶队式沿路径折扣回传 γ^Δ) | +| 挥发 ρ | 时间衰减(长期记忆取 MMAS 式低挥发 0.02–0.1) | +| MMAS [τmin,τmax] | 防老边垄断、保新边机会;停滞重初始化 | +| ACS q0=0.9 / ξ=0.1 | 游走利用/探索配比;经过即微量局部挥发 | + +## 3. 先例排查(关键问题:有无直接先例) + +### 3.1 直接先例:sentAInce【代码核实,存在性强证据】 + +[sentAInce](https://github.com/dcnconsult/sentAInce)(Apache-2.0, v0.1.12,2026-07 创建、2026-09 活跃,3 stars / 1 fork,单人项目): + +- **沉积**:仅当动作被验证成功(Bash exit 0)时沿路径在 verb-altitude 有向图上沉积 τ——"earned memory":读取不沉积、重复不沉积、失败不沉积;含衰减、剪枝下限、每类边上限、资格迹信用分配(γ^Δ,bucket-brigade 式); +- **检索**:UserPromptSubmit 将 prompt 分类到 goal-class(落点),splice(k=8) 从该类最强边源节点做**贪婪 widest-path 游走**(`cur = max(outs, key=lambda x: x[1])[0]`),top-k τ 边渲染为 additionalContext 注入; +- **与本案同构点**:落点 + 信息素引导邻域游走取上下文(投票 2-1,分歧仅在"落点"对应关系的解释强度,代码事实无争议); +- **重大限定**:无同行评审、无外部采用、自身对照 flock 实验未达显著性门槛——**"先例存在"证据强,"先例有效"证据为零**; +- **法律意义**:公开 GitHub 仓库构成现有技术,宽泛框架独权不可行(见第 7 节);Apache-2.0 许可使其代码可作为工程参照。 + +### 3.2 功能等价但机制不同:HAGE【benchmark 级论文,代码无关】 + +[HAGE(arXiv 2605.09942,2026-05)](https://arxiv.org/html/2605.09942v2):在 agent 记忆图谱上用**下游任务奖励的 RL(策略梯度 + 边锚定正则化)**联合优化路由行为与边表示——"由检索/路由行为的结果强化图边权供后续 query 使用"在功能层面已有先例。 +- 游走打分 S(n_j|n_i,q) = λ·cos(v_j,q) + (1−λ)·w_ij(q):语义相似 × 学习边权的融合,与 τ/η 融合在**抽象设计模式**层面同构,可直接参照; +- **两点不可照抄**:其 w_ij(q) 是 query-conditioned 动态表示(非 ACO 式持久标量);融合是学习系数线性加和(非 τ^α·η^β 乘性); +- **全文零信息素术语**——支持"显式 ACO 路线在学术文献空白"的判断。 + +### 3.3 ACO×LLM 先例但场景不同:ACO-ToT【宣称,存在性代码级】 + +[ACO-ToT(arXiv 2501.19278,2025-01)](https://arxiv.org/abs/2501.19278):经典转移规则与挥发迁移到 LLM 思维树推理;GSM8K/ARC/MATH 达 84.2%/88.9%/22.6%(对最弱基线 CoT 提升 28.6 点,**但对最强基线 IRPO 仅 +1.8~2.6 点**,论文自报、单模型、无复现)。**关键限定:信息素仅作用于单次推理任务内部的推理树,不跨 query 持久化**——经两次全文核查确认未应用于记忆/检索/KG 场景,不构成核心设想先例。 + +### 3.4 IR/推荐领域的历史先例【一手论文机制核实】 + +| 工作 | 机制 | 与本案差异 | +|---|---|---| +| **NaïveRank**(约 2014,Brunel 大学库) | (query,document) 对上沉积信息素:点击即 +1,排名完全由信息素决定;指数半衰期挥发 φW·(1/2)^((t−φT)/δ) 在每次强化前执行,明确用于对抗正反馈自强化 | 载体是点击日志的 query→doc 二部结构,非记忆图谱邻域游走;注意作者在自己的实验中**关闭了挥发**,防收敛是设计 rationale 而非实验结论 | +| Web 搜索个性化(Procedia CS 2021) | 每用户偏好画像,画像不足时借用他人画像(stigmergy 类比) | 载体为用户画像而非图边权;摘要无真实 ACO 算子(部分类比) | +| Ant Collaborative Filtering(Appl. Sci. 2020) | 信息素交换建模评分、挥发建模 concept drift | 推荐场景,非 agent 记忆 | + +### 3.5 主流文献的缺席证据与静态基线 + +2026-01 版 [GraphRAG 功能综述(arXiv 2504.10499v2)](https://arxiv.org/html/2504.10499v2) 768KB 全文 grep:pheromone/ant colony/stigmergy 零命中。主流范式恰为本案的静态对照组:query 实体对齐到 KG 节点后作为 **Personalized PageRank 种子做邻域扩散**(HippoRAG, NeurIPS 2024)——静态/扩散权重而非信息素权重。 +**限定**:缺席证据限于综述覆盖范围;**本轮未系统检索专利库**,工业内部实现未知。 + +## 4. 算法设计草案 + +### 4.1 设计依据的独立 benchmark 证据链 + +[Pershina/He/Grishman, NAACL 2015(N15-1026)](https://aclanthology.org/N15-1026.pdf):PPR 实体消歧在 AIDA 27,816 个提及上达 **91.77% micro**(超当时 SOTA)。消融证明**信号互补缺一不可**:单用静态启发值 62.61%、单用图游走一致性 85.56%、融合后 91.77%——为 τ(游走证据)× η(语义/静态启发)融合设计提供独立支撑。聚合约束 c1/c2(忽略同实体竞争候选、每实体只取最大贡献候选)将准确率从 90.26% 提至 91.77%,**机制是过滤错误候选注入游走的噪声**——直接对应抗污染设计。 +限定:其最终组合为加性 score 且 iSim 兼作 PPR 重启分布,对乘性 τ^α·η^β 的支撑在信号互补性层面而非公式同一。 + +### 4.2 在线更新(伴随会话自动执行) + +``` +on tool_call(query, hits, adopted, task_context): + for (a, b) in co_occurred(hits): # 共现沉积 + Δτ_ab += adopt_weight(adopted) * γ^depth(a, b) + +on task_done(success): # 精英强化:仅成功路径 + if success: + for edge, Δ in retrieval_path.edges_with_distance(): + τ_edge += Q * γ^Δ # 资格迹信用回传 + +on tick(Δt): # 或懒挥发:读取时按 Δt 折算 + τ ← clip(τ * (1−ρ)^(Δt/τ_unit), τmin, τmax) # MMAS 钳制 + if stagnation_detected(): τ ← τmax # 停滞重初始化(MMAS) +``` + +### 4.3 查询提取(落点 + 信息素引导游走) + +``` +extract(query, token_budget): + seeds = entity_link(query) # 落点(PPR 消歧式,见 4.1) + if not seeds: + ctx = fallback_grep_vector(query) # 回退通道 + write_back_candidate_nodes(ctx) # 新实体回写,图谱自动生长 + return ctx + frontier, ctx, cost = seeds, [], 0 + while cost < token_budget: + cand = edges(frontier) + cand = apply_aggregation_constraints(cand) # c1/c2 抗噪(4.1) + p_ij ∝ τ_ij^α · η_ij^β # τ=行为边权, η=语义×静态 + next = argmax(p) if rand() < q0 else sample(p) # ACS 利用/探索 + ctx.append(next); cost += tokens(next) + frontier = next + return ctx +``` + +### 4.4 参数初始值(教科书 Box 3.1 核实值 → 本场景外推) + +| 参数 | 教科书经典值 | 本场景建议 | 依据 | +|---|---|---|---| +| α | 1 | **1.0** | Box 3.1 | +| β | 2~5 | **2** | Box 3.1 下沿;长期行为信号应占主导 | +| ρ | AS 0.5 / ACS 0.1 / MMAS 0.02 | **0.02–0.1** | 长期在线记忆取 MMAS 式低挥发 | +| q0 | 0.9 (ACS) | **0.85–0.9** | ACS 经典 | +| ξ(局部挥发) | 0.1 (ACS) | 0.1(可选) | ACS 经典 | +| [τmin,τmax] | τmax=1/(ρ·C^bs),τmin=τmax/a | MMAS 钳制;**τmin 更关键**(保探索下限) | Stützle 1999 实验结论 | +| τ0 初始化 | AS 低常数 / **MMAS 初始化到 τmax** | **τ0=τmax** | MMAS 约定:高初始值促进早期探索,缓解冷启动 | +| 游走预算 | —— | ~10^3 tokens | HippoRAG2 PPR 紧凑子图实测量级(上轮调研) | + +## 5. 评测设计 + +- **必含基线**(有 benchmark 依据):A 纯向量 RAG;B 静态 Personalized PageRank 图(HippoRAG 式,主流范式对照);C 信息素加权(本案)。 +- **冷启动/覆盖率是受控变量**:KG-GraphRAG 因建图覆盖率仅 ~65%(HotPotQA 65.8%、NQ 65.5% 的答案实体入图)在 QA 上普遍逊于向量 RAG(HotPotQA F1:KG-GraphRAG-Triplets 25.02 vs RAG 60.04)——**这是稀疏/冷启动风险的 benchmark 级直接证据**,评测必须控制图覆盖率变量([arXiv 2502.11371v2 / OpenReview K6N6gCCYcb](https://arxiv.org/html/2502.11371v2),ACM 已发表)。 +- 指标:recall@k / MRR、token 成本、延迟、**收敛轮数**(行为加权需多少轮交互才显著优于静态基线 B)、抗噪恢复。 +- 消融(参照 ACO-ToT 对照设计范式):去挥发、去 MMAS 钳制、去精英回传、τ-only(β=0,应现自放大停滞)、η-only(α=0)。 +- 自建任务集(代码场景无现成 benchmark):跨文件符号定位 ×20、影响面分析 ×20、架构问答 ×20。 + +## 6. 风险与缓解(实证观察—缓解闭环) + +| 风险 | 实证证据 | 缓解(均有支撑) | +|---|---|---| +| 正反馈失控/马太效应 | NaïveRank 8 用户 ×6 任务实验:目标页升至首位后被更多点击、信息素持续增加,可能收敛次优(作者自述初步结果、hedged,且其自评在该静态领域 negligible)【宣称】 | MMAS [τmin,τmax](τmin 保探索下限,实验证明比 τmax 更关键)+ 停滞重初始化【教科书】;指数半衰期挥发【宣称】 | +| 冷启动/覆盖稀疏 | KG 覆盖率 ~65% 致检索失效(HotPotQA F1 25.02 vs 60.04)【benchmark】 | 静态底图 + τ0=τmax 高初始探索 + 回退通道与回写生长 | +| 错误候选噪声 | 聚合约束 c1/c2 使准确率 90.26→91.77%【benchmark】 | 游走前过滤低置信节点贡献 | +| 规模扩展 | 无已发表方案 | 懒挥发(读取时折算)+ 局部子图更新 + SQLite 单文件 | +| **对抗性污染** | **所有已核实来源中完全无针对性防御研究(空白)** | 本案自拟:精英强化仅限任务成功路径;采纳信号校验;τmin 保底使污染边可随挥发恢复——**空白即专利空位(见第 7 节)** | + +## 7. 专利新颖性含义(修正版,重要) + +**宽泛框架已被覆盖**:sentAInce 公开了"agent 行为成功沉积信息素于持久记忆结构 + 落点分类 + 信息素引导贪婪游走取 top-k 上下文注入"——"行为信息素记忆"作为大概念的独权不再可行。 + +**仍可主张的区别特征组合**(独权须收窄至此层级): + +| 区别维度 | sentAInce | 本案 | +|---|---|---| +| 转移规则 | 贪婪 widest-path(max τ) | **随机比例规则 τ^α·η^β + ACS q0 利用/探索**(教科书规则,含探索防停滞) | +| 抗早熟约束 | 每类边上限 + 剪枝下限 | **MMAS [τmin,τmax] + 停滞重初始化**(有理论依据的界) | +| 落点粒度 | goal-class 粗分类 | **实体链接到具体节点**(PPR 消歧式,91.77% benchmark) | +| 沉积信号 | 仅验证成功(exit 0) | **共现频次 × 采纳率 × 成功奖励多信号**;失败/读取的差异化处理可再设计 | +| 图内容 | verb-altitude 动作图("什么动作有效") | **知识实体图**(代码符号/文档/知识点/事项)+ 静态/行为双层边 | +| 上下文构造 | 固定 k=8 splice | **token 预算约束的紧凑子图**(~10^3 tokens,PPR 式) | +| 失效管理 | 衰减 + 剪枝 | **双时序失效(保留历史)+ 变更级联失效传播** | +| 场景 | 编程 CLI 单场景 | 多场景泛化(编程/办公/学习/生活) | + +**附加空位**:对抗性污染防御在所有已核实来源(含 sentAInce)中均为空白,可作分案储备。 + +**两点诚实警示**: +1. **有效性证据缺失是全行业现状**(唯一直接先例自身实验未达显著性)——专利不要求实证优越性,但产品投入前必须用第 5 节的评测自行验证"信息素加权 > 静态 PPR"; +2. **窗口在收窄**:sentAInce 两个月前创建、HAGE 四个月前发表,该领域演进以月计。且本轮未系统检索专利库——正式递交前的 CNIPA/incoPat 查新不可省。 + +## 8. 开放问题 + +1. 信息素在线沉积(sentAInce 式)与 RL 离线训练(HAGE 式)在同一记忆图谱上的头对头对比完全缺失——无 recall@k、成本、收敛速度、抗噪的任何实验证据; +2. 专利与工业界未公开实现未知(本轮未系统检索专利库); +3. 理论性质未明:稳态下 τ 加权邻域游走与 Personalized PageRank 的数学关系(是否等价于某种自适应加权 PPR)、收敛条件,均无已发表分析——**这本身是可发表的研究点**; +4. 百万级边规模的挥发调度成本(全边周期蒸发 vs 访问时惰性衰减)无已发表工程方案。 + +## 9. 来源清单 + +- Dorigo & Stützle, *Ant Colony Optimization*, MIT Press([原书 PDF](https://web2.qatar.cmu.edu/~gdicaro/15382/additional/aco-book.pdf));[Scholarpedia: ACO](http://www.scholarpedia.org/article/Ant_colony_optimization) +- [sentAInce(直接先例,代码核实)](https://github.com/dcnconsult/sentAInce) +- [HAGE, arXiv 2605.09942](https://arxiv.org/html/2605.09942v2) +- [ACO-ToT, arXiv 2501.19278](https://arxiv.org/abs/2501.19278) +- [NaïveRank(Brunel 大学库全文)](https://bura.brunel.ac.uk/bitstream/2438/13248/1/Fulltext.pdf) +- [Web 搜索个性化(Procedia CS 2021)](https://www.sciencedirect.com/science/article/pii/S1877050921011686/pdf?md5=9980107be61f5ba6365ba6cb56a0b274&pid=1-s2.0-S1877050921011686-main.pdf);[Ant Collaborative Filtering (Appl. Sci. 2020)](https://www.mdpi.com/2076-3417/10/20/7245) +- [GraphRAG 功能综述 2026-01, arXiv 2504.10499v2](https://arxiv.org/html/2504.10499v2);[HippoRAG, arXiv 2405.14831](https://arxiv.org/abs/2405.14831) +- [Pershina et al., NAACL 2015 (N15-1026)](https://aclanthology.org/N15-1026.pdf) +- [GraphRAG 系统评测(OpenReview K6N6gCCYcb / arXiv 2502.11371v2)](https://arxiv.org/html/2502.11371v2) +- [Bucket Brigade 机制(IDSIA 存档)](https://people.idsia.ch/~juergen/bucketbrigade/node2.html) diff --git a/md/agentic-rl.md b/md/agentic-rl.md new file mode 100644 index 00000000..cdaad39f --- /dev/null +++ b/md/agentic-rl.md @@ -0,0 +1,215 @@ +# dashscope/finetune/agentic_rl.py 错误码与错误注册表对齐评估 + +## 评估目标 + +审查 `dashscope/finetune/agentic_rl.py` 对外 SDK 接口中抛出的错误(error/message)是否与 `/Users/zhansheng.lzs/ali/pro/ptm/dashscope-error-registry/python/dashscope_error_registry/error_registry.py` 中定义的错误体系对齐。 + +参考:`error.md` 中对 SDK 错误透传原则的审查结论。 + +--- + +## 错误注册表结构 + +`error_registry.py` 定义了两类错误: + +- `PublicError`: 包含 `key`, `status_code`, `error_code`(字符串类名), `error_msg`, `anthropic_error_code` +- `InternalErrorDef`: 包含 `name`, `external`(对应 PublicError), `message`, `allow_retry`, `cause`, `solution` + +核心 `PublicError` 包括: + +| key | status_code | error_code | error_msg | +|---|---|---|---| +| invalid_request | 400 | BadRequestError | The request is invalid. Please check the request and try again. | +| missing_parameter | 400 | BadRequestError | Missing required parameter: {parameter}. | +| auth_failed | 401 | AuthenticationError | Authentication failed. Please provide valid authentication credentials. | +| invalid_api_key | 401 | AuthenticationError | Incorrect API key provided. | +| permission_denied | 403 | PermissionDeniedError | You do not have permission to access this resource. | +| resource_not_found | 404 | NotFoundError | The requested resource was not found: {resource}. | +| request_too_large | 413 | RequestTooLargeError | The request exceeds the maximum allowed size of {limit}. | +| rate_limit_exceeded | 429 | RateLimitError | Rate limit exceeded. Please slow down your requests. | +| internal_error | 500 | InternalServerError | The server encountered an internal error. Please try again later. | +| service_unavailable | 503 | ServiceUnavailableError | The service is temporarily unavailable. Please try again later. | +| request_timeout | 504 | GatewayTimeoutError | The request timed out. Please try again later. | + +--- + +## agentic_rl.py 当前错误处理清单 + +| 位置 | 错误码 | 当前 message | 错误类型 | 触发场景 | +|---|---|---|---|---| +| `__init__:61-64` | 3001 | Invalid API key configuration | `ValueErrorWithCode` | API key 配置无效 | +| `register_functions:111-114` | 3002 | Function registration failed | `RegistrationError` | 函数注册失败 | +| `upload_datasets:144-147` | 3003 | Datasets upload failed | `DatasetsError` | 数据集上传失败 | +| `submit_job:203-207` | 3004 | Duplicate function names detected... | `ValueErrorWithCode` | 函数名重复(本地校验) | +| `submit_job:256-259` | 3005 | Job submission failed | `RuntimeErrorWithCode` | 任务提交失败 | +| `run:321-324` | 3006 | RL tuning workflow failed | `RuntimeErrorWithCode` | 工作流执行失败 | +| `test_functions:440-443` | 3007 | Unsupported function type: {functype} | `ValueErrorWithCode` | 函数类型不支持(本地校验) | +| `test_functions:459-462` | 3008 | Function test failed | `ValidationError` | 函数测试失败 | + +--- + +## 对齐评估 + +### 1. 错误码格式未对齐(HIGH) + +`error_registry.py` 使用**字符串类名**作为 `error_code`,例如 `BadRequestError`、`AuthenticationError`、`InternalServerError`。 + +`agentic_rl.py` 使用**自定义整数错误码** `3001-3008`,与注册表的字符串体系完全不兼容。 + +**结论:未对齐。** + +### 2. 缺少注册表要求的标准字段(HIGH) + +`agentic_rl.py` 的异常类(`ValueErrorWithCode`、`RuntimeErrorWithCode`、`RegistrationError`、`DatasetsError`、`ValidationError`)仅包含: + +- `message` +- `error_code`(整数) +- `__cause__` + +缺少 `error_registry.py` 中 `PublicError` 定义的字段: + +- `status_code` +- `key` +- `anthropic_error_code` + +**结论:未对齐。** + +### 3. 输入校验类错误未映射到 INVALID_REQUEST(HIGH) + +以下场景属于**客户端输入校验失败**,应映射到注册表的 `INVALID_REQUEST` / `BadRequestError`: + +| 当前错误码 | 场景 | 建议映射 | +|---|---|---| +| 3004 | 函数名重复 | `INVALID_REQUEST` / `BadRequestError` | +| 3007 | 不支持的函数类型 | `INVALID_REQUEST` / `BadRequestError` | +| 3008 | 函数测试输入校验失败 | `INVALID_REQUEST` / `BadRequestError` | + +当前使用 3004/3007/3008 等自定义整数码,用户无法通过统一错误码识别这是请求参数问题。 + +**结论:未对齐。** + +### 4. API key 错误未映射到 INVALID_API_KEY/AuthenticationError(HIGH) + +`__init__:61-64` 在 API key 配置无效时抛出 `error_code=3001`、message 为 `"Invalid API key configuration"`。 + +注册表中对应错误为 `INVALID_API_KEY`: + +- `status_code=401` +- `error_code="AuthenticationError"` +- `error_msg="Incorrect API key provided."` + +当前实现未使用标准 `AuthenticationError` 错误码,也未携带 `401` 状态码。 + +**结论:未对齐。** + +### 5. 服务端/网络类错误未映射到标准服务端错误码(MODERATE) + +以下场景属于**服务端调用或底层依赖失败**,应根据实际失败原因映射到 `INTERNAL_ERROR`、`SERVICE_UNAVAILABLE`、`REQUEST_TIMEOUT` 等: + +| 当前错误码 | 场景 | 建议映射 | +|---|---|---| +| 3002 | 函数注册失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | +| 3003 | 数据集上传失败 | 透传底层 OSS/API 错误,或 fallback 到 `INTERNAL_ERROR` / `INVALID_FILE` | +| 3005 | 任务提交失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | +| 3006 | RL 调优工作流失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | + +当前实现使用固定整数码和硬编码 message,无法区分服务端内部错误、服务不可用、超时等情形。 + +**部分缓解:** 这些异常在捕获前会检查 `hasattr(e, "error_code")`,如果底层异常已有 `error_code` 则直接抛出。但: + +- 该机制依赖底层异常是否携带 `error_code`,未明确与注册表字段兼容; +- 新抛出的 `RuntimeErrorWithCode` / `RegistrationError` / `DatasetsError` 仍然使用整数码。 + +**结论:部分对齐,但包装层未对齐。** + +### 6. 错误消息未使用注册表标准模板(MODERATE) + +注册表为每类错误提供了标准化 `error_msg`(部分支持变量替换,如 `{parameter}`、`{resource}`、`{limit}`)。 + +`agentic_rl.py` 的消息均为硬编码英文,例如: + +- `"Invalid API key configuration"` +- `"Duplicate function names detected. All function names must be unique."` +- `"Unsupported function type: {functype}"` + +这些消息语义正确,但未与注册表统一,导致: + +- 多语言/多 SDK 难以保持一致; +- 无法通过注册表的 `key` 定位错误定义和解决方案。 + +**结论:未对齐。** + +### 7. 异常类继承体系与注册表结构不匹配(LOW-MODERATE) + +`agentic_rl.py` 的异常定义在 `dashscope/finetune/reinforcement/common/errors.py` 中,形成了独立的层级: + +- `AgenticRLError`(基类,整数 `error_code`) + - `DeploymentError` → `RegistrationError` / `DatasetsError` + - `ValidationError` + - `InputError` + - `OutputError` + - 等 + +该层级未继承或引用注册表中的 `PublicError` / `InternalErrorDef`,无法直接用于与注册表一致的错误序列化。 + +**结论:未对齐。** + +--- + +## 与 error.md 原则的对比 + +`error.md` 的核心结论是: + +> SDK 应原样透传 HTTP API 返回的 `code` 和 `message`,不得修改。 + +`agentic_rl.py` 的问题在于: + +- 对于**本地 SDK 输入校验错误**(如 3004、3007),它没有使用 API 侧统一约定的 `BadRequestError` 等错误码; +- 对于**服务端调用失败**(如 3002、3003、3005、3006),虽然保留了带 `error_code` 的底层异常,但新包装的错误码和消息是 SDK 自定义的,覆盖了 API 原本可能返回的结构化错误。 + +因此,`agentic_rl.py` 的错误处理方式与 `error.md` 主张的"透传 API 错误"原则存在冲突。 + +--- + +## 修复建议 + +### 短期(保持兼容性前提下对齐) + +1. **将整数错误码映射为注册表字符串错误码**: + - 3001 → `AuthenticationError` + - 3004 / 3007 / 3008 → `BadRequestError` + - 3002 / 3003 / 3005 / 3006 → 透传底层错误,或 fallback 到 `InternalServerError` + +2. **在异常中补充 `status_code` 和 `key` 字段**: + - 使异常对象能够被统一错误处理中间件识别。 + +3. **使用注册表的标准 message 模板**: + - 例如 API key 错误使用 `"Incorrect API key provided."`; + - 输入校验错误使用 `"The request is invalid. Please check the request and try again."` 或 `"Missing required parameter: {parameter}."`。 + +### 长期(推荐) + +1. **直接复用 `dashscope_error_registry` 中的 `PublicError` / `InternalErrorDef`**: + - 本地输入校验错误直接抛出基于 `INVALID_REQUEST` 的异常; + - 认证失败直接抛出基于 `INVALID_API_KEY` 的异常; + - 服务端错误透传 API 返回的 `code`/`message`,不重新包装。 + +2. **移除 `3001-3008` 自定义整数码**: + - 这些码与注册表不兼容,会造成多 SDK / 多语言不一致。 + +3. **统一异常基类**: + - 让 AgenticRL 相关异常继承自一个与注册表结构兼容的基类,支持 `status_code`, `error_code`, `message`, `anthropic_error_code` 等字段。 + +--- + +## 结论 + +`dashscope/finetune/agentic_rl.py` 对外 SDK 接口的错误处理**未与 `dashscope_error_registry/error_registry.py` 对齐**。主要问题包括: + +- 使用自定义整数错误码(3001-3008),而非注册表要求的字符串错误码; +- 缺少 `status_code`、`key`、`anthropic_error_code` 等标准字段; +- 输入校验、认证、服务端错误未映射到注册表中的标准错误类型; +- 错误消息为硬编码,未使用注册表标准模板; +- 与 `error.md` 主张的"原样透传 API 错误"原则存在冲突。 + +建议按上述修复建议逐步改造,优先处理错误码格式和输入校验类错误的映射。 diff --git a/md/api_entities_review2_evaluation.md b/md/api_entities_review2_evaluation.md new file mode 100644 index 00000000..3609341a --- /dev/null +++ b/md/api_entities_review2_evaluation.md @@ -0,0 +1,80 @@ +# dashscope/api_entities 第二份代码 Review 评估报告 + +> 对第二份 review 报告(安全/资源/性能视角)中各条风险建议的逐条核对结果(已对照源码验证)。 +> 日期:2026-07-27 +> 关联文档:`api_entities_review_evaluation.md`(第一份 review 的评估) + +## 总体结论 + +该报告质量高于第一份,约 2/3 条目属实可采纳,但存在 **4 处关键错误**,其中 #12(heartbeat)方向完全相反,照改会引入更严重的问题。 + +--- + +## 🔴 高风险部分评估 + +| 条目 | 评估 | 说明 | +|------|------|------| +| 1a. RSA PKCS1v15 → OAEP | ⚠️ 属实但**不可单侧修** | PKCS1v15 确是弱填充(Bleichenbacher 攻击面),但这是客户端-服务端加密协议:客户端单方面改 OAEP,服务端将无法解密 AES key,直接功能故障。需服务端协同 + 协议版本协商,不能按其建议直接改,P0 定级不当 | +| 1b. `encryption.py:94` 无 timeout | ✅ 属实 | 与第一份 review 一致,P0 同意。建议复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS` | +| 1c. AES key 内存清零 | ❌ 基本无效 | Python `bytes` 不可变,`self.aes_key_bytes = b""` 只是丢引用,原始字节仍留在内存等待 GC,无法真正清零。且 key 在整个请求生命周期都要用于 encrypt/decrypt,不能提前清。安全收益趋近于零 | +| 1d. 公钥缓存 + TTL | ✅ 合理优化 | 注意只能缓存公钥本身,AES key/IV 必须每次请求新生成 | +| 2a. `aio_session.py:66` get_event_loop 弃用 | ✅ 属实 | Python 3.10+ 发 DeprecationWarning,3.12+ 更严格 | +| 2b. finalize 按 id 查找竞态 | ✅ 属实 | 实际比"竞态"更糟:session 在 `WeakKeyDictionary` 值中持有强引用时 finalize 根本不会触发,该回调近乎死代码(与第一份评估结论一致) | +| 2c. `threading.RLock` → `asyncio.Lock` | ❌ **建议错误,不能采纳** | 锁保护的是跨线程、跨 event loop 的全局 dict;临界区内无 await、执行极短,不会实质阻塞事件循环。`asyncio.Lock` 绑定单个 loop,跨 loop 使用反而是 bug | +| 3a. aio 路径 `should_close` 恒 False | ✅ 属实 | `http_request.py:167-172` 两分支均 False,`:228-229` 的 close 是死代码。但语义正确(共享 session 不该关闭),属代码冗余而非泄漏 | +| 3b. 同步请求无连接复用 | ✅ 属实,真实性能问题 | `http_request.py:469` 每次新建 `requests.Session`,每请求承担 TCP+TLS 握手。可参照 aio_session 做模块级共享(urllib3 连接池线程安全)。注意保留用户传入外部 session 的路径 | +| 4a. `websocket_request.py:179` create_task 无引用 | ✅ 属实 | duplex 模式后台发送任务异常被静默吞掉,保存引用并 await 的建议合理 | +| 4b. `close()` 引用未赋值的 `self.ws` | ✅ 属实,但应删非修 | 全文件无 `self.ws =` 赋值,调用必抛 AttributeError;但**全库无任何调用方**,是死方法,直接删除比修复更合适 | + +## 🟡 中风险部分评估 + +| 条目 | 评估 | 说明 | +|------|------|------| +| 5. 类型注解错误 3 处 | ✅ 全部属实 | `to_request_object`/`_only_parameters` 标注 `-> str` 实返 dict、`get_batch_binary_data` 空迭代返 None。代码中的 `# type: ignore[return-value]` / `[return]` 即佐证。可修 | +| 6. URL replace 脆弱 | ✅ 属实,方案更优 | `replace("/api/", "/api-task/", 1)` 比第一份 review 的修复更简洁合理。但重申:`query=True` 在 SDK 内**无调用方**,近似死代码,优先级应降 | +| 7. `dashscope_response.py:167` choices[0] | ✅ 属实 | `choices` 为空列表时抛 IndexError,加空检查合理 | +| 8. `logger.debug(e); raise e` 模式 | ⚠️ 结论对、论据错 | 存在 3 处(`http_request.py:231/520`、`aiohttp_request.py:321`)。改 `logger.exception` + 裸 `raise` 的建议成立(debug(e) 确实丢堆栈)。但"`raise e` 会重置 traceback"**说法错误**——Python 3 中 `raise e` 保留 `e.__traceback__`,与裸 `raise` 差异极小 | + +## 🟢 优化部分评估 + +| 条目 | 评估 | 说明 | +|------|------|------| +| 9. 响应解析代码重复 | ✅ 属实,谨慎推进 | 重构涉及流式/加密/multipart 多路径,回归风险高,建议测试覆盖充分后再做 | +| 10. DictMixin 透传方法 | ✅ 可删但收益微小 | 低优先级,删除需回归验证 dataclass + slots 行为 | +| 11. 死代码 | ✅ 属实 | `api_request_data.py:96-103` 注释块位于 `return` 之后(unreachable);`_to_json_only_data` 全库无调用。均可删 | +| 12. heartbeat=6000 "过于频繁" | ❌ **单位搞反,方向完全错误** | aiohttp `ws_connect(heartbeat=)` 单位是**秒**(`Optional[float]`),6000 = 100 分钟,绝非"6 秒过于频繁"。按其建议改 30000 会变成 8.3 小时。真正的问题恰恰相反:6000 疑似把毫秒当秒写错,若原意 6 秒应写 `heartbeat=6`,30 秒应写 `heartbeat=30` | +| 13. `headers.update()` 代替新建 dict | ✅ 成立 | 微优化,无关紧要 | + +--- + +## 采纳清单 + +### ✅ 可直接采纳 +| 条目 | 修复 | +|------|------| +| 1b | encryption 请求加 timeout(P0) | +| 1d | 公钥类级缓存 + TTL | +| 2a/2b | aio_session:改用 `asyncio.get_event_loop` 的安全替代 + 移除无效 finalize 回调 | +| 3a | 移除 aio 路径 should_close 死代码 | +| 3b | 同步请求共享 `requests.Session`(保留外部 session 路径) | +| 4a | duplex 后台 task 保存引用并 await | +| 5 | 修正 3 处类型注解 | +| 7 | choices 空列表检查 | +| 11 | 删除 unreachable 注释块与 `_to_json_only_data` | +| 13 | `add_headers` 改 `update` | + +### ⚠️ 属实但处理方式应调整 +| 条目 | 调整 | +|------|------| +| 4b | `close()` 直接删除(死方法),而非修复 self.ws | +| 6 | 改用 `replace("/api/", "/api-task/", 1)`,但先确认 query 分支是否有外部使用者 | +| 8 | 改 `logger.exception` + 裸 `raise`(论据修正:raise e 不丢 traceback) | +| 9/10 | 长期重构项,需测试覆盖先行 | + +### ❌ 不能采纳(照改会引入新问题) +| 条目 | 原因 | +|------|------| +| 1a | OAEP 需服务端协同,单侧改直接功能故障 | +| 1c | Python 不可变 bytes 无法真正清零内存 | +| 2c | asyncio.Lock 不能跨 loop,RLock 是正确选择 | +| **12** | **heartbeat 单位是秒不是毫秒,按建议改 30000 = 8.3 小时;若要调整应写 heartbeat=6 或 30** | diff --git a/md/api_entities_review_evaluation.md b/md/api_entities_review_evaluation.md new file mode 100644 index 00000000..1c4d50d2 --- /dev/null +++ b/md/api_entities_review_evaluation.md @@ -0,0 +1,39 @@ +# dashscope/api_entities 代码 Review 评估报告 + +> 对原始 review 报告中 11 条风险建议的逐条核对结果(已对照源码验证)。 +> 日期:2026-07-27 + +## 逐条评估 + +| # | 原严重度 | 文件/位置 | 问题 | 评估结论 | 说明 | +|---|----------|-----------|------|----------|------| +| 1 | 🔴 严重 | `encryption.py:94` | HTTP 请求无 timeout | ✅ 属实,P0 同意 | `Encryption.initialize()` 在构建请求时同步调用(`api_request_factory.py:126`),无 timeout 会阻塞调用线程。建议复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS` 常量而非硬编码 30 | +| 2 | 🟡 中等 | `http_request.py:126-127` | query 模式 `replace("api","api-task")` | ⚠️ 属实但应降级 | 全 SDK 内**没有任何调用方传 `query=True`**(仅 factory 从 kwargs 透传),近似死代码,只有用户显式传入才触发。默认 URL 只含一个 "api",误替换需自定义 base_address 含 "api"(如 `api.example.com`)。建议降为低优先级;原 review 给出的修复片段过于复杂 | +| 3 | 🟡 中等 | `aiohttp_request.py:80-82` | 同上 | ⚠️ 同 #2 | 同上 | +| 4 | 🟡 中等 | `websocket_request.py:75` | Authorization 用小写 "bearer" | ✅ 属实 | RFC 7235 中 auth scheme 大小写不敏感,服务端已兼容多年,属一致性问题。改为 "Bearer" 安全,可修 | +| 5 | 🟡 中等 | `dashscope_response.py:43` | `DictMixin.pop` 的 default 无默认值 | ✅ 属实,但**原建议修复有误** | 若按 review 建议改为 `default=None`,`pop(missing_key)` 会静默返回 None,而 `dict.pop` 无 default 时应抛 KeyError,语义不符。正确做法用 sentinel:`_MISSING = object()`,default 缺省时走 `super().pop(key)` | +| 6 | 🟡 中等 | `api_request_factory.py:107-114` | 手动拼接 URL 不健壮 | ❌ 不成立,不建议改 | 拼接输入全部是 SDK 内部受控常量(task_group/task/function),现状简单可靠。`urljoin` 对无尾斜杠路径会**吞掉末段路径**,改用它反而风险更高 | +| 7 | 🟡 中等 | `aio_session.py:59-86` | GC 回调清理逻辑复杂 | ✅ 属实 | `weakref.finalize` 触发时 session 已不可达,回调内遍历 `_aio_sessions.values()` 按 `id()` 匹配基本不可能命中,该回调实际近乎无效。同意"待优化"(如 atexit 或显式 shutdown API),不阻塞发布 | +| 8 | 🟡 中等 | `http_request.py:438-441` | task_id 被 output 覆盖 | ✅ 属实但是预期行为 | 响应同时含两字段时 `output` 优先是正确语义;改为 if/elif 仅提升可读性。建议降为低优先级 | +| 9 | 🟡 中等 | `api_request_data.py:167-168` | `_only_parameters` pop 修改实例状态 | ✅ 属实,但**原建议修复有 bug** | review 建议只 `get` 不删,会导致 `raw_input` 同时残留在 `parameters` 中被序列化进请求体,改变请求行为。正确修复:`params = dict(self.parameters)` 复制后再 pop。目前该方法仅在 `get_http_payload`(第 129 行)调用一次,仅对象复用/重试场景受影响 | +| 10 | 🟢 低 | `aiohttp_request.py` | `_handle_response` 未传 headers | ✅ 属实 | 已确认该文件所有 `DashScopeAPIResponse(...)` 构造均未传 `headers`,与 `http_request.py` 不一致,建议补齐 | +| 11 | 🟢 低 | `http_request.py:92-96` | headers 合并顺序可能被用户覆盖 | ❌ 不成立 | `**self.headers` 展开的是 `base_request.py:19` 中 SDK 自己设置的 `user-agent` / `X-DashScope-DataInspection`,**不是用户自定义 headers**;用户 headers 通过 `add_headers()` 后置合并覆盖,行为正确,无需修改 | + +## 总体结论 + +原报告 11 条中 **8 条属实、2 条不成立(#6、#11)、2 条属实但重要性应下调(#2/#3、#8)**。另有三点必须注意: + +1. **#6、#11 不成立**,建议不做改动。 +2. **#5、#9 的"建议修复代码"本身会引入新 bug**(改变 dict 语义 / 改变请求体),采纳前必须按上表说明改写。 +3. **#2/#3 的 query 分支在 SDK 内无调用方**,修复前建议先确认是否存在外部使用者,否则可考虑降级或标记为死代码。 + +## 修正后的建议修复顺序 + +| 优先级 | 事项 | +|--------|------| +| P0 | #1 `encryption.py` 添加 timeout(复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS`) | +| P1 | #4 WebSocket "Bearer" 大写;#10 aiohttp 响应补 headers | +| P2 | #5 `pop` 用 sentinel 修复;#9 `_only_parameters` 复制字典后再 pop | +| P3 | #7 aio_session 清理机制优化;#8 if/elif 可读性调整 | +| 观察 | #2/#3 确认 query 分支是否有外部使用者后再决定 | +| 不修 | #6 URL 拼接维持现状;#11 headers 合并顺序行为正确 | diff --git a/md/ark-vs-dashscope-v2-comparison.md b/md/ark-vs-dashscope-v2-comparison.md new file mode 100644 index 00000000..763ebfab --- /dev/null +++ b/md/ark-vs-dashscope-v2-comparison.md @@ -0,0 +1,75 @@ +# Ark Runtime Python SDK vs DashScope v2 SDK 架构对比 + +> 对比对象: +> +> - **ark**:`/Users/zhansheng.lzs/ali/pro/ptm/ark-runtime-python/src/arkruntime`(Stainless 从 OpenAPI spec 生成 + 手写扩展) +> - **dashscope v2**:`/Users/zhansheng.lzs/ali/pro/ptm/moe-dashscope-sdk-python/dashscope/v2`(全手写,54 个文件) + +## 定位差异(先说清楚,避免错配对比) + +| | ark-runtime-python | dashscope/v2 | +|---|---|---| +| 生产方式 | Stainless 从 OpenAPI spec 生成 + 手写扩展 | 全手写(54 个文件) | +| 协议形态 | 仅 OpenAI 兼容 HTTP/SSE | native/openai/anthropic 三协议 + SSE + WebSocket + Job 轮询 | +| 类型覆盖 | 请求侧 TypedDict params + 响应侧 pydantic model,全覆盖 | 只有响应侧有模型,请求侧是 20+ 个 `Optional` 长参数列表 | +| 同步/异步 | 代码生成保证两端一致 | 人肉复制,约一半代码是 async 孪生 | + +--- + +## Ark 的优点 + +1. **容错式反序列化哲学**(最值得借鉴):`_models.py` 的 `construct_type` 不做严格校验,只做宽松强转——union 逐个尝试、枚举/datetime 容错解析、不匹配就原样返回。配合 `extra="allow"`,服务端新增字段、新增枚举值、返回结构小漂移都不会炸用户进程。校验失败时另有 `ArkAPIResponseValidationError` 单独成类(`_base_client.py:486`),把"服务端坏了"和"SDK 解析坏了"区分开。 +2. **统一的请求定制通道**:每个资源方法一致暴露 `extra_headers/extra_query/extra_body/timeout/max_retries`(`_request_options.py:44`),`extra_body` 与 JSON body 深合并(`_base_client.py:236-243`)。用户总有逃生舱。 +3. **分页抽象**:`SyncCursorPage/AsyncCursorPage`(`pagination.py`),`for item in page` 直接迭代,cursor 从末条 `item.id` 推导。 +4. **响应封装**:`ArkAPIResponse.parse(to=...)` 按 cast 类型记忆化解析结果(`_response.py:305-321`),headers/request_id/elapsed 统一暴露;`with_raw_response` 风格切换。 +5. **重试可由服务端控制**:尊重非标准头 `x-should-retry` 和 `retry-after-ms`(`_base_client.py:383-417`),网关可以精细指挥客户端退避。 +6. **每请求 `X-Client-Request-Id` UUID**(`_base_client.py:195`),比进程级 session-id 更利于单请求排障。 +7. **生成式的本质红利**:API 覆盖全、新接口随 spec 跟进、sync/async 行为天然一致。 + +## Ark 的缺点 + +1. sync/async 在 `_base_client.py` 里逐行复制约 300 行(`_request` L527 vs L1011)——代码生成也没解决这个问题。 +2. 历史包袱重:`_response.py`/`_legacy_response.py` 两套响应封装并存 1000+ 行;pydantic v1 兼容路径已是死代码(pyproject 要求 v2)仍占 `_models.py` 约 1/3。 +3. 死字段:`idempotency_key` 定义了但从不消费(`_request_options.py:66`)——POST 重试同样无幂等保护。 +4. 手写扩展(managed agents)绕开生成管线,另起 `dump_body` 序列化器(`_managed_agents_serialize.py:55`),一个库两套序列化约定。 +5. **无类型检查门禁**:只有 Ruff,没有 mypy/pyright CI,类型正确性靠自觉。 +6. 能力面窄:无 WebSocket、无异步任务轮询框架、无文件直传预处理。 +7. `volcengine-python-sdk` 是顶层强依赖,为 STS/E2E 加密拖了重依赖。 + +## Dashscope v2 的优点 + +1. **protocols 适配层**(ark 完全没有的架构亮点):`ChatProtocol` ABC + frozen `ChatRequest`(`protocols/base.py:38-127`),canonical 消息模型 ↔ 三种 wire 格式双向翻译;`make_chunk_parser()` 支持有状态流解析(anthropic tool_use 累积)。ark 只有 OpenAI 一种协议形态。 +2. **Job 异步任务基础设施**(`_jobs.py`):`X-DashScope-Async` 提交 → 轮询,指数退避 1s→8s,`UNKNOWN` 视为终态防死等,`_sleep` 测试接缝。生成式 SDK 通常不管这块。 +3. **WebSocket 复用/解复用**:`WsConnection` 按 `task_id` 在一条物理连接上解复用多任务(`ws_connection.py`),支撑实时音频族。 +4. **OSS 直传预处理**(`_oss.py`):chat 调用前自动上传本地媒体,用户无感。 +5. **错误目录驱动**(`_error_registry.py`,650 行工具生成):异常带 message + solution + allow_retry,面向用户排障,比 ark 的纯状态码映射有人味得多。 +6. **配置分层与网关推导**:参数 > settings > env > config.toml > 1.x 文件;有 workspace 时推导 `{workspace}.{region}.maas.aliyuncs.com`(`_config.py:184-195`)。 +7. **运维细节好**:`SensitiveHeadersFilter` 日志脱敏(`_logging.py:27-79`)、`with_options()` 派生客户端共享连接池(`client.py:178-225`)、v2 流量标记 `module2`(`_ident.py:42-71`)。 + +## Dashscope v2 的缺点 + +1. **sync/async 人肉复制,比 ark 更严重**——`client.py:51 vs 272`、`http.py`、`_jobs.py`、各资源文件全是孪生,手工复制会漂移,ark 至少是机器复制保证一致。 +2. **资源方法样板极重**:每个方法重复 `timeout/api_key/workspace` 三件套透传 + `check_response` + `with_headers`(如 `_jobs.py:254-306`)。 +3. **两套 SSE 解析器**(`transport/sse.py` 用 httpx_sse,`resources/_sse.py` 手工逐行)。 +4. **无分页抽象**:裸 `page_no/page_size` 返回 `JobList/ModelList/FileList`,无迭代器、无 cursor。 +5. **请求参数无类型化**:`completions.py:73-102` 有 20+ 个 Optional 参数,IDE 补全和校验都弱。 +6. 脆弱补丁:`files.py:37-40` 用英文正则嗅探 "not found" 消息来补救网关把 404 报成 400 的问题。 +7. POST 一律重试无幂等区分(`http.py:174`)——这点 ark 也一样,算行业通病。 + +--- + +## Dashscope v2 可借鉴 Ark 的清单(按价值排序) + +| 优先级 | 借鉴点 | 解决 dashscope v2 的哪个痛点 | 落地建议 | +|---|---|---|---| +| ★★★ | 容错式构造(`construct_type` 宽松强转 + 不匹配原样返回) | 服务端新增枚举值/判别字段新值时 pydantic v2 严格校验会炸 | pydantic v2 下用 `model_construct` + 自定义容错 wrap 即可达到类似效果,不必照搬 v1/v2 双栈 | +| ★★★ | 统一请求定制通道 `extra_headers/extra_query/extra_body` | 用户无逃生舱,网关要加个实验性 header 都得发版 | 抽一个 `RequestOptions` 数据类贯穿 resources,顺带收敛 timeout/api_key/workspace 三件套样板 | +| ★★ | 分页抽象(cursor + 可迭代 page) | list 接口裸分页参数 | 给 `files/models/jobs.list` 加 `SyncPage` 迭代器,成本很低 | +| ★★ | `with_raw_response` + `parse(to=)` 记忆化 | 手工 `with_headers(...)` 散装 | 把 `ResponseBase` 升级为带 headers 的统一封装 | +| ★★ | `APIResponseValidationError` 单独成类 | 解析失败和 API 失败混在一族 | 在 errors.py 加一个子类即可 | +| ★ | 每请求 UUID(`X-Client-Request-Id`) | 进程级 session-id 定位不了单请求 | 加进 `_ident.py` | +| ★ | async 孪生改生成(ark 的教训是"人肉复制必漂移") | sync/async 漂移风险 | 不必上 Stainless;用 `unasync` 之类从 sync 源生成 async 孪生即可 | + +**不建议借鉴的**:Stainless 全量代码生成(会冲掉 protocols/Job/WebSocket 这些手写差异化能力,且 ark 自己的生成代码也有双客户端复制、双序列化约定问题);pydantic v1 兼容层;`_legacy_response.py` 式长期并存的双实现。 + +**结论**:反向看,ark 缺的东西(协议适配层、Job 轮询框架、错误目录、日志脱敏)恰好是 dashscope v2 的强项——v2 的核心架构不需要动,借鉴集中在**类型容错、请求定制、分页**这三个工程细节上。 diff --git a/md/cli-comparison-dashscope-vs-openai-vs-claude.md b/md/cli-comparison-dashscope-vs-openai-vs-claude.md new file mode 100644 index 00000000..ad62b83b --- /dev/null +++ b/md/cli-comparison-dashscope-vs-openai-vs-claude.md @@ -0,0 +1,743 @@ +# CLI 深度对比:DashScope CLI vs OpenAI CLI vs Claude CLI + +> 基于源码级分析(DashScope)+ 官方文档(OpenAI/Claude) +> 生成时间:2026-07-31 +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 1. 定位差异 — 根本性不同 + +| 维度 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 本质 | API 管理工具 | API 测试工具 | AI 编程 Agent | +| 类比 | 类似 aws cli / gcloud | 类似 curl 的高级封装 | 类似 Cursor / Copilot 的终端版 | +| 用户 | 开发者管理模型/微调/部署 | 开发者快速测试 API | 开发者用 AI 写代码 | +| 交互模式 | ❌ 无 | ❌ 无 | ✅ 完整 REPL | +| Agent 能力 | ❌ 无 | ❌ 无 | ✅ 读写文件、执行命令、搜索代码 | +| 安装 | `pip install dashscope` | `pip install openai[cli]` | `npm install -g @anthropic-ai/claude-code` | +| 入口命令 | `dashscope` | `openai` | `claude` | + +**核心洞察**:三者不在同一赛道。DashScope 和 OpenAI CLI 是"API 管理工具",Claude CLI 是"AI Agent 终端"。 + +--- + +## 2. 命令体系对比 + +### 2.1 命令结构 + +**DashScope CLI** + +``` +dashscope [options] +``` + +```bash +# 文本生成 +dashscope generation create -m qwen-plus -p "Hello" + +# 文件管理 +dashscope files upload -f data.jsonl -p fine_tune +dashscope files list +dashscope files get +dashscope files delete + +# 模型管理 +dashscope models list +dashscope models get + +# 微调 +dashscope ft create --training-file-ids file_xxx +dashscope ft list +dashscope ft get +dashscope ft stream + +# 部署 +dashscope deployments create --deployed-model xxx +dashscope deployments list + +# Embeddings / Rerank / Tokenization +dashscope embeddings create -m text-embedding-v2 -i "Hello" +dashscope rerank create -m gte-rerank -q "query" --documents '["doc1","doc2"]' +dashscope tokenization create -m qwen-plus -p "Hello" + +# 多模态 +dashscope image-synthesis create -m wanx-v1 -p "A cat" +dashscope video-synthesis create -m video-generation-model -p "A cat walking" +dashscope multimodal-conversation create -m qwen-vl-plus -p "Describe this" --image-url https://... +dashscope speech-synthesis create -m cosyvoice-v1 -p "Hello" +dashscope transcription create -m paraformer-v2 -f audio.wav + +# 强化学习 +dashscope rl create --config rl_config.yaml +dashscope rl list +dashscope rl get +dashscope rl logs +``` + +**OpenAI CLI** + +``` +openai api . [options] +``` + +```bash +# Chat Completions +openai api chat.completions.create -m gpt-4o -g user "Hello" + +# 流式 +openai api chat.completions.create -m gpt-4o -g user "Hello" --stream + +# Files +openai api files.create -f data.jsonl -p fine-tune +openai api files.list +openai api files.retrieve +openai api files.delete + +# Fine-tuning +openai api fine_tuning.jobs.create -m gpt-4o-mini --training_file file-xxx +openai api fine_tuning.jobs.list +openai api fine_tuning.jobs.retrieve +openai api fine_tuning.jobs.cancel + +# Models +openai api models.list +openai api models.retrieve gpt-4o + +# Images +openai api images.generate -p "A cat" --size 1024x1024 + +# Embeddings +openai api embeddings.create -m text-embedding-3-small -i "Hello" + +# Audio +openai api audio.transcriptions.create -m whisper-1 -f audio.mp3 +openai api audio.speech.create -m tts-1 -i "Hello" --voice alloy + +# Assistants (deprecated) +openai api assistants.create --instructions "You are helpful" -m gpt-4o +``` + +**Claude CLI** + +```bash +# 交互模式(REPL) +claude + +# 单次提问(脚本模式) +claude -p "Explain this codebase" + +# 管道模式 +cat error.log | claude -p "What's wrong?" +git diff | claude -p "Review this change" + +# 继续上次会话 +claude --continue +claude --resume + +# 指定模型 +claude --model claude-sonnet-4-20250514 + +# 输出格式 +claude -p "Hello" --output-format json +claude -p "Hello" --output-format stream-json + +# 权限模式 +claude --permission-mode acceptEdits +claude --permission-mode plan + +# MCP 服务器 +claude --mcp-server "npx @anthropic-ai/mcp-server-github" + +# 非交互 + 自动批准所有操作 +claude -p "Fix all bugs" --dangerously-skip-permissions +``` + +### 2.2 命令覆盖范围 + +| 功能域 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 文本生成 | ✅ generation create | ✅ chat.completions.create | ✅ 内置(Agent 模式) | +| 流式输出 | ✅ --stream | ✅ --stream | ✅ --output-format stream-json | +| 文件管理 | ✅ upload/get/list/delete | ✅ create/retrieve/list/delete | ❌ 不需要(直接读本地文件) | +| 模型列表 | ✅ models list/get | ✅ models.list/retrieve | ❌ 不需要 | +| 微调 | ✅ ft create/list/get/stream/cancel | ✅ fine_tuning.jobs.* | ❌ 不支持 | +| 部署 | ✅ deployments create/list/get/scale/delete | ❌ 不支持 | ❌ 不支持 | +| Embeddings | ✅ embeddings create | ✅ embeddings.create | ❌ 不支持 | +| Rerank | ✅ rerank create | ❌ 不支持 | ❌ 不支持 | +| Tokenization | ✅ tokenization create | ❌ 不支持 | ❌ 不支持 | +| 图像生成 | ✅ image-synthesis create | ✅ images.generate | ❌ 不支持 | +| 视频生成 | ✅ video-synthesis create | ✅ videos.create | ❌ 不支持 | +| 多模态对话 | ✅ multimodal-conversation create | ✅(通过 messages 传图) | ❌ 不支持 | +| 语音合成 | ✅ speech-synthesis create | ✅ audio.speech.create | ❌ 不支持 | +| 语音识别 | ✅ transcription create | ✅ audio.transcriptions.create | ❌ 不支持 | +| 强化学习 | ✅ rl create/list/get/logs | ❌ 不支持 | ❌ 不支持 | +| OSS 上传 | ✅ oss upload | ❌ 不支持 | ❌ 不支持 | +| 应用管理 | ✅ application | ❌ 不支持 | ❌ 不支持 | +| 代码编辑 | ❌ | ❌ | ✅ 核心能力 | +| 命令执行 | ❌ | ❌ | ✅ 核心能力 | +| 代码搜索 | ❌ | ❌ | ✅ 核心能力 | +| 会话管理 | ❌ | ❌ | ✅ --continue / --resume | +| MCP 集成 | ❌ | ❌ | ✅ --mcp-server | + +**结论**: + +- DashScope CLI 覆盖面最广(18 个资源域),但全是 API 管理 +- OpenAI CLI 覆盖 8 个资源域,纯 API 测试 +- Claude CLI 覆盖 0 个 API 资源域,但提供了完全不同的 Agent 能力 + +--- + +## 3. 用户体验对比 + +### 3.1 安装与配置 + +| 步骤 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 安装 | `pip install dashscope` | `pip install openai[cli]` | `npm install -g @anthropic-ai/claude-code` | +| 认证 | `export DASHSCOPE_API_KEY=sk-xxx` 或 `-k` 参数 | `export OPENAI_API_KEY=sk-xxx` 或 `openai auth login` | claude 首次运行引导 OAuth 登录 | +| 验证 | 无 auth 命令 | `openai auth login` / `openai auth logout` | OAuth 自动管理 | +| 版本 | `pip show dashscope` | `openai --version` | `claude --version` | + +**差距**: + +- OpenAI 有 `openai auth login` 交互式认证 +- Claude 有 OAuth 引导流程,首次使用零配置 +- DashScope 只有环境变量和 `-k` 参数,无 auth 命令 + +### 3.2 帮助系统 + +| 特性 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 全局帮助 | `dashscope --help` | `openai --help` | `claude --help` | +| 子命令帮助 | `dashscope generation --help` | `openai api chat.completions.create --help` | `claude --help` | +| 参数说明 | ✅ Typer 自动生成 | ✅ Click 自动生成 | ✅ 内置 | +| 示例 | ❌ 无 | ❌ 无 | ✅ 帮助中有示例 | +| Shell 补全 | ❌ add_completion=False | ✅ 支持 | ✅ 支持 | +| Rich 格式化 | ✅ 彩色输出 | ❌ 纯文本 | ✅ 彩色 + Markdown 渲染 | + +### 3.3 输出格式 + +| 特性 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 默认输出 | JSON(Rich 格式化) | JSON(原始) | 纯文本 / Markdown | +| --output-format | ❌ 无 | ❌ 无 | ✅ text / json / stream-json | +| 表格输出 | ✅(rl 命令有 Table) | ❌ | ❌ | +| JSON 美化 | ✅ console.print_json() | ✅ 自动格式化 | ✅ --output-format json | +| 进度条 | ✅(rl 命令有 Progress) | ❌ | ❌ | +| 错误着色 | ✅ `[red]Error:[/red]` | ❌ 纯文本 | ✅ 彩色 | +| 成功标记 | ✅ `[green]✓[/green]` | ❌ | ✅ | + +### 3.4 错误处理 + +**DashScope CLI** + +```python +# common.py — 统一错误处理 +def print_failed_message(rsp): + err_console.print( + f"[red]Failed[/red] request_id: {rsp.request_id}, " + f"status_code: {rsp.status_code}, " + f"code: {rsp.code}, message: {rsp.message}", + ) + +def ensure_ok(rsp, check_business_error=True): + if rsp.status_code != HTTPStatus.OK: + print_failed_message(rsp) + raise typer.Exit(1) + # 还检查 HTTP 200 但业务错误的情况 + if check_business_error: + error_code = output.get("code") + if error_code: + err_console.print(f"[red]Business Error[/red] ...") + raise typer.Exit(1) + return output +``` + +**亮点**: + +- `ensure_ok()` 统一处理 — 消除重复的 if/else +- `handle_sdk_error` 装饰器 — 捕获 SDK 异常转为友好消息 +- 区分 HTTP 错误和业务错误(HTTP 200 但 code 非空) +- Rich 彩色输出 + +实际错误输出: + +``` +Failed request_id: req-xxx, status_code: 429, code: Throttling, message: Requests rate limit +``` + +**OpenAI CLI** + +```json +// 错误直接输出 JSON +{ + "error": { + "message": "You exceeded your current quota...", + "type": "insufficient_quota", + "code": "insufficient_quota" + } +} +``` + +- 无彩色输出 +- 无 request_id 高亮 +- 无建议操作 + +**Claude CLI** + +``` +Error: Rate limit exceeded. Please wait a moment and try again. +``` + +- 简洁友好的错误消息 +- 权限拒绝时有明确提示 +- 工具执行失败时显示具体原因 + +--- + +## 4. 架构设计对比 + +### 4.1 技术栈 + +| 组件 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| 语言 | Python | Python | TypeScript (Node.js) | +| CLI 框架 | Typer (基于 Click) | Click | Ink (React for CLI) | +| 输出美化 | Rich | 无 | Ink + Markdown 渲染 | +| 异步 | asyncio(仅 rl 命令) | 无 | 完整异步 | +| 包管理 | pip | pip | npm | +| 入口 | dashscope.cli.main() | openai.cli.main() | claude (Node binary) | + +### 4.2 代码组织 + +**DashScope CLI** + +``` +dashscope/cli/ +├── __init__.py # Typer app + 命令注册 + 兼容层 +├── __main__.py # python -m dashscope.cli +├── common.py # 共享工具(ensure_ok, error, success) +├── generation.py # 文本生成 +├── files.py # 文件管理 +├── models.py # 模型管理 +├── fine_tunes.py # 微调 +├── deployments.py # 部署 +├── embeddings.py # 向量化 +├── rerank.py # 重排序 +├── tokenization.py # 分词 +├── image_synthesis.py # 图像合成 +├── video_synthesis.py # 视频合成 +├── image_generation.py # 图像生成 +├── multimodal_conversation.py # 多模态对话 +├── multimodal_embedding.py # 多模态向量化 +├── transcription.py # 语音识别 +├── speech_synthesis.py # 语音合成 +├── oss.py # OSS 上传 +├── application.py # 应用管理 +├── agentic_rl.py # 强化学习(最复杂,21.8KB) +└── code_generation.py # 代码生成 +``` + +模式:每个资源域一个文件,每个文件定义一个 `typer.Typer()` app。 + +**OpenAI CLI** + +``` +openai/cli/ +├── main.py # 入口 +├── _cli.py # Click group 定义 +├── _errors.py # 错误处理 +└── (resources 通过 SDK 自动生成) +``` + +模式:极简 — 只是 SDK 方法的薄封装,大部分代码自动生成。 + +**Claude CLI** + +``` +@anthropic-ai/claude-code/ +├── src/ +│ ├── cli.ts # 入口 + 参数解析 +│ ├── repl/ # 交互式 REPL +│ ├── agent/ # Agent 循环 +│ ├── tools/ # 内置工具(Read/Edit/Bash/Glob) +│ ├── permissions/ # 权限系统 +│ ├── mcp/ # MCP 服务器集成 +│ ├── sessions/ # 会话管理 +│ └── hooks/ # 生命周期钩子 +└── ... +``` + +模式:完整的 Agent 框架,CLI 只是入口。 + +### 4.3 命令模式对比 + +| 模式 | DashScope | OpenAI | Claude | +| --- | --- | --- | --- | +| 资源-动作 | `dashscope files upload` | `openai api files.create` | N/A | +| 命名风格 | kebab-case (fine-tunes) | dot-notation (fine_tuning.jobs) | 无子命令(flag 驱动) | +| 参数风格 | `--training-file-ids` | `--training_file` | `--model` | +| 输入方式 | 命令行参数 | 命令行参数 + stdin | stdin + 交互式 | +| JSON 输入 | `--messages '[{"role":"user",...}]'` | `-g user "Hello"` | 直接输入 | + +--- + +## 5. 功能深度对比 + +### 5.1 流式输出 + +**DashScope CLI** + +```bash +dashscope generation create -m qwen-plus -p "Hello" --stream +``` + +```python +# 实现:逐个 chunk 输出 +if stream: + for rsp in response: + if rsp.status_code == HTTPStatus.OK: + typer.echo(rsp.output) # 原始 output dict +``` + +问题: + +- 输出的是 `rsp.output`(原始 dict),不是纯文本 +- 无增量拼接 +- 无进度指示 + +**OpenAI CLI** + +```bash +openai api chat.completions.create -m gpt-4o -g user "Hello" --stream +``` + +- 每个 chunk 输出 JSON +- 支持 `--stream` flag +- 输出格式与非流式一致(JSON) + +**Claude CLI** + +```bash +claude -p "Hello" --output-format stream-json +``` + +- 流式 JSON 事件(assistant / tool_use / result) +- 交互模式下实时渲染 Markdown +- 支持 `--output-format text` 纯文本流 + +### 5.2 多模态输入 + +**DashScope CLI** + +```bash +# 图像理解 +dashscope multimodal-conversation create \ + -m qwen-vl-plus \ + -p "Describe this image" \ + --image-url "https://example.com/cat.jpg" + +# 语音识别 +dashscope transcription create \ + -m paraformer-v2 \ + -f audio.wav +``` + +**OpenAI CLI** + +```bash +# 图像(通过 messages JSON) +openai api chat.completions.create \ + -m gpt-4o \ + -g user '[{"type":"text","text":"Describe"},{"type":"image_url","image_url":{"url":"https://..."}}]' + +# 语音 +openai api audio.transcriptions.create -m whisper-1 -f audio.mp3 +``` + +**Claude CLI** + +```bash +# 不直接支持多模态 API 调用 +# 但可以读取本地文件(包括图片)作为上下文 +claude -p "Describe this image" < image.png # 管道输入 +``` + +### 5.3 异步任务管理 + +**DashScope CLI — 独有优势** + +```bash +# 创建异步任务 +dashscope image-synthesis create -m wanx-v1 -p "A cat" --async + +# 查看任务状态 +dashscope image-synthesis get + +# 等待任务完成(内置轮询) +dashscope video-synthesis create -m xxx -p "A cat" --wait + +# 微调任务流式日志 +dashscope ft stream + +# RL 任务日志 +dashscope rl logs --follow +``` + +亮点: + +- 内置任务轮询(POLL_INTERVAL = 30s) +- Rich Progress 进度条(rl 命令) +- 任务状态跟踪(PENDING → RUNNING → SUCCEEDED/FAILED) + +**OpenAI CLI** + +```bash +# 微调任务 +openai api fine_tuning.jobs.create -m gpt-4o-mini --training_file file-xxx +openai api fine_tuning.jobs.retrieve +openai api fine_tuning.jobs.cancel +# 无内置轮询,需手动重复 retrieve +``` + +**Claude CLI** + +``` +# 不适用 — Claude CLI 不管理异步任务 +``` + +### 5.4 管道与脚本集成 + +**DashScope CLI** + +```bash +# 基本管道 +echo '{"prompt": "Hello"}' | dashscope generation create -m qwen-plus -p "$(cat)" + +# 批量处理(需要外部脚本) +for file in *.txt; do + dashscope embeddings create -m text-embedding-v2 -i "$(cat $file)" > "${file}.json" +done +``` + +问题: + +- 无原生 stdin JSON 输入 +- 无 `--json` 输入模式 +- 批量操作需要外部 shell 脚本 + +**OpenAI CLI** + +```bash +# stdin JSON 输入 +echo '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}' | \ + openai api chat.completions.create + +# 文件输入 +openai api chat.completions.create < request.json +``` + +亮点:支持从 stdin 读取完整 JSON 请求体。 + +**Claude CLI** + +```bash +# 管道模式 — 最强大 +cat error.log | claude -p "What's wrong with this?" +git diff HEAD~1 | claude -p "Write a commit message" +find . -name "*.py" | xargs claude -p "Find security issues" --output-format json + +# 脚本集成 +result=$(claude -p "What is 2+2?" --output-format json) +echo "$result" | jq '.result' +``` + +亮点: + +- 管道输入是核心设计 +- `--output-format json` 便于脚本解析 +- 可以处理任意文件内容 + +--- + +## 6. Claude CLI 独有能力(DashScope/OpenAI 完全缺失) + +### 6.1 交互式 REPL + +``` +$ claude +╭────────────────────────────────────────────╮ +│ Claude Code │ +│ │ +│ Type /help for available commands │ +╰────────────────────────────────────────────╯ + +> Fix the bug in auth.py where login fails for expired tokens + +I'll look at the auth.py file to understand the issue. + +[Read auth.py] +[Edit auth.py - line 42: Added token expiry check] +[Bash: python -m pytest tests/test_auth.py -v] + +All tests pass. The fix adds an expiry check before validating the token. +``` + +### 6.2 Agent 工具调用 + +Claude CLI 内置工具: + +- Read — 读取文件 +- Edit — 编辑文件(精确行级修改) +- Write — 写入新文件 +- Bash — 执行 shell 命令 +- Glob — 文件模式搜索 +- Grep — 内容搜索 +- WebSearch — 网络搜索 + +### 6.3 权限系统 + +```bash +# 6 种权限模式 +claude --permission-mode default # 每次操作都询问 +claude --permission-mode acceptEdits # 自动批准文件编辑 +claude --permission-mode plan # 只规划不执行 +claude --permission-mode auto # 自动批准安全操作 +claude --permission-mode dontAsk # 从不询问(危险) +claude --permission-mode bypassPermissions # 完全绕过(极危险) +``` + +### 6.4 会话管理 + +```bash +claude # 新会话 +claude --continue # 继续上次会话 +claude --resume # 选择历史会话恢复 +claude --session-id xxx # 恢复指定会话 +``` + +### 6.5 MCP 服务器集成 + +```bash +# 连接 GitHub MCP +claude --mcp-server "npx @anthropic-ai/mcp-server-github" + +# 连接数据库 MCP +claude --mcp-server "npx @anthropic-ai/mcp-server-postgres postgresql://..." + +# 在会话中使用 MCP 工具 +> List all open PRs on the dashscope/dashscope-sdk-python repo +``` + +--- + +## 7. 综合评分 + +| 维度 | DashScope CLI | OpenAI CLI | Claude CLI | +| --- | --- | --- | --- | +| API 覆盖广度 | ⭐⭐⭐⭐⭐ (18域) | ⭐⭐⭐⭐ (8域) | ⭐ (0域) | +| 命令一致性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 错误处理 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | +| 输出美化 | ⭐⭐⭐⭐ (Rich) | ⭐⭐ | ⭐⭐⭐⭐⭐ | +| 脚本集成 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 交互体验 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | +| Agent 能力 | ❌ | ❌ | ⭐⭐⭐⭐⭐ | +| 异步任务管理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ | +| 多模态支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | +| RL/微调 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ | +| Shell 补全 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| 文档/示例 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | + +--- + +## 8. DashScope CLI 改进建议 + +### 8.1 短期(P0)— 提升现有体验 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 1 | 添加 `dashscope auth login` | 交互式认证,保存 Key 到本地 | OpenAI `openai auth login` | +| 2 | 启用 Shell 补全 | add_completion=True + 安装脚本 | Typer 原生支持 | +| 3 | stdin JSON 输入 | `echo '...' \| dashscope generation create --stdin` | OpenAI CLI | +| 4 | `--output-format` 选项 | json / text / table 可选 | Claude CLI | +| 5 | 流式输出纯文本 | 流式时只输出 content 文本,不输出完整 dict | Claude CLI | + +### 8.2 中期(P1)— 补齐能力 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 6 | 管道模式 | `cat file.py \| dashscope generation create -m qwen-plus --stdin-prompt "Review this"` | Claude CLI | +| 7 | 批量处理 | `dashscope embeddings create --batch-file inputs.txt` | 自研 | +| 8 | 任务等待 + 进度 | 所有异步任务支持 `--wait` + Rich Progress | 已有(rl),扩展到所有 | +| 9 | `dashscope config` | 配置管理(默认模型、默认 Key、输出格式) | aws configure | +| 10 | JSON Schema 验证 | 输入参数本地验证,不发请求就报错 | OpenAI SDK | + +### 8.3 长期(P2)— 差异化竞争 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 11 | 交互式 REPL | `dashscope chat` 进入对话模式 | Claude CLI | +| 12 | Agent 模式 | 集成 acli 的 Agent 能力到 CLI | Claude CLI + 自有 acli | +| 13 | MCP 支持 | `dashscope --mcp-server` 连接外部工具 | Claude CLI | +| 14 | 会话管理 | `--continue` / `--resume` 多轮对话 | Claude CLI | +| 15 | 权限系统 | 工具调用分级确认 | Claude CLI | + +### 8.4 差异化定位建议 + +DashScope CLI 不应试图复制 Claude CLI(那是 Agent 赛道),而应强化自身优势: + +``` +DashScope CLI 定位:阿里云 AI 平台的完整管理工具 +├── 模型管理(list / get / deploy / scale) +├── 微调全流程(upload → create → monitor → deploy) +├── RL 训练全流程(create → logs → monitor) +├── 多模态全栈(图像/视频/语音/文本) +└── 与阿里云生态集成(OSS / RAM / 工作空间) +``` + +**独特价值**: + +- OpenAI CLI 不支持部署管理 → DashScope 有 +- Claude CLI 不做 API 管理 → DashScope 有 +- 两者都不支持 RL 训练 → DashScope 有 +- 两者都不支持视频生成 → DashScope 有 + +--- + +## 9. 命令设计最佳实践对比 + +### 9.1 命名规范 + +| 方面 | DashScope | OpenAI | 建议 | +| --- | --- | --- | --- | +| 资源名 | fine-tunes (kebab) | fine_tuning.jobs (dot) | DashScope 更好(shell 友好) | +| 动作名 | create / list / get / delete | create / list / retrieve / delete | 统一用 get 比 retrieve 更直觉 | +| 缩写 | ft = fine-tunes | 无 | DashScope 好 | +| 隐藏命令 | ✅ hidden=True | ✅ | 两者都好 | + +### 9.2 参数设计 + +| 方面 | DashScope | OpenAI | Claude | +| --- | --- | --- | --- | +| 模型 | `-m` / `--model` | `-m` / `--model` | `--model` | +| 输入 | `-p` / `--prompt` | `-g` / `--message` (role + content) | 直接输入 | +| 流式 | `-s` / `--stream` | `--stream` | `--output-format stream-json` | +| 全局 Key | `-k` / `--api-key` | 环境变量 | OAuth | +| 输出格式 | ❌ 无 | ❌ 无 | `--output-format` | + +### 9.3 退出码 + +| 退出码 | DashScope | OpenAI | Claude | +| --- | --- | --- | --- | +| 0 | 成功 | 成功 | 成功 | +| 1 | API 错误 / SDK 异常 | API 错误 | 执行失败 | +| 2 | 参数错误 | 参数错误 | 权限拒绝 | + +--- + +## 参考 + +- DashScope CLI 源码:`dashscope/cli/`(24 个文件) +- OpenAI CLI 文档 +- Claude CLI 文档 diff --git a/md/cli-source-level-comparison-2026-08.md b/md/cli-source-level-comparison-2026-08.md new file mode 100644 index 00000000..2f6952a4 --- /dev/null +++ b/md/cli-source-level-comparison-2026-08.md @@ -0,0 +1,259 @@ +# CLI 源码级对比(仅 CLI,不含 SDK):DashScope CLI vs OpenAI CLI vs Anthropic CLI + +> 对比维度:1) 用户接口友好 2) 错误码/错误契约 3) 流式/非流式、同步/异步 +> 源码/文档依据:生成时间:2026-08 +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 0. 首先必须讲清楚的事实:三家"CLI"已经不是同一物种 + +### 0.1 OpenAI 把 API CLI 删了 + +openai-python v2.35.0(2026-05-06)CHANGELOG 明确记录: + +``` +### Chores +* remove legacy python cli (32f36e4) +* rename legacy python cli entrypoint (a3b182d) +``` + +源码验证:v2.52.0 的 pyproject.toml 已无 CLI 入口点、无 cli extra、无 cli 模块。也就是说—— + +OpenAI 曾经有和 DashScope CLI 同类的东西(`openai api chat.completions.create ...`,随 pip 包分发),但在 2026 年 5 月主动删除了它。 + +OpenAI 现在的官方 CLI 是 Codex CLI:Rust 编写的独立二进制,`curl ... install.sh | sh` / npm / brew 安装,定位是编程 Agent(对标 Claude Code),不再是 API 管理工具。 + +### 0.2 Anthropic 从未在 Python SDK 里放过 CLI + +anthropic-sdk-python 源码中没有任何 CLI 模块/入口点。Anthropic 的官方 CLI 是 Claude Code(npm 包 `@anthropic-ai/claude-code`),同样是编程 Agent。 + +### 0.3 DashScope 是三家中唯一"SDK 内嵌 API 管理 CLI"的 + +| | DashScope | OpenAI | Anthropic | +| --- | --- | --- | --- | +| SDK 内嵌 CLI | ✅ dashscope 命令(Typer) | ❌ v2.35.0 已删除 | ❌ 从未有过 | +| 独立 Agent CLI | ⚠️ acli(独立项目) | ✅ Codex CLI(Rust 二进制) | ✅ Claude Code(npm) | +| API 管理类 CLI | ✅ 仍在维护 | ❌ 放弃该品类 | ❌ 放弃该品类 | + +**战略解读**:OpenAI/Anthropic 的战略是"API 调用请用 SDK,终端交互请用 Agent",中间的"API 管理 CLI"品类被两家同时放弃。DashScope CLI 因此有了双重身份: + +1. **机会**:该品类只剩 DashScope 一家在做,aws cli 式的平台管理工具有独立价值; +2. **警示**:两家头部厂商都判断"API CLI"不值得维护,DashScope 需要想清楚这个 CLI 的独特价值(答案大概率是:微调/部署/RL 等长流程管理,而不是 `generation create` 这种一条 curl 就能替代的调用)。 + +--- + +## 1. 用户接口友好 + +### 1.1 安装与认证 + +| | DashScope CLI | Codex CLI | Claude Code CLI | +| --- | --- | --- | --- | +| 安装 | `pip install dashscope`(SDK 附赠,零额外步骤) | `curl install.sh \| sh` / `npm i -g @openai/codex` / `brew install --cask codex`,原生二进制 | `npm i -g @anthropic-ai/claude-code` | +| 认证方式 | 环境变量 `DASHSCOPE_API_KEY` 或 `-k` 参数 | `codex login`(浏览器 OAuth,绑定 ChatGPT 订阅)或 API key;`codex login status` 退出码 0/1 可脚本化探测 | claude 首次运行引导 OAuth | +| 凭证管理命令 | ❌ 无 | ✅ codex login/logout/login status | ✅ /login、/logout | +| 多账号/多环境 | ❌ | ✅ ~/.codex/config.toml + `-c key=value` 单次覆盖 | ✅ settings 分层(企业/用户/项目) | + +**差距点**:DashScope CLI 连 `dashscope auth whoami`(验证当前 key 有效性)都没有。Codex 的 `login status` 用退出码做脚本探测是 CI 友好的标准做法,值得直接抄。 + +### 1.2 交互模型 + +| | DashScope CLI | Codex CLI | Claude Code CLI | +| --- | --- | --- | --- | +| 默认形态 | 一次性命令 | 交互式 TUI(codex 直接进入) | 交互式 REPL(claude 直接进入) | +| 脚本模式 | 所有命令天生可脚本化 | `codex exec`(短写 `codex e`) | `claude -p "..."` | +| 会话恢复 | ❌ | ✅ `codex resume [--last/--all]`、`codex exec resume` | ✅ `claude --continue/--resume` | +| 斜杠命令 | ❌ | ✅ /resume /goal /login 等 20+ | ✅ /help /clear /compact 等 | +| 图片/文件输入 | 参数传 URL/路径 | TUI 直接粘图 | 管道 `cat x \| claude -p` | + +### 1.3 帮助系统与可发现性 + +| | DashScope CLI | Codex CLI | Claude Code CLI | +| --- | --- | --- | --- | +| 帮助 | Typer 自动生成,no_args_is_help=True | 每命令独立参考页 + 成熟度标注(Stable/Beta/Experimental) | claude --help + /help | +| Shell 补全 | ❌ 源码确认 add_completion=False(cli/__init__.py:244) | ✅(Rust 端 clap 支持) | ✅ | +| 示例 | ❌ help 里无 example | ✅ 文档含场景化示例 | ✅ | +| 配置系统 | ❌ 无(全靠命令行参数) | ✅ ~/.codex/config.toml,命令行 -c k=v 覆盖,优先级明确 | ✅ settings.json 分层 | + +**差距点**:`add_completion=False` 是 DashScope CLI 里一个一行就能修的短板(Typer 原生支持生成 bash/zsh/fish 补全脚本)。 + +### 1.4 用户接口友好小结 + +- **Codex / Claude Code**:OAuth 登录引导 + 交互 TUI + 会话恢复 + 分层配置 + Shell 补全——把 CLI 当"产品"打磨; +- **DashScope CLI**:停留在"SDK 的命令行薄封装"水平,但有一个别人没有的优点——pip 装 SDK 即得,零额外安装,且命令覆盖面(18 个资源域)远超对手; +- **最痛的三个缺口**:无 auth 命令、无 shell 补全、无配置文件。 + +--- + +## 2. 错误码与错误契约 + +### 2.1 DashScope CLI:三家里错误信息最"全",但契约最"弱" + +源码(cli/common.py)显示 DashScope CLI 的错误处理其实是认真设计过的: + +```python +# common.py:35 — 错误输出带四要素 +def print_failed_message(rsp): + err_console.print( + f"[red]Failed[/red] request_id: {rsp.request_id}, " + f"status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}") + +# common.py:44 — 双层错误检查(HTTP 层 + 业务层) +def ensure_ok(rsp, check_business_error=True): + if rsp.status_code != HTTPStatus.OK: ... # HTTP 错误 + ... + if error_code and error_code != "": ... # HTTP 200 但业务错误 +``` + +- **四要素齐全**:request_id / status_code / code / message —— Codex 和 Claude Code 的错误输出都没有这么全的排查信息(Agent 类 CLI 面向人类,不需要); +- **双层检查**:能识别"HTTP 200 但业务失败"——这是 DashScope 网关特性,另两家不存在此问题; +- **装饰器兜底**:`handle_sdk_error("action")` 把 SDK 异常统一转成红色友好错误。 + +但契约层面有三个问题: + +1. **退出码单一**:所有失败都是 exit 1(`raise typer.Exit(1)`),无法像 `codex login status` 那样用不同退出码区分错误类别。脚本只能靠解析 stderr 文本判断错误类型。 +2. **流式分支的错误不退出**(源码实锤的 bug,cli/generation.py:172-189): + +```python +if stream: + for rsp in response: + if rsp.status_code == HTTPStatus.OK: + typer.echo(rsp.output) + else: + print_failed_message(rsp) # ← 只打印!循环继续,命令最终 exit 0 +else: + ... + print_failed_message(response) + raise typer.Exit(1) # ← 非流式分支有退出 +``` + +流式中途出错时命令以 0 退出,CI 里会被当成成功——这是与另两家对比后暴露出的真实缺陷。 + +3. **错误不可机器解析**:没有 `--output-format json` 的错误输出,stderr 是 Rich 着色的自然语言。 + +### 2.2 Codex CLI:为自动化设计的错误契约 + +- `--json` 模式下所有状态变化(含错误)是 NDJSON 事件流,错误是结构化事件而非文本; +- `codex login status` 用退出码表达状态(0=有凭证); +- `codex cloud` 列表 plain-text 打印任务状态,`--json` 时给出 tasks 数组 + cursor 分页——官方明确建议"Use --json for automation"; +- 权限/沙箱拒绝是显式审批流(approval policy),不是事后报错:untrusted / on-failure / on-request / never 四级,越界动作先问人。 + +### 2.3 Claude Code CLI + +- `--output-format json` 时错误进 JSON 结构,退出码非 0; +- 权限拒绝走 permission-mode 交互(acceptEdits/plan/bypassPermissions),同样把"错误"前置为"审批"。 + +### 2.4 错误契约对比表 + +| | DashScope CLI | Codex CLI | Claude Code | +| --- | --- | --- | --- | +| 错误信息完整度(request_id 等) | ⭐⭐⭐⭐⭐ 四要素 | ⭐⭐⭐ 结构化事件 | ⭐⭐⭐ | +| 退出码区分度 | ⭐ 只有 0/1 | ⭐⭐⭐⭐ 分命令设计 | ⭐⭐⭐ | +| 机器可解析错误 | ❌ 着色文本 | ✅ NDJSON | ✅ JSON | +| 流式中途出错行为 | ❌ exit 0(bug) | ✅ 错误事件 | ✅ 错误事件 | +| 危险操作防护 | N/A(只读多) | ✅ 沙箱+审批四级 | ✅ 权限模式 | +| 双层错误(HTTP+业务)检查 | ✅ 独有 | N/A | N/A | + +--- + +## 3. 流式 / 非流式、同步 / 异步 + +### 3.1 流式输出 + +| | DashScope CLI | Codex CLI | Claude Code | +| --- | --- | --- | --- | +| 开关 | `--stream` / `-s`(逐命令) | 交互 TUI 天然流式;exec 默认格式化流式输出 | 交互天然流式;`-p` 配 `--output-format stream-json` | +| 流的内容 | 原始 dict:`typer.echo(rsp.output)` + 单独一行 usage(generation.py:173-178) | NDJSON 事件(--json)或格式化渲染 | JSON 事件(assistant/tool_use/result)或渲染文本 | +| 增量控制 | 底层 incremental_output 参数,CLI 未暴露语义化选择 | N/A | N/A | +| 人类可读性 | ❌ 流出的是 Python dict 的 repr,不是纯文本 | ✅ 默认渲染 | ✅ Markdown 渲染 | +| 机器可读性 | ⚠️ 是 dict repr 不是 JSON,管道里没法 jq | ✅ NDJSON | ✅ JSON | + +DashScope 流式输出的具体问题(generation.py:175):`typer.echo(rsp.output)` 打印的是 dict 的字符串表示(单引号、Python 字面量),既不是增量文本也不是合法 JSON——人看不懂、机器没法解析,是三家里最弱的流式实现。改进方向很明确: + +- **人类模式**:只打印增量 content 文本; +- **机器模式**:`--output-format json` 时每 chunk 一行合法 JSON。 + +### 3.2 非流式 + +三家都是一条命令拿完整结果,差别在输出形态: + +| | DashScope CLI | Codex CLI | Claude Code | +| --- | --- | --- | --- | +| 默认输出 | output dict + usage(两行 Python repr) | 格式化文本 | 渲染文本 | +| JSON 输出选项 | ❌ 无(默认就不是 JSON) | ✅ --json | ✅ --output-format json | + +### 3.3 同步 vs 异步 —— DashScope 的主场 + +这是维度反转的一节:Agent 类 CLI 没有"异步任务"概念(任务在会话内同步完成),而 DashScope CLI 服务的 API 有大量异步长任务(微调、RL、视频生成、部署),CLI 为此建了完整的异步设施(cli/common.py:19-21、cli/agentic_rl.py): + +| 异步能力 | DashScope CLI | Codex CLI | Claude Code | +| --- | --- | --- | --- | +| 任务提交 | ✅ ft create / rl create / 异步图像视频任务 | ❌ | ❌ | +| 状态轮询 | ✅ 内置 POLL_INTERVAL=30s 轮询 + Rich Progress 进度条 | ❌ | ❌ | +| 日志跟随 | ✅ rl logs --follow(LOG_PAGE_SIZE=1000 分页拉取) | ❌ | ❌ | +| 任务列表/详情 | ✅ ft list/get、rl list/get | ❌ | ❌ | +| 取消 | ✅ ft cancel | ❌ | ❌ | +| 云端并行任务 | ❌ | ✅ codex cloud(云端环境并行跑任务,--json 输出 tasks 数组+cursor 分页,可从 Web/GitHub/Linear/Slack 发起) | ❌ | + +**值得注意**:Codex cloud 本质上是 OpenAI 版的"异步任务管理",只是对象从"微调任务"变成了"编程任务"——提交、状态查询、分页列表、JSON 输出,形态与 DashScope 的 ft/rl 管理惊人相似。这说明"长任务管理 CLI"这个品类没有消失,只是换了载体。 + +### 3.4 同步/异步维度小结 + +- DashScope CLI 的同步调用(generation create 等)体验弱于对手的 Agent 交互,但这不是它的核心价值; +- DashScope CLI 的异步任务管理(提交→轮询→日志→取消)是三家中唯一面向 AI 训练/生成流水线的,配合 Rich Progress 有不错的体验; +- 对标 Codex cloud 后,DashScope 的异步管理可以补的一课是:任务列表的 `--json` 机器输出 + cursor 分页。 + +--- + +## 4. 总评分 + +| 维度 | DashScope CLI | Codex CLI | Claude Code | +| --- | --- | --- | --- | +| 安装便捷(pip 附赠) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| 认证体验 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 交互模式 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 配置系统 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 错误信息完整度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | +| 错误契约(退出码/机器可读) | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| 流式输出质量 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 异步任务管理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐(cloud) | ⭐ | +| API 资源覆盖 | ⭐⭐⭐⭐⭐(18 域) | ⭐(Agent 域) | ⭐(Agent 域) | + +--- + +## 5. 给 DashScope CLI 的建议(纯 CLI 视角) + +### 立即修(小改动,高回报) + +1. **修流式错误退出码 bug**:generation.py 流式分支出错后补 `raise typer.Exit(1)`(§2.1 实锤); +2. **开 Shell 补全**:`add_completion=True`(一行改动,cli/__init__.py:244); +3. **流式输出改为增量纯文本**:默认打 content 增量,加 `--output-format json` 时输出每行合法 JSON; +4. **加 `dashscope auth whoami/status`**:验证 key 有效性,退出码区分(抄 Codex login status)。 + +### 中期 + +5. **错误退出码分级**:认证错=2、参数错=3、限流=4、服务端=5,脚本可免解析 stderr; +6. **`--output-format json` 全局化**:所有命令(尤其 ft list / rl list)支持 JSON + cursor 分页输出; +7. **配置文件**:`~/.dashscope/config.toml` 存默认 model/workspace/输出格式,`-c` 覆盖(抄 Codex 的 config 优先级设计)。 + +### 定位建议 + +8. 不要再和 Codex/Claude Code 比"对话体验"——那是 Agent 赛道,acli 在跟; +9. DashScope CLI 的对标物其实是 Codex cloud + aws cli 的混合体:把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成三家中无可替代的那一个,这才是 OpenAI/Anthropic 删除 API CLI 之后留下的空位。 + +--- + +## 附:源码证据索引 + +| 结论 | 证据 | +| --- | --- | +| OpenAI 删除 API CLI | openai-python/CHANGELOG.md v2.35.0 "remove legacy python cli";v2.52.0 无 cli 入口点/extra | +| Anthropic SDK 无 CLI | anthropic-sdk-python 全仓无 cli 模块/入口点 | +| DashScope 关闭补全 | dashscope/cli/__init__.py:244 add_completion=False | +| 错误四要素+双层检查 | dashscope/cli/common.py:35-91 | +| 流式错误 exit 0 bug | dashscope/cli/generation.py:172-189 | +| 流式输出 dict repr | dashscope/cli/generation.py:175 typer.echo(rsp.output) | +| 异步轮询常量 | dashscope/cli/common.py:19-21 POLL_INTERVAL=30 | +| RL 进度条/日志 | dashscope/cli/agentic_rl.py Rich Progress、logs follow | +| Codex exec/--json/login status/resume/cloud | developers.openai.com/codex Developer commands 参考页 | +| Codex 沙箱+审批 | developers.openai.com/codex/security | diff --git a/md/conclusion-sdk-architecture-stainless.md b/md/conclusion-sdk-architecture-stainless.md new file mode 100644 index 00000000..cf8b2970 --- /dev/null +++ b/md/conclusion-sdk-architecture-stainless.md @@ -0,0 +1,57 @@ +# 结论:SDK 架构(Stainless 孪生架构实证) + +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 实证:两个仓库的目录几乎完全同构 + +`comm -3` 对比两个 src/ 目录,只有 14 项不同,其余 17 项(_base_client.py、_client.py、_exceptions.py、_streaming.py、_models.py、_response.py、_resource.py、_qs.py、pagination.py、resources/、types/、lib/……)文件名逐一对应。且文件头直接写着: + +``` +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +``` + +连 _streaming.py 里的注释都一样("initially copied from httpx-sse")——因为是同一份模板复制出去的。 + +## Stainless 是什么 + +Stainless(stainlessapi.com)是一家专门做"SDK 生成器"的公司。工作方式: + +``` +厂商的 OpenAPI spec(机器可读的 API 定义) + │ + ▼ +Stainless 生成器(模板 + 各语言运行时) + │ + ▼ +完整的类型安全 SDK:Python / TypeScript / Go / Java / Kotlin / Ruby +``` + +客户包括 OpenAI、Anthropic、Cloudflare、Cohere、Lithic 等。厂商不手写 SDK,而是维护 OpenAPI spec,Stainless 的 CI 在 spec 变更后自动重新生成 SDK 并发 PR。 + +## "孪生架构"具体指什么 + +同一个模板生成,所以两个 SDK 的"骨架"完全一样: + +| 层 | OpenAI | Anthropic | 来源 | +| --- | --- | --- | --- | +| HTTP 基座 | SyncAPIClient/AsyncAPIClient(httpx) | 同名同实现 | 模板 | +| 重试逻辑 | _should_retry + Retry-After + 指数退避 | 逐行同构 | 模板 | +| 异常层次 | APIStatusError → 400/401/403/404/409/422/429/5xx | 同一棵树 | 模板 | +| 流式机制 | Stream[T] / AsyncStream[T] SSE 迭代器 | 同名同构 | 模板 | +| 未传参哨兵 | NOT_GIVEN | NOT_GIVEN | 模板 | +| 响应模型 | Pydantic + model_construct | 同 | 模板 | + +差异只在两处: + +1. **API 表面**——resources/ 和 types/ 目录由各自的 OpenAPI spec 生成,内容完全不同(chat.completions vs messages); +2. **手写扩展**——就是 comm 输出里那 14 个不同项:OpenAI 的 providers/(多云)、_provider.py、realtime/websocket 相关;Anthropic 的 lib/credentials/(OAuth 凭证链)、_middleware.py、AnthropicAWS/AnthropicFoundry 子类。 + +## 这对我们的对比意味着什么 + +三点推论: + +1. 对比 OpenAI SDK vs Anthropic SDK 时,"框架能力"那部分结论对两者同时成立。重试、异常驱动、类型系统、四象限覆盖(同步/异步 × 流式/非流式)这些优点不是某一家的设计选择,而是 Stainless 模板的标配。真正值得单独评的是两家的手写扩展(Anthropic 的 text_stream、OpenAI 的 parse() 等)。 +2. DashScope 的差距要重新定性:不是"DashScope 工程师写得不如人",而是**手写实现 vs 工业化流水线的差距**。Stainless 模式下,API 加了个新参数 → spec 更新 → SDK 自动生成带完整类型和测试的版本,天级送达;手写模式下要人肉改签名、补文档、补测试,还容易漂移(DashScope Generation.call 里那 30 个 Any 参数和手工 `if x is not None` 搬运就是这种漂移的产物)。 +3. 这也解释了之前建议里 P2 那条"代码生成":如果 DashScope 要追平,方向不是继续手写补丁,而是把百炼 API 沉淀成 OpenAPI spec,接入生成器(Stainless 是商业服务,也可用开源方案如 oapi-codegen/自研模板),让 SDK 从 spec 自动长出类型和资源类。 diff --git a/md/dashscope-2.0-1x-dependency-decoupling.md b/md/dashscope-2.0-1x-dependency-decoupling.md new file mode 100644 index 00000000..682b6e7c --- /dev/null +++ b/md/dashscope-2.0-1x-dependency-decoupling.md @@ -0,0 +1,146 @@ +# DashScope 2.0 ↔ 1.x 依赖清单与解耦方案 + +> 目标仓库:`moe-dashscope-sdk-python` +> 作用:作为"2.0 上提为顶层、1.x 归入 legacy"这次大版本重构的**强制前置(Step 0)**。 +> 背景:2.0(`dashscope/v2/`)并非自包含——它对 1.x 有 **8 处反向依赖**。任何 `git mv` +> 之前必须先把这 8 处解掉或归类,否则搬动 1.x 会直接打断 2.0 的运行时导入。 + +## 1. 解耦原则 + +1. **共享基础设施**:若代码量不多,**直接在 2.0 内重新实现**,让 2.0 自己拥有,消除对 1.x 的依赖。 +2. **功能级委托**:做**反向搬迁**——把实现从 1.x 移到 2.0,原 1.x 路径改为**反向引用 2.0**(留薄垫片)。 +3. **例外(单一事实来源)**:凡"进程级唯一 / 发行版级唯一"的模块,**禁止复制到 2.0**,否则破坏其唯一性不变量。此类保持为 1.x/2.0 **共用的中立叶子**,两边都 import 它(下详)。 + +> 原则 3 是对原则 1 的必要修正:`version`、`_sdk_session` 表面上也是"共享基础设施且代码极少", +> 但它们的价值恰恰在于**全局只有一份**。复制进 2.0 = 两个版本号 / 两个 session id = bug。 + +## 2. 依赖清单总表 + +| # | 2.0 引用点 | 依赖的 1.x 模块 | 规模 | 类别 | 原则 | 动作 | 风险 | +|---|---|---|---|---|---|---|---| +| D1 | `transport/_ident.py:53`、`transport/http.py:125` | `dashscope.version.__version__` | 4 行 | 单一来源 | 3 | 保持共用叶子;重构后两边仍指向同一处 | 低 | +| D2 | `transport/_ident.py:24` | `dashscope.common._sdk_session.sdk_session_id` | 43 行 | 单一来源 | 3 | 保持共用叶子(模块注释已声明此为唯一来源) | 低 | +| D3 | `v2/__init__.py:19` | `dashscope.common.api_key.save_api_key` | 函数 ~12 行 | 小型基础设施 | 1 | 在 2.0 内重写 `save_api_key`(仅文件写入) | 低 | +| D4 | `resources/chat/completions.py:17` | `dashscope.utils.param_utils.ParamUtil` | 31 行 | 小型基础设施 | 1 | 在 2.0 内重写该纯静态方法 | 低 | +| D5 | `v2/__init__.py:20` | `dashscope.tokenizers.{get_tokenizer,list_tokenizers}` | 434 行(tiktoken 封装) | 中型基础设施 | 1 / 2 | 优先随 2.0 拥有(见 §3.3) | 中 | +| D6 | `resources/_oss.py:26`(5 处调用) | `dashscope.utils.oss_utils.check_and_upload_local` | 312 行模块,与 1.x 深缠 | 中型基础设施 | 1 | 把 OSS 签名+上传逻辑移植为 2.0 原生(httpx + v2 errors) | 中 | +| D7 | `client.py:147/361`、`resources/_agentstudio.py:49/50/53` | `dashscope.agentstudio`(Client/AsyncClient/exceptions/pagination) | 8533 行,完整子 SDK | 功能级委托 | 2 | 整包移入 2.0,`dashscope/agentstudio` 改薄垫片反向引用 | 中高 | +| D8 | `resources/reinforcement_learning.py:74-76` | `dashscope.finetune.{agentic_rl, reinforcement, reinforcement.common}` | `agentic_rl.py` 685 行 + `reinforcement/` 11270 行(不含 examples 2349 行) | 功能级委托 | 2 | 仅移 `reinforcement/` 子包 + `agentic_rl.py` 入 2.0,原路径反向引用;`finetune/finetunes.py`、`deployments.py` 留在 1.x | 高 | + +## 3. 分类详解 + +### 3.1 单一事实来源(原则 3,禁止复制)—— D1、D2 + +- **D1 `dashscope/version.py`**:仅 `__version__ = "2.0.0"`,是**整个发行版**的版本号唯一来源。2.0 的 + `_ident.py` / `http.py` 用它拼 `User-Agent`。绝不能"在 2.0 内再写一份版本号"——两处会漂移。 + 重构后无论 1.x/2.0 目录怎么挪,`version.py` 应留在**中立的发行版根**(或 `dashscope/_shared/`),两边共同 import。 +- **D2 `dashscope/common/_sdk_session.py`**:进程级唯一 `x-dashscope-sdk-session-id`。模块自身注释已写明: + *"一个进程可能同时经 1.x 与 2.0 发请求,2.0 还会把 rl/agentstudio 委托进 1.x;若各自 mint id,一个逻辑会话会裂成多个 id,服务端日志分组失效。"* + 这正是我们要解耦的委托场景本身。因此它**必须保持为两边共用的叶子**,复制到 2.0 会破坏"一进程一 id"不变量。 + +**动作**:D1/D2 不参与"移入 2.0",而是确立为**中立共享层**(建议 `dashscope/_shared/`,只依赖 stdlib, +不 import `dashscope/__init__`,避免环)。这一步同时是重构后避免循环导入的锚点。 + +### 3.2 小型共享基础设施(原则 1,直接在 2.0 重写)—— D3、D4 + +- **D3 `save_api_key`**:v2 只用"写 key 文件"这一支(不碰读 1.x 全局 `dashscope.api_key` 的 `get_default_api_key`)。 + 在 2.0 内实现约 12 行(`os.makedirs` + 写文件),常量 `DEFAULT_DASHSCOPE_CACHE_PATH` / + `DEFAULT_DASHSCOPE_API_KEY_FILE_PATH` 一并带入 2.0 的 config 常量表。随后 `v2/__init__.py` 改为本地导出。 +- **D4 `ParamUtil.should_modify_incremental_output`**:31 行纯函数(按模型名判断是否关闭增量输出),无任何依赖。 + 直接在 2.0 内落一个等价函数(如 `dashscope/v2/resources/chat/_params.py`),`completions.py` 改本地导入。 + +**动作**:两者代码量都很小、无外部依赖,是原则 1 的教科书案例,可各自独立成 PR,零回归面。 + +### 3.3 中型共享基础设施(原则 1,但非平凡)—— D5、D6 + +- **D5 `tokenizers`(434 行)**:是对 tiktoken 的自包含封装,2.0 仅通过 `v2/__init__.py` 再导出 + `get_tokenizer` / `list_tokenizers` 作便利入口。它**不与 1.x 业务耦合**,更像可整体归属 2.0 的叶子。 + 建议:重构时把 `dashscope/tokenizers/` 归 2.0 拥有,原路径留再导出垫片(本质是原则 2 的轻量版); + 若短期不动,则至少确认它是纯叶子,可安全共享。**注意**:不要"复制"——tokenizer 加载/缓存应只有一份。 +- **D6 `oss_utils.check_and_upload_local`(312 行模块)**:是本次**最重的基础设施移植**。该模块 import 了 + `DashScopeAPIResponse`、`client.base_api.GetMixin`、`common.error/logging/utils`、`requests`——与 1.x 客户端机制深缠。 + v2 在 `_oss.py` 有 5 处调用(多模态本地文件上传取 OSS 凭证并上传)。 + 建议:只把**实际用到的上传路径**(`upload_file` + `check_and_upload_local` 的签名/上传逻辑,约百余行) + 移植为 **2.0 原生实现**(走 v2 的 httpx transport 取凭证、抛 `dashscope.v2.errors`),不引入 1.x 的 + `GetMixin` / `DashScopeAPIResponse`。这是"代码量不算少"的边界项,单列一个 PR,配 OSS 上传回归测试。 + +### 3.4 功能级委托(原则 2,反向搬迁 + 1.x 反向引用)—— D7、D8 + +共同前提:`agentstudio` 与 `finetune.reinforcement` **都未在 1.x 包根 `dashscope/__init__.py` 再导出** +(经核对),只通过深路径 `dashscope.agentstudio` / `dashscope.finetune.reinforcement` 访问。 +因此反向引用垫片的兼容面**只是这两条深路径**,不涉及 1.x 顶层 65 个公开名,风险可控。 + +- **D7 `agentstudio`(8533 行)**:是一个完整子 SDK,且已是"2.0 风格"(自带 `transport` / `RequestOptions` / + `APIResponse`)。2.0 侧 `resources/_agentstudio.py` 只是**透明错误对齐代理**——挂载 1.x 的 + `agentstudio.Client`,把其异常族翻译成 `dashscope.v2.errors`(设计注释明言"不改动任何 1.x 代码")。 + **动作(原则 2)**:把整个 `dashscope/agentstudio/` 迁到 2.0 归属(重构后即 2.0 顶层下的 `agentstudio/`); + 原 `dashscope/agentstudio/__init__.py` 改为**薄反向引用**(`from <2.0>.agentstudio import *` 或 `sys.modules` 别名), + 保证 `import dashscope.agentstudio` 与其深导入仍解析到同一份实现。2.0 的错误对齐代理可就地保留。 + **注意(实测补充)**:agentstudio 并非完全自包含——`client.py`/`exceptions.py`/`streaming.py`/`transport.py` + 还 import 1.x 的 `common.api_key.get_default_api_key`、`common.env.validate_region` / + `validate_workspace_id`、`common.error.DashScopeException`(其异常基类)、`common.logging.logger`、 + `common.utils.get_sdk_headers`。迁入 2.0 后这些成为对 1.x `common/` 的新增依赖;搬迁前需把这几支 + 归类为中立共享层(同 D1/D2)或一并处理,否则 §4 的递减 gate 会在 agentstudio 上卡住。 +- **D8 `finetune.reinforcement` + `agentic_rl`(合计约 11955 行运行时)**:本次**最大搬迁**。 + 2.0 的 `resources/reinforcement_learning.py:151` 经模块内 `_new_agentic_rl()` 构造 + `_agentic_rl.AgenticRL(api_key=...)`——只从 v2 client 取 `config.api_key` 字符串传入, + 不再持有 client 本体,耦合面比撰写本文时更小,业务归属仍在 2.0。 + **动作(原则 2,注意是"部分搬迁")**:只把 `dashscope/finetune/reinforcement/`(子包)与 + `dashscope/finetune/agentic_rl.py` 迁入 2.0 归属;**`finetune/finetunes.py`、`finetune/deployments.py` + 属于 1.x 根公开 API(`FineTunes` / `Deployments`),留在 1.x 不动**。原 + `dashscope.finetune.reinforcement` / `dashscope.finetune.agentic_rl` 改为反向引用垫片。 + `reinforcement/examples/`(2349 行)是样例,按仓库惯例处理(随包或移到 `examples/`),不计入运行时耦合。 + **注意(实测补充)**:`agentic_rl.py` 自身还拖带 1.x 依赖——`:10` `client.base_api.CreateMixin`、 + `:11` `finetune.customize_types`、`:17` `finetune.finetunes.FineTunes`(正是上文计划留在 1.x 的模块)。 + 迁入 2.0 会因此新增 v2→1.x 依赖,同样会卡住 §4 的递减 gate;搬迁前需先处理这三支 + (CreateMixin/customize_types 归类或重写,对 `FineTunes` 的依赖解耦)。 + +### 3.5 共享产品面:`acli`(顶层中立,1.x/2.0 都支持)—— 不属于任何一侧 + +`dashscope/acli/`(约 **36614 行**)是内嵌的交互式 AI 助手(agentic CLI),经 `python -m dashscope.acli` +启动(`acli/__main__.py` → `dashscope.acli.cli:main`),并在 `setup.py` 拥有独立可选依赖组 +(`acli` / `acli-anthropic` / `acli-openai` / `acli-voice` / `acli-camera` / `acli-all`)。 + +它的定位与 D1–D8 不同,**不是 2.0 对 1.x 的反向依赖**,而是一个**同时服务于 1.x 与 2.0 的顶层产品面**: + +- **依赖方向(实测)**:`acli` → 1.x,仅 2 处真实导入——`acli/ui/tts.py:287` 用 `dashscope.audio.tts_v2`、 + `acli/ui/voice.py:61` 用 `dashscope.audio.asr.{Recognition,RecognitionCallback}`。 + `acli` **不 import `dashscope.v2`**;1.x 包根与 2.0 也**都不 import `acli`**(仅 `v2/transport/_ident.py:22` + 有一句注释提及)。即 `acli` 是 SDK 的**消费方 / 独立入口**,不是任何一侧 API 层的组成部分。 + +**动作(布局归属)**:重构后 `acli` **保持在顶层 `dashscope/acli/` 不动**,与 `_shared`(§3.1)同属 +**中立层**——既不进 1.x legacy 桶,也不并入 2.0 实现桶。理由:它对两栈都成立,且是被安装的独立产品入口。 + +**关键约束(勿在解耦中改写 acli 内部)**:据 `dashscope/common/_sdk_session.py` 的模块注释,`acli` 是 +**agenticCLI 仓库的单向同步副本(one-way sync copy)**,连 session id 都刻意自持一份——因为任何本地改动 +都会被下次同步覆盖。因此: + +1. **不要**把 `acli` 内部作为本次解耦的重构对象(改了也会被 sync 冲掉)。 +2. 重构搬动 1.x 时,**必须保证 `acli` 依赖的两条 1.x 音频路径继续可解析**:`dashscope.audio.tts_v2`、 + `dashscope.audio.asr`。两种做法二选一——(a) 由 1.x→legacy 的兼容垫片让这两条深路径照旧解析(推荐, + 零改 acli);(b) 若确需改 `acli` 的 import,则**同步更新上游 sync 映射**,否则下次同步回退。 +3. `acli` 未来若要改用 2.0 client(它"支持 2.0"),属于 **agenticCLI 上游**的演进,不在本仓库解耦范围内。 + +## 4. 建议执行顺序(小→大,每项独立可合并) + +1. **D1/D2**:确立 `dashscope/_shared/` 中立层(version + sdk_session),两边改指此处。*(低风险,且是防环锚点)* +2. **D3**、**D4**:各自在 2.0 内重写。*(低风险,两个独立小 PR)* +3. **D5**:tokenizers 归属 2.0 + 垫片。*(中)* +4. **D6**:OSS 上传逻辑 2.0 原生化。*(中,需回归测试)* +5. **D7**:agentstudio 反向搬迁 + 垫片。*(中高)* +6. **D8**:reinforcement/agentic_rl 部分反向搬迁 + 垫片。*(高,最后做)* + +原则:每一步完成后,`grep -rn "dashscope\.<非v2>" dashscope/v2/` 的命中应**单调递减**,直到只剩 D1/D2 +指向 `_shared`。D3–D8 全部落地后,2.0 对 1.x **业务模块零依赖**,此时才具备"1.x 移入 legacy、2.0 上提顶层" +的物理搬迁条件(见 `dashscope-v2-refactor-tasks.md` / 顶层重构评估)。 + +## 5. 回归验证 gate(每步必过) + +- `python -c "import dashscope; import dashscope.v2"` 无循环导入、无告警。 +- `pytest tests/v2` 全绿;涉及 D6/D7/D8 时补对应回归(OSS 上传、agentstudio 挂载与错误翻译、RL 构造)。 +- 反向引用垫片生效:老深路径 `import dashscope.agentstudio`、`from dashscope.finetune.reinforcement import ...` + 仍解析到迁移后的**同一份对象**(`is` 一致),不产生双实现。 +- `x-dashscope-sdk-session-id` 在"同进程同时用 1.x 与 2.0"下**仍为同一 id**(守护 D2 不变量)。 +- `python -m dashscope.acli --help` 可启动;`acli` 依赖的 `dashscope.audio.tts_v2`、`dashscope.audio.asr` + 两条深路径在重构后**仍可解析**(守护 §3.5 中立层约束);`acli` 保持在顶层 `dashscope/acli/` 未被搬动。 +- 全仓 secret 扫描通过,版本号仅一处(D1)。 diff --git a/md/dashscope-2.0-advantages-report.md b/md/dashscope-2.0-advantages-report.md new file mode 100644 index 00000000..5a6b1371 --- /dev/null +++ b/md/dashscope-2.0-advantages-report.md @@ -0,0 +1,569 @@ +# dashscope 2.0 差异化优势分析报告 + +> 基于 `md/dashscope-2.0-design.md`(§0 / §2.2 / §2.7 / §3 / §4 / §5)与三份对比调研 +> (`sdk-comparison-openai-vs-dashscope.md`、`sdk-comparison-anthropic-vs-dashscope.md`、 +> `dashscope-2.0-interfaces-mapping.md`)收敛。 +> 状态:**活文档**。本报告不停留在自我背书——每条优势都要在实现级文档里验真(见 §7)。 +> **约定:后续围绕这些结论的讨论,一律沉淀进本文档**(新增验真节 / 修订对应章节 / +> 补入待探究清单),不只在对话里口头结论。 +> 验真依据:`md/dashscope-2.0-interfaces-examples.md`(实现级调用示例,反映 `dashscope/v2/` 现状)。 + +--- + +## 0. 结论速览 + +dashscope 2.0 的差异化不在"比 OpenAI/Anthropic 更现代"——现代基线(Client 实例化、 +Pydantic、异常驱动、自动重试、httpx、流式人体工学)是**追平**,不是超越(设计原则 P1: +对齐行业基线、不做发明)。 + +真正的护城河是两条正交的轴: + +- **轴一「一个 SDK 管全部」= 横向覆盖面**:一个 Client 覆盖(模型模态 × 协议 × provider)。 +- **轴二「全链路长任务」= 纵向生命周期**:files → 微调 → 部署 → RL → agentstudio 的生产链路, + 其中耗时环节由统一的 `jobs.create/fetch/wait` 长任务模式支撑。 + +OpenAI/Anthropic 基本只占「轴一的文本那一格 + 单次推理调用」,既无协议通吃,也无平台链路。 + +> **验真后修正(详见 §7)**:两条轴里有的点是真护城河,有的点被夸大。逐点验真后, +> **真正扛得住的**是「长任务统一模式 + 错误可定位性 + files→微调→部署 这段链路」; +> 而「协议通吃」「全链路统一体验」「两行 quickstart」三个卖点都有**实现级硬边界** +> (协议特有参数切不动 / agentstudio 未对齐 v2 / `workspace` 强制),从客户友好维度 +> 不能照单全收。下面各章是"宣称",§7 是"验真",两者对照读。 + +--- + +## 1. 先分清两类能力:追平基线 vs 差异化 + +设计原则 P1 明确:以下都是 OpenAI/Anthropic(同为 Stainless 生成)**已验证的基线**, +2.0 只是把 1.x 的短板补齐,**不构成相对两家的优势**: + +| 追平项 | 1.x 现状 | 2.0 落点 | +| --- | --- | --- | +| Client 实例化 | 全局变量 `dashscope.api_key`,多租户易错 | `dashscope.Client(...)` 资源式 | +| 异常驱动错误处理 | 返回值 `if status_code != 200`,漏检即静默失败 | 9 个扁平异常类 + 目录驱动(§2.7) | +| 自动重试 | 核心路径无重试 | `max_retries=2` + Retry-After + 指数退避 | +| 类型化响应 | `**kwargs` 透传、无 IDE 提示 | Pydantic + `@overload` + `.pyi` | +| 流式人体工学 | 手设 `incremental_output` + 深层路径 | `stream.text_stream` / `get_final_message()` | +| 便捷属性 | `resp.output.choices[0].message.content` 四层 | `resp.output_text` 一行 | +| httpx 统一栈 | requests + aiohttp 双栈 | httpx 同步/异步共用、连接池复用 | + +> **诚实提醒**:工程成熟度上两家仍领先——Anthropic 的 `@beta_tool` 装饰器 + `tool_runner` +> 自动工具循环、spec 自动生成流水线(DashScope codegen 要到 M6 才接管)、生态与社区。 +> 2.0 的定位是「追平 + 错位竞争」,不是全面超越。 + +--- + +## 2. SDK 易用性 / 出错可定位:相对现状 + +这两项是调研评分中 DashScope 差距最大的维度,也是 P2「契约正确性优先于功能数量」要修的 +"最高危静默错误"。下面给"现在相对处在什么位置"。 + +### 2.1 易用性(usability) + +| 阶段 | 状态 | 相对竞品 | +| --- | --- | --- | +| 1.x | 三套调用风格并存(`Generation.call` / `AioGeneration.call` / `ImageSynthesis.async_call`)、全局变量配置、参数散落 `**kw`、响应路径四层、流式需手设 `incremental_output` | **代际落后** | +| 2.0 | 资源式 Client、`output_text` 便捷属性、`stream.text_stream`/`get_final_message()`、`@overload` 流式/非流式静态可分、Quickstart 两行上手 | **基线齐平** OpenAI/Anthropic | + +- 结论:从"代际落后"→"基线齐平"。 +- **仍差一点**:工具调用人体工学——Anthropic 有 `@beta_tool`(从函数签名+docstring 自动生成 + schema)和 `tool_runner`(自动跑"调用→回填→再问"循环),2.0 目前仍需手写 schema、手动解析 + `tool_calls`、手动循环。这是易用性上唯一明显未追平的点(见待探究清单)。 + +### 2.2 出错信息可定位(error locatability / 可运维性) + +| 维度 | 1.x 现状 | 2.0 落点 | 出处 | +| --- | --- | --- | --- | +| 错误暴露方式 | 返回值式,漏写 `if` 即**静默失败** | 异常驱动,9 个扁平状态类 | §2.7 | +| 异常携带信息 | `status_code`/`code`/`message` 分散 | 统一带 `status_code / code / message / **request_id** / body / response.headers` | 设计 §2.2 line 131 | +| request_id | **日志缺 request_id**,无法定位到工单 | request_id 入日志 + 挂异常,**排障可定位到工单级** | §2.2 日志 | +| "200 但业务失败" | 网关特性,易漏检 | 三层来源一个异常族(HTTP 层 / 业务层 200 带 code / SDK 层) | §2.7 | +| 错误码 | `"Throttling"`/`"InvalidApiKey"` 裸字符串 | 可 import 的 `ErrorCode` 枚举(13 项线缆码)+ 目录派生类名/状态码 | §2.7 | +| 敏感信息 | 无过滤,**API Key 有泄露入日志风险** | `SensitiveHeadersFilter` 过滤 Authorization | §2.2 日志 | +| 排障开关 | 无统一入口 | `DASHSCOPE_LOG=debug` 一键开启,记录状态码/重试原因/等待时间 | §2.2 日志 | +| 流中出错 | 被吞、不退出 | 统一抛异常,不 yield 错误对象 | §2.2 流式 | +| CLI 退出码 | **流式出错仍 exit 0**(CI 误判成功) | 退出码分级 0/1/2/3/4 + 流式中途出错 exit 1 | §4.2 | + +- 结论:可定位性是 2.0 投入最重的维度——`request_id` 全链路贯通(日志 + 异常 + 响应模型 + `ResponseBase`)、目录驱动的错误码、敏感头过滤,对齐 Anthropic 的 `_request_id` 与异常层级, + 把"静默错误"这一唯一会导致用户数据/CI 结果错误的类别系统性堵死。 +- 相对竞品:**齐平**(异常细分对齐两家,request_id 对齐 Anthropic,目录驱动的错误码枚举甚至比 + 手写更不易漂移)。 + +--- + +## 3. 轴一:「一个 SDK 管全部」——横向覆盖面 + +"全部"是**三层覆盖**,任意单层拎出来 OpenAI/Anthropic 都不全: + +| 层 | 含义 | 两家官方 SDK 的缺口 | +| --- | --- | --- | +| **模型模态全** | 文本 / 图像 / 视频 / 语音(ASR+TTS+realtime) / embedding / rerank / tokenization | 主要是 chat + 少量模态 | +| **协议全** | `protocol="native" \| "openai" \| "anthropic"`,一个 Client 说三种协议 | `openai` 只会说 OpenAI 协议 | +| **provider 全** | 靠协议层,第三方模型(DeepSeek/Kimi/MiniMax…)零成本接入,无需厂商专属 adapter(§3.3) | 只连自家 | + +第 2、3 层是相对两家官方 SDK 最硬的差异:把「模态 × 协议 × provider」用一个 Client 兜住。 + +### 3.1 亮点:多模态 content 格式 + +`{"text":..., "image":...}` 比 OpenAI 的 `{"type":"input_text",...}`、 +Anthropic 的 block 结构更直观(两份对比的「🟢 亮点」一致认可)。 + +--- + +## 4. 轴二:「全链路长任务」——纵向生命周期 + +**注意概念关系**:「全链路」是范围,「长任务」是执行模式,二者不是并列。 + +### 4.1 全链路 = AI 平台的完整生产流程 + +``` +files(上传数据) → fine_tunes(微调) → deployments(部署) → rl(强化学习) → agentstudio(托管 Agent) +``` + +- 不止"调模型推理",而是覆盖训练到上线到 Agent 托管的整条链路。 +- 对应 §4 的战略定位:OpenAI 已于 2026-05 删除 API CLI、Anthropic 从未有过, + "AI 平台长流程管理"品类只剩 DashScope。 + +### 4.2 长任务 = 支撑链路耗时环节的统一异步模式 + +``` +jobs.create → fetch → wait # 统一 Job 对象:id / status / result / error;wait 内置退避轮询 +``` + +覆盖:images / videos / audio.asr 批量 / embeddings 批量 / fine_tunes / rl。 +因为这些操作都耗时、不能同步等,2.0 把它们抽象成同一套模式—— +**长任务是"全链路"里慢操作的底层机制,不是与微调、agentstudio 并列的第三样东西**。 + +--- + +## 5. 协议合并的价值与前提 + +### 5.1 痛点:用户在两协议间切换成本高("协议碎片化税") + +OpenAI 与 Anthropic 协议看着像、实则处处不同,同时用两家要维护两套: + +- 两个 SDK / 两份依赖、两套 Client 初始化; +- 两套异常层级(`except openai.RateLimitError` 与 `except anthropic.RateLimitError` 是两个类); +- 两种流式惯用法(Anthropic `text_stream`/`get_final_message` vs OpenAI 遍历 chunk 自累积); +- 工具定义不同(OpenAI `functions`/`tool_calls` ↔ Anthropic `input_schema`/`tool_use`/`tool_result`); +- 消息结构不同(Anthropic `system` 是独立顶层字段,OpenAI 是 messages 里的 role); +- SSE 事件格式、采样响应形状不同。 + +模型能力可互换,但**接入代码不可互换**——每加一家 provider 就多一套错误模型、流式写法、工具 schema。 + +### 5.2 dashscope 的解法:一套接口,协议翻译收敛进 adapter + +```python +client = dashscope.Client(protocol="openai" | "anthropic" | "native", base_url=...) +resp = client.chat.completions.create(model=..., messages=..., tools=..., stream=True) +``` + +- 一套异常族、一套流式 API、一套 Pydantic 类型、一套重试、一套配置;上层无感。 +- 写一次代码,靠 `protocol=` / `base_url=` 换 provider,碎片化税被消掉。 +- `strict=False` 默认透传未知参数,厂商新参数(reasoning/thinking)不被 SDK 发版节奏卡住。 +- native 协议仍能拿到 dashscope 全量能力,这是纯 openai/anthropic SDK 给不了的。 + +### 5.3 优势成立的三个前提(方案已点) + +1. **最小公分母风险**:统一接口不能丢各家特有能力,否则"通吃"变"通弱"。 + 兜法:`extra_body` 逃生舱 + 高频参数显式化(`enable_thinking`/`reasoning_effort`)(§3.3)。 +2. **协议层只覆盖 HTTP 文本类**:ws 实时接口(Recognition/OmniRealtime)在两家无对应物,统一不了,需文档明示(§7 风险)。 +3. **价值对谁最大**:对"走百炼网关 / 多 provider 混用 / 需要全链路"的用户价值最高; + 纯单一官方场景,用户直接用 `openai` SDK 更原生——卖点不是替代单家官方 SDK。 + +--- + +## 6. CLI:对 toB 的成本 & 是否"只给大模型 function call" + +### 6.1 先纠正定位:CLI 首要不是给大模型用的 + +设计 §4 写得很清楚——`dashscope` CLI 的对标物是 **Codex cloud + aws cli 的混合体**, +**明确不与 Agent 类 CLI 比对话体验**(那是 `dashscope expert` / acli 的赛道)。它的存在理由是 +把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成命令,首要受众是: + +- **人类运维 / 平台管理员**:手动跑长流程、查任务状态; +- **CI/CD 脚本**:`--output json`(NDJSON 可 jq)+ 退出码分级(0 成功 /1 服务端 /2 认证 /3 参数 /4 限流)+ `auth whoami` 退出码探测,脚本免解析 stderr。 + +`chat create` 这类"一条 curl 就能替代"的调用只是便捷入口,不是重点。 + +**真正给大模型/agent 用的是 `dashscope expert`**(agentic CLI),而且 expert 是把 SDK 资源 +**直接注册成 tools/skills**(§5.2),让 agent 一句话调全量能力——**不是 shell out 去调 CLI**。 + +### 6.2 那"能被大模型 function call 调用"对不对?——对,但是副产品 + +CLI 因为 `--output json` + 分级退出码,**天然对 agent 友好**:LLM 通过 bash/function-calling +工具调它,能拿到结构化输出和可靠的成功/失败信号。所以"能被大模型调用"成立, +但这是**设计属性带来的次要受众**,不是 CLI 存在的理由。把 CLI 说成"完全是给大模型 function +call 用"会混淆三条线:CLI(人+CI)、expert(agent 对话)、SDK tools(agent 直接调能力)。 + +### 6.3 对 toB 用户的成本:基本零边际成本 + +| 成本维度 | 情况 | +| --- | --- | +| 安装 | CLI 与 SDK **同包、同版本号发布**(§2.4 规则 13),不做双包分发;装 `dashscope` 就带 CLI | +| 只用 SDK 的 toB | 不碰 CLI = **零成本**,CLI 不强加任何东西 | +| 存量脚本 | 新旧命令树并存,隐藏别名保留(`fine-tunes`→`ft`、`agentic-rl`→`rl`),**不破坏现有自动化** | +| 共存期负担 | 成本主要落在**厂商侧**(维护 `cli/` 单命令树 + 旧命令兼容映射层),不是用户侧 | +| toB 收益 | 长流程(微调/部署/批处理)可脚本化、可进 CI、退出码可靠——这正是 toB 运维需要的 | + +结论:对 toB,CLI 是"已经在包里、用不用随你、用了能把长流程自动化"的低成本工具; +共存期旧命令兼容层的维护成本由 DashScope 自己承担,不外溢给用户。 + +--- + +## 7. 护城河验真(逐点 / 客户友好维度) + +依据 `md/dashscope-2.0-interfaces-examples.md`(实现级示例,反映 `dashscope/v2/` 现状), +对 §3–§6 的每条宣称逐点验真。图例:✅ 真优势 | ⚠️ 打折 / 有硬边界 | ❌ 被夸大。 + +### 7.1 轴一:一个 SDK 管全部 + +**1a 模型模态全 —— ✅ 真优势** +文本 / 多模态 / 图像 / 视频 / asr / tts / realtime / 文本向量 / 多模态向量 / 批量向量 / +rerank / tokenization 全部落地(示例 §1–4)。客户友好成立:动词收敛(`create` / `generate` / +`jobs.*`)、`Usage`/`index`/分页字段全 SDK 统一、响应信封一律扁平化(不写 `.output.`)。 +- ⚠️ 代价:为"广而一致",命名评审花大力气消灭同义词(design §2.6),且仍有裂缝(见 2c)。广度不免费。 + +**1b 协议全「写一次代码换 protocol」—— ⚠️ 被夸大,有硬边界** +示例 §0.3 参数支持矩阵显示同一套代码在三协议下**不等价**: + +| 参数 | native | openai | anthropic | +| --- | --- | --- | --- | +| `seed` / `presence_penalty` / `response_format` | ✅ | ✅ | **❌ 无条件 400** | +| `enable_thinking` / `repetition_penalty` | ✅ | 🔀 透传 | **❌ 无条件 400** | + +- 用了 `enable_thinking` / `response_format`,**切到 anthropic 直接 400**,"一套代码通吃"只在 happy path 成立。 +- `max_tokens` 在 anthropic 缺省被**自动补 4096**(§0.2)——隐藏行为。 +- 辩证:anthropic 选"响亮 400"而非"静默丢弃"(§0.3 注释:假成功更糟),**设计取向是对的、友好的**; + 但它同时坐实"协议通吃"是**有漏的抽象**——客户仍须知道"哪个参数在哪个协议活着",换 provider 非零成本。护城河比宣称的浅。 + +**1c provider 全 —— ⚠️ 部分成立** +第三方模型靠协议层接入(§3.3),但受 1b 同一约束:厂商特有参数(reasoning/thinking)只能走 +`extra_body` 逃生舱,**非一等公民**;`models.list(provider=)` 是**客户端匹配**(§5.3),非服务端能力。能用,但不是"零成本通吃"。 + +### 7.2 轴二:全链路长任务 + +**2a 长任务 `jobs.create/fetch/wait` —— ✅ 真护城河(最硬一条)** +统一 `Job` 对象 + 类型化视图(`image_urls`/`video_url`/`result_url`/`results`,示例 §2.2)+ +一步式 `generate` vs 分步 `jobs.*` 双形态 + 内置退避轮询 + 状态枚举 + FAILED 抛异常。 +OpenAI/Anthropic **无此概念**。客户友好完全成立:省掉自写轮询/状态机/累积的全部样板。 + +**2b 全链路覆盖 —— ✅ 覆盖真实存在** +files → fine_tunes → deployments → rl → agentstudio 五环都在(示例 §5/§6/§7),非 PPT。 + +**2c 全链路「统一体验」—— ❌ 末梢断裂** +"全链路"是**覆盖意义上的真、体验意义上的假**: +- **agentstudio(最后一环)是二等公民**(示例 §7):返回 1.x 模型、**不扁平化**(仍 `output` 信封)、 + `except DashScopeError` **抓不到**,只能 `except Exception`。客户学会 v2 的信封+错误规则后到 agentstudio 全部失效—— + **严重不一致**。design §2.7 / M3-3.12 自承"还没做翻译,流式路径尤其漏网"。 +- **rl(倒数第二环)半 solid**(示例 §6):错误已翻成 `SdkError`(补救到位),但 4 个直通方法 + **不接受 `timeout=`**,违反"所有方法都支持 timeout"的承诺。 +- 结论:护城河在 `files→ft→deploy` 这段最实,`rl` 打折,`agentstudio` 是缺口。 + +### 7.3 横切:一个反向摩擦点(削弱"易用性追平") + +**`workspace` 现在是强制的**(示例 §0 + 代码 `dashscope/v2/_config.py`):默认端点全走 +workspace 专属网关 `https://{workspace}.cn-beijing.maas.aliyuncs.com`,共享网关已下线, +没给 `workspace` 又没 `base_url` 直接 `ValueError`。 + +**代码级事实(`_config.py`)**: +- `resolve_config`(line 415-424):无显式 `base_url` 时调 `_protocol_default_base_url("native", workspace)`; +- 该函数 `if not workspace: raise ValueError`(line 120-121),**无任何回退分支**; +- 老共享网关 `dashscope.aliyuncs.com` 在 v2 **彻底不引用**(docstring line 11-12 / 注释 line 36-38)。 + +**是否自动退回 1.x 老网关?——不会,且不该**: +- 行为是「无 base_url + 无 workspace → 立刻 `ValueError`」,不是静默回退老网关。 +- 静默退回另一个网关会**掩盖配置错误**(用户以为用新网关、实际打到别处),比响亮报错更糟。 + 当前 `_WORKSPACE_REQUIRED_MSG` 给了 3 种修法 + `base_url` 逃生口,fail-loud 设计是对的。 +- ⚠️ **已核实(官方文档纠正代码注释)**:`_config.py` docstring/注释多处称老网关 "has been retired", + 但官方 [Model Studio 文档](https://docs.modelstudio.console.alibabacloud.com/zh/model-studio/qwen-api-via-dashscope) + 明确:**老共享网关仍可正常使用、无下线时间表**,官方只是"推荐迁移到新网关(性能更卓越、稳定性更高)"。 + 故准确表述应是"v2 默认不再引用老网关",而非"retired"——**这是会误导维护者的注释 bug**。 + 连带结论:① 1.x 共存期矛盾解除(老网关没停,1.x 打 `dashscope.aliyuncs.com` 仍可"逐字节不变"); + ② **workspace 强制是 SDK 自我设限、非服务端逼迫**——老网关无需 workspace 且官方仍支持,v2 本可回退而非抛错。 + +**新老网关官方对照(来自上述文档)**: + +| 地域 | 新网关(需 workspace id) | 老共享网关(无需 workspace) | +| --- | --- | --- | +| 北京 | `{ws}.cn-beijing.maas.aliyuncs.com` | `dashscope.aliyuncs.com` | +| 新加坡 | `{ws}.ap-southeast-1.maas.aliyuncs.com` | `dashscope-intl.aliyuncs.com` | +| 香港 | `{ws}.cn-hongkong.maas.aliyuncs.com` | `cn-hongkong.dashscope.aliyuncs.com` | +| 美东/德/日 | `{ws}.us-east-1 / eu-central-1 / ap-northeast-1.maas.aliyuncs.com` | — | + +官方建议:推荐迁移新网关("卓越的性能和更高的稳定性");老域名仍可用,**无明确下线时间表**。 + +**对"是否该支持老网关"的修正建议(分层,替代原"不该静默回退")**: +- 给了 `workspace` → 走新网关(官方推荐正解,默认路径); +- 没给 `workspace` 也没 `base_url` → **回退老共享网关 `dashscope.aliyuncs.com` + 一条 info/DeprecationWarning** + ("建议配 workspace 以获得更优性能"),**而非直接 `ValueError`**。这样"两行 quickstart"就真能像 OpenAI 只靠 api_key 跑, + 友好度痛点消除,同时用 warning 把用户往新网关引导,不违背官方迁移导向。代价仅是老网关性能略差,且是用户自选。 + +**附带发现:地域硬编码**。v2 把网关写死 `cn-beijing`(`WORKSPACE_GATEWAY_TEMPLATE`),但官方有 **6 个地域**; +新加坡/美东/欧洲/日本用户**必须手拼 `base_url`**,无 `region=` 参数——又一处友好度缺口。 + +**客户友好度:确实是痛点(不是追平)**: + +| 对比 | OpenAI/Anthropic | dashscope 2.0 | +| --- | --- | --- | +| 上手所需 | `api_key` | `api_key` **+ workspace** | +| 缺配置时 | 能跑 | `resolve_config` 构造期直接 `ValueError` | +| "两行 quickstart" | 真两行 | `dashscope.chat(...)` 未设 `DASHSCOPE_WORKSPACE` 即抛错 | + +- workspace-id 是要额外去控制台找的概念,且报错发生在**构造期**而非首次请求,首屏直接劝退; + design §2.5 "对齐 OpenAI 5 行上手"被这一关打折。 +- 叠加易用性唯一明显未追平的 tool 自动循环(无 `@tool` / `tool_runner`),§2.1 的"基线齐平"应下调为 + **"接近齐平,有两处摩擦(workspace 强制 + 无 tool 自动循环)"**。 + +**可改善方向(都不破坏 fail-loud)**: +1. **单 key 自动探测 workspace**:一个 key 只对应一个 workspace 时,SDK 调"列 workspace"接口自动填,用户无感——对友好度提升最大。 +2. **报错延后到首次请求**:构造 Client 不炸,真正发请求才报错,import/构造阶段更宽容。 +3. **Quickstart 文档诚实化**:首屏就写 `DASHSCOPE_WORKSPACE`,别宣传会抛错的"两行示例"。 +4. 老网关若支持,只做**显式 opt-in**(`base_url=`),绝不静默回退。 + +### 7.4 验真汇总表 + +| 点 | 宣称 | 验真 | 客户友好维度 | +| --- | --- | --- | --- | +| 模态全 | 全覆盖 | ✅ 真 | 一致性好,靠大量命名治理换来 | +| 协议通吃 | 写一次换 protocol | ⚠️ 打折 | happy path 友好;协议特有参数切不动,漏抽象 | +| provider 通吃 | 第三方零成本 | ⚠️ 打折 | 靠逃生舱,非一等公民 | +| **长任务 jobs** | 统一异步模式 | ✅ **真护城河** | 最省心,两家没有 | +| 全链路覆盖 | 五环齐全 | ✅ 真 | — | +| 全链路统一体验 | 一致 | ❌ 末梢断裂 | agentstudio 破坏一致性,rl 半折 | +| 两行 quickstart | 对齐 OpenAI | ⚠️ 打折 | workspace 强制 = 上手摩擦 | +| 错误可定位 | request_id 全链路 | ✅ 真优势 | 见 §2.2,工单级定位,对齐 Anthropic | + +**一句话**:真正扛得住的护城河 = **长任务统一模式(2a)+ 错误可定位性(§2.2)+ files→ft→deploy 链路**; +"协议通吃""全链路统一""两行上手"三个卖点都有实现级硬边界,不能照单全收。 + +--- + +## 8. 战略发散:从 SDK 到 toB 研发套件 & AI coding 下的护城河重估 + +> 本节是发散性战略判断,非现状描述。触发问题:① SDK 能否升级为 toB 研发套件、突破"仅 HTTP 包装"? +> ② AI coding 下调用与代码生成都变容易,SDK 还剩哪些护城河,是否大模型就搞定了? + +### 8.1 价值阶梯:SDK → 套件 = 往上爬 + +"HTTP 包装"只是阶梯最底层。变成套件 = 从"封装一次调用"升级到"拥有调用周围的生命周期与运维"。 + +| 层 | 内容 | 护城河强度 | +| --- | --- | --- | +| L0 HTTP 包装 | requests → typed call | ❌ 已被 LLM 免费化 | +| L1 正确性契约 | 错误目录 / 重试 / 幂等 / request_id | ⚠️ 半成品(§2.2 已建) | +| L2 工作流编排 | jobs 轮询 / 批量 / 状态机 | ✅ "全链路"轴(§7.2a 已验真) | +| L3 本地研发工具 | CLI / config / auth / **录制回放** / mock 网关 / fixture | ✅ toB CI 刚需 | +| L4 可观测 & 运维 | OTel trace / 成本归因 / 配额 / 审计 | ✅ 企业刚需,LLM 生不出 | +| L5 质量闭环 | prompt 版本化 / 回归 eval / 护栏 / 模型迁移安全 | ✅ 高粘性 | +| L6 集成面 | 框架适配(LangChain/Dify/Spring) / **MCP server** / Terraform | ✅ AI 时代入口 | +| L7 平台运行时 | 托管 agent / session / memory | ✅ 但重 | + +**结论:toB 愿付费的是 L3–L6,不是 L0。** + +### 8.2 可打造的 toB 开发者工具(L3–L6) + +1. **录制回放 / mock 网关**(`dashscope dev record/replay`):CI 跑集成测试不烧 quota、不被限流抖动卡住; + 录真实流量 + 确定性回放,是运行时服务,LLM 生成不出来。 +2. **模型路由器 / 可靠性中间件**:跨模型/provider/地域的 fallback、熔断、负载均衡、限流感知—— + 把 §3 的"多协议能切"升级成"自动容灾",企业生产真痛点。 +3. **成本与可观测中间件**:OTel 一等公民 + 按团队/功能的 token 成本归因 + 配额预算强制(FinOps)。 +4. **MCP server**:把 SDK 资源暴露成 MCP tools,让 IDE/agent 原生调 dashscope——AI coding 时代的"新 README"。 +5. **IaC(Terraform provider)**:finetune/deployment 声明式管理。 +6. **迁移 codemod**:1.x→2.0、OpenAI→dashscope 自动改码,直接降切换成本,是获客武器。 + +> 设计里已有种子(codegen M6 / `dashscope serve` 入站兼容 / expert 注册 tools / CLI §4), +> 应把它们从零散 feature 提升为连贯的"toB 开发者平台"叙事,并补上最缺的 **L4/L5(运维与质量)**。 + +### 8.3 AI coding 下,护城河重估 + +**LLM 确实 commoditize 掉的**:把文档变成一次能跑的调用(L0)、生成解析/重试样板(L1 ergonomic)。 +一次性原型里 dev 把文档贴给 agent 就出代码,SDK 的"上手友好"价值被削弱。 + +**LLM 搞定不了、护城河仍在的**: + +1. **运行时 ≠ 代码生成**:SDK 是被维护、测试过、带版本的**运行时依赖**(连接池 / websocket 生命周期 / + 流式背压 / 后台轮询 / 重试状态机);LLM 产物是一次性、无版本、无测试的。生产不接受"大模型写的"。 +2. **正确性与信任**:LLM 幻觉参数/端点/错误码;目录驱动错误模型、"HTTP 200 但业务失败"双层检查、 + request_id 贯通——正是 LLM 最易写错处。SDK 是对真实服务端验证过的 ground truth。 +3. **AI coding 反而抬高好 SDK 的价值**(关键反转):对 agent 最友好的不是裸 HTTP,而是**强类型 SDK**—— + Pydantic + `.pyi` 是 agent 防幻觉的机器可读契约。裸调用 agent 更易错。 +4. **有状态 / 长任务生不出**:jobs 轮询、ws 会话、批量编排——codegen 产调用点,产不出持久机器 + (断点续传尚待建,§8.6)。 +5. **运维 / 治理横切面**:成本归因 + tracing + 配额 + 审计 + 多 provider 容灾,LLM 片段接不好更不会维护。 +6. **版本与迁移契约**:服务端 API 变,SDK 在稳定面后吸收 + 出 codemod;LLM 裸调用静默失效。 +7. **分发与供应链**:pip / semver / 安全补丁 / CVE 响应 / 签名发布——LLM 不能当依赖管理。 + +**最锋利的重构**:AI coding 时代,**SDK 的"用户"越来越多是 agent,不是人**。"开发者友好"部分变成 +"**agent 友好**":富类型、MCP server、确定性错误、幂等、自描述 spec(`llms.txt` / OpenAPI)。 +护城河从"transport 好不好用"转移到"**我是不是那个最可信、最机器可读的契约 + 能可靠执行它的运行时**"。 + +### 8.4 对本报告结论的冲击 + +§7 把"易用性追平 OpenAI"当卖点——**在 AI coding 语境下这条贬值最快**(LLM 抹平 ergonomic 差距)。 +真正抗 LLM 侵蚀的是:**L2 长任务运行时(§7.2a 已验真)、L1 正确性契约(§2.2 错误可定位)、多协议契约、 +以及尚未建的 L4/L5 运维质量层**。 + +> 投入建议:从"更漂亮的 HTTP 包装"转向"**最 agent-legible 的契约 + LLM 生不出来的运行时与治理**"。 + +### 8.5 架构取向:核心做薄做硬,能力做厚做可拆 + +"做套件"不等于"把 SDK 做厚"。这里有一条容易走偏的分界: + +- **做胖客户端(要避免)**:把录制、mock、路由、成本统计、本地运行全塞进 `Client` 对象, + 变成谁都得装、多数场景用不上的巨石。代价是耦合、版本漂移、维护面爆炸——且在 AI coding 下, + 纯调用层本就最先被 LLM 商品化,把重量加在最易贬值处是反的。 +- **薄核 + 外挂(推荐)**:`Client` 仍是一份干净**契约**(typed request/response、确定性错误、 + request_id 贯通),§8.2 的厚度全部挂在它周围、按需 opt-in(mock 网关 / 录制回放 / 成本与可靠性 + 中间件 / 本地 `serve` / 迁移 codemod / MCP server),彼此可拆、可独立版本化。 + +一句话:**核心做薄做硬,能力做厚做可拆。** 因此"升级为套件"更准的定义不是"护理开发环境的胖对象", +而是**从"API 包装"变成"运行时 + 契约 + 治理"**——且厚度必须落在 §8.3 列的、**LLM 生成不出来的地方** +(运行时行为、正确性与信任、治理/合规、有状态长任务),而非落在大模型随手能写的漂亮封装上。 + +### 8.6 用户四维度:好用 / 经济 / 效果 / 稳定 + +toB 客户评估"模型平台 / SDK"的真实打分表是这四维。逐维看"用户要什么 → SDK 能做厚到哪 +(LLM 生不出的部分)→ 代价",结论收敛到 §8.5 的薄核+外挂。 + +#### 经济:从"能调"到"知道钱花哪了 + 有办法省" + +用户要的不是便宜,是**可归因 + 可优化**。SDK 是每一次调用的必经点,`usage` 天然可见—— +这是成本归因唯一正确的插桩点,LLM 生成调用代码却看不到运行时的账。 + +**一次调用的 token 去向可拆为**:system prompt(**每轮重发**)、对话 history(**越聊越贵**)、 +RAG/context 注入、用户当前 message、**tool/function schema(每次调用都重发)**、多模态输入折算、 +输出侧可见 `content` vs **`reasoning_content`(思考 token,thinking 模型下常是答案数倍且照计费)**、 +**重试消耗的重复 token**、流式中断仍计费部分。 + +> ⚠️ 工程真相:服务端 `usage` 给**总数 + 有限明细**(reasoning/cached token 计数,v2 已透传保留, +> 输出侧 thinking 占比可直接读明细算)。要拆到 system/history/tool 的**输入侧**分段, +> 仍需 SDK **发请求前对 payload 分段各自 tokenize 估算**(客户端侧计数),或推动服务端返回明细。 + +**更经济的办法(SDK 可落地甚至可强制)**:① context cache 命中率 & 节省;② history 裁剪/摘要中间件; +③ 模型级联路由(turbo 兜底、max 攻坚,即 §8.2 路由器);④ batch 离线折扣;⑤ thinking 开关护栏; +⑥ tool schema 精简/按需注入;⑦ 语义去重缓存。 + +**产品形态**:`dashscope dev cost` 或 OTel cost span—— +> "本次运行 X token ≈ ¥Y:重复 system prompt 占 55%,reasoning_content 占 30%,真正答案只占 15%。" + +这种洞察 LLM 生不出(要运行时插桩 + 真实计价)。**代价**:计价表会变(→ SDK 只吐计数,计价可配置/远程拉取)、 +热路径插桩加延迟(→ 采样)、客户端分段 tokenize 是估算非精确。 + +#### 稳定:SDK 最原生、最该赢的维度 + +用户要"别抖、别丢、超时可控、失败可预期、长任务不断"。**已建**:连接池、ws 生命周期、流式背压、 +`submit_timeout` vs `wait` 预算分离、`max_retries` 退避(429 读 Retry-After)、目录化确定性错误。 +**可做厚(LLM 生不出)**:可靠性中间件——跨模型/地域 fallback、熔断、按模型/地域维度的 429 感知调度、 +写操作幂等键(client 生成 idempotency key,重试不重复创建)、长任务断点续传, +把 §3"多协议能切"升级成"自动容灾"。**代价**:部分企业要把容灾放网关/mesh → 按 §8.5 做 opt-in 外挂。 + +#### 效果:窄切、别越界 + +用户要"输出对、参数真生效、结构化输出可靠、换模型能验证变好没"。**高杠杆窄切**: +① **参数正确透传**——示例 §0.3 矩阵显示协议特有参数的两种失效形态:`enable_thinking/repetition_penalty` +在 openai 只是透传(可能不生效,静默)、`response_format/seed` 等五个参数在 anthropic 直接 400(响亮) +——薄包装错映射即效果事故;② **结构化输出可靠性**——`response_format` 解析 + schema 校验 + +不合格自动重试;③ **eval 回归(L5)**——prompt 版本化 + 对**录制 fixture** 跑离线 eval + 跨模型 A/B。 +**代价**:效果主要是模型的活,SDK 越界做全套 eval/agent 编排 = 大工程且撞 LangChain → **只取这三片**。 + +#### 好用:AI coding 下贬值最快的一维 + +呼应 §8.4:ergonomic"好用"是 LLM 最先抹平的。需**重定义"好用给谁"**——给**人**(quickstart、路径浅)贬值中, +别过度投入;给 **agent**(强类型 `.pyi`、MCP、确定性错误、幂等)升值中;给**运维**(config/auth/observability 一致)刚需。 + +#### 四维互相打架,SDK 是把权衡"显式可调"的地方 + +- 经济 ↔ 效果:便宜模型 / 裁 context / 关 thinking ↔ 质量 +- 经济 ↔ 稳定:重试和 fallback 都烧 token +- 好用 ↔ 稳定:裸透传省事但脆,强类型+校验要配置 + +把权衡做成**策略旋钮**(路由器/中间件:"质量下限之上最省" / "预算之内最稳")才是"套件"的产品化形态, +也是真护城河:**LLM 能生成一次调用,但生成不出带策略、带账、带容灾的运行时**。四维中 +**经济/稳定/效果抗 LLM 侵蚀**(要运行时、真实计价、插桩),**好用被侵蚀**——再次印证 §8.4 的投入建议。 + +### 8.7 四维潜力排序:经济最具"护城河 + 生态"潜力 + +结论:**经济 #1(唯一带正反馈、能长生态),稳定是紧随其后的地基,效果只能窄切,好用出局。** +真正能长成生态的**产品形态**是"经济 + 稳定"融合出的**进程内控制面(计量 / 路由 / 护栏)**。 + +用四条判据过筛,经济全中,其余各有硬伤: + +| 判据 | 经济 | 稳定 | 效果 | 好用 | +| --- | --- | --- | --- | --- | +| 抗 LLM 侵蚀 | ✅ 运行时插桩 + 真实计价 | ✅ 运行时依赖 | ⚠️ 主要是模型的活 | ❌ 贬值最快 | +| 数据会复利 | ✅✅ 每次调用沉淀花费数据 | ⚠️ 有指标但不积累壁垒 | ⚠️ eval 数据但窄 | ❌ | +| 治理级锁定 | ✅ 接进预算/FinOps,拆掉很痛 | ⚠️ 属"信任底线"非差异化 | ❌ | ❌ | +| 能长生态 | ✅ 可做多 provider 计量/路由入口 | ❌ 偏基础设施,难托管第三方 | ❌ | ❌ | + +**经济的关键是"复利 + 升级阶梯"**(四维里唯一带正反馈): +> 成本归因(看清花哪了)→ 模型路由(自动选便宜的)→ FinOps 治理(预算/配额/护栏强制)→ **多 provider 控制面** + +稳定虽最 SDK 原生,但是**防守性的"信任底线"**(做到不被夸、没做到才挨骂),难独立长成生态; +效果受制于模型本身;好用已被 AI coding 商品化。 + +**生态形态——经济与稳定在此合体**:单看"成本报表"只是 feature,让它变生态的是把 +经济(计量/路由)+ 稳定(fallback/熔断)融进同一个**进程内控制面**: + +- **零插桩、调用点 ground-truth**——比 Langfuse / Helicone / Datadog 那类**代理式**可观测更准 + (不多一跳、直接看真实 payload),这是 SDK 独有的位置优势。 +- 一旦成为"流量必经的计量+路由+策略执行点",SDK **不再是 client 库,而是别的模型/provider 都要穿过的平面**—— + 生态引力由此产生:多模型接入点 / FinOps 看板数据源 / 预算护栏执行点 / "推荐哪个模型"的市场入口。 + 这正是 §8.5 那层最厚的外挂、§8.6"策略旋钮"的产品化终局。 + +**诚实的张力(会封顶生态野心)**: +1. **正面撞 Langfuse / Helicone / LangSmith**(已是现成的 LLM 可观测/成本生态);SDK 差异化只能是"进程内、零插桩、ground-truth"。 +2. **provider 中立 vs 阿里商业利益的矛盾**:生态引力最强的是**中立**版(连去别家的流量也归你计量/路由), + 但百炼大概率不愿让自家 SDK 把流量导向竞品 → 会被限定在"百炼域内",**生态天花板被商业策略压住**。 + 这是"要不要做中立控制面"的战略抉择,非工程问题。 + +**一句话**:押经济、产品化为"经济+稳定"的进程内控制面;稳定是必须先赢的地基但非终局, +效果做窄切(参数送对 + 结构化兜底 + eval),好用只需对 agent 友好。最大变数不是技术,是**要不要 provider 中立**。 + +--- + +## 9. 一句话对齐 + +- **管全部** = 一个 Client 覆盖(模态 × 协议 × provider)—— 横向广度。 +- **全链路** = files→微调→部署→RL→agentstudio 的生产流程;**长任务(jobs.create/fetch/wait)是这条链路上耗时环节的统一执行范式** —— 纵向深度。 +- **易用性/可定位性** = 从代际落后追到基线齐平(仅 tool 自动循环仍差 Anthropic 一点)。 +- **CLI** = 给"人 + CI"管长流程,对 toB 零边际成本;agent 对话走 expert,CLI 只是顺带对 agent 友好。 + +--- + +## 10. 待探究清单(后续继续) + +- [ ] **L4/L5 运维质量层立项**(§8.2):OTel/成本归因、录制回放、eval 闭环——抗 LLM 侵蚀的核心,优先级最高。 +- [ ] **token 成本归因报表**(§8.6 经济):输入侧 system/history/tool 分段 tokenize(输出侧 + content/reasoning 直接读 usage 明细),出 `dashscope dev cost`;计价表可配置/远程拉取, + SDK 只吐计数。最具体、最易先落地的经济能力。 +- [ ] **策略旋钮中间件**(§8.6):把经济↔效果↔稳定的权衡做成"质量下限之上最省 / 预算之内最稳"的可调策略。 +- [ ] **进程内控制面立项 + provider 中立抉择**(§8.7):经济+稳定融合的计量/路由/护栏平面是最具生态潜力项; + 须先决"是否 provider 中立"——中立则生态天花板高但违百炼商业利益,域内则反之。战略级抉择,非工程问题。 +- [ ] **MCP server / agent-legible 契约**(§8.3):把 SDK 资源做成 MCP tools + `llms.txt`,让 agent 原生消费。 +- [ ] **模型路由器 / 可靠性中间件**(§8.2):多协议从"能切"升级到"自动容灾"。 +- [ ] 最小公分母的具体边界:哪些 provider 特有能力会被统一接口磨平?逃生舱覆盖率够不够? +- [ ] **协议特有参数的切换体验**:anthropic 无条件 400 拒绝 5 个参数(§7.1b),是否需要 + "跨协议安全子集"提示 / 自动降级 / 文档对照表,降低漏抽象的客户成本? +- [ ] **agentstudio 对齐 v2**(§7.2c):信封扁平化 + 异常翻译(含流式路径),何时补齐?这是全链路体验的最大缺口。 +- [ ] **rl 的 `timeout=` 一致性**(§7.2c):4 个直通方法能否统一接受 timeout,兑现"全方法支持 timeout"承诺? +- [ ] **workspace 强制的上手摩擦**(§7.3):是否提供"无 workspace 时回退老网关 + warning"或单 key 自动探测 workspace? +- [ ] **修正 `_config.py` 注释 bug**(§7.3):老网关官方未下线,"retired" 表述错误,应改为"v2 默认不引用"。 +- [ ] **地域硬编码**(§7.3):v2 写死 `cn-beijing`,官方有 6 地域;是否加 `region=` 参数,免国际用户手拼 base_url? +- [ ] `tool_runner` / `@tool` 自动工具循环要不要做?(当前相对 Anthropic 的明显差距) +- [ ] codegen(M6)落地后,协议 adapter 是否也能从 spec 生成,减少手写漂移? +- [ ] 入站兼容 `dashscope serve`(让存量 OpenAI SDK 用户零改造用 dashscope)是否立项? +- [ ] 长任务 `Job` 对象在跨模态(图像/视频/RL)下 `output` 类型化视图的一致性核查。 +- [ ] 竞品(MiniMax/Baseten 等)是否也在做"多协议合并",差异化窗口还有多久? +- [ ] CLI 与 expert 的能力边界:哪些长流程操作应在 CLI、哪些应进 expert tools,避免重复。 + +--- + +## 参考 + +- `md/dashscope-2.0-design.md`(§0 原则 / §2.2 资源树·日志 / §2.7 错误模型 / §3 协议兼容层 / §4 CLI 定位 / §5 expert / §7 风险) +- `md/dashscope-2.0-interfaces-mapping.md`(接口映射核对表) +- `md/sdk-comparison-openai-vs-dashscope.md` +- `md/sdk-comparison-anthropic-vs-dashscope.md` +- `md/sdk-deep-comparison-openai-anthropic-dashscope.md`(更细粒度对比,本报告未逐条展开) diff --git a/md/dashscope-2.0-agentstudio-agenticrl-migration.md b/md/dashscope-2.0-agentstudio-agenticrl-migration.md new file mode 100644 index 00000000..aebaed3f --- /dev/null +++ b/md/dashscope-2.0-agentstudio-agenticrl-migration.md @@ -0,0 +1,140 @@ +# AgentStudio / AgenticRL 迁移 v2 与统一 2.0 错误体系 + +> 状态:设计稿(v2,已按评审意见修订)。 +> 约束:`dashscope-error-registry` 仓库本次不动;catalog 以 v2 vendored 副本 +> (`dashscope/v2/_error_registry.py`,上游 1.0.11@cd873b6)为准。 +> +> 已确认的方向: +> 1. agentstudio / agentic_rl 实现归 v2,**1.x 路径保留反向引用 shim,不删除**。 +> 2. reinforcement 的 1.x 路径由 `dashscope/finetune/reinforcement/` 调整为 +> **`dashscope/reinforcement/`**。 +> 3. v2 目录下全部遵循 `dashscope.v2.errors`;agentstudio 与 +> agentic_rl(reinforcement)同样遵循新错误体系。 + +## 1. 背景与现状 + +两个子系统的实现都已经在 v2 下,1.x 路径是反向引用的 shim;但**错误体系仍是 1.x 风格**,靠 mount 边界的翻译层把子系统异常转成 v2 错误。本次的核心工作就是消除这两套独立错误体系和翻译层。 + +### 1.1 AgentStudio + +| 项 | 现状 | +|---|---| +| 实现 | `dashscope/v2/agentstudio/`(client / transport / resources / streaming / pagination) | +| 1.x 路径 | `dashscope/agentstudio/__init__.py`:纯 shim,re-export v2(**保留**) | +| 错误体系 | `dashscope/v2/agentstudio/exceptions.py`:独立层级,基于 `dashscope._shared._errors.DashScopeException`,与 `dashscope.v2.errors` **无血缘** | +| 翻译层 | `dashscope/v2/resources/_agentstudio.py`:透明代理。HTTP 层(`APIStatusError` 子类)走 `error_from_status`;本地/流错误(APIConnectionError / APITimeoutError / StreamError / StreamClosedError)走 `error_from_sdk_exception(family="sdk.agentstudio")` | +| 网关错误码 | anthropic 风格字符串(`invalid_request_error` / `authentication_error` / `overloaded_error` …),`exceptions.from_response` 里手维护 `_CODE_TO_CLASS` / `_STATUS_TO_DEFAULT` 两张表 | + +### 1.2 AgenticRL + +| 项 | 现状 | +|---|---| +| 实现 | `dashscope/v2/finetune/agentic_rl.py`(`Client(...).rl` 所用);`submit_job` 返回 `SubmitResult` | +| 1.x 路径 | `dashscope/finetune/agentic_rl.py`:v2 子类 + 5 个 1.x 动词(`submit_job`/`cancel`/`list`/`get`/`delete`/`logs`),委托 1.x 根 API `FineTunes`,返回 1.x `FineTune` dataclass(**保留**) | +| reinforcement 包 | `dashscope/finetune/reinforcement/`:`sys.meta_path` finder 按需别名到 v2,**同一 module 对象**。**本次移动到 `dashscope/reinforcement/`** | +| 错误体系 | `common/errors.py`:独立层级。`AgenticRLError` 带**数字** `error_code`(1000/1800…)+ timestamp + `_RootCauseMixin` root-cause 格式化;另有 `RuntimeErrorWithCode` / `ValueErrorWithCode` 混入内置类型 | +| 翻译层 | `resources/reinforcement_learning.py` 的 `_translate_rl_errors` → `error_from_sdk_exception(family="sdk.agentic_rl")` | +| catalog | 27 个 `sdk.agentic_rl.*` 条目:18 个类名精确命中、6 个 condition-only、1 个家族 fallback(`sdk.agentic_rl.Error`) | + +v2 侧 `client.rl.jobs` 已原生覆盖任务全生命周期(create/fetch/wait/list/cancel/delete/logs)。 + +## 2. 目标态 + +1. **单一实现**:实现只在 v2;1.x 路径保留反向引用 shim(agentstudio / agentic_rl / reinforcement 三者都是),shim 不含第二份实现。 +2. **单一错误体系**:两个子系统原生抛 `dashscope.v2.errors`;删除两个 mount 翻译层;错误码事实源只剩 catalog。 +3. **reinforcement 新 1.x 路径**:`dashscope.reinforcement`(反向引用 `dashscope.v2.finetune.reinforcement`)。 +4. 旧路径 import 到的错误类就是新的 v2 错误类(同一对象),保证 `except` / `isinstance` 行为一致。 + +## 3. AgentStudio 迁移方案 + +### 3.1 异常类映射 + +| 旧(agentstudio.exceptions) | 新(dashscope.v2.errors) | 说明 | +|---|---|---| +| `AgentStudioError` | `DashScopeError` | 基类 | +| `APIConnectionError` | `APIConnectionError` | catalog `sdk.ConnectionError` 支撑 | +| `APITimeoutError` | `APITimeoutError` | catalog `sdk.Timeout` 支撑 | +| `APIStatusError` | `APIStatusError` | HTTP 层基类 | +| `InvalidRequestError` (400) | `BadRequestError` | **改名** | +| `AuthenticationError` (401) | `AuthenticationError` | 同名 | +| `PermissionDeniedError` (403) | `PermissionDeniedError` | 同名 | +| `NotFoundError` (404) | `NotFoundError` | 同名 | +| `ConflictError` (409) | `APIStatusError`(基类) | catalog 无 409 条目(registry 缺口,本次不动 registry,先落基类) | +| `RateLimitError` (429) | `RateLimitError` | 同名 | +| `OverloadedError` (503) | `ServiceUnavailableError` | **改名** | +| `InternalServerError` (500) | `InternalServerError` | 同名 | +| `StreamError` | 新 `SdkError` 子类 | catalog `sdk.agentstudio.StreamError` 支撑 | +| `StreamClosedError` | 同上(子类) | catalog `sdk.agentstudio.StreamClosedError` 支撑 | + +属性对应:`code/message/request_id/status_code` 两边同名;旧 `raw` → v2 `body`。 + +### 3.2 wire code 交叉表 + +catalog public 条目自带 `anthropic_error_code` 字段,交叉表**从 catalog 派生**,不再手维护。两个对不上的例外显式补: + +- `permission_denied_error`(agentstudio 网关实际值)→ `permission_denied` 条目(catalog 字段值是 `permission_error`) +- `conflict_error` / 409 → 无条目,落 `APIStatusError` 基类 + +`from_response` 的 CMA/旧版 `error_code`/`error_message`/Spring 三种 body 形状的归一化逻辑保留(迁入 agentstudio transport),类选择改为「crosswalk → 状态映射 → 基类」。 + +### 3.3 改动清单 + +- 重写 `dashscope/v2/agentstudio/exceptions.py`:独立层级替换为 `dashscope.v2.errors` 体系;旧类名保留为 **deprecated alias**(指向新类,§6 决策点 3),使 1.x shim re-export 的旧名字不断裂 +- transport / streaming 的 raise 点改抛 v2 错误 +- 删除 `dashscope/v2/resources/_agentstudio.py` 翻译代理;`client.agentstudio` 直接挂载 +- **保留** `dashscope/agentstudio/` shim(re-export v2,含错误类新名字与旧别名) +- 测试:`tests/v2/test_agentstudio_shim.py` / `test_agentstudio_errors.py` 重写(翻译层断言改为原生错误断言);`tests/unit/test_agentstudio_*` 走 1.x shim,继续可用,按需调整异常断言 + +## 4. AgenticRL 迁移方案 + +### 4.1 API 面 + +- `dashscope/finetune/agentic_rl.py` **保留**:1.x 动词(`FineTunes` 委托、`FineTune` 返回)是 1.x 独有面;底层实现继续复用 v2,无第二份逻辑 +- `dashscope/v2/finetune/agentic_rl.py` 里对 `dashscope.api_key` 根全局的现有规避保持不变 + +### 4.2 错误体系统一 + +- `common/errors.py` 重写:异常类改为 `dashscope.v2.errors.SdkError` 子类,`code`/`solution`/`allow_retry` 由 catalog 条目支撑(沿用 `APIConnectionError` 的 `_SDK_ERROR_NAME` 模式);condition-only 条目不设专用类,由 `sdk_error()` 工厂构造 +- **数字 `error_code` 处理**:catalog 用 `sdk.agentic_rl.*` 名称作 code。旧数字码建议保留 property 兼容映射一个版本周期(§6 决策点 2);`{inner_code}` 占位符改由包装时的内部错误名填充 +- `RuntimeErrorWithCode` / `ValueErrorWithCode` 的内置类型混入取消(v2 体系不入内置层级) +- `_RootCauseMixin` 的 root-cause 遍历/格式化评估并入 `SdkError.__str__` 或删除 +- 删除 `_translate_rl_errors` 挂载翻译;工作流(register/upload/submit/run)原生抛 v2 错误 +- 旧类名保留为 deprecated alias(18 个精确命中类名 + `AgenticRLError` 基类名),1.x 路径 `except` 旧类名继续命中同一对象 + +### 4.3 reinforcement 路径迁移:`dashscope/finetune/reinforcement/` → `dashscope/reinforcement/` + +- 新包 `dashscope/reinforcement/`:整体平移现有 meta_path 别名 shim,目标仍为 `dashscope.v2.finetune.reinforcement`(实现位置不变) +- **旧路径 `dashscope/finetune/reinforcement/` 是否保留别名,见 §6 决策点 4**。外部进程按模块名字符串 import 的既有消费方: + - FC sandbox `start.sh` 的 `FC_SERVER_CLASSPATH` + - `examples/workspace/scripts/start_local_*.sh` + - `server.py` 传给 uvicorn 的 dotted path(worker 进程内 import) + 这些脚本若同步改为 `dashscope.reinforcement.*`,需同时更新模板与示例;已部署 sandbox 用其环境内旧版本,不受影响 +- `dashscope/finetune/agentic_rl.py` 内对 `...reinforcement` 的 re-export 改指新路径(保持两个旧名字可 import 的过渡方案由决策点 4 决定) + +## 5. 破坏性变更清单 + +| 变更 | 影响面 | +|---|---| +| agentstudio 异常改名(InvalidRequestError→BadRequestError、OverloadedError→ServiceUnavailableError、ConflictError 取消) | `except` 旧类名的代码(deprecated alias 可缓解) | +| agentstudio 异常属性 `raw` → `body` | 读 `.raw` 的代码 | +| agentstudio 异常基类从 `DashScopeException` 换到 `DashScopeError` | 基类捕获链(1.x 根 `DashScopeException` 捕获将失效,alias 无法解决基类切换) | +| rl 数字 `error_code` 语义切换为 catalog 名 | 按数字判断错误种类的代码(property 映射可缓解) | +| rl `*WithCode` 不再继承内置 RuntimeError/ValueError | `except RuntimeError` 兜底 rl 错误的代码 | +| `AgenticRLError` 基类从 `Exception` 换到 `SdkError` | 基类捕获链 | +| reinforcement 1.x 路径调整为 `dashscope.reinforcement` | 按旧 dotted path 启动的脚本(决策点 4) | + +## 6. 决策点(评审确认) + +1. ~~1.x import 路径是否删除~~ → **已确认:全部保留反向引用 shim** +2. **rl 数字 `error_code`**:直接取消,还是保留 property 兼容映射到 catalog 名? +3. **旧异常类名** deprecated alias 的范围与保留周期(agentstudio 的 InvalidRequestError/OverloadedError、rl 的 18 个类名) +4. **旧 `dashscope.finetune.reinforcement` 路径**:保留到 `dashscope.reinforcement` 的别名(推荐,外部脚本无感),还是直接切换并要求同步改脚本? +5. registry 缺口(409/conflict_error、permission_denied_error 别名):本次落基类 + 显式 crosswalk,后续是否推 registry 补条目? + +## 7. 实施步骤(建议分 PR) + +1. **PR-1 agentstudio 错误统一**:重写 exceptions + transport/streaming raise 点 + crosswalk;删 `_agentstudio.py` 翻译层;v2 测试改写;1.x shim 保持可用 +2. **PR-2 agentic_rl 错误统一**:重写 `common/errors.py`;工作流 raise 点改造;删 `_translate_rl_errors`;registry 完整性测试同步(`test_agentic_rl_reachability_counts` 等) +3. **PR-3 reinforcement 路径迁移**:新增 `dashscope/reinforcement/` shim;按决策点 4 处理旧路径;更新 FC 模板 / examples 脚本 / acli skill 文档中的模块名 + +每步验收:`pytest tests/v2 tests/unit` 全绿 + CLI 冒烟(rl 提交/查询)+ agentstudio 样例冒烟。 diff --git a/md/dashscope-2.0-asr-sdk.md b/md/dashscope-2.0-asr-sdk.md new file mode 100644 index 00000000..cfe01088 --- /dev/null +++ b/md/dashscope-2.0-asr-sdk.md @@ -0,0 +1,339 @@ +# 语音识别(ASR)Python SDK + +通过 `client.audio.asr`,您可以使用 WebSocket 实时识别音频流,或通过 HTTP 提交录音文件转写任务。本文对应当前仓库的 SDK 实现;模型能力、音频限制及服务端字段以相应模型协议为准。 + +| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | +| --- | --- | --- | +| 实时语音识别 | `audio.asr.recognize()` | 不支持 | +| 录音文件转写 | `audio.asr.transcribe()`、`audio.asr.jobs` | 支持相同批量接口,需使用 `await` | + +## 前期准备 + +1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 +2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置。不配置工作空间且未指定端点时,当前 SDK 会回退到共享网关并发出警告。 +3. 在本仓库根目录执行源码安装: + + ```bash + python -m pip install -e . + ``` + +4. 实时示例需要当前目录下的 `input.pcm`:有限长度、无文件头、16 kHz、16 bit、单声道的原始 PCM 音频。文件转写示例需要设置 `AUDIO_FILE_URL`,其值为服务端可访问的音频文件 URL。 + +客户端使用公开入口 `from dashscope import Client, AsyncClient`。也可以在创建客户端时显式传入 `api_key`、`workspace`、`region`;本文示例从环境配置读取凭据,不调用麦克风或下载音频。 + +## 快速开始 + +### 实时语音识别 + +以下代码可保存为 `asr_example.py`,在音频文件所在目录运行 `python asr_example.py`。示例使用 `fun-asr-realtime`,请先确认目标地域可用且已具备访问权限。 + +发送和接收分别运行在两个线程中,逐块读取文件、逐事件处理结果,不在内存中收集整段音频或全部事件。 + +```python +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +from dashscope import Client + + +def send_audio(session, audio_path): + with open(audio_path, "rb") as audio: + while True: + frame = audio.read(3200) + if not frame: + break + session.send_audio_frame(frame) + time.sleep(0.1) + session.stop() + + +def receive_events(session): + for event in session.events(): + payload = event.get("payload") or {} + sentence = (payload.get("output") or {}).get("sentence") or {} + if sentence.get("text") and not sentence.get("heartbeat"): + label = "最终" if sentence.get("sentence_end") else "中间" + print(label, sentence["text"], flush=True) + + +def main(audio_path): + with Client() as client: + session = client.audio.asr.recognize( + model="fun-asr-realtime", + format="pcm", + sample_rate=16000, + timeout=30.0, + ) + with ThreadPoolExecutor(max_workers=2) as executor: + try: + session.start() + print("task_id:", session.session.task_id) + futures = [ + executor.submit(receive_events, session), + executor.submit(send_audio, session, audio_path), + ] + for future in as_completed(futures): + future.result() + finally: + session.close() + print("request_id:", session.request_id) + print("usage:", session.usage) + print("first_package_delay(ms):", session.first_package_delay) + print("last_package_delay(ms):", session.last_package_delay) + + +if __name__ == "__main__": + main("input.pcm") +``` + +- `recognize()` 只创建 `Recognition` 对象;`start()` 才建立连接、发送 `run-task`,并在内部读取和等待 `task-started`。启动成功后才能发送音频。 +- 示例每次发送 3200 字节并暂停 0.1 秒,对应上述 PCM 格式约 100 毫秒的音频。这是模拟实时输入的建议节奏,不是 SDK 强制的分块大小或发送间隔;生产环境需遵守模型的服务端限制。 +- `stop()` 仅结束输入。接收线程继续读取直到 `task-finished`,以获得最后的识别结果和统计信息。 +- `Future.result()` 将工作线程异常传回主线程。`session.close()` 的 `finally` 必须位于线程池上下文内部,确保发送失败时先关闭连接、解除接收阻塞,再等待线程退出。不要只在退出线程池后关闭会话。 + +### 录音文件转写 + +`transcribe()` 提交任务并等待终态,返回 `Job`,不是转写正文。 + +```python +import os + +from dashscope import Client + + +with Client() as client: + job = client.audio.asr.transcribe( + file_urls=[os.environ['AUDIO_FILE_URL']], + model="paraformer-v2", + timeout=86400, + submit_timeout=30, + ) + print("task_id:", job.task_id, "status:", job.status) + for result in job.results: + print("transcription_url:", result.transcription_url) +``` + +请根据 `job.output` 和各文件结果判断转写情况。`transcription_url` 是服务端返回的结果地址,SDK 不自动下载或解析该地址中的转写文件。 + +### 异步录音文件转写 + +`AsyncClient` 的批量转写接口使用相同参数和 `Job` 返回结构;实时 `recognize()` 不提供异步版本。 + +```python +import asyncio +import os + +from dashscope import AsyncClient + + +async def main(): + async with AsyncClient() as client: + job = await client.audio.asr.transcribe( + file_urls=[os.environ['AUDIO_FILE_URL']], + model="paraformer-v2", + ) + for result in job.results: + print(result.transcription_url) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## 请求参数 + +### 实时识别:`client.audio.asr.recognize()` + +以下参数均为关键字参数。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `model` | `str` | 必填,无默认值。实时识别模型名称,例如 `fun-asr-realtime`;可用性取决于地域及服务权限。 | +| `format` | `str` | 可选,默认 `"pcm"`。输入音频格式,必须与实际数据一致;支持范围由模型决定。SDK 不转码。 | +| `sample_rate` | `int` | 可选,默认 `16000`,单位 Hz。输入音频采样率,SDK 不重采样。 | +| `parameters` | `dict` | 可选,默认 `None`。模型参数透传到 `run-task.payload.parameters`;其中同名键会覆盖由 `format`、`sample_rate` 形参生成的值,包括显式传入的值。 | +| `input` | `dict` | 可选,默认 `None`,发送时为 `{}`。任务启动时的初始上下文,透传到 `run-task.payload.input`,结构由模型协议定义。 | +| `phrase_id` | `str` | 可选,默认 `None`。已有热词资源 ID,转换为 `resources=[{"resource_id": phrase_id, "resource_type": "asr_phrase"}]`,不是 `parameters` 中的字段。资源须与模型匹配。 | +| `ws_url` | `str` | 可选,默认 `None`。完整 WebSocket 端点;未指定时采用全局 WebSocket 端点配置,否则按工作空间和地域选择默认端点。 | +| `connector` | `Callable` | 可选,默认 `None`,使用 SDK 默认连接器。自定义连接器按 `connector(url, headers)` 调用,返回兼容 `send`、`recv`、`settimeout`、`close` 的连接对象;自行负责连接建立阶段的超时。 | +| `timeout` | `float` | 可选,默认 `None`,继承客户端超时;客户端未另行配置时为 `300.0` 秒。值必须大于 0;默认连接器将其用于连接建立,并设置连接的读写超时,不是整个识别任务的总时限。 | +| `headers` | `Mapping[str, str]` | 可选,默认 `None`。附加握手请求头。SDK 会根据客户端配置写入认证和工作空间请求头,不应以此替代凭据配置。 | + +`DASHSCOPE_WEBSOCKET_BASE_URL` 可配置 WebSocket 端点,调用级 `ws_url` 优先。HTTP 的 `Client(base_url=...)` 不会自动成为实时识别的 WebSocket 地址。 + +### 文件转写:`client.audio.asr.transcribe()` + +`file_urls` 可按位置或关键字传入,其余均为关键字参数。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `file_urls` | `list[str]` | 必填,无默认值,不能为空。服务端可访问的音频文件 URL 列表,不是本地路径;文件数量、格式及大小限制由模型决定。 | +| `model` | `str` | 可选,默认 `"paraformer-v2"`。录音文件转写模型名称。 | +| `parameters` | `dict` | 可选,默认 `None`。服务端转写参数,原样传递;字段和默认行为以所选模型为准。 | +| `phrase_id` | `str` | 可选,默认 `None`。已有热词资源 ID,与实时接口相同,映射为 `asr_phrase` 类型的 `resources`。 | +| `timeout` | `float` | 可选,默认 `86400.0` 秒,即 24 小时。提交成功后等待任务终态的预算,不是提交请求超时。 | +| `submit_timeout` | `float` | 可选,默认 `None`,继承客户端超时。仅用于提交任务这一次 HTTP 请求,单位秒。 | +| `poll_interval` | `float` | 可选,默认 `1.0` 秒。初始轮询间隔,随后按 2 倍递增;建议使用正数。 | +| `max_interval` | `float` | 可选,默认 `8.0` 秒。后续轮询间隔上限,建议不小于 `poll_interval`。 | + +### 分步任务接口:`client.audio.asr.jobs` + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `jobs.create`:`model` | `str` | 必填关键字参数,无默认值;与 `transcribe()` 的默认模型行为不同。 | +| `jobs.create`:`file_urls` | `list[str]` | 必填关键字参数,无默认值,不能为空。 | +| `jobs.create`:`parameters`、`phrase_id` | `dict`、`str` | 均可选,默认 `None`,含义与 `transcribe()` 相同。 | +| `jobs.create`:`timeout` | `float` | 可选关键字参数,默认 `None`,继承客户端超时;仅为提交请求超时,单位秒。 | +| `jobs.fetch`:`task_id` | `str` | 必填,无默认值。要查询的任务 ID。 | +| `jobs.fetch`:`timeout` | `float` | 可选关键字参数,默认 `None`,继承客户端超时;仅为本次查询请求超时,单位秒。 | +| `jobs.wait`:`job` | `Job` 或 `str` | 必填,无默认值。任务快照或任务 ID。 | +| `jobs.wait`:`timeout` | `float` | 可选关键字参数,默认 `86400.0` 秒。等待终态的预算。 | +| `jobs.wait`:`poll_interval`、`max_interval` | `float` | 可选关键字参数,默认分别为 `1.0`、`8.0` 秒,含义与 `transcribe()` 相同。 | + +等待预算不会替换单次 HTTP 查询超时,也不是可中断在途请求的硬截止时间;查询仍使用客户端超时,因此实际返回时间可能超过等待预算。 + +## 关键接口 + +### 实时会话生命周期 + +`Recognition` 的推荐调用顺序为:创建对象 → `start()` → 并发发送音频和读取事件 → `stop()` → 读到 `task-finished` → `close()`。 + +| 接口 | 说明 | +| --- | --- | +| `start()` | 新建 WebSocket 连接并等待 `task-started`,成功后返回当前 `Recognition`。已打开时再次调用会抛出 `RuntimeError`。 | +| `send_audio_frame(data)` | 将音频作为 WebSocket 二进制帧发送,不是 Base64 或 JSON;通常传入 `bytes`,也接受 `bytearray`、`memoryview`。 | +| `update_context(context)` | `context` 必须是 `dict`。发送 `continue-task`,其 `payload.input` 就是该字典,用于后续音频的上下文,不会重启任务。 | +| `stop()` | 发送 `finish-task` 结束输入,不等待最终结果、不关闭连接;结束输入后不能再发送音频或更新上下文。 | +| `events()` | 阻塞读取并逐条产出原始事件字典,包含最终的 `task-finished`。请由一个接收线程持续消费,不要并发创建多个读取者。 | +| `close()` | 关闭底层连接,可重复调用;不会代替正常的结束输入及读取最终结果流程。 | + +`with client.audio.asr.recognize(...) as session:` 会自动执行 `start()`,退出时执行 `close()`,但不会自动发送音频、调用 `stop()` 或读完事件。使用线程池时仍应采用快速开始中的内部 `finally` 关闭方式,避免错误退出时等待读线程。 + +如模型支持文本上下文,可在启动时传入 `input={"context": {"text": "上一轮内容"}}`,或在会话启动后调用 `session.update_context({"context": {"text": "更新后的内容"}})`。更新应在发送相应后续音频之前完成;具体内容约束以模型协议为准。 + +`close()` 后可对同一个 `Recognition` 再次调用 `start()`。这会创建新连接、新任务并重置本次统计,不是原任务续传,也不复用 WebSocket 连接池。请先等待旧的收发线程退出,再重新启动;`Client.close()` 不替代实时会话的显式关闭。 + +### 提交、查询与等待转写任务 + +- `jobs.create(...)`:提交并返回任务快照,不等待转写完成。 +- `jobs.fetch(task_id, ...)`:查询一次,返回新的任务快照,不持续轮询。 +- `jobs.wait(job_or_task_id, ...)`:轮询直到终态,返回终态快照或抛出异常。 + +```python +import os + +from dashscope import Client + + +with Client() as client: + job = client.audio.asr.jobs.create( + model="paraformer-v2", + file_urls=[os.environ['AUDIO_FILE_URL']], + timeout=30, + ) + print("task_id:", job.task_id) + job = client.audio.asr.jobs.fetch(job.task_id, timeout=30) + job = client.audio.asr.jobs.wait(job, timeout=86400) + print(job.status, job.succeeded) +``` + +异步客户端对应 `await client.audio.asr.jobs.create(...)`、`await client.audio.asr.jobs.fetch(...)` 和 `await client.audio.asr.jobs.wait(...)`。这里的服务端异步任务与 Python 异步调用是两个概念:同步 `Client` 同样可以提交服务端任务。 + +## 返回事件与属性 + +### 实时事件 + +`events()` 返回原始 `dict`,通过 `event["header"]["event"]` 区分事件,而不是通过事件对象的属性访问。 + +| 事件 | SDK 行为 | +| --- | --- | +| `task-started` | 由 `start()` 内部读取,启动返回时保存在 `last_response` 中;不会再次由 `events()` 产出。 | +| `result-generated` | 由 `events()` 产出;识别结果通常位于 `payload.output.sentence`,也可能是心跳或空文本事件。 | +| `task-finished` | 由 `events()` 产出,随后迭代结束;发送 `stop()` 后仍须读取此事件。 | +| `task-failed` | 转换为 SDK 业务异常并关闭连接,不作为普通事件产出。 | + +按模型返回情况,`payload.output.sentence` 中可包含 `text`、`sentence_end`、`heartbeat` 等字段。中间文本可能被后续结果修订,不应简单拼接所有 `text`;心跳和空文本事件仍会产出,应用可按需忽略。完整字段见相关服务端事件文档。 + +### `Recognition` 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `last_response` | `dict` 或 `None` | 最近一次成功读取的原始事件。初始为 `None`;启动后为握手事件,随后在消费 `events()` 时更新。 | +| `usage` | `dict` 或 `None` | 最近一次读取到的非 `None` 的 `payload.usage`;后续事件缺失该字段或值为 `None` 时保留已有值,不自动累计,也不保证只在最后一条事件出现。 | +| `first_package_delay` | `float` 或 `None` | 单位毫秒。从第一次调用 `send_audio_frame()` 到读取首个非心跳且文本非空的识别结果的耗时;尚未满足条件时为 `None`。 | +| `last_package_delay` | `float` 或 `None` | 单位毫秒。从第一次调用 `stop()` 到读取 `task-finished` 的耗时;未调用 `stop()` 或未读到终态时为 `None`。 | +| `request_id` | `str` 或 `None` | 当前记录的请求标识,从已成功读取事件更新;缺少服务端 `request_id` 时可回退到任务 ID。异常排查时优先查看异常携带的请求标识。 | +| `session.task_id` | `str` | 底层会话的任务 ID。须在启动后、关闭前读取;未打开时访问 `Recognition.session` 会抛出 `RuntimeError`。 | + +统计是客户端**实际读取事件时**更新的,不是后台自动刷新;延迟包含网络、线程调度及消费滞后,不能等同于纯服务端推理耗时。`start()` 读取的握手只更新启动响应和请求标识,不通过 `events()` 的用量及延迟统计逻辑。关闭后已保存的统计仍可读取,再次启动会重置。 + +### 文件转写 `Job` + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `request_id` | `str` 或 `None` | 本次提交或查询响应的请求标识,不是任务 ID。 | +| `task_id` | `str` | 服务端任务 ID,可用于后续查询和等待。 | +| `status` | `str` | 任务状态,包括 `PENDING`、`RUNNING`、`SUCCEEDED`、`FAILED`、`CANCELED`。 | +| `output` | `dict` 或 `None` | 原始任务输出,保留服务端结果和每个文件的详情。 | +| `results` | `list[JobResult]` | `output.results` 的结构化视图;没有结果时为空列表。 | +| `results[i].transcription_url` | `str` 或 `None` | 第 i 个结果的转写文件地址;该字段属于结果项,不是 `Job` 顶层字段。 | +| `usage` | `Usage` 或 `None` | 服务端用量对象,保留服务特有字段;不存在时为 `None`。 | +| `finished` | `bool` | 状态为 `SUCCEEDED`、`FAILED` 或 `CANCELED` 时为 `True`,不代表成功。 | +| `succeeded` | `bool` | 仅在状态为 `SUCCEEDED` 时为 `True`。 | + +`Job` 是一次响应的快照,不会自行更新,也不包含 SDK 已下载的转写正文。请接收 `fetch()`、`wait()` 返回的新对象,并逐项检查结果;不要把缺少 `transcription_url` 的条目当作已取得转写内容。 + +## 异常处理 + +| 情况 | 行为与处理建议 | +| --- | --- | +| WebSocket 提前结束 | 底层 `recv()` 在 `task-finished` 前返回 `None` 或空字符串时,SDK 抛出 `APIConnectionError`,不能当作正常识别完成。 | +| 服务端 `task-failed` | SDK 根据业务错误码抛出 `DashScopeError` 或其业务子类,如 `AuthenticationError`、`BadRequestError`、`RateLimitError`;检查异常的 `code`、`message`、`request_id`。 | +| WebSocket 原生错误 | 连接器建立连接、底层 `send()`、`recv()` 抛出的异常通常原样传播,例如 `websocket.WebSocketTimeoutException`、其他 WebSocket 异常或 `OSError`;不会全部自动转换成 `APIConnectionError` 或 `APITimeoutError`。 | +| 协议错误 | 缺少事件头或握手事件不符等情况可能抛出 `WsEventError`(属于 `DashScopeError`);不要继续使用失败的会话。 | +| 本地参数或状态错误 | 空 `file_urls`、非字典上下文、未启动就发送、结束输入后继续发送等,可能分别触发 `ValueError`、`TypeError` 或 `RuntimeError`。 | +| HTTP 或转写业务失败 | 默认 `raise_on_error=True` 时,HTTP 错误、响应业务错误码以及解析到 `FAILED` 任务状态会抛出 SDK 异常;`wait()` 遇到 `CANCELED` 也会抛出 `DashScopeError`。 | +| 等待预算耗尽 | `transcribe()` 或 `jobs.wait()` 抛出 `DashScopeError`;这不表示服务端任务已取消。保留任务 ID 后可再次 `fetch()` 或 `wait()`。 | + +- 在收发线程外读取 `Future.result()` 并确保 `finally` 关闭会话。仅捕获 `DashScopeError` 无法覆盖全部 WebSocket 原生异常;不要静默忽略线程错误。 +- `Client(raise_on_error=False)` 可用于检查 HTTP 返回对象中的 `code`、`message`、`is_error` 和任务状态,但不会关闭 WebSocket 的失败事件异常,也不会抑制本地错误或等待超时。 +- `max_retries` 不提供 WebSocket 自动重连或音频重放。实时任务断开后,由应用决定是否新建任务及重新发送可重放的音频,避免重复结果;不要宣称可以续传原任务。 +- 排查时记录异常类型、请求 ID 和任务 ID;不要记录 API Key 或未经脱敏的完整音频、上下文及转写正文。 + +## 常见问题 + +**为什么 `stop()` 返回了,却没有最终识别结果?** + +`stop()` 只通知服务端输入结束。必须继续消费 `events()` 到 `task-finished`,不要立即关闭连接;握手事件则已由 `start()` 读取,不要额外等待一次 `task-started`。 + +**可以把 WAV 文件直接作为 PCM 发送吗?** + +不能仅修改扩展名。快速开始要求无文件头的原始 PCM;实际格式、采样率和声道必须匹配模型要求。SDK 不负责去除 WAV 文件头、转码或重采样。 + +**为什么用量或延迟仍为 `None`?** + +可能尚未消费包含用量的事件、没有非心跳的有效文本,或还未在 `stop()` 后读到 `task-finished`。这些值不是服务端必定返回的完整统计,不能用空值判定任务成功或失败。 + +**转写返回的是 URL,怎样获取文本?** + +检查任务及各文件结果后,由应用读取 `job.results` 中的 `transcription_url`,再自行下载和解析转写文件;本接口不会自动下载正文。 + +**能用 `AsyncClient` 做实时识别,或在断线后恢复旧任务吗?** + +当前均不支持。`AsyncClient` 仅提供本文的批量转写接口;实时识别使用同步会话并发收发。关闭后重新启动是新任务,不是旧任务恢复;但多条识别可以复用同一条连接,见下节。 + +## 连接复用 + +`recognize(..., connection=conn)` 把识别任务跑在共享的 +`WsConnection`(`client.ws_connection()` 创建)上:顺序任务免再次握手, +多路 JSON 任务可并发;二进制上行每条连接单 owner(一路活跃识别)。 +会话 `close()` 只摘离任务、不关共享连接。完整约束与示例见 +[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 + +## 相关文档 + +- [Fun-ASR 客户端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/fun-asr-client-events) +- [Fun-ASR 服务端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/fun-asr-server-events) +- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) +- [语音合成 Python SDK](dashscope-2.0-tts-sdk.md) +- [实时多模态 Python SDK](dashscope-2.0-realtime-sdk.md) diff --git a/md/dashscope-2.0-audio-management-sdk.md b/md/dashscope-2.0-audio-management-sdk.md new file mode 100644 index 00000000..21e5dc4a --- /dev/null +++ b/md/dashscope-2.0-audio-management-sdk.md @@ -0,0 +1,157 @@ +# DashScope 2.0 Python SDK:语音管理面(词表 / 热词 / 声音复刻 / 声音设计) + +本文覆盖 `client.audio` 的四个管理面子资源,对应 1.x 的三个管理类与官方声音设计 REST API: + +| v2 入口 | 1.x / 官方对应 | 说明 | +| --- | --- | --- | +| `client.audio.vocabularies` | `VocabularyService`(`audio/asr/vocabulary.py`) | ASR 定制词表(热词表)CRUD | +| `client.audio.phrases` | `AsrPhraseManager`(`audio/asr/asr_phrase_manager.py`) | paraformer 热词编译 CRUD | +| `client.audio.voices` | `VoiceEnrollmentService`(`audio/tts_v2/enrollment.py`) | 声音复刻(克隆音色)CRUD | +| `client.audio.voice_design` | 官方[声音设计 API](https://help.aliyun.com/zh/model-studio/voice-design-api-references)(1.x 无 SDK 类) | qwen 声音设计 CRUD | + +词表 / 声音复刻 / 声音设计三者共用同一信封:`POST /services/audio/{asr|tts}/customization`,请求体 `{model, input: {action, ...}, parameters?}`,仅 action 词与 ID 字段随族不同;v2 把 action 收敛为统一的 `create/list/get/update/delete` 方法名(1.x 的 `query_*` 统一为 `get`)。热词(phrases)实为一条编译任务:`create/update` POST `/fine-tunes`(`mode=compile_asr_phrase`、空文件集),`get/list/delete` 走 `/fine-tunes/outputs`。 + +所有方法共享关键字参数 `timeout` / `headers` / `api_key` / `workspace`;`model=` 可覆盖各资源默认服务模型(下表);模型专属的 `input` 字段经 `extra_input=` 透传(具名参数同名优先),顶层 `parameters=` 原样放入请求体。错误按 SDK 错误目录映射(如 `InvalidParameter` → `BadRequestError`);`raise_on_error=False` 时错误体现在返回对象的 `code` / `message` / `is_error` 上。返回值均携带 `request_id`;`update` / `delete` 返回 `JobStatus`。 + +| 资源 | 默认 `model` | 端点 | +| --- | --- | --- | +| `vocabularies` | `speech-biasing` | `/services/audio/asr/customization` | +| `voices` | `voice-enrollment` | `/services/audio/tts/customization` | +| `voice_design` | `qwen-voice-design` | `/services/audio/tts/customization` | +| `phrases` | 调用方 ASR 模型 | `/fine-tunes`、`/fine-tunes/outputs` | + +> 1.x 这两个服务内部对超时/断连做 3 次重试;v2 交由 transport 的重试循环统一处理(默认 2 次),无需在资源层重复。1.x `list_phrases(page=...)` 的 `page` 实参在 `**kwargs` 中被丢弃、从未到达线缆;v2 发送文档化的 `page_no`,不复刻该行为。 + +## 定制词表(vocabularies) + +对应官方[定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/)示例:为 paraformer 系列识别模型创建词表,返回 `vocabulary_id`。 + +```python +from dashscope.v2 import Client + +client = Client() +created = client.audio.vocabularies.create( + target_model="paraformer-realtime-v2", + prefix="med", # 仅小写字母/数字,10 字符以内 + vocabulary=[ + {"text": "心肌梗死", "weight": 4}, + {"text": "脑梗死", "weight": 4}, + ], +) +print(created.vocabulary_id) + +# 列表 / 查询内容 / 整体替换 / 删除 +tables = client.audio.vocabularies.list(prefix="med", page_index=0, page_size=10) +for item in tables.vocabularies: + print(item["vocabulary_id"], item.get("prefix")) + +table = client.audio.vocabularies.get(created.vocabulary_id) +print(table.vocabulary) # 词表内容 + +client.audio.vocabularies.update( + created.vocabulary_id, + vocabulary=[{"text": "冠心病", "weight": 4}], +) +client.audio.vocabularies.delete(created.vocabulary_id) +``` + +## 热词(phrases,paraformer) + +对应官方 [paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager)示例:`create` 返回编译任务,`phrase_id`(即任务 `job_id`)编译完成后即可传给识别: + +```python +from dashscope.v2 import Client + +client = Client() +phrase = client.audio.phrases.create( + model="paraformer-v2", + phrases={"下一首": 90, "上一首": 90}, # 词 -> 权重 +) +print(phrase.phrase_id, phrase.status) + +# 编译完成后在实时识别中引用(run-task 顶层 resources,见 ASR 指南) +# client.audio.asr.recognize(..., phrase_id=phrase.phrase_id) + +client.audio.phrases.update( + phrase.phrase_id, + model="paraformer-v2", + phrases={"暂停": 90}, +) +fetched = client.audio.phrases.get(phrase.phrase_id) +page = client.audio.phrases.list(page_no=1, page_size=10) +client.audio.phrases.delete(phrase.phrase_id) +``` + +`create` / `update` 的 `training_type` 默认 `"compile_asr_phrase"`;空 `phrases` 与空 `phrase_id` 在本地直接 `ValueError`。 + +## 声音复刻(voices) + +对应官方[声音复刻](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide)示例:以一段音频 URL 克隆音色,返回 `voice_id`,随后即可作为 TTS 的 `voice` 使用。 + +```python +from dashscope.v2 import Client + +client = Client() +voice = client.audio.voices.create( + target_model="cosyvoice-v3-plus", + prefix="demo", + url="https://your-audio-url/sample.wav", + language_hints=["zh"], +) +print(voice.voice_id) + +# audio = client.audio.tts.synthesize("你好", model="cosyvoice-v3-plus", voice=voice.voice_id) + +voices = client.audio.voices.list(prefix="demo") +detail = client.audio.voices.get(voice.voice_id) # status: OK / DEPLOYING / UNDEPLOYED +client.audio.voices.update(voice.voice_id, url="https://your-audio-url/new-sample.wav") +client.audio.voices.delete(voice.voice_id) +``` + +## 声音设计(voice_design) + +对应官方[声音设计](https://help.aliyun.com/zh/model-studio/voice-design-user-guide)示例:用文字描述生成音色,ID 字段为 `voice`,并随附 base64 预览音频。 + +```python +import base64 +from dashscope.v2 import Client + +client = Client() +designed = client.audio.voice_design.create( + target_model="qwen3-tts-vd-realtime-2026-01-15", + voice_prompt="沉稳的中年男性,音色低沉浑厚", + preview_text="各位听众朋友,大家好", + preferred_name="announcer", + language="zh", + parameters={"sample_rate": 24000, "response_format": "wav"}, +) +print(designed.voice) +preview = designed.preview_audio # {data(base64), sample_rate, response_format} +with open("preview.wav", "wb") as f: + f.write(base64.b64decode(preview["data"])) + +voices = client.audio.voice_design.list(page_index=0, page_size=10) +detail = client.audio.voice_design.get("announcer") +client.audio.voice_design.delete("announcer") +``` + +## 返回类型速查 + +| 类型 | 关键字段 | 说明 | +| --- | --- | --- | +| `Vocabulary` | `vocabulary_id`、`vocabulary` | `create` 只带 ID;`get` 带词表内容;其余字段随 `output` 保留 | +| `VocabularyList` | `vocabularies` | 词表条目 dict 列表 | +| `Voice` | `voice_id`、`status`、`preview_audio` | 克隆音色 | +| `VoiceList` | `voices` | 音色条目 dict 列表 | +| `DesignedVoice` | `voice`、`preview_audio`、`language`、`target_model` | 设计音色 | +| `DesignedVoiceList` | `voices`(另含 `total_count`) | 设计音色条目 | +| `Phrase` | `phrase_id`、`status` | 编译任务即热词集 | +| `PhraseList` | `phrases`、`page_no`、`page_size`、`total_count` | 热词集分页 | +| `JobStatus` | `status` | `update` / `delete` 的应答 | + +## 相关文档 + +- [DashScope 2.0 ASR SDK](dashscope-2.0-asr-sdk.md)(`phrase_id` 在识别中的使用) +- [DashScope 2.0 TTS SDK](dashscope-2.0-tts-sdk.md)(克隆/设计音色在合成中的使用) +- [定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/) / [paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager) +- [声音复刻](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide) / [声音设计](https://help.aliyun.com/zh/model-studio/voice-design-user-guide) diff --git a/md/dashscope-2.0-connection-reuse-sdk.md b/md/dashscope-2.0-connection-reuse-sdk.md new file mode 100644 index 00000000..45f24ab9 --- /dev/null +++ b/md/dashscope-2.0-connection-reuse-sdk.md @@ -0,0 +1,163 @@ +# DashScope 2.0 连接复用(语音实时面 + 全模型 HTTP) + +对应《dashscope-2.0-vs-1.x-alignment-review.md》§7.4 的 **M6**。 + +## 背景与动机 + +1.x 有大量用户以**连接复用**模式接入(主要是 Java 侧)。语音业务、尤其 +实时交互对延时极敏感:每省一次握手(TCP + TLS + websocket 升级,数百 +毫秒级)就直接降低首包延迟。因此连接复用是硬需求而非优化项。 + +- **ws 复用**:1.x 的 `WsSession` 形态是"一连接一任务",短任务高频调用 + (如逐句 TTS)下握手开销占比大;多路并发(如多路 ASR)下每路一条连接 + 也浪费 fd。2.0 引入 `WsConnection`:一条物理连接上按 `header.task_id` + 解复用多个任务,支持**并发多任务**与**顺序复用**(免再次握手)。 +- **HTTP 复用(所有模型)**:不止语音——2.0 所有资源的 HTTP 请求都 + 走同一个 keep-alive 连接池(每个 `Client` 一个 httpx client,1.x 是 + 每次调用新建 `requests.Session`)。M6 把池参数开放为 settings 配置, + 并给出同一 TCP 连接被复用的可测证明。 + +## 1. ws 复用:`WsConnection` + `connection=` + +### 1.1 基本用法 + +```python +import dashscope + +client = dashscope.Client(api_key="sk-xxx") + +# 工厂方法继承 client 的 api_key / workspace / region / timeout; +# 连接由调用方持有并负责关闭。 +conn = client.ws_connection() + +# 顺序复用:两个识别任务共用一条连接,第二个任务免握手 +for audio in (pcm_1, pcm_2): + with client.audio.asr.recognize( + model="paraformer-realtime-v2", + connection=conn, + ) as rec: + rec.send_audio_frame(audio) + rec.stop() + for result in rec.results(): + ... + +# 并发:ASR 上行 + TTS 下行跑在同一条连接上 +rec = client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn).start() +tts = client.audio.tts.connect(model="cosyvoice-v1", connection=conn).start() +rec.send_audio_frame(chunk) # 二进制上行 +for piece in tts.audio_chunks(): # 二进制下行 + play(piece) + +conn.close() +``` + +支持 `connection=` 的入口(run-task 帧协议族): + +| 入口 | 方向 | +| --- | --- | +| `client.audio.asr.recognize(...)` | JSON 下行 + 二进制上行 | +| `client.audio.tts.connect(...)` | JSON + 二进制下行 | +| `client.audio.translation.recognize(...)` | JSON 下行 + 二进制上行 | +| `client.tingwu.realtime(...)` | JSON 下行 + 二进制上行 | + +传入 `connection=` 后,`ws_url` / `connector` / `api_key` / `workspace` / +`headers` 等会话级参数被忽略(它们属于连接)。会话的 `close()` 只从连接 +上摘离本任务,**不会关闭共享 socket**;`WsConnection.close()` 才关闭物理 +连接并使所有未结束任务报错。 + +### 1.2 并发约束(协议决定) + +websocket 二进制帧**不携带 task_id**,无法解复用,因此每条连接: + +- **二进制上行单 owner**:第一个 `send_audio_frame` 的任务占用上行;其他 + 任务再发二进制会 `RuntimeError`。任务结束/关闭后释放。 +- **二进制下行单 owner**:TTS 会话在 `start()` 时自动 + `claim_binary_downlink()`;连接上同时只能有一个活跃合成(第二个 + `start()` 直接报错,要并发 TTS 请用多条连接)。仅有一个活跃任务时下行 + 会隐式归属该任务。 +- **纯 JSON 任务完全并发**:事件按 `header.task_id` 路由到各自队列,无 + 数量限制。 + +典型可行组合:1 路 ASR(二进制上行)+ 1 路 TTS(二进制下行)+ 任意多 +JSON 任务。典型不可行组合:2 路并发 TTS(下行无法区分)——开两条 +`WsConnection` 即可。 + +底层对象也可单独使用(`dashscope.WsConnection` / +`dashscope.v2.transport.ws_connection.WsTask`):`conn.open_task()` 得到与 +`WsSession` 同接口的任务视图(`run_task` / `continue_task` / +`finish_task` / `send_binary` / `recv` / `recv_event` / `close`)。 + +### 1.3 读线程与超时 + +`WsConnection` 内部有一个后台读线程,socket 读永久阻塞;每个任务的接收 +超时在解复用队列上生效(`open_task(timeout=...)`,默认取连接构造时的 +`timeout`)。socket 断开或协议错误会广播给所有任务。 + +### 1.4 realtime 族:协议不可复用(验证结论) + +realtime 协议(Omni / Qwen-Audio / Livetranslate,与 OpenAI Realtime +同构)**一条连接就是一个会话**:事件按会话级类型流转、没有 task_id,且 +会话结束服务端即关闭连接——既无法并发解复用,也无法顺序复用。这不是 +SDK 限制,是协议设计。`client.audio.realtime.connect` 每次新建连接即协议 +语义的直接体现(loopback 测试已固化该行为)。 + +要压首开延迟,方向是**预热连接**(在用户开始说话前提前 `connect()`), +而不是复用;realtime 会话通常持续数十秒到数分钟,握手开销摊销后占比很 +小。受益最大的是短任务高频的 TTS 与多路并发的 ASR——即 1.1 的形态。 + +## 2. HTTP keep-alive(所有模型生效) + +### 2.1 默认行为 + +每个 `Client` 持有一个 httpx 连接池,`client.chat` / `images` / `audio` / +`files` … 所有资源请求共享:默认 `max_connections=100`、 +`max_keepalive_connections=20`、`keepalive_expiry=5s`(与 httpx 默认一致; +1.x 每次调用新建 `requests.Session`,无跨调用复用)。 +`client.with_options(...)` 在未改连接级选项时**共享**同一个池。 + +注意:池是 per-Client 的——频繁新建 `Client` 实例会击穿复用;长驻一个 +client(或 with_options 派生)才是正确姿势。 + +### 2.2 settings 配置 + +```python +import dashscope + +dashscope.settings.http_max_connections = 50 # 默认 100 +dashscope.settings.http_max_keepalive_connections = 10 # 默认 20;0 = 关闭 keep-alive +dashscope.settings.http_keepalive_expiry = 30.0 # 默认 5.0 秒 +``` + +环境变量 / `~/.dashscope/config.toml` 同名键: + +| settings | 环境变量 | 默认 | +| --- | --- | --- | +| `http_max_connections` | `DASHSCOPE_HTTP_MAX_CONNECTIONS` | 100 | +| `http_max_keepalive_connections` | `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | 20 | +| `http_keepalive_expiry` | `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | 5.0 | + +优先级与其他配置一致:settings 显式赋值 > 环境变量 > config.toml > 默认。 +校验:`max_connections >= 1`、`max_keepalive_connections >= 0`、 +`keepalive_expiry > 0`。 + +### 2.3 复用证明 + +`tests/v2/test_http_pool.py` 起本地 HTTP/1.1 服务器记录对端端口: + +- 同一 `Client` 顺序 3 次请求 → 全部命中**同一 TCP 连接**(端口一致); +- `http_max_keepalive_connections = 0` → 每次请求新连接,证明 settings → + `httpx.Limits` → 传输行为端到端生效。 + +## 3. 1.x 对照 + +| 1.x | 2.0 | +| --- | --- | +| Java 侧连接池/复用(Python 1.x 无对应物,`WsSession` 一连接一任务) | `client.ws_connection()` + 各实时入口 `connection=`,并发 + 顺序复用 | +| 每次调用新建 `requests.Session` | 每 `Client` 一个 httpx 池,默认 keep-alive;池参数开放 settings | +| realtime(omni)一会话一连接 | 同(协议决定,不可复用;建议预热) | + +## 相关文档 + +- dashscope-2.0-asr-sdk.md / dashscope-2.0-tts-sdk.md / + dashscope-2.0-translation-sdk.md / dashscope-2.0-realtime-sdk.md +- dashscope-2.0-vs-1.x-alignment-review.md(§7.2-3、§7.4 M6) diff --git a/md/dashscope-2.0-decision-sdk.md b/md/dashscope-2.0-decision-sdk.md new file mode 100644 index 00000000..da08a780 --- /dev/null +++ b/md/dashscope-2.0-decision-sdk.md @@ -0,0 +1,288 @@ +# 决策模型(Decision / System One)Python SDK + +通过 `client.decision`,您可以针对一段业务状态(`state`),在一次前向推理中同时回答一组**类型化问题**:多选一(`choice`)、是非判断(`noul`)和有序评分(`score`)。每个答案都带有概率分布,`choice` / `score` 还附带置信度(`confidence`)。 + +决策模型(当前为 `decision-model-preview`)**不生成文本**,因此延迟和成本不随输出长度增长,适合替代"用大模型生成 JSON 再解析"的判断类环节。典型场景:工单路由、内容审核、Agent 路由、生成结果校验。 + +本文对应当前仓库的 SDK 实现;问题类型的语义、字段取值及服务端限制以决策模型 API 文档为准。 + +| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | +| --- | --- | --- | +| 单次决策 | `client.decision.create()` | `await client.decision.create()` | + +> 该接口为一次性请求-响应:不支持流式输出、函数调用(function calling)和联网搜索。 + +## 前期准备 + +1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 +2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置,目前支持 `cn-beijing`、`ap-southeast-1` 等地域。不配置工作空间且未指定端点时,当前 SDK 会回退到共享网关并发出警告。 +3. 在本仓库根目录执行源码安装: + + ```bash + python -m pip install -e . + ``` + +客户端使用公开入口 `from dashscope import Client, AsyncClient`。也可以在创建客户端时显式传入 `api_key`、`workspace`、`region`;本文示例从环境配置读取凭据。 + +## 快速开始 + +### 同步调用:工单路由 + +以下示例对一张工单同时提出三个问题:应由哪个团队处理(多选一)、是否需要通知值班人员(是非)、严重程度(评分)。可保存为 `decision_example.py` 直接运行。 + +```python +from dashscope import Client +from dashscope.v2.resources.decision import ( + ChoiceQuestion, + NoulQuestion, + ScoreQuestion, +) + +state = { + "ticket_id": "T-1001", + "content": "订单支付后超过 24 小时仍未到账,用户无法继续使用核心服务。", +} + +questions = { + "department": ChoiceQuestion( + instructions="应该由哪个团队处理?", + criteria={ + "billing": "支付、退款和账单问题", + "technical": "产品故障和集成问题", + }, + ), + "escalate": NoulQuestion(instructions="是否需要立即通知值班人员?"), + "severity": ScoreQuestion( + instructions="这个问题有多严重?", + criteria=[ + "轻微问题,不影响功能", + "部分功能受影响,但存在替代方案", + "核心功能不可用,没有替代方案", + "造成严重业务或安全影响", + ], + ), +} + +with Client() as client: + resp = client.decision.create( + model="decision-model-preview", + state=state, + questions=questions, + ) + +print("request_id:", resp.request_id) + +department = resp.answers["department"] +print("department:", department.choice) # billing +print("confidence:", department.confidence) # 0.88 +print("probabilities:", department.probabilities) # {"billing": 0.94, "technical": 0.06} + +escalate = resp.answers["escalate"] +print("escalate P(yes):", escalate.noul) # 0.99 + +severity = resp.answers["severity"] +print("severity:", severity.score) # 2.25(概率加权等级索引) +print("legend:", severity.legend["2"]) # 核心功能不可用,没有替代方案 + +print("input_tokens:", resp.usage.input_tokens) # 125 +print("latency_ms:", resp.latency_ms) # 52.9 +``` + +- `questions` 的键是您自定义的问题 ID,`answers` 按相同的键返回,一一对应。 +- 一次调用可提多个问题,只产生一次模型前向推理,按输入 token 计费(`usage` 中只有 `input_tokens`)。 +- `score` 答案是**概率加权等级索引**,可能落在两个等级之间(如上例 `2.25` 介于等级 2 和 3 之间);`legend` 把等级索引映射回您给的等级描述,索引以字符串为键。 + +### 三种问题类型 + +| 类型 | 模型类 | `criteria` | 答案字段 | 说明 | +| --- | --- | --- | --- | --- | +| 多选一 | `ChoiceQuestion` | `Dict[str, str]`,必填,1–255 个选项 | `choice`(选中的选项名)、`confidence`、`probabilities` | `criteria` 映射"选项名 → 选项描述";请给全选项集合,建议增加 `other` 兜底选项。 | +| 是非判断 | `NoulQuestion` | `Dict[str, str]`,可选 | `noul`(P(yes),0=否、1=是)、`probabilities` | `criteria` 可描述两个结果,如 `{"true": "...", "false": "..."}`;判定阈值由应用自定(如 0.5)。 | +| 有序评分 | `ScoreQuestion` | `List[str]`,必填,2–255 级 | `score`(概率加权等级索引)、`confidence`、`probabilities`、`legend` | `criteria` 按从低到高描述各等级;3–7 个清晰可区分的等级效果最佳。 | + +三个模型类都有可选字段 `instructions`(对该问题的补充说明)。未设置的可选字段不会出现在请求报文中(不会发送显式 `null`)。 + +### 原始字典形式(前向兼容) + +`questions` 的值也可以直接传原始 wire 字典,SDK 原样透传(包括 SDK 模型尚未收录的未来字段): + +```python +resp = client.decision.create( + model="decision-model-preview", + state="这段评论是否在索要退款?", # state 也可以是纯文本 + questions={ + "refund": { + "type": "noul", + "instructions": "是否在索要退款?", + "future_field": 1, # 未知字段透传,便于前向兼容 + }, + }, +) +print(resp.answers["refund"].noul) +``` + +类型化模型(`ChoiceQuestion` 等)会在本地做 pydantic 校验,能在发送前发现错误;原始字典不做本地校验,适合使用 SDK 尚未支持的新字段。 + +### 异步调用 + +`AsyncClient` 提供相同的接口,需 `await`: + +```python +import asyncio + +from dashscope import AsyncClient + + +async def main(): + async with AsyncClient() as client: + resp = await client.decision.create( + model="decision-model-preview", + state={"ticket_id": "T-1001", "content": "..."}, + questions={"q": {"type": "noul", "instructions": "是否需要人工介入?"}}, + ) + print(resp.answers["q"].noul) + + +asyncio.run(main()) +``` + +## 请求参数 + +### `client.decision.create()` + +所有参数均为关键字参数。 + +| 参数 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `model` | `str` | 是 | 模型名称,当前为 `decision-model-preview`。 | +| `state` | `str \| dict \| list` | 是 | 待决策的业务状态:自由文本、一段对话或结构化对象;结构化对象会被序列化后交给模型。 | +| `questions` | `Dict[str, QuestionInput]` | 是 | 问题集合,键为问题 ID,值为 `ChoiceQuestion` / `NoulQuestion` / `ScoreQuestion` 实例或原始字典。不能为空,否则抛出 `ValueError`。 | +| `timeout` | `float` | 否 | 本次请求超时时间(秒),覆盖客户端级 `timeout`。 | +| `api_key` | `str` | 否 | 本次请求使用的 API Key,覆盖客户端级配置。 | +| `workspace` | `str` | 否 | 本次请求使用的工作空间,覆盖客户端级配置。 | + +### 问题模型字段 + +`ChoiceQuestion`: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `type` | `Literal["choice"]` | 固定值 | 默认 `"choice"`,无需设置。 | +| `instructions` | `str` | 否 | 问题说明。 | +| `criteria` | `Dict[str, str]` | 是 | 选项名 → 选项描述,1–255 个;为空或缺失会抛出 `ValidationError`。 | + +`NoulQuestion`: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `type` | `Literal["noul"]` | 固定值 | 默认 `"noul"`,无需设置。 | +| `instructions` | `str` | 否 | 问题说明。 | +| `criteria` | `Dict[str, str]` | 否 | 描述两个结果,如 `{"true": "...", "false": "..."}`;裸 `NoulQuestion()` 合法。 | + +`ScoreQuestion`: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `type` | `Literal["score"]` | 固定值 | 默认 `"score"`,无需设置。 | +| `instructions` | `str` | 否 | 问题说明。 | +| `criteria` | `List[str]` | 是 | 从低到高的等级描述,2–255 级;少于 2 级会抛出 `ValidationError`。 | + +三个模型类均允许额外字段(`extra="allow"`),额外字段随报文透传。 + +## 返回字段 + +### `DecisionResponse` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `answers` | `Dict[str, DecisionAnswer]` | 各问题的答案,键与请求的 `questions` 键一致。 | +| `model` | `str` | 实际应答的模型名。 | +| `usage` | `Usage` 或 `None` | 用量对象;决策模型只产生 `input_tokens`(如 `125`),不生成输出 token。 | +| `latency_ms` | `float` 或 `None` | 服务端推理耗时(毫秒)。 | +| `request_id` | `str` | 请求 ID,排查问题时请提供。 | +| `headers` | `Dict[str, str]` | 响应头的小写字典副本,如 `resp.headers["x-request-id"]`。 | +| `status_code` / `code` / `message` / `is_error` | - | 错误信封字段,正常响应时 `is_error` 为 `False`;见"异常处理"。 | + +### `DecisionAnswer` + +按 `type` 填充对应字段,未涉及的字段为 `None`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `type` | `str` | 答案类型:`"choice"` / `"noul"` / `"score"`。 | +| `choice` | `str` | 多选一答案:选中的选项名。 | +| `noul` | `float` | 是非答案:P(yes),0 表示否、1 表示是。 | +| `score` | `float` | 评分答案:概率加权等级索引,可能落在两个等级之间(如 `1.43`)。 | +| `probabilities` | `Dict[str, float]` | 每个选项 / 等级的概率,总和为 1。 | +| `confidence` | `float` | 置信度;仅 `choice` / `score` 返回答案时携带。 | +| `legend` | `Dict[str, str]` | 仅 `score`:等级索引(字符串键)→ 等级描述。 | + +响应为平坦结构(无 `output` 信封),额外字段同样透传保留。 + +## 端点与路由 + +决策模型的服务端点为 `POST {网关}/compatible-mode/v1/systemone`。它挂在每个网关的 compatible-mode 路径下,**与客户端的 `protocol` 设置无关**——无论 chat 协议配置为 `native`、`openai` 还是 `anthropic`,决策请求都发往同一路径: + +- 默认(配置工作空间):`https://{workspace}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone`。 +- 显式 `base_url`:以您配置的 `base_url` 为服务根,请求发往 `{base_url}/systemone`(与 chat 的 base_url 规则一致)。 +- 未配置工作空间且未指定端点:回退到共享网关(如 `https://dashscope.aliyuncs.com/compatible-mode/v1/systemone`)并发出 `UserWarning`;生产环境请配置工作空间。 + +## 异常处理 + +默认 `raise_on_error=True` 时,以下失败都会抛出异常: + +| 情况 | 行为与处理建议 | +| --- | --- | +| HTTP 错误(4xx/5xx) | 按服务端错误码映射为 `DashScopeError` 子类抛出,如参数错误抛 `BadRequestError`、限流抛 `RateLimitError`、鉴权失败抛 `AuthenticationError`;检查异常的 `code`、`message`、`status_code`、`request_id`。 | +| 业务错误层(HTTP 200 + 错误体) | 服务端以 200 返回 `{"code", "message"}` 时同样按错误码抛出(如 `BadRequestError`),此时 `status_code` 为 200;不要仅凭 HTTP 状态判断成功。 | +| 本地参数错误 | `questions` 为空字典抛出 `ValueError`;问题模型字段不满足约束(如 `ChoiceQuestion` 无选项、`ScoreQuestion` 少于 2 级)抛出 pydantic `ValidationError`,请求不会发出。 | + +`Client(raise_on_error=False)` 时不抛异常,改为填充返回对象的错误信封: + +```python +client = Client(raise_on_error=False) +resp = client.decision.create( + model="decision-model-preview", + state="...", + questions={"q": {"type": "noul"}}, +) +if resp.is_error: + print(resp.code, resp.message, resp.request_id) + # answers 为空字典,不要按正常答案读取 +``` + +- 网络层失败(连接失败、超时)抛出 `APIConnectionError` / `APITimeoutError`。 +- 排查时请记录异常类型与 `request_id`;不要记录 API Key 或未经脱敏的业务状态内容。 + +## 常见问题 + +**为什么 `score` 答案是小数?** + +`score` 是各等级概率加权后的索引,不是离散等级。例如 `2.25` 表示答案主要分布在等级 2、部分落在等级 3(可结合 `probabilities` 查看分布)。需要离散等级时可四舍五入或按阈值判定。 + +**`noul` 返回 0.99 可以直接当作"是"吗?** + +`noul` 是 P(yes) 概率,不是布尔值。判定阈值由应用决定(常用 0.5);对关键操作可结合 `probabilities` 或设置更高阈值。 + +**为什么有的答案没有 `confidence`?** + +`confidence` 仅随 `choice` / `score` 答案返回;`noul` 答案可通过 `probabilities` 自行评估确定程度。 + +**配置了 `protocol="anthropic"`(或 `openai`),决策请求的端点会跟着变吗?** + +不会。`protocol` 只影响 chat 族接口的路由;决策模型固定走 compatible-mode 路径,三种协议配置下URL 完全一致。 + +**可以一次传多少个问题?** + +`questions` 至少一个(空字典会抛出 `ValueError`),多个问题共享一次前向推理。问题数量、单字段长度等上限以服务端限制为准;建议问题 ID 使用简短、可读、稳定的标识,答案按键原样返回。 + +**支持流式输出或 function calling 吗?** + +不支持。决策模型不生成文本,一次性返回全部答案;需要生成能力请使用 chat 族接口,再把生成结果交给 `decision.create()` 做校验是常见的组合用法。 + +## 相关文档 + +- [DashScope 2.0 设计](dashscope-2.0-design.md) +- [DashScope 2.0 迁移指南](dashscope-2.0-migration-guide.md) +- [语音识别 Python SDK](dashscope-2.0-asr-sdk.md) +- [语音合成 Python SDK](dashscope-2.0-tts-sdk.md) diff --git a/md/dashscope-2.0-design.md b/md/dashscope-2.0-design.md new file mode 100644 index 00000000..7dbd6b68 --- /dev/null +++ b/md/dashscope-2.0-design.md @@ -0,0 +1,596 @@ +# dashscope 2.0 设计方案 + +> 基于 V2 分支现状(见 `md/dashscope-interfaces.md` 全量接口清单),规划 dashscope 2.0。 + +## 0. 目标与原则 + +**目标** + +1. **用户接口友好**:一个现代化的 SDK + CLI(`dashscope`),以及一个 Agentic CLI(`dashscope expert`)。 +2. **协议兼容**:通过配置或传入 config,兼容 Anthropic / OpenAI 协议(既可作为客户端消费这两种协议,也保证 dashscope 自身能力可按这两种协议表达)。 +3. **接口自动化生成**:SDK / CLI / 文档 / 测试从单一 API 元数据源自动生成,人工只维护元数据与少量手写覆盖。 + +**原则**(对比调研后确立/修订) + +- **P1 对齐行业基线,不做发明**:Client 实例化、Pydantic 类型化、异常驱动、自动重试、httpx——OpenAI/Anthropic(同为 Stainless 生成)已验证的现代 SDK 基线,直接对齐,不自创风格。 +- **P2 契约正确性优先于功能数量**:错误被吞、退出码撒谎、流中出错不报——这类"静默错误"是调研发现的最高危问题,修复优先级高于任何新能力。 +- **P3 渐进迁移,不破坏存量**:1.x 代码冻结、2.0 物理隔离新建(§2.4 零接触方案),不建兼容层;DeprecationWarning 引导迁移;1.x 快修(M0)在维护分支单独实施,不等 2.0。 +- **P4 保留自有优势**:异步长任务(async_call/fetch/wait)、多模态 content 格式(`{"text":..., "image":...}`)、RL/微调/部署全流程是调研确认的领先项,2.0 统一化但**不削弱、不下线**。 +- **P5 差异化定位**:CLI 不跟随 Agent 赛道(expert 负责),主打两家已放弃的"AI 平台长流程管理"品类;SDK 不做厂商专属适配,靠协议层通吃(第三方模型已收敛到 OpenAI/Anthropic 兼容)。 +- **P6 codegen 是主线不是后置**:调研证实手写 vs 流水线是代际差距(OpenAI/Anthropic 均为 spec 生成)。specs 元数据从 M1 起同步建设,M6 全面接管,而非等功能写完再补。 + +现状问题(为什么要 2.0): + +- SDK 接口形态不统一:`Generation.call`、`AioGeneration.call`、`ImageSynthesis.async_call/fetch/wait` 三套风格并存,参数散落在 `**kw`,无统一 Client 对象。 +- CLI 与 SDK 一一映射、命令冗长(`dashscope multimodal_conversation create`),缺少面向场景的友好命令。 +- 协议层只有自有 `protocol/websocket.py`,OpenAI/Anthropic 兼容散落在 acli 的 providers 中,未沉淀为 SDK 能力。 +- 接口靠手写,SDK/CLI/文档三处重复劳动,易漂移。 +- (调研新增)错误契约弱:返回值式错误处理易漏检、核心路径无自动重试、流式出错不退出/不可解析、日志缺 request_id 且无敏感信息过滤。 +- (调研新增)战略窗口期:OpenAI/Anthropic 已放弃"API 管理 CLI"品类、其 SDK 全面 spec 生成化——DashScope 若不在 2.0 补齐契约与流水线,差距将从"体验落后"固化为"代际落后"。 + +### 对比调研共识(→ 设计硬需求) + +对 OpenAI/Anthropic SDK+CLI 的八份对比调研(见 `md/` 目录)收敛出的共识,全部纳入本方案: + +| 调研结论 | 出处 | 2.0 落点 | 预期收益 | +| --- | --- | --- | --- | +| Client 实例化 + 链式资源导航(类方法 + 全局变量是最大槽点) | sdk-* 对比 ×4 | §2.1 Client 资源式 API | 易用性:多租户/多环境安全,IDE 补全全链路可用 | +| 异常驱动错误处理(按 HTTP 状态细分异常类)+ 错误码枚举 | sdk-source-level-comparison-part2 | §2.7 错误模型(§2.2 摘要) | 稳定性:错误不再被漏检(返回值模式漏写 if 即静默失败);重试/降级逻辑可收敛 | +| 内置自动重试(默认 2 次 + Retry-After + 指数退避),DashScope 核心路径目前无重试 | sdk-source-level-comparison-part2 §2.4 | §2.2 传输层 | 稳定性:限流/抖动场景成功率直接提升,用户最痛点 | +| Pydantic 类型化响应 + `@overload` 精确签名(流式/非流式类型层面可区分) | sdk-source-level-comparison-part2 §1.3 | §2.2 约定 | 易用性:字段拼写错误静态期暴露,杜绝运行时 KeyError | +| 流式人体工学:text_stream 纯文本迭代器、final message 聚合、流内错误抛异常 | sdk-deep-comparison §3.3 | §2.2 流式统一模式 | 易用性 + 稳定性:用户少写累积/检查样板;流中错误不再被吞 | +| 便捷属性(`resp.output_text` 一行取文本)+ Quickstart 极简入口 | sdk-comparison-openai-vs-dashscope | §2.5 | 易用性:上手代码从 6 行降到 2 行,对齐 OpenAI 首屏体验 | +| httpx 统一同步/异步,消除 requests/aiohttp 双栈 | sdk-source-level-comparison-part2 §3.4 | §2.2 传输层 | 性能:连接池复用减少每请求 TCP/TLS 握手;包体积与双栈漂移风险同降 | +| 日志:request_id 入日志、敏感头过滤、重试原因/等待时间 | sdk-deep-comparison §6.7 | §2.2 日志 | 可运维性:排障可定位到工单级;消除 API Key 泄露入日志的安全隐患 | +| OpenAI/Anthropic SDK 均为 Stainless 从 OpenAPI spec 生成,手写 vs 流水线是代际差距 | conclusion-sdk-architecture-stainless | §6 codegen 升为战略项 | 研发效率:新参数上线从"人肉改签名+文档+测试"(周级)降到改 spec 自动生成(天级),消除漂移 | +| OpenAI 已删除 API CLI、Anthropic 从未有过;"API 管理 CLI"品类只剩 DashScope,独特价值在长流程管理(ft/部署/RL) | cli-source-level-comparison-2026-08 §0 | §4 定位 | 战略:占据两家放弃的品类空位,差异化而非跟随 | +| CLI 立即修:流式出错 exit 0 bug、shell 补全、auth whoami、`--output-format json` | cli-source-level-comparison-2026-08 §5 | §4.2 | 稳定性(CI 误判成功)+ 易用性(补全/认证探测)+ 可集成性(jq 可解析) | +| MiniMax/DeepSeek/Baseten/Kimi 协议已收敛到 OpenAI/Anthropic 兼容,无需厂商专属 adapter | vendor-sdk-research-compat | §3.3 | 研发效率:避免 N 个厂商 adapter 的维护负担;第三方模型开箱即用 | + +**收益汇总**(按维度): + +- **易用性**:上手 2 行代码、IDE 全补全、错误不用逐个 if 检查、流式无需手工累积——对齐 OpenAI/Anthropic 的现代基线(调研评分中 DashScope 差距最大的三项:类型提示、错误契约、流式人体工学)。 +- **稳定性**:自动重试 + 异常驱动 + 流内错误抛出 + CLI 退出码修复,覆盖"请求失败被静默吞掉"的全部已知路径;这是当前 SDK/CLI 唯一会导致用户数据/CI 结果错误的类别。 +- **性能**:httpx 连接池复用(现状每次请求新建 requests.Session);高并发场景减少握手开销,异步路径与同步共用一套实现、无行为漂移。 +- **研发效率**:codegen 使"新增一个 API 参数"的成本从周级手写降到天级自动生成,SDK/CLI/文档/测试四处不再漂移。 +- **战略**:独占"AI 平台长流程管理 CLI"品类 + 第三方模型零成本接入,构成 2.0 的差异化护城河。 + +--- + +## 1. 总体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ dashscope expert(Agentic CLI,演进自 dashscope.acli) │ +│ TUI/REPL、session、memory、mcp、tools、skills │ +├─────────────────────────────────────────────────────────────┤ +│ dashscope CLI(dashscope.cli 重写) │ +│ 命令树 = SDK 资源树的自动映射 + 场景化快捷命令 │ +├─────────────────────────────────────────────────────────────┤ +│ dashscope SDK 2.0(dashscope 包根) │ +│ Client 资源式 API(chat/images/audio/files/finetunes/...) │ +├──────────────────────┬──────────────────────────────────────┤ +│ Protocol 适配层 │ Transport 层 │ +│ openai / anthropic │ HTTP(SSE) / WebSocket / 任务轮询 │ +│ / dashscope-native │ │ +├──────────────────────┴──────────────────────────────────────┤ +│ API 元数据(specs/*.yaml)→ codegen → SDK/CLI/文档/测试 │ +└─────────────────────────────────────────────────────────────┘ +``` + +分层原则:expert 只用 SDK 公开 API;CLI 是 SDK 的薄壳;协议与传输在 SDK 内部隔离,上层无感。 + +--- + +## 2. SDK 2.0 设计 + +### 2.1 统一入口:资源式 Client + +对齐 OpenAI/Anthropic 的用户心智,一个 `Client` 对象挂所有资源;同步 + 异步双版本: + +```python +import dashscope + +client = dashscope.Client(api_key=..., protocol="openai" | "anthropic" | "native", base_url=...) + +resp = client.chat.completions.create(model="qwen-plus", messages=[...], stream=True, tools=[...]) +img = client.images.generate(model="wanx2.1-t2i", prompt="...", n=1, size="1024*1024") +aclient = dashscope.AsyncClient(...) +``` + +### 2.2 资源树与现有接口映射 + +| 新资源 | 覆盖现有接口(见 interfaces 清单) | 关键方法 | +| --- | --- | --- | +| `client.chat` | `Generation` / `AioGeneration` / `Conversation` / `CodeGeneration` / `Understanding` | `completions.create`(messages 统一格式,tools/stream 一等参数) | +| `client.apps` | `Application` | `completions.create(app_id, prompt)` | +| `client.multimodal` | `MultiModalConversation` / `AioMultiModalConversation` | `chat.create`(content parts:text/image_url/audio/video) | +| `client.images` | `ImageSynthesis` / `ImageGeneration` | `generate`(同步封装)/ `jobs.create/fetch/wait`(异步任务);按模型名路由协议——wan2.5 及以下走旧 text2image 端点(`/services/aigc/text2image/image-synthesis`,仅异步任务,`prompt` 入参 / `results` 出参);wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`generate` 同步打 `/services/aigc/multimodal-generation/generation`,`jobs` 异步任务打 `/services/aigc/image-generation/generation`,`messages` 入参 / `choices` 出参,出参归一化为统一 `Job.results`) | +| `client.videos` | `VideoSynthesis` | `generate` / `jobs.create/fetch/wait`;同端点(`/services/aigc/video-generation/video-synthesis`,仅异步任务,`video_url` 出参)下按模型名路由输入协议——wan2.6 及以下走旧首帧协议(`img_url` 入参,公开名 `image_url`);wan2.7-i2v 只应答新 i2v 协议(`media` 入参:`first_frame` / `last_frame` / `driving_audio` / `first_clip` 类型化素材列表、每种至多一个、必填,`image_url` 归一为单个 `first_frame` 素材项) | +| `client.embeddings` | `TextEmbedding` / `MultiModalEmbedding` / `BatchTextEmbedding` | `create` / `jobs.*`(批量任务) | +| `client.rerank` | `TextReRank` / `AioTextReRank` | `create(query, documents)` | +| `client.tokenization` | `Tokenization` | `count(prompt)` | +| `client.audio.asr` | `Recognition` / `Transcription` / `QwenTranscription` | `stream`(实时 ws)/ `transcribe(file_urls)` | +| `client.audio.tts` | `SpeechSynthesizer` / `tts_v2` / `HttpSpeechSynthesizer` / `qwen_tts` | `synthesize`(一次性)/ `stream`(流式) | +| `client.audio.realtime` | `OmniRealtimeConversation` | `session()`(ws 会话上下文管理器) | +| `client.fine_tunes` | `FineTunes` | `create/get/list/cancel/delete/stream_events/wait` | +| `client.deployments` | `Deployments` | `create/get/list/scale/delete` | +| `client.files` | `Files` | `upload/list/get/delete` | +| `client.models` | `Models` | `list/get` | +| `client.rl` | (CLI 的 `dashscope rl` 能力 SDK 化) | `register_functions/test_functions/upload_data/run/get/cancel/logs/list`;**复用 1.x 既有实现,不重写**——自有逻辑多,为避免影响 1.x,2.0 仅在 errors 与底层 HTTP 调用上对齐 v2,业务逻辑一律不动(见 3.7) | +| ~~`client.beta.assistants`~~ | `Assistants/Threads/Messages/Runs/Steps` | **整个 Assistants API 族确定下线,不迁入 2.0**:1.x 原模块冻结保留至 3.0 随旧代码删除,2.0 资源树中无此项(托管 Agent 场景由 `client.agentstudio` 承接) | +| `client.agentstudio` | `dashscope.agentstudio`(agents/sessions/events,含流式事件) | 托管 Agent API;1.x 子 SDK 代码已较新、与 2.0 风格接近,**仅合并入统一 Client,不动原来的代码和逻辑**;errors 与底层 HTTP 调用与 2.0 的对齐由专门环节统一处理(见 M3 任务 3.12:挂载层映射包装,不触碰 1.x 代码,不与 §2.4 冲突) | + +约定: + +- **长任务统一模式**:`jobs.create → fetch/wait → result`,`wait` 内置退避轮询;所有任务资源返回统一的 `Job` 对象(`id/status/result/error`)。 +- **同资源多协议按模型路由**:同一资源下不同模型落在不同服务端点/协议时,按模型名自动路由,不向用户暴露端点选择参数(与 `chat.completions` 含媒体 part 自动改走多模态端点同一约定)。当前应用点:`client.images`(见上表:wan2.6-t2i / wan2.6-image → multimodal-generation 协议,其余 → 旧 text2image 端点)、`client.videos`(wan2.7-i2v → media 新 i2v 协议,其余 → 旧 `img_url` 首帧协议)。新协议模型清单分别以 `v2/resources/images.py` 的 `MULTIMODAL_MODEL_PREFIXES`、`v2/resources/videos.py` 的 `MEDIA_MODEL_PREFIXES` 为准,新模型上线时前缀扩充。 +- **流式统一模式**:`stream=True` 一律返回类型化迭代器(`Stream[Chunk]` / `AsyncStream[Chunk]`,用 `@overload` 让流式/非流式返回类型在静态层面可区分);提供 `stream.text_stream` 纯文本迭代器与 `get_final_message()` 聚合助手;**流内错误统一抛异常,不 yield 错误对象**。ws 实时接口用上下文管理器。例外:`client.apps` 在 `raise_on_error=False` 兼容模式下以 `is_error=True` 终止事件透出流内错误(对齐 1.x `Application` 流式返回值语义),不回抛;chat/multimodal 无此例外。 +- **类型化**:入参出参全部 Pydantic 模型,`**kw` 仅保留 `extra_headers/timeout` 等逃逸舱;响应提供便捷属性(如 `resp.output_text` 一行取文本);内置 `.pyi`。 +- **错误模型**(对齐 OpenAI/Anthropic,见对比调研;**规范正文见 §2.7**): + - 默认异常驱动:HTTP/业务失败抛 `DashScopeError` 层级,按状态码细分为 9 个扁平类 `BadRequestError(400)` / `AuthenticationError(401)` / `PermissionDeniedError(403)` / `NotFoundError(404)` / `RequestTooLargeError(413)` / `RateLimitError(429)` / `InternalServerError(500)` / `ServiceUnavailableError(503)` / `GatewayTimeoutError(504)`;另有传输层 `APIConnectionError` / `APITimeoutError`、内容审核 `ContentFilterError`、SDK 自报 `SdkError`; + - 异常携带 `status_code / code / message / request_id / body / response.headers`; + - 成功响应同样携带 `resp.headers`(网关原始 HTTP 响应头,1.x `GenerationResponse.headers` 对齐):由 `types.common.with_headers` 在每个非流式 HTTP 成功返回点填充(`with_headers` 按结构化 Protocol 接受任何声明了 `headers` 字段的模型,不限于 `ResponseBase` 子类,如 audio `HttpSynthesis`);流式经 `Stream.headers`(chat)、`AppStream.headers`、`HttpSynthesisStream.headers` 透出,chat 另由 `get_final_message()` 回填到聚合结果;ws 资源无 HTTP 信封,保持空 dict;成功侧 `resp.headers` 是小写键的普通 dict(同名多值头以 `", "` 拼接,保证 `model_dump()` 可序列化),而 `errors.response_error` 放在异常上的是活的 `httpx.Headers`(大小写不敏感);"HTTP 200 但业务失败"(如任务 FAILED)抛出的异常同样携带 `headers`,且 `err.body` 只含 wire/作业快照,不会把 `headers` 字段带进去(调用方常直接记录 `err.body`); + - `"Throttling"`、`"InvalidApiKey"` 等字符串错误码收敛为可 import 的枚举常量(`dashscope.v2.errors.ErrorCode`,13 项;包根不导出),完整对照表见 §2.7; + - 兼容开关:`Client(raise_on_error=False)` 退回 1.x 返回值模式; + - 保留"HTTP 200 但业务失败"的双层检查(DashScope 网关特性,现有 `ensure_ok` 语义上移到 SDK)。 +- **内置重试**:默认 `max_retries=2`,429/5xx/连接错误自动重试,解析 `Retry-After`,指数退避 + 抖动;`Client(max_retries=N)` / `with_options()` 可调。 +- **传输层**:httpx 统一同步/异步(连接池复用),替换 requests + aiohttp 双栈;超时支持 connect/read 分离。 +- **日志**:`DASHSCOPE_LOG=debug` 一键开启;记录 request_id、HTTP 状态码、重试原因与等待时间;SensitiveHeadersFilter 过滤 Authorization,防止密钥入日志。 + +### 2.3 配置 + +```python +dashscope.Client(api_key=..., base_url=..., protocol=..., timeout=..., max_retries=...) +``` + +优先级:显式参数 > 环境变量(`DASHSCOPE_API_KEY` 等) > `~/.dashscope/config.toml`。1.x 的 `dashscope.api_key` 等模块级全局量原样冻结保留(供存量代码使用,deprecate 标注);2.0 新代码一律用 `Client(...)` 显式配置,两者互不影响。 + +### 2.4 兼容与长期共存策略(零接触物理隔离) + +前提修正:原"两个 minor 版本后移除"不成立,1.x 预计 1–2 年内无法下线。曾评估"单代码库、双门面共享核心"方案,结论是共享核心的行为渗透防护(语义隔离 + 契约测试)成本高于收益——**2.0 采用零接触物理隔离:1.0 代码一行不动、整体冻结,2.0 完全独立新建,HTTP/传输不复用**。 + +``` +用户代码(1.x 写法) ── dashscope.Generation.call / dashscope.cli(冻结,不动) +用户代码(2.0 写法) ── dashscope.Client / dashscope.chat(包根导出) + │ + dashscope/v2/(独立实现:httpx transport / protocols / resources) +``` + +**架构规则** + +1. **1.x 代码冻结**:现有模块(`Generation` 等类方法接口、`dashscope/cli/` 旧命令树、requests/aiohttp 传输)保留但不再修改(仅安全补丁走 1.x 维护分支);不建兼容层、不做转调——用户继续 `import dashscope` 用旧写法,行为逐字节不变。 +2. **2.0 独立目录**:全部新实现放 `dashscope/v2/`(client / resources / protocols / transport / errors),不与 1.x 共享任何请求代码(例外见规则 4);依赖共存(requests/aiohttp 留给 1.x,httpx 供 2.0),默认完整安装,不做 extras 拆分。 +3. **包根只导新面**:`dashscope/__init__.py` 导出 2.0 公开 API——`dashscope.Client` / `dashscope.AsyncClient` / `dashscope.chat()`——用户 import 路径无 `v2` 字样;`v2` 只是内部目录名,不是公开命名空间。 +4. **白名单式共享**:唯一允许的共享是零逻辑常量(端点 URL、模型名常量等),逐项白名单评审;任何含行为的代码一律不共享。**例外**:`rl` 与 `agentstudio` 两块直接复用 1.x 既有实现代码,不重写。**基本原则:两块的业务逻辑一律不动**,仅对齐 errors 与底层 HTTP 调用——`rl` 自有逻辑多,2.0 只建挂载层;`agentstudio` 已较新,仅合并入 Client(见 M3 任务 3.7/3.8)。**白名单登记**(2.0 期间对 1.x 的全部实际触碰,此外零触碰):① 包根 `dashscope/__init__.py` 导出 2.0 公开面(规则 3 授权);② `0c58a74` 将 `dashscope/finetune/reinforcement/common/constants.py` 的 `LOGGER_NAME` 默认值从 `dashscope` 改为 `dashscope.finetune.reinforcement`(rl 日志挂独立 logger 名,避免与包根 logger 配置互相干扰)。 +5. **新需求只进 2.0**:新接口、新参数、新模型能力一律只进 `v2/` 与新命令树;1.x(含旧 CLI 命令)不新增支持,文档引导用户为新能力迁移。 +6. **测试互不影响**:现有 1.x 测试套件原样保留作冻结回归(不改、不扩);2.0 测试全部新建于 `tests/v2/`。 +7. **下线即删目录**:3.0 时整目录删除 `dashscope/v2/` 之外全部旧代码并上移,无兼容层代码残留——删除动作机械、无风险。 + +**acli 独立于新旧隔离**:`dashscope/acli/` 代码量大,2.0 原样复用、不重写;它既不在 1.x 冻结集、也不进 `v2/`,作为第三条线独立演进(提交约束见 §7"开发与提交约束"),M5 仅做底座切换与 tools 注册。3.0 包结构重整时需同步迁移适配(见后续任务 8.1)。 + +**CLI 侧** + +8. 旧命令(`generation create`、`multimodal_conversation create` 等)不动、继续调用旧模块;新命令树(`chat`/`config`/`auth`/`images`...)单独建,只 import `v2/`。 +9. 新旧命令并存于同一 `dashscope` 入口。`ft`/`rl` 等习惯名由新命令树直接承接(参数/输出按 §4 改进,向后兼容旧参数名);其余旧命令名(`generation` 等)原样冻结,3.0 随旧代码一并删除。 + +**下线流程(遥测驱动,不设死线)** + +10. 旧接口挂 DeprecationWarning(Python 默认不显示,不扰民;此为唯一允许的 1.x 触碰),文档/官网推迁移指南; +11. 埋点统计旧接口调用占比(包版本 + 入口名,脱敏),每季度评估; +12. 移除条件:旧接口调用占比低于阈值(如 <5%)且无大客户依赖反馈 → 3.0 整目录删除旧代码,而非 2.x 中途砍掉; +13. 共存期内 1.x 与 2.0 接口使用同一版本号发布(`dashscope>=2.0` 即含两者),不做双包分发,避免用户选型困惑。 + +**成本与收益**:代价是包内短期存在两套 HTTP 依赖与部分重复实现(包体积略增、极个别 bug 需修两处);收益是 1.x 回归风险在结构上归零(不存在共享代码,渗透无从发生)、无兼容层开发维护成本、新需求迭代不触碰旧代码、3.0 下线是一次机械删除。参考先例:Azure SDK track1/track2、boto/boto3、openai v0→v1 均为物理隔离式换代。 + +### 2.5 Quickstart 体验(对齐 OpenAI 的 5 行上手) + +```python +import dashscope + +resp = dashscope.chat("qwen-plus", "讲个笑话") # 模块级极简入口(内部单例 Client) +print(resp.output_text) # 一行取文本 +``` + +- `dashscope.chat(model, prompt, **kw)` 为官方 Quickstart 首推写法,文档首页即用此例; +- 进阶用户切到 `dashscope.Client()` 资源式 API,两者返回同一套 Pydantic 模型。 + +### 2.6 命名规范(长期约束) + +**本节是硬约束**:`dashscope/v2/` 下新增任何资源、方法、参数或响应模型都必须遵守,评审时按此逐条核对。规范来自对全量 38 个响应模型 / 90+ 公开方法的横向评审(诊断过程与逐条改名清单见 [`dashscope-2.0-naming-review.md`](dashscope-2.0-naming-review.md),本节只落结论)。 + +命名的唯一目标:**同一个语义在全 SDK 只有一个名字,客户不需要猜**。因此规范以"消灭同义词"为主线,而不是以"取个好名字"为主线。 + +**动词收敛到 8 类**(不允许出现表外动词;表内动词的阻塞语义固定): + +| 语义 | 动词 | 阻塞 | 适用 | +| --- | --- | --- | --- | +| 同步推理 | `create` | 是 | chat / multimodal.chat / multimodal.embeddings / embeddings / rerank / apps.completions | +| 长任务提交 | `jobs.create` | **否** | images / videos / audio.asr / embeddings / fine_tunes / rl | +| 长任务查询·等待 | `jobs.fetch` / `jobs.wait` | 否 / 是 | 同上 | +| 一步式语法糖 | `generate` / `transcribe` | 是 | images / videos / audio.asr(= `create` + `wait`) | +| 持久资源 CRUD | `get` / `list` / `delete` / `cancel` / `scale` | 否 | fine_tunes / deployments / models / files | +| 双工会话 | `recognize` / `connect` | ctx manager | audio.asr / audio.realtime | +| 纯计算·文件 | `count` / `upload` | 是 | tokenization / files | +| 流式 | `stream=True` 或 `.stream()` / `.events()` | — | 见下"流式形态" | + +**阻塞规则(P0)**:`.create()` / `jobs.create()` **永不阻塞**,只提交并返回任务快照;`wait()` 才阻塞。裸领域动词(`generate` / `transcribe`)是 `create + wait` 的语法糖,**必须在 docstring 里明写它会阻塞**,否则客户会把两种 `create` 当成一回事。 + +**入参规范**: + +| 类别 | 规范 | +| --- | --- | +| 模型 | 一律 `model: str`,不允许 `model_name` 之类变体 | +| 任务主键 | 对外统一 `job_id`;服务端字段 `task_id` 保留为双向别名属性 | +| HTTP 超时 | `timeout: Optional[float]`,单次请求;**所有公开方法都必须有,且显式写进签名,不许藏在 `**kwargs` 里**。唯一例外见下"rl 例外" | +| 轮询预算 | `wait()` 上 `timeout` = 等待总预算;阻塞式一步法上 `timeout` = 总预算、提交那一次请求另用 `submit_timeout` | +| 服务端透传 | `parameters=`(并入线缆 parameters 段)/ `extra_input=`(并入 input 段);**禁止** `extra_parameters` 之类第三、第四个同义名 | +| 分页 | `page_no` / `page_size`,默认值全线一致;禁止 `page=` | +| 文件 URL | 多文件端点 `file_urls: List[str]`,单文件端点 `file_url: str`;单复数跟随端点真实入参个数,禁止裸 `url=` | +| 必填主输入 | keyword-only(顶层 `dashscope.chat()` 一行式入口例外) | +| 低频服务端参数 | 收进 `parameters=`,在 docstring 里列常用键,不铺到签名上 | +| 测试注入钩子 | 不进公开签名:用 `_` 前缀(如 `wait(_sleep=...)`)或 `with_options()` | + +**rl 例外**:`client.rl` 的四个 1.x 直通方法(`jobs.create` / `register_functions` / `test_functions` / `upload_data`)按 §2.4 规则 4 复用 1.x 业务逻辑,超时由 1.x 内部控制,**不接受 `timeout=`**。这是全 SDK 唯一的例外,必须在接口文档里显式写明,不能让"所有方法都支持 timeout"成为一句假承诺。 + +**输出字段规范**: + +| 类别 | 规范 | +| --- | --- | +| 时间戳 | `created_at` / `updated_at` / `submitted_at` / `scheduled_at` / `finished_at`;禁止 `gmt_*`、`*_time` 混用 | +| 用量 | 单一 `Usage` 类(`types/common.py`)全 SDK 复用,禁止再造同构 Usage 类或退化成裸 dict | +| 分页 | `page_no` / `page_size` / `total_count`;禁止 `total` | +| 序号 | 一律 `index`;禁止 `text_index` | +| 模型标识 | 一律 `model`;微调产物 `finetuned_model`,部署产物 `deployed_model`;禁止 `model_name` / `finetuned_output` / `version` | +| 任务标识 | 对外 `job_id`,`task_id` 为服务端别名 | +| 错误字段 | 所有顶层响应继承 `ResponseBase`(`request_id` / `status_code` / `code` / `message` / `is_error`)。`is_error` 由 `ResponseBase` 从非空 `code` **自动推导**(与 `errors.response_error` 同一判据),任何资源都不可能忘记设置它 | +| 信封投递 | payload 是 dict 时用 `flatten_envelope(body)` 整体摊平;payload 是列表或 `data` 下的单条目时(`files` / `models`),用 `response_envelope(body)` 把信封字段单独挂到解析出的模型上 | +| 响应形状 | **一律扁平**:服务端的 `output` / `data` 信封在 `_parse` 里拆掉(`flatten_envelope`),客户不写 `.output.` / `.data.` 这一跳 | +| 服务端字段别名 | 对外名与服务端线缆名不同时,用 Pydantic `Field(validation_alias=AliasChoices(对外名, 服务端名))` 做双向可读,**不改线缆报文** | + +**信封规则的三个例外**(都不是顶层 HTTP 响应,故不继承 `ResponseBase`): + +1. `FineTuneEvent` —— SSE 流帧,不是响应;自带 `is_error` 标记错误帧。 +2. `RegisteredFunctions` / `UploadedData` —— `client.rl.register_functions` / `upload_data` 的返回,由 1.x 返回值元组构造,**背后不存在 HTTP 信封**(1.x 失败即抛异常,这也是这两个方法没有 `timeout=` 的同一原因)。给它们加 `request_id` / `code` 只会是永远为空的装饰。 +3. 嵌套条目上的信封字段保持为空:`Deployment` / `File` / `Model` / `FineTune` 既是 `get` 的顶层返回、又是 `*List` 的条目类型("一个形状学一次"),信封只在顶层那份上填充——服务端每次响应报一次,而不是每个条目报一次。 + +**两条硬禁止**: + +1. 公开响应模型上不得出现裸 `Dict[str, Any]` / `Any` 字段。唯一豁免是服务端字段因服务而异、无法类型化的 `Job.output` 与 `FineTuneEvent.output`,且**必须同时提供类型化视图**(`Job.results` / `image_urls` / `video_url` / `result_url`)。 +2. 不得把阿里内部命名习惯泄漏到公开 SDK:`gmt_*`、`is_deleted`、`biz_*` 一类内部 DB / 中台风命名一律改名。例外是**服务端本身就是公开字段名**的情况(如 `apps` 的 `biz_params`、多模态向量的 `res_level`)——此时改名会造成 SDK 与线缆的新不一致,处理方式是在 docstring 里解释语义,而不是改名。 + +**流式形态**:chat 增量文本用 `stream=True` → `ChatStream`(`ChatCompletionChunk`);应用增量输出用 `stream=True` → `AppStream`(`AppCompletion`);训练事件用 `fine_tunes.stream_events()` → `FineTuneEvent`;音频用 `audio.tts.stream()` → `bytes`。websocket 双工会话不属于流式响应,用 `.events()` 迭代服务端事件。流中出错不静默:默认抛异常族,`raise_on_error=False` 时产出一个 `is_error=True` 的对象后结束迭代。 + +**新增资源时的自检顺序**:① 动词是否在 8 类表内、阻塞语义是否与表一致 → ② `timeout=` 是否显式在签名上 → ③ 响应是否继承 `ResponseBase` 且已扁平化 → ④ 时间戳 / 分页 / 序号 / 标识符字段名是否与规范一致 → ⑤ 有没有引入表外同义词 → ⑥ 若按 §2.4 规则 4 复用 1.x 实现,1.x 异常是否已翻译进 v2 异常族(见 §2.7)。 + +### 2.7 错误模型(目录驱动,长期约束) + +**本节是硬约束**:`dashscope/v2/errors.py` 里的异常类名、HTTP 状态与 SDK 侧错误元数据一律**不在 SDK 中手写**,而是从生成的错误目录 `dashscope/v2/_error_registry.py` 派生(`_` 前缀按 v2 约定标记内部模块:它不从 `dashscope/v2/__init__.py` 导出,只被 `errors.py` 与测试引用)。改写背景、逐条决策与验证数据见 [`dashscope-2.0-errors-registry-plan.md`](dashscope-2.0-errors-registry-plan.md)。 + +**目录是唯一事实源**(上游独立仓库 `dashscope-error-registry`,vendored 版本 `1.0.6` / commit `56b7208`): + +| 目录表 | 条数 | SDK 用途 | +| --- | --- | --- | +| `PUBLIC_ERRORS` | 16 | 每条的 `error_code` 就是一个异常类名,`status_code` 就是它的状态码 | +| `SDK_ERRORS` | 32 | 客户端自报错误的 `code` / `solution` / `allow_retry` | + +`_error_registry.py` 是上游 `tools/generate.py` 的产物、文件头即写明 DO NOT EDIT。**这句话的含义是"不得手改目录内容"**——增删错误码、改状态码、改文案都必须回上游 registry 仓库改 `proto/error_registry.proto` 与 `data/*.textproto` 再重新生成,不能在 SDK 里就地打补丁。 + +但它**不是**上游原文的逐字节副本,vendored 这份带有**已登记的本地适配**,整份替换会把它们全部回退,升级时必须逐条重新打上(清单见 registry-plan §1 D1): + +1. 上游第三张表 `InternalError`(277 条服务端组件级错误)**没有 vendor 进来**——它对 SDK 没有投影(proto 里 `InternalError.external` 指回某个 `PublicError.key`,最终还是落到那 16 条),且整份 vendor 会让 `dashscope/v2/` 第一次出现 CJK(388 行)并带来 97 处 black 拆不开的 E501。 +2. `vars` 形参改名 `substitutions` / `variables`——上游用 `vars` 遮蔽内建名,而仓库的 pylint disable 列表里**没有** W0622。 +3. 隐式 Optional 补成显式 `Optional[Dict[str, str]]`。 +4. 按仓库 pin 的 black 23.3.0 / `--line-length=79` 重排。 +5. provenance 从上游独立的 `_version.py` 内联进文件头;文件名加 `_` 前缀。 + +`tests/v2/test_error_registry.py` 钉住其中的文件头、provenance 与裁剪三项(`test_generated_header_survives_vendoring` / `test_catalog_size_and_provenance` / `test_vendored_copy_is_pruned_to_the_tables_v2_consumes`),`format_msg(substitutions=...)` 系列用例钉住第 2 项——**误将上游原文整份覆盖进来会立刻红**。 + +**派生规则**:9 个状态类是 `APIStatusError` 的**扁平兄弟**(互为兄弟、不再是 `InternalServerError` 兜一堆 5xx),`status_code` 逐个从目录读出而非写字面量——目录改状态码,SDK 随之改变,无需改代码。未收录的状态码(418 / 502 / 507…)落 `APIStatusError` 本身,**不猜**成 `InternalServerError`:猜出来的类会让 `except InternalServerError` 抓到并非 500 的失败,且实例的 `status_code`(502)与类声明的状态码(500)互相矛盾。重试判定与此无关——`transport/_retry.py` 按**原始状态码**决定是否重试(408 / 409 / 429 / 500 / 502 / 503 / 504),不看异常类。 + +**三层来源,一个异常族**: + +| 来源 | 判据 | 落到 | `status_code` | +| --- | --- | --- | --- | +| HTTP 层 | 非 2xx 状态码 | `_STATUS_MAP[status]` | 该状态码 | +| 业务层 | HTTP 200 但 body 带 `code` | `_CODE_MAP[code]` | `200` | +| SDK 层 | 根本没有响应 | `SdkError` | `None` | + +`APIStatusError` 因此读作"**网关声明的失败**"(前两层都算),而不是字面的"非 2xx 响应";实例的 `status_code` 说明是哪一层。这也是 `ContentFilterError` 一处 `except` 能同时覆盖两种上报方式的原因。 + +**crosswalk 与目录 key 是两个命名空间**:目录 key(`invalid_api_key`)是标准词表,网关线缆码(`InvalidApiKey`)是百炼的历史事实,两者不互相替代。`_WIRE_TO_PUBLIC` 是 SDK 自己维护的对照表,覆盖全部 13 个 `ErrorCode` 成员;它只决定"这次失败对应**哪条目录项**",**不决定异常类**——类永远由该目录项的 `error_code` 给出。 + +| `ErrorCode`(线缆码) | 目录 key | 异常类 | +| --- | --- | --- | +| `InvalidApiKey` | `invalid_api_key` | `AuthenticationError` | +| `AccessDenied` / `AccessDenied.Unpurchased` / `Arrearage` | `permission_denied` | `PermissionDeniedError` | +| `InvalidParameter` | `invalid_request` | `BadRequestError` | +| `Model.NotExist` | `resource_not_found` | `NotFoundError` | +| `DataInspectionFailed` | `content_policy_violation` | `ContentFilterError` | +| `Throttling` / `.RateQuota` / `.AllocationQuota` | `rate_limit_exceeded` | `RateLimitError` | +| `InternalError` | `internal_error` | `InternalServerError` | +| `ServiceUnavailable` | `service_unavailable` | `ServiceUnavailableError` | +| `GatewayTimeout` | `request_timeout` | `GatewayTimeoutError` | + +`Arrearage`(欠费)走 `permission_denied` 而不是 `insufficient_quota`:欠费是**请求被拒**,不是配额用尽,而目录里 `insufficient_quota` 是 429、意味着可重试。 + +**唯一的细化:`ContentFilterError(BadRequestError)`。** 内容审核在线缆上是 `DataInspectionFailed`、在目录里是 `content_policy_violation` → 400 → `BadRequestError`。V2 需要一个能被单独 catch 的类,做法是**子类化**而非新增目录外状态:`ContentFilterError` 是 `BadRequestError` 的子类,`except BadRequestError` 依然抓得到,目录不变式不破。该不变式由 `test_wire_code_never_leaves_the_catalogued_class` 对 crosswalk 每一行钉住。**将来任何细化都必须走同一条路**:子类化目录已命名的类,不得引入目录里没有的 `status_code`。 + +**`SdkError` 没有状态码,这不是偷懒。** 目录的客户端 SDK 子图里 `SdkError` 只有必填的 `solution`,**没有** `external` 字段指回 `PublicError`——标准本身就没给 SDK 侧错误任何公开投影。所以 `status_code` 保持 `None`;凭空造一个等于给标准里没有的东西加上含义。它的 `code` 是目录名(如 `sdk.agentic_rl.OSSUploadError`),`solution` 是随目录发布的修复建议,另有 `allow_retry` 供调用方自行判断(**不接进 `_retry.py`**:那是 HTTP 层,与 SDK 层不同层)。 + +**1.x 子系统挂载层的翻译义务**:按 §2.4 规则 4 复用 1.x 实现的资源,其 1.x 异常必须翻进 v2 异常族,否则用户写 `except DashScopeError` 抓不到。做法是 `error_from_sdk_exception(exc, family=...)` 两级查目录:`.` 命中就用该条,否则退到 `.Error` 家族兜底。**不嗅探 message 文本、也不嗅探 1.x 的数字 `error_code`**——两者都是 1.x 内部实现细节。捕获范围只取能覆盖该子系统全部异常类的最小基类元组,捕得更宽会把挂载层自己的 bug 贴成 `SdkError`。当前状态:`rl` 已接线(family `sdk.agentic_rl`,8 个直通方法 sync / async 全覆盖);`agentstudio` 待 M3 任务 3.12(其挂载层直接返回 1.x client 对象,需要透明代理才能拦截)。 + +**两级查目录依赖 `.Error` 兜底存在,而 `sdk.agentstudio` 没有这一条。**目录里 `sdk.agentstudio.*` 只有 4 条(`APIConnectionError` / `APITimeoutError` / `StreamError` / `StreamClosedError`),全是非 HTTP 的本地/流层错误;对这 4 条之外的类调用 `error_from_sdk_exception` 会 `KeyError`,反而盖掉原始异常。agentstudio 的 HTTP 层(1.x `APIStatusError` + 8 个子类)自带真实 `status_code`,**属于状态映射、不属于 `SDK_ERRORS`**,要走 `error_from_status`。所以 agentstudio 的翻译是**两条路**,不能照 `rl` 的单路写法。这两个事实已被 `tests/v2/test_errors.py` 与 `tests/v2/test_error_registry.py` 钉死,接线时要改的是用例、不是绕过它们。 + +**新增错误相关代码时的自检顺序**:① 类名与状态码是否从目录派生而非手写 → ② 目录里有没有这一条(没有就去上游 registry 仓库提,**不是**在 SDK 里造) → ③ 细化是否用子类化、有没有逃出目录命名的类 → ④ 有没有泄漏目录外的 `status_code` → ⑤ 新增的线缆码是否进了 crosswalk 且有对应用例 → ⑥ 复用 1.x 的路径是否已翻译。 + +--- + +## 3. 协议兼容层(Anthropic / OpenAI) + +`dashscope/protocols/` 下三个 adapter,实现同一内部接口 `ChatProtocol`: + +| protocol | 请求/响应形态 | 说明 | +| --- | --- | --- | +| `native` | dashscope 自有 HTTP/ws | 全能力,默认值 | +| `openai` | `/v1/chat/completions`(含 SSE) | 对齐 OpenAI SDK 心智;tool_calls、多模态 content parts 直接可映射 | +| `anthropic` | `/v1/messages`(含 SSE) | system 独立字段、`tool_use/tool_result` block 映射 | + +### 3.1 两个方向的兼容 + +1. **出站(客户端)**:`Client(protocol="openai", base_url=...)` 可直接对接任意 OpenAI/Anthropic 兼容端点(含百炼的 compatible-mode 与第三方网关)。参数翻译规则集中在 adapter: + - messages:`role/content` 归一化 → 目标协议格式(system 提取、多模态 parts 转换)。 + - tools:OpenAI `functions` ↔ Anthropic `input_schema` ↔ native `tools`。 + - 采样参数:`temperature/top_p/max_tokens/stop` 直接映射;无对应字段的参数默认透传到请求体(`strict=False`,保证厂商新参数如 reasoning/thinking 不被 SDK 发版节奏卡住),`strict=True` 时对未知参数抛错。 + - 响应:统一翻译回 SDK 的 `ChatCompletion` 对象(含 usage、finish_reason)。 +2. **入站(服务端,可选后置)**:`dashscope serve` 起一个本地 OpenAI/Anthropic 兼容端点,把请求翻译为 native 调用——让存量 OpenAI SDK 用户零改造用 dashscope。此阶段先留接口不实现。 + +### 3.2 与 acli providers 的关系 + +acli 现有 `providers/` 已有多协议接入逻辑,目标仍是"协议翻译收敛到 SDK、避免两处漂移",但**实现路径受同步机制约束**:`dashscope/acli/` 是 agenticCLI 仓库 `src/acli` 的**单向同步副本**(`scripts/sync_acli.sh`,同步时先 `rm -rf` 再拷贝),在副本上做本地修改会被下次同步整体覆盖,且副本一旦依赖 `dashscope.v2` 也无法回流独立仓库 agenticCLI。因此: + +- 协议翻译代码**只下沉到 SDK `v2/protocols/`**(M2 任务 2.2/2.3),2.0 期间**不改同步副本 `dashscope/acli/providers/`**; +- acli 切换为复用 SDK(删除 providers 重复实现)推迟到 **M5 任务 5.1**(底座切换)一次性处理,该改动按"开发与提交约束"单独 commit 并及时 cherry-pick 回 agenticCLI,保持两 repo 一致;3.0 迁移(8.1)时最终收敛。 + +### 3.3 第三方厂商模型(接入百炼)的兼容结论 + +对 MiniMax / DeepSeek / Baseten / Kimi 的调研(详见 `md/vendor-sdk-research-compat.md`)结论:**不需要为任何厂商做专属 adapter**——四家对话协议已全部收敛到 OpenAI 兼容(MiniMax/Baseten 另支持 Anthropic),百炼网关再收敛为 compatible-mode / native 两条通路,§3 的三协议 adapter 天然覆盖。需要做的只是六个通用落点: + +1. **reasoning/thinking 参数**:`extra_body` 透传逃生舱 + 高频参数(`enable_thinking`/`reasoning_effort`)显式化 + 响应模型含 `reasoning_content` 字段(流式单独事件化); +2. **模型目录可发现性**:`client.models.list(provider=...)` 按来源过滤,文档给"厂商 → 百炼 model id"对照表; +3. **错误映射**:`ErrorCode` 枚举覆盖第三方模型常见失败(内容审核/配额/模型不存在); +4. MiniMax 双 Key 形态:百炼统一 key,无需处理; +5. MiniMax 视频/声音克隆等私有能力:若上百炼走 native 异步任务(§2.2 长任务模式),不是协议兼容问题; +6. CLI 示例补厂商特有参数用法(`--extra-body '{"thinking":...}'`)与 compatible-mode/native 选择指引。 + +--- + +## 4. dashscope CLI 2.0 + +**定位**(来自 CLI 对比调研):OpenAI 已于 2026-05 删除 API CLI、Anthropic 从未有过,"SDK 内嵌 API 管理 CLI"品类只剩 DashScope。对标物是 **Codex cloud + aws cli 的混合体**:不与 Agent 类 CLI 比对话体验(那是 expert/acli 的赛道),而是把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成无可替代的差异点;`chat create` 这类一条 curl 可替代的调用仅作便捷入口。 + +### 4.1 命令树(自动映射 SDK 资源树) + +``` +dashscope chat create [--model -m] [--prompt | --messages @file.jsonl] [--stream] [--tools @tools.json] +dashscope multimodal create ... +dashscope images generate | jobs list/get/wait +dashscope videos generate | jobs ... +dashscope embeddings create [--input @texts.txt] +dashscope rerank create --query ... --documents @docs.json +dashscope audio asr stream|transcribe / tts synthesize|stream +dashscope files upload|list|get|delete +dashscope models list|get +dashscope ft create|get|list|stream|cancel|delete (保留现习惯名) +dashscope deployments create|get|list|scale|delete +dashscope rl register-functions|test-functions|upload-data|run|get|list|cancel|logs +dashscope apps call --app-id ... +dashscope config set|get|list (api_key/protocol/base_url 持久化) +dashscope expert ... (转发到 Agentic CLI,见 §5) +``` + +### 4.2 体验改进点(相对现状) + +- **认证命令**:`dashscope auth login/whoami/logout`,whoami 验证 key 有效性并以退出码表达(0=有效),CI 可脚本化探测(对标 `codex login status`)。 +- **Shell 补全**:Typer `add_completion=True`,提供 bash/zsh/fish 补全安装命令(现状 `cli/__init__.py` 显式关闭,一行可修)。 +- **全局参数**:`--api-key -k`、`--protocol`、`--base-url`、`--output json|table|yaml`(全局化,所有命令含 `ft list`/`rl list` 支持 JSON + cursor 分页)。 +- **输入友好**:`--prompt` 直接传文本;`--messages @file` 从文件读;支持管道 `cat msg.json | dashscope chat create -`;图片等本地文件自动经 `oss upload` 换 URL(复用现有 `cli/oss.py`)。 +- **流式输出**:TTY 下渲染增量纯文本(修复现状打印 dict repr 的问题);`--output json` 时每 chunk 一行合法 JSON(NDJSON),可被 jq 消费;**流式中途出错必须 exit 1**(修复现状 `cli/generation.py` 流式分支只打印错误仍 exit 0 的 bug)。 +- **退出码分级**:0 成功 / 1 服务端错误 / 2 认证错误 / 3 参数错误 / 4 限流,脚本免解析 stderr。 +- **任务轮询内置**:`images generate --wait` 一条命令等结果;所有长任务统一 `--wait` + Rich Progress。 +- **配置文件**:`~/.dashscope/config.toml` 存默认 model/workspace/输出格式,`-c key=value` 单次覆盖(优先级:命令行 > 环境变量 > 配置文件)。 +- **隐藏别名保留**:`fine-tunes`→`ft`、`agentic-rl`→`rl`,不破坏存量脚本。 + +实现:Typer 保留;命令层由 codegen(§6)从元数据生成骨架,每个命令只写参数渲染与输出格式化。 + +--- + +## 5. dashscope expert(Agentic CLI) + +基于现有 `dashscope.acli` 演进,不改交互面,只做三层整合: + +1. **底座切换**:acli 整体原样复用(代码量大,2.0 不重写、3.0 随包结构重整迁移,见 §2.4 与后续任务 8.1);仅模型调用从 acli 自有 providers 切到 SDK 2.0 Client(§3 协议兼容自动生效,`/provider`、`/model` 及 `load_config(provider, base_url)` 语义不变)。 +2. **入口与命名**:新增 `dashscope expert` 顶层命令(等价 `dashscope --tui` 等现有启动参数);`run_interactive` SDK 入口保留。 +3. **dashscope 能力即工具**:把 SDK 资源(文生图、ASR/TTS、embedding、文件上传等)注册为 expert 内置 tools/skills,让 agent 一句话调全量 dashscope 能力——这是 expert 相对通用 agentic CLI 的差异点。 +4. **问答 prompt 的 1.x/2.0 区分**(待办,见 M5 任务 5.4):expert 回答 dashscope 相关编码问题时识别代码语境版本,新代码推荐 2.0、存量 1.x 不强行迁移、新能力标注仅 2.0;片段可插拔、2.0 门面落地后才注入。 + +现有命令面(`/session`、`/memory`、`/mcp`、`/tts`、`/cron` 等,见 interfaces 清单第三部分)全部保留,不在本期重构范围。 + +--- + +## 6. 接口自动化生成(阶段三) + +**依据**(见 `md/conclusion-sdk-architecture-stainless.md`):OpenAI/Anthropic SDK 均由 Stainless 从 OpenAPI spec 自动生成(目录结构逐文件同构),spec 变更 → CI 自动重新生成 SDK 发 PR,天级送达。DashScope 手写实现的代际差距(30+ 个 `Any` 参数、手工 kwargs 搬运、SDK/CLI/文档漂移)正是手写 vs 流水线的差距——2.0 追平的路径不是继续手写补丁,而是把百炼 API 沉淀成 spec 接入生成器(Stainless 商业服务,或 oapi-codegen/自研模板)。 + +### 6.1 元数据源 + +`specs/*.yaml`(OpenAPI 3 扩展)描述每个 API:路径、方法、参数 schema、任务型/流式/普通三种形态标记、协议映射注解(openai/anthropic 对应字段)、CLI 命令名与示例。spec 格式**语言无关**:核心字段不含 Python 专属注解,语言特性走 `x-python`/`x-java`/`x-node` 等扩展命名空间,为后续多语言生成留基座(见后续任务 8.3)。 + +### 6.2 生成物 + +| 产物 | 说明 | +| --- | --- | +| SDK 资源方法 | 参数 dataclass + 请求/响应模型 + 同步/异步实现 | +| CLI 命令 | 参数定义 + help 文本 | +| 类型存根 / 文档 | `.pyi` 与 markdown 接口表(替代手写清单) | +| 冒烟测试 | 按元数据中的示例生成参数化用例 | + +`scripts/codegen.py` 一键生成,CI 校验"生成物与元数据一致"。手写覆盖点:复杂流式/实时接口(ws)、需要特殊 UX 的命令,用 `# manual` 标记豁免生成。 + +--- + +## 7. 目录与里程碑 + +### 目标目录布局 + +``` +dashscope/ + __init__.py # 包根导出 2.0 公开面:Client / AsyncClient / chat() + v2/ # 2.0 全部新实现(独立,不与 1.x 共享请求代码) + client.py # Client / AsyncClient + resources/ # chat, images, audio, files, finetunes, rl, agentstudio... + protocols/ # native / openai / anthropic adapter + transport/ # http(sse) / websocket / task polling(httpx) + errors.py # 异常族;类名与状态码从 _error_registry 派生(§2.7) + _error_registry.py # 生成的错误目录,vendored 上游制品,DO NOT EDIT + cli/ # 统一 CLI 命令树:2.0 新命令与 1.x 旧命令共存同一入口 + #(实施调整:不设独立 cli2/ 目录,旧点式命令经映射翻译兼容) + acli/ # expert(改底座,不改交互面) + generation.py, assistants/, threads/, ... # 1.x 旧模块,原样冻结 +specs/ # API 元数据(阶段三) +scripts/codegen.py +tests/ + (现有 1.x 测试原样保留,冻结回归) + v2/ # 2.0 测试 +``` + +### 里程碑(总览) + +| 阶段 | 内容 | 交付 | +| --- | --- | --- | +| M0 | **已移出 V2 范围**:1.x CLI 快修在 1.x 维护分支单独实施,清单见 [`md/m0-1x-cli-hotfix.md`](m0-1x-cli-hotfix.md) | — | +| M1 | SDK 2.0 骨架:Client、transport(httpx)、异常体系 + 错误码枚举 + 自动重试 + 日志改造、chat/embeddings/rerank/files/models 先行,native 协议 | 新 Client 可用,旧接口并存 | +| M2 | 协议层:openai / anthropic adapter + 参数翻译 + 配置 | `protocol=` 生效,兼容端点可直连 | +| M3 | SDK 全量覆盖:多模态/任务型(images/videos)、audio(含 ws)、ft/deployments/rl/agentstudio(Threads 除外:下线) | interfaces 清单 100% 映射(除下线项) | +| M4 | CLI 2.0 + `dashscope config` | 新命令合入 `dashscope/cli/` 单命令树,旧命令映射兼容保留 | +| M5 | expert 整合:acli 底座切 SDK、dashscope 能力注册为 tools | `dashscope expert` 发布 | +| M6 | codegen:specs 元数据 + 生成 SDK/CLI/文档/测试,CI 校验 | 新增接口只改 yaml | +| M7 | 2.0 GA:文档迁移 + 遥测埋点上线,进入 1–2 年新旧并存运营(1.x 冻结、2.0 独立演进);旧代码删除作为遥测门槛项推迟到 3.0 | 2.0 正式,共存机制可运营 | + +### 开发与提交约束 + +- **acli/expert 改动单独 commit**:所有涉及 Agentic CLI 的改动(M5 全部、expert prompt、acli tools)限定在 `dashscope/acli/**`(及其专属测试)范围内单独提交,不与其他模块混提,便于后续 cherry-pick 同步回 agenticCLI 仓库。 +- **acli 是单向同步副本**:`dashscope/acli/` 由 `scripts/sync_acli.sh` 从 agenticCLI 仓库 `src/acli` 同步而来(先删后拷 + import 重写),本地对副本的修改会被下次同步整体覆盖。因此 2.0 期间原则上不改 `dashscope/acli/**`(providers 收敛见 §3.2 / M2 任务 2.5);M5 的必要修改必须单独 commit 并及时 cherry-pick 回 agenticCLI,避免两 repo 长期分叉。 +- 其余模块按任务表粒度提交,每项 ≈ 一次 PR。 + +### 里程碑任务分解(每项 ≈ 一次 PR) + +#### M0 — 已移出本文档 + +1.x CLI 快修清单移至 [`md/m0-1x-cli-hotfix.md`](m0-1x-cli-hotfix.md),在 1.x 维护分支单独实施,不属于 V2 分支工作。 + +#### M1 — SDK 2.0 骨架 + +| # | 任务 | 交付 | +| --- | --- | --- | +| 1.1 | `v2/` 目录骨架 + `Client`/`AsyncClient` 空壳 + 配置解析(参数 > 环境变量 > config.toml) | `dashscope/v2/client.py`、`dashscope/v2/_config.py` | +| 1.2 | transport 层:httpx 同步/异步、连接池、超时分级、自动重试(Retry-After + 指数退避 + 抖动) | `dashscope/v2/transport/` | +| 1.3 | 异常体系 + `ErrorCode` 枚举(含"HTTP 200 业务失败"双层检查)+ `raise_on_error=False` 兼容开关;异常类名与状态码一律从生成的错误目录派生,不手写(§2.7) | `dashscope/v2/errors.py` + vendored `dashscope/v2/_error_registry.py` | +| 1.4 | 日志改造:request_id/状态码/重试原因入日志 + SensitiveHeadersFilter + `DASHSCOPE_LOG` | `dashscope/v2/_logging.py` | +| 1.5 | 内部规范消息模型(messages/content parts 归一定义,多模态预留) | `dashscope/v2/types/messages.py` | +| 1.6 | `client.chat`:Pydantic 响应 + `@overload` + `output_text` + `stream.text_stream/get_final_message` + reasoning_content 字段 | `dashscope/v2/resources/chat/` | +| 1.7 | `client.embeddings` / `client.rerank` / `client.tokenization` | 同上模式 | +| 1.8 | `client.files` / `client.models`(含 `list(provider=...)` 过滤) | 同上模式 | +| 1.9 | 包根导出 2.0 公开面:`dashscope.Client` / `AsyncClient` / `dashscope.chat(model, prompt)`(1.x 旧导出保持不动) | `dashscope/__init__.py` | +| 1.10 | specs 元数据格式原型:chat 接口先行写 yaml(P6:codegen 主线从 M1 起步,M6 接管) | `specs/chat.yaml` | + +#### M2 — 协议兼容层 + +| # | 任务 | 交付 | +| --- | --- | --- | +| 2.1 | `ChatProtocol` 内部接口 + native adapter 接入 v2 transport | `dashscope/v2/protocols/base.py` | +| 2.2 | openai adapter:messages/tools/采样参数翻译 + 未知参数透传(strict 开关) | `dashscope/v2/protocols/openai.py` | +| 2.3 | anthropic adapter:system 提取、tool_use/tool_result 映射 | `dashscope/v2/protocols/anthropic.py` | +| 2.4 | SSE 流式解析统一(error 事件抛异常) | `dashscope/v2/transport/sse.py` | +| 2.5 | acli providers 收敛:**不改同步副本 `dashscope/acli/providers/`**(会被 sync 覆盖且依赖无法回流,见 §3.2)——协议翻译只在 `v2/protocols/` 实现,acli 复用切换推迟到 M5 5.1 | `tests/v2/protocols/`(与 acli providers 行为一致性对照用例) | +| 2.6 | 兼容端点集成测试(百炼 compatible-mode + DeepSeek/Kimi 直连用例) | `tests/v2/protocols/` | + +#### M3 — SDK 全量覆盖 + +| # | 任务 | 交付 | +| --- | --- | --- | +| 3.1 | `client.multimodal.chat`(多模态 content parts) | `v2/resources/multimodal/` | +| 3.2 | 长任务统一基建:`Job` 对象 + `jobs.create/fetch/wait`(退避轮询) | `dashscope/v2/resources/_jobs.py` | +| 3.3 | `client.images` / `client.videos`(generate 同步封装 + jobs 异步);images 双协议路由:wan2.6-t2i / wan2.6-image 走 multimodal-generation 新协议(同步 + image-generation 异步任务),其余模型走旧 text2image 端点(见 §2.2 路由约定);videos 同端点双输入协议路由:wan2.7-i2v 走 media 新 i2v 协议(`media` 类型化素材入参),其余模型走旧 `img_url` 首帧协议 | `v2/resources/images/`、`v2/resources/videos/` | +| 3.4 | `client.audio.asr`:Transcription 批转写 + Recognition 实时 ws(上下文管理器) | `v2/resources/audio/` | +| 3.5 | `client.audio.tts`(一次性 + 流式)+ `client.audio.realtime`(Omni ws);ws 实时类资源(tts/realtime/asr.recognition)仅提供同步面,`AsyncAudio` 只挂 `asr.transcription` | 同上 | +| 3.6 | `client.fine_tunes` + `client.deployments` | `v2/resources/fine_tunes/`、`v2/resources/deployments/` | +| 3.7 | `client.rl`(现 CLI 能力 SDK 化):**复用 1.x 既有实现,不重写**——自有逻辑多,为避免影响 1.x,2.0 只建挂载层并在 errors、底层 HTTP 调用上对齐 v2,业务逻辑一律不动 | `v2/resources/reinforcement_learning.py`(薄挂载层)+ 复用 1.x rl 实现 | +| 3.8 | `client.apps`(Application)+ `client.agentstudio` 并入统一 Client(agents/sessions/events 流式):agentstudio 1.x 代码已较新、与 2.0 接近,**仅合并挂载入 Client,不动原来的代码和逻辑**;errors 与底层 HTTP 调用对齐 2.0 由专门环节统一处理(见任务 3.12) | `v2/resources/apps.py`、agentstudio 挂载入 Client | +| 3.9 | `interfaces 清单 100% 映射`核对表(排除下线项)+ 全量冒烟测试 | `tests/v2/smoke/` | +| 3.10 | `client.embeddings` 缺口补齐:多模态向量化 `create_multimodal`(MultiModalEmbedding)+ 批量任务 `jobs.create/fetch/wait`(BatchTextEmbedding,复用 3.2 Job 基建) | `v2/resources/embeddings.py` | +| 3.11 | 全局配置门面 `dashscope.settings`:单例收敛 api_key / base_url / websocket_base_url / workspace / protocol / timeout / max_retries / log_level,懒读环境变量(访问时读,不 import 时读),setter 即时生效(log_level 直接调 logger.setLevel);`resolve_config` 优先级变为「显式参数 > settings 赋值 > env > config.toml」,覆盖 1.x 模块级变量(`dashscope.api_key` / `base_http_api_url` 等)的运行时配置需求 | `dashscope/v2/_config.py`(Settings)、`dashscope/v2/_logging.py`(set_log_level) | +| 3.12 | errors 对齐专项:2.0 全资源(含 agentstudio、rl 挂载层)的 errors 与底层 HTTP 调用统一对齐 `dashscope.v2.errors` 层级——在挂载层做错误映射包装,不动 1.x 业务逻辑(不与 §2.4 冲突);并核查 `raise_on_error=False` 兼容模式在所有资源的行为一致性。**`rl` 半边已完成**:8 个 1.x 直通方法(sync + async)经 `error_from_sdk_exception` 翻成 `SdkError`,`except DashScopeError` 可捕获(见 §2.7)。**剩 `agentstudio`**:其挂载层直接返回 1.x client 对象,异常在 SDK 边界外抛出,需要透明代理才能拦截。**且只包调用层不够**——流式错误是在 `for event in stream` 迭代中途从生成器体里抛的(`StreamClosedError`、以及生产者线程 transport 异常的 `raise item` 重抛),`stream()` 早已返回,代理拦不到;而流式正是 agentstudio 的主要用法。**"只包调用层"比"完全不包"更糟**:用户会以为 `except DashScopeError` 已覆盖 agentstudio,却在最主要的路径上被漏网。完整成本清单(两条翻译路、无 `.Error` 兜底、11 个资源类 × sync/async 无共同基类、会被打破的现有断言、需要显式声明的 dunder 集合)见 errors registry plan 的 P2b 段 | `v2/resources/` agentstudio 挂载层 | + +#### M4 — CLI 2.0 + +> 实施注记:方案由"新建独立 `cli2/` 目录"调整为"合入 `dashscope/cli/` 单命令树"(上游 1.27.x 已按单树形态发布并经 fd2519e 同步入 V2)。V2 现状 `cli/` 仍构建在 1.x 上,切 v2 底座及 chat/config 等新命令随本阶段任务实施。 + +| # | 任务 | 交付 | +| --- | --- | --- | +| 4.1 | `cli/` 统一命令树骨架:全局参数(-k/--protocol/--base-url/--output)+ 错误着色统一(只 import v2/) | `dashscope/cli/__init__.py` | +| 4.2 | `dashscope config set/get/list`(config.toml 持久化) | `cli/config.py` | +| 4.3 | 生成类命令:chat/multimodal/images/videos/audio(stdin/@file/管道/本地文件自动换 OSS URL) | `cli/` 各命令 | +| 4.4 | 工具类命令:embeddings/rerank/tokenization/files/models/oss | 同上 | +| 4.5 | 长流程命令:ft/deployments/rl(承接习惯名,--wait + Progress + cursor 分页 JSON) | 同上 | +| 4.6 | apps 命令 + `dashscope expert` 转发入口 | 同上 | +| 4.7 | 旧点式命令经 `_COMMAND_MAP`/`_PARAM_MAP` 翻译到新命令、隐藏别名(`ft`/`agentic-rl`)保留,与 2.0 命令并存于同一 `dashscope` 入口;新命令 `--help` 补场景化示例 | 全命令 | + +#### M5 — expert 整合(本阶段全部任务按"开发与提交约束"单独 commit,限定 `dashscope/acli/**`) + +| # | 任务 | 交付 | +| --- | --- | --- | +| 5.1 | acli 模型调用底座切 SDK Client(`load_config`/`/provider`/`/model` 语义不变);含 providers 协议翻译切换复用 `v2/protocols/`(2.0 期间对同步副本的唯一改动点,见 §3.2,单独 commit 并回流 agenticCLI) | `acli/agent.py` 等 | +| 5.2 | dashscope 能力注册为内置 tools(images/tts/asr/embeddings/files 等) | `acli/tools/dashscope_*` | +| 5.3 | `dashscope expert` 命令 + 文档;回归 acli 全量斜杠命令 | — | +| 5.4 | expert 问答 prompt 策略:识别 1.x/2.0 语境(`Generation.call`→1.x、`dashscope.Client`→2.0);新代码一律推荐 2.0 写法;存量 1.x 代码修 bug 时保持原风格、不强行迁移;新参数/新能力标注仅 2.0。做成可插拔片段,2.0 门面可用后(M1+)才注入,1.x↔2.0 对照表放可检索文档而非 prompt 内 | `acli/prompt.py` 片段 + 知识文档 | + +#### M6 — codegen + +| # | 任务 | 交付 | +| --- | --- | --- | +| 6.1 | `specs/*.yaml` 元数据格式定义(含任务型/流式标记、协议映射注解) | `specs/schema` | +| 6.2 | 存量接口逆向成 spec(先 chat/files/models 三个试点) | `specs/*.yaml` | +| 6.3 | 生成器:SDK 资源方法 + Pydantic 模型 | `scripts/codegen.py` | +| 6.4 | 生成器:CLI 命令骨架 + help | 同上 | +| 6.5 | 生成器:文档接口表 + 冒烟测试;`# manual` 豁免机制 | 同上 | +| 6.6 | CI 校验"生成物与 spec 一致" | `.github/`/CI | + +#### M7 — 2.0 GA 与共存运营 + +| # | 任务 | 交付 | +| --- | --- | --- | +| 7.1 | 迁移指南(1.x → 2.0 对照表)+ 1.x/2.0 双轨文档体系 | docs ✅ [`dashscope-2.0-migration-guide.md`](dashscope-2.0-migration-guide.md)(对照表另见 `dashscope-2.0-interfaces-mapping.md`) | +| 7.2 | 现有 1.x 测试套件原样接入 CI 作冻结回归(不改不扩);2.0 全量测试新建于 `tests/v2/` | `tests/v2/` | +| 7.3 | 遥测埋点:旧接口调用占比(脱敏)+ 季度评估机制 | 核心 + dashboard | +| 7.4 | README/Quickstart 全面改写(以 `dashscope.chat` 为首例) | docs | +| 7.5 | 版本号切 2.0.0 发布 + changelog(明示共存策略与兼容承诺) | release | + +#### 后续(遥测门槛触发,不设死线) + +| # | 任务 | 触发条件 | +| --- | --- | --- | +| 8.1 | 3.0 包结构重整:整目录删除 1.x 旧代码(旧模块 + `cli/` 旧命令树)、`v2/` 内容上移为包主体、acli 迁移适配(import 从 `dashscope.v2.*` 切至包根;与 agenticCLI 仓库同步,单独 commit) | 旧接口调用占比 <5% 且无大客户依赖 | +| 8.2 | `dashscope serve`(入站兼容端点) | M2 后评估,视用户需求立项 | +| 8.3 | 多语言 codegen(Java 优先,其次 Node.js/Go):基于同一 `specs/*.yaml` 生成;过渡期 Java 仓库先接"CI 对照 spec 一致性校验"止血漂移 | M6 稳定后立项 | + +### 风险与决策点 + +- **双依赖并存**:零接触方案的代价是 requests/aiohttp(1.x)与 httpx(2.0)同时安装,包体积与依赖面增大;不做 extras 拆分(避免用户选型困惑),3.0 删旧代码时自然消除。 +- **重复实现的双修风险**:同一服务端 bug 若同时命中新旧两套代码需修两处;因 1.x 冻结、仅修安全/严重 bug,概率低且可控。 +- **"不优雅"的静态成本**:包内短期并存两套实现是刻意为之的权衡——换来的是 1.x 回归风险结构性归零与零兼容层成本;3.0 一次性删除收尾。 +- **多模态消息格式**:native 与 OpenAI content parts 差异最大的地方,M1 前需先定内部规范消息模型。 +- **ws 实时接口与协议层的关系**:Recognition/OmniRealtime 无 OpenAI/Anthropic 对应物,协议层仅覆盖 HTTP 文本类接口,需在文档明示。 +- **`dashscope serve`(入站兼容)**是否做、何时做,待 M2 后评估。 +- **expert 的包归属**:保持 `dashscope.acli` 模块名不变(存量用户脚本兼容),仅在命令层加 `expert` 入口。 +- **多语言 SDK 迭代不同步**(现状:Java 独立 repo、手写维护,接口迭代漂移):2.0 不新建语言生成器,靠 spec 单一事实源 + CI 一致性校验先收敛;多语言生成列入后续(8.3),避免本次迭代范围失控。 diff --git a/md/dashscope-2.0-design.v1.bak.md b/md/dashscope-2.0-design.v1.bak.md new file mode 100644 index 00000000..d55ceec7 --- /dev/null +++ b/md/dashscope-2.0-design.v1.bak.md @@ -0,0 +1,230 @@ +# dashscope 2.0 设计方案 + +> 基于 V2 分支现状(见 `md/dashscope-interfaces.md` 全量接口清单),规划 dashscope 2.0。 + +## 0. 目标与原则 + +1. **用户接口友好**:一个现代化的 SDK + CLI(`dashscope`),以及一个 Agentic CLI(`dashscope expert`)。 +2. **协议兼容**:通过配置或传入 config,兼容 Anthropic / OpenAI 协议(既可作为客户端消费这两种协议,也保证 dashscope 自身能力可按这两种协议表达)。 +3. **接口自动化生成**(后置):SDK / CLI / 文档 / 测试从单一 API 元数据源自动生成,人工只维护元数据与少量手写覆盖。 + +现状问题(为什么要 2.0): + +- SDK 接口形态不统一:`Generation.call`、`AioGeneration.call`、`ImageSynthesis.async_call/fetch/wait` 三套风格并存,参数散落在 `**kw`,无统一 Client 对象。 +- CLI 与 SDK 一一映射、命令冗长(`dashscope multimodal_conversation create`),缺少面向场景的友好命令。 +- 协议层只有自有 `protocol/websocket.py`,OpenAI/Anthropic 兼容散落在 acli 的 providers 中,未沉淀为 SDK 能力。 +- 接口靠手写,SDK/CLI/文档三处重复劳动,易漂移。 + +--- + +## 1. 总体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ dashscope expert(Agentic CLI,演进自 dashscope.acli) │ +│ TUI/REPL、session、memory、mcp、tools、skills │ +├─────────────────────────────────────────────────────────────┤ +│ dashscope CLI(dashscope.cli 重写) │ +│ 命令树 = SDK 资源树的自动映射 + 场景化快捷命令 │ +├─────────────────────────────────────────────────────────────┤ +│ dashscope SDK 2.0(dashscope 包根) │ +│ Client 资源式 API(chat/images/audio/files/finetunes/...) │ +├──────────────────────┬──────────────────────────────────────┤ +│ Protocol 适配层 │ Transport 层 │ +│ openai / anthropic │ HTTP(SSE) / WebSocket / 任务轮询 │ +│ / dashscope-native │ │ +├──────────────────────┴──────────────────────────────────────┤ +│ API 元数据(specs/*.yaml)→ codegen → SDK/CLI/文档/测试 │ +└─────────────────────────────────────────────────────────────┘ +``` + +分层原则:expert 只用 SDK 公开 API;CLI 是 SDK 的薄壳;协议与传输在 SDK 内部隔离,上层无感。 + +--- + +## 2. SDK 2.0 设计 + +### 2.1 统一入口:资源式 Client + +对齐 OpenAI/Anthropic 的用户心智,一个 `Client` 对象挂所有资源;同步 + 异步双版本: + +```python +import dashscope + +client = dashscope.Client(api_key=..., protocol="openai" | "anthropic" | "native", base_url=...) + +resp = client.chat.completions.create(model="qwen-plus", messages=[...], stream=True, tools=[...]) +img = client.images.generate(model="wanx2.1-t2i", prompt="...", n=1, size="1024*1024") +aclient = dashscope.AsyncClient(...) +``` + +### 2.2 资源树与现有接口映射 + +| 新资源 | 覆盖现有接口(见 interfaces 清单) | 关键方法 | +| --- | --- | --- | +| `client.chat` | `Generation` / `AioGeneration` / `Conversation` / `CodeGeneration` / `Understanding` | `completions.create`(messages 统一格式,tools/stream 一等参数) | +| `client.apps` | `Application` | `completions.create(app_id, prompt)` | +| `client.multimodal` | `MultiModalConversation` / `AioMultiModalConversation` | `chat.create`(content parts:text/image_url/audio/video) | +| `client.images` | `ImageSynthesis` | `generate`(同步封装)/ `jobs.create/fetch/wait`(异步任务) | +| `client.videos` | `VideoSynthesis` | 同上模式 | +| `client.embeddings` | `TextEmbedding` / `MultiModalEmbedding` / `BatchTextEmbedding` | `create` / `jobs.*`(批量任务) | +| `client.rerank` | `TextReRank` / `AioTextReRank` | `create(query, documents)` | +| `client.tokenization` | `Tokenization` | `count(prompt)` | +| `client.audio.asr` | `Recognition` / `Transcription` / `QwenTranscription` | `stream`(实时 ws)/ `transcribe(file_urls)` | +| `client.audio.tts` | `SpeechSynthesizer` / `tts_v2` / `HttpSpeechSynthesizer` / `qwen_tts` | `synthesize`(一次性)/ `stream`(流式) | +| `client.audio.realtime` | `OmniRealtimeConversation` | `session()`(ws 会话上下文管理器) | +| `client.fine_tunes` | `FineTunes` | `create/get/list/cancel/delete/stream_events/wait` | +| `client.deployments` | `Deployments` | `create/get/list/scale/delete` | +| `client.files` | `Files` | `upload/list/get/delete` | +| `client.models` | `Models` | `list/get` | +| `client.rl` | (CLI 的 `dashscope rl` 能力 SDK 化) | `register_functions/test_functions/upload_data/run/get/cancel/logs/list` | +| `client.beta.assistants` | `Assistants/Threads/Messages/Runs/Steps` | OpenAI Assistants 风格 CRUD(保持 `beta` 命名空间) | + +约定: + +- **长任务统一模式**:`jobs.create → fetch/wait → result`,`wait` 内置退避轮询;所有任务资源返回统一的 `Job` 对象(`id/status/result/error`)。 +- **流式统一模式**:`stream=True` 一律返回迭代器/异步迭代器,事件对象统一 `event_type + data`;ws 实时接口用上下文管理器。 +- **类型化**:入参出参全部 dataclass/pydantic,`**kw` 仅保留 `extra_headers/timeout` 等逃逸舱;提供 `.pyi`。 +- **错误模型**:统一 `DashScopeError` 层级(AuthError/RateLimitError/TaskFailedError...),带 `request_id`。 + +### 2.3 配置 + +```python +dashscope.Client(api_key=..., base_url=..., protocol=..., timeout=..., max_retries=...) +``` + +优先级:显式参数 > 环境变量(`DASHSCOPE_API_KEY` 等) > `~/.dashscope/config.toml`。保留 `dashscope.api_key` 等模块级全局量作为 1.x 兼容层(deprecate 标注)。 + +### 2.4 兼容性 + +- 包根继续导出旧接口(`Generation.call` 等),内部转调新 Client,打 DeprecationWarning;两个 minor 版本后移除。 +- `dashscope 2.0` 以新 Client 为唯一推荐入口。 + +--- + +## 3. 协议兼容层(Anthropic / OpenAI) + +`dashscope/protocols/` 下三个 adapter,实现同一内部接口 `ChatProtocol`: + +| protocol | 请求/响应形态 | 说明 | +| --- | --- | --- | +| `native` | dashscope 自有 HTTP/ws | 全能力,默认值 | +| `openai` | `/v1/chat/completions`(含 SSE) | 对齐 OpenAI SDK 心智;tool_calls、多模态 content parts 直接可映射 | +| `anthropic` | `/v1/messages`(含 SSE) | system 独立字段、`tool_use/tool_result` block 映射 | + +### 3.1 两个方向的兼容 + +1. **出站(客户端)**:`Client(protocol="openai", base_url=...)` 可直接对接任意 OpenAI/Anthropic 兼容端点(含百炼的 compatible-mode 与第三方网关)。参数翻译规则集中在 adapter: + - messages:`role/content` 归一化 → 目标协议格式(system 提取、多模态 parts 转换)。 + - tools:OpenAI `functions` ↔ Anthropic `input_schema` ↔ native `tools`。 + - 采样参数:`temperature/top_p/max_tokens/stop` 直接映射;不支持的参数按 `strict=False`(默认)静默丢弃、`strict=True` 抛错。 + - 响应:统一翻译回 SDK 的 `ChatCompletion` 对象(含 usage、finish_reason)。 +2. **入站(服务端,可选后置)**:`dashscope serve` 起一个本地 OpenAI/Anthropic 兼容端点,把请求翻译为 native 调用——让存量 OpenAI SDK 用户零改造用 dashscope。此阶段先留接口不实现。 + +### 3.2 与 acli providers 的关系 + +acli 现有 `providers/` 已有多协议接入逻辑:抽取其中的协议翻译代码下沉到 SDK `protocols/`,acli 改为复用 SDK,避免两处实现漂移。 + +--- + +## 4. dashscope CLI 2.0 + +### 4.1 命令树(自动映射 SDK 资源树) + +``` +dashscope chat create [--model -m] [--prompt | --messages @file.jsonl] [--stream] [--tools @tools.json] +dashscope multimodal create ... +dashscope images generate | jobs list/get/wait +dashscope videos generate | jobs ... +dashscope embeddings create [--input @texts.txt] +dashscope rerank create --query ... --documents @docs.json +dashscope audio asr stream|transcribe / tts synthesize|stream +dashscope files upload|list|get|delete +dashscope models list|get +dashscope ft create|get|list|stream|cancel|delete (保留现习惯名) +dashscope deployments create|get|list|scale|delete +dashscope rl register-functions|test-functions|upload-data|run|get|list|cancel|logs +dashscope apps call --app-id ... +dashscope assist ...(beta) +dashscope config set|get|list (api_key/protocol/base_url 持久化) +dashscope expert ... (转发到 Agentic CLI,见 §5) +``` + +### 4.2 体验改进点(相对现状) + +- **全局参数**:`--api-key -k`、`--protocol`、`--base-url`、`--output json|table|yaml`、`--json`(机器可读输出,供 jq/脚本)。 +- **输入友好**:`--prompt` 直接传文本;`--messages @file` 从文件读;支持管道 `cat msg.json | dashscope chat create -`;图片等本地文件自动经 `oss upload` 换 URL(复用现有 `cli/oss.py`)。 +- **流式输出**:TTY 下默认渲染流式文本,`--no-stream` 关闭。 +- **任务轮询内置**:`images generate --wait` 一条命令等结果。 +- **隐藏别名保留**:`fine-tunes`→`ft`、`agentic-rl`→`rl`,不破坏存量脚本。 + +实现:Typer 保留;命令层由 codegen(§6)从元数据生成骨架,每个命令只写参数渲染与输出格式化。 + +--- + +## 5. dashscope expert(Agentic CLI) + +基于现有 `dashscope.acli` 演进,不改交互面,只做三层整合: + +1. **底座切换**:模型调用从 acli 自有 providers 切到 SDK 2.0 Client(§3 协议兼容自动生效,`/provider`、`/model` 及 `load_config(provider, base_url)` 语义不变)。 +2. **入口与命名**:新增 `dashscope expert` 顶层命令(等价 `dashscope --tui` 等现有启动参数);`run_interactive` SDK 入口保留。 +3. **dashscope 能力即工具**:把 SDK 资源(文生图、ASR/TTS、embedding、文件上传等)注册为 expert 内置 tools/skills,让 agent 一句话调全量 dashscope 能力——这是 expert 相对通用 agentic CLI 的差异点。 + +现有命令面(`/session`、`/memory`、`/mcp`、`/tts`、`/cron` 等,见 interfaces 清单第三部分)全部保留,不在本期重构范围。 + +--- + +## 6. 接口自动化生成(阶段三) + +### 6.1 元数据源 + +`specs/*.yaml`(OpenAPI 3 扩展)描述每个 API:路径、方法、参数 schema、任务型/流式/普通三种形态标记、协议映射注解(openai/anthropic 对应字段)、CLI 命令名与示例。 + +### 6.2 生成物 + +| 产物 | 说明 | +| --- | --- | +| SDK 资源方法 | 参数 dataclass + 请求/响应模型 + 同步/异步实现 | +| CLI 命令 | 参数定义 + help 文本 | +| 类型存根 / 文档 | `.pyi` 与 markdown 接口表(替代手写清单) | +| 冒烟测试 | 按元数据中的示例生成参数化用例 | + +`scripts/codegen.py` 一键生成,CI 校验"生成物与元数据一致"。手写覆盖点:复杂流式/实时接口(ws)、需要特殊 UX 的命令,用 `# manual` 标记豁免生成。 + +--- + +## 7. 目录与里程碑 + +### 目标目录布局 + +``` +dashscope/ + client.py # Client / AsyncClient + resources/ # chat, images, audio, files, finetunes, rl, assist... + protocols/ # native / openai / anthropic adapter + transport/ # http(sse) / websocket / task polling + cli/ # CLI 2.0(生成 + 手写渲染) + acli/ # expert(改底座,不改交互面) + compat/ # 1.x 旧接口兼容层 +specs/ # API 元数据(阶段三) +scripts/codegen.py +``` + +### 里程碑 + +| 阶段 | 内容 | 交付 | +| --- | --- | --- | +| M1 | SDK 2.0 骨架:Client、transport、chat/embeddings/rerank/files/models 先行,native 协议 | 新 Client 可用,旧接口并存 | +| M2 | 协议层:openai / anthropic adapter + 参数翻译 + 配置 | `protocol=` 生效,兼容端点可直连 | +| M3 | SDK 全量覆盖:多模态/任务型(images/videos)、audio(含 ws)、ft/deployments/rl/assistants | interfaces 清单 100% 映射 | +| M4 | CLI 2.0 + `dashscope config` | 新命令树,旧命令保留别名 | +| M5 | expert 整合:acli 底座切 SDK、dashscope 能力注册为 tools | `dashscope expert` 发布 | +| M6 | codegen:specs 元数据 + 生成 SDK/CLI/文档/测试,CI 校验 | 新增接口只改 yaml | +| M7 | 1.x 兼容层下线、文档迁移 | 2.0 正式 | + +### 风险与决策点 + +- **多模态消息格式**:native 与 OpenAI content parts 差异最大的地方,M1 前需先定内部规范消息模型。 +- **ws 实时接口与协议层的关系**:Recognition/OmniRealtime 无 OpenAI/Anthropic 对应物,协议层仅覆盖 HTTP 文本类接口,需在文档明示。 +- **`dashscope serve`(入站兼容)**是否做、何时做,待 M2 后评估。 +- **expert 的包归属**:保持 `dashscope.acli` 模块名不变(存量用户脚本兼容),仅在命令层加 `expert` 入口。 diff --git a/md/dashscope-2.0-errors-registry-plan.md b/md/dashscope-2.0-errors-registry-plan.md new file mode 100644 index 00000000..400a9075 --- /dev/null +++ b/md/dashscope-2.0-errors-registry-plan.md @@ -0,0 +1,882 @@ +# V2 errors 层按 `error_registry.py` 标准改写计划 + +> **前提(三条硬边界)** +> 1. **V2 分支是接口标准**:资源树、方法名、模型名、`ResponseBase` 信封一律不动。 +> 2. **只修 errors 相关**:`errors.py` 及其消费者,其余不碰。 +> 3. **不参考 `V2-errors` 分支的实现**:唯一输入是 `error_registry.py` 这个目录文件本身 +> (新 errors 标准)+ V2 现有代码。因此本计划**不做分支合并**, +> `md/dashscope-2.0-errors-merge-plan.md` 的合并路线已被本计划取代。 +> 4. **不需要向后兼容**:V2 是全新版本,尚未发布、无存量用户。 +> 异常继承关系只为**语义正确**服务,不为"存量 `except` 子句继续生效"服务; +> 不做兼容别名、不做 deprecation shim;既有测试**允许随设计一起改**。 +> (本条推翻了本文早先以兼容性为依据的 D3 / D5 / §5 / §6,下文已按此重写。) + +--- + +## 1. 输入:registry 到底约定了什么 + +`error_registry.py`(vendored 到 `dashscope/v2/_error_registry.py`,649 行) +是**生成制品**,顶部带 provenance: + +```python +__version__ = "1.0.6" +__commit__ = "56b7208" +__snapshot__ = False +``` + +两张表、两个 frozen dataclass: + +**`PublicError` ×16**(`PUBLIC_ERRORS`) + +| 字段 | 含义 | 对 V2 的约束 | +| --- | --- | --- | +| `key` | 内部规范键(`invalid_request` / `auth_failed` / …) | **不是**网关下发的 wire code | +| `status_code` | 400/401/403/404/**413**/429/500/**503**/**504** | V2 必须每个都能落类 | +| `error_code` | **异常类名**(字符串) | 类名一字不差由目录决定 | +| `error_msg` | 默认文案,带 `{parameter}` / `{resource}` / `{limit}` 占位 | 配 `format_msg()` | +| `anthropic_error_code` | `invalid_request_error` / `authentication_error` / `permission_error` / `not_found_error` / `request_too_large` / `rate_limit_error` / `billing_error` / `api_error` / `overloaded_error` / `timeout_error` | anthropic 兼容协议的错误形状 | + +`error_code` 一共要求 **9 个异常类**: +`BadRequestError` `AuthenticationError` `PermissionDeniedError` `NotFoundError` +`RequestTooLargeError` `RateLimitError` `InternalServerError` +`ServiceUnavailableError` `GatewayTimeoutError` + +**`SdkErrorDef` ×32**(`SDK_ERRORS`,1.0.5 里叫 `INTERNAL_ERRORS`):`name` +(`sdk.InvalidApiKey` ×1 / +`sdk.agentic_rl.*` ×27 / `sdk.agentstudio.*` ×4)、`message`、`solution`、 +`allow_retry`、`vars`,配 `format_message()`。 +**注意:这张表没有 status_code,也没有异常类名**——本地/SDK 侧失败,不是 HTTP 响应。 + +### 1.1 最关键的一条约束(决定整体设计) + +**`PublicError` 没有 wire code 字段。** 所以: + +- `_STATUS_MAP`(HTTP 状态 → 异常类)**可以完全由目录推导** ✅ +- `_CODE_MAP`(网关 wire code → 异常类)**无法由目录推导** ❌ + ——`Throttling.RateQuota`、`DataInspectionFailed`、`Model.NotExist` 这些 + 真实下发码到规范键的对应关系,是 V2 自己的知识,目录里没有。 + +结论:改写后 V2 要**新增一份显式 crosswalk**(wire code → catalog entry), +它是手维护的,但**类选择仍然由目录驱动**(crosswalk 只负责选到哪个 +`PublicError`,异常类和状态码都从该条目取)。这份 crosswalk 必须配完整性测试, +否则它会像现在的 `_CODE_MAP` 一样悄悄漏项(见 §2 差距 4)。 + +### 1.2 第二条决定性事实:32 条 `sdk.*` 只有 23 条可由异常类型到达 + +已逐条核对 `SDK_ERRORS.name` 的后缀与 1.x 实际存在的异常类: + +| 分组 | 条数 | 可由 `type(exc).__name__` 到达 | +| --- | --- | --- | +| `sdk.agentstudio.*` | 4 | **4/4**(`dashscope/agentstudio/exceptions.py:75,81,135,141`) | +| `sdk.agentic_rl.*` | 27 | **18/27** | +| `sdk.InvalidApiKey` | 1 | 由 v2 挂载层自己抛,不依赖 1.x 类 | + +`dashscope/finetune/reinforcement/common/errors.py` 里**不存在**这 9 个类: + +- 8 条是**操作结果条件**,不是异常类型:`FunctionRegistrationFailed` + `DatasetsUploadFailed` `DuplicateFunctionNames` `JobSubmissionFailed` + `WorkflowFailed` `UnsupportedFunctionType` `FunctionTestFailed` + `FunctionTestTimeout`。1.x 是用**通用类 + 具体消息 + 数字 `error_code`** + 表达它们的,例如"Duplicate function names detected"由 + `common/model.py:1589` 抛 `ValidationError`,`InputError(f"...", error_code=4011)` + (`common/utils.py:187`)。 +- 1 条是 `sdk.agentic_rl.Error`:1.x 对应的基类叫 `AgenticRLError` + (`common/errors.py:35`),**不叫 `Error`**。 + +后果(决定 D6): + +1. **不能**声称"类名与目录后缀逐条对得上"——早先版本的 D6 这么写,是错的。 +2. 类型推导只覆盖 18 + 4 = 22 条;`AgenticRLError` 及其未收录子类要有 + **家族级兜底**(落到 `sdk.agentic_rl.Error`)。 +3. 那 8 条条件级条目**不要**靠嗅探 message 文本或数字 `error_code` 去反推—— + 脆弱、且属于 1.x 内部编码。它们在 V2 侧就是**目录有、代码不产出**的条目 + (目录是跨 SDK 共享制品,存在不可达条目是正常的)。 +4. 因此 P0 的完整性测试方向必须反过来:**不是**"每条 `name` 都能找到 1.x 类" + (会红),而是"每个 v2 挂载层可能捕获到的 1.x 类,都能在目录里找到落点或兜底"。 + +### 1.3 第三条输入约束:`SdkErrorDef` 不带 `external`,**不要照 `InternalErrorDef` 补** + +曾考虑把 `SdkErrorDef` 改成 1.0.6 里 `InternalErrorDef` 的形状(多一个 +`external: PublicError` 和 `cause: str`),让 sdk.* 也能投影到公开错误。 +**结论:不改。** 这个不对称是上游 schema 层面的刻意设计,不是生成器漏了字段: + +| 证据 | 位置 | 内容 | +| --- | --- | --- | +| proto 里两个 message 本就不同 | `proto/error_registry.proto:25-32` vs `:41-53` | `InternalError` 有 `string external = 2; // Reference to PublicError.key`;`SdkError` **没有 `external`**,改成 `solution` 必填 + 四个分语言 `solution_python/java/go/cpp` | +| 三层模型把两者分在不同子图 | `README.md:9-20` 的 mermaid | `InternalError → external → PublicError` 全在 **服务端** 子图里;`SdkError → solution` 单独待在 **客户端SDK** 子图,**没有连向 PublicError 的边** | +| 措辞明确 | `README.md:24` | SdkError 是"客户端 SDK **独立**错误码,包含 `solution`(**必填**修复建议)" | +| 校验只针对 InternalError | `tools/generate.py` 的 `main()` | 任何 internal error 的 `external` 不在已知 public key 里就 `sys.exit(1)`;**sdk error 没有任何等价校验**,因为 schema 不给它这个字段 | +| 字段顺序也对得上 | 生成制品 | 生成的 `SdkErrorDef(name, message, solution, allow_retry, vars)` 与 `InternalErrorDef(name, external, message, allow_retry, vars, cause, solution)` 的差异逐字对应各自 proto 的字段序,说明是 schema 决定的,不是生成器随手排的 | + +语义上也讲得通:`external` 描述的是"服务端组件级错误对外暴露成哪个公开错误码", +它的前提是**存在一个 HTTP 响应**要按公开码下发;sdk.* 全是本地/客户端侧失败 +(配置缺失、上传失败、函数注册失败……),**根本没有响应可下发**, +所以没有公开投影可言,只有 `solution` 这种"给人看的修复建议"。 + +对 D6 的影响(重要):**`SdkError.status_code = None` 不是本地偷懒, +而是目录给出的唯一忠实读法。** 目录里没有为 sdk.* 定义任何公开投影, +V2 若自己编一个状态码,等于在生成制品之外私加语义。 + +若将来确实需要 sdk.* 的公开投影(比如要让 `rl` 失败也能进统一的可观测口径), +正确做法是**向上游 registry 仓库提 RFC 加一个 optional `external`**, +由 proto + textproto 生成,而不是改 `_error_registry.py` 这个标了 +`DO NOT EDIT` 的制品——手改会让下次重新生成时静默丢失。 + +--- + +## 2. V2 errors 现状与差距(逐条已核实) + +> **这是改写前的基线**:下表 `errors.py` 的行号一律指 commit `d2088de` 之前的 +> 那份文件,改写后已不对应任何现有代码。差距 1–5、8、10 的闭环见 §5 与 §6, +> 差距 6、7、9 按 §7 决策 1/2/4 **仍不接线**。 + +| # | 差距 | 证据 | 后果 | +| --- | --- | --- | --- | +| 1 | 缺 3 个目录要求的异常类 | `errors.py:82-107` 只有 6 个 `APIStatusError` 子类 | 413/503/504 无处落 | +| 2 | `_STATUS_MAP` 缺 413/500/503/504 | `errors.py:110-116` 只有 400/401/403/404/429 | 413 落到裸 `APIStatusError`;503/504 靠 `>=500` 兜底成 `InternalServerError`,**无法区分** | +| 3 | `status_code` 是硬编码字面量 | `errors.py:83,87,91,95,99,103` | 目录改值,代码不跟随(正是"标准"要消除的) | +| 4 | `_CODE_MAP` 漏掉 2 个已声明的 wire code | `ErrorCode.INVALID_PARAMETER`(:24)、`MODEL_NOT_EXIST`(:25) 不在 `_CODE_MAP`(:118-130) | `InvalidParameter` 只能靠状态码兜底;`Model.NotExist` **不会**变成 `NotFoundError` | +| 5 | 503/504 被压平 | `errors.py:128-129` 都指向 `InternalServerError` | 用户无法按类区分服务不可用与网关超时 | +| 6 | `anthropic_error_code` 无消费者 | `protocols/anthropic.py` 只 import `BadRequestError, DashScopeError, ErrorCode`,全文无错误体输出 | anthropic 兼容协议不产出 anthropic 形状的错误 | +| 7 | `allow_retry` 无消费者 | `transport/_retry.py` 的 `RETRYABLE_STATUS_CODES` 只看 HTTP 状态 | sdk.* 错误的可重试性信息丢失 | +| 8 | **rl / agentstudio 的 1.x 异常裸奔** | `reinforcement_learning.py` 467 行 **0 个 `except`**;1.x 抛 `ValueErrorWithCode` / `RuntimeErrorWithCode` / `RegistrationError` / `DatasetsError` / `ValidationError`(定义在 `dashscope/finetune/reinforcement/common/errors.py`) | `except DashScopeError` **抓不到** rl 失败;32 条 `sdk.*` 目录无人使用 | +| 9 | 占位符文案未使用 | 目录 `format_msg()` 支持 `{parameter}` 等 | 默认消息不含具体参数名/资源名 | +| 10 | 无目录完整性测试 | `tests/v2/` 无对应文件 | 目录与异常类脱钩不会被发现 | + +差距 8 是里面唯一的**功能性缺陷**(不是精度问题):rl 是 1.x 直通层, +用户拿到的异常不在 2.0 异常族里。差距 1–5、10 是"按标准改写"的主体。 +差距 6、7、9 是目录里已有、V2 尚未接线的字段(见 §7 决策项)。 +**其中差距 9 已于 P3 接线**(见 §7 决策 4),差距 6、7 仍按决策 1/2 不接。 + +--- + +## 3. 目标设计 + +**D1|registry 以生成制品身份落地,provenance = 上游 1.0.6。** + +已落地:`dashscope/v2/_error_registry.py`,649 行,最长行 78 字符, +**零非 ASCII**,flake8 / black(79) / pylint 全绿(pylint 10.00/10)。 +`git hash-object` = `908fbf161dae98c2e2bae5337671fdb533b352a8`。 + +文件头:首行与上游逐字一致,provenance 三行是上游独立 `_version.py` 的 +内容内联进来(适配 4): + +```python +"""Auto-generated by tools/generate.py -- DO NOT EDIT.""" +... +__version__ = "1.0.6" +__commit__ = "56b7208" +__snapshot__ = False +``` + +权威源是**独立仓库** `gitlab.alibaba-inc.com/dashscope/dashscope-error-registry` +(`tools/generate.py` + `proto/error_registry.proto` + `data/*.textproto`, +产出 python/go/java/cpp 四语言)。原文即 +`python/dashscope_error_registry/error_registry.py`。生成器不在 SDK 仓库里。 + +本仓库各 ref 里的历史副本(仅供考古,均**未采用**): + +| 来源 | 路径 | 版本 | +| --- | --- | --- | +| `upstream/dev/errors` | `dashscope/common/error_registry.py` | 1.0.3(`8e9c1a0`) | +| `V2-errors` | `dashscope/v2/error_registry.py` | 1.0.5(`ededf0f`) | + +**与 1.0.6 原文的一致性已逐字段核实**(不是靠版本号声明): + +| 表 | 本文件 | 原文 1.0.6 | 核实结果 | +| --- | --- | --- | --- | +| `PublicError` schema | 5 字段 | 5 字段 | 字段名逐个相同 | +| `PUBLIC_ERRORS` | 16 条 | 16 条 | 4 个字段**逐条全同,含顺序** | +| `SdkErrorDef` schema | 5 字段 | 5 字段 | 字段名逐个相同 | +| `SDK_ERRORS` | 32 条 | 32 条 | 5 个字段**逐条全同,含顺序** | +| `InternalErrorDef` / `INTERNAL_ERRORS` | **不含** | 277 条 | 见下 | + +**为什么裁掉那张 277 条的表**(1.0.6 相对 1.0.5 的唯一实质增量): + +- 它描述的是**服务端组件**的错误:`gateway.*` / `mint.*` / `api.*` / + `turbo.*` / `model.*`。客户端 SDK 一条也不会抛。 +- 实测原文整份 vendor 的代价:black(79) 之后 **3553 行**、 + **388 行中文**(`mint.*` 的 `cause` / `solution`)、 + **97 处 E501**(black 拆不开长字符串字面量,最长行 267 字符)。 + 而裁剪后的文件是 649 行 / 0 中文 / 0 E501。 +- 也就是说,直接采用原文会让 `dashscope/v2/` **第一次出现 CJK**, + 并把一个 lint 干净的文件换成 97 处 E501。 +- 裁剪已由 `tests/v2/test_error_registry.py:: + test_vendored_copy_is_pruned_to_the_tables_v2_consumes` 钉死, + 将来谁要 vendor 全量,必须有意识地改这条测试。 + +若确实需要那张表(例如要让 SDK 解释网关内部码),正解不是把原文塞进 +`dashscope/v2/`,而是:让上游提供分表产出或纯 ASCII 变体, +或改为依赖 `dashscope-error-registry` wheel(`python/build.sh` 已在打包上传)。 + +**相对上游原文的 5 处本地适配**(整文件替换时会全部回退,需重新打上; +在此记录避免届时误判为"目录被污染"): + +1. `substitutions: Dict[str, str] = None` → 显式 `Optional[Dict[str, str]]` + (隐式 Optional 被现代 mypy 拒)。 +2. `format_msg(self, vars=...)` / `format_message(self, vars=...)` 形参 + **改名**为 `substitutions` / `variables`——上游用 `vars` 遮蔽内建名, + 仓库的 pylint disable 列表里**没有** W0622,会直接报错。 +3. 长字符串与目录列表按仓库 pin 的 black 23.3.0 / `--line-length=79` 重排 + (原文的 `PUBLIC_ERRORS = [...]` 是单行数千字符)。 +4. 上游把 provenance 放在独立的 `python/dashscope_error_registry/_version.py`; + 这里内联进文件头,因为 vendoring 只取一个文件。 +5. **文件名** `error_registry.py` → `_error_registry.py`(§8 已完成): + v2 根下内部模块一律带 `_` 前缀,这份不导出、只被 `errors.py` 与测试引用。 + +顶部 provenance 视为不可手改;所有适配逻辑写在 `errors.py`。 + +**D2|异常类名 = 目录的 `error_code`,一字不差。** 新增 3 个: +`RequestTooLargeError`、`ServiceUnavailableError`、`GatewayTimeoutError`。 +`status_code` 类属性与 `_STATUS_MAP` 全部由 `PUBLIC_ERRORS` 推导,删掉字面量。 + +**D3|扁平继承:9 个类全部直接挂 `APIStatusError`。** + +```python +class BadRequestError(APIStatusError): status_code = 400 +class AuthenticationError(APIStatusError): status_code = 401 +class PermissionDeniedError(APIStatusError): status_code = 403 +class NotFoundError(APIStatusError): status_code = 404 +class RequestTooLargeError(APIStatusError): status_code = 413 # 新增 +class RateLimitError(APIStatusError): status_code = 429 +class InternalServerError(APIStatusError): status_code = 500 +class ServiceUnavailableError(APIStatusError): status_code = 503 # 新增 +class GatewayTimeoutError(APIStatusError): status_code = 504 # 新增 +``` + +理由(按前提 4,不再以兼容性为依据): + +- 目录给的是 `(status_code, error_code)` 二元组,**扁平结构正好一一对应**, + 不引入目录没说的东西。 +- 目录里 `INTERNAL_ERROR` 就是 500。把 503/504 挂在它下面,等于把 + `InternalServerError` 的含义偷偷扩成"任意 5xx"——这是 **invent 出来的分类学**, + 恰恰是"标准"要消除的。 +- 想要"任意服务端失败"的兜底,用 `except APIStatusError` + `err.status_code >= 500` + 显式表达,而不是靠继承意外获得。 + +配套删掉 `error_from_status` 里的 `>= 500` 特例 +(改写前那行是 `cls = InternalServerError if status_code >= 500 else APIStatusError`; +改写后类选择只剩 `errors.py:286` 的 `_CODE_MAP` → `_STATUS_MAP` → `APIStatusError` 三级查表): +未映射的状态码一律落 `APIStatusError`,**不再猜成 500**。502/507 这类 +目录未收录的码因此不会被贴上 `InternalServerError` 的名字。 + +**D4|两套命名空间都保留,用 crosswalk 连接。** +`ErrorCode`(wire code 枚举,13 项)是网关真实下发值;目录 `key`(16 项)是内部规范键。 +**不合并**。新增 `_WIRE_TO_PUBLIC: Dict[str, PublicError]` 显式表达对应关系, +`_CODE_MAP` 由它派生。同时补上差距 4 的两个漏项: +`InvalidParameter → INVALID_REQUEST`、`Model.NotExist → RESOURCE_NOT_FOUND`。 + +**D5|`ContentFilterError` 改成 `BadRequestError` 的子类——目录特例消失。** + +现状:`ContentFilterError(DashScopeError)`,与目录冲突。目录里 +`CONTENT_POLICY_VIOLATION` 的 `error_code` 是 `BadRequestError`(400)。 +早先的做法是让 crosswalk "V2 覆盖目录",那是为了不改既有测试;按前提 4 不再需要。 + +```python +class ContentFilterError(BadRequestError): + """内容审核拦截(wire code DataInspectionFailed)。""" +``` + +这样 `isinstance(err, BadRequestError)` 成立,**目录契约天然满足**, +同时保留 DashScope 确实需要的细粒度区分(内容审核是高频码,且会在 +HTTP 200 上以业务错误形式下发)。D4 的 crosswalk 里 +`DataInspectionFailed → CONTENT_POLICY_VIOLATION` 直接取目录类, +V2 只是**在它下面细化**,不是覆盖。 + +由此得到一条**可测试的不变式**(进 P0 完整性测试): + +> crosswalk 为任一 wire code 选出的异常类,必须是该条目 `error_code` +> 所指类的**子类或同类**。 + +即:目录决定"至少是哪一类",V2 只能往下细化,不能横向改判。 + +HTTP 200 业务错误路径不受影响:`error_from_business_code` 显式传 +`status_code=200`,实例属性覆盖类属性,`ContentFilterError.status_code` +在该路径上读作 200(与现状一致)。 + +**D6|sdk.* 错误 → 单一新类 `SdkError(DashScopeError)`,两级识别。** + +```python +class SdkError(DashScopeError): + solution: Optional[str] # 目录 SdkErrorDef.solution + allow_retry: bool # 目录 SdkErrorDef.allow_retry +``` + +`status_code=None`——这些是本地/SDK 侧失败,**没有 HTTP 语义,不编造状态码**; +`code=def_.name`(如 `sdk.agentic_rl.InputError`)。 + +识别按 §1.2 的事实分两级,**不手维护 32 条映射表**: + +```python +# 一级:家族命名空间 + 类名精确命中(覆盖 18 + 4 = 22 条) +name = f"{family}.{type(exc).__name__}" # family = "sdk.agentic_rl" / "sdk.agentstudio" +def_ = _SDK_ERRORS.get(name) +# 二级:家族兜底条目 +if def_ is None: + def_ = _SDK_ERRORS[f"{family}.Error"] # sdk.agentic_rl.Error +``` + +要点: + +- `AgenticRLError`(基类)及其未被目录收录的子类,一律落 `sdk.agentic_rl.Error`。 + 这样**任何** rl 失败都保证拿到 `SdkError`,不会漏成裸 1.x 异常。 +- 那 8 条条件级条目(`DuplicateFunctionNames` 等)**故意不接**: + 要靠 message 文本或 1.x 的数字 `error_code`(4011/4012…)反推, + 既脆弱又侵入 1.x 内部编码。目录是跨 SDK 共享制品,有不可达条目是正常的。 +- `sdk.InvalidApiKey` 不由 1.x 类型到达,而是 v2 挂载层在 api_key 缺失时**自己抛**。 + +**agentstudio 家族没有 `.Error` 兜底条目**,两级方案对它不成立。已核实 +`dashscope/agentstudio/exceptions.py` 自己就分了两层(源码里有分节注释): + +| 层 | 类 | 目录归属 | 应译成 | +| --- | --- | --- | --- | +| 传输层(无 HTTP 响应) | `APIConnectionError`(:75) `APITimeoutError`(:81) `StreamError`(:135) `StreamClosedError`(:141) | `SDK_ERRORS` 的 4 条 `sdk.agentstudio.*`,**恰好一一对应** | `SdkError` | +| HTTP 层(有响应) | `APIStatusError`(:92) 及其 8 个子类 | 不在 `SDK_ERRORS` 里 | **按 `status_code` 映射到 v2 状态类** | + +关键:agentstudio 的 HTTP 层异常带真实 `status_code`, +**不能**压进 `SdkError`(`status_code=None`),那会丢掉状态码。 +它们的 `code` 取值(`invalid_request_error` / `authentication_error` / …) +还正好是目录的 `anthropic_error_code` 词表,说明目录设计者也是这么分层的。 + +**D7|翻译只发生在 v2 挂载层,不动 1.x;但两个挂载点代价不同。** + +- **rl(P2 做)** ✅ 已完成:`resources/reinforcement_learning.py` 是 v2 自己的资源模块, + 方法体在 v2 手里,直接 `try/except` 包住 1.x 调用即可 + (**改写前**全文 467 行、0 个 `except`;现 513 行, + 翻译集中在 `:111-121` 的 `_translate_rl_errors()`)。 +- **agentstudio(本次不做,单列 P2b)**:`client.py:113-128` 的 + `agentstudio` property **把整个 1.x `AgentStudioClient` 对象返回给用户**, + 用户随后自己调它的方法——v2 根本不在调用路径上, + `try/except` 无处可加。要翻译就必须包一层**透明代理** + (转发全部属性/方法、同步与异步两套),那是资源层的结构改动, + 超出"只修 errors"的范围,也会碰前提 1 的资源树。 + 该处代码本身已写明这是待办: + > "Until the dedicated errors-alignment task (design doc M3 3.12) lands, + > 1.x error types and HTTP behavior propagate as-is" + + 所以 P2 只修 rl;agentstudio 保持现状(缺口继续挂在 design doc M3 3.12), + 4 条 `sdk.agentstudio.*` 目录条目在本次改写后仍不可达。 + +1.x 代码零改动,符合既有约束。 + +--- + +## 4. 分阶段执行 + +**P0|落地目录(不改任何行为)** ✅ 已完成 +- `error_registry.py` 原样进 V2(生成制品,provenance 三行不手改) +- 新增 `tests/v2/test_error_registry.py`,**只放当前就能绿的用例**: + - 目录自身完整性:`key` 唯一、`status_code` 合法、`error_code` 非空、 + `anthropic_error_code` 非空;`SDK_ERRORS.name` 唯一 + - `SDK_ERRORS` 可达性(方向按 §1.2,**不是**"每条 `name` 都有 1.x 类"): + `sdk.agentic_rl.*` 中恰有 18 条命中 `common/errors.py` 的真实类, + `sdk.agentstudio.*` 4 条全部命中 `agentstudio/exceptions.py`, + 且家族兜底 `sdk.agentic_rl.Error` 存在。 + 把这三个数字**钉死**,上游重新生成目录时会立刻暴露漂移。 + - `format_msg()` / `format_message()` 的占位符替换行为 +- 注意:`PUBLIC_ERRORS.error_code` → 真实类的解析用例**不能**放这一阶段, + 3 个类还不存在,会红。随 P1 一起落。 +- 此阶段结束时应全绿且行为无变化 + +**P1|按目录重写 `errors.py`(主体)** ✅ 已完成 +- 新增 3 个异常类,9 个类扁平挂 `APIStatusError`(D2/D3),`status_code` 由目录取值 +- `ContentFilterError` 改挂 `BadRequestError`(D5) +- `_STATUS_MAP` 改为由 `PUBLIC_ERRORS` 推导(同状态多条去重) +- 新增 `_WIRE_TO_PUBLIC` crosswalk,`_CODE_MAP` 由它派生(D4) +- 删掉 `error_from_status` 的 `>= 500` 兜底特例(D3) +- 补差距 4、5 +- 目录 ↔ 代码的耦合用例**落在 `test_errors.py`,不放 `test_error_registry.py`**: + 后者的 docstring 已把自己限定为"只验目录自身,不断言 `errors.py` 如何映射目录", + 耦合用例依赖 `errors.py`,放进去会让"目录完整性"这层被下游代码污染。 + 已落的耦合用例: + - 每条 `PUBLIC_ERRORS.error_code` 都能解析成一个真实异常类 + - 解析出的类的 `status_code` 属性与目录条目一致 + - `_WIRE_TO_PUBLIC` 覆盖 `ErrorCode` 全部 13 项(**锁死差距 4 不再复发**) + - **D5 不变式**:crosswalk 为每个 wire code 选出的类,是对应目录条目 + `error_code` 类的子类或同类;未被 refine 的必须**恰好等于**该类 + - 目录 `key` 与 wire code 两个命名空间不相交(crosswalk 存在的理由) + - `_STATUS_MAP` 由目录推导,而非手写 +- **同步改 `tests/v2/test_errors.py`**(前提 4:测试随设计改,不冻结)。改后必须覆盖: + - 413 → `RequestTooLargeError`、503 → `ServiceUnavailableError`、 + 504 → `GatewayTimeoutError`,三者互不为子类(锁死 D3 的扁平性) + - 未收录状态码(如 502)→ 裸 `APIStatusError`,**不是** `InternalServerError` + - `ContentFilterError` 同时 `isinstance BadRequestError`(锁死 D5) + - HTTP 200 + `InvalidParameter` → `BadRequestError`; + HTTP 200 + `Model.NotExist` → `NotFoundError` + +**P2|rl 的 sdk.* 翻译(差距 8,唯一功能缺陷)** ✅ 已完成 +- 新增 `SdkError` + `error_from_sdk_exception(exc, *, family)`(D6) +- `resources/reinforcement_learning.py`:**8 个**公开工作流方法全部包住 + (`Rl`/`AsyncRl` 的 `register_functions`/`test_functions`/`upload_data` + + `RlJobs`/`AsyncRlJobs` 的 `create`)。用的是**一个普通同步 + `@contextlib.contextmanager`**,同步体(`asyncio.run(...)`)和异步体 + (`await ...`)都能包,不需要两套。 +- **catch 的是 3 个基类,不是 1 个**:`AgenticRLError` 只 root 了 + `common/errors.py` 19 个异常类里的 17 个,`RuntimeErrorWithCode` / + `ValueErrorWithCode` 分别继承 `RuntimeError` / `ValueError`,**不在其下**。 + 三元组正好覆盖 19/19(已用 `issubclass` 逐条验过)。 + 不再放宽的依据:`common/utils.py:139` 已经把 transport / timeout / + unexpected 全部 `except Exception` 收进 `RuntimeErrorWithCode`, + 所以捕得更宽只会把**本模块自己的 bug** 贴成 `SdkError`。 + 另:pydantic 的 `ValidationError` 也是 `ValueError` 子类,但不在三元组内, + 已验 `issubclass(pydantic.ValidationError, _RL_EXCEPTIONS) is False`。 +- **v2 模型适配留在 `with` 外面**:`_to_job(...)` / `UploadedData(...)` + 不被包,pydantic 校验失败不会被误译成 SDK 错误。 +- 测试(`test_errors.py` 5 个机制用例 + `test_rl.py` 8 个接线用例): + `code` 为 `sdk.agentic_rl.*`、`status_code is None`、原始 message 保留、 + `solution` / `allow_retry` 取自目录、`__cause__` 链住原异常、 + 未收录类落 `sdk.agentic_rl.Error` 兜底、`except DashScopeError` 可捕获、 + 同步与异步两路都覆盖、**非 rl 家族的 `ValueError` 原样穿透**。 +- **偏离 D6 一处:`sdk.InvalidApiKey` 本次不接线。** + D6 说它"由 v2 挂载层在 api_key 缺失时自己抛",但挂载层无法可靠判断 + "缺失":`Config.api_key` 是 `resolve_config()` **当时**读的 + `os.environ[DASHSCOPE_API_KEY]`,而 1.x 的 `set_api_key` 是**调用时**读环境变量。 + 用户先建 `Client()`、后 export key,今天能用;加了前置检查就会误报 + `sdk.InvalidApiKey`。要避免误报就得在 rl 模块里复制 `_config` 的解析逻辑 + (含私有 `_API_KEY_ENV`),代价大于收益。 + 现状已可接受:这条路径 1.x 抛 + `ValueErrorWithCode("Invalid API key configuration")` + (`finetune/agentic_rl.py:61`,非 `cli/agentic_rl.py`), + 经翻译后是 `SdkError(code="sdk.agentic_rl.ValueErrorWithCode")`, + 仍被 `except DashScopeError` 捕获,只是 `code` 不够专名。 + 真要修,正确位置是 `_config.py` 的 key 解析(超出"只修 errors"范围)。 + +**P2b|agentstudio 翻译(本次不做,已论证)** +需要透明代理才能 intercept,属资源层结构改动,超出"只修 errors"范围。 +缺口继续挂在 design doc M3 3.12 与 `client.py:114-119` 的既有注释上。 + +**现状的用户可见危害(比"没接"本身更值得记一笔)**:1.x +`dashscope.agentstudio` 导出 14 个异常类,其中 **8 个与 +`dashscope.v2.errors` 同名**——`APIConnectionError`、`APIStatusError`、 +`APITimeoutError`、`AuthenticationError`、`InternalServerError`、 +`NotFoundError`、`PermissionDeniedError`、`RateLimitError`。实测这 14 个对 +`issubclass(x, dashscope.v2.errors.DashScopeError)` **全为 False**(它们挂在 +`AgentStudioError(DashScopeException)` 这条 1.x 基类下),所以 +`from dashscope.v2.errors import RateLimitError` 去接 `client.agentstudio` +的限流**永远接不到**,且从 import 语句上完全看不出问题。 + +**更隐蔽的是其中 6 个连父类名都一样**:`APITimeoutError` 两侧都继承 +`APIConnectionError`;`AuthenticationError` / `InternalServerError` / +`NotFoundError` / `PermissionDeniedError` / `RateLimitError` 两侧都继承 +`APIStatusError`。于是 traceback 前两层完全同名,只有第三层 +(`AgentStudioError` vs `DashScopeError`)能区分,读栈的人几乎必然看漏。 +这与下面"只包调用层比完全不包更糟"是同一类风险的**现成版本**—— +不必等接线,今天就已经在误导用户。 + +已核实的实现成本(留给后来人,不必重新踩): + +- **翻译分两条路(分层依据见 §3 D7 的表,不在此重述)。** + 可操作的落点:4 条非 HTTP 类走 + `error_from_sdk_exception(exc, family="sdk.agentstudio")` → `SdkError`; + HTTP 层走 + `error_from_status(exc.status_code, message=exc.message, + body=exc.raw, headers={"x-request-id": exc.request_id})`。 + **陷阱**:`sdk.agentstudio` 没有 `.Error` 家族兜底(`sdk.agentic_rl` 有), + 所以对那 4 条之外的类调用 `error_from_sdk_exception` 会 `KeyError`, + **反而把原始异常盖掉**。现状已被测试钉死: + `test_errors.py:477-485`(断言 `KeyError`)、 + `test_error_registry.py:265-287`(断言无兜底、且 HTTP 层类与目录不相交)。 + 接线时要改的是这两个用例,不是绕过它们。 +- **rl 的做法搬不过来。** rl 是逐方法手写 `@contextmanager` + (`reinforcement_learning.py:111-121`);agentstudio 是 11 个资源类 + × 同步/异步双胞胎、约 75 个 `async def`,且**没有共同基类或 mixin** + (都是裸类 + `self._client`)。逐方法重写等于复制整个子 SDK, + 所以只有**递归透明代理**可行。 +- **代理会破坏现有断言。** `test_apps.py:146-158` 与 + `smoke/test_full_surface.py:560-599` 断言 + `isinstance(client.agentstudio, AgentStudioClient)`; + `test_apps.py:233-242`、`:246-255` 通过 `mounted.transport.is_closed` + 验证关闭,代理必须原样透传 `transport`。 +- **决定性的那条:流式错误在迭代中途抛,代理拦不到。** + `SessionEvents.stream()` 是**急切**发请求(`session_events.py:127`) + 然后返回 `_TypedEventStream`;此后的失败发生在 `for event in stream` + 期间——`StreamClosedError` 抛在生成器体里(`streaming.py:88`、`:169`), + 生产者线程的 transport 异常也是迭代中 `raise item` 重抛 + (`streaming.py:125`、`:208`)。只包 `stream()` 这一层, + 仅能覆盖"建流即失败",**覆盖不到流式的主要失败模式**。 + 要覆盖就得再套一层鸭子类型代理,转发 + `__enter__` / `__exit__` / `__iter__` / `close` / `text_stream` + (异步侧再加 `__aenter__` / `__aexit__` / `__aiter__` / `__anext__` / + `aclose`),而 `text_stream` 是**属性、其 getter 本身是生成器** + (`session_events.py:155-178`)。注意隐式 dunder 查找走**类型**而非 + `__getattr__`,所以每个 dunder 都必须显式声明,**漏一个就是一个静默缺口**。 +- **因此"只包调用层"比"完全不包"更糟**:它会让用户相信 + `except DashScopeError` 已覆盖 agentstudio,然后在最主要的流式路径上被漏网。 + 加上这里无法对真实后端做集成验证,本次维持不接线, + 保持上述两个"未解决"用例原样,作为对现状的显式声明。 + +**P3|可选接线(取决于 §7 决策)** +- `anthropic_error_code` → anthropic 兼容协议输出 anthropic 形状错误体(差距 6; + **仍不做**,决策 1) +- `allow_retry` → 是否影响重试策略(差距 7;**仍不做**,决策 2。 + 注意 `_retry.py` 是 HTTP 层,与 sdk.* 不同层,**不要混用**) +- `format_msg` / `format_message` 占位符填充(差距 9)✅ 已完成, + 见 `errors.py` 的 `_catalog_message()` 与决策 4 + +**P4|文档同步** ✅ 已完成 + +三份文档都已同步,且**每段示例都用 mock transport 对真实实现跑过**(9.1 的 +import 名单、9.2 业务层 → `RateLimitError` 且 `status_code == 200`、9.1 未收录 +502 → 裸 `APIStatusError`、9.5 `SdkError` 的 `code`/`solution`/`allow_retry`/ +`__cause__`、9.6 `raise_on_error=False` → `is_error=True` 且连接错误仍然抛)。 + +- `md/dashscope-2.0-design.md`:**新增 §2.7 错误模型(目录驱动,长期约束)**—— + 目录是唯一事实源(1.0.6 / `56b7208`、`PUBLIC_ERRORS` 16 条、`SDK_ERRORS` + 32 条、`InternalError` 为何不 vendor)、派生规则、三层来源一个异常族、 + crosswalk 与目录 key 的命名空间分离(附 13 项对照表 + `Arrearage` 为何走 + `permission_denied`)、`ContentFilterError` 是唯一细化且必须子类化、 + `SdkError` 无状态码的标准依据、1.x 挂载层翻译义务与当前进度、 + 新增错误代码时的 6 步自检。另同步 5 处:§0 调研共识表的落点 + (§2.2 → §2.7)、§2.2 错误模型摘要(9 个扁平类 + 修掉不存在的 + `dashscope.errors.ErrorCode` 路径)、§2.6 自检清单加第 ⑥ 步、 + §7 目录树补 `_error_registry.py`、里程碑 1.3 与 3.12(记录 rl 半边已完成、 + 剩 agentstudio)。 +- `md/dashscope-2.0-naming-review.md`:**偏离计划一处**——计划写"§6.4 记录 + 新增异常类名",但 §6.4 是**输出字段**改名清单,异常类名塞进去会破坏该节 + 的分类;改为**新增 §6.5 异常类名**,10 行按 P0/P1/P2 分级,并点明本节 + 记录的不是"改成什么名"而是"哪些名从 SDK 手写变成目录派生"。 +- `md/dashscope-2.0-interfaces-examples.md`:**偏离计划一处**——计划写"异常族 + 清单与 `except` 示例",但该文档原先**完全没有**错误处理章节(只有开头约定里 + 一句"未捕获的 HTTP 错误默认抛出异常族");改为**新增 §9 错误处理**六节 + (9.1 异常族与 import 路径 + 类/状态码表、9.2 两层错误一个族、9.3 异常属性、 + 9.4 重试、9.5 `SdkError`、9.6 `raise_on_error=False`),并在 §6 Agentic RL + 补"两半错误行为统一"、§7 AgentStudio 补"异常仍是 1.x 原生类型、 + `except DashScopeError` 抓不到"。 + +**写文档过程中查出并改正一处事实错误**:初稿在 design §2.7 与 naming-review +§6.5 都写了"去掉 `>= 500` 猜测可以避免连带触发重试"。**这是错的**—— +`transport/_retry.py:12` 的 `RETRYABLE_STATUS_CODES`(408/409/429/500/502/ +503/504)由 `http.py:173,323` 按**原始响应状态码**判定,与 `errors.py` 最终 +选出哪个异常类**完全无关**;502 改前改后都会被重试。两处已改为陈述真实危害: +猜出来的类让 `except InternalServerError` 抓到并非 500 的失败,且实例 +`status_code`(502)与类声明的(500)矛盾。 + +`md/dashscope-2.0-m1-m3-review.md` **故意不改**:该文第 76 行记录的 +"`APIConnectionError`/`APITimeoutError` 全库零引用"缺陷如今已修(传输层 +`_translate_transport_error` 已接线),但文首明写"本文结论按当时状态保留, +**不作回填**",属带日期的评审快照,遵守其自身约定。 + +**P5|resource 层旁路点接线** ✅ 已完成 + +先核实"要不要改":`dashscope/v2/resources/` 下 **48 处** `check_response()` +调用全部经 `response_error()` → `error_from_status()` / +`error_from_business_code()`,P0/P1 的目录改写**自动生效**,无需逐点改。 +真正要动的只有**绕过 `check_response` 自己造异常**的旁路点,共 3 处: + +| 文件 | 改前 | 改后 | +| --- | --- | --- | +| `models.py`(`get`,同步 + 异步) | 手写 `NotFoundError(..., status_code=404)` | `error_from_business_code(code=ErrorCode.MODEL_NOT_EXIST.value, headers=response.headers)` | +| `audio/realtime.py:128` | 裸 `DashScopeError`(WS `type:error` 事件) | `error_from_business_code(...)`,与 `transport/sse.py:69-76` 的 `error` 分支同形 | +| `fine_tunes.py` | ① `_check_terminal` 丢弃 `job.message`(同步 + 异步)② SSE 无 `status:` 行时默认 500(`_iter_sse` + 异步内联副本) | ① `message=job.message or ...`,对齐 `_jobs.py:222` ② 新增模块级 `_stream_error()` 供同步/异步共用,删掉 `_raise_stream_error` 方法 | + +**`fine_tunes` 的 500 是本次唯一实质缺陷**:`status:` 在 1.x +`StreamEventMixin` 线格式里是**可选行**,缺省时把失败说成服务端 500——与 D3 +拒绝"未收录状态码猜 `InternalServerError`"是同一条理由:**没有任何一方声明过 +这个状态码**。现在缺省即走业务错误层,类由 body 的 `code` 选, +`status_code == 200`。 + +**故意不改(9 处裸 `raise DashScopeError`,分 3 类)** + +1. **wait 超时 / wait 期间被取消**(各 4 处:`_jobs.py:210,300` 与 + `:230,320`、`fine_tunes.py:370,554` 与 `:387,571`)。这两类**正是** + `SdkError` 的目标场景(客户端自己判定、服务端没回失败),但目录 + `SDK_ERRORS` 32 条只有 `sdk.InvalidApiKey` + `sdk.agentic_rl.*`(27)+ + `sdk.agentstudio.*`(4),**没有 jobs / fine-tune 家族**。 + `_error_registry.py` 是"DO NOT EDIT"生成物,按既定约束**不能在 SDK 里造 + 目录条目**——要补得去上游 `dashscope-error-registry` 仓库提。故此处 + **阻塞于上游**,不是遗漏。 +2. **本地入参校验**(`fine_tunes.py:189` `_job_id`)。这不是网关失败, + resource 层其余同类一律用内建 `ValueError` / `RuntimeError`;改成 + `ValueError` 会变更对外异常类型,超出本次"仅改 errors 相关、不动函数名与 + 输入输出"的范围。 +3. **`transport/ws.py` 的 `WsEventError`**。在 `transport/` 而非 + `resources/`,不属 P5 范围——**已在 P6 单独处理**。 + +另记一处**行为不对称、但不属 errors**:`_jobs.py` 把 FAILED 检查放在 `_parse` +(每次 `get()` 都抛)、CANCELED 放在 `_check_canceled`(只有 `wait()` 抛); +`fine_tunes.py` 两者都在 `_check_terminal`(只有 `wait()` 抛)。这是"何时抛" +的行为决策,统一它会改变 `get()` 的可观测行为,不在本次范围。 + +**P6|transport 层接线(`ws.py`)** ✅ 已完成 + +P5 收口后全 v2 复审发现的最大缺口,与 realtime 那一处同类: +`transport/ws.py` 的 `_check_failed()` 把**所有**服务端 `task-failed` 事件写死 +成 `WsEventError`,payload 里的 wire `code` 只被存进实例属性、**从不参与选类**。 +实测 `payload.code == "Throttling"`: + +``` +e.code == "Throttling" # 拿到了 +e.status_code is None # 没有 +isinstance(e, APIStatusError) -> False +isinstance(e, RateLimitError) -> False # except 抓不到 +``` + +影响 **`client.audio.tts` 与 `client.audio.asr` 两个公开接口**(`_check_failed` +被 `run_task` / `recv_event` / `recv` 三处调用,是唯一收口点,改一处即全覆盖)。 + +改法与 realtime 一致:走 `error_from_business_code`,类由 `payload.code` 经 +crosswalk 选。**一处非显然的接线**:WS 帧把 `request_id` 放在 `header` 而非 +`payload`,而 `error_from_business_code` 只从 body 或 `x-request-id` 读它, +websocket 又没有 HTTP 响应头可传——所以**仅在有值时**用 +`headers={"x-request-id": ...}` 这条既定回退通道送进去,`body` 仍保留原始整帧。 + +**`WsEventError` 收窄语义而非删除**:它仍在 `ws.py` 的 `__all__` 里,且 `:141` +那处(`run_task` 收到非 `task-started` 事件)**没有 wire code**,属本地判定的 +协议时序违规,crosswalk 无从下手。一个类同时表示"服务端说任务失败"和"服务端 +破坏了帧协议"正是原先混淆的根源,现在 docstring 明确排除前者。 + +**随之而来的行为变更**:`task-failed` 的 code 若未命中 crosswalk,现在落裸 +`DashScopeError` 而非 `WsEventError`——与 `sse.py` / realtime 的未命中行为一致。 +无消费者受损:`WsEventError` 在 `ws.py` 之外**零 import、零测试断言** +(`tts.py` / `asr.py` 只 import `Connector, WsSession`,`transport/__init__.py` +的 `__all__` 也没有它),除基类外本来就没人能按名字 catch 它。 + +**故意留下的一处(非目录问题,待决)**:`ws.py:186` 与 `:205` 的 +`json.loads(raw)` 让裸 `json.JSONDecodeError` 直接逃出去,而对应的 SSE 路径 +(`sse.py:45-51`)把"畸形报文"和"非 dict 报文"都包成 `DashScopeError`。 +所以畸形 WS 帧目前 `except DashScopeError` **抓不到**。这是健壮性一致性缺口、 +不是目录接线问题,且会改变对外异常类型,故未并入本次。 + +--- + +## 5. 行为变更清单(不做兼容性评估) + +前提 4:V2 未发布、无存量用户,**不评估"是否破坏存量"**。 +下表只列出相对当前 V2 的**可观测行为差异**,供 review 时逐条确认是有意为之。 + +| 变更 | 现状 → 改后 | 依据 | +| --- | --- | --- | +| 503 | 压平成 `InternalServerError` → `ServiceUnavailableError` | D2/D3,差距 5 | +| 504 | 压平成 `InternalServerError` → `GatewayTimeoutError` | D2/D3,差距 5 | +| 413 | 落裸 `APIStatusError` → `RequestTooLargeError` | D2,差距 2 | +| 未收录状态码(502/507…) | 猜成 `InternalServerError` → 裸 `APIStatusError` | D3 | +| `InvalidParameter`(HTTP 200 业务错误) | 基类 `DashScopeError` → `BadRequestError` | D4,差距 4 | +| `Model.NotExist` | 不映射(靠状态码兜底)→ `NotFoundError` | D4,差距 4 | +| `ContentFilterError` 的父类 | `DashScopeError` → `BadRequestError` | D5 | +| rl 失败 | 裸 1.x 异常(`except DashScopeError` 抓不到)→ `SdkError` | D6/D7,差距 8 | +| agentstudio 失败 | **不变**,仍裸奔 1.x 异常 | P2b 延后(D7 已论证需透明代理) | +| 空 body 的失败(如网关直接回 503) | `message == ""` → 目录 `error_msg` 兜底 | 决策 4,差距 9 | +| `models.get` 查无此模型 | 仍是 `NotFoundError`,但 `status_code` 由伪造的 404 → 真实的 **200**,并补上 `code == "Model.NotExist"` | P5 | +| fine-tune SSE 错误帧缺 `status:` 行 | 伪造 500 → `InternalServerError`;改后由 body 的 `code` 选类,`status_code == 200` | P5 | +| fine-tune `wait()` 遇 FAILED | 丢弃服务端 `job.message`,只给本地套话 → 优先 `job.message` | P5 | +| realtime WS `type:error` 事件 | 恒为基类 `DashScopeError` → 按事件 `code` 选目录类(如 `Throttling` → `RateLimitError`) | P5 | +| audio `task-failed` 事件(tts / asr) | 恒为 `WsEventError`(`status_code is None`,不是 `APIStatusError`)→ 按 payload 的 `code` 选目录类,`status_code == 200` | P6 | +| `task-failed` 的 code 未命中 crosswalk | `WsEventError` → 裸 `DashScopeError`(与 `sse.py` / realtime 的未命中行为一致) | P6 | +| 公开异常类数量 | 11 → 15(`APIStatusError` 子类 6 → 9;另加 `SdkError`) | D2/D6 | + +**`APIStatusError` 的语义随之放宽(P1 已改其 docstring)。** +D5 把 `ContentFilterError` 挂到 `BadRequestError` 之后,HTTP 200 业务错误 +造出的实例**也是** `APIStatusError`,其 `status_code == 200`。 +所以基类不能再读作"非 2xx 响应",而是"**网关声明的失败**(两层都算), +实例的 `status_code` 说明是哪一层"。这样 `except ContentFilterError` +一处即可覆盖两种上报方式——正是统一异常族的目的。 + +**新增类不从 `dashscope/v2/__init__.py` 导出(P1 决定,推翻早先建议)。** +理由是一致性而非成本:该文件现在只导出 `errors` 模块 + +`DashScopeError` + `ErrorCode`(`except` 需要的两样),9 个状态类一律走 +`dashscope.v2.errors`;包根 `dashscope/__init__.py:84` 也只再导出 +`AsyncClient, Client, chat, settings`,且属 1.x 不可改范围。 +只加 3 个会显得随意,加 9 个会让 `__all__` 失焦。全部 v2 消费者 +本来就直接 `from dashscope.v2.errors import ...`,无需中转。 + +--- + +## 6. 验证清单 + +- [x] `pytest tests/v2`:305 → **324**(P0 加 19 个目录用例)→ **336** + (P1 加 12 个耦合用例)→ **349**(P2 加 13 个:5 个翻译机制 + 8 个 rl + 接线)→ **356**(决策 4 加 7 个目录文案兜底)→ **359**(P5 新加 3 个、 + 加强 2 个)→ **360 passed / 3 skipped**(P6 新加 1 个、加强 1 个), + 无回归。P5/P6 这 7 个用例都用 `git stash` 只回退对应源文件验证过 + **改前必失败**(P5 命中 `assert 500 == 200`,P6 命中 `raise WsEventError`) +- [x] `pytest tests/unit`(**476 passed / 6 skipped**)/ + `tests/legacy`(**65 passed / 31 skipped**)不回归 +- [x] **基名冲突已修**:`tests/v2/test_messages.py` 与 1.x 的 + `tests/unit/test_messages.py`(Assistants Messages 资源)同基名, + `tests/` 下无 `__init__.py`,pytest 的 prepend 导入模式两者都叫 + `test_messages` —— `pytest.ini` 的 `testpaths = tests` 意味着裸跑 + `pytest` 会被 `import file mismatch` **中断收集**(实测 922 collected + + 1 error,一个用例都不执行)。已把 v2 那份改名 + `test_types_messages.py`(它测的是 `dashscope/v2/types/messages.py` + 这个规范消息模型,按 v2 测试"以被测模块命名"的惯例补上包名限定), + 1.x 那份不动。裸跑收集恢复:**934 collected / 0 error** +- [x] **D3 扁平性**:`test_catalog_classes_are_flat_siblings` 断言 9 个类 + `__bases__ == (APIStatusError,)` 且两两互不为子类 +- [x] **D5 不变式**:`test_wire_code_never_leaves_the_catalogued_class` + + `test_unrefined_wire_codes_map_to_exactly_the_catalogued_class` +- [x] `test_errors.py` 已按新继承关系改写(503/502 两处旧断言已更新), + §5 表中 **errors 层**每条变更都有对应用例; + `rl 失败` 一行由 P2 的 8 个 `test_rl.py` 用例覆盖, + 仅 `agentstudio` 一行按 P2b 延后 +- [x] crosswalk 完整性:`test_crosswalk_covers_every_declared_wire_code` + 断言 `set(_WIRE_TO_PUBLIC) == {m.value for m in ErrorCode}`(13 项) +- [x] pylint(pinned disable + 本地 `R0917`):`dashscope/v2` 整棵源码树 + = **10.00/10,零告警** +- [x] pylint:`tests/v2` 整棵树同样 **10.00/10**,但**需要把仓库根放进 + `sys.path`**:`python3 -m pylint --disable=,R0917 + --init-hook='import sys; sys.path.insert(0, ".")' tests/v2`。 + 不加 `--init-hook` 时本机会报 **66 处 `E0611`(`No name 'v2' in + module 'dashscope'`)+ 1 处 `E1102`(`dashscope.Client is not + callable`)**——`tests/` 下没有 `__init__.py`,pylint 把 `tests/v2` + 而不是仓库根插入 `sys.path`,astroid 于是解析到本机 user + site-packages 里装的 **dashscope 1.26.3**(该版本没有 `v2` 子包), + 而不是仓库里的这份。属本机环境污染,不是仓库缺陷,也与本次改写无关 + (未改动的 `test_transport.py` 同样如此)。lint `dashscope/v2` 时 + pylint 会沿 `dashscope/__init__.py` 上溯到仓库根,所以源码树不受影响 +- [x] flake8:`dashscope/v2` + `tests/v2` **零告警**。原先记录的两处既有 + E501 已修(`_config.py:54` 的 `WORKSPACE_WS_GATEWAY_TEMPLATE` 加括号 + 换行、`test_protocols.py:23` 的尾注释提到上一行),两处都不改值。 + 全仓(排除 `tests/legacy`、`samples`)只剩 2 处 `F824`,都在 + `tests/unit/test_multimodal_dialog.py`——1.x 测试代码,不在本次改动范围 +- [x] black `--line-length=79 --check`:P2 的 4 个改动文件 + (`errors.py` / `reinforcement_learning.py` / `test_errors.py` / + `test_rl.py`)**clean**; + 全仓仍有 7 个文件是 25.x drift(含 `_error_registry.py` 的 + docstring 后空行),仓库 pin 23.3.0,**不采纳** +- [x] `_error_registry.py` 落地后**未被手改**:`git hash-object` 仍为 D1 记录的 + `908fbf1…`,§8 的改名也没动内容(`git mv` 只改路径,相似度 100%)。 + 注意这个 hash 钉的是"P0 落地后没人再动过目录内容",**不是**"等于上游原文"—— + vendored 这份带 5 处本地适配(D1),本来就不等于原文。 + provenance 的比对基准是上游 registry 仓库 1.0.6 的 + `python/dashscope_error_registry/error_registry.py`, + **不是** SDK 仓库的 `upstream/dev/errors`,那份是过时的 1.0.3 +- [x] 1.x 代码零改动:`git status --porcelain` 只有 `dashscope/v2/*`、 + `tests/v2/*`、`md/*` +- [x] V2 接口面是**纯超集**:对 `errors.py` 做 AST 模块级名字 diff(vs `HEAD`), + `removed: none`,`added:` 5 个公开名(`GatewayTimeoutError` + `RequestTooLargeError` `ServiceUnavailableError` `SdkError` 四个类 + + `error_from_sdk_exception` 一个函数)+ 8 个下划线内部名; + 资源树 / 方法名 / 模型名 / `ResponseBase` 未碰 +- [x] `SdkError` 相关用例:`test_errors.py` 5 个(`…has_no_http_semantics` + 钉 `status_code is None`、`…resolves_the_catalog_by_class_name`、 + `…keeps_the_original_message`、`…falls_back_to_the_family_entry`、 + `…needs_a_family_fallback` 钉 `sdk.agentstudio` 缺家族兜底会 `KeyError`) + + `test_rl.py` 8 个(`…translates_1x_failure` 钉 `code`/`__cause__`、 + `…catchable_as_dashscope_error`、`…carries_its_own_solution` 钉 + `solution`/`allow_retry`、`…falls_back_to_the_family_entry`、 + `test_non_rl_exceptions_pass_through` 反向钉"非 rl 异常不翻译"、 + 以及 `test_functions` 与 sync/async 两条接线) +- [x] **P4 文档同步已验证**:三份文档的交叉链接目标全部存在 + (design / registry-plan / interfaces-examples / interfaces-mapping / + naming-review / m0-1x-cli-hotfix);章节编号无冲突(design 新增 §2.7 后 + §3 顺延不变、naming-review §6.1–6.5、examples §0–9);文档里写下的每条 + 行为都**跑过真实实现**(mock transport),不是读代码推断出来的 +- [x] **接口级覆盖审计(P5/P6 收口后做的全 v2 复审)** + + **方法**:AST 扫 `dashscope/v2/resources/**` 每个资源类的每个公开方法, + 再**手工解开委托链**。只数 raise 点是不够的,有两个盲区: + ① 多数方法体内并不直接调用目录构造器,而是委托给 `_jobs` / + `_checked_completion` / `WsSession`,按 raise 点统计会把它们误判成未接; + ② **透传型接口在 v2 代码里根本不 `raise`**(`agentstudio` 就是), + grep 全库也扫不到——所以必须从 `client.py` 的公开面反向逐接口核。 + + 已解开的委托链:`multimodal.chat` → 复用 `chat.completions` 的 + `_send`/`_asend`/`_build_request`/`ChatStream`;`chat.completions` → + `_checked_completion` → `response_error`;`images`/`videos`/ + `embeddings`/`asr`(离线转写) 的 Jobs → `_jobs`;`rl` 的 Jobs → + `fine_tunes`/`_jobs`,1.x 挂载调用 → `_translate_rl_errors`; + `audio.tts` + `audio.asr`(实时识别) → `WsSession._check_failed`(P6)。 + + | v2 接口 | 错误路径 | 状态 | + | --- | --- | --- | + | `chat.completions` | `_checked_completion` → `response_error` | ✅ | + | `multimodal.chat` | 复用 completions 的 `_send`/`ChatStream` | ✅ | + | `multimodal.embeddings` | `check_response` | ✅ | + | `embeddings` / `images` / `videos` | `check_response`;作业委托 `_jobs` | ✅ | + | `rerank` / `tokenization` / `files` / `deployments` | `check_response` | ✅ | + | `models` | `check_response`;`get` 另走 crosswalk(P5) | ✅ | + | `apps` | `check_response` ×2 + `error_from_business_code` | ✅ | + | `fine_tunes` | `check_response` ×6 + `_stream_error`(P5) | ✅* | + | `audio.tts` | `WsSession._check_failed` → crosswalk(P6) | ✅ | + | `audio.asr` | 离线走 `_jobs`,实时走 `WsSession` | ✅ | + | `audio.realtime` | `error_from_business_code`(P5) | ✅ | + | `rl` / `reinforcement_learning` | `_translate_rl_errors` + `_jobs` | ✅ | + | **`agentstudio`** | **返回 1.x 客户端对象本身,异常原样透传** | ❌ P2b | + + **结论**:15 个 resource 里 **14 个的全部公开接口都在目录上**; + 凡"服务端报了失败"(有 wire `code` 或 HTTP 状态)的路径, + **100% 经 crosswalk 选类**,"报了 code 却写死类"这个缺陷类已清零。 + 唯一未接的是 `agentstudio`(既定延后,见 P2b 的同名类危害)。 + + *两处"接了一半",按口径分开记: + ① **8 处 wait 超时/取消**(`_jobs.py:210,230,300,320`、 + `fine_tunes.py:370,387,554,571`)抛的是 v2 `DashScopeError`, + `except DashScopeError` **接得到**,但没有目录 `code`/`solution`—— + 阻塞于上游 `SDK_ERRORS` 缺 jobs/fine-tune 家族(见 P5)。 + ② **`ws.py:186`/`:205` 的 `json.loads`** 让裸 `JSONDecodeError` 逃出去, + `except DashScopeError` **接不到**,而 `sse.py:45-51` 的对等场景是包好的 + (见 P6 末,待决)。 + +--- + +## 7. 决策项(已按最小范围默认采纳,可推翻) + +这 4 项**都不是兼容性问题**,前提 4 不解决它们。默认全部采纳"最小范围"选项, +因为它们都符合前提 2(只修 errors 相关);若要推翻,只影响 P3,P0–P2 不受影响。 + +1. **P3 是否纳入本次?默认:不纳入。** `anthropic_error_code` 目前无消费者, + 接线意味着 anthropic 兼容协议要开始输出错误体(新增行为), + 范围会外溢到协议层。单独排期。 + + 已核实(比"没人用"更硬的一条):**根本没有可实现的对端契约**。 + `protocols/anthropic.py` 只 import `BadRequestError, DashScopeError, + ErrorCode`,两处抛出(`:187-191`、`:204`)都只用于**请求侧校验**, + 全文不产出任何错误响应体;design doc 也没规定 anthropic 形状错误体 + 的字段与嵌套。所以接线不是"补一个消费者",而是**先发明一份协议契约** + ——那必须先和 anthropic 兼容协议的定义方对齐,不能由 errors 改写顺手决定。 +2. **`allow_retry` 是否接重试?默认:不接。** `_retry.py` 是 HTTP 状态层, + `allow_retry` 是 sdk.* 本地错误层,两层语义不同。 + 仅作为 `SdkError` 的只读属性暴露,由用户自行判断。 +3. **`SdkError` 是否按 `sdk.agentic_rl.*` / `sdk.agentstudio.*` 再分子类?默认:不分。** + 一个类 + `code` 字段足够;分子类会把目录的 31 条变成 31 个类, + 与"目录驱动、类名稳定"的初衷相反。 +4. **`format_msg` 占位符谁来填?默认:body 的 `message` 优先。** ✅ 已实施 + 目录文案带 `{parameter}` / `{resource}` / `{limit}`,但网关下发时通常已带 + `message`,V2 现有逻辑就是 body 优先(`errors.py:311` + `message = data.get("message") or message`,业务层同理见 `:398`)。 + 占位符只在 body 无 message 时兜底,且填不出变量就原样保留占位 + (**不要静默丢字段**)。 + + 实现为 `errors.py` 的 `_catalog_message(code, status_code)`,两处构造 + (`error_from_status` / `error_from_business_code`)都写成 + `message or _catalog_message(...)`。**条目识别顺序**:先按 wire code 查 + `_WIRE_TO_PUBLIC`;查不到才按状态码查 `_STATUS_ENTRY`,而 `_STATUS_ENTRY` + **只收录恰有一条条目声明的状态**(403/404/413/500/503/504)。 + 400/401/429 各有 2–5 条同状态条目、文案互不相同,只凭状态码挑一条 + 等于替网关编一个它没说过的原因,因此这三种状态在没有 wire code 时 + **不兜底**(保持空 message)。同理,目录 key(如 `missing_parameter`) + 不是 wire code,`_CODE_MAP` 认不出它,兜底也认不出—— + **兜底的识别能力与选类严格一致,不会出现"类选对了、文案是别人的"**。 + + 实际触发面:`response_error()` 对 4xx/5xx 传 `message=response.text` + (`errors.py:390`),所以**空 body 的响应**(负载均衡/网关直接回 503 + 之类)会走到兜底;带 body 时 `response.text` 非空,兜底不触发。 + 业务层经 `response_error()` 进来的 message 恒非空,兜底只对直接调用 + 公开 API 的人生效(仓内 `_jobs.py` / `apps.py` / `fine_tunes.py` + 的调用点都自带非空 message)。 + **SSE 有意不接**:`transport/sse.py:64,71` 传的是 + `body.get("message") or "stream error"`,字面量恒真,流内错误继续用 + transport 自己的文案,不改。 + +--- + +## 8. 附:`error_registry.py` 的位置 ✅ 已完成(独立 commit) + +只能放 `dashscope/v2/`:它不 import 任何 1.x 内容,1.x 有独立错误体系, +放 `dashscope/` 根会暗示两边共用。 + +但原位置有一处约定不一致:v2 根下内部模块一律带 `_` 前缀 +(`_config.py` / `_logging.py` / `_quickstart.py`),不带前缀的是公开面 +(`client.py` / `errors.py`)。`error_registry.py` 无前缀,却也没从 +`dashscope/v2/__init__.py` 导出(只导出 `errors` / `DashScopeError` / `ErrorCode`), +实际只被 `errors.py` 和测试引用——**读起来像公开 API,其实是内部数据**。 + +已按建议改名 `_error_registry.py`,与 errors 改写分成两个 commit。改动面: +`git mv` + **4 处 import**(`errors.py` 两处、`test_errors.py` 一处、 +`test_error_registry.py` 一处)+ 三处 docstring 路径引用 + 四份 md 文档的 +路径引用(design §2.7 / 目录树 / 里程碑 1.3、naming-review §6.5、 +interfaces-examples §9)。测试文件名 `test_error_registry.py` **不改**: +pytest 的 `test_*.py` 发现规则不要求与被测模块名前缀一致。 + +**代价**:改名本身成了第 5 处本地适配(见 D1),整份替换升级时要一起重打。 +若将来要让用户查错误码,走 `errors.ErrorCode` 即可,不需要公开目录本身。 diff --git a/md/dashscope-2.0-interfaces-examples.md b/md/dashscope-2.0-interfaces-examples.md new file mode 100644 index 00000000..dc135a1f --- /dev/null +++ b/md/dashscope-2.0-interfaces-examples.md @@ -0,0 +1,1862 @@ +# dashscope 2.0 接口调用示例集 + +本文按资源分组给出 dashscope 2.0 可直接运行的调用示例,全部基于 `dashscope/v2/` +当前实现逐条核对(同步为主,有异步对应物的接口附异步版本)。1.x → 2.0 的逐接口 +对照见 [`md/dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md); +设计依据见 [`md/dashscope-2.0-design.md`](dashscope-2.0-design.md)。 + +## 为什么是 2.0:相对 1.x 的优化、对齐业界与收益 + +2.0 做两件事:把 OpenAI / Anthropic 已验证的现代 SDK 基线**追平**,再叠加 +DashScope 独有的**差异化能力**。 + +**追平业界基线**(1.x 短板 → 2.0 落点): + +| 维度 | 1.x | 2.0 | +| --- | --- | --- | +| 入口形态 | 全局变量 `dashscope.api_key` + 类方法 `.call`,多租户 / 多环境易错 | `dashscope.Client(...)` 资源树,同步 / 异步孪生(§0) | +| 错误契约 | 返回值自查 `status_code`,漏写 `if` 即静默失败 | 异常驱动:9 个扁平状态类 + 错误目录派生,`request_id` 全链路贯通(§9) | +| 自动重试 | 核心路径无重试 | 默认 `max_retries=2`,`Retry-After` + 指数退避 + 抖动(§9.4) | +| 类型化 | `**kw` 透传、无 IDE 提示、字段拼写错误运行时才炸 | Pydantic 响应 + `@overload` 精确签名(流式 / 非流式静态可区分) | +| 流式体验 | 默认重放全量、需手设 `incremental_output`、深层取值 | 一律增量 chunk,`text_stream` / `get_final_message()` 助手(§0.5) | +| HTTP 栈 | requests + aiohttp 双栈,每次调用新建 Session | httpx 统一同步 / 异步 + 每 Client 连接池 keep-alive(§8.5) | +| 日志 | 缺 request_id、无敏感信息过滤 | request_id / 状态码 / 重试原因入日志,Authorization 过滤,`DASHSCOPE_LOG` 一键开启(§8.2) | + +**差异化优势**(两家官方 SDK 没有的): + +- **一个 SDK 管全部**:一个 Client 覆盖「模型模态(文本 / 多模态 / 图像 / 视频 / + 语音 / 向量 / 排序 / 分词)× 协议(native / openai / anthropic,§0.1)× + provider(百炼上第三方模型靠协议层零成本接入)」。 +- **全链路长任务**:`files → fine_tunes → deployments → rl → agentstudio` 的 + 生产链路全覆盖;耗时环节由统一的 `jobs.create/fetch/wait` 长任务模式支撑 + (统一 `Job` 对象 + 类型化视图 + 内置退避轮询,§2.2),OpenAI / Anthropic + SDK 无此概念。 +- **协议翻译收敛在 adapter**:一套调用代码、一套异常族、一套流式 API, + 换 provider 只改 `protocol=` / `base_url=`,不再维护多套 SDK(§0.2 / §0.3)。 + +**收益**:上手从 6 行降到 2 行(§0);「请求失败被静默吞掉」的已知路径系统性 +堵死;连接池复用消除每请求 TCP/TLS 握手;同一语义全 SDK 只有一个名字 +(动词收敛 8 类、字段命名统一,见设计文档 §2.6),学习与排障成本同降。 + +约定: + +- 节标题以 2.0 接口为主体,括号内的"1.x:…"是与之对应的旧入口,仅供迁移对照。 +- 示例中的 `sk-xxx` 替换为真实 API Key;不显式传 `api_key` 时读取环境变量 + `DASHSCOPE_API_KEY`。 +- 示例中用 `# 可选` 标注可省略的参数(多数带默认值,写成 `# 可选,默认 …`); + 未标注的为必填。 +- `timeout=`(秒)几乎处处可用,示例中不再重复演示。**例外**:`client.rl` 的 + 1.x 直通方法(`jobs.create` / `register_functions` / `test_functions` / + `upload_data` / `query_function_instance_logs` / + `query_all_function_instance_logs` / `delete_function_instance`)复用 1.x + 业务逻辑,不接受 `timeout=`;`client.agentstudio` 是 1.x 子 SDK 的懒挂载桥, + 参数约定以 1.x 为准。 +- `timeout=` 的语义按方法类型区分:普通请求方法上是**单次 HTTP 请求**的超时; + 阻塞式领域动词(`images.generate` / `videos.generate` / + `audio.asr.transcribe`)上是**等待任务终态**的总预算(默认 24h),提交这一次 + 请求的超时另用 `submit_timeout=`;`*.jobs.wait` / `fine_tunes.wait` / + `rl.jobs.wait` 上同样是等待预算。 +- 除 `client.rl` 直通方法外,每个公开方法还接受调用级 `api_key=` / + `workspace=` 覆盖(跟随 Client 配置优先级),示例中不再重复演示。 +- 未捕获的失败默认抛出 `dashscope.v2.errors` 异常族 + (`AuthenticationError` / `RateLimitError` / `NotFoundError` / ..., + 构造 Client 时传 `raise_on_error=False` 可关闭)。完整异常族清单、 + 每个类对应的状态码与 `except` 写法见 **§9**。 + +## 0. 客户端构造与一行调用 + +```python +import dashscope + +# 标准构造(推荐):显式传 key + workspace,或走环境变量 +# DASHSCOPE_API_KEY / DASHSCOPE_WORKSPACE +client = dashscope.Client(api_key="sk-xxx", workspace="ws-xxx") + +# 异步客户端:资源树与同步完全一致 +aclient = dashscope.AsyncClient(api_key="sk-xxx", workspace="ws-xxx") + +# 一行式快捷入口(懒加载全局单例 Client,等价于 +# Client().chat.completions.create(model=..., messages=[prompt])) +resp = dashscope.chat("qwen3.7-plus", "给我讲个笑话") +print(resp.choices[0].message.content) + +# 用完显式释放连接 +client.close() +# async 侧: await aclient.aclose() +``` + +**`workspace` 建议配置、非必需**:默认端点走 workspace 专属网关 +`https://{workspace}.{region}.maas.aliyuncs.com`(官方推荐,性能与稳定性更优)。 +参数、`DASHSCOPE_WORKSPACE`、`config.toml` 三处都没给 `workspace`、又没有显式 +`base_url` 时,回退到 1.x 共享网关(`cn-beijing` 为 +`https://dashscope.aliyuncs.com`)并发出一次 `UserWarning`——api_key-only 的 +quickstart 仍能跑通,但建议尽早配置 workspace 切到专属网关。websocket 默认端点 +同理(`wss://{workspace}.{region}.maas.aliyuncs.com`),可用调用级 `ws_url=` 或 +`DASHSCOPE_WEBSOCKET_BASE_URL` 覆盖(`audio.realtime` 例外:走独立路径,只认 +`ws_url=`,见 §4.6)。显式给 `base_url` 是自建 / 私有网关的逃生口,完全接管端点。 + +**`region`(默认 `cn-beijing`)**选择六个地域网关之一:`cn-beijing` / +`ap-southeast-1` / `cn-hongkong` / `us-east-1` / `eu-central-1` / +`ap-northeast-1`(非法值直接 `ValueError`)。国际用户传 `region=`(或 +`DASHSCOPE_REGION` / `settings.region` / config.toml 的 `region`)即可切换 +默认端点,不必手拼 `base_url`。 + +自定义端点 / 协议(对应 1.x `base_http_api_url` 等模块变量): + +```python +# 全部可选(不传则回落到 settings / 环境变量 / config.toml / 默认值) +client = dashscope.Client( + api_key="sk-xxx", # 缺省读 DASHSCOPE_API_KEY + workspace="ws-xxx", # 缺省回退共享网关 + UserWarning + region="cn-beijing", # 默认 cn-beijing,六地域之一 + base_url="https://ws-xxx.cn-beijing.maas.aliyuncs.com/api/v1", # 逃生口 + protocol="native", # 默认 "native";或 "openai" / "anthropic" + timeout=60.0, + max_retries=2, + raise_on_error=True, # 默认 True + config_path="~/.dashscope/config.toml", + protocol_options={"strict": False}, + http_client=None, # 注入自带 httpx.Client(自定义连接池 / 代理) +) +``` + +`protocol` 决定的默认端点只作用于 `protocol_base_url`,即协议适配的 chat 家族 +(`chat.completions` / `multimodal.chat`)实际发往的地址;其余资源一律走 +`base_url`。 + +### 0.1 协议选择:`native` / `openai` / `anthropic` + +`protocol` 只改变 chat 类调用(`chat.completions` / `multimodal.chat`)的线缆格式, +**调用方式与返回类型(`ChatCompletion`)不变**,其余资源(embeddings/images/audio +等)不受影响。三个取值: + +- `"native"`(默认):DashScope 自有协议(`/services/...`),全能力。 +- `"openai"`:OpenAI 兼容(`/v1/chat/completions`),可对接百炼 compatible-mode + 或任意 OpenAI 兼容端点 / 第三方网关。 +- `"anthropic"`:Anthropic 兼容(`/v1/messages`,system 提取、tool_use 映射)。 + +未显式配置 `base_url` 时,默认端点按资源家族区分:`chat.completions` / +`multimodal.chat` 跟随 `protocol`(`openai` → `.../compatible-mode/v1`, +`anthropic` → `.../apps/anthropic`,见 `config.protocol_base_url`),其余资源 +始终走 native 网关 `.../api/v1`;一旦显式配置 `base_url`,则对全部资源生效。 + +```python +# 用 OpenAI 兼容协议对接百炼 compatible-mode;调用代码与 native 完全一致 +# (base_url 缺省即可,chat 家族自动落到 compatible-mode 端点) +openai_client = dashscope.Client(api_key="sk-xxx", protocol="openai") +resp = openai_client.chat.completions.create(model="qwen3.7-plus", messages="你好") + +# protocol_options:strict=True 时未知参数抛错;默认 False 透传 +# (厂商新参数不必等 SDK 发版即可使用) +strict_client = dashscope.Client( + api_key="sk-xxx", protocol="openai", protocol_options={"strict": True}, +) +``` + +字段级的协议翻译规则见设计文档 [`dashscope-2.0-design.md` §3](dashscope-2.0-design.md)。 + +### 0.2 同一次调用在三种协议下的线缆报文 + +SDK 调用代码三个协议完全一致: + +```python +resp = client.chat.completions.create( + model="qwen3.7-plus", + messages=[ + {"role": "system", "content": "你是一个有帮助的助手。"}, + {"role": "user", "content": "你好"}, + ], + temperature=0.7, # 可选 + max_tokens=512, # 可选 +) +``` + +差异只在实际收发的 HTTP 报文: + +**`native`(默认)** —— `POST {base_url}/services/aigc/text-generation/generation` + +```json +{ + "model": "qwen3.7-plus", + "input": { + "messages": [ + {"role": "system", "content": "你是一个有帮助的助手。"}, + {"role": "user", "content": "你好"} + ] + }, + "parameters": {"result_format": "message", "temperature": 0.7, "max_tokens": 512} +} +``` + +```json +{ + "request_id": "...", + "output": { + "choices": [ + {"finish_reason": "stop", + "message": {"role": "assistant", "content": "你好!有什么可以帮你?"}} + ] + }, + "usage": {"input_tokens": 21, "output_tokens": 9, "total_tokens": 30} +} +``` + +**`openai`** —— `POST {base_url}/chat/completions` +(base_url 为 `.../compatible-mode/v1` 时即百炼兼容端点) + +```json +{ + "model": "qwen3.7-plus", + "messages": [ + {"role": "system", "content": "你是一个有帮助的助手。"}, + {"role": "user", "content": "你好"} + ], + "temperature": 0.7, + "max_tokens": 512 +} +``` + +```json +{ + "id": "chatcmpl-...", + "model": "qwen3.7-plus", + "choices": [ + {"index": 0, "finish_reason": "stop", + "message": {"role": "assistant", "content": "你好!有什么可以帮你?"}} + ], + "usage": {"prompt_tokens": 21, "completion_tokens": 9, "total_tokens": 30} +} +``` + +**`anthropic`** —— `POST {base_url}/v1/messages`(请求头带 `anthropic-version: 2023-06-01`) +(base_url 为 `.../apps/anthropic` 时即百炼 Anthropic 兼容端点,不含 `/v1`) + +```json +{ + "model": "qwen3.7-plus", + "system": "你是一个有帮助的助手。", + "messages": [ + {"role": "user", "content": "你好"} + ], + "max_tokens": 512, + "temperature": 0.7 +} +``` + +注意 system 被提取为独立字段;`max_tokens` 在该协议下必填,缺省时 SDK 自动补 +`4096`;`stop` 参数会改名为 `stop_sequences`。 + +```json +{ + "id": "msg-...", + "role": "assistant", + "content": [{"type": "text", "text": "你好!有什么可以帮你?"}], + "stop_reason": "end_turn", + "usage": {"input_tokens": 21, "output_tokens": 9} +} +``` + +三种响应都被 adapter 翻译回同一个 `ChatCompletion`,所以 +`resp.choices[0].message.content`、`resp.usage.total_tokens` 等取法完全一致。 + +### 0.3 参数在三种协议下的支持矩阵 + +同一段调用代码,参数在三个协议下的落点并不相同。图例: +✅ 原生支持(映射到该协议的正确字段)| 🔀 透传(进 body/parameters,不报错, +除非 `strict=True`)| ❌ 400 拒绝(抛 `BadRequestError`,`status_code=400`、 +`code=InvalidParameter`)。 + +| 规范参数 | native | openai | anthropic | +| --- | --- | --- | --- | +| `model` / `messages` / `stream` | ✅ | ✅ | ✅(system 拆为独立字段) | +| `temperature` / `top_p` | ✅ | ✅ | ✅ | +| `max_tokens` | ✅ | ✅ | ✅(缺省自动补 `4096`) | +| `stop` | ✅ | ✅ | ✅(改名 `stop_sequences`) | +| `tools` | ✅ | ✅ | ✅(转 `input_schema`) | +| `seed` | ✅ | ✅ | ❌ | +| `presence_penalty` | ✅ | ✅ | ❌ | +| `response_format` | ✅ | ✅ | ❌ | +| `repetition_penalty` | ✅ | 🔀 | ❌ | +| `enable_thinking` | ✅ | 🔀 | ❌ | +| `parameters` | 🔀 | 🔀 | 🔀 | + +> 表中是**调用侧参数名**。`parameters=` 转发到协议层时叫 `extra_parameters` +> (`ChatProtocol.build_chat_request` 的形参名),读协议代码时对得上即可。 + +三个协议的差异要点: + +- **native**:所有规范参数都落进线缆 `parameters` 段(恒定带 `result_format="message"`), + 不存在“未知参数”,故 `strict` 在 native 上是惰性的(无效果)。 +- **openai**:OpenAI 原生参数(`temperature/top_p/max_tokens/stop/seed/` + `presence_penalty/response_format/tools`)直接进 body 顶层;OpenAI 没有的 + `repetition_penalty` / `enable_thinking` 与 `parameters` 归入“未知”, + 默认透传,`strict=True` 时抛 `DashScopeError`。 +- **anthropic**:只认 Messages API 有的字段;`seed` / `repetition_penalty` / + `presence_penalty` / `enable_thinking` / `response_format` / `n` / `logprobs` / + `top_logprobs` / `thinking_budget` / `enable_search` / `search_options` / + `parallel_tool_calls` **无条件 400 拒绝**(与 `strict` 无关)——因为静默丢弃对 + 用户不可见,请求会”假成功”但参数并未生效。这些名字即使通过 `parameters` 夹带 + 进来也一样 400 拒绝,堵死绕过路径;其余 `parameters` 则 `strict=False` 透传、 + `strict=True` 抛 `DashScopeError`。 + +> 一句话记忆:native 啥都收且全生效;openai 对没有的参数默认透传(可切 `strict` +> 改成报错);anthropic 对无对应项的参数直接 400,逼你显式删除或改用其它协议。 + +### 0.4 响应信封已扁平化 + +服务端普遍把业务字段包在 `output` / `data` 里,2.0 在解析时统一摊平,**响应对象 +上直接读业务字段**,不需要 `.output.` / `.data.` 这一跳: + +```python +ft = client.fine_tunes.get("ft-xxx") +print(ft.status, ft.finetuned_model) # 不是 ft.output.status + +info = client.files.get("file-xxx") +print(info.file_id, info.name) # 不是 info.data["file_id"] +``` + +每个响应对象都继承 `ResponseBase`,共用一组公共字段:`request_id` / `code` / +`message` / `is_error` / `status_code` / `headers`(网关原始 HTTP 响应头, +小写键 dict;ws 资源无 HTTP 信封,保持空 dict)。**`is_error` 由非空 `code` +自动推导**(与 SDK 判断是否抛异常用的是同一条规则),所以在 +`raise_on_error=False` 下任何资源都不会漏标业务错误: + +```python +job = client.fine_tunes.get("ft-xxx") # 服务端 HTTP 200 + code="Job.Failed" +if job.is_error: + print(job.code, job.message, job.request_id) +``` + +三个类型**不带**信封,因为它们的载荷里根本没有 HTTP 信封:`FineTuneEvent` +(SSE 事件帧,自带 `is_error` 标记错误帧)、`RegisteredFunctions` / +`UploadedData`(rl 侧透传 1.x 返回值拼出来的记录)。此外列表里嵌套的条目 +(`FileList.files[i]` / `ModelList.models[i]` 等)信封字段为空——服务端一次响应 +只报一次 `request_id`,读信封请用列表对象本身。 + +`output` 仅在**服务端字段因服务而异**、无法类型化的地方保留,目前只有 +`Job.output`(异步任务原始载荷)与 `FineTuneEvent.output`(事件流原始帧); +这两处都配了类型化的便捷属性(见 §2.2)。 + +### 0.5 四种流式形态 + +| 场景 | 写法 | 产出 | +| --- | --- | --- | +| chat 增量文本 | `client.chat.completions.create(..., stream=True)` | `ChatStream`,`for chunk in stream` → `ChatCompletionChunk`(`choices[].delta`) | +| 百炼应用增量输出 | `client.apps.completions.create(..., stream=True)` | `AppStream`,`for partial in stream` → `AppCompletion`(每帧是完整快照或增量,取决于 `incremental_output`) | +| 微调训练事件 | `for event in client.fine_tunes.stream_events(job_id)` | `FineTuneEvent`(`output` 为原始帧,`is_error` 标记错误帧) | +| 语音合成音频 | `for chunk in client.audio.tts.stream(text)` | `bytes` | +| 语音合成音频(HTTP) | `for chunk in client.audio.tts.http.stream(text, ...)` | `HttpSynthesisChunk`(`audio` 为解码后字节,末片带整段音频 URL) | + +异步侧对应 `async for`;websocket 双工(`audio.asr.recognize` / +`audio.realtime.connect`)不是"流式响应",而是长连接会话,用 `.events()` 迭代 +服务端推送的事件 dict(见 §4.1 / §4.4)。 + +流中出错不会静默:默认(`raise_on_error=True`)抛出异常族;置 `False` 时在流里 +产出一个 `is_error=True` 的对象后结束迭代。 + +## 1. 文本 / 对话 + +### 1.1 `client.chat.completions.create`(1.x:`Generation.call`) + +```python +resp = client.chat.completions.create( + model="qwen3.7-plus", + messages=[ + {"role": "system", "content": "你是一个有帮助的助手。"}, + {"role": "user", "content": "用一句话介绍杭州"}, + ], + temperature=0.7, # 可选 + max_tokens=512, # 可选 +) +print(resp.choices[0].message.content) +print(resp.usage.total_tokens, resp.request_id) +``` + +返回 `ChatCompletion`(三个协议统一解析成它,线缆差异见 §0.2): + +```json +{ + "request_id": "...", + "model": "qwen3.7-plus", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "……", + "reasoning_content": null, + "tool_calls": null + } + } + ], + "usage": {"input_tokens": 21, "output_tokens": 9, "total_tokens": 30} +} +``` + +便捷属性 `resp.output_text` 等价于 `choices[0].message.content`。流式时逐 chunk +返回 `ChatCompletionChunk`(`choices[].delta` 取代 `message`,末尾 chunk 带 +`usage`)。 + +messages 也接受纯字符串(自动包装成 user 消息): + +```python +resp = client.chat.completions.create(model="qwen3.7-plus", messages="你好") +``` + +具名参数全集:`model` / `messages` / `stream` / `temperature` / `top_p` / +`top_k` / `max_tokens` / `stop` / `seed` / `tools` / `tool_choice` / +`response_format` / `n` / `logprobs` / `top_logprobs` / `presence_penalty` / +`repetition_penalty` / `enable_thinking` / `thinking_budget` / `enable_search` / +`search_options` / `parallel_tool_calls` / `incremental_output`。签名之外的模型 +参数统一走 `parameters=`(原样并入线缆 `parameters`,兼容协议下并入请求体顶层): + +```python +resp = client.chat.completions.create( + model="qwen3-vl-plus", + messages=[{"role": "user", "content": [ + {"image": "https://example.com/cat.jpg"}, {"text": "这张图里有什么?"}]}], + parameters={"vl_high_resolution_images": True}, # 可选:签名外的模型参数 +) +``` + +流式(对应 1.x `Generation.call(..., stream=True)`): + +```python +stream = client.chat.completions.create( + model="qwen3.7-plus", + messages="写一首短诗", + stream=True, # 可选,默认 False +) +for chunk in stream: + delta = chunk.choices[0].delta + if delta.content: + print(delta.content, end="", flush=True) + # 思考模型还会带 delta.reasoning_content +``` + +`ChatStream` 另提供便捷助手:`stream.text_stream` 只迭代增量文本, +`stream.get_final_message()` 把整流聚合为一条完整 message(含 tool_calls), +`stream.headers` 取响应头。 + +工具调用与结构化输出: + +```python +resp = client.chat.completions.create( + model="qwen3.7-plus", + messages="北京今天天气怎么样?", + tools=[{ # 可选 + "type": "function", + "function": { + "name": "get_weather", + "description": "查询城市天气", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, + }], +) +tool_calls = resp.choices[0].message.tool_calls + +resp2 = client.chat.completions.create( + model="qwen3.7-plus", + messages="给我一个用户 JSON", + response_format={"type": "json_object"}, # 可选 +) +``` + +思考模型(对应 1.x `enable_thinking` 增量输出参数): + +```python +resp = client.chat.completions.create( + model="qwen3-235b-a22b-thinking-2507", + messages="9.11 和 9.9 哪个大?", + enable_thinking=True, # 可选 +) +print(resp.choices[0].message.reasoning_content) +print(resp.choices[0].message.content) +``` + +异步(对应 1.x `AioGeneration.call`): + +```python +resp = await aclient.chat.completions.create( + model="qwen3.7-plus", + messages="你好", +) + +stream = await aclient.chat.completions.create( + model="qwen3.7-plus", messages="写一首短诗", stream=True, +) +async for chunk in stream: + print(chunk.choices[0].delta.content or "", end="") +``` + +### 1.2 多轮对话:messages 列表自行维护 + +2.0 的 chat 无状态,多轮对话由调用方自己维护 messages 列表: + +```python +messages = [{"role": "system", "content": "你是一个有帮助的助手。"}] + +for user_input in ["我叫小明", "我叫什么?"]: + messages.append({"role": "user", "content": user_input}) + resp = client.chat.completions.create(model="qwen3.7-plus", messages=messages) + answer = resp.choices[0].message.content + messages.append({"role": "assistant", "content": answer}) + print(answer) +``` + +### 1.3 代码生成 / 文本理解:同一端点换 model + +代码生成、文本理解等场景不需要独立接口,同一端点换模型即可: + +```python +# 代码生成 +resp = client.chat.completions.create( + model="qwen3-coder-plus", + messages="用 Python 写一个二分查找", +) + +# 理解 / 总结类任务同理,只是 model 不同 +resp = client.chat.completions.create( + model="qwen3.7-plus", + messages="总结这段话:……", +) +``` + +### 1.4 `client.apps.completions.create`(1.x:`Application.call`) + +```python +# 非流式 +resp = client.apps.completions.create( + app_id="your-app-id", + prompt="帮我写一份周报大纲", # prompt 与 messages 二选一(至少传一个) +) +print(resp.text) # AppCompletion,信封已扁平化 + +# 带会话与 RAG 参数 +resp = client.apps.completions.create( + app_id="your-app-id", + prompt="继续", + session_id=resp.session_id, # 可选 + rag_options={"pipeline_ids": ["pipe-xxx"]}, # 可选 +) +``` + +`prompt` 传单轮提问,`messages` 传调用方自己维护的完整对话;两者都给时 +`prompt` 作为最新一条 user 消息追加到 `messages` 之后。要让服务端替你保存对话 +状态,用 `session_id=`(会话)或 `memory_id=`(长期记忆)。 +`biz_params=` 承载应用自身的流程 / 插件入参(服务端字段名,位于线缆 `input` +内),模型侧旋钮与签名未列出的参数放 `parameters=`。其余具名参数: +`has_thoughts`(返回思考过程)/ `doc_tag_codes` / `doc_reference_type` / +`image_list` / `file_list` / `temperature` / `top_p` / `top_k` / `seed`。 + +返回 `AppCompletion`: + +```json +{ + "request_id": "...", + "text": "……", + "finish_reason": "stop", + "session_id": "session-xxx", + "thoughts": null, + "doc_references": null, + "workflow_message": null, + "usage": {"models": [{"model_id": "qwen3.7-plus", "input_tokens": 30, "output_tokens": 50}]} +} +``` + +业务级错误体现在继承来的 `code` / `message` / `is_error` 字段上。 + +```python +# 流式 +for partial in client.apps.completions.create( + app_id="your-app-id", + prompt="讲个长一点的故事", + stream=True, # 可选,默认 False + incremental_output=True, # 可选 +): + print(partial.text, end="") +``` + +## 2. 多模态 + +### 2.1 `client.multimodal.chat.create`(1.x:`MultiModalConversation.call`) + +content 为 parts 列表(沿用 1.x 的 `{"image": ...}` / `{"text": ...}` / `{"audio": ...}` +part 形式): + +```python +resp = client.multimodal.chat.create( + model="qwen3-vl-plus", + messages=[{ + "role": "user", + "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "这张图里有什么?"}, + ], + }], +) +print(resp.choices[0].message.content) + +# 流式(对应 AioMultiModalConversation 的异步流式同理加 await/async for) +stream = client.multimodal.chat.create( + model="qwen3-vl-plus", + messages=[{ + "role": "user", + "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "详细描述这张图"}, + ], + }], + stream=True, # 可选,默认 False +) +for chunk in stream: + print(chunk.choices[0].delta.content or "", end="") +``` + +返回与文本对话相同的 `ChatCompletion`(结构见 §1.1);多模态端点返回的 list 形态 +content 会被扁平化为纯文本字符串。 + +**多数情况下不需要这个资源**:`chat.completions.create` 会检查 messages,只要 +有 image / audio / video part 就自动改走多模态端点,签名与返回类型完全一致。 + +```python +# 等价于上面的 multimodal.chat.create +resp = client.chat.completions.create( + model="qwen3-vl-plus", + messages=[{"role": "user", "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "这张图里有什么?"}, + ]}], +) +``` + +`multimodal.chat.create` 保留为**显式逃生口**:无论有没有媒体 part,每次调用都 +打到多模态端点。典型用途是给 qwen-vl 系列模型发一条纯文本消息(按内容路由会 +把它留在文本端点上)。兼容协议(`openai` / `anthropic`)没有独立的多模态端点, +part 翻译在它们自己的 chat 路径上完成,所以那里两个资源是同一次调用。 + +### 2.2 `client.images.generate`:提交 + 等待一步到位(1.x:`ImageSynthesis.call`) + +```python +job = client.images.generate( + model="wanx2.1-t2i-turbo", + prompt="一只在月球上弹吉他的猫,水彩风格", + negative_prompt="模糊, 低质量", # 可选 + size="1024*1024", # 可选,写 "1024x1024" 也可以 + n=1, # 可选 +) +# 返回已完成 Job +for url in job.image_urls: + print(url) +``` + +其余具名参数:`seed` / `style` / `prompt_extend`(智能改写)/ `watermark` / +`function` / `image_urls`(wan2.6-image 参考图列表)/ `extra_input=`(并入线缆 +`input` 段);`generate` 还接受 `poll_interval=` / `max_interval=` 调轮询节奏。 + +**按模型名自动路由协议**:wan2.5 及以下走旧 text2image 端点(仅异步任务); +wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`generate` 同步打 +multimodal-generation 端点,`jobs` 异步任务打 image-generation 端点),出参统一 +归一化为 `Job.results`。调用代码不变,换模型即自动切换。 + +对应 1.x `async_call/fetch/wait` 的分步写法: + +```python +job = client.images.jobs.create( + model="wanx2.1-t2i-turbo", + prompt="一只在月球上弹吉他的猫", + size="1024*1024", # 可选 +) +print(job.task_id, job.status) # PENDING / RUNNING + +snapshot = client.images.jobs.fetch(job.task_id) +print(snapshot.status) + +done = client.images.jobs.wait(job) # 退避轮询到终态,超时抛 DashScopeError +# wait 的 poll_interval / max_interval / timeout 均可选(默认 1s → 8s 退避,总预算 24h) +done = client.images.jobs.wait(job, poll_interval=2.0, timeout=600.0) + +# 任务管理:取消与列举 +client.images.jobs.cancel(job.task_id) +page = client.images.jobs.list(status="SUCCEEDED", page_no=1, page_size=10) +# list 可按 start_time / end_time / model_name / api_key_id / region / status 过滤 +``` + +`generate` 与 `jobs.*` 均返回 `Job`(images / videos / transcription / +embeddings.jobs 共用此结构): + +```json +{ + "task_id": "task-xxx", + "status": "SUCCEEDED", + "request_id": "...", + "submitted_at": "...", + "scheduled_at": "...", + "finished_at": "...", + "output": {"results": [{"url": "https://..."}]}, + "usage": {"image_count": 1}, + "code": null, + "message": null +} +``` + +`status` 取值 `PENDING` / `RUNNING` / `SUCCEEDED` / `FAILED` / `CANCELED` +(另有终态 `UNKNOWN`:任务状态无法确认,`wait` 遇到它直接返回、不抛异常)。 +`FAILED` 时默认(`raise_on_error=True`)抛携带 `code`/`message` 的异常;`wait` +期间任务被 cancel 同样抛错。 + +`Job` 是**唯一保留 `output` 原始字典**的响应类型(各服务的结果字段不同),但配了 +类型化视图,正常取值不必碰 `output`: + +| 取法 | 含义 | +| --- | --- | +| `job.results` | `List[JobResult]`,每项有 `.url` / `.transcription_url`,服务端多给的字段仍可按属性读 | +| `job.image_urls` | `List[str]`,所有结果的 `url` | +| `job.video_url` | `Optional[str]`,视频任务的结果 | +| `job.result_url` | `Optional[str]`,单结果任务的唯一产物(图片 / 视频 / 文件 URL 都收在这一个属性下) | +| `job.job_id` | `task_id` 的别名(`FineTune` 把同一个标识拼作 `job_id`) | +| `job.succeeded` / `job.finished` | 状态判断 | + +时间戳统一为 `submitted_at` / `scheduled_at` / `finished_at`;服务端原始的 +`submit_time` / `scheduled_time` / `end_time` 也能读到同一份值(校验别名), +`status` 同理接受 `task_status`。 + +异步版本(`AioImageSynthesis`)只需 `aclient.images.jobs.create` / `await ...wait(job)`。 + +### 2.3 `client.videos.generate`(1.x:`VideoSynthesis.call`) + +```python +# 文生视频 +job = client.videos.generate( + model="wan2.7-t2v", + prompt="一只小猫在草地上奔跑", + size="1280*720", # 可选;另有 duration / resolution / ratio / + # seed / prompt_extend / watermark 具名参数 +) +print(job.video_url) + +# 图生视频(prompt 与 image_url 可同传,至少传一个) +job = client.videos.generate( + model="wanx2.1-i2v-turbo", + image_url="https://example.com/cat.jpg", +) + +# wan2.7-i2v 起走 media 新协议:类型化素材列表, +# type 为 first_frame / last_frame / driving_audio / first_clip,每种至多一个; +# image_url= 是单个 first_frame 素材的简写(与显式 first_frame 同传会报错) +job = client.videos.generate( + model="wan2.7-i2v", + prompt="镜头缓缓推近", + media=[ + {"type": "first_frame", "url": "https://example.com/start.png"}, + {"type": "last_frame", "url": "https://example.com/end.png"}, + ], +) + +# 分步:jobs.create / fetch / wait / cancel / list,与 images 相同 +job = client.videos.jobs.create(model="wan2.7-t2v", prompt="海浪拍打礁石") +done = client.videos.jobs.wait(job) +``` + +返回值同为 `Job`(结构见 §2.2),视频结果读 `job.video_url`。模型专属的 +`input` 字段经 `extra_input=` 透传,其余服务端参数放 `parameters=`。 + +### 2.4 `client.multimodal.embeddings.create`(1.x:`MultiModalEmbedding.call`) + +```python +resp = client.multimodal.embeddings.create( + model="multimodal-embedding-v1", + input=[ + {"text": "一只猫"}, + {"image": "https://example.com/cat.jpg"}, + # 还支持 {"video": url}、{"audio": url} + ], + # fps / res_level(分辨率档位 0-3)/ max_video_frames 是顶层参数, + # 仅对快照模型生效 +) +for item in resp.embeddings: + print(item.index, item.type, len(item.embedding)) + +# 异步:await aclient.multimodal.embeddings.create(...) +``` + +返回与文本向量相同的 `EmbeddingResponse`(结构见 §3.1),文本 / 图片 / 音频 / +视频的结果落在同一个向量空间里,靠 `item.type` 区分。 + +input 元素也可用类型化的 `EmbeddingItem`(1.x 的 +`MultiModalEmbeddingItemText/Image/Audio` 收敛而来): + +```python +from dashscope.v2.resources.multimodal.embeddings import EmbeddingItem + +resp = client.multimodal.embeddings.create( + model="multimodal-embedding-v1", + input=[ + EmbeddingItem(text="一只猫", factor=1.0), # factor 可选 + EmbeddingItem(image="https://example.com/cat.jpg"), + ], +) +``` + +每个 input 元素至少要设置 `text` / `image` / `audio` / `video` 之一(全空报错); +`EmbeddingItem` 的具名字段就是这四个模态加 `factor`。签名未列出的参数放 +`parameters=`。 + +`oss://` 输入由服务端解析,SDK 自动带上 `X-DashScope-OssResourceResolve: enable` +请求头,**不需要**调用方额外开关(1.x 需要手工传 headers,2.0 已移除该参数)。 + +## 3. 向量 / 排序 / 分词 + +### 3.1 `client.embeddings.create`(1.x:`TextEmbedding.call`) + +```python +resp = client.embeddings.create( + model="text-embedding-v3", + input=["今天天气不错", "明天会下雨吗"], + dimension=1024, # 可选 + text_type="document", # 可选;检索场景 query 侧传 "query" + # 另有 output_type / instruct 具名参数 +) +for emb in resp.embeddings: + print(emb.index, len(emb.embedding)) +print(resp.usage.total_tokens) + +# 异步:await aclient.embeddings.create(...) +``` + +`input` 也接受单个字符串。返回 `EmbeddingResponse`: + +```json +{ + "request_id": "...", + "embeddings": [ + {"index": 0, "type": "text", "embedding": [0.013, ...]}, + {"index": 1, "type": "text", "embedding": [0.021, ...]} + ], + "usage": {"total_tokens": 12} +} +``` + +序号字段统一叫 `index`(1.x 文本向量侧的 `text_index` 是它的校验别名,读到同一 +份值);多模态向量复用同一个 `Embedding` / `EmbeddingResponse`,靠 `type` 区分 +模态。 + +### 3.2 `client.embeddings.jobs.create/fetch/wait`(1.x:`BatchTextEmbedding`) + +```python +job = client.embeddings.jobs.create( + model="text-embedding-async-v2", + file_url="https://example.com/batch_input.txt", # 待向量化文本文件 URL + text_type="document", # 可选 +) +done = client.embeddings.jobs.wait(job) +print(done.result_url) # 结果文件 URL + +# 任务管理:fetch / cancel / list 与 images.jobs 同形 +``` + +返回 `Job`(结构见 §2.2);结果文件 URL 读 `done.result_url`,需要看服务端原始 +字段时再落到 `done.output`。 + +### 3.3 `client.rerank.create`(1.x:`TextReRank.call`) + +```python +resp = client.rerank.create( + model="gte-rerank-v2", + query="什么是量子计算?", + documents=[ + "量子计算是一种利用量子力学原理进行计算的技术。", + "今天超市鸡蛋打折。", + "量子比特可以同时处于 0 和 1 的叠加态。", + ], + top_n=2, # 可选 + return_documents=True, # 可选 + instruct="...", # 可选 +) +for r in resp.results: + print(r.index, r.relevance_score) + +# 异步:await aclient.rerank.create(...) +``` + +返回 `RerankResponse`: + +```json +{ + "request_id": "...", + "results": [ + {"index": 0, "relevance_score": 0.92, "document": {"text": "……"}}, + {"index": 2, "relevance_score": 0.71, "document": {"text": "……"}} + ], + "usage": {"total_tokens": 40} +} +``` + +`document` 仅在 `return_documents=True` 时携带。 + +### 3.4 `client.tokenization.count`(1.x:`Tokenization.call`) + +```python +# model 必填;messages 与 prompt 至少传一个(同给时 prompt 追加为最新 user 消息) +count = client.tokenization.count( + model="qwen3.7-plus", + messages=[ + {"role": "system", "content": "你是助手"}, + {"role": "user", "content": "你好"}, + ], +) +print(count.usage.input_tokens) + +# 或按纯 prompt 计 +count = client.tokenization.count(model="qwen3.7-plus", prompt="你好,世界") +``` + +返回 `TokenCount`:`{"request_id": "...", "usage": {"input_tokens": 12}}`,另带 +`token_ids` / `tokens` 明细;`count.input_tokens` 是 `usage.input_tokens` 的便捷 +属性。`messages` 与 chat 一样接受纯字符串与 parts 列表。 + +只需要本地分词(不发请求)时用 `dashscope.v2.get_tokenizer(model)`; +`dashscope.v2.list_tokenizers()` 列出可用分词器。 + +## 4. 语音 + +完整参数、双向流示例、元数据和生命周期说明见: + +- [ASR Python SDK:实时识别与文件转写](dashscope-2.0-asr-sdk.md) +- [TTS Python SDK:一次性、输出流式与双向流合成](dashscope-2.0-tts-sdk.md) +- [Realtime Python SDK:Omni、Qwen-Audio 与 Livetranslate](dashscope-2.0-realtime-sdk.md) +- [实时语音翻译(Gummy)Python SDK](dashscope-2.0-translation-sdk.md) +- [语音管理面:词表 / 热词 / 声音复刻 / 声音设计](dashscope-2.0-audio-management-sdk.md) + +### 4.1 `client.audio.asr.recognize(...)` 上下文管理器(1.x:`Recognition.start/send_audio_frame/stop`) + +```python +from concurrent.futures import ThreadPoolExecutor + + +def receive_recognition(rec): + for event in rec.events(): + print(event) + + +with client.audio.asr.recognize( + model="paraformer-realtime-v2", + format="pcm", + sample_rate=16000, +) as rec: + with ThreadPoolExecutor(max_workers=1) as pool: + receiver = pool.submit(receive_recognition, rec) + try: + for chunk in iter_pcm_chunks(): # 由应用提供实时 PCM 帧。 + rec.send_audio_frame(chunk) + rec.stop() + receiver.result() + finally: + rec.close() +``` + +识别走 websocket 双工,无 HTTP JSON 响应;`rec.events()` 逐个产出识别事件 +dict(结构同 1.x Recognition 回调里的服务端事件),`rec.results()` 只产出 +识别结果帧。其余可选参数:`vocabulary_id`(定制词表)/ `phrase_id`(热词)/ +`diarization_enabled`(说话人分离)/ `disfluency_removal_enabled`(去语气词)/ +`input=`(透传线缆 input 段)/ `parameters=`。 +会话方法全集:`start()`(进上下文自动调用)/ `send_audio_frame()` / +`update_context()` / `stop()` / `events()` / `results()` / `close()`; +`rec.request_id` / `first_package_delay` / `last_package_delay` 可取。 +`recognize_file(path, ...)` 支持本地文件一次性识别。 + +### 4.2 `client.audio.asr.transcribe`(1.x:`Transcription.async_call/fetch/wait`) + +```python +# 一步到位:提交录音文件转写并等待完成 +# (QwenTranscription 也走这里,换 model 即可,如 model="qwen3-asr-flash-filetrans") +job = client.audio.asr.transcribe( + ["https://example.com/meeting1.wav", "https://example.com/meeting2.wav"], + model="paraformer-v2", # 可选,默认 "paraformer-v2" + # submit_timeout= / poll_interval= / max_interval= 可选,语义同 images.generate +) +for result in job.results: + print(result.transcription_url) + +# 分步控制 +jobs = client.audio.asr.jobs +job = jobs.create(model="paraformer-v2", file_urls=["https://example.com/a.wav"]) +print(job.task_id) +snapshot = jobs.fetch(job.task_id) +done = jobs.wait(job) + +# 异步:await aclient.audio.asr.transcribe(...) +``` + +`transcribe` 返回已完成 `Job`(结构见 §2.2),转写结果读 +`[r.transcription_url for r in job.results]`。 + +### 4.3 `client.audio.tts.synthesize/stream`(1.x:`SpeechSynthesizer.call`) + +```python +# 一次性合成:拿到完整音频字节后写文件 +# 当前走 CosyVoice SpeechSynthesizer WebSocket 协议,不通用适配所有 TTS 协议。 +audio = client.audio.tts.synthesize( + "你好,欢迎收听今天的节目。", # text 必填(位置参数) + model="cosyvoice-v1", # 可选,默认 "cosyvoice-v1" + voice="longxiaochun", # 可选,默认 "longxiaochun" + format="mp3", # 可选,默认 "mp3" + sample_rate=22050, # 可选,默认 22050 + # 其余可选具名参数:volume(音量)/ rate(语速)/ pitch(音调)/ + # language_hints(语种提示)/ enable_ssml +) +with open("out.mp3", "wb") as f: + f.write(audio) + +# 流式合成(对应 1.x tts_v2 SpeechSynthesizer 的流式回调):边收边播 +with open("out.mp3", "wb") as f: + for chunk in client.audio.tts.stream( + "这是一段比较长的文本,会被逐步合成为音频流。", + model="cosyvoice-v1", # 可选,默认 "cosyvoice-v1" + voice="longxiaochun", # 可选,默认 "longxiaochun" + ): + f.write(chunk) # 或直接喂给播放器 +``` + +`stream` 返回 `Iterator[bytes]`(逐块产出的音频字节);`synthesize` 等价于把流 +拼成完整 `bytes` 返回。这两个便捷入口只输出音频,不暴露逐条 JSON 元数据。 + +逐段输入文本时使用 `client.audio.tts.connect(...)` 会话: +`start()` → 多次 `send_text()` / `flush()` → `stop()` → 读完结果 → `close()`。 +进入上下文会自动 `start()`;发送文本与接收结果应并发。 +会话 `events()` 同时返回音频 `bytes` 和原始 JSON,能够读取时间戳、用量等字段; +`audio_chunks()` 只返回音频,但仍更新会话的 `last_response` 和 `usage`。 +完整示例见 [TTS Python SDK](dashscope-2.0-tts-sdk.md)。 + +### 4.3.1 `client.audio.tts.http.synthesize/stream`(1.x:`HttpSpeechSynthesizer.call` / qwen-tts HTTP) + +真 HTTP POST(不经 WebSocket),sambert/cosyvoice 与 qwen-tts 两族共用,按模型名 +前缀路由端点(`qwen*` → multimodal-generation,其余 → audio/tts/SpeechSynthesizer): + +```python +# 一次性(对应 1.x HttpSpeechSynthesizer.call(stream=False)):音频以临时 URL 返回 +result = client.audio.tts.http.synthesize( + "今天天气怎么样?", + model="sambert-zhichu-v1", + voice="zhichu", + format="wav", + sample_rate=16000, +) +print(result.audio_url, result.audio_id, result.expires_at, result.usage) + +# 流式(对应 1.x qwen SpeechSynthesizer.call(stream=True)):SSE 逐片产出 +with open("out.mp3", "wb") as f: + for chunk in client.audio.tts.http.stream( + "那我来给大家推荐一款T恤,这款T恤是纯棉的。", + model="qwen3-tts-flash", + voice="Cherry", + language_type="Chinese", + ): + if chunk.audio: # 本片音频帧(已 base64 解码) + f.write(chunk.audio) + if chunk.is_final: # finish_reason == "stop",带整段音频 URL + print(chunk.audio_url, chunk.usage) +``` + +模型专属的 `input` 字段(如 cosyvoice 的 `volume/rate/pitch`)经 `extra_input=` 透传, +顶层 `parameters=` 原样放入请求体;具名参数与 `extra_input` 同名时具名优先。 +完整参数与返回类型表见 [TTS Python SDK](dashscope-2.0-tts-sdk.md) 的 HTTP 合成一节。 + +### 4.4 `client.audio.realtime.connect(...)`(1.x:`audio.qwen_omni.OmniRealtimeConversation`) + +```python +# connect() 仅 model 必填;ws_url / connector / timeout / headers 均可选 +with client.audio.realtime.connect(model="qwen-omni-turbo-realtime") as rt: + rt.send_event({ + "type": "session.update", + "session": {"modalities": ["text", "audio"], "voice": "Chelsie"}, + }) + rt.send_event({ + "type": "conversation.item.create", + "item": { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "你好"}], + }, + }) + rt.send_event({"type": "response.create"}) + for event in rt.events(): # 连接关闭时迭代结束 + if event["type"] == "response.text.delta": + print(event["delta"], end="") + elif event["type"] == "response.done": + break +``` + +`RealtimeSession` 还提供 `update_session()`、媒体输入、对话条目和响应控制等便捷方法。 +`recv_event()` 取一条事件,正常关闭后返回 `None`;活动响应或结束确认缺失时会抛异常。 +`rt.model` / `rt.url` 可读模型与 websocket 地址。 +`rt.events()` 产出服务端推送的原始事件 dict;`response.done` 只表示本轮响应结束, +不会自动结束会话。Omni/Live 可调用 `end_session()` 后读取至 `session.finished`; +Qwen-Audio 则读完当前响应后 `close()`,不调用 `end_session()`。 +三类模型的字段映射和流程差异见 [Realtime Python SDK](dashscope-2.0-realtime-sdk.md)。 + +### 4.5 `client.audio.translation.recognize(...)`(1.x:`TranslationRecognizerRealtime.start/send_audio_frame/stop`) + +Gummy 实时翻译(`gummy-realtime-v1` / `gummy-chat-v1`)与 `asr.recognize` 同协议、同会话生命周期, +翻译参数具名化,结果走双语类型化视图(对应 1.x 回调里的 +`(transcription_result, translation_result, usage)` 组合): + +```python +with client.audio.translation.recognize( + model="gummy-realtime-v1", + format="pcm", + sample_rate=16000, + source_language="zh", + translation_enabled=True, + translation_target_languages=["en"], +) as session: + session.send_audio_frame(chunk) # 发送线程逐帧喂音频 + session.stop() + for result in session.results(): # 接收线程迭代双语结果 + transcription = result.get_transcription() + for translation in result.get_translations(): + print(translation["lang"], translation["text"]) +``` + +本地文件一次性翻译(1.x `TranslationRecognizerRealtime.call(file)`): +`client.audio.translation.recognize_file(path, model="gummy-realtime-v1", ...)`, +逐项产出 `TranslationResult`。完整示例见 [实时语音翻译(Gummy)Python SDK](dashscope-2.0-translation-sdk.md)。 +qwen3-livetranslate 实时翻译模型走 `realtime.connect`(4.4),不在本资源面。 + +### 4.5.1 `client.tingwu.call` / `client.tingwu.realtime`(1.x:`TingWu.call` / 实时理解) + +听悟(会议 / 音视频理解)两个入口(对应 1.x `dashscope.multimodal.tingwu` 一对): + +```python +# 离线纪要:一次 POST 返回解析结果 +result = client.tingwu.call( + model="tingwu-turbo", + input={"file_url": "https://example.com/meeting.mp4", ...}, + parameters={...}, # 可选 +) +print(result.output, result.usage) + +# 实时理解:websocket 双工上下文管理器,协议同 asr.recognize +with client.tingwu.realtime( + model="tingwu-turbo", + app_id="your-app-id", # 必填 + format="pcm", # 可选 + sample_rate=16000, # 可选 + terminology="...", # 可选,术语库 + max_end_silence=800, # 可选 +) as session: + session.send_audio_frame(chunk) + session.stop() + for event in session.events(): # 产出服务端事件 dict,speech-end 后结束 + ... +``` + +`client.tingwu.realtime` 支持 `connection=` 复用(§4.6.1);异步侧只有 +`aclient.tingwu.call`(ws 实时类保持同步面,与 audio 其余资源一致)。 + +### 4.6 自定义 websocket 连接:`connector=` + +`audio.asr.recognize` / `audio.tts.connect` / `audio.tts.synthesize` / +`audio.tts.stream` / `audio.realtime.connect` / `audio.translation.recognize` +都接受 `connector=`,用于注入自建连接 +(代理、自定义握手或测试)。SDK 会关闭返回的连接对象,不提供连接池归还协议。 +它是一个**可调用对象**: + +```python +# Connector = Callable[[str, Dict[str, str]], Any] +# 入参是 (url, headers),返回一个 websocket 会话对象; +# 缺省时用内置的 websocket-client 实现。 +def my_connector(url, headers): + return my_ws_library.connect(url, header=[f"{k}: {v}" for k, v in headers.items()]) + +rt = client.audio.realtime.connect( + model="qwen-omni-turbo-realtime", + connector=my_connector, # 可选 +) +``` + +只需要换地址时用 `ws_url=`。ASR/TTS 还支持环境变量 +`DASHSCOPE_WEBSOCKET_BASE_URL`;Realtime 使用独立路径,不使用此环境变量。 + +### 4.6.1 连接复用:`client.ws_connection()` + `connection=` + +一条物理连接上跑多个任务(1.x Java 侧常用的复用模式;语音实时对延时 +敏感,省一次握手就是首包延迟): + +```python +conn = client.ws_connection() # 继承 client 的 key/workspace/region/timeout + +# 顺序复用:第二个任务免握手 +for chunk in (pcm_1, pcm_2): + with client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn) as rec: + rec.send_audio_frame(chunk) + rec.stop() + for result in rec.results(): + ... + +# 并发:1 路 ASR(二进制上行) + 1 路 TTS(二进制下行) + 任意多 JSON 任务 +rec = client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn).start() +tts = client.audio.tts.connect(model="cosyvoice-v1", connection=conn).start() +conn.close() # 由调用方关闭;各会话 close() 只摘离任务、不关共享连接 +``` + +支持 `connection=`:`audio.asr.recognize` / `audio.tts.connect` / +`audio.translation.recognize` / `tingwu.realtime`。协议约束:二进制帧不带 +task_id,单连接二进制上行/下行各单 owner(并发 TTS 需多条连接);realtime +族协议无 task_id、会话结束即关连接,不可复用(压首开延迟用提前 `connect()` +预热)。HTTP 侧所有资源默认共享每 Client 的 keep-alive 池,池参数见 +`settings.http_max_connections` 等(8.5)。详见 +[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 + +### 4.7 管理面:`client.audio.vocabularies` / `phrases` / `voices` / `voice_design`(1.x:`VocabularyService` / `AsrPhraseManager` / `VoiceEnrollmentService` / 声音设计) + +1.x 三个管理类的五动词(`create_*` / `list_*` / `query_*` / `update_*` / `delete_*`) +统一为 `create/list/get/update/delete`(`query_*` → `get`);声音设计在 1.x 无 SDK +类,按官方 REST 建模: + +```python +# 定制词表(1.x VocabularyService):create 返回 vocabulary_id +vocab = client.audio.vocabularies.create( + target_model="paraformer-realtime-v2", + prefix="med", + vocabulary=[{"text": "心肌梗死", "weight": 4}], +) +client.audio.vocabularies.get(vocab.vocabulary_id) +client.audio.vocabularies.list(prefix="med") +client.audio.vocabularies.update(vocab.vocabulary_id, vocabulary=[...]) +client.audio.vocabularies.delete(vocab.vocabulary_id) + +# paraformer 热词(1.x AsrPhraseManager):编译任务,phrase_id 供 asr.recognize 使用 +phrase = client.audio.phrases.create(model="paraformer-v2", phrases={"下一首": 90}) +client.audio.phrases.update(phrase.phrase_id, model="paraformer-v2", phrases={"暂停": 90}) +client.audio.phrases.get(phrase.phrase_id) +client.audio.phrases.list(page_no=1) +client.audio.phrases.delete(phrase.phrase_id) + +# 声音复刻(1.x VoiceEnrollmentService):create 返回 voice_id,可作 TTS voice +voice = client.audio.voices.create( + target_model="cosyvoice-v3-plus", + prefix="demo", + url="https://your-audio-url/sample.wav", +) +client.audio.voices.get(voice.voice_id) +client.audio.voices.list(prefix="demo") +client.audio.voices.update(voice.voice_id, url="https://your-audio-url/new.wav") +client.audio.voices.delete(voice.voice_id) + +# 声音设计(官方 REST,qwen-voice-design):文字描述生成音色,返回 voice + 预览音频 +designed = client.audio.voice_design.create( + target_model="qwen3-tts-vd-realtime-2026-01-15", + voice_prompt="沉稳的中年男性,音色低沉浑厚", + preview_text="各位听众朋友,大家好", +) +client.audio.voice_design.get(designed.voice) +client.audio.voice_design.list() +client.audio.voice_design.delete(designed.voice) +``` + +完整参数与返回类型见 [语音管理面 Python SDK](dashscope-2.0-audio-management-sdk.md)。 + +## 5. 微调 / 部署 / 资源 + +### 5.1 `client.fine_tunes.*`(1.x:`FineTunes.*`) + +```python +# 创建(对应 FineTunes.call) +ft = client.fine_tunes.create( + model="qwen-turbo", + training_file_ids=["file-xxx"], + validation_file_ids=["file-yyy"], # 可选 + mode="sft", # 可选 + hyper_parameters={"n_epochs": 2, "learning_rate": "1e-5"}, # 可选 + finetuned_model="my-ft-name", # 可选 +) +print(ft.job_id, ft.status) + +# 等待终态(对应 FineTunes.wait,退避轮询 + 超时) +done = client.fine_tunes.wait(ft, timeout=7200) # timeout 可选(默认 24h) +print(done.finetuned_model) + +# 事件流(对应 FineTunes.stream_events) +for event in client.fine_tunes.stream_events(ft.job_id): + print(event.output) + +# 训练日志(对应 1.x FineTunes.logs) +logs = client.fine_tunes.logs(ft.job_id, offset=1, lines=200) # offset/lines 可选 +print(logs.content) + +# 其余管理操作 +client.fine_tunes.cancel(ft.job_id) +jobs = client.fine_tunes.list(page_no=1, page_size=10) # page_no/page_size 可选 +one = client.fine_tunes.get(ft.job_id) +client.fine_tunes.delete(ft.job_id) +``` + +`create/get/wait` 返回 `FineTune`(信封已扁平化,字段直接在对象上): + +```json +{ + "request_id": "...", + "job_id": "ft-xxx", + "job_name": "my-ft-name", + "status": "SUCCEEDED", + "model": "qwen-turbo", + "base_model": "qwen-turbo", + "finetuned_model": "ft-20260827-xxxx", + "training_file_ids": ["file-xxx"], + "validation_file_ids": [], + "hyper_parameters": {"n_epochs": 2}, + "created_at": "...", + "finished_at": "..." +} +``` + +`ft.task_id` 是 `job_id` 的别名(与 `Job` 对齐);`ft.succeeded` / `ft.finished` +判断状态。服务端的 `finetuned_output` / `create_time` / `end_time` 是校验别名, +读到同一份值。 + +`list` 返回 `FineTuneList`(`jobs` + `page_no`/`page_size`/`total_count`); +`stream_events` 逐条产出 `FineTuneEvent`(`output` 为事件内容,`is_error` 标记 +错误帧);`logs` 返回 `JobLogs`(`request_id` / `job_id` / `content` 文本); +`cancel` / `delete` 返回 `JobStatus`(`request_id` / `status`)。 + +### 5.2 `client.deployments.*`(1.x:`Deployments.*`) + +```python +# 部署微调产物(对应 1.x Deployments.call) +dep = client.deployments.create( + model="qwen-turbo", + capacity=2, + model_version="ft-20260827-xxxx", # 可选,微调出的模型版本 + suffix="my-suffix", # 可选 + # 其余可选:plan / template_id / deploy_spec / billing_method +) +print(dep.deployed_model, dep.status) + +client.deployments.scale(dep.deployed_model, capacity=4) # 扩缩容 +print(client.deployments.get(dep.deployed_model).status) +for d in client.deployments.list(page_size=20).deployments: # 分页参数可选 + print(d.deployed_model, d.status) # d 为 Deployment,字段直接可读 +for event in client.deployments.stream_events(dep.deployed_model): # 部署事件流 + print(event) +client.deployments.delete(dep.deployed_model) +``` + +`create/get/scale` 返回 `Deployment`(`deployed_model` / `model` / `base_model` / +`status` / `capacity` / `base_capacity` / `ready_capacity` / `workspace_id` / +`charge_type` / `created_at` / `updated_at` + `request_id`);`list` 返回 +`DeploymentList`(`deployments` + `page_no`/`page_size`/`total_count`);`delete` +返回 `JobStatus`(`request_id` / `status`)。 + +`deployed_model` 是后续 `get` / `scale` / `delete` 的定位键,作为位置参数传入。 + +### 5.3 `client.models.list/get`(1.x:`Models.list/get`) + +```python +page = client.models.list(page_no=1, page_size=20, provider="qwen") # 全部可选 +for m in page.models: + print(m.model, m.provider) + +m = client.models.get("qwen3.7-plus") # 不存在抛 NotFoundError +print(m.model) +``` + +`list` 返回 `ModelList`: + +```json +{ + "request_id": "...", + "models": [ + {"model": "qwen3.7-plus", "source": "...", "provider": "qwen", "open_source": false} + ], + "page_no": 1, + "page_size": 20, + "total_count": 100 +} +``` + +`get` 返回单个 `Model`(`list(provider=...)` 的过滤在客户端按 provider/source +匹配)。服务端的 `opensource` 是 `open_source` 的校验别名。 + +### 5.4 `client.files.*`(1.x:`Files.upload/list/get/delete`) + +```python +# purpose="fine-tune" 时本地先做 JSONL 校验 +up = client.files.upload( + file_path="./train.jsonl", + purpose="fine-tune", # 可选,默认 "fine-tune" + description="训练集 v1", # 可选 +) +print(up.file_id) + +for f in client.files.list(page_no=1, page_size=10).files: # 分页参数可选 + print(f.file_id, f.name) + +info = client.files.get(up.file_id) +client.files.delete(up.file_id) +``` + +`upload` / `get` 返回 `File`(`file_id` / `name` / `purpose` / `size` / +`uploaded_at`;信封已扁平化,不再需要 `.data["file_id"]`);`list` 返回 +`FileList`(`files` + `page_no`/`page_size`/`total_count`);`delete` 返回 +`DeletionStatus`(`deleted` 布尔值 + `request_id`)。服务端的 `upload_time` 是 +`uploaded_at` 的校验别名。 + +## 6. Agentic RL + +对应 1.x CLI `dashscope rl` 子命令的编程化使用(`client.rl`; +`client.reinforcement_learning` 是同一实例的长名别名)。资源面分两层: +`client.rl.*` 是本地准备动作,`client.rl.jobs.*` 是训练任务管理。 + +```python +# 注册 rollout / reward 函数(rl register_functions) +registered = client.rl.register_functions( + rollout_classpaths=["my_project.rollout:MyRollout"], # 可选 + reward_classpaths=["my_project.reward:MyReward"], # 可选 + group_reward_classpaths=["my_project.group:MyGroup"], # 可选 + workspace_dir="./", # 可选,默认 "./" + lazy_load=True, # 可选,默认 True +) +print(registered.rollout_instance_ids) + +# 本地测试函数(rl test_functions):三个参数均必填 +client.rl.test_functions( + instance_id=registered.rollout_instance_ids[0], + func_type="rollout", + input_data={"query": "1+1=?"}, +) + +# 上传训练 / 验证数据(rl upload_data) +ids = client.rl.upload_data( + training_files=["./rl_train.jsonl"], + validation_files=["./rl_val.jsonl"], # 可选 +) +print(ids.training_file_ids) + +# 启动完整训练流程(rl run) +job = client.rl.jobs.create(config_path="./rl_config.yaml", job_name="my-rl-job") +# 两参数均可选;config 里的其余字段用 **kwargs 透传 + +# 任务管理(rl get / list / cancel / logs / wait,v2 错误模型) +print(client.rl.jobs.fetch(job.job_id).status) +print(client.rl.jobs.list(page_size=5)) # page_no/page_size 可选 +print(client.rl.jobs.logs(job.job_id, lines=100).content) # offset/lines 可选 +client.rl.jobs.cancel(job.job_id) +client.rl.jobs.delete(job.job_id) +done = client.rl.jobs.wait(job) + +# 函数实例管理(1.x 直通,同样无 timeout=) +client.rl.query_function_instance_logs(instance_id) # 单页日志(原始 dict) +client.rl.query_all_function_instance_logs(instance_id) # 聚合全部分页 +client.rl.delete_function_instance(instance_id) +``` + +返回值:`register_functions` 返回 `RegisteredFunctions`(`rollout_entity_ids` / +`reward_entity_ids` / `group_reward_entity_ids` 及对应的 `*_instance_ids`); +`upload_data` 返回 `UploadedData`(`training_file_ids` / `validation_file_ids`); +`test_functions` 返回 1.x 原对象(业务逻辑复用 1.x);`jobs.create` / +`jobs.fetch` / `jobs.wait` 复用微调资源模型 `FineTune`,`jobs.list` 复用 +`FineTuneList`,`jobs.cancel` 返回 `JobStatus`,`jobs.logs` 返回 `JobLogs` +(结构见 §5.1)。 + +**这些直通方法没有 `timeout=`**:`jobs.create` / `register_functions` / +`test_functions` / `upload_data` / `query_function_instance_logs` / +`query_all_function_instance_logs` / `delete_function_instance` 直通 1.x 业务逻辑 +(涉及本地打包上传与 YAML 解析),超时由 1.x 内部控制;`jobs.fetch` / +`jobs.list` / `jobs.cancel` / `jobs.delete` / `jobs.logs` / `jobs.wait` 走 v2 +传输层,`timeout=` 可用。 + +**但两半的错误行为是统一的**:直通方法的 1.x 异常在挂载层翻成 `SdkError` +(背后没有 HTTP 响应,`status_code is None`,另带目录给出的 `solution`), +任务管理走 v2 传输层抛 `APIStatusError` 子类。两者都是 `DashScopeError` +的子类,所以**一个 `except DashScopeError` 覆盖整个 `client.rl`**(示例见 §9.5)。 + +## 7. AgentStudio(托管 Agent) + +2.0 **完整包含** AgentStudio:`client.agentstudio` 是把 1.x 子 SDK +(`dashscope.agentstudio`)原样懒挂载进来,功能、参数、返回模型与 1.x 完全一致, +不是裁剪版。同步 Client 挂 `agentstudio.Client`,异步 Client 挂 +`agentstudio.AsyncClient`(方法同名同参,加 `await` / `async for` 即可)。 + +典型链路:建 Agent → 开会话 → 发消息 → 流式读回复: + +```python +as_client = client.agentstudio # 首次访问时才构造 + +# 1. 创建托管 Agent(name / model 必填;tools / mcp_servers / skills / +# multiagent / system_prompt / description / metadata 可选) +agent = as_client.agents.create( + name="demo", + model="qwen3.7-plus", + system_prompt="你是助手", # 可选 +) + +# 2. 创建会话(agent 必填,传 id 或 {"id": ..., "version": ...}; +# environment_id / title / resources / vault_ids / +# environment_variables / mcp_configs / metadata 可选) +session = as_client.sessions.create(agent=agent.id, title="第一次对话") + +# 3. 发送事件 + 流式读取回复(user_message 是客户端事件构造辅助函数, +# 同模块还有 user_interrupt / user_tool_result 等) +from dashscope.agentstudio import user_message + +as_client.sessions.events.send(session.id, [user_message("你好")]) +with as_client.sessions.events.stream(session.id) as stream: + for text in stream.text_stream: # 逐条产出完整文本块 + print(text, end="") +# 增量文本:events.stream(session.id, event_deltas=["message"]) 后用 +# stream.text_deltas;直接 for event in stream 则产出类型化 ServerEvent + +# 4. 无需单独关闭:client.close()(异步 aclient.aclose())会连带关闭它 +``` + +资源与方法全集(`*` 为必填;未标注的返回类型即方法名对应模型): + +| 资源 | 方法与关键入参 | 返回 | +| --- | --- | --- | +| `agents` | `create(name*, model*, description/system_prompt/tools/mcp_servers/skills/multiagent/metadata)`、`retrieve(agent_id, version=)`(别名 `get`)、`update(agent_id, version=*, ...)`、`archive(agent_id)`、`list(limit/page/include_archived)`、`list_versions(agent_id)` | `Agent` / `AgentVersion` | +| `sessions` | `create(agent*, environment_id/title/resources/vault_ids/environment_variables/mcp_configs/metadata)`、`retrieve`、`update(title/metadata/vault_ids/...)`、`list(agent_id/statuses/created_at_gt/gte/lt/lte)`、`archive`、`delete` | `Session` / `DeleteResponse` | +| `sessions.events` | `send(session_id, events*)`(空列表抛 `ValueError`)、`list(types/created_at_*/limit/order)`、`stream(session_id, event_deltas=, timeout=)` | `CursorPage[ServerEvent]` / 事件流 | +| `sessions.resources` | `add(session_id, file_id*, resource_type="file", mount_path=)`、`retrieve`、`list`、`delete` | `SessionResource` | +| `sessions.threads` | `list(session_id)`、`retrieve(session_id, thread_id)`、`archive(...)`;`threads.events.list(session_id, thread_id, ...)`(仅 list,无流式) | `SessionThread` | +| `files` | `upload(file*, mime_type=, progress=)`(路径 / 文件对象均可)、`retrieve`、`download(file_id)`(`FileContent` 是 bytes 子类,带 `.write_to_file(path)`)、`list(scope_id=)`、`delete` | `File` | +| `skills` | `create(file_id= 或 file=,二选一)`、`retrieve`、`list(source=)`、`delete`;`skills.versions.create/list/retrieve/download` | `Skill` / `SkillVersion` | +| `vaults` | `create(display_name*, metadata=)`、`retrieve`、`update`、`list(keyword=)`、`archive`、`delete`;`vaults.credentials.create(vault_id, auth*)/retrieve/update/list/archive/delete` | `Vault` / `Credential` | +| `environments` | `create(name*, config*, description/scope/metadata)`(`scope` 默认 `"organization"`)、`retrieve`、`update`、`list`、`archive`、`delete` | `Environment` | +| `deployments` | `create(name*, agent*, initial_events*, description/environment_id/schedule/resources/vault_ids/environment_variables/metadata)`、`retrieve`、`update`、`list(agent_id/keyword/status/created_at_gte/lte)`、`pause` / `unpause` / `archive`、`run(deployment_id)`、`list_runs(deployment_id)` | `Deployment` / `DeploymentRun` | +| `deployment_runs` | `retrieve(deployment_run_id)`、`list` | `DeploymentRun` | +| `webhook_endpoints` | `create(url*, events*, description=)`、`retrieve`、`update`、`delete`、`enable` / `disable`、`test`、`reset_secret`、`list()`、`list_events(webhook_id)` | `WebhookEndpoint` / `WebhookEvent` | +| `security` | `overview()`、`list_agent_logs(current_page/page_size/risk_level/status/risk_name/...)`(页码分页) | `SecurityOverview` / `SecurityAlertList` | + +分页统一为 `CursorPage[T]`(异步 `AsyncCursorPage[T]`):`data` 当前页列表、 +`next_page` 下一页游标、`has_next()` / `get_next()` 手动翻页;直接 `for` 迭代 +会自动翻完所有页。 + +agentstudio 各方法的返回类型是 1.x 子 SDK 自带模型(`Agent` / `Session` / +`ServerEvent` 等),不属于 v2 类型体系,字段以 1.x 为准(响应仍是 `output` +信封,不扁平化)。**异常则已对齐 v2**:挂载层用透明代理把 1.x 异常翻成 +`dashscope.v2.errors` 一族,分两条路—— + +- **HTTP 层错误**(1.x `APIStatusError` 一族):按 `status_code` 进状态映射, + 抛 v2 同名状态类(`BadRequestError` / `AuthenticationError` / + `PermissionDeniedError` / `NotFoundError` / `RateLimitError` / + `InternalServerError` / `ServiceUnavailableError`;409 等目录未收录的状态 + 落通用 `APIStatusError`),`request_id` 保留。1.x 在本地抛的、没带 + `status_code` 的 HTTP 族错误(如缺 `api_key` 的 `AuthenticationError`) + 按其类代表的状态映射,不会退化成 500。 +- **无响应的本地 / 流式错误**(1.x `APIConnectionError` / `APITimeoutError` / + `StreamError` / `StreamClosedError`):翻成 `SdkError`,`code` 为 + `sdk.agentstudio.*`,带目录给出的 `solution` / `allow_retry`(见 §9.5)。 + +所以**一个 `except DashScopeError` 覆盖整个 `client.agentstudio`**: + +```python +from dashscope.v2.errors import DashScopeError, NotFoundError, SdkError + +try: + with client.agentstudio.sessions.events.stream(session.id) as stream: + for text in stream.text_stream: + print(text, end="") +except NotFoundError: + ... # session 不存在(HTTP 404) +except SdkError as e: + ... # 连接 / 超时 / 流式协议错误,e.solution 可直接展示 +except DashScopeError: + ... # 其余全部 +``` + +代理对 `isinstance`(`isinstance(client.agentstudio, AgentStudioClient)` 仍为 +真)、`.transport` 访问和上下文管理协议透明;子资源、流式事件流与分页 +(`CursorPage` / `AsyncCursorPage`)递归包裹,**迭代与翻页中途抛的错误同样被 +翻译**(翻页抓取走的是代理守卫,自动翻页和显式 `get_next()` 都覆盖)。注意 +翻译只发生在 v2 挂载层:直接构造 1.x 的 `dashscope.agentstudio.Client` 仍抛 +原生异常族。 + +懒挂载需要 `workspace`(v2 只向它传 api_key / workspace / region,不透传 +`base_url`):workspace 缺失、且未配置 `DASHSCOPE_AGENTSTUDIO_URL` / +`AGENTSTUDIO_URL` 环境变量时,首次访问 `client.agentstudio` 抛 `ValueError`。 + +## 8. 全局配置 + +### 8.1 Client 参数或 `dashscope.settings`(1.x:`dashscope.api_key` / `save_api_key()`) + +```python +import dashscope + +# 方式一:每个 Client 独立配置(推荐,对应 1.x 配置文件 resolve_config) +client = dashscope.Client( + api_key="sk-xxx", + workspace="ws-xxx", + config_path="~/.dashscope/config.toml", +) + +# 方式二:全局门面,赋值即时生效(对应 1.x 运行时改模块变量) +dashscope.settings.api_key = "sk-xxx" # 未显式赋值时懒读 DASHSCOPE_API_KEY +dashscope.settings.base_url = "https://ws-xxx.cn-beijing.maas.aliyuncs.com/api/v1" +dashscope.settings.workspace = "ws-xxx" +dashscope.settings.region = "cn-beijing" +dashscope.settings.protocol = "native" +dashscope.settings.timeout = 30.0 +dashscope.settings.max_retries = 3 +dashscope.settings.clear() # 清掉显式赋值,回落到环境变量 +``` + +配置优先级:**显式参数 > settings 赋值 > 环境变量 > config.toml > 默认值**。 +`config.toml` 支持 `api_key` / `base_url` / `workspace` / `region` / `protocol` +等键;`api_key` 的最低兜底还有 1.x 的 key 文件 `~/.dashscope/api_key`。 + +`with_options()` 从一个已有 Client 派生出只改若干配置的新实例(共享连接池): + +```python +strict = client.with_options(protocol="openai", timeout=10.0) +``` + +### 8.2 日志级别统一为 `DASHSCOPE_LOG` + +```python +# 环境变量:DASHSCOPE_LOG=DEBUG(取代 1.x 的 DASHSCOPE_LOGGING_LEVEL / LOG_LEVEL) +# 代码内: +dashscope.settings.log_level = "DEBUG" +``` + +### 8.3 websocket 地址 + +```python +# 全局:环境变量 DASHSCOPE_WEBSOCKET_BASE_URL=wss://... +# 或代码内 dashscope.settings.websocket_base_url = "wss://..." +# 单次调用:资源方法的 ws_url= 参数 +audio = client.audio.tts.synthesize( + "你好", + ws_url="wss://ws-xxx.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference", # 单次覆盖 +) +``` + +### 8.4 实例级关闭(1.x:`close_shared_aio_session()`) + +```python +client.close() # 同步 +await aclient.aclose() # 异步 +# 也可用上下文管理: +# with dashscope.Client() as client: ... +# async with dashscope.AsyncClient() as aclient: ... +``` + +### 8.5 HTTP 连接池(keep-alive) + +每个 `Client` 持有一个 httpx 连接池,所有资源的 HTTP 请求默认复用连接 +(1.x 每次调用新建 `requests.Session`);`with_options` 未改连接级选项时 +共享同一个池。池参数走 settings / 环境变量 / config.toml: + +```python +dashscope.settings.http_max_connections = 50 # DASHSCOPE_HTTP_MAX_CONNECTIONS,默认 100 +dashscope.settings.http_max_keepalive_connections = 10 # DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS,默认 20;0 = 关闭 +dashscope.settings.http_keepalive_expiry = 30.0 # DASHSCOPE_HTTP_KEEPALIVE_EXPIRY,默认 5.0 秒 +``` + +池是 per-Client 的:长驻一个 client 才能吃到复用,频繁新建实例会击穿。 + +### 8.6 环境变量一览 + +| 环境变量 | 作用 | 默认 | +| --- | --- | --- | +| `DASHSCOPE_API_KEY` | API Key | — | +| `DASHSCOPE_API_KEY_FILE_PATH` | 从文件读 API Key 的路径 | — | +| `DASHSCOPE_WORKSPACE` | workspace id | —(缺省回退共享网关 + 告警) | +| `DASHSCOPE_REGION` | 地域(六选一) | `cn-beijing` | +| `DASHSCOPE_BASE_URL` | 自定义 HTTP 端点 | — | +| `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | +| `DASHSCOPE_TIMEOUT` | 单次请求超时(秒) | — | +| `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` | +| `DASHSCOPE_CONFIG` | config.toml 路径 | `~/.dashscope/config.toml` | +| `DASHSCOPE_LOG` | 日志级别(debug/info/warning/error) | `warning` | +| `DASHSCOPE_WEBSOCKET_BASE_URL` | ws 端点(不含 realtime 族) | 跟随 workspace/region | +| `DASHSCOPE_HTTP_MAX_CONNECTIONS` | HTTP 连接池上限 | `100` | +| `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | keep-alive 连接数(0=关闭) | `20` | +| `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | keep-alive 空闲回收(秒) | `5.0` | +| `DASHSCOPE_AGENTSTUDIO_URL` / `AGENTSTUDIO_URL` | agentstudio 端点(无 workspace 时的逃生口) | — | + +## 9. 错误处理 + +规范正文见 [`md/dashscope-2.0-design.md`](dashscope-2.0-design.md) §2.7:异常类名与 +HTTP 状态一律从生成的错误目录 `dashscope/v2/_error_registry.py` 派生,不在 SDK 里手写。 + +### 9.1 异常族 + +具体异常类从 `dashscope.v2.errors` import;`dashscope.v2` 只便捷再导出 +`DashScopeError` 与 `ErrorCode`(包根 `dashscope` 都不导出): + +```python +from dashscope.v2 import DashScopeError # 基类,够用时只 import 这一个 +from dashscope.v2.errors import ( + APIStatusError, # 网关声明的失败(9.2 的两层都算) + APIConnectionError, # 连不上,无响应 + APITimeoutError, # 超时(APIConnectionError 子类) + ContentFilterError, # 内容审核拦截(BadRequestError 子类) + SdkError, # SDK 自己报的失败,无响应 + ErrorCode, # 线缆错误码枚举(13 项) +) +``` + +9 个状态类是 `APIStatusError` 的**扁平兄弟**: + +| 类 | `status_code` | 什么时候抛 | +| --- | --- | --- | +| `BadRequestError` | 400 | 参数非法 / 缺参数 / 文件非法 / URL 非法 | +| `AuthenticationError` | 401 | 认证失败 / API Key 无效 | +| `PermissionDeniedError` | 403 | 无权限、未开通、**欠费** | +| `NotFoundError` | 404 | 模型或资源不存在 | +| `RequestTooLargeError` | 413 | 请求体超限 | +| `RateLimitError` | 429 | 限流、并发超限、配额用尽 | +| `InternalServerError` | 500 | 服务端内部错误 | +| `ServiceUnavailableError` | 503 | 服务不可用 | +| `GatewayTimeoutError` | 504 | 网关超时 | + +`ContentFilterError` 是 `BadRequestError` 的**子类**(不是第 10 个兄弟),所以 +`except BadRequestError` 也抓得到审核拦截;要单独处理就把 +`except ContentFilterError` 写在**前面**: + +```python +try: + client.chat.completions.create(model="qwen3.7-plus", messages=[{"role": "user", "content": "..."}]) +except ContentFilterError as e: # 必须排在 BadRequestError 之前 + print("内容被审核拦截", e.request_id) +except BadRequestError as e: # 其余 400 + print("请求参数有问题", e.code, e.message) +``` + +`APIConnectionError` / `APITimeoutError` / `SdkError` 的 `status_code` 是 `None`: +它们背后没有 HTTP 响应。目录未收录的状态码(502 / 507 / 418…)落 +`APIStatusError` 本身,SDK 不猜它是哪一类。 + +### 9.2 两层错误,一个异常族 + +DashScope 网关除了返回非 2xx,也可能返回 **HTTP 200 但 body 带业务错误码** +(`code` / `message`)。两层抛的是**同一族异常**,实例的 `status_code` +说明是哪一层: + +```python +from dashscope.v2.errors import RateLimitError + +try: + resp = client.chat.completions.create(model="qwen3.7-plus", messages=[{"role": "user", "content": "..."}]) +except RateLimitError as e: + # e.status_code == 429 → HTTP 层限流 + # e.status_code == 200 → 业务层限流(线缆码 Throttling / .RateQuota / .AllocationQuota) + # 两种都该退避重试,所以按类 catch 就够,不必区分 + print(e.code, e.status_code, e.request_id) +``` + +线缆码(`InvalidApiKey` / `Throttling` / `DataInspectionFailed`…)与目录 key +是两个命名空间:SDK 内部用 crosswalk 把线缆码对到目录项,再由目录项决定异常类。 +用户只需认类,`e.code` 保留原始线缆码供排障。业务层出现的**未知线缆码** +(crosswalk 未收录)落 `DashScopeError` 基类本身,`status_code` 同样是 200。 + +### 9.3 异常上能拿到什么 + +每个异常都携带同一组字段: + +```python +try: + client.images.generate(model="wan2.2-t2i-flash", prompt="一只猫") +except DashScopeError as e: + e.message # 人类可读消息 + e.code # 线缆错误码,如 "Throttling.RateQuota";SDK 侧错误是目录名 + e.status_code # HTTP 状态;业务层是 200;无响应时是 None + e.request_id # 排障用,提工单必带 + e.body # 原始响应体 + e.headers # 响应头 +``` + +`SdkError` 额外有 `solution` / `allow_retry`(见 9.5)。 + +### 9.4 重试 + +`Client(max_retries=N)` 默认 **2** 次,自动重试 408 / 409 / 429 / 500 / 502 / +503 / 504 与连接错误,优先按服务端的 `Retry-After`(含 `retry-after-ms`)等待, +否则指数退避 + 抖动。重试发生在传输层,按**原始状态码**判定,与最终抛出的 +异常类无关;服务端也可以用 `x-should-retry` 响应头显式要求或禁止某次重试 +(优先于状态码判定)。 + +SDK 已经重试过还抛出来,说明这一轮没救回来。是否再手工重试要看错误类别: + +```python +import time +from dashscope.v2.errors import ( + GatewayTimeoutError, + RateLimitError, + ServiceUnavailableError, +) + +# 只有这几类值得再来一轮;401/403/404/400 重试没有意义,同样的请求还会同样失败 +RETRYABLE = (RateLimitError, ServiceUnavailableError, GatewayTimeoutError) + +for attempt in range(3): + try: + resp = client.chat.completions.create( + model="qwen3.7-plus", + messages=[{"role": "user", "content": "..."}], + ) + break + except RETRYABLE: + if attempt == 2: + raise + time.sleep(2**attempt) +``` + +### 9.5 `SdkError`:SDK 自己报的失败 + +`client.rl` 的 1.x 直通方法(`jobs.create` / `register_functions` / +`test_functions` / `upload_data` / 三个函数实例管理方法)涉及本地打包上传与 +YAML 解析,失败时背后**没有 HTTP 响应**,抛 `SdkError`: + +```python +from dashscope.v2.errors import SdkError + +try: + client.rl.upload_data(training_files=["./rl_train.jsonl"]) +except SdkError as e: + e.code # 目录名,如 "sdk.agentic_rl.OSSUploadError" + e.solution # 目录随附的修复建议,可直接展示给用户 + e.allow_retry # 该错误是否值得重试;SDK 不自动重试这一层 + e.status_code # None + e.__cause__ # 原始 1.x 异常,排障时可看 +``` + +`SdkError` 是 `DashScopeError` 的子类,所以 `client.rl` 的两半——直通 1.x 的 +工作流步骤、走 v2 传输的任务管理——**一个 `except DashScopeError` 全覆盖**: + +```python +try: + registered = client.rl.register_functions(rollout_classpaths=["my_project.rollout:MyRollout"]) + job = client.rl.jobs.create(config_path="./rl_config.yaml") + done = client.rl.jobs.wait(job) +except DashScopeError as e: + # 上传失败 → SdkError;任务查询/取消失败 → APIStatusError 子类 + print(type(e).__name__, e.code, e.message, e.request_id) +``` + +`client.agentstudio` 同样做了这层翻译(§7):挂载层代理把 1.x 异常翻成 v2 +族——HTTP 层错误进状态映射,无响应的本地 / 流式错误翻成 `SdkError` +(`sdk.agentstudio.*` 一族),`except DashScopeError` 同样全覆盖。 + +### 9.6 关掉抛异常:`raise_on_error=False` + +```python +lenient = client.with_options(raise_on_error=False) +resp = lenient.chat.completions.create( + model="qwen3.7-plus", + messages=[{"role": "user", "content": "..."}], +) +if resp.is_error: # ResponseBase 从非空 code 自动推导 + print(resp.code, resp.message, resp.request_id) +``` + +返回值模式下不抛异常,改由每个顶层响应继承的 `ResponseBase` 携带 +`code` / `message` / `is_error`。流式则是产出一个 `is_error=True` 的对象后 +结束迭代(`client.apps` 对齐 1.x `Application` 语义)。**连接失败与超时仍然 +抛异常**:那两种情况根本没有响应对象可返回。 diff --git a/md/dashscope-2.0-interfaces-mapping.md b/md/dashscope-2.0-interfaces-mapping.md new file mode 100644 index 00000000..933a5fd4 --- /dev/null +++ b/md/dashscope-2.0-interfaces-mapping.md @@ -0,0 +1,159 @@ +# dashscope 2.0 接口映射核对表(M3 3.9 / 3.10) + +对照 [`md/dashscope-interfaces.md`](dashscope-interfaces.md) 第一部分(SDK 接口), +核对 `dashscope/v2/` 资源树的覆盖情况。 + +状态图例:✅ 已迁移 | 🔀 收敛合并 | ❌ 确定下线 | ⚠️ 缺口(未迁移) + +## 文本 / 对话 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `Generation.call` | `client.chat.completions.create` | ✅ | +| `AioGeneration.call` | `AsyncClient.chat.completions.create` | ✅ | +| `Conversation.call` | `client.chat.completions.create`(history 转 messages) | 🔀 | +| `CodeGeneration.call` | `client.chat.completions.create`(同一端点,model 区分) | 🔀 | +| `Understanding.call` | `client.chat.completions.create`(同一端点,model 区分) | 🔀 | +| `Application.call` | `client.apps.completions.create(app_id=..., prompt=...)` | ✅ | + +## 多模态 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `MultiModalConversation.call` | `client.multimodal.chat.create`(content parts) | ✅ | +| `AioMultiModalConversation.call` | `AsyncClient.multimodal.chat.create` | ✅ | +| `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` + `client.images.jobs.create/fetch/wait` | ✅ | +| `ImageGeneration.call/async_call/fetch/wait`(wan2.6-t2i / wan2.6-image) | `client.images.generate`(同步打 multimodal-generation 端点)+ `client.images.jobs.create/fetch/wait`(异步任务打 image-generation 端点);`client.images` 按模型名自动路由协议 | ✅ | +| `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` + `client.videos.jobs.create/fetch/wait` | ✅ | +| `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | ✅ | +| `AioMultiModalEmbedding.call` | `AsyncClient.multimodal.embeddings.create` | ✅ | + +> `client.chat.completions.create` 会检查 messages,含 image/audio/video part 时 +> 自动改走多模态端点(签名与返回类型不变);`client.multimodal.chat.create` 保留 +> 为显式逃生口——无论有无媒体 part 都打到多模态端点(如给 qwen-vl 发纯文本)。 +> 兼容协议(`openai`/`anthropic`)无独立多模态端点,两者是同一次调用。 +> +> `client.images` 同样按模型名路由:旧 text2image 端点仅支持 wan2.5 及以下模型; +> wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`messages` 入参 / +> `choices` 出参),同步结果与任务结果都归一化为统一 `Job`(`results` / `image_urls`)。 + +## 向量 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `TextEmbedding.call` | `client.embeddings.create` | ✅ | +| `BatchTextEmbedding`(call/fetch/wait) | `client.embeddings.jobs.create/fetch/wait`(复用 Job 基建,M3 3.10) | ✅ | + +## 排序 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `TextReRank.call` | `client.rerank.create` | ✅ | +| `AioTextReRank.call` | `AsyncClient.rerank.create` | ✅ | + +## 分词 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `Tokenization.call` | `client.tokenization.count` | ✅ | + +## 语音 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器(`send_audio_frame/stop/events`) | ✅ | +| `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe(file_urls)` + `client.audio.asr.jobs.*` | ✅ | +| `audio.qwen_asr.QwenTranscription` | 同上(`transcribe(model=...)` 按模型区分) | 🔀 | +| `SpeechSynthesizer.call`(sambert/cosyvoice) | `client.audio.tts.synthesize/stream`(ws,`model/voice` 区分) | ✅ | +| `audio.tts_v2.SpeechSynthesizer`(CosyVoice2 流式) | `client.audio.tts.stream` | 🔀 | +| `HttpSpeechSynthesizer.call` | `client.audio.tts.http.synthesize/stream`(M4,HTTP 形态;ws 统一入口亦可) | ✅ | +| `audio.qwen_tts.SpeechSynthesizer` | `client.audio.tts.http.*`(M4 按模型名前缀路由)/ `client.audio.tts.synthesize`(ws) | ✅ | +| `TranslationRecognizerRealtime` / `TranslationRecognizerChat` | `client.audio.translation.recognize(...)`(M3) | ✅ | +| `VocabularyService` / `AsrPhraseManager` | `client.audio.vocabularies` / `client.audio.phrases`(M5) | ✅ | +| `VoiceEnrollmentService`(声音复刻) | `client.audio.voices`(M5;qwen 声音设计另建模为 `client.audio.voice_design`,1.x 无对应类) | ✅ | +| `audio.qwen_omni.OmniRealtimeConversation` | `client.audio.realtime.connect(...)`(M2 起含 omni 类型化助手) | ✅ | + +> audio 资源树的推理入口为 `asr` / `tts` / `realtime` / `translation`,管理面为 +> `vocabularies` / `phrases` / `voices` / `voice_design`;`asr` 下直接是 +> `transcribe` / `recognize` / `jobs.*`,不再有 `transcription` 中间层。 +> 实时语音连接复用(一条 ws 多任务/顺序复用)见 +> [`dashscope-2.0-connection-reuse-sdk.md`](dashscope-2.0-connection-reuse-sdk.md)(M6)。 + +## 微调 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `FineTunes.call` | `client.fine_tunes.create` | ✅ | +| `FineTunes.cancel/list/get/delete` | `client.fine_tunes.cancel/list/get/delete` | ✅ | +| `FineTunes.stream_events` | `client.fine_tunes.stream_events` | ✅ | +| `FineTunes.wait` | `client.fine_tunes.wait`(退避轮询 + 超时) | ✅ | +| `FineTunes.logs` | `client.fine_tunes.logs`(rl 侧同一能力为 `client.rl.jobs.logs`) | ✅ | + +## 部署 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | ✅ | + +## 资源 + +| 1.x 接口 | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `Models.list/get` | `client.models.list/get` | ✅ | +| `Files.upload/list/get/delete` | `client.files.upload/list/get/delete` | ✅ | + +## Agentic RL + +| 1.x 接口(CLI `dashscope rl`) | 2.0 资源 / 方法 | 状态 | +| --- | --- | --- | +| `rl register_functions` | `client.rl.register_functions`(复用 1.x 实现) | ✅ | +| `rl test_functions` | `client.rl.test_functions` | ✅ | +| `rl upload_data` | `client.rl.upload_data` | ✅ | +| `rl run` | `client.rl.jobs.create` | ✅ | +| `rl get/cancel/logs/list` | `client.rl.jobs.fetch/cancel/logs/list`(v2 HTTP/错误模型) | ✅ | +| `rl wait`(CLI 轮询) | `client.rl.jobs.wait`(退避轮询 + 超时) | ✅ | + +> `client.reinforcement_learning` 是 `client.rl` 的长名别名(同一实例)。 +> rl 资源面分两层:`client.rl.*` 是本地准备动作,`client.rl.jobs.*` 是训练任务 +> 管理(与 `images.jobs` / `fine_tunes` 同一套 Job 语义)。 +> **`jobs.create` / `register_functions` / `test_functions` / `upload_data` +> 这四个 1.x 直通方法不接受 `timeout=`**(本地打包上传与 YAML 解析的超时由 1.x +> 内部控制);其余 `jobs.*` 走 v2 传输层,`timeout=` 可用。 + +## Assistants API【确定下线】 + +| 1.x 接口 | 状态 | +| --- | --- | +| `Assistants.create/retrieve/list/update/delete` | ❌ | +| `Threads.create/retrieve/update/delete/create_and_run` | ❌ | +| `Messages.create/retrieve/list/update/delete` | ❌ | +| `Runs.*` | ❌ | +| `Steps.list/retrieve` | ❌ | + +托管 Agent 场景由 `client.agentstudio` 承接(1.x 子 SDK 懒挂载,逻辑不动)。 + +## AgentStudio + +| 接口 | 2.0 挂载 | 状态 | +| --- | --- | --- | +| `agentstudio.Client.agents/sessions/events/files/skills/vaults/environments/deployments` | `client.agentstudio.*`(懒挂载 1.x `Client`/`AsyncClient`) | ✅ | + +## 全局配置 + +| 1.x | 2.0 | 状态 | +| --- | --- | --- | +| `dashscope.api_key` / `save_api_key()` | `Client(api_key=...)` + 配置文件(`resolve_config`) | 🔀 | +| `dashscope.api_key` / `base_*` 模块变量运行时修改 | `dashscope.settings` 全局门面(赋值即时生效,懒读 env,M3 3.11) | ✅ | +| `DASHSCOPE_LOGGING_LEVEL` / `LOG_LEVEL` 两套日志级别 | `DASHSCOPE_LOG` 统一(完整级别)+ `settings.log_level` setter | 🔀 | +| `base_http_api_url` / `base_compatible_api_url` | `Client(base_url=..., protocol=...)` | 🔀 | +| `base_websocket_api_url` | `DASHSCOPE_WEBSOCKET_BASE_URL` 环境变量 + 资源 `ws_url=` 参数 | 🔀 | +| `close_shared_aio_session()` | `client.close()` / `await client.aclose()` | 🔀 | + +## 缺口清单 + +| 缺口 | 1.x 对应 | 说明 | +| --- | --- | --- | +| ~~`client.embeddings` 多模态向量化~~ | `MultiModalEmbedding` / `AioMultiModalEmbedding` | 已补齐(M3 3.10):`multimodal.embeddings.create`,端点 `/services/embeddings/multimodal-embedding/multimodal-embedding` | +| ~~`client.embeddings` 批量任务~~ | `BatchTextEmbedding` | 已补齐(M3 3.10):`embeddings.jobs.create/fetch/wait`,复用 3.2 Job 基建 | + +> M3 3.10 后核对表无遗留缺口(除确定下线的 Assistants API 族)。 diff --git a/md/dashscope-2.0-m1-m3-review.md b/md/dashscope-2.0-m1-m3-review.md new file mode 100644 index 00000000..66d4f590 --- /dev/null +++ b/md/dashscope-2.0-m1-m3-review.md @@ -0,0 +1,182 @@ +# dashscope 2.0 M1–M3 深度 Review + +> 评审对象:`V2` 分支 `dashscope/v2/`(HEAD = 0c58a74),对照 [`dashscope-2.0-design.md`](dashscope-2.0-design.md) 的 M1/M2/M3 任务表与 P1–P6 原则。 +> 评审方法:逐文件通读 v2 全部源码(约 6000 行)+ 对照设计文档逐任务核对 + 运行测试套件(**254 passed / 3 skipped**,skipped 为 env-gated 的 live 集成测试)+ git 考古验证零接触承诺。 +> 评审日期:2026-08-28。 +> +> **命名已过时**:本文引用的方法名(`apps.call`、`embeddings.create_multimodal`、`asr.recognition`、`asr.transcription.transcribe` 等)在随后的命名统一中已改名,现行名称与设计约束见 [`dashscope-2.0-naming-review.md`](dashscope-2.0-naming-review.md) 与 [`dashscope-2.0-design.md`](dashscope-2.0-design.md) §2.6。本文结论按当时状态保留,不作回填。 + +--- + +## 0. 总体结论 + +**M1–M3 的功能骨架已按设计落地,资源树 100% 映射(除计划下线的 Assistants),测试覆盖扎实;但设计的最高优先级原则 P2(契约正确性优先)仍有三处未闭合的"静默错误"缺口,另有若干健壮性问题集中在重试、流式聚合与兼容开关路径上。** + +| 维度 | 评价 | +| --- | --- | +| 功能实现完整度 | ★★★★☆ 任务表 21/21 项有形交付;agentstudio 的 errors/HTTP 对齐例外条款未兑现,`with_options()` 缺失,AsyncAudio 面不全 | +| 代码健壮性 | ★★★☆☆ 主干路径稳;3 处 P2 级静默错误 + 重试/聚合/兼容开关路径若干中危问题 | +| 架构与理念一致性 | ★★★★☆ 分层/隔离/资源树严格兑现;P6 codegen 进度偏弱,1 行 1.x 触碰超出白名单 | + +--- + +## 1. 功能实现完整度(按任务表逐项核对) + +### M1 — SDK 2.0 骨架(1.1–1.10) + +| # | 任务 | 状态 | 证据与备注 | +| --- | --- | --- | --- | +| 1.1 | Client/AsyncClient + 配置解析 | ✅ 有偏差 | `client.py`、`v2/_config.py` 齐备。**偏差**:`resolve_config` 中 `protocol/timeout/max_retries` 三个键的 env 槽位硬编码为 `None`(`_config.py:220-259`),只有 `api_key/base_url/workspace` 读环境变量——与 3.11 声明的优先级链「显式 > settings > env > config.toml」不一致(env 环断了三键)。 | +| 1.2 | transport(httpx 双栈 + 重试) | ✅ | `transport/http.py`:Retry-After 解析、指数退避 + 抖动、`x-should-retry` 头均在。健壮性问题见 §2-①。 | +| 1.3 | 异常体系 + ErrorCode + 双层检查 + 兼容开关 | ✅ | `errors.py`:`ErrorCode` 枚举、`_STATUS_MAP`/`_CODE_MAP`、HTTP-200-业务失败双层检查、`raise_on_error` 开关全部落地。 | +| 1.4 | 日志改造 | ✅ | `_logging.py`:request_id/状态码/重试决策入日志,`SensitiveHeadersFilter` 同时挂 handler 与子 logger(22fc5d8 修复),`DASHSCOPE_LOG` 一键开启。 | +| 1.5 | 内部规范消息模型 | ✅ | `types/messages.py`:归一化 + 边界加固(dcb8b33)。 | +| 1.6 | client.chat | ✅ | Pydantic 响应、`@overload` 区分流式/非流式、`output_text`、`text_stream`/`get_final_message`、`reasoning_content` 全部在。聚合缺陷见 §2-③。 | +| 1.7 | embeddings / rerank / tokenization | ✅ | 三资源齐备;tokenization 的 prompt→messages 合并与 rerank instruct 参数经 909110d 修正。 | +| 1.8 | files / models | ✅ | 齐备(69f1711 修正 wire 协议)。`provider` 过滤为客户端实现,见 §2-⑦。 | +| 1.9 | 包根导出 | ✅ | `dashscope/__init__.py:84` 导出 `Client/AsyncClient/chat/settings`;附 705 行手写 `__init__.pyi`。 | +| 1.10 | specs 元数据原型 | ⚠️ 偏弱 | `specs/chat.yaml` 存在,但 M1 之后无新增——与 P6「specs 从 M1 起同步建设」有差距,目前仍是单文件试点。 | + +### M2 — 协议兼容层(2.1–2.6) + +| # | 任务 | 状态 | 证据与备注 | +| --- | --- | --- | --- | +| 2.1 | ChatProtocol ABC + native | ✅ | `protocols/base.py` + `native.py`,接口收敛干净。 | +| 2.2 | openai adapter | ✅ | `openai.py`:strict 开关与透传(e786de8)、`reasoning_content` 回显(9aba4cd)、`stream_options.include_usage`。小问题见 §2-⑪。 | +| 2.3 | anthropic adapter | ✅ | `anthropic.py`(449 行):per-stream 状态隔离(bf6cbab)、system 提取、tool_use/tool_result 合并、默认 max_tokens。8.27 修复后是三 adapter 中最扎实的。 | +| 2.4 | SSE 统一解析 | ✅ | `transport/sse.py`:流内错误抛异常、request_id 透传、畸形帧包装(1a2daef),另有 435 行专项验证套件。 | +| 2.5 | acli providers 收敛 | ✅ | 严格遵守「不改同步副本」:协议翻译只在 `v2/protocols/`,`dashscope/acli/providers/` 零改动。 | +| 2.6 | 兼容端点集成测试 | ⚠️ 形式达标 | `tests/v2/protocols/test_integration_endpoints.py` 存在(百炼 compatible-mode + 通用网关 3 例),但全部 env-gated——CI 默认 skip,「兼容端点可直连」无持续回归保障。建议至少在发版流水线中跑一次真实 key。 | + +### M3 — SDK 全量覆盖(3.1–3.11) + +| # | 任务 | 状态 | 证据与备注 | +| --- | --- | --- | --- | +| 3.1 | multimodal.chat | ✅ | content parts 全链路;与 chat 的复制问题见 §3-P6。 | +| 3.2 | Job 基建 | ✅ | `_jobs.py`:`Job` 统一对象 + 退避轮询(1s→8s 封顶)。`DEFAULT_WAIT_TIMEOUT=86400` 与 1.x 的语义差异见 §2-⑫。 | +| 3.3 | images / videos | ✅ | `generate` 同步封装 + `jobs.create/fetch/wait`,模式统一。 | +| 3.4 | audio.asr | ✅ | 批转写走 Job 基建;实时 Recognition 为 ws 上下文管理器,fake connector 可测。 | +| 3.5 | audio.tts + audio.realtime | ⚠️ 有缺口 | 同步面齐备(synthesize/stream + omni session),fe03bc6 已修 realtime 吞传输错误。**缺口**:`AsyncAudio` 只挂了 `asr.transcription`(`audio/__init__.py:55-57`)——异步用户没有 `tts`、`realtime`、`asr.recognition`。ws 实时接口以同步为主可辩护,但资源树 sync/async 不对称且文档未声明,属未解释的缺口。 | +| 3.6 | fine_tunes / deployments | ✅ | 完整 CRUD + `stream_events` + `wait`(sleep 可注入,测试友好)。 | +| 3.7 | rl 薄挂载 | ✅ | 复用 1.x `agentic_rl`,job 管理(get/list/cancel/logs)走 v2 transport + 错误模型,`client.reinforcement_learning` 别名在。符合「业务逻辑不动」的约束。asyncio.run 风险见 §2-⑥。 | +| 3.8 | apps + agentstudio 挂载 | ⚠️ 例外未兑现 | apps 完整(含流式 + compat 错误事件)。**agentstudio 未兑现设计例外条款**:3.8 明确要求「errors 与底层 HTTP 调用对齐 2.0」,但 `client.py:112-122` 的懒挂载只传了 `api_key/workspace`,1.x AgentStudio 的 HTTP 与错误体系原样保留——挂载层形同 pure pass-through。要么实现对齐,要么修订设计文档豁免此项。 | +| 3.9 | 映射核对表 + 全量冒烟 | ✅ | `dashscope-2.0-interfaces-mapping.md` 声明无遗留缺口(除 Assistants 下线);`tests/v2/smoke/test_full_surface.py`(641 行)逐资源过一遍。 | +| 3.10 | embeddings 缺口补齐 | ✅ | `create_multimodal` + 批量 `jobs.*`(68c0e87)。 | +| 3.11 | settings 全局门面 | ✅ | 懒读 env、setter 即时生效、包根导出。线程安全与 clear() 语义见 §2-⑨。 | + +**任务表外的小缺口**:设计 §2.2 提到「`Client(max_retries=N)` / `with_options()` 可调」——`with_options()`(派生修改配置的客户端副本,OpenAI/Anthropic SDK 均有)未实现。要么补上(很便宜:Config 是 frozen dataclass,`dataclasses.replace` 即可),要么从设计文档删去。 + +--- + +## 2. 代码健壮性(按严重度分级) + +### 高危(P2 级——设计原则点名要消灭的"静默错误") + +**① 传输层错误逃逸 SDK 异常体系** +`transport/http.py:113-122`(同步)与 `:252-261`(异步):重试耗尽后 `raise` 原样抛出裸 `httpx.TransportError`。而 `errors.py:68-72` 定义的 `APIConnectionError`/`APITimeoutError` **全库零引用**——定义了不用。后果:用户按文档 `except DashScopeError` 无法捕获连接错误/超时,异常驱动契约在最常见的失败路径上破防(OpenAI SDK 此处映射为 `APIConnectionError`)。 +**修法**:重试耗尽与 `open_stream` 连接失败时映射进 `APIConnectionError`(超时映射 `APITimeoutError`),保留 `__cause__`。 + +**② chat 非流式在 `raise_on_error=False` 下静默吞错** +`chat/completions.py` 非流式路径:`check_response(response, raise_on_error=False)` 静默返回后,紧接着 `protocol.parse_completion(response.json())` 把错误体解析成一个**空的、看似成功的** ChatCompletion 返回。这正是 P2 点名的最高危类别在兼容开关下复发。对比 apps 资源 compat 模式至少产出 `is_error=True` 事件(f3805d8),chat 连标记都没有。 +**修法**:compat 模式下检查响应体 `code` 字段,将其原样放进返回模型(1.x `GenerationResponse` 语义),或在 ChatCompletion 上补 `is_error/code/message`。 + +**③ `get_final_message()` 对 OpenAI 协议 tool_calls 聚合错误** +`chat/completions.py:144-145`:`_aggregate` 对 `tool_calls` 是「最后一个 chunk 覆盖」。native/anthropic 的流式 tool_calls 是累积式,碰巧正确;OpenAI 协议是非累积片段(按 index 分发、arguments 逐段追加),覆盖式聚合会丢工具、截断参数。 +**修法**:按 `index` 归并、字符串字段追加(OpenAI 语义),或按协议分发聚合策略。 + +### 中危 + +**④ `files.upload` 重试会发空 body** +`files.py:136-143`:`with open(...) as f` 把活句柄塞进 multipart;transport 重试时句柄已消耗,第二次尝试发空文件(httpx 不会重读流式 body)。 +**修法**:上传路径重试前 `f.seek(0)` 不可控——应读入 bytes 或对该请求禁用重试。 + +**⑤ 流内错误处理 chat 与 apps 行为分裂** +设计 §2.2:「流内错误统一抛异常,不 yield 错误对象」。chat/SSE 遵守(且 `raise_on_error=False` 也不影响,流内错误照样 raise);apps 在 compat 模式 yield `is_error=True` 事件。两种语义并存且均无文档说明——用户换资源即踩坑。需要统一,或在设计文档中把 apps 列为显式例外。 + +**⑥ rl 同步方法用 `asyncio.run` 包 1.x 异步实现** +`reinforcement_learning.py:136/153/168/191`:在已有运行中 event loop 的环境(Jupyter、FastAPI、pytest-asyncio)直接 `RuntimeError`。1.x 用户群里有 notebook 场景,这是真实风险。 +**修法**:短期文档标注「同步 rl 方法不可在运行中 loop 内调用」;中期给 1.x agentic_rl 补同步入口(属 M 外工作)。 + +**⑦ `models.list(provider=...)` 客户端过滤误导分页** +`models.py:90-93`:只过滤当前页结果,`total_count` 不调整——过滤后 `total_count=100` 但本页只有 3 条,分页消费者会被误导。服务端不支持 provider 过滤时,应至少在文档/docstring 中声明「total_count 为过滤前值」。 + +**⑧ `resolve_config` 三键不读环境变量** +同 1.1 偏差(`_config.py:220-259`)。`DASHSCOPE_PROTOCOL`/`DASHSCOPE_TIMEOUT` 之类的 env 不生效,运维侧无法用 env 统一切协议。 + +### 低危(记录即可) + +- **⑨** `Settings` 用普通 dict 无锁(`_config.py:52` 起),并发赋值非线程安全;`log_level` setter 有副作用(调 `set_log_level`)但 `clear()` 不恢复 logger 级别——状态泄漏。 +- **⑩** `transport/ws.py:180-183` `recv()` 仍 broad-except(finished 时吞异常返回 None);8.27 修了 `recv_event` 与 realtime,`recv()` 的宽松行为是 TTS 路径在用,可接受但应加注释说明取舍。 +- **⑪** openai adapter `parse_completion` 用 `payload["id"]`(completion id)充当 `request_id`(`openai.py:136/153`)——排障工单对不上,语义错位。 +- **⑫** `DEFAULT_WAIT_TIMEOUT=86400` 与 1.x SDK `wait(timeout=-1)`(无限等待)不对齐;1.x CLI 是 3600。86400 是有意的有限等待折中(防挂死),方向正确,但应在迁移指南中明示。 +- **⑬** `fine_tunes.py` 的 `_check_terminal` 在 sync/async 两个类中逐字重复(`:340-353` 与 `:520-533`)——sync/async 手写复制已开始产生实际重复,M6 codegen 之前的漂移风险现实存在。 +- **⑭** `__init__.pyi` 705 行手写 stub 与实现双维护,codegen 缺位期间必然漂移。 + +--- + +## 3. 架构设计与理念一致性 + +### P1 对齐行业基线 —— ✅ 高 + +Client 实例化 + 资源树导航、httpx 双栈、Pydantic 响应、异常层级、自动重试、`output_text` 便捷属性、`dashscope.chat()` 2 行上手——OpenAI/Anthropic 验证过的基线全部落地,未自创风格。扣分项:`with_options()` 缺失;传输错误未纳入 SDK 异常体系(§2-①)——这两项恰是 Stainless 系 SDK 的标配。 + +### P2 契约正确性优先 —— ⚠️ 部分违反 + +设计的自我定位是「修复静默错误的优先级高于任何新能力」,且开发过程确实在践行:8.25–8.27 的修复提交链(1a2daef 流内错误带 request_id、e786de8 错误信封解析、bf6cbab 流状态隔离、f3805d8 apps compat 错误事件、fe03bc6 realtime 传输错误传播)全部是契约类修复。但 §2-①②③ 三处残留恰好都在 P2 点名的类别里(吞错、错误逃逸、聚合撒谎),**建议作为 M3 收尾项清零后再进 M4**。 + +### P3 渐进迁移 / 零接触隔离 —— ✅ 基本达标(1 处轻微越界) + +git 考古(`git log master..HEAD -- 'dashscope/**' ':!dashscope/v2/**'`)确认 2.0 工作对 1.x 的触碰仅 2 处: + +1. `dashscope/__init__.py` 追加导出(f68a311、e6142da)——设计 §2.4 规则 3 明确要求,合规。 +2. `dashscope/finetune/reinforcement/common/constants.py` 的 `LOGGER_NAME` 从 `"dashscope"` 改为 `"dashscope.finetune.reinforcement"`(0c58a74)——动机正当(避免 1.x rl 的 logger 名与包根 logger 混淆),但规则 1 只允许安全补丁、规则 10 只允许 DeprecationWarning,这一行是 v2 动机驱动的 1.x 行为变更,**超出白名单**。影响极小(日志名变化),建议在设计文档白名单中补记豁免,不宜静默破例。 + +另注:V2 分支同时承载 1.x 常规迭代(v1.26.6–v1.27.2 发布、CLI 打包、Recognition 参数显式化等提交),2.0 与 1.x 发布共用分支——不违反隔离(文件层面无交叉),但发布节奏耦合值得注意。 + +### P4 保留自有优势 —— ✅ 高 + +长任务统一收敛为 `Job` + `jobs.create/fetch/wait`(images/videos/embeddings 批量/asr 转写四处复用同一基建),多模态 content parts 进规范消息模型,RL/微调/部署全流程完整迁入。有限化 wait 超时(86400)是增强而非削弱。无能力下降项。 + +### P5 差异化 CLI 定位 —— 本期范围外(M4 起) + +### P6 codegen 是主线 —— ⚠️ 偏弱 + +`specs/` 目前只有 M1 的 `chat.yaml` 一个试点,M2/M3 期间无新增。与此同时手写复制面在扩大:sync/async × chat/multimodal 的 create/stream/aggregate 脚手架近乎重复(normalize 后 diff 仍有 232 行)、`__init__.pyi` 手写 705 行、`_check_terminal` 已出现逐字重复(§2-⑬)。「specs 从 M1 起同步建设」的执行力度落后于计划,复制债务在 codegen 接管前会持续累积。 + +### §3.3 第三方厂商结论落实 —— ✅ + +reasoning_content 回显(9aba4cd)、`models.list(provider=)` 过滤(客户端)、ErrorCode 覆盖审核/配额类错误——三个 SDK 侧落点都在;CLI 示例属 M4 范围。 + +### 目录布局 —— ✅ + +`v2/` 独立实现、`tests/v2/` 独立测试、包根只导新面,与 §7 目标布局一致。形式偏差:设计写 `resources/images/`、`resources/videos/` 目录,实际是单文件——无关紧要。 + +--- + +## 4. 建议行动项(按优先级) + +| 优先级 | 事项 | 出处 | +| --- | --- | --- | +| P0 | 传输错误映射进 `APIConnectionError`/`APITimeoutError`,删除零引用死类或启用之 | §2-① | +| P0 | chat 非流式 compat 模式不得静默吞错(返回体带 code/is_error) | §2-② | +| P0 | 修 `get_final_message()` 的 OpenAI tool_calls 片段聚合 | §2-③ | +| P1 | `files.upload` 重试空 body(读 bytes 或禁重试) | §2-④ | +| P1 | 统一 chat/apps 流内错误语义,或在设计文档显式豁免 apps | §2-⑤ | +| P1 | 兑现 3.8 agentstudio「errors/HTTP 对齐 2.0」或修订设计文档 | §1-3.8 | +| P2 | 补 `with_options()` 或从设计文档删除 | §1 | +| P2 | AsyncAudio 补齐 tts/realtime 或文档声明 ws 资源仅同步 | §1-3.5 | +| P2 | `resolve_config` 三键 env 补齐或文档化 | §1-1.1 | +| P2 | rl 同步方法 event-loop 冲突写入文档限制 | §2-⑥ | +| P3 | `models.list` 文档声明 total_count 为过滤前值 | §2-⑦ | +| P3 | Settings 线程安全与 `clear()` 恢复 logger | §2-⑨ | +| P3 | 设计文档白名单补记 0c58a74 的 1.x LOGGER_NAME 豁免 | §3-P3 | +| P3 | specs 元数据恢复节奏(P6 纪律);openai request_id 语义修正 | §3-P6、§2-⑪ | +| P3 | 迁移指南写明 `DEFAULT_WAIT_TIMEOUT=86400` 与 1.x 的差异 | §2-⑫ | + +--- + +## 5. 测试证据 + +- `pytest tests/v2`:**254 passed / 3 skipped**(skipped 为 M2 2.6 的 live 集成测试,env-gated)。 +- 测试资产:23 个测试文件,含协议层 947 行(`test_protocols.py`)+ SSE 专项 435 行 + 全资源冒烟 641 行;openai/anthropic 各有 conformance 套件。 +- 冒烟测试覆盖核对表全部资源(chat/multimodal/embeddings/rerank/tokenization/files/models/images/videos/audio asr·tts·realtime/fine_tunes/deployments/rl/apps/agentstudio/async 面)。 diff --git a/md/dashscope-2.0-migration-guide.md b/md/dashscope-2.0-migration-guide.md new file mode 100644 index 00000000..6e4b10cb --- /dev/null +++ b/md/dashscope-2.0-migration-guide.md @@ -0,0 +1,179 @@ +# DashScope 1.x → 2.0 迁移指南 + +> 2.0 是一次有意的重设计,不是 1:1 移植:接口形状向 OpenAI/Anthropic 习惯收敛, +> 管理面与批量任务对齐并增强,推理面差异集中在错误处理与等待语义(§3)。 +> 本文汇总迁移时必须知道的**接口对照、行为差异与不在 2.0 范围的能力**。 +> 逐接口完整核对表见 [`dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md); +> 2.0 各域用法见 [`dashscope-2.0-interfaces-examples.md`](dashscope-2.0-interfaces-examples.md)。 + +## 1. 共存与分层 + +- `dashscope>=2.0` 同一个发布包内含 1.x 与 2.0 两套接口,不做双包分发;1.x 进入 + 冻结维护(只修缺陷、不增功能),存量代码可以先不动,按模块逐步迁移。 +- 2.0 实现直接位于 `dashscope` 根包,`Client` / `AsyncClient` / `chat` / + `settings` / `WsConnection` / `errors` 等均从根包导入;原 `dashscope.v2` + 命名空间已移除。 +- 包根的 `Generation` / `ImageSynthesis` 等 1.x 导出保持不变,两套接口可在 + 同一进程混用(共用 `DASHSCOPE_API_KEY` 等环境变量)。旧 + `dashscope.aigc`、`dashscope.client.base_api`、`dashscope.files` 等 deep import + 通过懒加载别名解析到 `dashscope.legacy_1x`,存量代码无需修改,也不新增警告。 +- AgentStudio 与 FineTune/Agentic RL 的物理实现归入 `dashscope.applications`; + 原 `dashscope.agentstudio`、`dashscope.finetune` 公开路径通过别名保持不变。 + +## 2. 三分钟对照 + +### 2.1 鉴权与全局配置 + +| 1.x | 2.0 | +| --- | --- | +| `dashscope.api_key = "sk-..."` / `save_api_key()` | `Client(api_key=...)`;缺省读 `DASHSCOPE_API_KEY`;支持 `~/.dashscope/config.toml` | +| 运行时改 `dashscope.api_key` 等模块变量 | `dashscope.settings` 全局门面,赋值即时生效 | +| `base_http_api_url` / `base_compatible_api_url` | `Client(base_url=..., protocol=...)` | +| `base_websocket_api_url` | `DASHSCOPE_WEBSOCKET_BASE_URL` 环境变量或资源级 `ws_url=` 参数 | +| `DASHSCOPE_LOGGING_LEVEL` / `LOG_LEVEL` 两套级别 | `DASHSCOPE_LOG` 统一 + `settings.log_level` | +| `close_shared_aio_session()` | `client.close()` / `await aclient.aclose()`(或上下文管理器) | + +### 2.2 各域速查 + +| 域 | 1.x | 2.0 | +| --- | --- | --- | +| 文本生成 | `Generation.call` / `AioGeneration.call` | `client.chat.completions.create`(AsyncClient 同名) | +| 多模态对话 | `MultiModalConversation.call` | 同上(含媒体 part 自动路由)或显式 `client.multimodal.chat.create` | +| 图像 | `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` + `client.images.jobs.create/fetch/wait` | +| 视频 | `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` + `client.videos.jobs.*` | +| 向量 | `TextEmbedding.call` / `BatchTextEmbedding` | `client.embeddings.create` / `client.embeddings.jobs.*` | +| 多模态向量 | `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | +| 排序 | `TextReRank.call` | `client.rerank.create` | +| 分词 | `Tokenization.call` | `client.tokenization.count`(本地分词 `dashscope.tokenizers.get_tokenizer`) | +| 百炼应用 | `Application.call` | `client.apps.completions.create(app_id=...)` | +| 实时识别 | `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器 | +| 批量转写 | `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe(file_urls)` + `client.audio.asr.jobs.*` | +| 语音合成 | `SpeechSynthesizer.call`(sambert/cosyvoice/qwen) | `client.audio.tts.synthesize/stream`(ws);`client.audio.tts.http.*`(HTTP) | +| 实时翻译 | `TranslationRecognizerRealtime` | `client.audio.translation.recognize(...)` | +| Omni 实时 | `OmniRealtimeConversation` | `client.audio.realtime.connect(...)`(omni 类型化助手) | +| 词表/热词/声纹 | `VocabularyService` / `AsrPhraseManager` / `VoiceEnrollmentService` | `client.audio.vocabularies` / `phrases` / `voices`(+ `voice_design`) | +| 微调 | `FineTunes.*` | `client.fine_tunes.*` | +| 部署 | `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | +| 模型/文件 | `Models.list/get`、`Files.*` | `client.models.*`、`client.files.*` | +| Agentic RL | CLI `dashscope rl ...` | `client.rl.*` + `client.rl.jobs.*` | + +```python +# 1.x +from dashscope import Generation +resp = Generation.call(model="qwen3-max", prompt="你好") +if resp.status_code == 200: + print(resp.output.text) + +# 2.0 +import dashscope +client = dashscope.Client() # 缺省读 DASHSCOPE_API_KEY +resp = client.chat.completions.create(model="qwen3-max", messages="你好") +print(resp.choices[0].message.content) # 失败直接抛异常,见 §3.1 +``` + +## 3. 行为差异(迁移前必读) + +### 3.1 错误处理:返回信封 → 抛异常 + +1.x 所有调用返回响应对象,由调用方自查 `status_code` / `code`;2.0 默认 +`raise_on_error=True`,失败直接抛异常族(`DashScopeError` 及其子类,按错误目录 +从线缆码映射): + +```python +# 1.x +resp = Generation.call(...) +if resp.status_code != 200: + print(resp.code, resp.message) + +# 2.0 +from dashscope import DashScopeError +try: + resp = client.chat.completions.create(...) +except DashScopeError as e: + print(e.code, e.message, e.status_code, e.request_id) + +# 兼容模式:恢复信封风格,自查 is_error / code / message +client = dashscope.Client(raise_on_error=False) +``` + +### 3.2 异步任务等待语义 + +| 维度 | 1.x(`wait_timeout=-1`) | 2.0(`Jobs.wait`) | +| --- | --- | --- | +| 等待超时 | 默认无限等待;超时**返回**错误响应 | 默认 `timeout=86400`(24h);超时**抛** `DashScopeError` | +| 任务 FAILED | 返回响应,自查 `output.task_status` | `raise_on_error=True` 时按错误目录抛映射异常;`=False` 时返回终态 `Job` | +| 任务 CANCELED | 返回响应 | `raise_on_error=True` 时抛 `DashScopeError`;`=False` 时返回 `Job` | +| 轮询节奏 | 固定间隔 | 指数退避(`poll_interval` → `max_interval`) | + +### 3.3 视频 `prompt_extend` 默认值 + +1.x `VideoSynthesis.call` 的 deprecated 别名参数 `extend_prompt` **默认 `True`** +(`dashscope/legacy_2x/aigc/video_synthesis.py:63`):不显式传参时默认开启提示词扩展。 +2.0 只保留 `prompt_extend=None`(缺省由服务端决定),不恢复 `extend_prompt` +别名。**依赖 1.x 默认开启的调用,迁移后需显式传 `prompt_extend=True`。** + +### 3.4 流式输出形态 + +1.x `Generation.call(stream=True)` 默认每帧**重放全量**文本(需 +`incremental_output=True` 才给增量);2.0 流式一律**增量**(OpenAI 形态), +自行拼接: + +```python +stream = client.chat.completions.create(model="qwen3-max", messages="你好", stream=True) +for chunk in stream: + delta = chunk.choices[0].delta + if delta.content: + print(delta.content, end="", flush=True) + # 思考模型另有 delta.reasoning_content +``` + +百炼应用(`client.apps.completions`)保留 `incremental_output` 开关,帧形态由它决定。 + +### 3.5 根 `save_api_key` 的实现来源 + +包根 `save_api_key` 现由 `dashscope._config` 提供,签名仍为 +`save_api_key(api_key, api_key_file_path=None)`,默认写入 +`~/.dashscope/api_key`。与旧 1.x writer 相比,新实现显式使用 UTF-8、展开 +`~`,并为自定义路径创建缺失的父目录;`DASHSCOPE_API_KEY_FILE_PATH` 仍只 +影响读取,不改变默认写入位置。需要旧 writer 时可从 +`dashscope.legacy_1x.common.api_key` 显式导入。 + +### 3.6 日志名称与隔离 + +`DASHSCOPE_LOG` 和 `settings.log_level` 的环境变量、级别及脱敏语义不变。 +当前客户端使用独立的 `dashscope._logging` logger 层级,不再以 `v2` 命名, +也不会通过配置共享的 `dashscope` 根 logger 改变 1.x 的 level、handlers 或 +filters。按名称获取当前客户端 logger 时应使用 `dashscope._logging`。 + +## 4. 2.0 新能力(迁移后可选用) + +- 全资源 `AsyncClient` 异步孪生(1.x 仅零散 aio 入口)。 +- pydantic 强类型响应(`extra="allow"` 兜底,字段可点出)。 +- 连接复用:`client.ws_connection()` 一条 ws 承载多任务/顺序复用,全模型 HTTP + keep-alive 连接池——见 [`dashscope-2.0-connection-reuse-sdk.md`](dashscope-2.0-connection-reuse-sdk.md)。 +- `client.with_options(...)` 派生不同配置的客户端,共享连接池。 +- 音频专项:HTTP 合成 `client.audio.tts.http`、实时翻译 `client.audio.translation`、 + 词表/热词/声纹管理面、omni 类型化助手(1.x 均无对应或形态不同)。 + +## 5. 不在 2.0 范围的能力 + +以下能力 **v2 不提供**;均已评审拍板,不再视为缺口。 + +| 能力 | 1.x 位置 | 处置 | 迁移建议 | +| --- | --- | --- | --- | +| Assistants / Threads / Runs | `dashscope/legacy_2x/assistants/`、`dashscope/legacy_2x/threads/` | **随 1.x 下线,2.0 有意不包含** | 勿用于新代码;托管 Agent 场景评估 `client.agentstudio` 或自建编排 | +| CodeGeneration | `legacy_2x/aigc/code_generation.py` | hold,2.0 先不支持 | 继续用 1.x;代码生成场景可直接走 `chat.completions` 同一端点 | +| nlp Understanding | `legacy_2x/nlp/understanding.py` | hold,2.0 先不支持 | 继续用 1.x | +| MultiModalDialog | `legacy_2x/aigc/multimodal_dialog.py` | hold,2.0 先不支持 | 继续用 1.x;或改 `client.multimodal.chat.create` | +| Conversation / History | `dashscope/legacy_2x/aigc/conversation/` | hold,2.0 先不支持 | 继续用 1.x;多轮对话以 messages 数组自行维护(`chat.completions`) | +| `dashscope` CLI(v2 版) | acli(自包含,走 1.x) | v2 无 CLI;计划项,后续再加 | 命令行场景继续用 1.x CLI | + +## 6. 相关文档 + +- [DashScope 2.0 设计规范](dashscope-2.0-design.md) +- [2.0 接口示例总览](dashscope-2.0-interfaces-examples.md) +- [2.0 接口映射核对表](dashscope-2.0-interfaces-mapping.md) +- [2.0 vs 1.x 对齐评审](dashscope-2.0-vs-1.x-alignment-review.md) +- 语音分册:[ASR](dashscope-2.0-asr-sdk.md) / [TTS](dashscope-2.0-tts-sdk.md) / + [实时翻译](dashscope-2.0-translation-sdk.md) / [Realtime](dashscope-2.0-realtime-sdk.md) / + [连接复用](dashscope-2.0-connection-reuse-sdk.md) / [音频管理面](dashscope-2.0-audio-management-sdk.md) diff --git a/md/dashscope-2.0-migration.md b/md/dashscope-2.0-migration.md new file mode 100644 index 00000000..0ef3480a --- /dev/null +++ b/md/dashscope-2.0-migration.md @@ -0,0 +1,344 @@ +# DashScope 2.0 根目录迁移与 `legacy_2x` 归档方案 + +> 状态:设计稿,尚未实施。 +> +> 前置方案:`md/dashscope-2.0-agentstudio-agenticrl-migration.md`。 +> 本文只描述后续目录迁移;不修改 `dashscope-error-registry`,不重复实施 +> AgentStudio / AgenticRL 错误体系统一,也不调整已经完成的 `examples/` 迁移。 + +## 1. 目标 + +1. `dashscope/v2/` 不再作为实现目录,2.0 实现提升到 `dashscope/` 根目录; + 根目录公开 API(`dashscope.Client`、`dashscope.AsyncClient` 等)与实现位置一致。 +2. AgentStudio、AgenticRL / reinforcement 遵循前置方案已经确认的结论: + - 单一实现; + - 原生使用统一错误体系; + - 保留既有 1.x API 行为和需要兼容的导入路径; + - `dashscope.reinforcement` 是 reinforcement 的公开入口, + `dashscope.finetune.reinforcement` 继续可用。 +3. `dashscope/acli/` 保留在根目录,不归档。 +4. 其余属于 2.x 阶段旧公开面的目录和模块迁入 `dashscope/legacy_2x/`, + 明确标记为后续下线,不在本次迁移中直接删除。 +5. `tests/v2/` 提升为 `tests/` 根目录;其余测试套件迁入 + `tests/legacy_1x/`。测试发现、CI、coverage、pre-commit、测试内部导入和 + 路径计算同步修正。 +6. `examples/` 已完成,本次不再搬迁或改写。 + +## 2. 范围界定与不可混淆的目录 + +“除 acli 外其他 2.x 目录”按本文 §4 的显式清单执行,不能解释为“移动除 +acli 外的所有目录”。以下目录有特殊职责,不纳入 `legacy_2x`: + +| 目录 | 处理 | +|---|---| +| `dashscope/acli/` | 当前 CLI,保留根目录 | +| `dashscope/_shared/` | 跨层共享基础设施,保留根目录 | +| `dashscope/legacy_1x/` | 既有 1.x 业务实现,不是本次新建的归档目录 | +| `dashscope/agentstudio/` | 提升为 2.0 实现目录,遵循前置方案 | +| `dashscope/reinforcement/` | 提升为 reinforcement 实现目录和公开入口 | +| `dashscope/finetune/` | 保留 1.x finetune API 与 AgenticRL 兼容面,按 §3.3 合并 | +| `dashscope/tokenizers/` | 合并 2.0 tokenizer 实现与既有兼容导出,不整体归档 | +| `dashscope/v2/` | 内容迁出后删除该实现命名空间 | + +`legacy_2x` 是 2.x 阶段旧 API / 兼容面的过渡归档位置;`legacy_1x` 是其下层 +历史实现的现有位置。本次不把两者合并,也不借目录迁移制造第二份实现。 + +`dashscope.v2` 当前定位为内部实现命名空间,不作为需要长期保留的公开兼容面。 +迁移时所有内部 import、patch target、动态模块字符串和测试统一改为根目录新路径; +最终不保留 `dashscope/v2/` 实现副本。 + +## 3. `dashscope/` 迁移设计 + +### 3.1 目标结构(关键部分) + +```text +dashscope/ +├── __init__.py # 合并导出,不用 v2/__init__.py 覆盖 +├── client.py # 当前 Client / AsyncClient 实现 +├── errors.py # 当前统一错误体系 +├── _error_registry.py # vendored catalog,来源和 pin 不变 +├── _config.py +├── _logging.py +├── _quickstart.py +├── agentic_rl.py # 当前 AgenticRL 实现,见 §3.3 +├── agentstudio/ # 当前 AgentStudio 实现 +├── reinforcement/ # 当前 reinforcement 实现及公开入口 +├── finetune/ +│ ├── _submit.py # 当前 submit transport +│ ├── agentic_rl.py # 既有 1.x AgenticRL 行为和返回类型 +│ └── reinforcement/ # 旧 reinforcement 导入路径的 alias +├── protocols/ +├── resources/ +├── transport/ +├── types/ +├── tokenizers/ # 合并后的实现与资源 +├── acli/ +├── _shared/ +├── legacy_1x/ +└── legacy_2x/ # 过渡归档,后续另行下线 +``` + +### 3.2 常规 v2 文件映射 + +| 当前位置 | 目标位置 | 说明 | +|---|---|---| +| `dashscope/v2/client.py` | `dashscope/client.py` | 现有 `dashscope/client/` 先按 §4 归档,消除文件/包冲突 | +| `dashscope/v2/errors.py` | `dashscope/errors.py` | catalog 映射和错误类不变 | +| `dashscope/v2/_error_registry.py` | `dashscope/_error_registry.py` | 不改 registry 上游或 vendored 内容 | +| `dashscope/v2/_config.py` | `dashscope/_config.py` | 仅调整内部导入 | +| `dashscope/v2/_logging.py` | `dashscope/_logging.py` | 仅调整内部导入 | +| `dashscope/v2/_quickstart.py` | `dashscope/_quickstart.py` | 仅调整内部导入 | +| `dashscope/v2/protocols/` | `dashscope/protocols/` | 整目录提升 | +| `dashscope/v2/resources/` | `dashscope/resources/` | 整目录提升 | +| `dashscope/v2/transport/` | `dashscope/transport/` | 整目录提升 | +| `dashscope/v2/types/` | `dashscope/types/` | 整目录提升 | +| `dashscope/v2/tokenizers/` | `dashscope/tokenizers/` | 合并,不覆盖既有兼容导出 | +| `dashscope/v2/__init__.py` | 合并进 `dashscope/__init__.py` | 根导出仍以现有 `dashscope/__init__.py` 为基线 | +| `dashscope/v2/__init__.pyi`(若后续存在) | 合并进 `dashscope/__init__.pyi` | 类型导出与运行时一致 | + +仅存在 `__pycache__` 的 `devkit/` 不属于 tracked source,不纳入迁移清单; +清理生成物不作为源码迁移提交。 + +### 3.3 AgentStudio / AgenticRL / reinforcement 特殊映射 + +这些路径不能直接按相对目录覆盖,否则会破坏前置方案已经确认的兼容契约。 + +#### AgentStudio + +- `dashscope/v2/agentstudio/*` → `dashscope/agentstudio/*`,由 shim 位置变为唯一实现。 +- 现有 `dashscope.agentstudio` 导入路径不变,公开类、helper、分页和 SSE 行为不变。 +- 异常继续原生使用根目录统一错误体系;旧异常别名继续保留。 +- 原 `tests/v2/test_agentstudio_shim.py` 中“1.x 路径与 v2 路径是同一模块”的断言 + 不能机械替换为自身比较,应改为验证公开导出、deep import、错误别名和可选依赖行为。 +- `dashscope/v2/agentstudio/` 随 v2 目录退出,不保留第二份实现。 + +#### AgenticRL + +- `dashscope/v2/finetune/agentic_rl.py` → `dashscope/agentic_rl.py`,避免覆盖 + 现有 1.x `dashscope/finetune/agentic_rl.py`。 +- `dashscope/v2/finetune/_submit.py` → `dashscope/finetune/_submit.py`,submit + wire format、header 顺序和返回 envelope 不变。 +- 根 `Client(...).rl` 及其 mount 改为从 `dashscope.agentic_rl` 和 + `dashscope.reinforcement` 导入。 +- `dashscope.finetune.agentic_rl.AgenticRL` 继续保留 1.x 子类、五个任务管理动词、 + `FineTunes` 委托和 `FineTune` 返回类型;当前实现继续返回 `SubmitResult`。 +- 不因目录迁移把 1.x 动词合并进当前实现,也不让两个路径返回类型悄悄一致。 + +#### reinforcement + +- `dashscope/v2/finetune/reinforcement/*` → `dashscope/reinforcement/*`, + 由公开 shim 位置变为唯一实现。 +- `dashscope.finetune.reinforcement` 保留为 alias,deep import 仍解析到同一模块对象。 +- `dashscope.reinforcement` 保持公开 FC / uvicorn / `python -m` dotted path: + `dashscope.reinforcement.component.server.server`。 +- 懒加载约束不变:`component.server.*` 和 + `component.observability.genai.*` 不得因包迁移变成 eager import,也不得把 + fastapi / opentelemetry 提升为根包硬依赖。 +- `dashscope.finetune.agentic_rl` 的 re-export 和旧 CLI 继续可用。 +- 数字 `error_code` 兼容、catalog code / solution / allow_retry、root cause 行为不变。 + +### 3.4 根导出与冲突处理 + +1. `dashscope/__init__.py` 和 `.pyi` 采用合并方式,当前 2.0 导出成为规范来源。 +2. 过渡期内仍需保留的根级旧导出可重定向到 `legacy_2x`,其最终删除与 + `legacy_2x` 下线一起评审;不能在目录迁移提交中静默扩大删除范围。 +3. `dashscope/client.py` 与旧 `dashscope/client/` 不能共存为同一导入名。旧包迁入 + `legacy_2x` 后,旧的 `dashscope.client.*` 深层导入属于兼容性变化,必须在迁移 + 指南和破坏性变更清单中单独列出。 +4. tokenizer 的 `qwen.tiktoken` 随实现移动,运行时 loader 与 `MANIFEST.in` + 必须使用同一路径;不能手改生成的 `egg-info` 来伪装迁移完成。 +5. 所有 import 修改同时覆盖字符串形式:monkeypatch、`importlib`、runpy、uvicorn、 + entry point、Sphinx module mapping 和规格文件,不能只改静态 `from/import`。 + +## 4. `dashscope/legacy_2x/` 归档与下线标记 + +### 4.1 本次归档清单 + +以下现有顶层旧公开面包目录迁入 `dashscope/legacy_2x/`,保持目录名: + +```text +aigc/ api_entities/ app/ assistants/ audio/ cli/ client/ common/ +embeddings/ io/ multimodal/ nlp/ protocol/ rerank/ threads/ utils/ +``` + +以下顶层旧模块一并迁入: + +```text +files.py model.py models.py +``` + +`agentstudio/`、`reinforcement/`、`finetune/`、`tokenizers/` 不在该清单中, +按 §3 的特殊规则处理。`acli/`、`_shared/`、`legacy_1x/` 也不归档。 + +### 4.2 归档后的行为 + +- `dashscope/legacy_2x/__init__.py` 和各入口 docstring 标注“过渡兼容,后续下线”。 +- 在包入口使用带正确 `stacklevel` 的 `DeprecationWarning`,并通过测试固定其含义; + 不改为静默删除或立即抛出导入异常。 +- 归档代码继续复用既有实现,不能复制出另一套业务逻辑。 +- `setup.py` 的 console entry point 从 `dashscope.cli:main` 调整到归档后的实际 + 位置,直至该旧 CLI 正式下线;acli 入口不受影响。 +- `setuptools.find_packages()` 会继续打包 `dashscope.legacy_2x`,这是过渡期预期行为; + 正式下线时再单独调整打包与根导出。 +- 本次不确定具体删除版本。下线前另行完成使用面评估、迁移指南、发布说明和删除 PR。 + +## 5. `tests/` 迁移设计 + +### 5.1 目录映射 + +| 当前位置 | 目标位置 | +|---|---| +| `tests/v2/test_*.py` | `tests/test_*.py` | +| `tests/v2/protocols/` | `tests/protocols/` | +| `tests/v2/smoke/` | `tests/smoke/` | +| `tests/unit/` | `tests/legacy_1x/unit/` | +| `tests/integration/` | `tests/legacy_1x/integration/` | +| `tests/legacy/` | `tests/legacy_1x/legacy/` | +| `tests/data/` | 保持在 `tests/data/` | + +`tests/data/` 是共享 fixtures,不是测试套件,不归档,避免无意义地修改全部数据 +路径。`__pycache__`、coverage 和 pytest cache 是生成物,不纳入迁移。 + +`tests/legacy_1x/` 是测试物理归档位置,不等于立即停止执行。过渡期内其中仍包含 +acli、打包、共享层、AgenticRL 等有效架构守卫,必须通过独立 legacy CI job 继续运行; +后续可按测试实际保护对象再拆分,不在目录迁移 PR 中顺手重写全部测试策略。 + +### 5.2 测试代码需要同步的内容 + +1. 所有 `dashscope.v2...` 导入和 patch target 改为 §3 的根实现路径。 +2. `tests.unit...` 改为 `tests.legacy_1x.unit...`;为新增包层级补齐必要的 + `__init__.py`,保证既有跨测试模块导入可解析。 +3. `tests.legacy...` 等动态导入按新包路径调整;`tests/data` 引用保持不变。 +4. 修正依赖 `__file__` 层级的 repository-root 计算: + - 原 `tests/v2/` 文件上移一级; + - 原 unit / integration / legacy 文件下移一级。 +5. 重写依赖物理目录或模块所有权的架构测试,而不是仅替换字符串: + - AgentStudio shim 身份断言; + - reinforcement 三命名空间、模块身份和懒加载断言; + - package root 所有权断言; + - layer decoupling 的 v2 路径、registry 物理路径和中立包清单。 +6. 测试文档中的命令统一改为当前套件、legacy 套件和全量套件三种形式。 + +## 6. 测试流程与配置修正 + +### 6.1 pytest 与 CI + +建议固定以下命令,避免“移动目录后 CI 实际没有运行当前 2.0 测试”: + +```bash +# 当前套件(根 tests,排除过渡归档) +python -m pytest tests --ignore=tests/legacy_1x -q + +# 过渡 legacy 套件(过渡期继续执行) +python -m pytest tests/legacy_1x -q + +# 全量回归 +python -m pytest tests -q +``` + +- `pytest.ini` 的 `testpaths = tests` 可保留,但必须通过 `norecursedirs` 或默认命令 + 明确当前套件是否包含 `legacy_2x`;配置和 CI 的语义必须一致。 +- `.github/workflows/unit_test.yml` 当前只运行 `tests/unit`,需改为当前根测试的 + coverage job,并增加 `tests/legacy_1x` 的独立 job;不能只把字符串改成 + `tests/legacy_1x/unit` 后宣称 2.0 已验收。 +- coverage 继续以实际运行的测试为准;若增加 source / omit 配置,根实现与 + `legacy_2x` 的覆盖口径要分别说明。 +- `requirements-test.txt`、`setup.py`、`.gitignore` 中关于 `tests/unit` 的说明同步更新。 + +### 6.2 pre-commit 与静态检查 + +- `.pre-commit-config.yaml` 中所有 `tests/legacy` exclude 改为 + `tests/legacy_1x/legacy`,逐个核对正则锚点,避免旧排除失效后突然对归档代码启用 + 新规则。 +- 对当前根测试保持现有 Black / lint 规则;归档测试沿用迁移前的排除策略。 +- 路径迁移后运行 import / patch target 搜索,禁止残留指向已删除的 + `dashscope/v2` 或 `tests/v2` 的有效配置。 + +### 6.3 打包、规格和文档 + +- `MANIFEST.in`:`dashscope/v2/tokenizers/qwen.tiktoken` 改为新根路径。 +- `setup.py`:核对 `find_packages()`、package data、console entry point; + 重新生成构建元数据,不手改 `dashscope.egg-info/SOURCES.txt`。 +- `specs/chat.yaml` 的 v2 module mapping 改为根模块。 +- 根 README、迁移指南和仍有效的架构文档更新规范导入路径;历史设计稿可保留当时 + 背景,但若作为操作手册使用则必须注明新路径。 +- `docs/source` 的历史 API 文档如继续发布,导入目标需指向保留的兼容路径或在 + `legacy_2x` 下线时同步下线。 + +## 7. 破坏性变化与兼容边界 + +| 项目 | 本次处理 | +|---|---| +| `dashscope.v2` 内部实现路径 | 退出,内部引用全部迁移;不保留第二实现 | +| 根公开 API(`dashscope.Client` 等) | 名称和行为保持,改为根实现直接提供 | +| `dashscope.agentstudio` | 路径和公开行为保持,shim 位置变为实现 | +| `dashscope.finetune.agentic_rl` | 保留 1.x 子类、动词和 `FineTune` 返回 | +| 当前 AgenticRL 实现 | 新实现位置为 `dashscope.agentic_rl`,仍返回 `SubmitResult` | +| `dashscope.reinforcement` | 公开路径保持,变为实现位置 | +| `dashscope.finetune.reinforcement` | 保留 alias 和模块身份 | +| 旧 `dashscope.client.*` deep import | 随旧包进入 `legacy_2x`,列入迁移指南 | +| 其他归档旧 API | 过渡期从 `legacy_2x` 可用;根级保留项按 §3.4 单独审查 | +| `tests/data` | 不变 | +| `examples` | 已完成,本次不改 | + +## 8. 分阶段实施建议 + +1. **准备与基线**:提交前记录完整测试基线;确认工作区中的 AgentStudio / + reinforcement 修改已纳入基线,避免迁移覆盖未提交工作。 +2. **包迁移**:先处理 `legacy_2x` 和目标冲突,再移动常规 v2 文件,最后处理 + AgentStudio / AgenticRL / reinforcement 特殊映射与根导出。 +3. **引用迁移**:修改静态和动态导入、entry point、package data、specs; + 搜索确认没有有效实现引用旧 v2 路径。 +4. **测试迁移**:提升当前测试、归档 legacy 测试、修复包导入和路径深度, + 重写身份 / 分层架构断言。 +5. **流程迁移**:修正 pytest、CI、coverage、pre-commit 和测试文档。 +6. **验收与发布说明**:执行 §9;单独列出 `legacy_2x` 过渡政策和后续下线事项。 + +每个阶段保持可审查的小提交;目录移动与大规模行为改写不混在同一提交中。 +本次只做位置、引用和必要兼容调整,不顺便重构无关 API。 + +## 9. 验收标准 + +### 9.1 当前实现 + +```bash +python -m pytest tests --ignore=tests/legacy_1x -q +coverage run -m pytest tests --ignore=tests/legacy_1x +coverage report -m +``` + +至少额外固定: + +- 根 `Client` / `AsyncClient`、配置、错误、transport、types、protocols 可导入; +- AgentStudio 公开导入、deep import、SSE 和错误别名行为; +- 两类 AgenticRL 返回类型和 1.x 动词; +- reinforcement 两条公开路径的模块身份、runpy、uvicorn dotted path 和可选依赖懒加载; +- tokenizer 资源能从源码树和 wheel 读取。 + +### 9.2 过渡 legacy 与全量 + +```bash +python -m pytest tests/legacy_1x -q +python -m pytest tests -q +pre-commit run --all-files +git diff --check +``` + +并验证: + +- CI 当前 job 确实执行根测试,legacy job 确实执行 `tests/legacy_1x`; +- 默认 pytest 的包含 / 排除语义与文档一致; +- 构建 sdist / wheel,检查 `legacy_2x` 过渡包、tokenizer 数据和 entry point; +- 在干净环境安装 wheel 后执行当前 API 与保留兼容路径的导入冒烟; +- 搜索无残留的 `dashscope/v2`、`tests/v2` 有效路径引用; +- `examples/` 不再产生迁移 diff,既有示例冒烟保持通过。 + +## 10. 非目标 + +- 不修改 error registry 或其 vendored 错误内容。 +- 不重新设计 AgentStudio / AgenticRL 错误体系。 +- 不在本次删除 `legacy_1x` 或 `legacy_2x`。 +- 不把 acli 移入 `legacy_2x`。 +- 不移动 `tests/data`。 +- 不重复修改已经完成的 `examples/`。 +- 不在目录迁移中增加无关功能、重构或清理生成物。 diff --git a/md/dashscope-2.0-naming-review.md b/md/dashscope-2.0-naming-review.md new file mode 100644 index 00000000..0d74e39f --- /dev/null +++ b/md/dashscope-2.0-naming-review.md @@ -0,0 +1,661 @@ +# DashScope 2.0 SDK 命名评审 + +对每个接口的**调用语句**与**返回结构体类**做全量抽取, +并对照 `dashscope/v2/` 当前实现逐一核对(方法名、返回类型注解、参数名),评估命名的 +一致性与客户友好度。抽取以代码为准(原始依据 `md/dashscope-2.0-sdk-interface.md` 已不在 +仓库中);接口清单见 [`dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md), +调用示例见 [`dashscope-2.0-interfaces-examples.md`](dashscope-2.0-interfaces-examples.md)。 + +核心结论:**`create` 的双重语义(同步推理 / 创建持久资源)不是问题**——OpenAI SDK 同样如此 +(`chat.completions.create` 阻塞,`fine_tuning.jobs.create` 不阻塞),资源语义本身已消歧。 +真正的问题是: + +1. 除 `create` 之外还有 **11 个**表达"发起一次调用"的动词; +2. 返回类型存在 **4 套**互斥的后缀规范,且有 5 处裸 `dict` / `Any` 逃逸; +3. 同样是"跑一个长任务",**阻塞语义各不相同**。 + +--- + +## 一、全量抽取表 + +| 章节 | 调用语句 | 返回类型 | 语义 | 动词 | +|---|---|---|---|---| +| 0 | `dashscope.chat(model, prompt)` | `ChatCompletion` | 同步推理 | chat | +| 1.1 | `client.chat.completions.create(...)` | `ChatCompletion` / `ChatCompletionChunk` | 同步推理 | **create** | +| 1.4 | `client.apps.call(...)` | `AppCompletion` | 同步推理 | **call** ⚠️ | +| 2.1 | `client.multimodal.chat.create(...)` | `ChatCompletion` | 同步推理 | **create** | +| 2.2 | `client.images.generate(...)` | `Job` | 一步式(阻塞) | **generate** ⚠️ | +| 2.2 | `client.images.jobs.create/fetch/wait` | `Job` | 任务生命周期 | create/fetch/wait | +| 2.3 | `client.videos.generate(...)` | `Job` | 一步式(阻塞) | **generate** ⚠️ | +| 2.3 | `client.videos.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | +| 2.4 | `client.embeddings.create_multimodal(...)` | `MultiModalEmbeddingResponse` | 同步推理 | **create_*** ⚠️ | +| 3.1 | `client.embeddings.create(...)` | `EmbeddingResponse` | 同步推理 | **create** | +| 3.2 | `client.embeddings.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | +| 3.3 | `client.rerank.create(...)` | `RerankResponse` | 同步推理 | **create** | +| 3.4 | `client.tokenization.count(...)` | `TokenCount` | 计算 | **count** | +| 4.1 | `client.audio.asr.recognition(...)` | `Recognition`(ctx manager) | 双工会话 | **名词** ⚠️ | +| 4.1 | `rec.send_audio_frame/stop/events` | `None` / `Iterator[dict]` | 会话内 | — | +| 4.2 | `client.audio.asr.transcription.transcribe(urls)` | `Job` | 一步式(阻塞) | **transcribe** ⚠️ | +| 4.2 | `client.audio.asr.transcription.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | +| 4.3 | `client.audio.tts.synthesize(text)` | `bytes` | 一步式 | **synthesize** ⚠️ | +| 4.3 | `client.audio.tts.stream(text)` | `Iterator[bytes]` | 流式 | **stream** ⚠️ | +| 4.4 | `client.audio.realtime.session(model=)` | `RealtimeSession`(ctx manager) | 双工会话 | **名词** ⚠️ | +| 4.4 | `rt.send_event/recv_event/events/close` | — | 会话内 | — | +| 5.1 | `client.fine_tunes.create(...)` | `FineTune` | 资源创建(不阻塞) | **create** | +| 5.1 | `client.fine_tunes.get/list/wait` | `FineTune` / `FineTuneList` | CRUD | — | +| 5.1 | `client.fine_tunes.cancel/delete` | `FineTuneCancel` / `FineTuneDelete` | CRUD | — | +| 5.1 | `client.fine_tunes.stream_events(job_id)` | `Iterator[FineTuneEvent]` | 流式 | **stream_*** ⚠️ | +| 5.1 | `client.rl.logs(ft.job_id)` | `RlLogs` | 跨资源 ⚠️ | **logs** | +| 5.2 | `client.deployments.create/get/list/delete` | `Deployment` / `DeploymentList` / `DeploymentDelete` | CRUD | — | +| 5.2 | `client.deployments.scale(model, 4)` | `Deployment` | CRUD | **scale** | +| 5.3 | `client.models.list/get` | `ModelList` / `ModelObject` | CRUD | — | +| 5.4 | `client.files.upload(file_path=)` | `FileUploadResponse` | CRUD | **upload** ⚠️ | +| 5.4 | `client.files.list/get/delete` | `FileList` / `FileObject` / `DeletionStatus` | CRUD | — | +| 6 | `client.rl.register_functions(...)` | `Dict[str, List[str]]` | 裸 dict ⚠️ | 动词_名词 | +| 6 | `client.rl.test_functions(...)` | `Any`(1.x 对象) | 类型逃逸 ⚠️ | 动词_名词 | +| 6 | `client.rl.upload_data(...)` | `Dict[str, List[str]]` | 裸 dict ⚠️ | 动词_名词 | +| 6 | `client.rl.run(...)` | `Any`(1.x 对象),**不阻塞** | 语义撞车 ⚠️ | **run** | +| 6 | `client.rl.get/list/cancel/logs` | `FineTune` / `FineTuneList` / `FineTuneCancel` / `RlLogs` | 复用微调 | — | +| 7 | `client.agentstudio.agents/sessions.create` 等 | 1.x 类型 | 懒挂载子 SDK | create | + +--- + +## 二、诊断:方法名与返回类型(按严重度) + +### P0-1 `Job.output` 是全 SDK 唯一的裸 dict 出口 + +`images` / `videos` / `transcription` / `embeddings.jobs` 四类任务共用 `Job`,但字段是 +`output: Optional[Dict[str, Any]]`、`usage: Optional[Dict[str, Any]]`。客户必须背下每个服务的 +字段名: + +- `job.output["results"][0]["url"]`(images) +- `job.output["video_url"]`(videos) +- `job.output["results"][*]["transcription_url"]`(transcription) +- `done.output["url"]`(batch embeddings) + +接口文档 §3.2 甚至只能写"字段名以服务返回为准,如 `url` 或 `results.url`"——这就是没有类型化 +的直接后果。 + +同类问题: + +| 位置 | 现状 | 对照 | +|---|---|---| +| `AppCompletion.usage` | `Optional[Dict[str, Any]]` | chat / embeddings / rerank / tokenization 的 usage 都是 typed model | +| `FileUploadResponse.data` | `Dict[str, Any]`,取 `up.data["file_id"]` | 同一资源的 `files.list` 返回 typed `FileObject` | +| `rl.register_functions` / `rl.upload_data` | `Dict[str, List[str]]` | — | +| `rl.run` / `rl.test_functions` | `Any`(1.x 原对象) | — | + +**建议**:`Job` 保留 `output` 原始字典(服务端字段各异,不宜强类型化整体),但补 typed 视图与 +便捷属性:`JobResult`(`url` / `transcription_url` / 其余透传)、`job.results`、`job.image_urls`、 +`job.video_url`、`job.result_url`。`FileUploadResponse` 改为直接返回 `FileObject`(`up.file_id`)。 + +### P0-2 `generate` / `transcribe` 阻塞,`run` / `create` 不阻塞 + +`images.generate` = `jobs.create` + `jobs.wait`(阻塞到终态),`audio.asr.transcribe` 同理。 +但 `rl.run` 文档写"启动完整训练流程"却**立即返回**,随后要 `rl.get(job.job_id).output.status` +自行轮询;`fine_tunes.create` 同样不阻塞。三个"跑长任务"的入口阻塞语义各不相同,是最容易踩的坑。 + +**建议**确立一条明规则并写进文档首页与每个方法的 docstring: + +> `.create()` 永不阻塞;`.wait()` 才阻塞;裸领域动词(`generate` / `transcribe`) +> 是 `create + wait` 的语法糖,只在任务必然终结时提供。 + +按此规则:`rl.run` → **`rl.jobs.create`**,并补 `rl.jobs.wait(job)`。落地时把 `rl.get/list/cancel/logs` +一并收进 `rl.jobs`(`get` 随之统一为 `fetch`),`rl` 顶层只留 RL 独有的工作流步骤——否则每个任务 +动词都有 `rl.x` 与 `rl.jobs.x` 两种写法,正是本章要消灭的歧义。 + +### P1-1 `apps.call` 是唯一残留的 1.x 动词 + +`AppCompletion` 这个类名已经与 `ChatCompletion` 平行,方法却叫 `call`。改成 +**`client.apps.completions.create(app_id=..., prompt=...)`** 之后,"同步推理一律 `.create()`" +零例外,且类型名与方法名自洽。这是收益最高的一处改名。 + +### P1-2 `embeddings.create_multimodal` 应挂到 multimodal 命名空间 + +`_multimodal` 方法后缀不可扩展(下次加 `create_batch`?)。`client.multimodal` 命名空间已存在 +(`multimodal.chat`),补 `multimodal.embeddings.create(...)` 即可与之完全对称。 + +另外 `MultiModalEmbedding`(输出元素)与 `MultiModalEmbeddingItem`(输入元素)名字过近;且输出 +元素 `MultiModalEmbedding{index, type, embedding}` 与文本侧 `Embedding{text_index, type, embedding}` +字段几乎相同,**建议合并为一个 `Embedding` 类**、响应合并为 `EmbeddingResponse`。 + +### P1-3 `chat.completions.create` 与 `multimodal.chat.create` 是同一件事的两个入口 + +两者返回同一个 `ChatCompletion`,只是 native 端点不同。客户被迫先判断"我这次算不算多模态"。 +建议 `chat.completions.create` 在检测到 `content` 为 parts 列表时自动路由到多模态端点, +`multimodal.chat.create` 降级为显式逃生口。 + +**落地**:端点常量从资源层移到协议层(`ChatProtocol.MULTIMODAL_PATH`,仅 native 适配器有值, +兼容协议在自己的 chat 路径上翻译 parts),`chat.completions.create` 依据 +`types.messages.has_media_parts()` 自动路由;纯文本 parts 列表仍走文本端点。 +`multimodal.chat.create` 保留为逃生口(强制走多模态端点,覆盖 qwen-vl 纯文本提问这类场景), +两者共用 `chat/completions.py` 里的 `_build_request` / `_send` / `_asend`,签名完全一致。 + +### P1-4 delete / cancel 结果有 4 个类名,实际只有 2 种形状 + +| 类 | 形状 | 用于 | +|---|---|---| +| `DeletionStatus` | `{request_id, deleted: bool}` | `files.delete` | +| `FineTuneDelete` | `{request_id, output: StatusResult}` | `fine_tunes.delete` | +| `FineTuneCancel` | `{request_id, output: StatusResult}` | `fine_tunes.cancel`、`rl.cancel` | +| `DeploymentDelete` | `{request_id, output: StatusResult}` | `deployments.delete` | + +后三个结构完全一样。**建议**删掉 `FineTuneDelete` / `FineTuneCancel` / `DeploymentDelete`, +统一复用 `StatusResult`(改名 `JobStatus` 更达意)。 + +### P1-5 同一个"透传字典"有 6 个名字 + +| 参数名 | 出现位置 | 含义 | +|---|---|---| +| `parameters` | images / videos / `jobs.create` / asr | 服务端 `parameters` 段透传 | +| `extra_parameters` | apps / embeddings / rerank / tokenization | **同上** | +| `extra_input` | images / videos | 服务端 `input` 段透传 | +| `hyper_parameters` | fine_tunes | 服务端字段名 | +| `rag_options` | apps | 服务端字段名 | +| `protocol_options` | `Client` | SDK 自有,协议适配开关 | + +前两个是同一个东西,**必须统一为 `parameters`**。其余四个语义各异,可保留。 + +### P1-6 `file_urls` vs `url` + +`audio.asr.*.create(file_urls=[...])` 与 `embeddings.jobs.create(url=...)` 都是"待处理文件 URL", +一复数一单数。 + +**修正**:核对 1.x 实现(`dashscope/embeddings/batch_text_embedding.py`,请求体为 +`input = {"url": url}`)后确认,批量向量化端点**只接受一个文件**,原建议的 `file_urls: List[str]` +会造出一个服务端无法兑现的复数签名。落地为 **`file_url: str`**,并在 docstring 里点明与 +`asr.transcribe(file_urls=)` 的差异。规则因此改为:单复数必须跟随端点的真实入参个数, +不做表面统一。 + +### P1-7 微调训练日志挂在 `rl` 资源面上 + +接口文档 §5.1 明写"训练日志(对应 1.x `FineTunes.logs`,2.0 移到 rl 资源面)",于是纯微调用户 +要写 `client.rl.logs(ft.job_id)`——完全无法发现。既然 `rl.get/list/cancel` 已复用 `FineTune*` +模型说明后端同源,应改为 `client.fine_tunes.logs(job_id)`;`RlLogs` 随之改名 `JobLogs`。 + +**落地**:`fine_tunes.logs(job_id)` 为唯一实现,`RlLogs` → `JobLogs`(字段 `logs` → `content`)。 +不留 `rl.logs` alias——RL 用户经 P0-2 引入的 `rl.jobs.logs` 转发到同一实现,两条路径都是 +资源树的正常形态,而非新旧并存的过渡写法。 + +### P2-1 会话方法用名词 + +`audio.asr.recognition(...)`、`audio.realtime.session(...)` 都是名词当方法名。 +建议 `audio.asr.recognize(...)`、`audio.realtime.connect(...)`(后者与 OpenAI Realtime +`client.realtime.connect` 一致)。 + +### P2-2 层级深度 3~6 层不齐 + +| 深度 | 例 | +|---|---| +| 3 | `embeddings.create` | +| 4 | `multimodal.chat.create`、`audio.tts.synthesize`、`images.jobs.create` | +| 6 | `audio.asr.transcription.jobs.create` | + +最深那条里 `transcription` 是冗余层(`asr` 已含转写义)。建议拍平为 `audio.asr.transcribe(...)` ++ `audio.asr.jobs.create/fetch/wait` + `audio.asr.recognize(...)`,全部收敛到 4~5 层。 + +### P2-3 `*Object` / `*Output` 两套 payload 后缀,且扁平化程度不一 + +- `ModelObject`、`FileObject`(models / files) +- `AppOutput`、`FineTuneOutput`、`DeploymentOutput`(apps / fine_tunes / deployments)——1.x + `response.output` 信封遗留 + +导致取值深度不一:chat / embeddings / rerank / models 扁平(`resp.choices[0]`、`m.model`), +apps / fine_tunes / deployments 要穿 `output`(`dep.output.deployed_model`)。且便捷属性覆盖不全: +`FineTune.job_id` ✓、`AppCompletion.text` ✓、`Deployment` ✗。 + +**建议**:要么全部扁平化,要么给 `Deployment` 补 `dep.deployed_model` / `dep.status`,与另两个 +对齐。`ModelObject` → `Model`。 + +### P2-4 必填主输入的位置 / 关键字风格不统一 + +允许位置传参:`tts.synthesize("你好", ...)`、`transcribe(urls, ...)`、`dashscope.chat(model, prompt)`、 +`deployments.scale(model, 4)`。 +强制 keyword-only:`images.generate(*, model, prompt)`、`videos.generate(*, ...)` 等。 + +建议除顶层快捷入口 `dashscope.chat()` 外统一 keyword-only(IDE 补全与重构更安全), +`scale(model, capacity=4)`。 + +### P2-5 流式有 4 种表达 + +| 形态 | 位置 | 评价 | +|---|---|---| +| `create(stream=True)` → `Iterator[Chunk]` | chat / multimodal | OpenAI 惯例,保留 | +| `synthesize()` → `bytes` vs `stream()` → `Iterator[bytes]` | audio.tts | 返回类型不同,不宜用 kwarg 合并,保留 | +| `stream_events(job_id)` → `Iterator[FineTuneEvent]` | fine_tunes | 唯一的 `stream_*` 前缀方法 | +| `rec.events()` / `rt.events()` → `Iterator[dict]` | websocket 会话 | 服务端事件 schema,dict 可接受 | + +各有理由,但需在文档同一处集中说明选用规则,否则客户会以为漏了什么。 + +--- + +## 三、诊断:入参(按严重度) + +以下基于对 `dashscope/v2/resources/**` 全部公开方法签名的逐条抽取(共 44 个方法)。 + +### P0-A `timeout` 一词三义,且在相邻两层含义相反 + +| 签名 | `timeout` 的实际含义 | +|---|---| +| `Jobs.fetch(task_id, *, timeout=None)` | 单次 HTTP 请求超时 | +| `Jobs.create(*, ..., timeout=None)` | 单次 HTTP 请求超时 | +| `Jobs.wait(job, *, timeout=86400)` | **等待终态的总预算**(默认 24h) | +| `Images.generate(*, timeout=None, wait_timeout=86400)` | `timeout` = 提交那次 HTTP;`wait_timeout` = 轮询 | + +于是 `client.images.generate(..., timeout=600)`——客户的直觉是"最多等 10 分钟",实际语义是 +"提交请求的 HTTP 超时 10 分钟",而等待仍按 24h 走。同一个方法里 `timeout` 与 `wait_timeout` +并存,且 `wait(timeout=600)` 在下一层又变成了等待预算。**这是会直接导致误用的命名。** + +建议:阻塞式一步法(`generate` / `transcribe`)中 `timeout` 表示整体等待预算,提交超时改名 +`submit_timeout`(或走 `client.with_options(timeout=)`);`Jobs.wait` 保持 `timeout`(上下文 +已明确是等待)。 + +### P0-B `task_id` vs `job_id`:两套任务主键 + +| 资源 | 主键属性 | 传参名 | +|---|---|---| +| images / videos / transcription / batch-embedding | `Job.task_id` | `jobs.fetch(task_id)` | +| fine_tunes / rl | `FineTune.job_id`(→ `output.job_id`) | `get(job_id)` / `cancel(job_id)` / `logs(job_id)` | + +同一个 SDK 里"任务标识"有两个名字,客户从 images 走到 fine_tunes 必须重学一遍。 +建议:`Job` 增 `job_id` 别名属性、`FineTune` 增 `task_id` 别名属性,文档统一推荐 `job_id` +(`task_id` 是服务端字段名,不宜直接改)。 + +### P1-C 分页参数 `page` vs `page_no`,默认值也不齐 + +| 方法 | 页码参数 | `page_size` 默认 | +|---|---|---| +| `files.list` | **`page`** ⚠️ | 10 | +| `deployments.list` | `page_no` | 10 | +| `fine_tunes.list` | `page_no` | 10 | +| `models.list` | `page_no` | **20** ⚠️ | +| `rl.jobs.list` | `page_no` | 10 | + +`files.list(page=)` 是孤例,应统一为 `page_no`。`models.list` 的默认 20 若确为服务端默认, +需在 docstring 标注原因,否则对齐为 10。 + +### P1-D 资源标识符参数名有 7 种 + +`task_id` / `job_id` / `file_id` / `deployed_model` / `model_name` / `app_id` / `instance_id`。 +其中两处需修: + +- `models.get(model_name)` → **`model=`**:与 `ModelObject.model` 字段名、以及全 SDK 其余 40 处 + `model=` 都不一致。 +- `rl.test_functions(instance_id, functype, input_data)`:全 SDK 唯一"三个连续必填位置参数"的 + 方法(其余都是 keyword-only),且 `functype` 拼写不规范 → `func_type`,并改 keyword-only。 + +`file_id` / `app_id` / `deployed_model` 与服务端字段一致,保留。 + +### P1-E `**kwargs` 转发吞掉了下一层的完整签名 + +| 位置 | 现状 | 下一层实际签名 | +|---|---|---| +| `Asr.recognition(**kwargs)` | 无任何可见参数 | `Recognition.__init__(*, model, format, sample_rate, parameters, ws_url, timeout)` | +| `Realtime.session(**kwargs)` | 同上 | `RealtimeSession.__init__(...)` | +| `ImageJobs.fetch(task_id, **kwargs)` | 只可见 `task_id` | `Jobs.fetch(task_id, *, timeout)` | +| `ImageJobs.wait(job, **kwargs)` | 只可见 `job` | `Jobs.wait(job, *, poll_interval, max_interval, timeout)` | +| `VideoJobs` / `EmbeddingJobs` / `TranscriptionJobs` 的 `fetch` / `wait` | 同上 | 同上 | + +功能上是通的(`**kwargs` 会转发),但 IDE 补全、类型检查、`help()`、API 文档自动生成全部失效。 +文档开头承诺"所有资源的每次调用都支持 `timeout=` 关键字参数",而客户在工具层面**看不到**这个 +参数——这正是 `**kwargs` 的代价。建议显式写出签名再转发。 + +### P1-F `rl.*` 4 个方法确实没有 `timeout=` + +`register_functions` / `test_functions` / `upload_data` / `run` 既无 `timeout` 也无 `**kwargs`, +与文档开头的全局承诺直接矛盾(这几个走 1.x 实现)。→ 补上,或修正文档表述为"除 `rl` 的 +1.x 透传方法外"。 + +### P1-G `apps.call` 22 个参数:3 个互斥输入 + 3 个透传字典 + +- 输入入口:`prompt` / `messages` / `history` 三者并存且**无互斥校验**(文档只说"prompt 与 + messages 二选一")。`history` 是 1.x 遗留,与 §1.2"2.0 无状态、自行维护 messages"的设计 + 原则冲突。 +- 透传字典:`biz_params` / `rag_options` / `extra_parameters` 三个(其中 `extra_parameters` + 应并入 `parameters`,见 P1-5)。 + +→ 补互斥校验与清晰报错;`history` 标 deprecated 或移除;参数按"高频在前、低频收进 +`parameters`"重排。 + +**落地**:`history` 直接移除——2.0 接口文档里从未出现它,1.x 的形状是 `{user, bot}` 而非 +`role/content`,且与 §1.2"2.0 无状态、自行维护 messages"冲突。移除后只剩 `prompt` / +`messages`,两者组合语义明确(同时给出时 `prompt` 追加为最新一条 user 消息), +`_validate` 继续保证"二者至少给一个"。`extra_parameters` 已并入 `parameters`(见 P1-5)。 + +`biz_params` **不改名**:它是百炼应用侧 flow/plugin 入参的服务端公开字段,且落在 `input` +而非 `parameters` 段,改叫 `parameters` 会指向另一个位置。改为在 `create` docstring 中 +写明它与 `parameters` 的分工。 + +### P1-H 流式返回类型不对称 + +| 入口 | `stream=True` 返回 | 有无便捷能力 | +|---|---|---| +| `chat.completions.create` / `multimodal.chat.create` | `ChatStream` | `text_stream` / `get_final_message()` / `close()` | +| `apps.call` | 裸 `Iterator[AppCompletion]` | 无 | + +同样是流式,chat 给了包装类,apps 给裸迭代器。建议 apps 返回 `AppStream`(至少带 +`text_stream`)。 + +### P2-I `size="1024*1024"` 不接受 `1024x1024` + +服务端用 `*` 分隔,OpenAI 用 `x`。从 OpenAI 迁移的客户会写 `size="1024x1024"`,且 SDK 不校验, +直接吃一个 400。→ 两种写法都接受并归一化。 + +### P2-J 孤例参数 + +| 参数 | 出现位置 | 问题 | +|---|---|---| +| `extra_headers` | 仅 `embeddings.create_multimodal` | 其余 40+ 方法都没有;要么全线补,要么删掉走 `with_options`。**已移除**:它唯一的真实用途是让服务端解析 `oss://` 私桶地址,现在 SDK 检测到 `oss://` 入参就自动带上 `X-DashScope-OssResourceResolve: enable`(与 1.x 行为一致),客户不必再知道这个头 | +| `connector` | `tts.synthesize/stream`、`Recognition` | 无类型注解,文档未提及。**已补**:`transport.ws.Connector = Callable[[str, Dict[str, str]], Any]` 公开导出,三处签名统一注解 | +| `sleep: Callable[[float], Any]` | `Jobs.wait` / `FineTunes.wait` | 测试注入钩子暴露在公开签名 → 改 `_sleep` 或经 `with_options`。**已改** `_sleep`(全线 7 个资源 + 6 个测试文件),docstring 点明它是替换轮询间隔的测试缝 | + +### P2-K `input` 的接受度不一致 + +- `embeddings.create(input: Union[str, List[str]])` ✓ 接受单个 +- `chat.completions.create(messages: Union[str, List[MessageInput]])` ✓ 接受单个 +- `embeddings.create_multimodal(input: List[...])` ✗ 必须列表 + +→ `create_multimodal` 也应接受单个 item / dict。 + +### P2-L 低频、不透明的服务端参数散在签名上 + +`res_level` / `enable_fusion` / `max_video_frames` / `fps` / `output_type` / `instruct` / +`doc_tag_codes` / `doc_reference_type` / `has_thoughts` / `memory_id` / `image_list` / +`file_list` / `biz_params`。 + +`res_level` 这类缩写没有任何自解释性。签名越长,`model=` / `prompt=` 这些高频参数在 IDE 补全 +列表里越靠后、越难发现。建议:高频参数留在签名上,低频的收进 `parameters=`,并在 docstring +给出常用键清单。 + +--- + +## 四、诊断:输出字段(按严重度) + +以下基于对 `dashscope/v2/**` 全部 pydantic 模型(38 个)字段的逐条抽取。 +**字段名比类名更容易产生歧义**——类名客户只写一次,字段名要写在每一处取值代码里。 + +### P0-C `gmt_create` / `gmt_modified`:阿里内部 DB 命名泄漏到公开 SDK + +`DeploymentOutput` 的时间字段叫 `gmt_create` / `gmt_modified`。这是阿里内部数据库建表规范, +对外部客户完全不可理解(`gmt` 会被读成时区),且与同一 SDK 的其他时间字段全不一致: + +| 模型 | 时间字段 | +|---|---| +| `DeploymentOutput` | **`gmt_create` / `gmt_modified`** ⚠️ | +| `FineTuneOutput` | `create_time` / `end_time` | +| `Job` | `submitted_time` / `scheduled_time` / `end_time` | +| `FileObject` | `upload_time` | + +附带问题:`Job.submitted_time` 是从服务端 `submit_time` **主动改名**而来 +(`from_payload` 里 `submitted_time=output.get("submit_time")`)。客户对着服务端文档 / 抓包 +调试时会发现字段名对不上,这类"SDK 自作主张的改名"要么别做,要么在 docstring 标注。 + +建议全线统一为 `created_at` / `updated_at` / `submitted_at` / `finished_at`(ISO 8601 生态惯例, +OpenAI / Anthropic / Stripe 均如此),或退一步统一为 `create_time` / `modify_time` / `end_time`。 + +### P0-D `finetuned_output`:产出与消费两端叫不同名字 + +微调产出的模型版本 ID,在产出侧叫 `FineTuneOutput.finetuned_output`,在消费侧叫 +`deployments.create(version=...)`: + +```python +done = client.fine_tunes.wait(ft) +version = done.output.finetuned_output # "ft-20260827-xxxx" +dep = client.deployments.create(model="qwen-turbo", version=version) +``` + +`finetuned_output` 这个名字无法让人知道它是一个**模型版本标识**("output" 在本 SDK 里到处是 +响应信封的意思,见 P2-3)。同一个值两个名字,是客户必须靠读文档才能跨过的坎。 + +建议:`finetuned_output` → **`finetuned_model`**,`deployments.create(version=)` → +**`model_version=`**,两端对齐。同时 `DeploymentOutput.model_name` → **`model`**(与全 SDK +其余 40 处 `model` 一致;该模型里已有 `base_model`,`model_name` 反而更像"基座名",歧义大)。 + +### P1-I 同一个 `usage` 字段有 4 个类 + 4 处退化成裸 dict / Any + +| 模型 | `usage` 类型 | 字段 | +|---|---|---| +| `ChatCompletion` / `ChatCompletionChunk` | `Usage` | input_tokens / output_tokens / total_tokens | +| `EmbeddingResponse` | `EmbeddingUsage` | total_tokens | +| `RerankResponse` | `RerankUsage` | total_tokens | +| `TokenCount` | `TokenCountUsage` | input_tokens | +| `AppCompletion` | **`Dict[str, Any]`** ⚠️ | — | +| `MultiModalEmbeddingResponse` | **`Dict[str, Any]`** ⚠️ | — | +| `Job` | **`Dict[str, Any]`** ⚠️ | — | +| `FineTuneOutput` | **`Any`** ⚠️ | — | + +4 个结构几乎相同的 Usage 类,4 处完全无类型。`EmbeddingUsage{total_tokens}` 与 +`RerankUsage{total_tokens}` 是**逐字节相同的两个类**。 + +建议合并为单一 `Usage`(三个 token 字段都给默认值),全 SDK 统一使用;`AppCompletion.usage` +文档里明确是 `{"models": [...]}` 结构,应有独立的 `AppUsage{models: List[ModelUsage]}`。 + +### P1-J "总条数"两个名字:`total` vs `total_count` + +| 模型 | 字段 | +|---|---| +| `FileList` | `total_count` | +| `ModelList` | `total_count` | +| `DeploymentListOutput` | **`total`** ⚠️ | +| `FineTuneListOutput` | **`total`** ⚠️ | + +同为分页列表的总条数,两个名字。统一为 `total_count`。 + +### P1-K "序号"两个名字:`text_index` vs `index` + +`Embedding.text_index`(文本向量)vs `MultiModalEmbedding.index`(多模态向量)——**同一个 +`embeddings` 资源面下**,客户写 `emb.text_index` 和 `item.index`。文档 §3.1 与 §2.4 就分别 +示范了这两种取法。其余 `Choice.index` / `StreamChoice.index` / `RerankResult.index` 都叫 `index`。 + +统一为 `index`(`text_index` 是服务端字段名,用 alias 承接即可)。这也印证 P1-2 的结论: +`Embedding` 与 `MultiModalEmbedding` 本就该是同一个类。 + +### P1-L list 响应两种形状:扁平 vs 套 `output` + +| 扁平(字段在顶层) | 套 `output` 信封 | +|---|---| +| `ChatCompletion.choices` | `AppCompletion.output.text` | +| `EmbeddingResponse.embeddings` | `FineTune.output.job_id` | +| `RerankResponse.results` | `Deployment.output.deployed_model` | +| `ModelList.models` / `.page_no` | `FineTuneList.output.jobs` / `.output.page_no` | +| `FileList.files` / `.page_no` | `DeploymentList.output.deployments` / `.output.page_no` | +| `TokenCount.usage` | `FileUploadResponse.data["file_id"]` | +| `RlLogs.logs` | `Job.output["results"]` | + +**`FileList` 扁平而 `FineTuneList` / `DeploymentList` 套信封**——同样是 `*.list()` 的返回值, +取值深度差一层,这是最刺眼的对比。便捷属性也只补了一半:`FineTune.job_id` ✓、 +`AppCompletion.text` ✓、`TokenCount.input_tokens` ✓、`ChatCompletion.output_text` ✓, +而 `Deployment` ✗、`FineTuneList` ✗、`DeploymentList` ✗、`FileUploadResponse` ✗。 + +建议二选一并全线执行: +- **方案 A(推荐)**:全部扁平化,`Deployment.deployed_model`、`FineTuneList.jobs`、 + `FileUploadResponse` 直接返回 `FileObject`。与服务端信封的映射放在 `_parse` 里。 +- **方案 B**:保留 `output` 但给每个模型补齐便捷属性,做到"穿不穿信封都能取到"。 + +### P1-M 业务错误字段 `code` / `message` / `is_error` 只覆盖 3 / 38 个模型 + +| 有 | 无 | +|---|---| +| `ChatCompletion`(code / message / is_error) | `EmbeddingResponse`、`RerankResponse`、`TokenCount` | +| `AppCompletion`(code / message / is_error) | `ModelList`、`FileList`、`FileObject`、`RlLogs` | +| `Job`(code / message,**缺 is_error**) | `FineTune`、`Deployment`、`FineTuneList`、`DeploymentList` | + +DashScope 的特点是 HTTP 200 也可能带业务错误码。现在客户在 chat / apps 上能看 `is_error`, +在 fine_tunes / deployments 上只能靠 `output.status == "FAILED"` 自己判断,在 embeddings / +rerank 上**完全无从判断**。`Job` 有 `code`/`message` 却没有 `is_error`,与另两个模型不齐。 + +建议抽一个 `ResponseBase`(`request_id` + `code` + `message` + `is_error`)让所有顶层响应模型 +继承,`raise_on_error=False` 时行为全线一致。 + +### P2-M 其余字段级问题 + +| 字段 | 问题 | 建议 | +|---|---|---| +| `ChatCompletion.status_code` | 全 SDK 孤例,其他模型都没有 HTTP 状态码 | 移入 `ResponseBase` 或删除 | +| `ModelObject.opensource` | 非标准拼写,语义模糊(开源模型?开源许可?) | `open_source` | +| `RlLogs.logs: str` | 类名与字段名重复(`rl.logs(id).logs`),且看不出是 str 还是 list | `JobLogs.content` | +| `FineTuneEvent.output: Any` | 完全无类型 | 至少 `Dict[str, Any]`,理想是 typed event | +| `AppOutput.doc_references: List[Dict]` | RAG 引用来源是高频消费字段,却是裸 dict 列表 | typed `DocReference` | +| `AppOutput.thoughts: List[Dict]` | 同上 | typed `Thought` | +| `Job.task_id: str`(REQUIRED) | 类型上必填,但 `from_payload` 用 `or ""` 兜底,语义可空 | 改 `Optional[str]` 或真必填 | +| `Job.output` / `FileUploadResponse.data` | 裸 dict(见 P0-1) | typed | +| `MultiModalEmbeddingItem.factor` | 修饰项排在 4 个模态字段之前,读起来像第 5 个模态 | 移到最后 | +| `ImagePart.url` vs 文档示范的 `{"image": url}` | typed part 与 1.x dict part 两套表示法,文档只示范 dict 形式(而 embeddings 侧反而示范 typed) | 文档统一示范 typed,dict 作为兼容写法 | + +--- + +## 五、建议规范 + +### 5.1 动词规范(收敛到 8 个) + +| 语义 | 动词 | 阻塞 | 适用 | +|---|---|---|---| +| 同步推理 | `create` | 是 | chat / multimodal.chat / multimodal.embeddings / embeddings / rerank / **apps.completions** | +| 长任务提交 | `jobs.create` | 否 | images / videos / audio.asr / embeddings / fine_tunes / rl | +| 长任务查询·等待 | `jobs.fetch` / `jobs.wait` | 否 / 是 | 同上(现状已完全一致 ✓) | +| 一步式语法糖 | `generate` / `transcribe` | 是 | images / videos / audio.asr(= create + wait,需明写) | +| 持久资源 CRUD | `get` / `list` / `delete` / `cancel` / `scale` | 否 | fine_tunes / deployments / models / files(现状已一致 ✓) | +| 双工会话 | `recognize` / `connect` | ctx manager | audio.asr / audio.realtime | +| 纯计算·文件 | `count` / `upload` | 是 | tokenization / files | +| 流式 | `stream=True` 或 `.stream()` / `.events()` | — | 见 P2-5 | + +### 5.2 入参规范 + +| 类别 | 规范 | 说明 | +|---|---|---| +| 模型 | 一律 `model: str` | 不允许 `model_name` 之类变体 | +| 任务主键 | 对外统一 `job_id`,服务端字段 `task_id` 保留为别名 | 见 P0-B | +| HTTP 超时 | `timeout: Optional[float]` | 单次请求;所有公开方法都必须有,且**显式写出**不用 `**kwargs`。唯一例外:`rl` 的 4 个 1.x 透传方法(见 P1-F) | +| 轮询预算 | `wait` 上下文里叫 `timeout`;阻塞式一步法里 `timeout` = 总预算,提交超时叫 `submit_timeout` | 见 P0-A | +| 服务端透传 | `parameters=`(parameters 段)/ `extra_input=`(input 段) | 消灭 `extra_parameters` | +| 分页 | `page_no` / `page_size`,默认值全线一致 | 消灭 `page=` | +| 文件 URL | `file_urls: List[str]`(多文件端点)/ `file_url: str`(单文件端点) | 消灭裸 `url=`;单复数跟随端点真实入参个数,见 P1-6 | +| 必填主输入 | keyword-only(顶层 `dashscope.chat()` 例外) | 见 P2-4 | +| 低频服务端参数 | 收进 `parameters=`,docstring 列常用键 | 见 P2-L | +| 测试注入钩子 | 不进公开签名(`_` 前缀或 `with_options`) | 见 P2-J 的 `sleep` | + +### 5.3 输出字段规范 + +| 类别 | 规范 | 说明 | +|---|---|---| +| 时间戳 | `created_at` / `updated_at` / `submitted_at` / `finished_at` | 消灭 `gmt_*`、`*_time` 混用,见 P0-C | +| 用量 | 单一 `Usage` 类,全 SDK 复用 | 消灭 4 个同构 Usage 类与 4 处裸 dict,见 P1-I | +| 分页 | `page_no` / `page_size` / `total_count` | 消灭 `total`,见 P1-J | +| 序号 | 一律 `index` | 消灭 `text_index`,见 P1-K | +| 模型标识 | 一律 `model`;微调产物 `finetuned_model`;部署产物 `deployed_model` | 消灭 `model_name` / `finetuned_output` / `version`,见 P0-D | +| 任务标识 | 对外 `job_id`,`task_id` 为服务端别名 | 见 P0-B | +| 错误字段 | 所有顶层响应继承 `ResponseBase`(`request_id` / `code` / `message` / `is_error`) | 见 P1-M | +| 响应形状 | 一律扁平,服务端 `output` / `data` 信封在 `_parse` 里拆掉 | 见 P1-L 方案 A | +| 禁止 | 公开模型上不得出现裸 `Dict[str, Any]` / `Any` 字段(服务端原始 `Job.output` 除外,且必须另给 typed 视图) | 见 P0-1 | +| 禁止 | 不得出现阿里内部命名习惯(`gmt_*`、`is_deleted`、`biz_*`) | `biz_params` 见 P1-G | + +--- + +## 六、改名清单 + +**加粗为破坏性变更**(需同步改接口文档与测试)。 + +> 前提:`dashscope.__version__` 当前为 `1.27.1`,2.0 尚未发布(只存在于 `V2` 分支), +> 因此**直接改名,不保留兼容 alias、不加 deprecation 垫片**。清单中少数标注"保留 alias"的 +> 条目,指的是服务端字段名与 SDK 对外名的双向别名属性(如 `Job.job_id` ↔ `Job.task_id`), +> 而非新旧 API 的过渡兼容。 + +### 6.1 方法名 / 资源树 + +| 优先级 | 现状 | 目标 | +|---|---|---| +| P0 | `rl.run` 不阻塞,与 `generate`/`transcribe` 语义撞车 | **`rl.jobs.create`** + 新增 `rl.jobs.wait`;`rl.get/list/cancel/logs` 一并收进 **`rl.jobs`**(`get` → `fetch`) | +| P1 | `apps.call` | **`apps.completions.create`** | +| P1 | `embeddings.create_multimodal` | **`multimodal.embeddings.create`** | +| P1 | `chat.completions.create` 不识别 parts | parts 自动路由到多模态端点 | +| P1 | `rl.logs` 承载微调日志 | **`fine_tunes.logs`**(`rl.jobs.logs` 转发,不留 alias) | +| P2 | `audio.asr.recognition` | `recognize` | +| P2 | `audio.realtime.session` | `connect` | +| P2 | `audio.asr.transcription.*`(6 层) | 拍平到 `audio.asr.*`(4~5 层) | +| P2 | `rl.register_functions` / `test_functions` / `upload_data` | `rl.functions.register` / `.test` / `rl.data.upload` | +| P2 | `fine_tunes.stream_events` 与 agentstudio `sessions.events.stream` 两种形态 | 保留但在文档集中说明(见 P2-5) | + +### 6.2 返回类型 + +| 优先级 | 现状 | 目标 | +|---|---|---| +| P0 | `Job.output` / `Job.usage` 裸 dict | 补 `JobResult` + `job.results` / `job.image_urls` / `job.video_url` / `job.result_url` | +| P0 | `rl.run` → `Any`(1.x 对象) | **`rl.jobs.create` → `FineTune`**:1.x dataclass 经 `_to_job` 适配为 v2 模型 | +| P0 | `rl.test_functions` → `Any` | **不改类型**:返回值就是客户自己 rollout/reward 函数的返回值,任何标注都是假的。以 docstring 明写这一事实 | +| P1 | `rl.register_functions` / `upload_data` → `Dict[str, List[str]]` | typed model | +| P1 | `FileUploadResponse.data` 裸 dict | 改返回 **`FileObject`**(`up.file_id`) | +| P1 | `FineTuneDelete` / `FineTuneCancel` / `DeploymentDelete` | 统一 **`JobStatus`**(原 `StatusResult`) | +| P1 | `MultiModalEmbedding` / `MultiModalEmbeddingResponse` | 合并入 **`Embedding` / `EmbeddingResponse`** | +| P1 | `AppCompletion.usage` 裸 dict | typed `Usage` | +| P1 | `apps.call(stream=True)` → 裸 `Iterator` | **`AppStream`**(带 `text_stream`),与 `ChatStream` 对齐 | +| P1 | `RlLogs` | **`JobLogs`** | +| P2 | `ModelObject` | `Model` | +| P2 | `Deployment` 无便捷属性 | 补 `dep.deployed_model` / `dep.status` | +| P2 | `MultiModal*` 大小写与 `multimodal` 命名空间不一致 | 统一 `Multimodal*` | + +### 6.3 入参 + +| 优先级 | 现状 | 目标 | +|---|---|---| +| P0 | `generate`/`transcribe` 的 `timeout` = HTTP 超时、`wait_timeout` = 轮询 | `timeout` = 总预算,提交超时改 **`submit_timeout`** | +| P0 | `Job.task_id` vs `FineTune.job_id` | 互为别名属性(服务端字段名映射,非兼容垫片),文档统一推荐 `job_id` | +| P1 | `files.list(page=)` | **`page_no=`** | +| P1 | `models.list(page_size=20)` vs 其余 10 | 对齐或 docstring 说明 | +| P1 | `models.get(model_name=)` | **`model=`** | +| P1 | `rl.test_functions(instance_id, functype, input_data)` 位置参数 | keyword-only,`functype` → **`func_type`** | +| P1 | `Asr.recognition(**kwargs)` / `Realtime.session(**kwargs)` / 4 个 `jobs.fetch`/`wait(**kwargs)` | **显式写出签名**再转发 | +| P1 | `rl.register_functions` / `test_functions` / `upload_data` / `jobs.create` 无 `timeout=` | **不补**:1.x `AgenticRL` 本身不接受 timeout,补上就是静默失效的假参数。改为在 `reinforcement_learning` 模块 docstring 中显式声明例外,文档同步修正全局承诺 | +| P1 | `extra_parameters`(apps / embeddings / rerank / tokenization / chat) | **`parameters`** | +| P1 | `embeddings.jobs.create(url=)` | **`file_url=`**(单数,见 P1-6 修正) | +| P1 | `apps.call` 的 `prompt`/`messages`/`history` 无互斥校验 | **移除 `history`**(2.0 文档从未出现、1.x 形状是 `{user, bot}`、与无状态原则冲突);剩下 `prompt`/`messages` 组合语义明确并保留"至少给一个"校验,见 P1-G | +| P2 | `videos.generate(img_url=)` | **已改** `image_url=`(内部仍写 `img_url` 上线) | +| P2 | `images.generate(size="1024*1024")` 不接受 `1024x1024` | **已改**:两种都接受,`_normalize_size` 归一化为服务端写法 | +| P2 | `extra_headers` 仅 `create_multimodal` 有 | **已移除**,改为检测到 `oss://` 入参自动带 `X-DashScope-OssResourceResolve`,见 P2-J | +| P2 | `connector` 无类型注解、未文档化 | **已补** `transport.ws.Connector` 公开别名并统一注解 | +| P2 | `Jobs.wait(sleep=...)` 测试钩子入公开签名 | **已改** `_sleep` | +| P2 | `create_multimodal(input=)` 不接受单个 item | **已改**:单个 dict / `EmbeddingItem` 均可 | +| P2 | `deployments.scale(model, 4)` 位置参数 | **已改** `scale(model, capacity=4)` keyword-only | +| P2 | `res_level` 等低频不透明参数散在签名 | **不收进 `parameters=`**:`res_level` / `max_video_frames` / `fps` 都是 1.x 已文档化的独立入参,且 `res_level` 就是服务端字段名,改名只会制造新的对不上。改为在 `multimodal.embeddings.create` docstring 中说明"仅快照模型可用",其余未列名参数一律走 `parameters=` | + +### 6.4 输出字段 + +| 优先级 | 现状 | 目标 | +|---|---|---| +| P0 | `DeploymentOutput.gmt_create` / `gmt_modified` | **`created_at` / `updated_at`** | +| P0 | `FineTuneOutput.create_time` / `end_time`、`Job.submitted_time` / `scheduled_time` / `end_time`、`FileObject.upload_time` | 统一 **`created_at` / `submitted_at` / `scheduled_at` / `finished_at` / `uploaded_at`** | +| P0 | `FineTuneOutput.finetuned_output` | **`finetuned_model`** | +| P0 | `Job.output` / `Job.usage` / `AppCompletion.usage` / `FineTuneOutput.usage` 裸 dict / Any | typed(见 6.2) | +| P1 | `deployments.create(version=)` | **`model_version=`**(与 `finetuned_model` 对齐) | +| P1 | `DeploymentOutput.model_name` | **`model`** | +| P1 | `Usage` / `EmbeddingUsage` / `RerankUsage` / `TokenCountUsage` 四个同构类 | 合并为单一 **`Usage`** | +| P1 | `DeploymentListOutput.total` / `FineTuneListOutput.total` | **`total_count`** | +| P1 | `Embedding.text_index` | **`index`** | +| P1 | `code` / `message` / `is_error` 仅 3 个模型有 | **已完成**:抽出 `ResponseBase`,全部顶层响应继承。`is_error` 改由 `ResponseBase` 的 after-validator 从非空 `code` **自动推导**(与 `errors.response_error` 同一判据),修掉了"非抛异常模式下除 chat/apps/fine_tunes 外全都漏标 `is_error`"的缺陷——不再有资源能忘记设置它。载荷是列表或 `data` 下单条、`flatten_envelope` 无从合并的地方,新增 `response_envelope(body)` 单独投递信封字段。三个例外(载荷里没有 HTTP 信封):`FineTuneEvent`(SSE 帧,自带 `is_error`)、`RegisteredFunctions` / `UploadedData`(透传 1.x 返回值的记录)。规范落进 `md/dashscope-2.0-design.md` §2.6 | +| P1 | `AppCompletion.output.*` / `FineTune.output.*` / `Deployment.output.*` / `*List.output.*` / `FileUploadResponse.data` 信封 | **已完成全面扁平化**(P1-L 方案 A)。`File` / `Model` / `Deployment` / `FineTune` 四个条目类型同时改为继承 `ResponseBase`;files、models 两处解析器原先从 `data` / `output` 下的单条建模、信封到不了模型上,已用 `response_envelope()` 逐个穿进 `_parse_upload` / `_parse_list` / `_parse_file` / `_parse_delete` 与 `models.get`(含 async) | +| P2 | `ChatCompletion.status_code` 孤例 | **已移入** `ResponseBase`,全部顶层响应共用 | +| P2 | `ModelObject.opensource` | **已改** `Model.open_source`(服务端 `opensource` 保留为 validation alias) | +| P2 | `RlLogs.logs` | **已改** `JobLogs.content` | +| P2 | `FineTuneEvent.output: Any` | **已改** `Union[Dict[str, Any], str, None]`(SSE 帧不保证是 JSON) | +| P2 | `AppOutput.doc_references` / `thoughts` 裸 dict 列表 | **已改**:扁平化后为 `AppCompletion.doc_references: List[DocReference]` / `thoughts: List[Thought]` | +| P2 | `Job.task_id: str` 声明必填但实际可空 | **已对齐**:`task_id: str = ""`,与 `job_id` 属性的 `str` 返回类型一致 | +| P2 | `MultiModalEmbeddingItem.factor` 排在模态字段之前 | **已改**:类名 `EmbeddingItem`,`factor` 移到四个模态字段之后 | +| P2 | `Apps.call(biz_params=)` 内部命名 | **不改名,明确文档化**:它是百炼 flow/plugin 入参的服务端公开字段,且落在 `input` 段而非 `parameters` 段;`create` docstring 已写明两者分工,见 P1-G | + +### 6.5 异常类名 + +**这一类名字不再由 SDK 决定。** 异常类名与 HTTP 状态一律从上游生成的错误目录 `dashscope/v2/_error_registry.py`(`PUBLIC_ERRORS` 16 条)派生:每条目录项的 `error_code` 就是类名、`status_code` 就是它的状态码。所以本节记录的不是"改成了什么名",而是**哪些名从 SDK 手写变成目录派生**,以及派生之后暴露出的缺类。规范正文见 `md/dashscope-2.0-design.md` §2.7,逐条决策与验证数据见 `md/dashscope-2.0-errors-registry-plan.md`。 + +| 优先级 | 现状 | 目标 | +|---|---|---| +| P0 | 状态类的类名与 `status_code` 在 `errors.py` 里手写字面量,`_STATUS_MAP` 只有 5 项(400 / 401 / 403 / 404 / 429) | **已改**:9 个类全部从 `PUBLIC_ERRORS` 派生(`_CATALOG_STATUS` / `_STATUS_MAP`),目录改状态码 SDK 自动跟随,无需改代码 | +| P0 | 503 / 504 无类,与 500 一并落 `InternalServerError`(`ServiceUnavailable` / `GatewayTimeout` 两个线缆码也被压平) | **已补** `ServiceUnavailableError(503)` / `GatewayTimeoutError(504)`;目录里它们本就是两条独立项,压平是 SDK 侧丢信息 | +| P0 | 413 无类,落裸 `APIStatusError` | **已补** `RequestTooLargeError(413)` | +| P0 | 目录未收录的状态码走 `>= 500` 猜测,418 / 502 / 507 一律变成 `InternalServerError` | **已改**:落 `APIStatusError` 本身,**不猜**——猜出来的类会让 `except InternalServerError` 抓到并非 500 的失败,且实例 `status_code`(502)与类声明的(500)矛盾。重试判定不受影响:`transport/_retry.py` 按原始状态码决定,不看异常类 | +| P1 | 13 个 `ErrorCode` 线缆码只有 11 个进 `_CODE_MAP`;漏掉的 `InvalidParameter` 在业务层(HTTP 200)落基类 `DashScopeError`、`Model.NotExist` 完全无映射 | **已补** `_WIRE_TO_PUBLIC` crosswalk 覆盖全部 13 项,业务层与 HTTP 层现在得到同一个类 | +| P1 | `ContentFilterError(DashScopeError)` 与其余状态类不同族,`except BadRequestError` 抓不到审核失败 | **已改** `ContentFilterError(BadRequestError)`:目录把 `content_policy_violation` 命名为 `BadRequestError`,细化只能用子类化,不能引入目录外的状态 | +| P1 | `rl` 直通 1.x,失败抛裸 1.x 异常(`common/errors.py` 共 19 个类),`except DashScopeError` 抓不到 | **已补** `SdkError`:对应目录 `SDK_ERRORS`(32 条)的客户端侧错误,`code` = 目录名、`solution` = 目录随附的修复建议;挂载层 8 个直通方法(sync + async)全部经 `error_from_sdk_exception` 翻译 | +| P2 | `APIStatusError` 读作"非 2xx 响应" | **已改语义**:读作"**网关声明的失败**"——业务层(HTTP 200 带 `code`)造出的实例也是它,实例的 `status_code` 说明是哪一层 | +| P2 | `SdkError` 是否该配一个状态码 | **不给,`status_code is None`**:目录的 `SdkErrorDef` 没有 `external` 字段指回 `PublicError`,标准本身就没给 SDK 侧错误任何公开投影,凭空造一个等于给标准里没有的东西加含义 | +| P2 | `agentstudio` 直通 1.x 异常 | **未改**,仍裸奔:其挂载层直接返回 1.x client 对象,异常在 SDK 边界外抛出,需要透明代理才能拦截,挂在 M3 任务 3.12 | + +**没有引入任何 SDK 自造名。** 新增的 4 个类逐个对应目录里已有的条目:`RequestTooLargeError` / `ServiceUnavailableError` / `GatewayTimeoutError` 是 `PUBLIC_ERRORS` 的 `error_code`,`SdkError` 是 `SDK_ERRORS` 所在的客户端子图。公开异常类 11 → 15,`APIStatusError` 的直接子类 6 → 9 且互为扁平兄弟。唯一带 SDK 判断的是 crosswalk:它把百炼历史线缆码对到目录 key 上,属"选哪条目录项",不属命名——类永远由目录项给出,`test_wire_code_never_leaves_the_catalogued_class` 对每一行钉住这一点。 diff --git a/md/dashscope-2.0-realtime-sdk.md b/md/dashscope-2.0-realtime-sdk.md new file mode 100644 index 00000000..242dddd5 --- /dev/null +++ b/md/dashscope-2.0-realtime-sdk.md @@ -0,0 +1,449 @@ +# Realtime 实时交互 Python SDK + +`client.audio.realtime` 使用同一个同步 WebSocket 会话接口,支持 Omni 实时多模态交互、Qwen-Audio 实时音频对话和 Qwen-Livetranslate 实时翻译。本文介绍当前仓库的 DashScope 2.0 实现,不适用于旧版 `OmniRealtimeConversation` / `OmniRealtimeCallback`。 + +SDK 通过 `recv_event()` 或 `events()` 返回原始 JSON 事件,不启动后台回调线程。音频输入可使用原始 `bytes`;音频输出位于 JSON 事件的 Base64 字段中,需要调用方解码。 + +## 前期准备 + +1. 在当前仓库根目录安装源码,确保使用的是包含本文接口的版本。本文不声明线上 PyPI 包的最低版本。 + + ```bash + python -m pip install -e . + ``` + +2. 配置 `DASHSCOPE_API_KEY`;推荐同时配置 `DASHSCOPE_WORKSPACE`,并按模型的服务地域设置 `DASHSCOPE_REGION`。不应把 API Key 写入代码或提交到仓库。 +3. 示例输入 `input.pcm` 为无文件头的 16 kHz、16 bit、单声道 PCM。SDK 不会自动重采样、解码 WAV 文件或采集麦克风;实际音频格式必须与会话配置一致。 + +```python +from dashscope import Client + +with Client(timeout=30.0) as client: + print(client.config.region) +``` + +`Client` 可通过 `api_key`、`workspace`、`region`、`timeout` 显式覆盖配置。默认地域为 `cn-beijing`,默认超时为 300 秒;地域可配置不代表该地域提供所有模型。未配置 workspace 时,SDK 回退至共享网关并发出警告。 + +> Realtime 当前仅支持同步 `Client`,不提供 `AsyncClient.audio.realtime`。需要双向并发时,由应用安排一个发送线程和一个接收线程;同一时刻只能有一个接收者。 + +## 快速开始 + +### Omni / Qwen-Audio:手动提交一轮音频 + +下面的完整示例同时发送和接收。默认使用 Omni;将 `MODEL` 改为 `qwen-audio-3.1-realtime-plus`,即可使用 Qwen-Audio 的配置和结束流程。 + +```python +import base64 +import time +from concurrent.futures import ThreadPoolExecutor + +from dashscope import Client + +MODEL = "qwen-omni-turbo-realtime" +IS_QWEN_AUDIO = MODEL.startswith("qwen-audio-") + + +def receive_response(session): + with open("response.pcm", "wb") as output: + for event in session.events(): + kind = event.get("type") + if kind == "response.audio.delta": + output.write(base64.b64decode(event["delta"])) + elif kind in ( + "response.text.delta", + "response.audio_transcript.delta", + ): + print(event.get("delta", ""), end="", flush=True) + elif kind == "response.done": + print("\nusage:", event.get("response", {}).get("usage")) + return + raise RuntimeError("连接已关闭,未收到本轮 response.done") + + +with Client(timeout=30.0) as client: + with client.audio.realtime.connect(model=MODEL) as session: + settings = { + "input_audio_format": "pcm" if IS_QWEN_AUDIO else "pcm16", + "output_audio_format": "pcm" if IS_QWEN_AUDIO else "pcm16", + "enable_turn_detection": False, + } + if not IS_QWEN_AUDIO: + settings["voice"] = "Chelsie" + session.update_session(["text", "audio"], **settings) + for event in session.events(): + if event.get("type") == "session.updated": + break + else: + raise RuntimeError("连接已关闭,未收到 session.updated") + + with ThreadPoolExecutor(max_workers=1) as pool: + receiver = pool.submit(receive_response, session) + try: + with open("input.pcm", "rb") as source: + while True: + chunk = source.read(3200) + if not chunk: + break + session.append_audio(chunk) + time.sleep(0.1) + session.commit() + session.create_response() + receiver.result() + if not IS_QWEN_AUDIO: + session.end_session() + for event in session.events(): + print(event) + finally: + session.close() +``` + +3200 字节约等于上述输入格式的 100 ms 音频,示例用等待模拟实时采集;麦克风本身按实时节奏产帧时,不需要再等待。`response.pcm` 没有 WAV 文件头,播放时必须使用服务端实际输出的采样率、位宽和声道配置。 + +- **Omni / Qwen-Audio 的 Manual 模式**:`append_audio()` → `commit()` → `create_response()` → 读取至 `response.done`。 +- **Qwen-Audio 关闭连接**:读完当前响应后调用 `close()` 或退出上下文,不调用 `end_session()`。 +- **Omni 结束会话**:调用 `end_session()`,继续接收至 `session.finished`。 +- 多轮对话时,不要每轮退出会话上下文;保持连接,重复输入和响应流程。 + +### Livetranslate:边输入边翻译,并读取最终结果 + +下面以 `qwen3.5-livetranslate-flash-realtime` 为例,使用服务端默认语音检测。文件发送结束后请求结束会话,接收线程继续读取最终转写、译文、用量和 `session.finished`。 + +```python +import base64 +import time +from concurrent.futures import ThreadPoolExecutor + +from dashscope import Client + + +def receive_translation(session): + with open("translation.pcm", "wb") as output: + for event in session.events(): + kind = event.get("type") + if kind == "response.audio.delta": + output.write(base64.b64decode(event["delta"])) + elif kind in ( + "response.text.text", + "response.audio_transcript.text", + ): + print("text:", event.get("text"), "stash:", event.get("stash")) + elif kind in ( + "response.text.delta", + "response.audio_transcript.delta", + ): + print(event.get("delta", ""), end="", flush=True) + elif kind == "response.done": + print("usage:", event.get("response", {}).get("usage")) + else: + print(event) + + +with Client(timeout=30.0) as client: + with client.audio.realtime.connect( + model="qwen3.5-livetranslate-flash-realtime", + ) as session: + session.update_session( + output_modalities=["text", "audio"], + translation_params={"language": "en"}, + ) + for event in session.events(): + if event.get("type") == "session.updated": + break + else: + raise RuntimeError("连接已关闭,未收到 session.updated") + with ThreadPoolExecutor(max_workers=1) as pool: + receiver = pool.submit(receive_translation, session) + try: + with open("input.pcm", "rb") as source: + while True: + chunk = source.read(3200) + if not chunk: + break + session.append_audio(chunk) + time.sleep(0.1) + session.end_session() + receiver.result() + finally: + session.close() +``` + +此示例也可将模型替换为 `qwen3-livetranslate-flash-realtime` 或 `qwen3.8-livetranslate-flash-realtime`;只传输出模态和目标语种时,SDK 会按模型转换字段名,其他参数保留服务端默认值。 + +> Livetranslate 不使用上面的对话触发流程。Manual 模式下 `commit()` 会让服务端自动生成翻译响应,不再发送 `create_response()`;默认语音检测模式则自动提交。不要在每次 `response.done` 时关闭整个会话。 + +## 请求参数 + +### 创建会话:`client.audio.realtime.connect()` + +返回 `RealtimeSession`,此时尚未连接。进入 `with` 上下文或调用返回对象的 `connect()` 后才建立连接。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `model` | `str` | 必填,模型名称;没有默认模型。 | +| `ws_url` | `str` 或 `None` | 默认 `None`,根据 workspace 和 region 选择 Realtime 网关。显式设置时覆盖默认地址。 | +| `timeout` | `float` 或 `None` | 秒;默认继承 `Client` 配置。显式值必须大于 0,作用于连接和 socket 操作,不是会话总时长。 | +| `headers` | `Mapping[str, str]` 或 `None` | 额外握手头;按不区分大小写的名称覆盖默认头,包括鉴权头。不要在日志中输出 API Key。 | +| `connector` | `Callable[[str, dict], Any]` 或 `None` | 自定义连接函数,参数为 URL 和 headers;返回支持 `send`、`recv`、`settimeout`、`close` 的对象。默认使用 `websocket-client`。 | + +如果 `ws_url` 已包含 `model` 查询参数,该参数决定实际服务端模型和 SDK 配置映射;`session.model` 仍保留构造时的 `model` 参数。建议两处保持一致。 + +Realtime 使用独立的 `/api-ws/v1/realtime` 路径,不受 `DASHSCOPE_WEBSOCKET_BASE_URL` 的 ASR/TTS inference 地址覆盖影响;需要覆盖时传 `ws_url`。`Client.base_url` 是 HTTP 地址,也不替代 `ws_url`。 + +自定义 `connector` 不接收第三个超时参数,建立连接阶段的超时由连接函数负责;SDK 在连接返回后设置 socket 超时。其 `close()` 应能解除正在阻塞的接收操作。 + +### 更新会话:`session.update_session()` + +发送 `session.update`,返回 `None`,不等待 `session.updated`。应用应确认配置成功后再发送依赖这些配置的音频。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `output_modalities` | `list[str]` | Omni 必填;Qwen-Audio/Live 可省略。常用 `['text', 'audio']`,取值组合须由模型支持。Live 3.8 映射为 `output_modalities`,其他模型映射为 `modalities`。 | +| `voice` | `str` 或 `None` | 音色。Omni 未传时发送 `None`;新模型未传时不发送,使用服务端默认值。Live 3.8 写入 `audio.output.voice`。 | +| `input_audio_format` | `str` | Omni 默认 `pcm16`;新模型不补默认值。Qwen-Audio 的 PCM 使用 `pcm`,而非 `pcm16`。Live 3.8 映射为 `audio.input.format.type`。 | +| `output_audio_format` | `str` | Omni 默认 `pcm16`;新模型不补默认值。Live 3/3.5 的输出 PCM 使用 `pcm`;Live 3.8 写入 `audio.output.format.type`。 | +| `enable_input_audio_transcription` | `bool` | Omni 默认 `True`。Live 3/3.5 显式 `True` 选择 `qwen3-asr-flash-realtime`,`False` 发送 `{'model': None}`。Live 3.8 ASR 始终开启,显式关闭会抛出 `ValueError`。Qwen-Audio 示例不主动设置此项。 | +| `input_audio_transcription_model` | `str` 或 `None` | 指定转写模型;Omni 默认 `None`。Live 3/3.5 显式 `None` 关闭 ASR,Live 3.8 禁止这一关闭配置。 | +| `enable_turn_detection` | `bool` | Omni 默认 `True`;新模型未传不更改。显式 `False` 发送语音检测字段为 `None`,用于模型支持的 Manual 模式。 | +| `turn_detection_type` | `str` | Omni 默认 `server_vad`。新模型只有显式设置才发送;显式开启检测且未给类型时,Live 3.8 选择 `speaker_detection`,其他模型选择 `server_vad`。 | +| `prefix_padding_ms` | `int` | 检测起点前保留的音频时长,毫秒;Omni 默认 300,新模型未传不发送,支持情况按模型协议。 | +| `turn_detection_threshold` | `float` | 检测阈值;Omni 默认 0.2。新模型未传不发送。Live 3.8 的 `speaker_detection` 阈值由协议固定为 0.5,不应使用 Omni 默认值替代。 | +| `turn_detection_silence_duration_ms` | `int` | 判定结束的静音时长,毫秒;Omni 默认 800,新模型未传不发送。 | +| `turn_detection_param` | `dict` 或 `None` | 额外语音检测参数,字典同名字段覆盖上述字段。Omni 的 `None` 表示不追加;新模型显式 `None` 表示关闭检测,未传则不更改。 | +| `translation_params` | `dict` 或 `None` | 翻译配置,写入 `translation`,如 `{'language': 'en'}`。Live 3/3.5 可按协议设置 `corpus.phrases` 热词映射。新模型空字典保持为空,不补 `language: None`。 | +| `transcription_params` | `dict` 或 `None` | 输入转写配置;`language` 等字段进入 `input_audio_transcription`,`corpus_text` 转为 `corpus.text`,`input_audio_format`/`sample_rate` 提升为音频配置。详见下方规则。 | +| `input_audio_config` | `dict` 或 `None` | 嵌套输入格式,如 `{'type': 'pcm', 'sample_rate': 16000}`,写入 `audio.input.format`。优先用于支持新结构的模型。 | +| `output_audio_config` | `dict` 或 `None` | 嵌套输出格式,如 `{'type': 'pcm', 'sample_rate': 24000}`,写入 `audio.output.format`。 | +| `input_video_representation_compact` | `str` 或 `None` | 写入 `video.input.representation_compact`;是否支持取决于模型,并非所有模型都接收图像。 | +| `**kwargs` | `Any` | 额外 session 字段,如 `instructions`、`tools`、`sample_rate`、`audio`。字段按协议透传,同名原始字段优先;SDK 不保证所有模型支持所有扩展参数。 | + +### 默认值、增量更新和覆盖规则 + +**Omni** 保留完整默认配置行为:每次调用 `update_session()` 都需要输出模态,并重新构建默认音色、格式、ASR、VAD 等字段,不是只更新显式参数。只修改一个原始字段时可使用 `send_event()`: + +```python +session.send_event({ + "type": "session.update", + "session": {"instructions": "请简洁回答。"}, +}) +``` + +**Qwen-Audio、Live 3/3.5/3.8** 只发送显式参数,没有传入的字段不发送,也不会从上一次调用缓存回填: + +```python +session.update_session(instructions="请简洁回答。") +``` + +对应的 session 内容仅为 `{'instructions': '请简洁回答。'}`。此示例适用于支持 `instructions` 的模型;透传能力不代表模型能力。 + +- 新模型区分“未传”和“显式 `None`”;保留显式的 `False`、`0`、空字符串、空字典和空列表。保留这些值不意味着服务端一定接受,字段的合法性仍以模型协议为准。 +- Omni 的 `transcription_params={}` 会补输入 `pcm` / 16000,并过滤该字典的 `None` 值;新模型不补这两个默认值,也不统一过滤 `None`。 +- Live 3/3.5 的显式 ASR 关闭开关优先于转写模型和 `transcription_params`。原始 `input_audio_transcription` kwargs 则可整体覆盖便捷参数生成的字段。 +- Omni 任一不为 `None` 的嵌套格式配置(包括 `{}`)会抑制旧式顶层音频格式;Qwen-Audio/Live 的显式嵌套格式参数也走嵌套序列化,不能据此推断旧模型支持它。 +- Live 3.8 将旧式 `voice`、`input_audio_format`、`output_audio_format`、`sample_rate`、`turn_detection` 转为嵌套字段,不混发对应的旧式顶层字段。嵌套字段覆盖同名便捷字段,未冲突的格式字段会合并;显式 `None` 可整体覆盖子配置。 +- `**kwargs` 的原始 `audio` 会先替换由 `input_audio_config` / `output_audio_config` 生成的 `audio`。不建议同时用两种入口配置同一个子对象;Live 3.8 随后会合并仍存在的顶层便捷字段。 +- Live 3.8 禁止关闭 ASR,并拒绝 `same_language_skip_options`。这是 `update_session()` 的检查;`send_event()` 不执行该检查,原始发送也不代表服务端会接受非法配置。 + +Live 3.8 的配置示例(在已打开的会话中执行): + +```python +session.update_session( + output_modalities=["text", "audio"], + voice="Tina", + input_audio_config={"type": "pcm", "sample_rate": 16000}, + output_audio_config={"type": "pcm", "sample_rate": 24000}, + turn_detection_param={"type": "speaker_detection", "threshold": 0.5}, + translation_params={"language": "en"}, +) +``` + +生成的 session 内容如下;`event_id` 由 SDK 自动补充: + +```json +{ + "output_modalities": ["text", "audio"], + "translation": {"language": "en"}, + "audio": { + "input": { + "format": {"type": "pcm", "sample_rate": 16000}, + "turn_detection": {"type": "speaker_detection", "threshold": 0.5} + }, + "output": { + "format": {"type": "pcm", "sample_rate": 24000}, + "voice": "Tina" + } + } +} +``` + +## 关键接口 + +### 连接与事件收发 + +| 方法 | 参数 | 返回值 | 描述 | +| --- | --- | --- | --- | +| `session.connect()` | 无 | 当前 `RealtimeSession` | 建立连接,不自动读取 `session.created`;重复打开会抛出 `RuntimeError`。上下文入口会自动调用,勿先 connect 再进入同一对象的 with。 | +| `send_event(event)` | `event: dict` | `None` | 发送 JSON 事件;缺少 `event_id` 时生成 `event_` 加 UUID。显式 ID 保留,不修改调用方字典。 | +| `recv_event()` | 无 | `dict` 或 `None` | 阻塞接收一条事件。正常关闭后返回 `None`;活动响应或结束确认未完成时断线会抛异常。 | +| `events()` | 无 | `Iterator[dict]` | 连续读取。`response.done` 不自动终止迭代;收到 `session.finished` 后产出该事件一次并结束。 | +| `close()` | 无 | `None` | 幂等关闭本地连接,不发送 `session.finish`、不等待残留结果。重新 `connect()` 会建立新 socket 并清空会话统计。 | + +### 音频与图像输入 + +| 方法 | 参数 | 返回值 | 描述 | +| --- | --- | --- | --- | +| `append_audio(audio_b64)` | `str` 或 `bytes` | `None` | 发送 `input_audio_buffer.append`;字符串视为已编码的 Base64,bytes 自动 Base64 编码。不是发送文本转语音输入。 | +| `append_video(video_b64)` | `str` 或 `bytes` | `None` | 发送单帧 `input_image_buffer.append`;不是视频文件上传,也不从视频自动抽帧。 | +| `commit()` | 无 | `None` | 发送 `input_audio_buffer.commit`,不等待提交确认,也不隐式调用 `create_response()`。 | +| `clear_appended_audio()` | 无 | `None` | 发送 `input_audio_buffer.clear`,清除服务端未提交的音频缓冲;不是取消已在生成的响应。 | + +适用模型的图像输入可以在已追加音频后执行: + +```python +from pathlib import Path + +session.append_video(Path("frame.jpg").read_bytes()) +``` + +Livetranslate 的图像使用 JPG/JPEG,编码前不超过 500 KB、帧率不超过 2 fps,且必须先至少追加一次音频;模型版本是否支持以协议为准。Qwen-Audio 的所列客户端协议没有图像事件,不要默认套用。SDK 不进行图像转换、大小检查或自动限帧。 + +### 对话和响应控制(Omni / Qwen-Audio) + +| 方法 | 参数 | 返回值 | 描述 | +| --- | --- | --- | --- | +| `create_item(item, *, previous_item_id=None)` | `item: dict`;前置条目 ID 可选 | `None` | 发送 `conversation.item.create`;可创建文本消息或函数调用结果。指定 `previous_item_id` 时传给服务端决定插入位置。 | +| `retrieve_item(item_id)` | `item_id: str` | `None` | 发送 `conversation.item.retrieve`,从后续事件读取结果。 | +| `delete_item(item_id)` | `item_id: str` | `None` | 发送 `conversation.item.delete`。 | +| `create_response(modalities, instructions, **kwargs)` | 两个参数均可省略;类型分别为 `list[str]`、`str`,均可显式 `None` | `None` | 发送 `response.create`。Omni 无参时发送 `{'instructions': None}`,Qwen-Audio 无参时发送空 response 对象;其他字段按 kwargs 透传。 | +| `cancel_response()` | 无 | `None` | 发送 `response.cancel`;继续接收服务端取消结果和 `response.done`,不关闭会话。 | + +在已打开的 Omni 会话中,文本输入示例为: + +```python +session.create_item({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "请简单介绍一下你自己。"}], +}) +session.create_response(modalities=["text", "audio"]) +``` + +工具结果通过 `create_item({'type': 'function_call_output', 'call_id': ..., 'output': ...})` 回填,再按模型协议创建响应。参数中的 ID 和 output 需使用真实函数调用结果。Livetranslate 的翻译流程不使用上述客户端对话和响应控制事件。 + +### 结束会话:`end_session()` + +| 模型 | 行为 | +| --- | --- | +| Omni | 发送 `session.finish`,立即返回;继续读取至 `session.finished`。 | +| Live 3/3.5/3.8 | 发送 `session.finish`;继续读取剩余输入转写、翻译音频、用量和最终确认。收到 `session.finished` 时 SDK 自动关闭连接。 | +| Qwen-Audio | 抛出 `NotImplementedError`,不发送事件、不关闭连接、不改变会话状态。应读完或取消当前响应,再 `close()`。 | + +请求结束后不能再发送音频、修改配置或创建响应;连接仍打开时重复 `end_session()` 不重复发送。若只收到 `response.done` 就断线,但尚未收到请求的 `session.finished`,SDK 仍将其视为未完整结束。 + +## 模型差异与调用链路 + +| 场景 | Omni | Qwen-Audio | Live 3/3.5 | Live 3.8 | +| --- | --- | --- | --- | --- | +| 配置方式 | helper 每次补 Omni 默认值 | 仅发送显式字段 | 仅发送显式字段 | 仅发送显式字段并映射嵌套音频配置 | +| 输出模态协议字段 | `modalities` | `modalities` | `modalities` | `output_modalities` | +| 输入转写 | 按会话配置 | 不注入 Omni 转写配置 | 默认开启,可显式关闭 | 始终开启,不可关闭 | +| Manual 提交后的响应 | `commit` 后 `create_response` | `commit` 后 `create_response` | `commit` 后自动生成 | 按模型支持的检测模式运行,不照搬 Omni 响应控制 | +| 主要输出文本事件 | `.delta` | `.delta` | `.text`,含 `text` / `stash` | `.delta` | +| 结束整个会话 | finish + finished | 读完当前响应后 close | finish + finished | finish + finished | + +```text +Omni / Qwen-Audio Manual +connect → update_session → session.updated + → append_audio(发送与接收并发)→ commit → create_response + ← response.created / 文本与音频 / response.done + → 下一轮输入,或按模型结束连接 + +Livetranslate +connect → update_session → session.updated + → append_audio(语音检测自动提交,或支持的 Manual 模式 commit) + ← 输入转写 / response.created / 译文与音频 / response.done + → end_session + ← 剩余结果 / session.finished → 自动关闭 +``` + +Qwen-Audio 支持 `server_vad`、`smart_turn` 和 Manual 等模式,不能套用 Omni 的所有检测参数;其音色仅首次更新有效,输入格式和检测设置有首次音频输入之前的限制。SDK 不维护服务端完整的 IDLE/生成/打断状态机,应按模型协议安排配置时机。 + +## 返回事件与会话属性 + +### 事件内容 + +| 事件 | 主要内容 | 处理方式 | +| --- | --- | --- | +| `session.created` / `session.updated` | `session` | 确认会话和配置,SDK 更新 `session_id`。 | +| `input_audio_buffer.speech_started` / `.speech_stopped` | 音频起止毫秒、条目 ID;部分版本提供 `speaker_id` | 原样保留,按 `item_id` 关联,不换算成 SDK 首包延迟。 | +| `conversation.item.input_audio_transcription.*` | 输入转写、语言等模型字段 | 原样读取;不计入译文/回复首文本延迟。 | +| `response.created` | `response.id` | 开始一轮响应,重置该轮首文本/首音频延迟。 | +| `response.text.delta` / `response.audio_transcript.delta` | `delta` | 按响应和条目分组,依次累加文本。 | +| `response.text.text` / `response.audio_transcript.text` | `text`、`stash` | Live 3/3.5 的文本片段及临时文本。`stash` 可能修正,不应作为确定文本永久累加;以最终 done 事件校准。 | +| `response.audio.delta` | Base64 `delta` | 解码后写文件或播放;没有单独的 bytes 迭代器。 | +| `response.text.done` / `response.audio_transcript.done` | 最终 `text` / `transcript` | 用于确认最终文本,具体结构依模型协议。 | +| `response.done` | `response`,可能含 `usage`、状态及输出条目 | 保存每轮用量和完成状态,不能仅凭事件类型认定生成成功。 | +| `session.finished` | 会话最终确认 | 结束迭代并自动关闭。 | +| 其他事件 | 函数参数、环境音转写、自定义扩展等 | 不进行字段白名单过滤,原样返回。 | + +Realtime 不提供独立 `usage` 属性或完整事件历史缓存。每轮用量从 `event['response'].get('usage')` 获取;原始字段可以包含 `input_tokens`、`output_tokens`、`total_tokens` 及细分类别,以该模型实际返回为准。 + +### 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `model` | `str` | 构造时的模型参数。 | +| `url` | `str` | 实际连接 URL,包括模型查询参数。 | +| `session_id` | `str` 或 `None` | 从已读取的 session.created / updated 中更新。 | +| `last_message` | `dict` 或 `None` | 最近读取的 JSON 事件,包括服务端 error;后续事件会覆盖它,不是历史。 | +| `last_response_id` | `str` 或 `None` | 最近一次 response.created 中的响应 ID。 | +| `last_response_create_time` | `float` 或 `None` | 收到 response.created 时的本地单调时钟毫秒值,不是 Unix 时间戳。 | +| `last_first_text_delay` | `float` 或 `None` | 从读取 response.created 到读取该轮首条输出文本事件的毫秒数;支持 `.text` 和 `.delta`。 | +| `last_first_audio_delay` | `float` 或 `None` | 从读取 response.created 到读取该轮首条 response.audio.delta 的毫秒数。 | + +统计在应用消费事件时更新,不是服务端耗时,也不是从发送音频起算。相同响应的重复首包、已结束响应的迟到包和显式不同 response_id 的包不会覆盖该轮首包延迟。只发送不读取时,属性不会由后台自动更新。 + +## 异常处理 + +| 场景 | 表现与处理 | +| --- | --- | +| 服务端 `error` 事件 | 转为 `dashscope.v2.errors` 中的业务异常,如 `BadRequestError`、`RateLimitError`;原始事件在异常 `body` 和 `last_message` 中,不作为普通事件 yield。 | +| `invalid_request_error` 且未请求结束 | SDK 保持连接;修正参数后可继续发送。若通过 `events()` 遇到异常,该迭代器已经终止,需要创建新迭代器或调用 `recv_event()`。 | +| 其他服务端错误,或结束期间的错误 | 关闭连接并抛异常。不要继续向旧会话发送。 | +| 活动响应缺少 `response.done`,或已请求结束但缺少 `session.finished` 时断线 | 抛出 `APIConnectionError`,不把截断输出当作完整结果。 | +| socket 超时、网络重置、发送失败 | 关闭连接并传播底层异常,包括 `websocket.WebSocketTimeoutException`、`OSError` 等;不保证全部转换为 SDK 连接异常。 | +| 非法媒体类型或非法 JSON | 类型/解析异常;媒体只支持字符串或 bytes,不自动读取路径。 | +| 未连接、重复连接、结束后发送 | `RuntimeError`。 | +| Live 3.8 的禁止配置 | `ValueError`,在发送前拒绝,不破坏当前连接。 | +| Qwen-Audio 调用 `end_session()` | `NotImplementedError`;改用读取完成后关闭连接。 | + +WebSocket 不使用 HTTP 自动重试来重放音频或响应请求,`Client.max_retries` 不提供断线续传。应用需要自行决定是否重建会话,以及如何处理已发送但未确认的输入。 + +## 常见问题 + +### 为什么 `response.done` 后 `events()` 还在等待? + +它只代表当前响应结束,会话可以继续多轮交互。单轮读取可以在该事件后退出循环;结束整个会话需按对应模型执行 finish 或 close 流程。`break` 不会自动关闭会话。 + +### 能否同时在两个线程调用 `events()`? + +不应这样做,事件会被两个消费者分流。只保留一个读取者;它可以将事件分发给播放、字幕和统计逻辑。发送与接收可以并行。 + +### 是否有 `start()` / `stop()`? + +Realtime 的生命周期接口为 `connect()` / `end_session()` / `close()`,不是 ASR/TTS 的 `start()` / `stop()`。`cancel_response()` 取消单轮响应,与关闭整个会话不同。 + +### 同一连接可以复用吗? + +Omni/Qwen-Audio 的同一会话可完成多轮对话,Live 也可在会话中连续翻译多段音频。`close()` 后重新 `connect()` 是新连接、新会话,不是恢复旧上下文。这是协议设计而非 SDK 限制:realtime 协议没有 task_id,一条连接就是一个会话,会话结束服务端即关闭连接,因此 `WsConnection` 多任务复用对它不适用(ASR/TTS/翻译/TingWu 等 run-task 族可以,见[连接复用指南](dashscope-2.0-connection-reuse-sdk.md))。要压首开延迟,建议在用户开始说话前提前 `connect()` 预热。仅关闭 `Client` 不替代实时会话的关闭;重连前应等待旧的收发线程退出。 + +## 相关文档 + +- [ASR Python SDK](dashscope-2.0-asr-sdk.md) +- [TTS Python SDK](dashscope-2.0-tts-sdk.md) +- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) +- [2.0 接口示例总览](dashscope-2.0-interfaces-examples.md) +- [Omni Python SDK(本文结构参考,接口为旧版)](https://help.aliyun.com/zh/model-studio/omni-realtime-python-sdk) +- [Qwen-Audio 客户端事件](https://help.aliyun.com/zh/model-studio/fun-audiochat-client-events) +- [Qwen-Audio 服务端事件](https://help.aliyun.com/zh/model-studio/qwen-audio-realtime-server-events) +- [Livetranslate 客户端事件](https://help.aliyun.com/zh/model-studio/live-translator-client-events) +- [Livetranslate 服务端事件](https://help.aliyun.com/zh/model-studio/live-translator-server-events) diff --git a/md/dashscope-2.0-translation-sdk.md b/md/dashscope-2.0-translation-sdk.md new file mode 100644 index 00000000..e70da935 --- /dev/null +++ b/md/dashscope-2.0-translation-sdk.md @@ -0,0 +1,160 @@ +# 实时语音翻译(Gummy)Python SDK + +通过 `client.audio.translation`,您可以在一条 WebSocket 双工会话上边发送音频边获取识别与翻译结果(对应 1.x `TranslationRecognizerRealtime` / `TranslationRecognizerChat`,模型如 `gummy-realtime-v1`、`gummy-chat-v1`)。本文对应当前仓库的 SDK 实现;模型能力、音频限制及服务端字段以相应模型协议为准。 + +更新的 qwen3-livetranslate 实时翻译模型(如 `qwen3.5-livetranslate-flash-realtime`)走 Realtime 协议,请使用 [`audio.realtime.connect`](dashscope-2.0-realtime-sdk.md) 并配置 `update_session(translation_params=...)`,不在本资源面。 + +| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | +| --- | --- | --- | +| 实时语音翻译 | `audio.translation.recognize()` | 不支持 | +| 本地文件翻译 | `audio.translation.recognize_file()` | 不支持 | + +## 前期准备 + +1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 +2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置。 +3. 在本仓库根目录执行源码安装:`python -m pip install -e .`。 +4. 实时示例需要当前目录下的 `input.pcm`:有限长度、无文件头、16 kHz、16 bit、单声道的原始 PCM 音频。 + +## 快速开始 + +### 实时语音翻译 + +以下代码可保存为 `translation_example.py`,在音频文件所在目录运行 `python translation_example.py`。发送和接收分别运行在两个线程中,逐块读取文件、逐条处理双语结果。 + +```python +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +from dashscope import Client + + +def send_audio(session, audio_path): + with open(audio_path, "rb") as audio: + while True: + frame = audio.read(3200) + if not frame: + break + session.send_audio_frame(frame) + time.sleep(0.1) + session.stop() + + +def receive_results(session): + for result in session.results(): + transcription = result.get_transcription() + if transcription and transcription.get("text"): + label = "最终" if result.is_sentence_end() else "中间" + print(label, "识别:", transcription["text"], flush=True) + for translation in result.get_translations(): + label = "最终" if translation.get("sentence_end") else "中间" + print(label, f"翻译({translation['lang']}):", translation["text"], flush=True) + + +def main(audio_path): + with Client() as client: + session = client.audio.translation.recognize( + model="gummy-realtime-v1", + format="pcm", + sample_rate=16000, + source_language="zh", + translation_enabled=True, + translation_target_languages=["en"], + timeout=30.0, + ) + with ThreadPoolExecutor(max_workers=2) as executor: + try: + session.start() + futures = [ + executor.submit(receive_results, session), + executor.submit(send_audio, session, audio_path), + ] + for future in as_completed(futures): + future.result() + finally: + session.close() + print("request_id:", session.request_id) + print("usage:", session.usage) + + +if __name__ == "__main__": + main("input.pcm") +``` + +对应 1.x `TranslationRecognizerRealtime.start()` + `send_audio_frame()` + `stop()` 的回调流程:v2 用主动迭代 `session.results()` 替代 `TranslationRecognizerCallback.on_event`,每一项就是 1.x 回调收到的 `(transcription_result, translation_result, usage)` 组合。 + +### 本地文件翻译 + +对应 1.x `TranslationRecognizerRealtime.call(file)`:文件自动按 12800 字节分块、单会话跑到底,逐项产出类型化结果(1.x `TranslationRecognizerResultPack` 的逐项形态)。 + +```python +from dashscope import Client + +with Client() as client: + for result in client.audio.translation.recognize_file( + "input.pcm", + model="gummy-realtime-v1", + format="pcm", + sample_rate=16000, + source_language="zh", + translation_enabled=True, + translation_target_languages=["en"], + ): + transcription = result.get_transcription() + if transcription and result.is_sentence_end(): + print("识别:", transcription["text"]) + for translation in result.get_translations(): + if translation.get("sentence_end"): + print("翻译:", translation["text"]) +``` + +### gummy-chat 单句模型 + +`gummy-chat-v1` 与实时模型共用同一协议与入口,直接 `recognize(model="gummy-chat-v1", ...)` 即可。1.x `TranslationRecognizerChat` 与实时版的差别仅在客户端行为(收到首个句结束后停止发送音频);v2 不单独建模,需要该行为时在 `results()` 里判断 `is_sentence_end()` 后自行停止发送。 + +## 接口说明 + +`Translation` 的推荐调用顺序与 `Recognition` 相同:创建对象 → `start()` → 并发发送音频和读取结果 → `stop()` → 读到 `task-finished` → `close()`。`with client.audio.translation.recognize(...) as session:` 自动执行 `start()` / `close()`。 + +| 接口 | 说明 | +| --- | --- | +| `translation.recognize(...)` | 创建 `TranslationSession`。除下表的翻译参数外,与 `asr.recognize` 相同(`format`/`sample_rate`/`parameters`/`input`/`phrase_id`/`ws_url`/`connector`/`timeout`/`headers`/`api_key`/`workspace`)。 | +| `translation.recognize_file(path, ...)` | 本地文件一次性翻译,产出 `Iterator[TranslationResult]`;路径校验在调用时即生效。 | +| `session.send_audio_frame(data)` | 二进制音频帧,通常 `bytes`。 | +| `session.stop()` | 结束输入,不等待最终结果、不关闭连接。 | +| `session.events()` | 原始事件字典流(含 `task-finished`)。 | +| `session.results()` | 类型化双语结果流,过滤心跳与 `task-finished`。 | +| `session.close()` | 关闭底层连接,可重复调用。 | + +翻译参数(缺省时不发送,由服务端默认值接管:识别开、翻译关、源语言自动): + +| 参数 | 说明 | +| --- | --- | +| `source_language` | 源语言代码(如 `"zh"`、`"en"`),缺省为自动检测。 | +| `transcription_enabled` | 是否输出源语言识别结果。 | +| `translation_enabled` | 是否启用翻译;启用时需给 `translation_target_languages`。 | +| `translation_target_languages` | 目标语言列表(当前仅支持一个目标语言),如 `["en"]`。 | +| `max_end_silence` | VAD 断句静音阈值(毫秒,200–6000)。 | + +`TranslationResult` 是单个结果事件的类型化视图: + +| 接口 | 说明 | +| --- | --- | +| `get_transcription()` | 源语言句子字典(`text`/`begin_time`/`end_time`/`words`/...),该帧无识别结果时为 `None`。 | +| `get_translations()` | 各目标语言句子字典列表(`lang`/`text`/`sentence_end`/...),无翻译结果时为空列表。 | +| `is_sentence_end()` | 识别或任一翻译到达句尾时为 `True`。 | +| `get_usage()` | 该帧携带的 `usage`,无则 `None`。 | +| `request_id` / `usage` / `output` | 原始字段属性;`output` 保留完整服务端输出。 | + +## 连接复用 + +`recognize(..., connection=conn)` 把翻译任务跑在共享的 +`WsConnection`(`client.ws_connection()` 创建)上:顺序任务免再次握手, +纯 JSON 事件并发无限制;二进制上行每条连接单 owner(一路活跃翻译/识别)。 +详见 [连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 + +## 相关文档 + +- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) +- [ASR Python SDK](dashscope-2.0-asr-sdk.md) +- [Realtime Python SDK](dashscope-2.0-realtime-sdk.md)(qwen3-livetranslate 协议族) diff --git a/md/dashscope-2.0-tts-sdk.md b/md/dashscope-2.0-tts-sdk.md new file mode 100644 index 00000000..19b670af --- /dev/null +++ b/md/dashscope-2.0-tts-sdk.md @@ -0,0 +1,373 @@ +# DashScope 2.0 Python SDK:语音合成(TTS) + +通过 `client.audio.tts` 将文本转换为语音,支持一次性合成、流式音频输出及双向流式交互。本文对应当前仓库实现,仅支持同步 `Client`;`AsyncClient.audio.tts` 尚未实现。 + +当前入口使用 CosyVoice 的 `SpeechSynthesizer` WebSocket 协议;不能假设仅更换模型名就能调用采用不同协议的所有 TTS 模型。 + +## 前期准备 + +1. 准备已开通目标模型权限的 API Key,确认模型与音色在所选工作空间及地域可用。 +2. 在本仓库根目录安装源码版本,不以未经验证的已发布版本号作为前提: + + ```bash + python -m pip install -e . + ``` + +3. 在运行环境中安全配置以下环境变量,勿将密钥写入代码或提交到版本库。以下示例均通过 `Client()` 读取配置。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `DASHSCOPE_API_KEY` | `str` | API Key,用于服务鉴权。 | +| `DASHSCOPE_WORKSPACE` | `str` | 工作空间 ID,建议配置,用于工作空间网关路由及请求头。 | + +可通过 `Client(region=...)` 指定与模型服务一致的地域。本文示例调用真实服务并可能产生费用,请在完成配置后按需运行。 + +## 快速开始 + +### 1. 一次性合成 + +`synthesize()` 接收完整文本,等待音频全部返回后将各块拼接为 `bytes`。适合较短文本;结果整体保存在内存中。 + +```python +from dashscope import Client + +with Client() as client: + audio = client.audio.tts.synthesize( + "你好,欢迎使用语音合成服务。", + model="cosyvoice-v1", + voice="longxiaochun", + format="mp3", + sample_rate=22050, + ) + with open("output.mp3", "wb") as output: + output.write(audio) +``` + +### 2. 流式接收音频 + +`stream()` 返回惰性的音频迭代器,首次迭代时才建立连接。内部一次发送全部文本,然后调用 `stop()`;流式的是输出,不是文本输入。 + +```python +from dashscope import Client + +with Client() as client: + chunks = client.audio.tts.stream( + "你好,这段音频将按块写入文件。", + format="mp3", + ) + try: + with open("stream-output.mp3", "wb") as output: + for chunk in chunks: + output.write(chunk) + finally: + chunks.close() +``` + +业务需要提前 `break` 或处理音频时发生异常,也应保留 `finally` 中的 `chunks.close()`,释放内部 WebSocket 会话。提前退出的文件仅包含已消费的音频,不代表完整合成结果。音频块不保证可独立解码,也不保证与句子一一对应。 + +### 3. 双向流式:边发送文本,边接收音频与事件 + +使用 `connect()` 创建会话,在一个线程发送文本、另一个线程读取结果。示例输入为有限元组;接入 LLM 时可替换为逐步产出字符串片段的可迭代对象,无需先收集完整文本。 + +```python +import json +from concurrent.futures import FIRST_EXCEPTION, ThreadPoolExecutor, wait + +from dashscope import Client + + +def send_parts(session, parts): + for text in parts: + session.send_text(text) + session.stop() + + +def receive_results(session): + with open("duplex-output.mp3", "wb") as output: + for frame in session.events(): + if isinstance(frame, bytes): + output.write(frame) + continue + print("JSON 事件:", json.dumps(frame, ensure_ascii=False)) + payload = frame.get("payload") or {} + sentence = (payload.get("output") or {}).get("sentence") + if isinstance(sentence, dict): + for word in sentence.get("words") or (): + print( + "词时间戳:", word.get("text"), + word.get("begin_time"), word.get("end_time"), + ) + if payload.get("usage") is not None: + print("本次 usage:", payload["usage"]) + + +parts = ("你好,", "这是双向流式语音合成。", "文本与音频可以并行传输。") +with Client() as client: + session = client.audio.tts.connect(format="mp3", timeout=30) + with ThreadPoolExecutor(max_workers=2) as executor: + try: + session.start() + print("task_id:", session.session.task_id) + print("启动事件:", session.last_response) + sender = executor.submit(send_parts, session, parts) + receiver = executor.submit(receive_results, session) + done, _ = wait((sender, receiver), return_when=FIRST_EXCEPTION) + for future in done: + future.result() + print("最近 usage:", session.usage) + print("首包延迟(毫秒):", session.first_package_delay) + finally: + session.close() +``` + +`wait(..., FIRST_EXCEPTION)` 在任一任务失败或两者全部成功时返回,`Future.result()` 将线程内异常传播到调用方。`finally` 位于线程池上下文内部,先关闭会话以解除 WebSocket 阻塞,再由线程池等待线程退出。实际 LLM 上游若可能阻塞,还需由业务为上游设置超时或取消机制。 + +发送线程的 `stop()` 只结束输入,接收线程仍持续读取至 `task-finished`。不要在发送结束后立即关闭会话,否则可能丢失尾部音频、时间戳及用量。 + +## 请求参数 + +`connect()`、`synthesize()`、`stream()` 共享以下连接与合成参数;除 `text` 外,均以关键字传入。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `text` | `str` | `synthesize(text, ...)`、`stream(text, ...)` 必填的完整文本。`connect()` 不接收此参数,改用会话的 `send_text(text)` 分段输入。 | +| `model` | `str` | 默认 `"cosyvoice-v1"`。模型名称。 | +| `voice` | `str` | 默认 `"longxiaochun"`。音色须与模型匹配。 | +| `format` | `str` | 默认 `"mp3"`。输出音频编码格式;支持范围由模型服务决定。 | +| `sample_rate` | `int` | 默认 `22050`,单位 Hz。可用采样率及其与格式的组合由服务端校验。 | +| `parameters` | `Optional[dict]` | 默认 `None`。额外模型参数,原样合并进任务参数;同名键覆盖显式参数,详见下文。 | +| `ws_url` | `Optional[str]` | 默认 `None`。自定义 WebSocket 地址;未指定时优先使用全局 WebSocket 地址配置,否则按工作空间及地域生成默认地址。不是 HTTP `base_url`。 | +| `connector` | `Optional[Callable]` | 默认 `None`,使用 `websocket-client`。自定义连接函数签名为 `connector(url, headers)`,返回 WebSocket 兼容对象。 | +| `timeout` | `Optional[float]` | 单位秒,须大于 0。默认 `None` 表示沿用 `client.config.timeout`,并非无限等待。默认连接器将其用于建连,并在连接后设置 socket 超时;不是整个合成任务的总时限。 | +| `headers` | `Optional[Mapping[str, str]]` | 默认 `None`。附加 WebSocket 握手请求头。SDK 根据客户端配置填入鉴权和工作空间请求头;配置存在时会覆盖同名 `Authorization`、`X-DashScope-WorkSpace`。 | + +合成参数先由 `voice`、`format`、`sample_rate` 和固定默认值 `text_type="PlainText"` 组成,再合并 `parameters`。 + +> **覆盖提示**:`parameters` 中的 `voice`、`format`、`sample_rate`、`text_type` 会覆盖前面的值。例如 `format="mp3"` 与 `parameters={"format": "pcm"}` 同时传入,实际请求为 PCM。请同步调整文件后缀和播放器配置。模型特有参数、取值范围及组合约束由服务端校验,SDK 不保证所有模型支持同一组参数。 + +自定义 `connector` 的返回对象需支持 `send()`、`recv()`、`close()`、`settimeout()`;SDK 不向连接函数传递第三个超时参数,因此自定义建连过程的超时由连接函数负责,SDK 在返回后设置 socket 超时。其 `close()` 应能解除阻塞的接收操作。 + +## 关键接口 + +### 入口方法 + +| 接口 | 返回值 | 说明 | +| --- | --- | --- | +| `client.audio.tts.connect(...)` | `SynthesisSession` | 仅创建惰性会话对象,不立即联网。进入上下文或调用 `start()` 才打开连接。 | +| `client.audio.tts.synthesize(text, ...)` | `bytes` | 完整发送文本、结束输入、读取全部音频并拼接;内部自动管理连接。 | +| `client.audio.tts.stream(text, ...)` | `Iterator[bytes]` | 惰性建立会话,一次发送文本并结束输入,逐块输出音频;正常耗尽时自动关闭连接,提前退出需显式关闭迭代器。 | + +### 会话方法 + +除 `send_text(text)` 外,下列会话方法均无参数。 + +| 接口 | 返回值 | 说明 | +| --- | --- | --- | +| `start()` | `SynthesisSession` | 打开 WebSocket,发送 `run-task` 并等待 `task-started`,返回自身。启动事件记录于 `last_response`,不再由结果迭代器输出。 | +| `send_text(text)` | `None` | 发送一个 `str` 文本片段;非字符串抛出 `TypeError`。只能在启动成功、输入尚未结束时调用。 | +| `flush()` | `None` | 请求合成已缓冲文本,发送 `continue-task`,输入为 `{"flush": true}`。不结束文本输入,之后仍可 `send_text()`。 | +| `stop()` | `None` | 发送 `finish-task` 结束输入,不等待合成完成;之后须继续消费结果。结束输入使用此方法,没有 `finish()` 别名。 | +| `cancel()` | `None` | 发送带 `{"directive": "cancel"}` 的 `finish-task`。终态到来前,即使已 `stop()` 也可取消;取消不是暂停,不能恢复输入。 | +| `events()` | `Iterator[Union[dict, bytes]]` | 按到达顺序输出原始 JSON 事件与音频字节,包括 `task-finished` 后结束;遇 `task-failed` 抛异常而非输出失败事件。 | +| `audio_chunks()` | `Iterator[bytes]` | 仅输出音频,但内部仍读取 JSON 并更新 `last_response`、`usage` 等属性,直至任务终态。 | +| `close()` | `None` | 幂等关闭连接,不等待结果,也不替代正常的 `stop()` 加结果消费。关闭后的对象不可重启。 | + +生命周期与并发约束: + +- 启动方式二选一:`with client.audio.tts.connect(...) as session:` 自动启动并关闭;或显式 `start()` 配合 `try/finally: close()`。已 `start()` 的对象不要再次进入 `with session:`,也不要重复 `start()`。 +- `stop()` 或 `cancel()` 后均不可再调用 `send_text()`、`flush()`。连接未关闭时,重复 `stop()` 或 `cancel()` 不会重复发送相同结束动作;终态后也不再发送。关闭后调用这些操作会报错。 +- 同一会话允许一个发送者和一个接收者并行。接收迭代器由锁保护:`events()` 与 `audio_chunks()` 不能同时消费,也不能同时运行两个同类接收迭代器,否则抛出 `RuntimeError`。 +- 若手动持有会话的接收迭代器,提前停止时调用该迭代器的 `close()` 释放读取锁;这不等于关闭会话。若不再继续读取,还需关闭会话。需要音频和元数据时,优先统一使用一个 `events()`。 +- 每次新任务建立独立 WebSocket,无连接池或任务间复用。任务结束或会话关闭后,使用新的 `connect()` 对象发起下一任务。仅关闭 `Client` 不能替代显式会话的资源清理。 + +## 返回事件与属性 + +### 原始事件 + +JSON 事件保留服务端结构,不转换为专用响应模型;音频帧直接返回 `bytes`。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `header.event` | `str` | 事件名称,例如 `task-started`、`result-generated`、`task-finished`、`task-failed`。 | +| `header.task_id` | `str` | 服务端事件中的任务标识。 | +| `header.request_id` | `str`(若返回) | 服务端请求标识,以原始事件为准。 | +| `payload.output` | `dict`(若返回) | 合成结果元数据,具体字段取决于模型及事件。 | +| `payload.output.sentence.words` | `list[dict]`(若返回) | 词级信息;`begin_time`、`end_time` 为毫秒时间戳。并非所有模型或事件均返回。 | +| `payload.usage` | `dict`(若返回) | 服务端原始用量信息。CosyVoice 通常使用 `characters`;仅在服务端实际返回时才有 `tokens` 等字段。 | +| 音频帧 | `bytes` | 按请求格式编码的音频分片,不是 JSON,也不是 Base64 字符串。 | + +以下仅为字段位置示意,不是真实响应,不承诺所有模型均返回这些字段或相同结构: + +```json +{ + "header": {"event": "result-generated", "task_id": "示意任务标识"}, + "payload": { + "output": { + "sentence": { + "words": [{"text": "你好", "begin_time": 0, "end_time": 420}] + } + }, + "usage": {"characters": 2} + } +} +``` + +如需逐句或逐词时间戳,请在消费 `events()` 时处理每个 JSON 事件,不要等任务完成后只查询 `last_response`。 + +### 会话属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `last_response` | `Optional[dict]` | 最近一次 JSON 事件,初始为 `None`;启动后是 `task-started`,正常完整消费后通常为 `task-finished`,会覆盖之前的结果事件。字节帧不覆盖它。失败事件也会被保留。 | +| `usage` | `Optional[dict]` | 最近一次非 `null` 的 `payload.usage` 原始值,初始为 `None`。后续事件缺失用量或返回 `null` 时保留上次值;不是 SDK 累加值,不保证包含 token 计数。 | +| `first_package_delay` | `Optional[float]` | 单位毫秒,从首次 `send_text()` 调用时的计时点到接收迭代器首次消费音频字节。尚未消费音频时为 `None`;包括调用方延迟读取造成的等待,不是纯服务端耗时。 | +| `session` | `WsSession` | 底层会话,仅在已启动且未关闭时可访问,否则抛出 `RuntimeError`。通常无需直接调用底层收发方法。 | +| `session.session.task_id` | `str` | 在示例的 `session` 变量上获取底层任务 ID。建议在会话关闭前保存用于排查。 | + +`SynthesisSession` 没有直接的 `request_id` 便捷属性。请求标识可查看 JSON 原始 `header`;异常也提供 `request_id`。属性只随实际接收而更新,不会由后台线程自动刷新;若只调用 `stop()` 而不消费结果,元数据不会自动完整。 + +## 异常处理 + +服务端业务失败与底层连接异常需分别处理;不要假设所有 WebSocket 错误都会被统一转换成 HTTP 风格异常。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `InvalidApiKey` | `AuthenticationError` | 服务端 `task-failed` 的鉴权错误映射,检查 API Key。 | +| `InvalidParameter` | `BadRequestError` | 检查模型、音色、格式、采样率和模型专属参数。 | +| `Throttling` | `RateLimitError` | 检查并发、速率及配额。由业务决定是否新建任务重试。 | +| 其他业务错误码 | `DashScopeError` 或对应子类 | 按 SDK 业务码映射;未知码保留在基础异常中。 | +| 提前 EOF | `APIConnectionError` | 底层接收返回空字符串或 `None`,且尚未收到 `task-finished`,视为连接异常而非成功结束。 | +| socket 超时或断连 | 原生异常 | 如 `websocket.WebSocketTimeoutException`、`websocket.WebSocketConnectionClosedException`、`OSError`,可能原样传播;并不保证变为 `APITimeoutError`。 | +| 非法操作或输入 | `RuntimeError` / `TypeError` / `ValueError` | 包括重复启动、并发读取、结束输入后继续发送、非字符串文本及非正数超时。 | + +收到 `task-failed` 时,SDK 根据 `header.error_code`、`header.error_message` 抛出异常,异常对象的 `body` 保留完整失败事件,`code`、`message`、`request_id` 可用于定位问题。会话的 `last_response` 同样保留失败事件。异常的 `status_code` 不能作为 WebSocket 任务成功的判断依据。 + +启动、收发失败时 SDK 会清理连接;调用方仍应使用上下文管理或 `finally` 释放资源。协议头异常可能抛出 `WsEventError`,非法 JSON 可能抛出原生 JSON 解析异常。 + +WebSocket 路径不提供自动重连、重放或断点续传;客户端 HTTP 重试配置不会自动重放 TTS 任务。确需重试时,应新建会话并由业务确定文本边界,避免重复播放、重复合成或重复计费。 + +## 常见问题 + +**如何衔接 LLM 的流式输出?** + +使用 `connect()`,将 LLM 字符串片段逐次传给 `send_text()`,同时在另一个线程持续消费一个结果迭代器。`stream(text)` 仍要求完整字符串,不接受文本迭代器作为双向输入。 + +**`flush()`、`stop()`、`cancel()` 和 `close()` 有何区别?** + +`flush()` 请求处理缓冲文本但保留输入;`stop()` 正常结束输入并保留输出;`cancel()` 请求取消且不可恢复,`stop()` 后终态前仍可调用;`close()` 直接释放连接。`stop()`、`cancel()` 均不等待任务结果。需要确认终态时继续读取;无需保留输出时可显式关闭。 + +**为什么拿不到时间戳、用量或首包延迟?** + +先确认已消费结果。`audio_chunks()` 不输出 JSON,但会更新会话属性;逐词信息应通过 `events()` 当场读取。模型可能不返回词时间戳或用量。`usage` 不等于固定的 token 统计,首包延迟也仅在首次消费音频后产生。 + +**为什么文件内容与扩展名不匹配?** + +检查 `parameters` 是否覆盖 `format`、`sample_rate`,并按实际编码保存或播放。SDK 只返回音频字节,不负责转码,也不会因文件名后缀改变音频格式。 + +**能否复用会话或使用异步客户端?** + +不能用同一会话启动下一任务,也不能重启已关闭对象。每个任务需新建会话和连接;当前没有 `AsyncClient.audio.tts`,异步应用需自行安排同步调用的线程及生命周期。 + +## HTTP 合成(client.audio.tts.http) + +ws 入口之外,`client.audio.tts.http` 提供纯 HTTP POST 的合成方式(不经过 WebSocket):一次性返回整段音频的临时 URL,或以 SSE 流式逐段返回音频帧。这是 1.x `HttpSpeechSynthesizer`(sambert/cosyvoice)与 qwen-tts `SpeechSynthesizer` 两个模型族的 v2 形态,共用同一入口,SDK 按模型名前缀路由: + +- `qwen*`(如 `qwen-tts`、`qwen3-tts-flash`)→ `services/aigc/multimodal-generation/generation` +- 其他(sambert/cosyvoice 等)→ `services/audio/tts/SpeechSynthesizer` + +两个端点的请求体同为 `{model, input}`、响应同为 `output.audio`,因此调用代码完全一致,只需替换 `model` 与 `voice`。 + +### 一次性合成(sambert 官方示例平移) + +对应 1.x `HttpSpeechSynthesizer.call(stream=False)`;音频以临时 URL 返回,需自行下载。 + +```python +import requests +from dashscope.v2 import Client + +client = Client() +result = client.audio.tts.http.synthesize( + "今天天气怎么样?", + model="sambert-zhichu-v1", + voice="zhichu", + format="wav", + sample_rate=16000, +) +print(result.audio_url, result.expires_at) +with open("output.wav", "wb") as f: + f.write(requests.get(result.audio_url).content) +``` + +### 流式合成(qwen-tts 官方示例平移) + +对应 1.x qwen `SpeechSynthesizer.call(stream=True)`;SSE 由 SDK 自动开启(`X-DashScope-SSE: enable` 请求头),音频帧已按 base64 解码为字节。 + +```python +from dashscope.v2 import Client + +client = Client() +with open("output.mp3", "wb") as f: + for chunk in client.audio.tts.http.stream( + "那我来给大家推荐一款T恤,这款T恤是纯棉的,穿着很舒服。", + model="qwen3-tts-flash", + voice="Cherry", + language_type="Chinese", + ): + if chunk.audio: + f.write(chunk.audio) + if chunk.is_final: + print(chunk.audio_url, chunk.usage) +``` + +cosyvoice 的流式分片额外携带 `chunk.sentence`(`sentence-*` 类型的句级元数据);qwen 族分片只有 `audio` 与末尾的整段 URL 引用。末片 `is_final`(`finish_reason == "stop"`)不保证携带音频字节,整段音频以 `audio_url` 引用为准。 + +### HTTP 请求参数 + +`http.synthesize()` 与 `http.stream()` 参数相同;除 `text` 外均以关键字传入。 + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `text` | `str` | 必填。待合成文本。 | +| `model` | `str` | 必填。模型名称,同时决定端点路由(见上)。 | +| `voice` | `str` | 必填。音色须与模型匹配。 | +| `format` | `Optional[str]` | 默认 `None`(服务端默认)。输出音频编码格式。 | +| `sample_rate` | `Optional[int]` | 默认 `None`(服务端默认)。采样率,单位 Hz。 | +| `language_type` | `Optional[str]` | 默认 `None`。语种类型,如 `"Auto"`、`"Chinese"`、`"English"`;主要面向 qwen-tts 族,写入 `input.language_type`。 | +| `parameters` | `Optional[dict]` | 默认 `None`。请求体顶层 `parameters`,原样透传。 | +| `extra_input` | `Optional[dict]` | 默认 `None`。额外 `input` 字段(如 cosyvoice 的 `volume`/`rate`/`pitch`),原样合并;显式参数同名时覆盖它。 | +| `timeout` | `Optional[float]` | 默认 `None`,沿用客户端超时配置。 | +| `headers` | `Optional[Mapping[str, str]]` | 默认 `None`。附加请求头;`stream()` 的 SSE 头已由 SDK 设置,同名传入可覆盖。 | + +### HTTP 返回类型 + +`http.synthesize()` 返回 `HttpSynthesis`;`http.stream()` 逐片返回 `HttpSynthesisChunk`。两者都保留原始 `output` 字典、`usage` 与 `request_id`。 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `audio_url` | `Optional[str]` | 整段音频的临时 URL;`HttpSynthesis` 直接携带,`HttpSynthesisChunk` 仅末片携带。 | +| `audio_id` | `Optional[str]` | 音频 ID。 | +| `expires_at` | `Optional[int]` | `audio_url` 的过期时间戳。 | +| `audio` | `Optional[bytes]` | 仅 `HttpSynthesisChunk`:本片音频帧(已 base64 解码),无音频的分片为 `None`。 | +| `sentence` | `Optional[dict]` | 仅 `HttpSynthesisChunk`:cosyvoice `sentence-*` 分片的句级元数据。 | +| `finish_reason` / `is_final` | `Optional[str]` / `bool` | 仅 `HttpSynthesisChunk`:末片 `finish_reason == "stop"`,`is_final` 为其便捷判断。 | + +HTTP 路径的错误处理与其他 HTTP 资源一致:错误状态码按错误目录映射(如 `InvalidParameter` → `BadRequestError`);SSE 流中的 `event:error` 帧在迭代中途抛出同样映射的异常。ws 路径的连接类注意事项(重连、异步)不适用于此入口。 + +## 连接复用 + +`connect(..., connection=conn)` 把合成任务跑在共享的 +`WsConnection`(`client.ws_connection()` 创建)上,逐句高频合成免去反复 +握手。合成输出是二进制音频(线上不带 task_id),所以每条连接同一时刻 +只能有一路活跃合成——`start()` 会自动占用下行,第二路并发合成会直接 +报错(要并发请再开一条连接);顺序合成则天然复用。详见 +[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 + +## 相关文档 + +- [CosyVoice 客户端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/cosyvoice-client-events) +- [CosyVoice 服务端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/cosyvoice-server-events) +- [Sambert Python SDK](https://help.aliyun.com/zh/model-studio/sambert-python-sdk) +- [Qwen-TTS API](https://www.alibabacloud.com/help/zh/model-studio/qwen-tts-api) +- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) +- [DashScope 2.0 ASR SDK](dashscope-2.0-asr-sdk.md) +- [DashScope 2.0 Realtime SDK](dashscope-2.0-realtime-sdk.md) diff --git a/md/dashscope-2.0-vs-1.x-alignment-review.md b/md/dashscope-2.0-vs-1.x-alignment-review.md new file mode 100644 index 00000000..c5e258b1 --- /dev/null +++ b/md/dashscope-2.0-vs-1.x-alignment-review.md @@ -0,0 +1,452 @@ +# DashScope SDK 2.0 对 1.x 接口/功能对齐 — 深度 Review + +> 评审范围:`dashscope/v2/`(2.0)对照 `dashscope/`(1.x: aigc / audio / embeddings / rerank / tokenizers / app / assistants / threads / finetune / files / models / utils)。 +> 方法:按 6 个功能域逐文件比对公开方法、参数、流式语义、响应字段。 +> 结论先行:**v2 是一次有意的重设计,不是 1:1 移植**。管理面与批量任务基本对齐甚至更优;推理面(chat / multimodal / image / video / audio)的多处功能回退已按 P0/P1/P2 修复(commit `c9604e2` / `d8ac331` / `b69b585`),TingWu 能力域已补 `client.tingwu`(`82e15d5`)。CodeGeneration、nlp Understanding、MultiModalDialog、Conversation 已决策 **hold,v2 先不支持**;音频侧 HTTP-TTS / omni 类型化助手 / 实时翻译 / 词表声纹管理经 2026-09 产品页核查确认存活,单独立章 **§7 语音模块专项(需立项,专门做)**。所有不同步到 v2 的能力在 **§6 整体标记**。按"能否无损迁移"衡量,**决策范围内的主干场景已可平移**。 +> +> **范围说明**:1.x 的 Assistants / Threads / Runs 子系统正在下线,**2.0 有意不包含**,不计为缺口(详见 §3 P2-0)。`dashscope` CLI 在 2.0 是**后续再加**的计划项,同样不计缺口(详见 §3 P2-7)。 + +--- + +## 0. 对齐计分卡 + +| 领域 | 对齐度 | 主要缺口 | +|---|---|---| +| files / models / fine_tunes / deployments | 🟢 对齐+ | ~~deployments 缺 `stream_events`;finetune/deployment 响应少 `creator/modifier/group`~~ ✅ `b69b585` | +| apps(百炼 Application) | 🟢 对齐+ | ~~`workflow_message`、`usage.models[]` 未建模~~ ✅ `b69b585` | +| 批量转写 `asr.transcribe` | 🟢 对齐 | — | +| chat / multimodal.chat | 🟢 对齐 | ~~本地文件上传、per-call 鉴权、命名参数、累积流、`search_info`~~ ✅ `c9604e2` / `d8ac331` | +| embeddings / rerank | 🟢 对齐 | ~~batch 缺 `cancel/list`;本地 tokenizer 缺失;multimodal 本地文件上传缺失~~ ✅ `c9604e2` / `d8ac331` / `b69b585` | +| tokenization | 🟢 对齐 | ~~`token_ids/tokens` 未类型化;无本地 tiktoken~~ ✅ `d8ac331` / `b69b585`(v2 再导出 1.x `get_tokenizer`) | +| images / videos | 🟢 对齐 | ~~缺 `cancel/list`、本地文件上传、`function` 字段~~ ✅ `c9604e2` / `d8ac331` / `b69b585`;视频 `extend_prompt` 默认值差维持 v2 语义(已标注:迁移指南 §3.3) | +| 实时音频 asr.recognize / tts / realtime | 🟡 部分 | 增量 TTS / `recognize_file` / `RecognitionResult` / callback 适配器 ✅ `b69b585`;omni 类型化助手 ✅(M2)、实时翻译 ✅(M3 `client.audio.translation`)、HTTP-TTS ✅(M4 `client.audio.tts.http`)、管理面 ✅(M5 `client.audio.vocabularies/phrases/voices/voice_design`) | +| reinforcement_learning | 🟢 对齐 | ~~base_url 不一致;缺 function-instance 日志/删除;`jobs.delete` 缺失~~ ✅ `b69b585`(按 v2 默认 endpoint,见 P2-2) | +| 认证(save_api_key 密钥文件) | 🟢 对齐 | ~~v2 不读 `~/.dashscope/api_key`~~ ✅ `b69b585`(key 文件为最低优先级) | +| **TingWu(听悟)** | 🟢 **已补** | ✅ `82e15d5`:`client.tingwu`(离线 call + realtime 会话,wire 形状与 1.x 一致) | +| **nlp Understanding / MultiModalDialog / Conversation** | ⚪ **hold** | 已决策:v2 先不支持(见 §1 P0-4),存量用户继续用 1.x | +| **CodeGeneration** | ⚪ **hold** | 已决策:v2 先不支持(见 §1 P0-3) | +| **Assistants / Threads / Runs** | ⚪ 不在范围 | 1.x 下线中,2.0 有意不包含(非缺口) | +| **`dashscope` CLI** | ⚪ 后续再加 | 2.0 计划项,非缺口(见 §3 P2-7) | + +--- + +## 1. 系统性回退(P0,迁移阻塞级) + +### P0-1 本地文件上传 / OSS 预处理全面移除 + +> **状态**:✅ 已修复(commit `c9604e2`)。v2 复用 1.x `oss_utils.check_and_upload_local`(`v2/resources/_oss.py`),chat(native)/images/videos/embeddings 均先上传本地路径再置 `X-DashScope-OssResourceResolve: enable`。 + +**现象**:1.x 在 multimodal chat、multimodal embeddings、image、video 中会把本地路径自动上传 OSS,并置请求头 `X-DashScope-OssResourceResolve: enable`;v2 只接受已有 `oss://` / HTTP(S) URL。传本地文件的 qwen-vl / 图生图 / 视频用户会直接失败或拿到不可解析的入参。 + +**1.x 参照** +- 工具:`dashscope/utils/oss_utils.py` — `check_and_upload_local:174`、`preprocess_message_element:280`、`upload_file:149`、`OssUtils.upload:34` / `get_upload_certificate:112`。 +- 调用:`dashscope/aigc/multimodal_conversation.py:151` `_preprocess_messages`(:218/:480),命中上传后 `headers["X-DashScope-OssResourceResolve"]="enable"`(:154/:419)。 +- 图像/视频:`dashscope/aigc/image_synthesis.py:287-343`、`video_synthesis.py:293-321`。 + +**v2 现状** +- `dashscope/v2/protocols/native.py:22` `_translate_part` 仅读 `part["url"]`,无上传分支。 +- `dashscope/v2/types/messages.py` 媒体 part 仅 `url`/`frames`。 +- `dashscope/v2/resources/multimodal/embeddings.py:71` `_uses_oss` 只在值已以 `oss://` 开头时置 resolve 头,**不上传**。 +- images/videos 无任何 `check_and_upload_local` 调用。 + +**改法** +1. v2 直接复用 1.x 的 `dashscope/utils/oss_utils.py`(纯工具,不引入循环依赖)。 +2. 在 `native.py:_translate_part` 增加本地路径分支:当 `part["url"]` 是 `file://` / 本地存在路径时,调用 `check_and_upload_local(model, url, api_key)` 换成 `oss://`,并标记"本次有上传"。 + - 因 `_translate_part` 是纯函数且无 api_key,需把上传提到 `build_chat_request`(native.py:59)层:在 `_translate_messages` 前对 messages 跑一遍 `preprocess_message_element`,并把 `has_upload` 透传到 `ChatRequest.headers`,命中时加 `X-DashScope-OssResourceResolve: enable`。 + - api_key 来源:`client.config.api_key`(protocol 目前拿不到 client,需在 `completions._build_request` 处把 config/api_key 传入,或让 protocol 持有一个 uploader 回调)。最小改动:在 `resources/chat/completions.py:_build_request`(completions.py:64)里先对 `messages` 做上传预处理,再交给 protocol。 +3. multimodal embeddings:把 `multimodal/embeddings.py:_contents`(:47)里每个 item 的 `image/audio/video` 值过一遍 `check_and_upload_local`,`_uses_oss` 改为"上传过或本就是 oss://"即置 resolve 头。 +4. images/videos:在 `images.py` 构造 body 前,对 `image_urls` / `extra_input` / `media` 里的本地路径调用 `check_and_upload_local`(`videos.py` 同理对 `image_url`/`media`)。 +5. 上传需要 api_key,统一从 `self._client.config.api_key` 取。 + +**验收**:本地 png/wav/mp4 路径 → 抓包确认请求体已是 `oss://` 且带 `X-DashScope-OssResourceResolve: enable`。 + +--- + +### P0-2 per-call `api_key` / `workspace` / `headers` 全部取消 + +> **状态**:✅ 已修复(commit `c9604e2`,采用方案 A)。transport 四个方法新增 `api_key=`/`workspace=` 形参并覆盖默认头;全部资源方法(含 ws 侧 audio)透传。 + +**现象**:1.x 几乎每个方法都能逐调用覆盖 `api_key=` / `workspace=`(多租户、子账号),chat 还能注入 `plugins` 头;v2 只有 client 级配置。多租户调用方与 per-call header 注入场景无法平移。 + +**1.x 参照**:`generation.py:49` `call(..., workspace, api_key, ...)`;`generation.py:186-192` 注入 `X-DashScope-Plugin`;各资源 `call` 均带 `api_key/workspace`。 + +**v2 现状** +- `dashscope/v2/client.py:53` 仅 client 级;`with_options`(client.py:152)可派生新 client(重量级)。 +- `dashscope/v2/transport/http.py`:`_build_headers:48` 只在 transport `__init__`(:114-126)用 client 级 api_key/workspace;`request:136` / `open_stream` 已支持 per-call `headers=` 与 `base_url=`,但**没有 `api_key=` / `workspace=` 形参**。 + +**改法**(二选一,推荐 A) +- **A. 传输层加 per-call 鉴权形参**:给 `http.py` 的 `request` / `open_stream`(sync+async 共 4 个)增加 `api_key: Optional[str]=None`、`workspace: Optional[str]=None`,在内部用 `_build_headers(api_key or self._api_key, workspace or self._workspace, ...)` 覆盖默认头,再经 `_with_sdk_headers` 合并(注意大小写不敏感,已实现 http.py:71)。资源方法签名透传 `api_key/workspace` 到 transport。ws 侧在 `transport/ws.py:WsSession.__init__` 同样接受覆盖。 +- **B. 仅在资源层透传 headers**:资源方法接受 `extra_headers: Mapping`,直接作为 `headers=` 传给 transport(caller 头已优先且大小写不敏感)。改动小,但把鉴权拼串责任丢给用户。 + +> 注:`plugins` 头属 B 即可覆盖;若要保留 1.x 的 `plugins=` 具名 kwarg,在 chat create 增加 `plugins` 形参并映射为 `X-DashScope-Plugin` 头。 + +**验收**:同一 client 下两次调用传不同 `api_key`,抓包 `Authorization` 不同;传 `workspace` 时 `X-DashScope-WorkSpace` 覆盖 client 默认。 + +--- + +### P0-3 CodeGeneration 缺失(需决策) + +> **状态**:⚪ **已决策:hold,v2 先不支持**(2026-09 评审拍板)。文档核查亦显示官方文档无独立页面,代码生成场景走通用 chat API。存量用户继续用 1.x;后续若后端确认仍在线且有必要,再单独立项。 + +**1.x 参照**:`dashscope/aigc/code_generation.py:52/72`(scene 化代码生成)。 +**v2 现状**:无资源。 +**改法**:确认该 API 是否仍在线。若在线,作为 `chat.completions` 的一个 scene 入口或独立 `client.code_generation` 资源补;若已下线(同 Assistants/Threads/Runs),在迁移文档标注废弃即可,不计缺口。**先与后端确认存废再动手。** + +> **文档核查(2026-09)**:官方文档主入口([DashScope API 参考](https://help.aliyun.com/zh/model-studio/qwen-api-via-dashscope))及站内检索均无 CodeGeneration 独立页面,代码生成场景走通用 chat/文本生成 API。**倾向"不再作为独立 scene 主推",仍以后端确认为准。** + +> Assistants / Threads / Runs 已确认 1.x 下线中、2.0 有意不包含,见 §3 P2-0,不再作为 P0 阻塞项。 + +--- + +### P0-4 整个能力域在 v2 无对应(需决策) + +**现象**:除 CodeGeneration(P0-3)外,以下 1.x 能力域在 v2 整体无对应资源,迁移这些能力的用户没有落点: + +- **nlp Understanding**:`dashscope/nlp/understanding.py`(百炼知识库问答/理解)。 +- **TingWu / TingWuRealtime**:`dashscope/multimodal/tingwu/`(听悟会议转写,含实时)。 +- **MultiModalDialog**:`dashscope/aigc/multimodal_dialog.py`(多模态对话,带历史管理)。 +- **Conversation / History**:`dashscope/conversation/`(对话状态托管)。 + +**改法**:与后端逐一确认**存废/归属**(随 1.x 下线?并入百炼 apps?独立立项?),再决定 v2 是补资源、委派 1.x 还是在迁移文档标注下线。**先确认再动手**;确认前这些域按 🔴 缺失计(见计分卡)。 + +> **状态**:✅ TingWu 已补(commit `82e15d5`)。新增 `client.tingwu`:`TingWu.call`(离线,POST `services/aigc/multimodal-generation/generation`,返回类型化 `TingWuResult`)与 `TingWu.realtime`(duplex ws 会话,`task_group=aigc`/`task=multimodal-generation`/`function=generation`,start/stop wire 形状与 1.x 一致;stop 的 `appId` 修正为真实值——1.x `tingwu_realtime.py` 因 `_Request.app_id` 从未赋值而恒发 `None`)。`WsSession.finish_task` 增加可选 `input` 载荷。**Understanding/MultiModalDialog/Conversation 已决策:hold,v2 先不支持**(2026-09 评审拍板),存量用户继续用 1.x,后续视后端归属结论再立项。 +> +> **文档核查(2026-09)**: +> - **TingWu 确认存活**:官方文档有专页且 2026-09 仍在更新([服务端 Python SDK](https://help.aliyun.com/zh/model-studio/tingwu-sdk-python) / [realtime](https://help.aliyun.com/zh/model-studio/tingwu-sdk-python-realtime)),示例直接用 1.x `dashscope.multimodal.tingwu.tingwu.TingWu`(经 `multimodal-generation/generation` 端点跑 `createTask/getTask` 离线任务)。→ 从"待确认"升级为**"确认保留,v2 缺口坐实"**,建议立项补 `client.tingwu`(可复用 v2 jobs/transport,wire 形状与 1.x 一致)。 +> - **Understanding / MultiModalDialog / Conversation 文档无独立页面**:站内检索无对应 API 文档;多轮对话改以 messages 数组教学([multi-round-conversation](https://help.aliyun.com/zh/model-studio/multi-round-conversation)),多模态交互由 omni 实时([multimodal-interaction-protocol](https://help.aliyun.com/zh/model-studio/multimodal-interaction-protocol))承接。**倾向"不再主推/已并入其他面"**,仍以后端确认为准。 + +--- + +## 2. 功能回退(P1) + +### P1-1 高频命名参数降级为无类型 `parameters=` 透传 + +> **状态**:✅ 已修复(commit `d8ac331`)。chat 9 个具名参数(top_k/tool_choice/n/logprobs/top_logprobs/thinking_budget/enable_search/search_options/parallel_tool_calls)在三个协议落地:anthropic 对无对应项显式拒绝、`top_k` 映射为 Anthropic 原生字段;images/videos/audio 具名参数同步补齐,具名优先于 `parameters=` 兜底。 + +**现象**:`top_k / tool_choice / n / logprobs / top_logprobs / thinking_budget / enable_search / search_options / parallel_tool_calls`(chat),`seed/style/duration/resolution/ratio/prompt_extend/watermark`(image/video),`volume/rate/pitch/vocabulary_id/language_hints`(audio)等在 v2 不再是具名 kwarg,可发现性与类型校验下降;且 **openai 协议 strict 模式会拒绝未知 key**(`dashscope/v2/protocols/openai.py:137-143`)。 + +**v2 现状**:`native.py:59` `build_chat_request` 形参只到 `tools`;其余靠 `extra_parameters`(native.py:95)。chat create 形参见 `completions.py:383`。 + +**改法** +1. 在 `completions.py` 的 `create`(sync :383 / async :477 / 各 overload)与 `native.py:build_chat_request`(:59)、`openai.py` 适配器同步增加具名形参:`top_k / tool_choice / n / logprobs / top_logprobs / thinking_budget / enable_search / search_options / parallel_tool_calls`,在 native 的 `for key,value in (...)`(native.py:81)元组里登记。 +2. openai 协议侧:把这些映射到 OpenAI 字段名;strict 白名单(openai.py:137)相应放行。 +3. image/video:把 `seed/size/style/negative_prompt/duration/resolution` 等提升为 `images.generate` / `videos.generate` 具名 kwarg(当前在 `images.py` 走 `parameters`),保留 `parameters=` 兜底。 +4. audio:`tts.synthesize`(tts.py:45)增加 `volume/rate/pitch/language_hints` 具名;asr.recognize(asr.py:255)增加 `vocabulary_id/phrases/disfluency_removal_enabled/diarization_enabled` 等,内部按 1.x 形状拼 `resources=[{resource_id,resource_type:"asr_phrase"}]`(参照 `recognition.py:341`、`transcription.py:245`)。 + +**验收**:具名传参等价于旧的 `parameters={...}`;openai 协议不再因这些 key 报错。 + +--- + +### P1-2 chat 流式:无累积模式 + text_stream 丢 reasoning/tool_call + +> **状态**:✅ 已修复(commit `d8ac331`)。create 新增 `incremental_output`;native 侧沿用 1.x `ParamUtil.should_modify_incremental_output` 语义——常规模型 wire 保持增量、SDK 端合并为累积快照,tts/omni/qwen-deep-research 透传服务端;openai/anthropic 恒客户端合并。`ChatStream`/`AsyncChatStream` 新增 `reasoning_stream` / `tool_call_stream`。注:1.x 的 merge 实现位于 `dashscope/utils/message_utils.py`(含 n>1 分包/usage 按 index 聚合),v2 在 `completions.py` 以类型化 per-choice slot 重新实现,未逐行移植 n>1 的 finish_reason 隐藏逻辑。 + +**现象**:v2 native 恒置 `incremental_output=True`(native.py:79-80),无累积快照模式;1.x 的 incremental→cumulative 客户端 merge(generation.py:204-243)无对应。`ChatStream.text_stream` 只产出 `delta.content`,**丢弃 `reasoning_content` 与 tool_call 增量**(completions.py:114-119)。 + +**改法** +1. `text_stream`(completions.py:114)保持只出正文;新增 `reasoning_stream` 与 `tool_call_stream`(或在 chunk 上保留 delta 即可,文档说明)。`get_final_message`(completions.py:180 聚合)已含 reasoning/tool_calls,补齐 stream 侧便捷访问。 +2. 累积模式:给 chat create 增加 `incremental_output: Optional[bool]=None`,透传到 native(默认仍 True);若用户传 False,提供一个 `accumulate` 包装器在 SDK 侧把增量合并成累积快照(对齐 1.x merge 行为),避免依赖服务端。 + +**验收**:`for t in stream.text_stream` 与 1.x 一致;`incremental_output=False` 时每个 chunk 是到目前为止的全量文本。 + +--- + +### P1-3 image/video/batch-embedding 缺 `cancel` / `list` + +> **状态**:✅ 已修复(commit `d8ac331`)。`Jobs`/`AsyncJobs` 新增 `cancel`(POST /tasks/{id}/cancel)与 `list`(GET /tasks,`JobList` 分页模型),images/videos/embeddings 的 jobs 门面全部透传。 + +**现象**:1.x 任务类资源都有 cancel/list;v2 `_jobs.py` 只有 create/fetch/wait。 + +**1.x 参照**:`image_synthesis.py:544 cancel` / `:565 list`;`video_synthesis.py:564/585`;`batch_text_embedding.py:167 cancel` / `:188 list`。 + +**v2 现状**:`dashscope/v2/resources/_jobs.py:163 Jobs`(:182 create / :205 fetch / :221 wait),`AsyncJobs:269`。无 cancel/list。 + +**改法** +1. 在 `Jobs` / `AsyncJobs` 增加: + - `cancel(task_id, *, timeout=None)`:1.x 走 `PUT/POST /tasks/{task_id}/cancel`(以 `image_synthesis.cancel` 实际 method/path 为准),复用 `self._client.transport.request(..., sdk_module=self._module)`。 + - `list(*, page_no, page_size, status=None, ...)`:`GET /services/{service}/... ` 或 1.x list 对应 path,返回任务列表类型。 +2. images/videos/embeddings 的包装类(`ImageJobs`/`VideoJobs`/`EmbeddingJobs` 及 async)透传 `cancel/list`。 +3. 新增响应模型 `JobList`(pydantic),对齐 1.x 分页字段。 + +**验收**:`client.images.jobs.cancel(task_id)` / `.list()` 可用;与 1.x 返回字段一致。 + +--- + +### P1-4 chat 响应丢 `output.search_info` / `thoughts` / 原始 output 透传 + +> **状态**:✅ 已修复(commit `d8ac331`)。`ChatCompletion.search_info` 类型化(`SearchInfo.search_results[]`,extra="allow" 兜底未建模字段)。`thoughts` 确认 chat 侧无返回,不建模。 + +**现象**:`parse_completion`(`dashscope/v2/types/completions.py:122`)只保留 `choices/usage/envelope`,`output.search_info`(web 搜索引用)、`output.thoughts`、顶层 `finish_reason`/`text` 被丢弃 → `enable_search` 结果在 v2 不可观测。 + +**改法** +1. `types/completions.py` 的 `ChatCompletion` 增加 `search_info: Optional[SearchInfo]=None`(建模 `search_results[].{title,url,site_name,index}`),`parse_completion` 从 `output.search_info` 提取。 +2. 保留一个 `raw_output: Optional[Dict]=None`(或 `model_extra`)承接未建模字段,避免信息硬丢。 +3. `thoughts` 若 chat 侧确有返回,按 apps 的 `Thought`(apps.py:49)同构建模。 + +**验收**:`enable_search=True` 时 `resp.search_info.search_results` 非空。 + +--- + +### P1-5 本地 tokenizer(get_tokenizer / tiktoken)缺失 + tokenization 丢 token_ids + +> **状态**:✅ 已修复。`TokenCount.token_ids`/`tokens` 于 commit `d8ac331` 补齐;本地 tokenizer 入口于 commit `b69b585` 落地——`dashscope.v2` 直接再导出 1.x `get_tokenizer`/`list_tokenizers`(委派 `dashscope/tokenizers`,不重写 tiktoken),与包根既有导出指向同一对象。 + +**现象**:1.x `get_tokenizer(model)` 返回离线 tiktoken 对象(`encode/decode`);v2 无本地 tokenizer,`tokenization.count` 是服务端往返且只返回计数。 + +**1.x 参照**:`dashscope/tokenizers/tokenizer.py:17 get_tokenizer` / `:43 list_tokenizers`;`qwen_tokenizer.py:103 encode` / `:156 decode`;`tokenization.py:44 Tokenization.call`(返回 `output.token_ids/tokens`)。 + +**v2 现状**:`dashscope/v2/resources/tokenization.py:32 _parse` 只映射 `usage`+envelope,丢 `token_ids/tokens`;`count:70`。 + +**改法** +1. `tokenization.py:_parse`(:32)补 `token_ids`/`tokens` 字段;`TokenCount`(:24)增加 `token_ids: Optional[List[int]]`、`tokens: Optional[List[str]]`。 +2. 本地 tokenizer:决策是否在 v2 暴露 `client.tokenizers.get(model)`。若要,直接复用 1.x `dashscope/tokenizers`(同 P0-3 委派思路),不重写 tiktoken 逻辑。 + +**验收**:`count` 返回 token_ids;`get_tokenizer` 入口在 v2 可达(或文档指明用 1.x)。 + +--- + +### P1-6 `save_api_key` 落盘的 key 文件 v2 不读取 + +> **状态**:✅ 已修复(commit `b69b585`)。`resolve_config` 的 `_pick` 链在 `config.toml` 之后补 key 文件级(`DASHSCOPE_API_KEY_FILE_PATH` → `~/.dashscope/api_key`,文件缺失回退 None 不抛错);`dashscope.v2` 再导出 1.x `save_api_key`,与包根同一对象。 + +**现象**:1.x 支持 `dashscope.save_api_key(...)` 把 key 写入 `~/.dashscope/api_key`(或 `DASHSCOPE_API_KEY_FILE_PATH` 指定路径),之后免配读取(`dashscope/common/api_key.py:45 save_api_key` / `:15 get_default_api_key`)。v2 的 key 解析(`v2/_config.py:542-549 resolve_config`、`Config.api_key` :279)只看 显式参数 → `DASHSCOPE_API_KEY` 环境变量 → `config.toml`,**不读 key 文件** → 走 `save_api_key` 流程的存量用户迁到 v2 直接丢鉴权。 + +**改法** +1. `resolve_config` 的 `_pick` 链在 `config.toml` 之后补两级:`DASHSCOPE_API_KEY_FILE_PATH` 环境变量指定的文件、`~/.dashscope/api_key` 默认文件(读取对齐 1.x `get_default_api_key` 的优先级)。 +2. v2 暴露 `save_api_key(api_key, api_key_file_path=None)`(顶层 `dashscope.v2` 或 `client` 静态方法),实现直接复用/委派 1.x `common/api_key.py:45`,不重写。 + +**验收**:无显式 key、无环境变量、仅有 key 文件时 v2 Client 可初始化;`save_api_key` 入口在 v2 可达。 + +--- + +### P1-7 video `extend_prompt` 默认值与 image/video `function` 字段 + +> **状态**:✅ `function` 已修复(commit `b69b585`)。`images.generate`/`videos.generate` 及各自 jobs(sync+async)新增具名 `function: Optional[str]=None`,写入 `inputs` 且具名优先于 `extra_input`;images 两个 input 构造器(legacy + multimodal)均覆盖。`extend_prompt` 按改法 1 维持 v2 `prompt_extend=None` 语义,默认值差已在迁移指南 §3.3 标注。 + +**现象** +1. 1.x video 的 `extend_prompt: bool = True`(`video_synthesis.py:63` / `:199`,`prompt_extend` 的 deprecated 别名,**默认开**,写入 `inputs["extend_prompt"]` :213);v2 `videos.generate` 的 `prompt_extend` 默认 `None` 且只进 `parameters`。显式行为不同,逐参数对齐时需注意默认值差。 +2. 1.x image/video 有 `function: str = None` 形参(`image_synthesis.py:46` → `inputs["function"]` :312-313;`video_synthesis.py` → `inputs["function"]` :219);v2 `images.generate`/`videos.generate` 无此具名参数(P1-1 只恢复了 parameters 侧字段,`function` 属 inputs 侧)。 + +**改法** +1. video:维持 v2 `prompt_extend=None`(不擅自引入 1.x 的默认 True),但在迁移文档标注默认值差;`extend_prompt` 别名不再恢复(1.x 已 deprecated)。 +2. image/video:`generate`/`jobs.create`(sync+async)增加具名 `function: Optional[str]=None`,写入 `inputs`(非 `parameters`),具名优先于 `extra_input`。 + +**验收**:`images.generate(function="...")` / `videos.generate(function="...")` 与 1.x inputs 形状一致。 + +--- + +## 3. 一致性 / 补全(P2) + +### P2-0 Assistants / Threads / Runs — 有意不在 2.0 范围(非缺口) + +> **状态**:✅ 已在迁移指南 §5 显式声明下线(`md/dashscope-2.0-migration-guide.md`)。 + +1.x 的 Assistant 运行时(`dashscope/assistants/`、`dashscope/threads/`、`threads/runs/`)正在下线,**2.0 有意不实现**。这是产品决策,不计为对齐缺口,也不需要委派/重写。 + +**唯一动作**:在迁移文档显式声明"Assistants/Threads/Runs 不在 2.0 范围,随 1.x 下线",避免用户误以为是遗漏。CodeGeneration(P0-3)若同属下线范畴,一并标注。 + +### P2-1 deployments 缺 `stream_events` / 通用 `put` + +> **状态**:✅ 已修复(commit `b69b585`)。SSE 实现抽出为共享模块 `v2/resources/_sse.py`(fine_tunes 同步重构复用);`Deployments.stream_events`/`AsyncDeployments.stream_events`(GET `/deployments/{id}/stream`)落地,raise/yield 契约与 fine_tunes 一致;新增类型化 `DeploymentEvent`。通用 `put` 未补(1.x `PutMixin` 无实际调用方,`scale` 已覆盖唯一在用场景)。 + +1.x `dashscope/finetune/deployments.py:20` 继承 `StreamEventMixin`/`PutMixin`;v2 `deployments.py:119` 只有固定 `scale`(:188)。 +**改法**:把 `fine_tunes.py` 已有的 `stream_events`(fine_tunes.py:626 SSE 实现)抽出复用到 `Deployments.stream_events(deployed_model)`;path 用 `/deployments/{id}/stream`(对齐 1.x)。 + +### P2-2 RL base_url 不一致 + 缺 function-instance 操作 + 缺 `jobs.delete` + +> **状态**:✅ 已修复(commit `b69b585`)。**决策:RL 走 v2 默认 endpoint**——1.x 把每个 RL 动词强转 `DASHSCOPE_HTTP_BASE_URL` 属 1.x 配置问题,不在 v2 复刻(评审会上拍板,替代原"host 对齐"改法)。已补:`RlJobs.delete`、三个 function-instance 方法(`query_function_instance_logs`/`query_all_function_instance_logs`/`delete_function_instance`,委派 1.x `AgenticRL`)、`test_functions` 的 `pull_logs/log_page_size/start_time/end_time`。 + +1.x `dashscope/finetune/agentic_rl.py:353-438` 的 `cancel/list/get/delete/logs` 强制 `DASHSCOPE_HTTP_BASE_URL`;v2 `reinforcement_learning.py:199 RlJobs`(:239-292)委派 `FineTunes` 走 v2 标准 endpoint → **host 不等价时行为偏**。另缺 `query_function_instance_logs`/`query_all_function_instance_logs`/`delete_function_instance`(agentic_rl.py:513/560/607)与 `jobs.delete`(1.x agentic_rl.py:400)。 +**改法**:确认 RL 管理面正确 host;若确需 `DASHSCOPE_HTTP_BASE_URL`,在 `RlJobs` 的 fetch/cancel/list/logs 调用里传 `base_url=`(transport 已支持 per-request base_url,http.py:92)。补 `RlJobs.delete` 与三个 function-instance 方法(委派 1.x `AgenticRL` 对应方法)。`test_functions`(rl.py:331)补 `pull_logs/log_page_size/start_time/end_time`。 + +### P2-3 任务等待语义翻转(需文档化 + 可选对齐) + +> **状态**:✅ `raise_on_error=False` 分支已修复(commit `b69b585`)。`Jobs.wait`/`AsyncJobs.wait` 在 compat 模式下对 `FAILED/CANCELED` 终态直接返回 Job 而非抛错,且不再空轮询。`DEFAULT_WAIT_TIMEOUT`(86400 vs 1.x 无限)维持 v2 语义,已在迁移指南 §3.2 标注。 + +v2 `Jobs.wait` 默认 `timeout=86400`(1.x 为无限 `-1`),`FAILED/CANCELED` 在 `raise_on_error` 下抛异常而非返回(_jobs.py:73/250/260)。 +**改法**:`DEFAULT_WAIT_TIMEOUT` 是否改回"无限"由产品决策;至少在迁移文档显式说明,并保证 `raise_on_error=False` 时返回终态 Job 而非抛错(确认 `_parse`/`_check_canceled` 在 False 分支返回对象)。 + +### P2-4 响应字段补全 + +> **状态**:✅ 已修复(commit `b69b585`)。`AppCompletion.workflow_message`(嵌套 `WorkflowMessage`)与 `AppUsage.models[]`(`AppModelUsage`)落地;`FineTune` 补 `user_identity/modifier/creator/group`;`Deployment` 补 `modifier/creator`。 + +- apps:`AppCompletion`(apps.py:80)补 `workflow_message`(1.x `application_response.py:120-166`)与 `usage.models[]` 每模型 token 明细类型化(当前仅 `extra="allow"`)。 +- fine_tunes:`FineTune`(fine_tunes.py:86)补 `user_identity/modifier/creator/group`(1.x `customize_types.py:47-50`)。 +- deployments:`Deployment`(deployments.py:45)补 `modifier/creator`(customize_types.py:143-144)。 + +### P2-5 AgentStudio 委派不走 v2 栈(说明性) +`client.py:115 agentstudio` 委派 1.x `agentstudio.Client`,只转发 `api_key/workspace/region`,**不转发 base_url/timeout/http_client**,且不经过 v2 transport/header/error 栈(刚统一的 `x-dashscope-sdk-*` 头对其不生效)。属有意复用,但需文档说明;若要统一标识,可在委派构造时把 v2 的 sdk 头/超时透传给 1.x agentstudio 的 transport。 + +### P2-6 音频实时面补全(较大,单列) + +> **状态**:✅ 前三项已修复(commit `b69b585`),立项项不变。已落地:callback 适配层 `v2/resources/audio/_callbacks.py:run_with_callback`(鸭子类型同时服务 asr/tts 两种 1.x callback 形状);增量 TTS `client.audio.tts.connect`→`SynthesisSession.send_text/stop/audio_chunks` + `enable_ssml`(联动 `text_type` PlainText/SSML),`tts.stream` 重构复用之;本地文件 ASR `asr.recognize_file(path)`(12800 字节分块,对齐 1.x);类型化结果 `RecognitionResult.get_sentence/is_sentence_end/get_usage` + `Recognition.results()`(过滤心跳与非 result 事件)。 + +- **callback 适配层**:1.x push 回调(`RecognitionCallback` recognition.py:140、`ResultCallback` tts_v2:69/90、omni/qwen-tts callback)无适配。建议提供 `events()`→callback 的可选适配器,降低迁移成本。 +- **增量 TTS**:`tts.stream`(tts.py:73)当前一次性 `continue_task({"text":text})`+`finish_task`(tts.py:105),且硬编码 `text_type:"PlainText"`、无 SSML。补 `streaming_call(text)`/`streaming_complete()` 等价能力与 `enable_ssml`(1.x tts_v2:627/657/885)。 +- **本地文件 ASR**:1.x `Recognition.call(file)` 自动分块(recognition.py:444);v2 需手动 `send_audio_frame`。补 `recognize_file(path)` 便捷方法。 +- **缺失能力(需立项)**:实时翻译 `TranslationRecognizerRealtime`(translation_recognizer.py:288)+`TranslationRecognizerChat`(:807)、omni 音视频类型化助手(`update_session/append_audio/append_video/create_response/commit`,omni_realtime.py:399)。当前 v2 仅 `realtime.connect` 裸事件可达。(注:omni 类型化助手 M2、实时翻译 M3、HTTP 合成 M4、词表/热词/声纹管理 M5、连接复用 M6 均已落地,见 §7.4。) +- **类型化结果**:`asr.recognize` 的 `events()` 产裸 dict;补 `RecognitionResult.get_sentence/is_sentence_end/get_usage`(recognition.py:99-125)等价类型。 + +### P2-7 `dashscope` CLI 暂未覆盖 v2(计划项) + +> **状态**:✅ 已在迁移指南 §5 提示"v2 无 CLI,命令行场景继续用 1.x CLI"(`md/dashscope-2.0-migration-guide.md`);v2 版 CLI 仍后续再加。 + +v2 暂不提供 CLI;现有 `dashscope` 命令行(acli,copy 自 agentic CLI,保持自包含、不依赖 dashscope 包)仍走 1.x。**v2 版 CLI 后续再加**,届时再评估入口命名与共用层。当前动作仅:在迁移文档提示"v2 无 CLI,命令行场景继续用 1.x CLI"。 + +--- + +## 4. v2 的净增益(对齐时勿回退) + +- 全资源 **async 孪生**(1.x 多为 sync 或零散 aio)。 +- **pydantic 强类型** + `extra="allow"` 兜底,响应可点出字段。 +- 统一 **`raise_on_error`** 与 `is_error/code/message/status_code` 信封(`types/common.py:24-49`)。 +- files `NotFoundError` 精化(files.py:159)、models `provider` 过滤(models.py:99)、deployments MU 字段 `deploy_spec/billing_method`(deployments.py:111)、`AppStream.text_stream`(apps.py:260)。 +- multimodal embedding 新增 `video` item;`with_options`(client.py:152)派生配置。 +- 统一 SDK 标识头 `x-dashscope-sdk-client`/`session-id`(本次已修)。 + +--- + +## 5. 落地优先级建议 + +| 优先级 | 项 | 估时倾向 | +|---|---|---| +| P0 | 本地文件上传(P0-1)、per-call 鉴权(P0-2) | ✅ `c9604e2` | +| P0 | CodeGeneration(P0-3) | ⚪ 已决策:hold,v2 先不支持 | +| P0 | 能力域(P0-4):TingWu ✅ `82e15d5`;Understanding/MultiModalDialog/Conversation | ⚪ 已决策:hold,v2 先不支持 | +| P1 | 命名参数(P1-1)、流式累积+reasoning(P1-2)、cancel/list(P1-3)、search_info(P1-4)、token_ids(P1-5)、key 文件(P1-6)、extend_prompt/function(P1-7) | ✅ `d8ac331` / `b69b585` | +| P2 | deployments.stream_events、RL endpoint/补全、等待语义、响应字段补全、音频实时面 | ✅ `b69b585`(音频立项项除外) | +| P2 | v2 CLI(P2-7) | 后续再加;✅ 迁移指南 §5 已提示 | +| 专项 | 语音模块专项(§7):ws 协议补齐与 omni 助手(`8eaaf81`)、M3 翻译门面、M4 HTTP 合成、M5 管理面、M6 连接复用(`WsConnection` + 全模型 HTTP keep-alive settings) | ✅ 全部落地;修复计划见 §7.4 | +| — | Assistants/Threads/Runs(P2-0) | 不在 2.0 范围;✅ 迁移指南 §5 已标注下线 | + +> 建议每个 P0/P1 项各开一个 issue,附本文对应小节的 file:line 作为实施清单起点;改动均落在 `dashscope/v2/`,复用 1.x 工具(oss_utils / tokenizers)而非重写,以保持物理隔离同时避免功能回退。 + +--- + +## 6. 不同步到 v2 的能力(整体标记) + +以下能力 **v2 不提供**,存量用户继续用 1.x;均已拍板或落地,不再视为缺口。迁移口径统一见 [`dashscope-2.0-migration-guide.md`](dashscope-2.0-migration-guide.md) §5。 + +| 能力 | 1.x 位置 | 处置 | 产品页现状(2026-09 核查) | +|---|---|---|---| +| Assistants / Threads / Runs | `dashscope/assistants/`、`threads/` | ⚪ 1.x 下线中,2.0 有意不包含(§3 P2-0) | — | +| CodeGeneration | `aigc/code_generation.py` | ⚪ hold,v2 先不支持(§1 P0-3) | 文档无独立页面 | +| nlp Understanding | `nlp/understanding.py` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面 | +| MultiModalDialog | `aigc/multimodal_dialog.py` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面 | +| Conversation / History | `conversation/` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面,多轮对话以 messages 数组教学 | +| HTTP-TTS(sambert 等) | `audio/http_tts/http_speech_synthesizer.py:114 HttpSpeechSynthesizer` | ✅ M4 已落地(`client.audio.tts.http`,见 §7.4) | [专页存活](https://help.aliyun.com/zh/model-studio/sambert-python-sdk),但示例已统一走 ws 版 `SpeechSynthesizer`,HTTP 形态不再主推 | +| qwen-tts HTTP | `audio/qwen_tts/speech_synthesizer.py:11` | ✅ M4 已落地(同上,按模型名前缀路由) | 存活且有独立页([模型信息](https://help.aliyun.com/zh/model-studio/model-qwen-tts) / [Qwen-TTS API](https://www.alibabacloud.com/help/zh/model-studio/qwen-tts-api)) | +| 实时翻译(gummy) | `audio/asr/translation_recognizer.py:288/807` | ✅ M3 已落地(`client.audio.translation`,见 §7.4) | [专页存活](https://help.aliyun.com/zh/model-studio/real-time-python-sdk),示例用 `TranslationRecognizerRealtime` | +| 定制词表 / 热词 | `audio/asr/vocabulary.py:31 VocabularyService`、`asr_phrase_manager.py:14 AsrPhraseManager` | ✅ M5 已落地(`client.audio.vocabularies` / `client.audio.phrases`,见 §7.4) | 存活([paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager) / [定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/)) | +| 声音复刻(声纹注册) | `audio/tts_v2/enrollment.py:31 VoiceEnrollmentService` | ✅ M5 已落地(`client.audio.voices`;同端点的 qwen 声音设计一并建模为 `client.audio.voice_design`,1.x 无对应类) | [专页存活](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide),但示例以 `tts_v2.SpeechSynthesizer` + 直接 HTTP API 创建音色,未用该类 | +| omni 类型化助手 | `audio/qwen_omni/omni_realtime.py:399`(update_session/append_audio/create_response 等) | ✅ M2 已落地(`client.audio.realtime` omni 类型化助手,`8eaaf81`,见 §7.4) | 存活且主推([Qwen-Omni-Realtime](https://help.aliyun.com/zh/model-studio/realtime) / [Realtime API 概述](https://help.aliyun.com/zh/model-studio/realtime-api-overview)) | +| `dashscope` CLI(v2 版) | acli(自包含,走 1.x) | ⚪ 后续再加(§3 P2-7) | — | + +> 上表各项均已拍板或落地:hold 项存量用户继续用 1.x,CLI 后续再加;语音相关项已全部落地(§7)。 + +--- + +## 7. 语音模块专项(需立项,专门做) + +> 依据语雀专项文档([语音模块专项](https://aliyuque.antfin.com/songsong.sss/tcd79h/vi8tshautdo2k5iy))成稿,正文已由专项负责人提供(2026-09-22),接口对齐清单与严重问题以语雀为准,本章已按 v2 最新代码校正状态。 +> +> 定位:本章各项**不属于对齐修复**,而是 v2 语音面的独立建设,按一个专项(或拆子项)立项推进,不挤占 P0-P2 的修复节奏。 + +### 7.1 专项范围(语雀界定) + +- **语音识别**:qwen-audio(fun-asr/paraformer)系列实时识别、funasr-flash 等离线识别、录音文件识别。 +- **语音合成**:qwen-audio、CosyVoice 等。 +- **声音复刻 / 声音设计 / 热词词表**:均基于 HTTP 协议,可替换通用接口或直接用 http 请求。 +- **Omni**:omni / 翻译 / realtime。 + +### 7.2 严重问题(专项首要,必须解决) + +1. **ASR 缺 `update_context`(必须)** — ✅ 已解决(`8eaaf81` 合入 `dev/v2-audio-streaming-completion`,`d1de92f`):`Recognition.update_context(context)` 走 `continue-task` + `{"input": context}` wire,与 1.x `audio/asr/recognition.py:632` 对齐;非 dict 输入 `TypeError`。 +2. **TTS 流式上传/控制协议缺失(必须)** — ✅ 已解决(`8eaaf81`,`c570d20`/`24bb67b`):`SynthesisSession` 显式生命周期 `start/stop/cancel/close`,`flush()`(continue-task `{"input":{"flush":True}}`,已经真实服务验证,无需 1.x 的重述 model/task 字段)、`cancel()`(finish-task `input.directive=cancel`,ws 层 `_cancel_sent` 幂等);命名按评审 no-alias 前提(naming-review §六:直接改名、不留兼容 alias)收敛为 canonical 名(曾短暂保留的 1.x 别名 `streaming_*` 与 `TtsStream`/`start_stream` 已移除,同一动作只留一个名字)。`async_streaming_complete` 不做(v2 实时面 sync-only 设计)。 +3. **连接复用缺失(必须)** — ✅ 已解决(M6 落地): + - **ws 连接复用**:`WsConnection` + `WsTask`(transport/ws_connection.py)——一条物理连接 + 后台读线程按 `header.task_id` 解复用,支持并发多任务与顺序复用(免再握手);`client.ws_connection()` 工厂继承 client 配置,asr/tts/translation/tingwu 入口均接受 `connection=`。协议约束:二进制帧无 task_id,单连接二进制上行/下行各单 owner(TTS `start()` 自动 claim;并发 TTS 需多条连接),纯 JSON 任务完全并发;realtime 族协议无 task_id、会话结束即关连接,不可复用(非 SDK 限制,loopback 测试固化;压首开延迟走预热)。 + - **HTTP 连接复用(所有模型)**:每 `Client` 一个 httpx keep-alive 池(1.x 每次调用新建 `requests.Session`),所有资源默认复用;池参数开放 settings/env/config.toml(`http_max_connections` / `http_max_keepalive_connections` / `http_keepalive_expiry`);`with_options` 未改连接级选项时共享池。`tests/v2/test_http_pool.py` 本地服务器证明同一 TCP 连接被复用、keepalive=0 端到端生效。背景(2026-09-23 评审):1.x 大量用户以连接复用模式接入(主要 Java 侧),语音实时交互对延时极敏感——连接复用是硬需求而非优化项。 + +### 7.2.1 `dev/v2-audio-streaming-completion` 合入修复清单 + +> 2026-09-23 以 merge `8eaaf81` 合入(tag `v2-audio-m1` / `v2-audio-m2`)。分支经 4 个真实模型线下手工验证(未纳入 CI,模型名以专项验证记录为准):qwen-audio-3.1-asr-flash-streaming、qwen-audio-3.0-tts-flash(voice longanfengyu)、qwen3.5-omni-flash-realtime、qwen-audio-3.0-realtime-flash。合入策略:架构以分支为准,HEAD 独有能力逐项 graft(module 标识头、per-call `api_key`/`workspace`、`RecognitionResult`/`results()`/`recognize_file`、合成具名参数 + `enable_ssml` 联动、jobs `headers` 参数)。 + +| 修复 | 分支 commit | 对应条目 | +|---|---|---| +| `Recognition.update_context(context)`(continue-task + `{"input": context}`) | `d1de92f` | §7.2-1 | +| `SynthesisSession` 显式生命周期 `start/stop/cancel/close` + `flush()` + `cancel()`(ws `_cancel_sent` 幂等) | `c570d20` / `24bb67b` | §7.2-2 | +| ws 严格 framing/生命周期:task-failed 经 header `error_code`/`error_message` 走错误目录、EOF → `APIConnectionError`、finish/cancel/close 幂等 | `2c9a0b2` | §7.2 基础 | +| `RealtimeSession` 具名方法全集 + `session_id`(omni 类型化助手) | `ab3ec1c` | §7.3 Omni 表(M2) | +| livetranslate / qwen-audio 协议族(wire_model 前缀路由;`_incremental_session` / `_nested_translation_session`) | `a147465` / `68f6afe` | §7.4 M3 前置 | +| asr / tts / realtime SDK 使用指南(`dashscope-2.0-{asr,tts,realtime}-sdk.md`) | `cca6dfd` | 文档 | +| loopback ws 集成测试基座(`_LocalWebSocketServer`,aiohttp 真实回环;单测 mock `FakeWebSocket`/`_sent_frames` 早于本分支,由 `52f87e7` 引入、分支内各 commit 复用扩展) | `3adc9f5` | 测试 | + +合入后仍开放:`async_streaming_complete`(不做,v2 实时面 sync-only);§7.2-3 连接复用随后于 M6 落地(见 §7.4)。合入后的命名收敛(移除 `streaming_*` 别名 / `TtsStream` / `start_stream`,`update_context(input)` → `(context)`)见 §7.2-2 与 commit `5f70c02`。M3 gummy 门面(`client.audio.translation`)、M4 HTTP 合成(`client.audio.tts.http`)、M5 管理面(`client.audio.vocabularies/phrases/voices/voice_design`)、M6 连接复用(`WsConnection` + HTTP keep-alive settings)均已于本阶段落地,见 §7.4。 + +### 7.3 接口对齐清单(语雀表格,按 v2 现状校正) + +**Transcription 离线转写** — ✅ 已对齐:`call→asr.transcribe`、`async_call→asr.jobs.create`、`fetch→asr.jobs.fetch`、`wait→asr.jobs.wait`。 + +**Recognition 实时转写** + +| 线上 SDK | v2 入口 | 状态 | +|---|---|---| +| `Recognition.start` | `asr.recognize(...)` + with(`start()`) | ✅ | +| `send_audio_frame` / `stop` / 取结果 | `send_audio_frame` / `stop` / `events()` | ✅ | +| `Recognition.call(file)` | `asr.recognize_file(path)` | ✅ 非必须,已补(`b69b585`) | +| **`Recognition.update_context`** | `Recognition.update_context(context)` | ✅(`8eaaf81`/`d1de92f`,见 7.2-1) | +| `get_first_package_delay` / `get_last_package_delay` | `first_package_delay` / `last_package_delay` 属性 | ✅(`8eaaf81`/`d1de92f`) | +| `get_last_request_id` | `Recognition.request_id` 属性 | ✅(`8eaaf81`/`d1de92f`) | +| `RecognitionCallback` | 主动迭代 `events()`;`_callbacks.run_with_callback` 适配 | 🔀 | +| `RecognitionResult` | `events()` 裸 dict;`results()` 类型化 | ✅(`b69b585`) | +| 热词 | `phrase_id`(run-task 顶层 `resources`,与 1.x 同位) | ✅(`8eaaf81`;原 `phrases` 参数废弃) | + +**语音合成** + +| 线上 SDK | v2 入口 | 状态 | +|---|---|---| +| `tts_v2.SpeechSynthesizer.call` | `audio.tts.synthesize(...)` | ⚠️ 一次性合成近似对应 | +| `streaming_call` | `SynthesisSession.send_text` | ✅(`8eaaf81`) | +| **`streaming_flush`** | `SynthesisSession.flush` | ✅(`8eaaf81`/`c570d20`,见 7.2-2) | +| `streaming_complete` | `stop()` + `audio_chunks()` | ✅(`8eaaf81`) | +| **`async_streaming_complete`** | 无 | ❌ 不做(v2 实时面 sync-only) | +| **`streaming_cancel`** | `SynthesisSession.cancel` | ✅(`8eaaf81`/`c570d20`,见 7.2-2) | +| `close`(长会话管理) | `SynthesisSession` 显式 `start/stop/close` | ✅(`8eaaf81`/`24bb67b`) | +| `HttpSpeechSynthesizer.call(stream=False/True)` | `audio.tts.http.synthesize/stream(...)`(真 HTTP,非 ws) | ✅(M4;`HttpSynthesis`/`HttpSynthesisChunk` 类型化音频 URL/ID/过期时间,见 §7.4) | + +**Omni** + +| 线上 SDK | v2 入口 | 状态 | +|---|---|---| +| `OmniRealtimeConversation.connect` | `audio.realtime.connect(...)` + with | ✅ | +| `send_raw` | `session.send_event(...)`(dict,非 JSON 串) | ⚠️ | +| 接收事件 | `recv_event()` / `events()` | ✅ | +| `update_session`/`create_item`/`append_audio`/`append_video`/`commit`/`clear_appended_audio`/`create_response`/`cancel_response` | `RealtimeSession` 同名具名方法(另含 `retrieve_item`/`delete_item`) | ✅(`8eaaf81`/`ab3ec1c`) | +| `end_session` | `RealtimeSession.end_session()` | ✅(`8eaaf81`/`ab3ec1c`) | +| `get_session_id` / response ID / 延迟指标 | `session_id` 属性;指标见 realtime 事件跟踪 | ✅/❔(`8eaaf81`) | + +**实时翻译** — livetranslate 协议变体(qwen3-livetranslate/qwen3.5/qwen3.8)已内建于 `audio.realtime.connect` 会话层(`8eaaf81`/`a147465`,含嵌套翻译 session 组装);gummy 族(gummy-realtime-v1 / gummy-chat-v1)由 `client.audio.translation` 子资源承载(M3 已落地,见 §7.4):与 `asr.recognize` 同 wire(task_group=audio / task=asr / function=recognition,与 1.x `translation_recognizer.py:451/987` 逐帧一致),翻译参数具名化(`source_language`/`transcription_enabled`/`translation_enabled`/`translation_target_languages`/`max_end_silence`),`results()` 产双语类型 `TranslationResult`(`get_transcription`/`get_translations`/`is_sentence_end`),`recognize_file(path)` 对应 1.x `call(file)`;chat 形态不单独建模(1.x Chat 仅差"句终停发"的客户端行为,wire 相同,以 `is_sentence_end()` 自流控)。 + +**已验证模型**(分支真实跑通,2026-09-23):`qwen-audio-3.1-asr-flash-streaming`、`qwen-audio-3.0-tts-flash`(voice: longanfengyu)、`qwen3.5-omni-flash-realtime`、`qwen-audio-3.0-realtime-flash`。 + +**管理面(HTTP)** — ✅ M5 已落地:定制词表 `client.audio.vocabularies`(1.x `VocabularyService`,POST `services/audio/asr/customization`)、热词 `client.audio.phrases`(1.x `AsrPhraseManager`,编译任务:`create/update` POST `/fine-tunes`,`get/list/delete` 走 `/fine-tunes/outputs`)、声音复刻 `client.audio.voices`(1.x `VoiceEnrollmentService`,POST `services/audio/tts/customization`)、声音设计 `client.audio.voice_design`(官方 REST,与复刻同端点,`model=qwen-voice-design`,action 词 `create/list/query/delete`)。1.x 五动词统一为 `create/list/get/update/delete`(`query_*` → `get`);1.x 服务内 3 次重试改由 transport 重试循环承担;1.x `list_phrases(page=)` 实参被 `**kwargs` 丢弃的缺陷不复刻(v2 发 `page_no`)。 + +### 7.4 修复计划 + +严重问题(7.2)优先于能力补齐,分阶段交付,每阶段独立 commit、独立可发版。 +**进度更新(2026-09-23)**:`dev/v2-audio-streaming-completion` 分支(9 commits,四模型真实验证)经 `8eaaf81` 合入,M1(除连接复用)/M2 已落地,M3 协议层随合入落地、gummy 门面(`client.audio.translation`)本阶段补齐;M4 HTTP 合成(`client.audio.tts.http`)、M5 管理面(`client.audio.vocabularies/phrases/voices/voice_design`)、M6 连接复用(`WsConnection` + 全模型 HTTP keep-alive settings)均已落地,语音专项 §7 全部关闭。 + +| 阶段 | 范围 | 状态 | 交付物 | 验收 | +|---|---|---|---|---| +| M1 ws 协议能力补齐 | 严重问题 1-2 | ✅ `8eaaf81`(tag `v2-audio-m1`) | `Recognition.update_context`;`SynthesisSession` 显式生命周期 + `flush/cancel`;ws 严格 framing/生命周期(`2c9a0b2`) | 真实模型验证(asr-flash-streaming / tts-flash)+ loopback ws 单测(`3adc9f5`) | +| M2 omni 类型化助手 | 7.3 Omni 行 | ✅ `8eaaf81`(tag `v2-audio-m2`) | `RealtimeSession` 具名方法全集(update_session/append_audio/append_video/commit/clear_appended_audio/create_item/retrieve_item/delete_item/create_response/cancel_response/end_session)+ `session_id`(`ab3ec1c`) | omni-flash-realtime / realtime-flash 真实验证 + `test_audio_omni_session.py`(`68f6afe`) | +| M3 实时翻译 | livetranslate / gummy | ✅ | livetranslate 三变体内建 `realtime.connect`(`8eaaf81`/`a147465`);gummy 门面 `client.audio.translation`:`TranslationSession`(复用 asr wire)+ 具名翻译参数 + 双语 `TranslationResult` + `recognize_file`;chat 形态评估结论:不单独建模(wire 相同,句终停发由调用方经 `is_sentence_end()` 表达) | [Gummy 页](https://help.aliyun.com/zh/model-studio/real-time-python-sdk)流程平移至 `md/dashscope-2.0-translation-sdk.md`;`test_audio_translation.py` wire 逐帧对齐 | +| M4 HTTP 合成 | `http_tts:114` + `qwen_tts:11`(合并) | ✅ | `client.audio.tts.http`(真 HTTP POST,非 ws):`synthesize` 一次性 → `HttpSynthesis`(audio_url/audio_id/expires_at/usage/request_id 类型化);`stream` SSE 流式 → `HttpSynthesisChunk`(audio 帧 base64 解码、sentence、is_final,末帧带整段音频 URL 引用);双模型族按模型名前缀路由(`qwen*` → aigc/multimodal-generation/generation,其余 → audio/tts/SpeechSynthesizer),具名参数 `voice/format/sample_rate/language_type` + `parameters=`/`extra_input=` 透传(具名优先) | sambert 与 qwen-tts 两个官方示例平移至 `md/dashscope-2.0-tts-sdk.md`;`test_audio_tts_http.py` MockTransport 6 例(端点路由/参数合并/SSE 分片/错误帧/错误状态) | +| M5 管理面 | 词表/热词/声音复刻/声音设计 | ✅ | `client.audio.vocabularies`(1.x `VocabularyService`,`services/audio/asr/customization`)/ `phrases`(1.x `AsrPhraseManager`,`/fine-tunes` + `/fine-tunes/outputs` 编译任务)/ `voices`(1.x `VoiceEnrollmentService`,`services/audio/tts/customization`)/ `voice_design`(官方 REST,同复刻端点,`qwen-voice-design`);五动词统一 `create/list/get/update/delete`,类型化返回(Vocabulary/Voice/DesignedVoice/Phrase + 各自 List + `JobStatus`) | 1.x 五动词 × 各服务 wire 逐项断言;官方四族示例平移至 `md/dashscope-2.0-audio-management-sdk.md`;`test_audio_management.py` MockTransport 8 例 | +| M6 连接复用(上调,见 7.2-3) | ws 复用 + 全模型 HTTP keep-alive | ✅ | ws:`WsConnection`+`WsTask`(读线程按 `header.task_id` 解复用,并发多任务 + 顺序复用免握手;二进制帧无 task_id → 单连接上行/下行各单 owner,纯 JSON 全并发),`client.ws_connection()` 工厂,asr/tts/translation/tingwu 接受 `connection=`;realtime 族验证结论:协议无 task_id、会话结束即关连接,不可复用(建议预热),loopback 测试固化;HTTP:每 `Client` 一个 keep-alive 池,池参数开放 settings/env/config.toml(`http_max_connections`/`http_max_keepalive_connections`/`http_keepalive_expiry`) | `test_ws_connection.py` 12 例(并发解复用/顺序复用单握手/二进制 owner/错误广播)+ `test_audio_connection.py` 资源集成 7 例;`test_http_pool.py` 本地服务器证明同一 TCP 连接被复用、keepalive=0 端到端生效;指南 `md/dashscope-2.0-connection-reuse-sdk.md` | + +**节奏约定** +- 每阶段 = 1 个 commit(与 P 级修复同样约定),完成后 §6 对应行与 7.3 清单回填 ✅ 与 commit。 +- M6 的 ws 复用经设计评审后落地(评审结论:并发 + 顺序复用全量 demux、realtime 作为验证项、HTTP 池参数开放 settings);HTTP keep-alive 为 transport 默认行为,随 M6 同批落地。 +- 背景(2026-09-23 评审):1.x 大量用户以连接复用模式接入(主要 Java 侧),语音实时交互对延时敏感——连接复用是硬需求而非优化项。 +- 范围与优先级以语雀专项文档为准;本表随专项推进滚动更新。 diff --git a/md/dashscope-2.0.3-python-sdk.md b/md/dashscope-2.0.3-python-sdk.md new file mode 100644 index 00000000..fe6652ce --- /dev/null +++ b/md/dashscope-2.0.3-python-sdk.md @@ -0,0 +1,675 @@ +# DashScope Python SDK 2.0.3 接口与迁移指南 + +本文面向 Python SDK 2.0.3,汇总当前包结构、1.x → 2.0 接口映射、核心调用示例与 +迁移注意事项。示例基于当前实现核对;完整参数与分领域说明见文末相关文档。 + +## 1. 安装与最小示例 + +Python 要求:**3.9 及以上**。 + +```bash +pip install -U dashscope +export DASHSCOPE_API_KEY="sk-xxx" +# 推荐配置 workspace;也可在 Client(workspace="ws-xxx") 中显式传入 +export DASHSCOPE_WORKSPACE="ws-xxx" +``` + +```python +import dashscope + +with dashscope.Client() as client: + response = client.chat.completions.create( + model="qwen-plus", + messages="用一句话介绍杭州", + ) + print(response.choices[0].message.content) + print(response.usage.total_tokens, response.request_id) +``` + +异步客户端具有同名资源与方法: + +```python +import dashscope + +async with dashscope.AsyncClient() as client: + response = await client.chat.completions.create( + model="qwen-plus", + messages="你好", + ) + print(response.output_text) +``` + +只需一次文本调用时,可以使用快捷入口: + +```python +response = dashscope.chat("qwen-plus", "给我讲个笑话") +print(response.output_text) +``` + +## 2. 2.0.3 包结构与兼容边界 + +### 2.1 当前规范入口 + +2.0 实现直接位于 `dashscope` 根包,**不再存在 `dashscope.v2` 命名空间**: + +```python +import dashscope +from dashscope import AsyncClient, Client, Config, Settings +from dashscope import DashScopeError, ErrorCode, WsConnection, WsTask +from dashscope import errors, settings +from dashscope.tokenizers import get_tokenizer, list_tokenizers +``` + +资源树挂在 `Client` / `AsyncClient` 上,主要入口如下: + +| 领域 | 当前入口 | +| --- | --- | +| 文本对话 | `client.chat.completions` | +| 百炼应用 | `client.apps.completions` | +| 多模态对话与向量 | `client.multimodal.chat` / `client.multimodal.embeddings` | +| 图像与视频 | `client.images` / `client.videos`,均含 `jobs` | +| 文本向量与批量向量 | `client.embeddings` / `client.embeddings.jobs` | +| 排序与分词 | `client.rerank` / `client.tokenization` | +| 语音 | `client.audio.asr` / `tts` / `realtime` / `translation` | +| 语音管理面 | `client.audio.vocabularies` / `phrases` / `voices` / `voice_design` | +| 听悟 | `client.tingwu` | +| 微调与部署 | `client.fine_tunes` / `client.deployments` | +| 文件与模型 | `client.files` / `client.models` | +| Agentic RL | `client.rl`,长名别名 `client.reinforcement_learning` | +| AgentStudio | `client.agentstudio` | + +### 2.2 1.x 与 2.0 的目录分工 + +- 根包保留 `Generation`、`ImageSynthesis`、`FineTunes`、`Deployments` 等既有 + 1.x 公开导出,存量代码可继续使用。 +- 1.x 深层实现统一位于 `dashscope.legacy_1x`;旧的 `dashscope.aigc.*`、 + `dashscope.files`、`dashscope.models`、`dashscope.client.base_api` 等 deep import + 通过懒加载别名继续可用,存量代码无需修改。 +- AgentStudio 与 FineTune/Agentic RL 的物理实现归入 `dashscope.applications`; + 公开 `dashscope.agentstudio`、`dashscope.finetune` 导入路径通过别名保持不变。 +- 2.0 Agentic RL 公开入口仍为 `dashscope.finetune.agentic_rl`,函数组件与运行框架 + 位于 `dashscope.finetune.reinforcement`。 +- 1.x Agentic RL 管理面位于 `dashscope.legacy_1x.finetune.agentic_rl`。 +- 不提供 `dashscope.reinforcement` 或 `dashscope.legacy_2x` 兼容命名空间。 +- `dashscope` 命令行入口仍由 1.x CLI 提供;2.0 以 `Client` 资源树为编程入口。 + +直接使用 2.0 Agentic RL 类型时: + +```python +from dashscope.finetune.agentic_rl import AgenticRL, SubmitResult +from dashscope.finetune.reinforcement import ( + Dataset, + TrainingDataset, + ValidationDataset, +) +``` + +2.0 `AgenticRL.submit_job()` 返回 `SubmitResult`。1.x wrapper 的同名方法返回 +`FineTune`,并额外提供 `cancel/list/get/delete/logs`;两者不能仅因类名相同就 +混用返回契约。常规业务代码优先使用 `client.rl`,无需直接管理这些实现类。 + +## 3. 1.x → 2.0 接口映射 + +状态说明:**迁移**表示有直接对应入口;**收敛**表示多个旧入口合并为同一资源。 + +### 3.1 文本、应用与多模态 + +| 1.x | 2.0.3 | 说明 | +| --- | --- | --- | +| `Generation.call` | `client.chat.completions.create` | 迁移 | +| `AioGeneration.call` | `await aclient.chat.completions.create` | 迁移 | +| `Conversation.call` | `client.chat.completions.create` | 收敛;调用方维护 messages | +| `CodeGeneration.call` | `client.chat.completions.create` | 收敛;通过 model 区分 | +| `Understanding.call` | `client.chat.completions.create` | 收敛;文本理解走对话端点 | +| `Application.call` | `client.apps.completions.create` | 迁移 | +| `MultiModalConversation.call` | `client.chat.completions.create` 或 `client.multimodal.chat.create` | 媒体 part 自动路由;后者为显式入口 | +| `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` / `client.images.jobs.*` | 一步到位或分步控制 | +| `ImageGeneration.call/async_call/fetch/wait` | `client.images.generate` / `client.images.jobs.*` | 按模型名自动路由协议 | +| `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` / `client.videos.jobs.*` | 返回统一 `Job` | +| `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | 文本、图片、音频、视频向量 | + +### 3.2 向量、排序与分词 + +| 1.x | 2.0.3 | +| --- | --- | +| `TextEmbedding.call` | `client.embeddings.create` | +| `BatchTextEmbedding.call/fetch/wait` | `client.embeddings.jobs.create/fetch/wait` | +| `TextReRank.call` / `AioTextReRank.call` | `client.rerank.create` / `await aclient.rerank.create` | +| `Tokenization.call` | `client.tokenization.count` | +| 本地 tokenizer | `dashscope.tokenizers.get_tokenizer` / `list_tokenizers` | + +### 3.3 语音与听悟 + +| 1.x | 2.0.3 | +| --- | --- | +| `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器 | +| `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe` / `client.audio.asr.jobs.*` | +| `QwenTranscription` | 同上,通过 `model=` 区分 | +| `SpeechSynthesizer.call` | `client.audio.tts.synthesize/stream` | +| `tts_v2.SpeechSynthesizer` | `client.audio.tts.stream/connect` | +| `HttpSpeechSynthesizer.call` | `client.audio.tts.http.synthesize/stream` | +| `TranslationRecognizerRealtime` | `client.audio.translation.recognize(...)` | +| `OmniRealtimeConversation` | `client.audio.realtime.connect(...)` | +| `VocabularyService` | `client.audio.vocabularies` | +| `AsrPhraseManager` | `client.audio.phrases` | +| `VoiceEnrollmentService` | `client.audio.voices` | +| `TingWu.call` / 实时理解 | `client.tingwu.call` / `client.tingwu.realtime` | + +2.0 音频 API 当前仍按 preview 管理,首次访问 `client.audio` 会发出 +`FutureWarning`;升级时应核对音频专题文档中的协议与生命周期约束。 + +### 3.4 微调、部署、资源与 Agent + +| 1.x | 2.0.3 | +| --- | --- | +| `FineTunes.call` | `client.fine_tunes.create` | +| `FineTunes.cancel/list/get/delete` | `client.fine_tunes.cancel/list/get/delete` | +| `FineTunes.stream_events/wait/logs` | `client.fine_tunes.stream_events/wait/logs` | +| `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | +| `Models.list/get` | `client.models.list/get` | +| `Files.upload/list/get/delete` | `client.files.upload/list/get/delete` | +| CLI `dashscope rl ...` | `client.rl.*` + `client.rl.jobs.*` | +| AgentStudio 子 SDK | `client.agentstudio.*` 懒挂载 | + +Assistants / Threads / Messages / Runs / Steps 不属于 2.0 资源面。新托管 Agent +场景使用 `client.agentstudio`;不要为了迁移旧对话状态而自行模拟这些资源。 + +## 4. 客户端配置与协议 + +### 4.1 构造参数与配置优先级 + +```python +client = dashscope.Client( + api_key="sk-xxx", + workspace="ws-xxx", + region="cn-beijing", + protocol="native", + timeout=60.0, + max_retries=2, + raise_on_error=True, + config_path="~/.dashscope/config.toml", + protocol_options={"strict": False}, + http_client=None, +) +``` + +配置优先级:**显式参数 > `dashscope.settings` 赋值 > 环境变量 > config.toml > +默认值**。不显式传 `api_key` 时读取 `DASHSCOPE_API_KEY`。建议配置 `workspace` +以使用专属网关;未配置 workspace 且未显式给 `base_url` 时,SDK 回退共享网关并 +发出一次 `UserWarning`。 + +`region` 默认 `cn-beijing`,支持 `cn-beijing`、`ap-southeast-1`、 +`cn-hongkong`、`us-east-1`、`eu-central-1`、`ap-northeast-1`。自建或私有网关 +可显式传 `base_url`。 + +运行时可使用全局配置门面,或派生只改部分选项的客户端: + +```python +dashscope.settings.timeout = 30.0 +dashscope.settings.max_retries = 3 + +strict = client.with_options(protocol="openai", timeout=10.0) +``` + +`with_options()` 在未改变连接级选项时共享原客户端连接池,派生实例不应比原实例 +活得更久。普通请求上的 `timeout=` 是单次 HTTP 请求超时;`images.generate`、 +`videos.generate`、`audio.asr.transcribe` 和各类 `wait` 上的 `timeout=` 是等待 +任务终态的总预算。 + +### 4.2 三种对话协议 + +`protocol` 只改变 `chat.completions` / `multimodal.chat` 的线缆格式,调用方式和 +统一返回类型不变: + +| 取值 | 端点形态 | 说明 | +| --- | --- | --- | +| `native` | DashScope `/services/...` | 默认,支持完整 DashScope 参数 | +| `openai` | `/chat/completions` | 对接百炼 compatible-mode 或 OpenAI 兼容网关 | +| `anthropic` | `/v1/messages` | system 独立、工具 schema 转换 | + +```python +openai_client = dashscope.Client(protocol="openai") +response = openai_client.chat.completions.create( + model="qwen-plus", + messages="你好", +) +``` + +`openai` 协议默认透传非 OpenAI 原生参数;`protocol_options={"strict": True}` +可改为拒绝未知参数。`anthropic` 对无对应字段的参数直接报 400,避免参数被静默 +丢弃;该协议下 `max_tokens` 缺省自动补 `4096`,`stop` 映射为 `stop_sequences`。 + +## 5. 核心调用示例 + +以下示例复用已构造的 `client` / `aclient`。 + +### 5.1 对话、工具与结构化输出 + +```python +response = client.chat.completions.create( + model="qwen-plus", + messages=[ + {"role": "system", "content": "你是一个有帮助的助手。"}, + {"role": "user", "content": "北京今天天气怎么样?"}, + ], + temperature=0.7, + max_tokens=512, + tools=[{ + "type": "function", + "function": { + "name": "get_weather", + "description": "查询城市天气", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, + }], +) +print(response.choices[0].message.tool_calls) + +structured = client.chat.completions.create( + model="qwen-plus", + messages="给我一个用户 JSON", + response_format={"type": "json_object"}, +) +print(structured.output_text) +``` + +签名之外的模型参数通过 `parameters=` 传递。多轮对话由调用方维护 messages 列表, +2.0 chat 本身不保存历史。 + +### 5.2 增量流式输出 + +```python +stream = client.chat.completions.create( + model="qwen-plus", + messages="写一首短诗", + stream=True, +) +for chunk in stream: + delta = chunk.choices[0].delta + if delta.content: + print(delta.content, end="", flush=True) +``` + +`ChatStream.text_stream` 只迭代文本,`get_final_message()` 聚合完整 message(含 +工具调用),`headers` 提供响应头。与 1.x 默认重放全量文本不同,2.0 chat 每帧 +都是增量。异步流式使用 `await aclient.chat.completions.create(..., stream=True)` +和 `async for`。 + +### 5.3 百炼应用 + +```python +response = client.apps.completions.create( + app_id="your-app-id", + prompt="帮我写一份周报大纲", +) +print(response.text) + +for partial in client.apps.completions.create( + app_id="your-app-id", + prompt="讲个故事", + stream=True, + incremental_output=True, +): + print(partial.text, end="") +``` + +`prompt` 与 `messages` 至少传一个;同时给出时,`prompt` 作为最新 user 消息追加。 +`session_id` 用于服务端会话状态,`biz_params` 用于应用流程或插件入参。 + +### 5.4 多模态对话与向量 + +```python +response = client.chat.completions.create( + model="qwen-vl-plus", + messages=[{ + "role": "user", + "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "这张图里有什么?"}, + ], + }], +) +print(response.output_text) + +embeddings = client.multimodal.embeddings.create( + model="multimodal-embedding-v1", + input=[ + {"text": "一只猫"}, + {"image": "https://example.com/cat.jpg"}, + ], +) +for item in embeddings.embeddings: + print(item.index, item.type, len(item.embedding)) +``` + +只要 messages 含 image / audio / video part,`chat.completions.create` 就自动走 +多模态端点。`client.multimodal.chat.create` 是始终使用该端点的显式入口。类型化 +向量输入可从 `dashscope.resources.multimodal.embeddings` 导入 `EmbeddingItem`。 + +### 5.5 图像、视频与统一 Job + +```python +image_job = client.images.generate( + model="wanx2.1-t2i-turbo", + prompt="一只在月球上弹吉他的猫,水彩风格", + size="1024*1024", +) +print(image_job.image_urls) + +video_job = client.videos.generate( + model="wan2.7-t2v", + prompt="一只小猫在草地上奔跑", + size="1280*720", +) +print(video_job.video_url) + +# 分步提交与轮询;images / videos / embeddings / asr 的 jobs 语义一致 +job = client.images.jobs.create( + model="wanx2.1-t2i-turbo", + prompt="一只猫", +) +snapshot = client.images.jobs.fetch(job.task_id) +done = client.images.jobs.wait(job, timeout=600.0) +``` + +`generate` 是“提交并等待”,`jobs.create` 只提交,`wait` 使用退避轮询。统一 +`Job` 常用字段为 `task_id` / `status` / `results` / `result_url` / `image_urls` / +`video_url`;只有服务相关的原始载荷保留在 `Job.output`。任务失败或被取消时, +默认抛出 `DashScopeError` 异常族。 + +### 5.6 文本向量、排序与分词 + +```python +embedded = client.embeddings.create( + model="text-embedding-v3", + input=["今天天气不错", "明天会下雨吗"], + dimension=1024, + text_type="document", +) +print([len(item.embedding) for item in embedded.embeddings]) + +ranked = client.rerank.create( + model="gte-rerank-v2", + query="什么是量子计算?", + documents=["量子计算利用量子力学原理。", "今天超市鸡蛋打折。"], + top_n=1, + return_documents=True, +) +print(ranked.results[0].index, ranked.results[0].relevance_score) + +count = client.tokenization.count(model="qwen-plus", prompt="你好,世界") +print(count.input_tokens) +``` + +批量向量使用 `client.embeddings.jobs.create(model=..., file_url=...)`,完成后从 +`job.result_url` 读取结果文件。本地分词不发送请求: + +```python +from dashscope.tokenizers import get_tokenizer + +tokenizer = get_tokenizer("qwen-plus") +token_ids = tokenizer.encode("你好,世界") +print(tokenizer.decode(token_ids)) +``` + +### 5.7 语音与实时会话 + +```python +# 文件转写:提交并等待 +job = client.audio.asr.transcribe( + ["https://example.com/meeting.wav"], + model="paraformer-v2", +) +print([item.transcription_url for item in job.results]) + +# 一次性语音合成 +audio = client.audio.tts.synthesize( + "你好,欢迎收听今天的节目。", + model="cosyvoice-v1", + voice="longxiaochun", + format="mp3", +) +with open("out.mp3", "wb") as file: + file.write(audio) + +# 流式语音合成 +with open("stream.mp3", "wb") as file: + for chunk in client.audio.tts.stream( + "这是一段较长的文本。", + model="cosyvoice-v1", + voice="longxiaochun", + ): + file.write(chunk) +``` + +实时 ASR、双向流 TTS、Realtime 与实时翻译采用 websocket 会话,不是普通 HTTP +流式响应。它们通过上下文管理器管理生命周期,并用 `events()` / `results()` 迭代 +服务端事件。发送与接收通常需要并发,完整示例见文末语音专题文档。 + +支持共享连接的会话可复用 `client.ws_connection()`;共享连接由调用方关闭。实时 +协议是否支持复用取决于任务帧是否携带 `task_id`,不要把 Realtime 会话强行放入 +共享连接。 + +### 5.8 文件、微调与部署 + +```python +uploaded = client.files.upload( + file_path="./train.jsonl", + purpose="fine-tune", +) + +fine_tune = client.fine_tunes.create( + model="qwen-turbo", + training_file_ids=[uploaded.file_id], + hyper_parameters={"n_epochs": 2, "learning_rate": "1e-5"}, +) +finished = client.fine_tunes.wait(fine_tune, timeout=7200) +print(finished.finetuned_model) + +for event in client.fine_tunes.stream_events(fine_tune.job_id): + print(event.output) + +# 部署后使用 deployed_model 作为 get / scale / delete 的定位键 +deployment = client.deployments.create(model="qwen-turbo", capacity=2) +client.deployments.scale(deployment.deployed_model, capacity=4) +print(client.deployments.get(deployment.deployed_model).status) +``` + +2.0 响应信封已扁平化:直接读取 `fine_tune.status`、`uploaded.file_id`,不再经过 +`.output.status` 或 `.data["file_id"]`。顶层响应共享 `request_id` / `code` / +`message` / `is_error` / `status_code` / `headers` 字段。 + +### 5.9 Agentic RL + +```python +registered = client.rl.register_functions( + rollout_classpaths=["my_project.rollout:MyRollout"], + reward_classpaths=["my_project.reward:MyReward"], +) + +client.rl.test_functions( + instance_id=registered.rollout_instance_ids[0], + func_type="rollout", + input_data={"query": "1+1=?"}, +) + +uploaded = client.rl.upload_data( + training_files=["./rl_train.jsonl"], + validation_files=["./rl_val.jsonl"], +) + +job = client.rl.jobs.create( + config_path="./rl_config.yaml", + job_name="my-rl-job", +) +print(client.rl.jobs.fetch(job.job_id).status) +done = client.rl.jobs.wait(job) +``` + +`client.rl.*` 负责函数注册、测试和数据上传,`client.rl.jobs.*` 负责训练任务生命周期。 +`jobs.create/register_functions/test_functions/upload_data` 以及函数实例管理方法 +复用底层工作流,不接受 `timeout=`;其余 `jobs` 方法支持 `timeout=`。整个 +`client.rl` 的错误统一由 `DashScopeError` 覆盖,本地工作流失败通常表现为 +`SdkError`。 + +### 5.10 AgentStudio + +```python +from dashscope.agentstudio import user_message + +studio = client.agentstudio +agent = studio.agents.create( + name="demo", + model="qwen-plus", + system_prompt="你是助手", +) +session = studio.sessions.create(agent=agent.id, title="第一次对话") +studio.sessions.events.send(session.id, [user_message("你好")]) + +with studio.sessions.events.stream(session.id) as stream: + for text in stream.text_stream: + print(text, end="") +``` + +AgentStudio 在首次访问 `client.agentstudio` 时构造,包含 `agents`、`sessions`、 +`events`、`files`、`skills`、`vaults`、`environments`、`deployments` 等完整子 +资源。其返回模型沿用 AgentStudio 自身类型体系,不套用 2.0 的响应扁平化;通过 +`client.agentstudio` 调用时,异常统一翻译为 `dashscope.errors` 异常族。关闭主 +Client 会连带关闭已挂载的 AgentStudio Client。 + +## 6. 错误、重试与流式失败 + +### 6.1 统一异常族 + +```python +from dashscope.errors import ( + APIConnectionError, + APITimeoutError, + BadRequestError, + ContentFilterError, + DashScopeError, + NotFoundError, + RateLimitError, + SdkError, +) + +try: + response = client.chat.completions.create( + model="qwen-plus", + messages="你好", + ) +except ContentFilterError as error: + print("内容审核拦截", error.request_id) +except BadRequestError as error: + print("请求参数错误", error.code, error.message) +except RateLimitError as error: + print("限流", error.status_code, error.request_id) +except DashScopeError as error: + print(type(error).__name__, error.code, error.message, error.request_id) +``` + +九个 HTTP 状态类为 `BadRequestError`(400)、`AuthenticationError`(401)、 +`PermissionDeniedError`(403)、`NotFoundError`(404)、 +`RequestTooLargeError`(413)、`RateLimitError`(429)、 +`InternalServerError`(500)、`ServiceUnavailableError`(503)和 +`GatewayTimeoutError`(504)。`ContentFilterError` 是 `BadRequestError` 子类, +单独捕获时必须放在前面。 + +异常常用字段:`message` / `code` / `status_code` / `request_id` / `body` / +`headers`。连接错误、超时和 SDK 本地错误没有 HTTP 响应,`status_code` 为 +`None`;`SdkError` 另提供 `solution` / `allow_retry`。HTTP 非 2xx 与 HTTP 200 +但 body 带业务错误码的情况使用同一异常族。 + +### 6.2 自动重试与返回值模式 + +默认 `max_retries=2`,传输层按 `Retry-After` 或指数退避加抖动重试连接错误及 +408 / 409 / 429 / 500 / 502 / 503 / 504。认证、权限、资源不存在和参数错误不应 +盲目做应用层重复重试。 + +若需要 1.x 风格的返回值自查: + +```python +lenient = client.with_options(raise_on_error=False) +response = lenient.chat.completions.create(model="qwen-plus", messages="你好") +if response.is_error: + print(response.code, response.message, response.request_id) +``` + +`raise_on_error=False` 不会吞掉连接失败和超时,因为没有响应对象可返回。流式 +业务错误在返回值模式下产出一个 `is_error=True` 的对象后结束迭代。 + +## 7. 全局配置与运维 + +### 7.1 常用环境变量 + +| 环境变量 | 作用 | 默认 | +| --- | --- | --- | +| `DASHSCOPE_API_KEY` | API Key | — | +| `DASHSCOPE_API_KEY_FILE_PATH` | API Key 文件读取路径 | — | +| `DASHSCOPE_WORKSPACE` | workspace id | — | +| `DASHSCOPE_REGION` | 地域 | `cn-beijing` | +| `DASHSCOPE_BASE_URL` | 自定义 HTTP 端点 | — | +| `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | +| `DASHSCOPE_TIMEOUT` | 请求超时(秒) | — | +| `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` | +| `DASHSCOPE_CONFIG` | config.toml 路径 | `~/.dashscope/config.toml` | +| `DASHSCOPE_LOG` | 当前客户端日志级别 | `warning` | +| `DASHSCOPE_WEBSOCKET_BASE_URL` | ASR/TTS 等 websocket 端点覆盖 | 跟随配置 | +| `DASHSCOPE_AGENTSTUDIO_URL` / `AGENTSTUDIO_URL` | AgentStudio 自定义端点 | — | + +HTTP 连接池可通过 `DASHSCOPE_HTTP_MAX_CONNECTIONS`、 +`DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS`、 +`DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` 或对应 settings 字段调整。长驻并复用 Client +才能充分利用 keep-alive。 + +### 7.2 日志与 API Key 文件 + +当前客户端使用独立的 `dashscope._logging` logger 层级,配置它不会改变 1.x 根 +logger 的 level、handlers 或 filters: + +```python +dashscope.settings.log_level = "DEBUG" +``` + +根 `save_api_key(api_key, api_key_file_path=None)` 来自 `dashscope._config`,默认 +写入 `~/.dashscope/api_key`。它使用 UTF-8、展开 `~`,并为自定义路径创建缺失的 +父目录;`DASHSCOPE_API_KEY_FILE_PATH` 只影响读取,不改变默认写入位置。 + +## 8. 升级检查清单 + +1. 将所有新代码的 `dashscope.v2...` 导入改为根 `dashscope...`;不要保留已删除的 + `dashscope.v2` 兼容假设。 +2. 新代码使用 `Client` / `AsyncClient`,不要继续依赖模块级 `api_key` 和 + `.call()` 类方法组织新调用。 +3. 将响应检查从“先判断 `status_code`”调整为异常处理;若暂时保留返回值模式, + 必须检查 `is_error`。 +4. 将 chat 流式消费改为增量 delta;应用流式仍按 `incremental_output` 判断帧语义。 +5. 将图像、视频、批量向量、转写等长任务统一到 `create/fetch/wait`,并为等待 + 操作设置符合业务预算的 `timeout`。 +6. 检查多轮对话是否改为由应用维护 messages;不要依赖 1.x `Conversation` history。 +7. 旧 deep import 可继续通过别名使用;新代码可按公开入口导入。AgentStudio 与 + FineTune/Agentic RL 物理实现位于 `dashscope.applications`,原公开路径不变; + 不存在 `dashscope.reinforcement`。 +8. Agentic RL 不要混淆 2.0 `SubmitResult` 与 1.x `FineTune` 返回契约;优先迁移到 + `client.rl`。 +9. 视频调用若依赖 1.x `extend_prompt=True` 的旧默认行为,迁移后显式传 + `prompt_extend=True`。 +10. 使用 `client.close()` / `await aclient.aclose()` 或上下文管理器释放连接; + 音频 preview、AgentStudio 与 RL 分别按专题文档复核生命周期和参数约束。 + +## 9. 相关文档 + +- [2.0 接口映射核对表](dashscope-2.0-interfaces-mapping.md) +- [2.0 接口调用示例集](dashscope-2.0-interfaces-examples.md) +- [1.x → 2.0 迁移指南](dashscope-2.0-migration-guide.md) +- [2.0 设计规范](dashscope-2.0-design.md) +- [ASR:实时识别与文件转写](dashscope-2.0-asr-sdk.md) +- [TTS:一次性、流式与双向流合成](dashscope-2.0-tts-sdk.md) +- [Realtime:Omni、Qwen-Audio 与 Livetranslate](dashscope-2.0-realtime-sdk.md) +- [实时语音翻译](dashscope-2.0-translation-sdk.md) +- [语音管理面](dashscope-2.0-audio-management-sdk.md) +- [websocket 连接复用](dashscope-2.0-connection-reuse-sdk.md) diff --git a/md/dashscope-interfaces.md b/md/dashscope-interfaces.md new file mode 100644 index 00000000..626d80ce --- /dev/null +++ b/md/dashscope-interfaces.md @@ -0,0 +1,222 @@ +# dashscope 全量接口清单(SDK / 自带 CLI / Agentic) + +每个接口一句话说明,含主要输入。分三部分:dashscope SDK(Python)、dashscope 自带 CLI、Agentic(SDK + acli CLI)。 + +--- + +# 一、dashscope SDK(Python) + +## 文本 / 对话 + +| 接口 | 说明 | +| --- | --- | +| `Generation.call(model, messages, **kw)` | 文本生成/对话,输入模型名+消息列表,支持 stream、tools、temperature | +| `AioGeneration.call(...)` | `Generation` 的 asyncio 异步版 | +| `Conversation.call(model, prompt, history)` | 老式对话接口,history 为 `History`/`HistoryItem` | +| `CodeGeneration.call(model, messages)` | 代码生成,输入同 messages 格式 | +| `Understanding.call(model, prompt)` | 语义理解,输入模型+单句文本 | +| `Application.call(api_key, app_id, prompt, **kw)` | 百炼应用调用,输入应用 ID+用户提示 | + +## 多模态 + +| 接口 | 说明 | +| --- | --- | +| `MultiModalConversation.call(model, messages)` | 多模态对话,messages 元素含 text/image/audio/video URL | +| `AioMultiModalConversation.call(...)` | 异步版 | +| `ImageSynthesis.call/async_call/fetch/wait(model, prompt, n, size)` | 文生图(通义万相),异步任务四件套 | +| `VideoSynthesis.call/async_call/fetch/wait(model, prompt/img_url)` | 文/图生视频 | +| `MultiModalEmbedding.call(model, input=[...])` | 多模态向量化,input 为 ItemText/ItemImage/ItemAudio 列表 | +| `AioMultiModalEmbedding.call(...)` | 异步版 | + +## 向量 / 排序 / 分词 + +| 接口 | 说明 | +| --- | --- | +| `TextEmbedding.call(model, texts)` | 文本批量向量化,输入文本列表 | +| `BatchTextEmbedding`(call/fetch/wait) | 异步批量 embedding 任务 | +| `TextReRank.call(model, query, documents)` | 语义重排,输入查询+文档列表 | +| `AioTextReRank.call(...)` | 异步版 | +| `Tokenization.call(model, prompt)` | 统计 prompt 的 token 数 | + +## 语音 + +| 接口 | 说明 | +| --- | --- | +| `Recognition.start()/send_audio_frame(bytes)/call(file)/stop()` | Paraformer 实时语音识别(websocket 流式) | +| `Transcription.async_call/fetch/wait(model, file_urls)` | 录音文件批量转写,输入音频 URL 列表 | +| `audio.qwen_asr.QwenTranscription` | Qwen 音频转写 | +| `SpeechSynthesizer.call(model, text, sample_rate)` | sambert/cosyvoice 语音合成 | +| `audio.tts_v2.SpeechSynthesizer.call/streaming_call/streaming_complete/streaming_cancel` | CosyVoice2 流式合成,输入文本流 | +| `HttpSpeechSynthesizer.call(model, text)` | HTTP 一次性合成 | +| `audio.qwen_tts.SpeechSynthesizer` | Qwen3-TTS 合成 | +| `audio.qwen_omni.OmniRealtimeConversation` | Omni 实时多模态对话(websocket 回调式) | + +## 微调 / 部署 / 资源 + +| 接口 | 说明 | +| --- | --- | +| `FineTunes.call/cancel/list/get/delete/stream_events/logs/wait` | 微调任务管理,输入训练/验证文件 ID+基座模型 | +| `Deployments.call/list/get/delete/scale` | 微调模型部署与扩缩容,输入 model_name+capacity | +| `Models.list/get(page_no, page_size)` | 查询可用模型列表/详情 | +| `Files.upload(file_path, purpose)/list/get/delete` | 文件上传(微调用)及管理 | + +## Assistants API(百炼智能体)【确定下线:2.0 不再支持】 + +> ⚠️ Assistants/Threads/Messages/Runs/Steps 整个 API 族将下线,dashscope 2.0 不再迁移支持;托管 Agent 场景由 AgentStudio 承接。 + +| 接口 | 说明 | +| --- | --- | +| `Assistants.create/retrieve/list/update/delete` | 助手 CRUD,输入 name/model/instructions/tools | +| `Threads.create/retrieve/update/delete/create_and_run` | 会话线程 CRUD | +| `Messages.create/retrieve/list/update/delete` | 线程内消息 CRUD | +| `Runs.create/create_thread_and_run/retrieve/list/submit_tool_outputs/cancel/wait` | 运行调度与工具结果回填 | +| `Steps.list/retrieve` | 查询运行步骤 | + +## AgentStudio(托管 Agent,`dashscope.agentstudio`) + +独立子 SDK,已是 Client 实例化风格(`Client`/`AsyncClient` + 自有异常体系 + 流式)。 + +| 接口 | 说明 | +| --- | --- | +| `client.agents.create(name, model, ...)` | 创建托管 Agent | +| `client.sessions.create(agent=agent.id)` | 创建会话 | +| `client.sessions.events.send(session.id, [...])` | 发送事件(`user_message` / `user_tool_result` 等) | +| `client.sessions.events.stream(session.id)` | 流式订阅会话事件(上下文管理器,含 `session_status` 等事件类型) | + +## 全局配置 + +| 接口 | 说明 | +| --- | --- | +| `dashscope.api_key` / `save_api_key()` | 密钥设置与持久化 | +| `base_http_api_url` / `base_websocket_api_url` / `base_compatible_api_url` | 端点配置 | +| `close_shared_aio_session()` | 关闭共享异步 HTTP 会话 | + +--- + +# 二、dashscope 自带 CLI(`dashscope` 控制台脚本) + +Typer 实现,全局选项 `-k/--api-key `。 + +## 生成类 + +| 命令 | 说明 | +| --- | --- | +| `dashscope generation create --model --prompt ` | 文本生成,支持流式与采样参数 | +| `dashscope multimodal_conversation create` | 多模态对话,输入含图片/音频/视频 URL 的消息 | +| `dashscope image_synthesis create` | 文生图(万相),输入 prompt+尺寸/数量 | +| `dashscope image_generation create` | 图片生成(另一组模型) | +| `dashscope video_synthesis create` | 文/图生视频,输入 prompt 或首帧图 | +| `dashscope speech_synthesis create` | 语音合成,输入文本+发音人 | +| `dashscope transcription create` | 录音转写,输入音频 URL | +| `dashscope code_generation create` | 代码生成(hidden 别名 call) | +| `dashscope understanding create` | 语义理解(hidden) | +| `dashscope application create` | 百炼应用调用,输入 app_id+prompt | + +## 检索 / 工具类 + +| 命令 | 说明 | +| --- | --- | +| `dashscope embeddings create` | 文本向量化,输入文本列表 | +| `dashscope multimodal_embedding create` | 多模态向量化 | +| `dashscope rerank create` | 重排,输入 query+文档列表 | +| `dashscope tokenization create` | token 计数,输入 prompt | +| `dashscope models list\|get` | 模型列表/详情 | +| `dashscope files upload\|get\|list\|delete` | 文件上传与管理,upload 输入本地路径+purpose | +| `dashscope oss upload` | 上传本地文件到 OSS 换取可访问 URL | + +## 微调 / 部署 / 资源 + +| 命令 | 说明 | +| --- | --- | +| `dashscope ft create` | 创建微调任务,输入训练/验证文件 ID+基座模型 | +| `dashscope ft get\|list\|stream\|cancel\|delete` | 微调任务查询/列表/事件流/取消/删除 | +| `dashscope deployments create` | 部署微调模型,输入 model_name+capacity | +| `dashscope deployments get\|list\|scale\|delete` | 部署查询/列表/扩缩容/删除 | + +(`fine-tunes` 是 `ft` 的隐藏别名) + +## Agentic RL 微调(`dashscope rl`) + +| 命令 | 说明 | +| --- | --- | +| `dashscope rl register_functions --functions ` | 注册 RL 自定义奖励/校验函数 | +| `dashscope rl test_functions` | 测试已注册函数 | +| `dashscope rl upload_data --file ` | 上传 RL 训练数据集 | +| `dashscope rl run --model --dataset ...` | 启动 Agentic RL 训练任务 | +| `dashscope rl get\|cancel\|logs` | 任务查询/取消/日志(输入 job_id) | +| `dashscope rl list` | 列出 RL 训练任务 | + +(`agentic-rl` 是 `rl` 的隐藏别名) + +--- + +# 三、Agentic(dashscope.acli) + +## Agentic SDK(`dashscope.acli.sdk`,包根直接导出) + +| 接口 | 说明 | +| --- | --- | +| `load_config(provider, model, api_key, base_url)` | 加载 acli 配置,入参仅补缺省值(workspace/全局配置与环境变量优先级不变) | +| `create_agent(config, system_prompt, session_id)` | 创建可编程 Agent;session_id 指定时会话持久化到 `.acli/session//history.json` | +| `run_stream(prompt, config, system_prompt, session_id)` | 异步流式跑一轮 agent(含工具调用),yield 文本块 | +| `run_once(prompt, ...)` | 异步单轮执行,返回完整回复文本 | +| `run_once_sync(prompt, ...)` | `run_once` 的同步包装,适合脚本/CI | +| `run_interactive(system_prompt, app_name, default_model, default_provider, api_key, base_url, command, tui, ...)` | 启动交互式 REPL/TUI(acli 的程序化入口) | + +## acli CLI — 启动参数 + +| 参数 | 说明 | +| --- | --- | +| `dashscope [--tui\|--cli\|--dry-run]` | 交互模式选择:TUI / 纯 CLI / 只打印不执行 | +| `-c/--command ` | 一次性执行命令后退出 | +| `--protocol

` | 协议覆盖;`--max-turns N` 限制 agent 轮数 | +| `--version` / `-h` | 版本 / 帮助 | +| `dashscope example ...` | 运行内置示例项目 | + +## acli CLI — 会话与上下文 + +| 命令 | 说明 | +| --- | --- | +| `/session new\|list\|switch\|rename\|fork\|remove\|scene` | 多会话管理;fork 复制会话,scene 管理场景记忆 | +| `/clear` | 清空当前对话历史 | +| `/compress` | LLM 摘要压缩上下文 | +| `/summarize` | 总结当前任务并记录关键步骤 | +| `/history stats\|list\|search\|export\|clear` | 历史统计/全文搜索/导出 | +| `/memory list\|search\|remove\|clear` | 长期记忆条目管理 | +| `/profile list\|search\|add\|remove\|clear` | 用户画像管理 | + +## acli CLI — 模型与能力 + +| 命令 | 说明 | +| --- | --- | +| `/provider` / `/model` | 切换模型服务商/模型 | +| `/capability` | 能力开关目录 | +| `/skill` | 技能管理 | +| `/subagents list\|reload\|enable\|disable\|config` | 子代理管理 | +| `/mcp list\|add\|remove` | MCP 服务连接(stdio 经 config.toml 配置) | +| `/trust` / `/rule` / `/directives` | 权限、规则、指令管理 | + +## acli CLI — 输入输出 + +| 命令 | 说明 | +| --- | --- | +| `/voice on\|off\|model\|silence\|max\|threshold` | 语音输入配置 | +| `/tts on\|off\|speed\|voice\|say\|last` | 语音播报(CosyVoice) | +| `/camera capture\|record` | 拍照/录像输入 | +| `/copy` / `/save [path]` | 复制/保存最近一条回复 | +| `/undo` | 撤销最近一次文件写入/删除(checkpoint) | +| `/theme` | 主题设置;`/json on\|off` JSON 输出 | +| `/log` / `/debug` / `/trace` | 日志与跟踪 | + +## acli CLI — 系统与运维 + +| 命令 | 说明 | +| --- | --- | +| `/cron add\|list\|remove\|pause\|resume` | 定时任务 | +| `/audit recent\|query\|clear` | 审计日志 | +| `/report` | 生成 trace 性能报告 | +| `/setup` | 初始化配置 | +| `/dev` | 模型注册与扩展指南 | +| `/example [restore]` | 示例项目列表/恢复 | +| `/privacy` | 隐私设置 | +| `/info` / `/stats` / `/help` / `/exit` | 运行时信息/统计/帮助/退出 | diff --git a/md/dashscope-sdk-expert-intro.md b/md/dashscope-sdk-expert-intro.md new file mode 100644 index 00000000..612ac6c6 --- /dev/null +++ b/md/dashscope-sdk-expert-intro.md @@ -0,0 +1,92 @@ +# DashScope SDK Expert 简介 + +DashScope SDK Expert 是一个面向 **DashScope Python SDK / Java SDK** 与 **阿里云百炼(Model Studio)** 的智能开发助手。它可以提供可运行的代码示例、解释 API 参数与错误码、阅读你的代码定位问题,并直接执行命令验证 SDK 行为——目标是让你**不翻文档、不手写样板代码**,只描述需求就能完成 SDK 开发与问题排查。 + +## 快速开始 + +1. **安装 / 升级 SDK**:DashScope SDK Expert 内置于 DashScope Python SDK,先安装或升级到最新版: + + ```bash + pip install -U dashscope + ``` + +2. **启动 CLI**:在终端执行 `dashscope` 命令,进入交互式会话。 +3. **查看帮助**:在会话中输入 `/help`,即可查看当前可用的全部命令与技能(skill)列表。 +4. **按需提问**:直接用自然语言提问即可,例如: + - “用 qwen-plus 写一个流式对话示例” + - “`InvalidApiKey` 这个错误怎么解决?” + - “帮我看下 `my_app.py` 为什么调用失败” + +> 提示:Expert 会交叉校验本机已安装的 dashscope 版本——版本过旧时先执行 `pip install -U dashscope` 升级,即可获得与最新 SDK 一致的答案。除 `/help` 外,还可通过 `/skill <名称>` 单行触发专项技能(如 `/skill text-generation`、`/skill error-code <错误码>`),具体可用列表以 `/help` 输出为准。 + +## 覆盖能力一览 + +| 领域 | 说明 | +|---|---| +| 文本生成 | Generation、OpenAI 兼容接口 | +| 多模态 | MultiModalConversation、图像合成、视频合成 | +| 语音 | 语音合成(TTS)、录音文件转写(ASR) | +| 检索 | 文本/批量/多模态向量、Rerank、RAG | +| 微调与部署 | SFT/CPT/DPO、独立推理端点、强化学习 | +| Agent | 托管 Agent API、应用调用、Assistants、插件与 MCP | +| CLI | dashscope 命令行全部命令组 | + +--- + +## 最佳实践一:只提需求,不读文档,直接产出可运行代码 + +**痛点**:DashScope SDK 接口多、版本迭代快,开发者经常要翻文档确认参数名、输入格式和模型列表。 + +**做法**:把需求直接用自然语言描述给 DashScope SDK Expert,它会: + +1. 匹配内置的 SDK 知识库(模型列表、参数、输入输出结构、错误码); +2. **交叉校验本机已安装的 SDK 版本**,确保给出的签名与用法与你环境一致(版本不一致时会主动提醒); +3. 直接生成可运行代码,并用 `run_command` 实际执行展示输出。 + +**示例对话**: + +> 用户:写一个 Python 示例,用 qwen-plus 流式输出,统计 token 消耗。 + +助手直接给出完整可运行脚本(messages 格式 + `stream=True` + usage 解析),无需你查任何文档。 + +**小技巧**: + +- 指定语言或框架:“用 Java SDK 实现”“封装成 FastAPI 接口”; +- 指定版本:“我们用的是 dashscope 1.20.x,这个参数支持吗?”——助手会核对安装版本后回答; +- 追问细节:“这个参数的取值范围是什么?”“还有哪些可选模型?” + +--- + +## 最佳实践二:排查问题时,让它自动定位并修复 Bug + +**痛点**:调用报错时,错误码含义不明、报错堆栈指向 SDK 内部,难以判断是代码问题还是参数问题。 + +**做法**:按以下任一路径交给 DashScope SDK Expert: + +1. **贴错误码/报错信息**:如 `Throttling.RateQuota`、`InvalidParameter`,助手会解释含义并给出可执行的修复建议(如调整并发、修改参数格式、申请配额)。 +2. **给出出问题的代码文件路径**:助手会自动读取文件,诊断 SDK 调用问题(常见根因:混用旧版 prompt 格式、messages 结构错误、api_key/workspace 未配置、异步任务未正确轮询等)。 +3. **自动修复闭环**:定位根因后,助手会直接修改文件,并运行测试/脚本验证修复效果,形成 **读取 → 诊断 → 修复 → 验证** 的完整流程。 + +**示例对话**: + +> 用户:运行 `gen.py` 报 `InvalidParameter: url error`,帮我修一下。 + +助手读取 `gen.py` → 发现传入了本地路径而非可访问的 URL → 修改为正确的输入方式 → 重新运行确认通过。 + +**小技巧**: + +- 报错时附上 `request_id`,便于与服务端日志对应; +- 复杂问题可先让助手“解释这段代码逻辑”,确认理解一致后再修复; +- 遇到 SDK 本身的缺陷或未满足的需求,助手会引导你到官方仓库提 Issue: + - Python SDK:https://github.com/dashscope/dashscope-sdk-python/issues + - Java SDK:https://github.com/dashscope/dashscope-sdk-java/issues + +--- + +## 小结 + +| 场景 | 一句话用法 | +|---|---| +| 开始使用 | 执行 `dashscope`,输入 `/help` 查看全部能力 | +| 开发 | 直接描述需求 → 拿到校验过版本的可运行代码 | +| 排障 | 贴报错或给文件路径 → 自动诊断、修复并验证 | diff --git a/md/dashscope-v2-refactor-tasks.md b/md/dashscope-v2-refactor-tasks.md new file mode 100644 index 00000000..37674fe8 --- /dev/null +++ b/md/dashscope-v2-refactor-tasks.md @@ -0,0 +1,195 @@ +# DashScope v2 借鉴 Ark 重构任务列表 + +> 目标仓库:`/Users/zhansheng.lzs/ali/pro/ptm/moe-dashscope-sdk-python`(`dashscope/v2/`) +> 依据文档:`ark-vs-dashscope-v2-comparison.md` +> 拆分原则:**一个任务一个 PR**,均可独立合并;除标注的依赖外无隐式耦合。 +> 全局约束:全部改动保持向后兼容(只新增 kwarg / 子类 / 模块,不改既有签名语义);测试放 `tests/v2/`,风格沿用现有 `httpx.MockTransport` 注入。 + +## 任务总览 + +| # | 任务 | 优先级 | 依赖 | 工作量 | 影响面 | +|---|---|---|---|---|---| +| T1 | `RequestOptions` 统一请求定制通道 | ★★★ | 无 | M | transport + 全部 resources | +| T2 | 容错式模型构造 `parse_model()` | ★★★ | T3 | M | types + 全部 resources | +| T3 | `APIResponseValidationError` 异常类 | ★★ | 无 | S | errors.py | +| T4 | 分页抽象 `BasePage` | ★★ | 无 | M | pagination.py + 3 个 list 资源 | +| T5 | 响应封装 `APIResponse.parse(to=)` | ★★ | T1、T2 | L | transport + resources 入口 | +| T6 | 每请求 `x-dashscope-client-request-id` | ★ | 无 | S | _ident.py + http.py | +| T7 | async 孪生漂移治理(unasync) | ★ | 无 | L(分阶段) | 构建/CI | + +推荐执行顺序:**T3 → T6 → T1 → T2 → T4 → T5 → T7**(先小后大,T5 依赖 T1/T2,T7 独立排最后)。 + +--- + +## T1:`RequestOptions` 统一请求定制通道 + +**目标**:借鉴 ark `_request_options.py:44`,给每个资源方法一致的逃生舱 `extra_headers / extra_query / extra_body`,并支持按请求覆盖 `max_retries`。 + +**现状痛点**: +- `SyncTransport.request()` 只接受 `headers/params/timeout/api_key/workspace`(`transport/http.py:156-172`),无 extra 通道;网关要加一个实验性 header 必须改 SDK 发版。 +- `max_retries` 只有 client 级(`http.py:149`),无法按请求关闭(如长任务提交不想重试)。 + +**改动范围**: +1. 新增 `dashscope/v2/_request_options.py`: + - `RequestOptions` dataclass:`extra_headers: Mapping[str,str]`、`extra_query: Mapping[str,Any]`、`extra_body: Mapping[str,Any]`、`timeout`、`max_retries: Optional[int]`。 + - 深合并函数 `_deep_merge(base: dict, extra: dict)`(对齐 ark `_base_client.py:236-243` 语义:dict 递归合并,其余覆盖)。 +2. `transport/http.py`: + - `SyncTransport.request()` / `AsyncTransport.request()` / `open_stream()` 增加 `options: Optional[RequestOptions] = None`。 + - header 合并顺序固定为:`sdk_headers(module)` < auth(api_key/workspace)< 调用方 `headers` < `extra_headers`(后者胜)。 + - `extra_query` 并入 `params`;`extra_body` 与 `json` dict 深合并(`json` 非 dict 时忽略并告警)。 + - 重试循环用 `options.max_retries if options and options.max_retries is not None else self._max_retries`(`http.py:174`、`http.py:223` 两处)。 +3. resources:方法签名统一追加 `extra_headers=None, extra_query=None, extra_body=None`(保留既有 `timeout/api_key/workspace` kwarg 不动);内部组装 `RequestOptions` 传给 transport。从 `chat/completions.py`、`embeddings.py`、`models.py`、`files.py`、`_jobs.py` 做起,其余资源后续跟进(可在 PR 内一次改完,模式相同)。 + +**验收标准**: +- `tests/v2/test_request_options.py`: + - extra_headers 出现在最终请求头且覆盖默认值; + - extra_query 并入 query string;extra_body 深合并(嵌套 dict 合并、标量覆盖); + - `max_retries=0` 时 503 不重试;`max_retries=1` 时重试一次;不设置时走 client 默认; + - 优先级冲突用例:调用方 headers 与 extra_headers 同键时 extra_headers 胜。 + +**风险**:低。纯新增参数;合并顺序需在 docstring 写死,避免后续歧义。 + +--- + +## T2:容错式模型构造 `parse_model()` + +**目标**:借鉴 ark `_models.py` 的 `construct_type` 哲学——服务端新增枚举值、判别字段新值、结构小漂移时不炸用户进程。 + +**现状痛点**:resources 直接 `Model(**data)` 构造(如 `models.py:67-73`),pydantic v2 校验失败会把原始 `ValidationError` 抛给用户;`extra="allow"` 只兜住了"多字段",兜不住"字段值类型漂移"和"Literal/判别新值"(`types/messages.py:30-55` 的 `extra="forbid"` + Literal 判别最脆)。 + +**改动范围**: +1. 新增 `dashscope/v2/types/_parse.py`: + ```python + def parse_model(cls: type[_M], data: Mapping[str, Any], *, strict: bool = False) -> _M + ``` + - 默认路径:`model_validate(data)`;失败时降级为 `cls.model_construct()` + 逐字段 best-effort 赋值(能 validate 的字段 validate,不能的原样塞入),并 `logger.debug` 记录漂移详情(模型名、字段、原始值类型)。 + - `strict=True`:校验失败抛 T3 的 `APIResponseValidationError`(保留 response body 供排障)。 + - 不移植 ark 的 700 行递归强转器,也不做 v1 双栈——pydantic v2 的 `model_construct` 已提供 90% 价值。 +2. 替换 resources 中的裸构造调用(`ModelList(**...)`、`Model(**models[0], ...)` 等,典型位置 `resources/models.py:67-73,157-161`、`resources/files.py`、`resources/_jobs.py`、`resources/chat/completions.py` 的 chunk 解析路径)。 +3. 配置项:`Config` / `Settings` 增加 `strict_validation: bool = False`(env `DASHSCOPE_STRICT_VALIDATION`),让用户可切回严格模式排障。 + +**验收标准**: +- `tests/v2/test_parse_model.py`: + - 未知枚举值、未知 message part `type`、字段类型漂移(int 给了 str)三种场景下默认不抛错、字段可读取、其余字段正常校验; + - `strict=True` 或 `strict_validation` 开启时抛 `APIResponseValidationError`,异常带 body; + - 现有 `tests/v2/` 全部用例不回归。 + +**风险**:中。容错会掩盖真实的服务端回归——必须有 debug 日志 + strict 开关双保险;CHANGELOG 明确行为变化。 + +--- + +## T3:`APIResponseValidationError` 异常类 + +**目标**:把"SDK 解析失败"与"API 调用失败"分成两类,借鉴 ark `_exceptions.py` 的 `ArkAPIResponseValidationError`。 + +**改动范围**: +1. `errors.py` 新增: + ```python + class APIResponseValidationError(DashScopeError): + """Response body failed SDK-side validation (strict mode).""" + ``` + 携带 `response`(httpx.Response 或 None)、`body`、`cause`(原始 `ValidationError`)。 +2. 评估是否在 `_error_registry.py` 的 `SDK_ERRORS` 注册条目(`sdk.response_validation_error`,带 solution 文案)——与错误目录的"SDK 侧错误"约定对齐;若注册则改为继承 `SdkError`。 +3. `errors.py` 顶部 docstring 的"third source"段落补充该类说明。 + +**验收标准**:`tests/v2/test_errors.py` 新增:异常可捕获、`str()` 含 request_id/body 摘要、`isinstance(err, DashScopeError)` 成立、(若注册目录)目录不变量测试通过。 + +**风险**:低。新增类,无既有行为变化。 + +--- + +## T4:分页抽象 `BasePage` + +**目标**:借鉴 ark `pagination.py`,给 list 接口统一的可迭代分页对象。 + +**现状痛点**:`models.list` 返回裸 `ModelList`(`models.py:48-52`,`page_no/page_size/total_count`),翻页要用户手工循环;`files.list`、`jobs.list`(`_jobs.py:182-207`)同构但各写各的。 + +**改动范围**: +1. 新增 `dashscope/v2/pagination.py`: + - `BasePage(Generic[T], ResponseBase)`:字段 `page_no/page_size/total_count`;方法 `has_next_page`(`page_no * page_size < total_count`)、`next_page()`(回调拉下一页,末页返回 None)、`__iter__`(委托 `self.items`)、`iter_pages()`(生成器跨页)。 + - `SyncPage` / `AsyncPage` 两个实现,构造时注入"取页回调"(`Callable[[int, int], BasePage]`),回调由资源方法闭包提供。 +2. 资源接入(保持返回类型向后兼容——让现有 List 模型继承/替换为 Page 子类): + - `models.list` → `ModelList(BasePage[Model])`; + - `files.list` → `FileList` 同理; + - `_jobs.py` 的 `JobList` 同理(注意 `UNKNOWN` 终态语义不受影响)。 +3. `provider` 客户端过滤(`models.py:122-125`)与分页的交互写进 docstring:过滤只作用于当前页,`total_count` 语义不变。 + +**验收标准**: +- `tests/v2/test_pagination.py`:`for m in page` 迭代当前页;`iter_pages()` 跨 3 页聚合且请求参数 page_no 递增;`total_count` 边界(整除、不足一页、空页)正确停止;`next_page()` 末页返回 None。 + +**风险**:低-中。`ModelList` 改继承关系是公开 API 变化但向后兼容(字段全保留);注意 `model_dump()` 序列化结果不要多出内部字段。 + +--- + +## T5:响应封装 `APIResponse.parse(to=)` 与 raw 访问 + +**目标**:借鉴 ark `_response.py`,收敛目前散装的 `check_response` + 手工解析 + `with_headers` 三件套,并提供记忆化的按类型解析。 + +**现状痛点**:每个资源方法重复"request → check_response → response.json() → 构造 → with_headers"五步(`models.py:111-126` 是典型),共十几处;用户拿不到原始 response(无法读非标准响应头、无法自行二次解析)。 + +**改动范围**: +1. 新增 `dashscope/v2/_response.py`: + - `APIResponse`:包装 `httpx.Response` + client 引用;属性 `headers / status_code / request_id / elapsed`;方法 `parse(cast_to=None)`——`cast_to` 为 None 时返回 `response.json()`,否则走 T2 的 `parse_model`,解析结果按 `cast_to` 缓存在 `_parsed_by_type: dict`(对齐 ark `_response.py:305-321` 的记忆化语义)。 + - 同步/异步共用一个类(httpx.Response 在 async 路径同样是已物化的对象,无需孪生)。 +2. transport 新增 `request_api()` 返回 `APIResponse`(内部就是 `request()` + 包装,不重写重试逻辑);`open_stream()` 的 `Stream` 对象补 `headers` 属性(`transport/sse.py` 已有 `Stream.headers`,对齐即可)。 +3. resources 改造分两小步(可同 PR 内两个 commit): + - a) 内部路径切到 `request_api()` + `resp.parse(Model)`,删除重复的 `with_headers` 调用(`parse_model` 内统一挂 headers); + - b) 资源方法增加 `raw: bool = False` kwarg(或 `with_raw_response` 风格),为 True 时返回 `APIResponse` 而非模型。 +4. `with_headers`(`types/common.py:100`)保留为公开工具不删(websocket 来源模型仍在用),docstring 标注 HTTP 路径已内建。 + +**验收标准**: +- `tests/v2/test_response.py`:`parse()` 两次调用返回同一缓存对象;`parse(OtherModel)` 按类型分别缓存;`request_id` 从 `x-request-id` 头提取;`raw=True` 时资源方法返回 `APIResponse` 且不抛 `check_response` 错误。 +- `tests/v2/` 存量用例不回归(重点:headers 字段在异常与成功路径行为不变,`types/common.py:100-116` 文档承诺的语义保持)。 + +**风险**:中。触碰所有资源的公共路径;建议先只切 `models` + `embeddings` 两个资源验证模式,再全量推广(可拆 T5a/T5b 两个 PR)。 + +--- + +## T6:每请求 `x-dashscope-client-request-id` + +**目标**:借鉴 ark `_base_client.py:195` 的 `X-Client-Request-Id`,给每次请求一个客户端生成的 UUID,与服务端 `x-request-id` 互补(网络层失败、请求未到达网关时也有 ID 可查)。 + +**改动范围**: +1. `transport/_ident.py`:新增 `CLIENT_REQUEST_ID_HEADER = "x-dashscope-client-request-id"` 与 `client_request_id() -> str`(`uuid.uuid4().hex`);同样受 `DASHSCOPE_DISABLE_SDK_HEADERS` 开关控制。 +2. `transport/http.py:184` 附近:在每次 `self._client.request()` 调用时生成并注入(**注意放重试循环内每次 attempt 生成一次,或循环外一次**——建议循环外一次:重试属于同一逻辑请求,与服务端 x-request-id 的关联更稳定;决策写进注释)。 +3. debug 日志(`http.py:214`)并排输出 client_request_id 与 x-request-id。 +4. 错误对象:连接层错误(`APIConnectionError`/`APITimeoutError`)无服务端 request_id 时,把 client_request_id 填入 `err.request_id` 便于排障(`http.py:199` 的 `_map_transport_error` 路径)。 + +**验收标准**:`tests/v2/test_ident.py`:header 存在且为 32 位 hex;同客户端两次请求 ID 不同;`DASHSCOPE_DISABLE_SDK_HEADERS` 设置后 header 消失;连接异常时 `err.request_id` 非空。 + +**风险**:低。网关在服务网格中可能剥离未知 header——不影响功能(纯客户端侧排障增强),但先与网关同学确认该 header 名未被占用。 + +--- + +## T7:async 孪生漂移治理(分阶段) + +**目标**:治理 sync/async 人肉复制的漂移风险(`client.py:51 vs 272`、`http.py:128 vs 320`、`_jobs.py`、`resources/*` 全套孪生)。ark 的教训是:机器复制至少保证一致,人肉复制必然漂移。**不上 Stainless**,用轻量方案。 + +**阶段一(本任务内必做):漂移检测** +1. 新增 `tests/v2/test_sync_async_parity.py`:对指定的成对模块(`client` / `transport.http` / `_jobs` / `resources.models` 等),用 `ast` 解析后做归一化比较(剔除 `async/await` 关键字、`Async` 类名前缀、`__aenter__/__aexit__`),方法集合与参数列表必须一致。 +2. CI 挂为必过项——先止血,保证新增方法两端同步。 + +**阶段二(试点):unasync 生成** +1. 评估并引入 [`unasync`](https://github.com/python-trio/unasync)(或等价脚本):以 sync 代码为源,token 替换规则生成 async(`AsyncModels`→`Models` 逆规则、`await `→``、`async def`→`def`)。 +2. 选 1 个简单资源试点(建议 `models.py`:无流式、无 WS,孪生结构最规整,`models.py:91-228`);生成物不手写、不进 review diff(pre-commit/CI 生成并校验一致)。 +3. 试点稳定后推广到 `files.py`、`_jobs.py` 等。 + +**不在范围内**:`transport/ws.py` / `ws_connection.py`(纯同步线程模型,无 async 孪生);`open_stream()`(SSE 的 async 生成规则涉及 `async with`/`__aiter__`,试点后再定规则)。 + +**验收标准**: +- 阶段一:parity 测试能捕获人为制造的漂移(删一个 async 方法,测试红)。 +- 阶段二:试点模块的 async 文件头标注 "GENERATED — DO NOT EDIT",CI 校验生成物与 sync 源一致。 + +**风险**:中-高。触及构建流程;阶段一零风险先行,阶段二试点失败可回退,不影响已合并的阶段一。 + +--- + +## 附:明确不做的事 + +| 事项 | 原因 | +|---|---| +| 引入 Stainless 全量代码生成 | 会冲掉 protocols/Job/WebSocket/OSS 直传等手写差异化能力;ark 自己的生成代码也存在双客户端复制、双序列化约定问题 | +| pydantic v1 兼容层 | v2 只支持 pydantic v2,ark 的 v1 路径已是死代码 | +| `_legacy_response.py` 式新旧双实现长期并存 | ark 的前车之鉴(1000+ 行历史包袱);T5 要求一步切干净 | +| POST 重试幂等区分 | ark 同样没做(`idempotency_key` 是死字段);需网关先支持幂等键,单独立项 | +| mypy/pyright 类型门禁 | 属于两个 SDK 的共同短板,不在"借鉴 ark"范围;如需治理另立任务 | diff --git a/md/error.md b/md/error.md new file mode 100644 index 00000000..b1cd272a --- /dev/null +++ b/md/error.md @@ -0,0 +1,356 @@ +# DashScope Python SDK — HTTP API 错误码/消息透传审查 + +## 目标 + +审查 SDK 是否对 HTTP API 输出的错误码(code)和错误消息(message)做了修改,确保原样透传。 + +## 架构概览 + +SDK 有 **两套独立的错误处理管线**: + +1. **核心 SDK 管线** — `dashscope/api_entities/`、`dashscope/common/`、`dashscope/client/`(用于 `dashscope.Generation`、`dashscope.Application` 等) +2. **AgentStudio 管线** — `dashscope/agentstudio/`(AgentStudio 子 SDK,使用 `httpx`) + +--- + +## 正常透传的路径(GOOD) + +以下代码路径都**正确保留**了 API 返回的 `code` 和 `message`: + +### 1. `_handle_error_message()` — 中央 JSON 错误解析器 + +**文件:** `dashscope/common/utils.py`, 行 270–291 + +```python +def _handle_error_message(error, status_code, flattened_output, headers): + code = None + msg = "" + request_id = "" + if flattened_output: + error["status_code"] = status_code + return error + if "message" in error: + msg = error["message"] + if "msg" in error: + msg = error["msg"] + if "code" in error: + code = error["code"] + if "request_id" in error: + request_id = error["request_id"] + return DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + code=code, + message=msg, + headers=headers, + ) +``` + +**结论:** 直接从 API JSON body 提取 `code` 和 `message`,原样透传。也处理了 `"msg"` 作为替代键。 + +### 2. `_handle_http_failed_response()` — sync 非 2xx 处理器 + +**文件:** `dashscope/common/utils.py`, 行 294–336 + +对 JSON 响应完全委托给 `_handle_error_message()`。对非 JSON/非 SSE 响应,`code="Unknown"` 但保留原始 body 作为 `message`。 + +### 3. `_handle_aiohttp_failed_response()` — async 非 2xx 处理器 + +**文件:** `dashscope/common/utils.py`, 行 361–408 + +与 sync 版本相同逻辑,JSON 和 SSE 内容类型委托给 `_handle_error_message()`。 + +### 4. HttpRequest SSE 流错误(sync) + +**文件:** `dashscope/api_entities/http_request.py`, 行 409–418 + +```python +if is_error: + yield DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + output=None, + code=msg["code"] if "code" in msg else None, + message=msg["message"] if "message" in msg else None, + headers=headers, + ) +``` + +直接从 SSE event JSON 提取。 + +### 5. HttpRequest SSE 流错误(async) + +**文件:** `dashscope/api_entities/http_request.py`, 行 299–306 + +```python +if is_error: + yield DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + code=msg["code"], + message=msg["message"], + headers=headers, + ) +``` + +### 6. AioHttpRequest SSE 流错误(async) + +**文件:** `dashscope/api_entities/aiohttp_request.py`, 行 172–178 + +```python +if is_error: + yield DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + code=msg["code"], + message=msg["message"], + ) +``` + +### 7. AioHttpRequest 非 2xx JSON 错误 + +**文件:** `dashscope/api_entities/aiohttp_request.py`, 行 222–246 + +```python +if "application/json" in response.content_type: + error = await response.json() + yield DashScopeAPIResponse( + request_id=request_id, + status_code=response.status, + code=error["code"], + message=message, + ) +``` + +### 8. `_handle_http_stream_response()` — sync stream 错误路径 + +**文件:** `dashscope/common/utils.py`, 行 483–493 + +```python +msg = json.loads(event.data) +yield event.eventType, DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + output=None, + code=msg["code"] if "code" in msg else None, + message=msg["message"] if "message" in msg else None, + headers=headers, +) +``` + +### 9. `DashScopeAPIResponse` — 响应容器 + +**文件:** `dashscope/api_entities/dashscope_response.py`, 行 80–122 + +纯数据容器,不做任何转换。所有 `from_api_response()` 类方法(约 15 个响应类型)都是直接 `code=api_response.code, message=api_response.message` 拷贝。 + +### 10. AgentStudio `from_response()` — 异常构造 + +**文件:** `dashscope/agentstudio/exceptions.py`, 行 177–244 + +当 API 响应 body 包含 `error.code` 和 `error.message` 时,原样保留。fallback(`f"HTTP {status_code}"`、状态码映射)仅在响应 body 缺少结构化错误信息时触发。 + +--- + +## 发现的问题 + +### 问题 1 — StreamEventMixin:错误码和消息被丢弃(HIGH) + +**文件:** `dashscope/client/base_api.py`, 行 1557–1565 + +```python +for is_error, status_code, data in cls._handle_stream(response): + if is_error: + yield DashScopeAPIResponse( + request_id=request_id, + status_code=status_code, + output=None, + code="", # ← 原始 code 丢弃 + message="", # ← 原始 message 丢弃 + ) +``` + +**影响:** 使用 `StreamEventMixin.stream_events()` 的 API(如任务日志流)在流错误时,用户**完全看不到**错误信息。`data` 变量中有完整的 JSON 错误但没有解析。 + +**对比正确代码:** `http_request.py:409-418`(相同模式,但正确解析了 `msg["code"]` 和 `msg["message"]`)。 + +### 问题 2 — WebSocket 握手:错误消息被替换(MODERATE) + +**文件:** `dashscope/api_entities/websocket_request.py`, 行 204–218 + +```python +except aiohttp.WSServerHandshakeError as e: + code = e.status + msg = e.message + if e.status in [HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED]: + msg = "Unauthorized, your api-key is invalid!" # ← 替换了服务端消息 + elif e.status == HTTPStatus.SERVICE_UNAVAILABLE: + msg = SERVICE_503_MESSAGE # ← 替换了服务端消息 + yield DashScopeAPIResponse( + request_id=task_id, + status_code=code, + code=code, # ← HTTP 状态码作为 code,非 API code + message=msg, + ) +``` + +**影响:** WebSocket 鉴权/服务失败时,用户丢失了服务端提供的诊断信息。`code` 被设为 HTTP 状态码整数,而非 API 自身的错误码。 + +### 问题 3 — `iter_over_async` 桥接:包装为自定义格式(LOW) + +**文件:** `dashscope/common/utils.py`, 行 124–131 + +```python +def iter_over_async(ait): + if error is not None: + yield DashScopeAPIResponse( + -1, + "", + "Unknown", + message=f"Error type: {type(error)}, message: {error}", + ) +``` + +**影响:** async→sync 转换异常时,`code="Unknown"`,message 被拼接为自定义格式字符串。这是传输层故障而非 API 错误修改,但会丢失内部异常携带的结构化错误信息。 + +### 问题 4 — 非 JSON 响应:`code="Unknown"`(LOW) + +**文件:** 多处 — `utils.py:215, 322, 333, 388, 405`、`aiohttp_request.py:168, 252`、`http_request.py:294, 404` + +当 API 返回非 JSON/非 SSE 响应(如 HTML 错误页、网关错误)时,`code` 设为 `"Unknown"`,原始 body 保留在 `message` 中。作为 fallback 可接受,但可考虑改为 `code=None` 以明确表示 API 未提供错误码。 + +--- + +## 汇总表 + +| 位置 | Code | Message | 结论 | +|---|---|---|---| +| `_handle_error_message()` (utils.py:270) | 保留 | 保留 | GOOD | +| `_handle_http_failed_response()` JSON (utils.py:300) | 保留 | 保留 | GOOD | +| `_handle_aiohttp_failed_response()` JSON (utils.py:367) | 保留 | 保留 | GOOD | +| HttpRequest SSE sync (http_request.py:409) | 保留 | 保留 | GOOD | +| HttpRequest SSE async (http_request.py:299) | 保留 | 保留 | GOOD | +| AioHttp SSE async (aiohttp_request.py:172) | 保留 | 保留 | GOOD | +| AioHttp 非 2xx JSON (aiohttp_request.py:241) | 保留 | 保留 | GOOD | +| `_handle_http_stream_response()` (utils.py:483) | 保留 | 保留 | GOOD | +| 所有 `from_api_response()` (dashscope_response.py) | 保留 | 保留 | GOOD | +| AgentStudio `from_response()` (exceptions.py:177) | 保留 | 保留 | GOOD | +| **StreamEventMixin** SSE (base_api.py:1559) | **丢弃 → `""`** | **丢弃 → `""`** | **PROBLEM (HIGH)** | +| **WebSocket 握手** (websocket_request.py:207) | HTTP 状态码 | **替换为硬编码字符串** | **PROBLEM (MODERATE)** | +| `iter_over_async` (utils.py:125) | `"Unknown"` | 自定义格式包装 | Minor | +| 非 JSON fallback (多文件) | `"Unknown"` | 保留原始 body | 可接受的 fallback | + +--- + +## 修复建议 + +1. **修复 StreamEventMixin(HIGH)** — `client/base_api.py:1559`:解析 `data` 为 JSON(参考 `http_request.py:409-418` 的做法),而非设置 `code=""` 和 `message=""`。 + +2. **修复 WebSocket 握手(MODERATE)** — `websocket_request.py:208`:保留 `e.message`(服务端消息)而非替换。如需友好提示,应追加而非替换。 + +3. **考虑移除 `code="Unknown"` fallback(LOW)** — 对真正无结构的响应可接受,但可改为 `code=None` 明确表示 API 未提供错误码。 + +--- + +## CLI 层审查(`dashscope/cli/`,分支 dev/sdk-cli-1) + +### 核心错误处理机制 + +CLI 层有 3 个统一的错误处理函数,所有命令模块共用: + +#### 1. `print_failed_message(rsp)` — 通用错误打印 + +**文件:** `dashscope/cli/common.py`, 行 35–41 + +```python +def print_failed_message(rsp): + err_console.print( + f"[red]Failed[/red] request_id: {rsp.request_id}, " + f"status_code: {rsp.status_code}, " + f"code: {rsp.code}, message: {rsp.message}", + ) +``` + +**结论:** 直接输出 `rsp.code` 和 `rsp.message`,原样透传。 + +#### 2. `ensure_ok(rsp)` — 统一响应检查与退出 + +**文件:** `dashscope/cli/common.py`, 行 44–91 + +```python +def ensure_ok(rsp, check_business_error=True): + if rsp.status_code != HTTPStatus.OK: + print_failed_message(rsp) # ← 透传 + raise typer.Exit(1) + + if check_business_error: + error_code = output.get("code") + message = output.get("message", "Unknown error") + if error_code and error_code != "": + err_console.print( + f"[red]Business Error[/red] ... code: {error_code}, message: {message}", + ) + raise typer.Exit(1) + return output +``` + +**结论:** HTTP 错误和业务错误都直接透传原始 code/message。`"Unknown error"` 仅作为 message 的 fallback(当 API 未返回 message 时)。 + +#### 3. `handle_sdk_error(action)` — SDK 异常装饰器 + +**文件:** `dashscope/cli/common.py`, 行 110–125 + +```python +def handle_sdk_error(action: str): + def decorator(command_function): + @wraps(command_function) + def wrapper(*args, **kwargs): + try: + return command_function(*args, **kwargs) + except typer.Exit: + raise + except Exception as exception: + error(f"{action}: {exception}") + return wrapper + return decorator +``` + +**结论:** `action` 是操作描述标签(如 "Generation request failed"),`{exception}` 是原始异常消息,不修改 API 错误码/消息。 + +### 各命令模块审查 + +| 模块 | 错误处理方式 | 结论 | +|---|---|---| +| `generation.py` | `print_failed_message(rsp)` | GOOD | +| `fine_tunes.py` | `ensure_ok` + `print_failed_message` | GOOD | +| `files.py` | `ensure_ok` | GOOD | +| `deployments.py` | `ensure_ok` + `print_failed_message` | GOOD | +| `models.py` | `ensure_ok` | GOOD | +| `embeddings.py` | `ensure_ok` | GOOD | +| `application.py` | `ensure_ok` | GOOD | +| `image_synthesis.py` | `ensure_ok` | GOOD | +| `image_generation.py` | `ensure_ok` | GOOD | +| `multimodal_conversation.py` | `ensure_ok` | GOOD | +| `multimodal_embedding.py` | `ensure_ok` | GOOD | +| `rerank.py` | `ensure_ok` | GOOD | +| `tokenization.py` | `ensure_ok` | GOOD | +| `understanding.py` | `ensure_ok` | GOOD | +| `transcription.py` | `ensure_ok` | GOOD | +| `speech_synthesis.py` | `print_failed_message` | GOOD | +| `video_synthesis.py` | `ensure_ok` | GOOD | +| `oss.py` | `error()` 自定义消息(非 API 错误,本地校验) | GOOD | +| `code_generation.py` | `ensure_ok` | GOOD | + +### 轻微注意点 + +**agentic_rl.py** — 用自定义格式拼接错误: + +```python +raise OutputError(f"API returned status {result.status_code}: {result.message}") +``` + +原始 `message` 保留在拼接字符串中未修改,但 `result.code`(API 业务错误码)未单独展示。影响较小,因为 `status_code` 和 `message` 都已包含。 + +### CLI 层结论 + +**CLI 层没有修改 API 的 error code 和 error message,全部原样透传。** 所有命令通过 `print_failed_message` 和 `ensure_ok` 两个统一入口处理错误,直接输出 SDK 返回的 `rsp.code` 和 `rsp.message`。 diff --git a/md/experiences.jsonl b/md/experiences.jsonl new file mode 100644 index 00000000..e9c37fd9 --- /dev/null +++ b/md/experiences.jsonl @@ -0,0 +1,8 @@ +{"task": "工具连续失败", "tools": ["read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file"], "outcome": "failure", "lesson": "连续 4 次失败 (read_file),需要换策略"} +{"task": "查看当前git 代码:是否修正了以下问题:\n\n问题 1 — StreamEventMixin:错误码和消息被丢弃(HIGH)\n文件: dashscope/client/base_api.py, 行 1", "tools": ["read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "search_files", "search_files", "list_directory", "search_files", "run_command", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file"], "outcome": "failure", "lesson": ""} +{"task": "pr:https://github.com/dashscope/dashscope-sdk-python/pull/162", "tools": ["mcp_connect", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} +{"task": "继续修复下", "tools": ["run_command", "read_file", "read_file", "write_file", "run_command", "run_command", "run_command", "run_command", "run_command", "read_file", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} +{"task": "问题4全部修复?刚才还提示部分修复", "tools": ["read_file"], "outcome": "success", "lesson": ""} +{"task": "CLI部分:CLI 层有 3 个统一的错误处理函数,所有命令模块共用:\n1. print_failed_message(rsp) — 通用错误打印\n文件: dashscope/cli/common.p", "tools": ["read_file", "read_file", "read_file", "search_files"], "outcome": "failure", "lesson": ""} +{"task": "commit & push", "tools": ["run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "success", "lesson": ""} +{"task": "https://github.com/dashscope/dashscope-sdk-python/pull/162 再check下是否都修复了", "tools": ["mcp_connect", "list_directory", "run_command", "read_file", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "read_file", "read_file", "read_file", "read_file", "run_command", "run_command", "read_file", "read_file", "run_command", "run_command", "read_file", "run_command", "read_file", "run_command", "run_command", "run_command", "run_command", "read_file", "read_file", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} diff --git a/md/m0-1x-cli-hotfix.md b/md/m0-1x-cli-hotfix.md new file mode 100644 index 00000000..81c016fc --- /dev/null +++ b/md/m0-1x-cli-hotfix.md @@ -0,0 +1,24 @@ +# M0 — 1.x CLI 快修清单 + +> 从 `md/dashscope-2.0-design.md` 拆出:M0 是对 **1.x** 的修复,在 1.x 维护分支单独实施,**不在 V2 分支**。 +> 背景依据:`md/cli-source-level-comparison-2026-08.md` §5"立即修"清单。 +> 完成后 1.x 进入仅维护期(只修 bug/安全补丁,不再有任何功能性改动;新能力一律进 2.0)。 + +--- + +## 任务清单(每项 ≈ 一次 PR) + +| # | 任务 | 涉及文件 | 验收 | +| --- | --- | --- | --- | +| 0.1 | 修流式分支出错仍 exit 0 的 bug:错误后 `raise typer.Exit(1)` | `cli/generation.py:172-189`(同类流式命令逐一排查) | 流式中途注入错误,退出码=1 | +| 0.2 | 开启 shell 补全:`add_completion=True` + README 安装说明 | `cli/__init__.py:244` | bash/zsh/fish 补全可用 | +| 0.3 | 新增 `dashscope auth whoami/login/logout`,whoami 以退出码表达 key 有效性 | 新 `cli/auth.py` | 有效 key 退出 0,无效退出 2 | +| 0.4 | 流式输出改增量纯文本(TTY);非 TTY 输出 JSON 行 | `cli/generation.py` 等生成类命令 | 管道 `| jq` 可解析 | +| 0.5 | 退出码分级(0 成功 / 1 服务端 / 2 认证 / 3 参数 / 4 限流)落到 `cli/common.py` | `cli/common.py` | CI 用例覆盖各错误类别 | + +## 说明 + +- **为什么先做**:五项均为"小改动、高回报",不依赖 2.0 架构;其中 0.1 是 CI 会误判成功的真实缺陷(对比调研实锤)。 +- **边界**:只做修复与体验补齐,**不引入新 API 能力**(遵循"1.x 仅维护"策略);0.3/0.5 属契约修复而非新功能。 +- **与 2.0 的关系**:2.0 方案(`dashscope-2.0-design.md`)里程碑从 M1 开始;M0 的产出(auth/config 习惯、退出码约定)会被 CLI 2.0(M4)继承。 +- **风险提示**:0.4 改变了默认输出格式(dict repr → 纯文本/JSON),可能影响解析旧输出的存量脚本;建议加 `DASHSCOPE_CLI_LEGACY_OUTPUT=1` 回退开关并提前公告。 diff --git a/md/research-openai-anthropic-references.md b/md/research-openai-anthropic-references.md new file mode 100644 index 00000000..f176c584 --- /dev/null +++ b/md/research-openai-anthropic-references.md @@ -0,0 +1,78 @@ +# 调研:OpenAI/Athropic SDK/CLI + +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 参考 + +### OpenAI + +- OpenAI SDK:https://developers.openai.com/api/docs/libraries +- OpenAI CLI:https://developers.openai.com/api/docs/libraries/openai-cli +- OpenAI python Git:https://github.com/openai/openai-python +- OpenAI Agents python:https://github.com/openai/openai-agents-python + - https://github.com/openai/openai-agents-python/blob/main/docs/quickstart.md +- OpenAI Skills:https://github.com/openai/ + +### Anthropic + +- Anthropic SDK:https://github.com/anthropics/anthropic-sdk-python +- Docs:https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/python +- CLI:https://platform.claude.com/docs/en/cli-sdks-libraries/cli/using +- Anthropic Claude API:https://platform.claude.com/docs/en/api/overview +- Anthropic Claude-code:https://github.com/anthropics/claude-code +- Docs:https://code.claude.com/docs/zh-CN/overview +- Agent SDK:https://code.claude.com/docs/zh-CN/agent-sdk/overview +- CLI:https://code.claude.com/docs/zh-CN/cli-reference +- Cookbooks:https://github.com/anthropics/claude-cookbooks +- Anthropic Skills:https://github.com/anthropics/skills/tree/main +- Anthropic Prompts:https://github.com/anthropics/prompt-eng-interactive-tutorial + +### Stainless + +- https://www.stainless.com/docs/ + +### oapi-codegen + +- https://github.com/oapi-codegen/oapi-codegen + +--- + +## 现状 + +三者深度对比: + +- https://alidocs.dingtalk.com/i/nodes/YQBnd5ExVEjea40qCgXO1470JyeZqMmz +- https://alidocs.dingtalk.com/i/nodes/N7dx2rn0JbxOaqnACZDa3wRpWMGjLRb3 + +结论: + +- https://alidocs.dingtalk.com/i/nodes/m9bN7RYPWdyrPBREcKpOnzwgVZd1wyK0 +- dashscope vs openai + - https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoxKPoqwCB7dobYDV1waOeDk +- dashscope vs athropic + - https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoxKPoqwCB7dobYDV1waOeDk + +--- + +## 三个对比分析 + +### 三者对比总结(OpenAI / Anthropic / DashScope) + +| 维度 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 上手体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | +| API 简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | +| 流式输出 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | +| Tool Use | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | +| 错误处理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | +| 多模态支持 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 模型多样性 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 工程化程度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | + +**一句话总结**: + +- **OpenAI**:上手最快,API 最简洁(output_text 一行直达) +- **Anthropic**:工程化程度最高(异常处理、自动重试、Tool 装饰器、流式助手) +- **DashScope**:模型多样性最强(文本/图像/视频/语音全覆盖),但需要在异常处理、Client 实例化、Tool Use 简化 三个方向补齐短板 diff --git a/md/sdk-comparison-anthropic-vs-dashscope.md b/md/sdk-comparison-anthropic-vs-dashscope.md new file mode 100644 index 00000000..2b6e2938 --- /dev/null +++ b/md/sdk-comparison-anthropic-vs-dashscope.md @@ -0,0 +1,307 @@ +# Anthropic Python SDK vs DashScope SDK 对比分析 + +> 基于 Anthropic Python SDK 文档 和 DashScope SDK 源码的深度对比 +> 生成时间:2026-07-31 +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 1. 初始化方式 + +| 维度 | Anthropic | DashScope | +| --- | --- | --- | +| API Key | `Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))` 或自动读取环境变量 | `dashscope.api_key = "..."` 全局变量,或每次调用传入 | +| Client 实例 | `client = Anthropic()` — 面向对象,创建实例 | `Generation.call(...)` — 全静态类方法,无需实例化 | +| Base URL | `client = Anthropic(base_url="...")` | `dashscope.base_http_api_url = "..."` 全局变量 | +| 异步 Client | `client = AsyncAnthropic()` — 独立的异步类 | `AioGeneration` 等独立的异步类 | + +**评价**:Anthropic 的 Client 实例模式明显更优 — 可以创建多个不同配置的 client(不同 key/不同 region),支持依赖注入自定义 HTTP client(httpx/aiohttp)。DashScope 的全局变量方式在多租户场景下容易出错。 + +--- + +## 2. 最简单的调用(Hello World) + +**Anthropic** + +```python +from anthropic import Anthropic + +client = Anthropic() +message = client.messages.create( + max_tokens=1024, + messages=[ + {"role": "user", "content": "Hello, Claude"} + ], + model="claude-opus-5", +) + +for block in message.content: + if block.type == "text": + print(block.text) +``` + +**DashScope** + +```python +import dashscope + +response = dashscope.Generation.call( + model="qwen-turbo", + messages=[{"role": "user", "content": "Hello"}] +) + +print(response.output.choices[0].message.content) +``` + +**评价**: + +- Anthropic 需要遍历 `message.content` blocks,稍显繁琐,但支持多模态输出(图片、工具调用等) +- DashScope 路径更深:`output.choices[0].message.content` 四层穿透 +- 两者都不如 OpenAI 的 `response.output_text` 一行直达 + +--- + +## 3. 流式输出 + +**Anthropic** + +```python +# 方式一:基础流式 +stream = client.messages.create( + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + model="claude-opus-5", + stream=True, +) +for event in stream: + print(event.type) + +# 方式二:高级流式助手(推荐) +async with client.messages.stream( + max_tokens=1024, + messages=[{"role": "user", "content": "Say hello"}], + model="claude-opus-5", +) as stream: + async for text in stream.text_stream: + print(text, end="", flush=True) + message = await stream.get_final_message() +``` + +**DashScope** + +```python +responses = Generation.call( + model="qwen-turbo", + messages=[...], + stream=True, + incremental_output=True, # 需要手动设置 +) +for response in responses: + print(response.output.choices[0].message.content, end="") +``` + +**评价**:Anthropic 的流式体验明显更好: + +- 提供 `stream.text_stream` 直接输出文本(无需解析 event type) +- `get_final_message()` 获取完整消息 +- 支持 context manager 管理资源 +- DashScope 需要额外理解 `incremental_output` 参数,且路径深 + +--- + +## 4. Function Calling / Tool Use + +**Anthropic** + +```python +from anthropic import Anthropic, beta_tool + +client = Anthropic() + +@beta_tool +def get_weather(location: str) -> str: + """Get the weather for a given location. + + Args: + location: The city and state, for example, San Francisco, CA + + Returns: + A JSON-encoded string with location, temperature, and weather condition. + """ + return json.dumps({"location": location, "temperature": "68°F", "condition": "Sunny"}) + +# 使用 tool_runner 自动处理工具调用 +runner = client.beta.messages.tool_runner( + max_tokens=1024, + model="claude-opus-5", + tools=[get_weather], + messages=[{"role": "user", "content": "What is the weather in SF?"}], +) + +for message in runner: + print(message) +``` + +**DashScope** + +```python +tools = [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City and state"} + }, + "required": ["location"] + } + } +}] + +response = Generation.call( + model="qwen-turbo", + messages=[{"role": "user", "content": "SF weather?"}], + tools=tools, +) + +# 需要手动解析 tool_calls 并调用函数 +tool_calls = response.output.choices[0].message.tool_calls +``` + +**评价**:Anthropic 大幅领先: + +- `@beta_tool` 装饰器:从函数签名和 docstring 自动生成 schema +- `tool_runner`:自动处理工具调用循环(调用 → 返回结果 → 再次调用模型) +- DashScope 需要手动定义 JSON schema,手动解析 tool_calls,手动循环调用 + +--- + +## 5. 异步任务(图像生成等) + +**Anthropic**:没有暴露异步任务概念,调用就是同步/流式。 + +**DashScope** + +```python +# 提交任务 +resp = ImageSynthesis.async_call(model="wanx-v1", prompt="...") +# 轮询状态 +while True: + result = ImageSynthesis.fetch(resp.output.task_id) + if result.output.task_status == "SUCCEEDED": + break +``` + +**评价**:DashScope 的异步任务模式增加了使用复杂度,但这是服务端架构决定的(图像生成耗时长),属于合理设计。Anthropic 主要做文本生成,不需要此类功能。 + +--- + +## 6. 错误处理 + +**Anthropic** + +```python +import anthropic + +try: + message = client.messages.create(...) +except anthropic.APIConnectionError as e: + print("The server could not be reached") +except anthropic.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except anthropic.APIStatusError as e: + print(f"Status code: {e.status_code}") +``` + +提供完整的异常类型层级: + +- BadRequestError (400) +- AuthenticationError (401) +- PermissionDeniedError (403) +- NotFoundError (404) +- ConflictError (409) +- UnprocessableEntityError (422) +- RateLimitError (429) +- InternalServerError (5xx) +- APIConnectionError (网络错误) + +**DashScope** + +```python +response = Generation.call(...) +if response.status_code != 200: + print(f"Error: {response.code}, {response.message}") + print(f"Request ID: {response.request_id}") +``` + +**评价**:Anthropic 明显更优: + +- 异常驱动的错误处理,符合 Python 惯例 +- 细粒度的异常类型,可以针对不同错误做不同处理 +- DashScope 使用返回值检查(status_code + code + message),容易遗漏错误处理 + +--- + +## 7. 高级功能 + +**Anthropic** + +| 功能 | 说明 | +| --- | --- | +| Token 计数 | `client.messages.count_tokens(...)` 请求前预计算 token 数 | +| Message Batches | 批量处理 API,支持异步大批量请求 | +| File Uploads | 支持 PathLike、bytes tuple、BinaryIO 等多种上传方式 | +| Request ID | `message._request_id` 便于调试和报告 | +| 自动重试 | 408/409/429/5xx 自动重试 2 次,指数退避 | +| 多平台支持 | Bedrock、Vertex、AWS、Microsoft Foundry 多平台 Client | +| HTTP Client 定制 | 支持 httpx/aiohttp 自定义 HTTP client | + +**DashScope** + +| 功能 | 说明 | +| --- | --- | +| 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖 | +| 异步任务 | async_call + fetch 模式对长耗时任务实用 | +| 多模态格式 | `{"text": ..., "image": ...}` 直观简洁 | +| Session 管理 | 支持 WebSocket、SSE 等多种连接方式 | + +**评价**:Anthropic 在工程化程度上领先(token 计数、批量 API、自动重试、多平台),DashScope 在模型多样性上有优势。 + +--- + +## 总结:DashScope 可改进的方向 + +### 🔴 高优先级 + +| # | 改进点 | 现状 | 建议 | +| --- | --- | --- | --- | +| 1 | 异常处理 | 返回值检查 `if response.status_code != 200` | 添加异常驱动错误处理:APIError、RateLimitError、AuthenticationError 等 | +| 2 | Client 实例化 | 全局变量 `dashscope.api_key` | 支持 `DashScope(api_key=..., base_url=...)` 实例,多租户友好 | +| 3 | 流式体验 | 需要 `incremental_output` 参数 + 深层路径 | 提供 `client.messages.stream()` 高级 API,直接输出文本 | + +### 🟡 中优先级 + +| # | 改进点 | 现状 | 建议 | +| --- | --- | --- | --- | +| 4 | Tool Use 简化 | 手动定义 JSON schema + 手动循环调用 | 添加 `@tool` 装饰器 + tool_runner 自动循环 | +| 5 | 自动重试 | 无内置重试机制 | 添加可配置的自动重试(429/5xx 等) | +| 6 | Token 计数 | 无预计算功能 | 添加 `Generation.count_tokens(...)` API | +| 7 | HTTP Client 定制 | 内部封装 httpx | 支持传入自定义 HTTP client(httpx/aiohttp) | + +### 🟢 低优先级(DashScope 做得好的地方) + +| # | 亮点 | 说明 | +| --- | --- | --- | +| 1 | 多模态 content 格式 | `{"text": ..., "image": ...}` 比 Anthropic 的 block 结构更直观 | +| 2 | 内置异步任务 | async_call + fetch 模式对长耗时任务(图像/视频)很实用 | +| 3 | 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖,Anthropic 主要是文本 | + +--- + +## 参考资源 + +- Anthropic Python SDK +- OpenAI Python SDK +- DashScope SDK 源码:dashscope-sdk-python-part/ diff --git a/md/sdk-comparison-openai-vs-dashscope.md b/md/sdk-comparison-openai-vs-dashscope.md new file mode 100644 index 00000000..d31127a7 --- /dev/null +++ b/md/sdk-comparison-openai-vs-dashscope.md @@ -0,0 +1,206 @@ +# OpenAI SDK vs DashScope SDK 对比分析 + +> 基于 OpenAI Quickstart 文档和 DashScope SDK 源码的深度对比 +> 生成时间:2026-07-30 +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 1. 初始化方式 + +| 维度 | OpenAI | DashScope | +| --- | --- | --- | +| API Key | 环境变量 `OPENAI_API_KEY` 自动读取,或传入 api_key 参数 | 环境变量 `DASHSCOPE_API_KEY` 自动读取,或每次调用传入 | +| Client 实例 | `client = OpenAI()` — 面向对象,创建实例 | `Generation.call(...)` — 全静态类方法,无需实例化 | +| Base URL | `client = OpenAI(base_url="...")` | `dashscope.base_http_api_url = "..."` 全局变量 | + +**评价**:OpenAI 的 Client 实例模式更易用 — 可以创建多个不同配置的 client(不同 key/不同 region),DashScope 的全局变量方式在多租户场景下容易出错。 + +--- + +## 2. 最简单的调用(Hello World) + +**OpenAI(Responses API)** + +```python +from openai import OpenAI +client = OpenAI() +response = client.responses.create( + model="gpt-5.6", + input="Write a one-sentence bedtime story about a unicorn." +) +print(response.output_text) # 一行拿到文本 +``` + +**DashScope** + +```python +import dashscope +response = dashscope.Generation.call( + model="qwen-turbo", + messages=[{"role": "user", "content": "写一个关于独角兽的睡前故事"}] +) +print(response.output.choices[0].message.content) # 路径很深 +``` + +**评价**:OpenAI 明显更简洁。`response.output_text` 一行直达,DashScope 需要 `output.choices[0].message.content` 四层穿透。 + +--- + +## 3. 流式输出 + +**OpenAI** + +```python +stream = client.responses.create( + model="gpt-5.6", + input="Say hello", + stream=True, +) +for event in stream: + print(event) +``` + +**DashScope** + +```python +responses = Generation.call( + model="qwen-turbo", + messages=[...], + stream=True, + incremental_output=True, +) +for response in responses: + print(response.output.choices[0].message.content, end="") +``` + +**评价**:两者流式接口相似,但 DashScope 需要额外理解 `incremental_output` 参数(增量 vs 累积输出),OpenAI 的 Responses API 天然就是增量的,零配置。 + +--- + +## 4. 图片/多模态输入 + +**OpenAI** + +```python +response = client.responses.create( + model="gpt-5.6", + input=[{ + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + {"type": "input_image", "image_url": "https://..."}, + ], + }], +) +print(response.output_text) +``` + +**DashScope** + +```python +from dashscope import MultiModalConversation +response = MultiModalConversation.call( + model="qwen-vl-max", + messages=[{ + "role": "user", + "content": [ + {"text": "这张图片里有什么?"}, + {"image": "https://..."}, + ], + }], +) +``` + +**评价**:DashScope 的 `{"text": ..., "image": ...}` 格式比 OpenAI 的 `{"type": "input_text", ...}` 更直观简洁,这是 DashScope 做得好的地方。 + +--- + +## 5. Function Calling / Tool Use + +**OpenAI** + +```python +tools = [{"type": "function", "name": "get_weather", ...}] +response = client.responses.create( + model="gpt-5.6", + input=[{"role": "user", "content": "Paris weather?"}], + tools=tools, +) +print(response.output[0]) # tool call +``` + +**DashScope** + +```python +tools = [{"type": "function", "name": "get_weather", ...}] +response = Generation.call( + model="qwen-turbo", + messages=[{"role": "user", "content": "Paris weather?"}], + tools=tools, +) +print(response.output.choices[0].message.tool_calls) +``` + +**评价**:接口几乎一致,OpenAI 的 `response.output[0]` 稍简洁。 + +--- + +## 6. 异步任务(图像生成等) + +**OpenAI**:没有暴露异步任务概念,调用就是同步/流式。 + +**DashScope** + +```python +# 提交任务 +resp = ImageSynthesis.async_call(model="wanx-v1", prompt="...") +# 轮询状态 +while True: + result = ImageSynthesis.fetch(resp.output.task_id) + if result.output.task_status == "SUCCEEDED": + break +``` + +**评价**:DashScope 的异步任务模式增加了使用复杂度,但这是服务端架构决定的(图像生成耗时长),属于合理设计。OpenAI 的 DALL-E 也是同步返回,不需要 task 管理。 + +--- + +## 总结:DashScope 可改进的方向 + +### 🔴 高优先级 + +| # | 改进点 | 现状 | 建议 | +| --- | --- | --- | --- | +| 1 | 响应路径太深 | `resp.output.choices[0].message.content` | 添加便捷属性 `resp.output_text`,像 OpenAI 一样一行取到文本 | +| 2 | 缺少 Client 实例化 | 全局变量 `dashscope.api_key` | 支持 `DashScope(api_key=..., base_url=...)` 实例,多租户友好 | +| 3 | Quickstart 体验 | 无类似 OpenAI 的 5 行代码上手 | 提供 `dashscope.chat(model, prompt)` 极简 API 或 `from dashscope import quick` 快捷入口 | + +### 🟡 中优先级 + +| # | 改进点 | 现状 | 建议 | +| --- | --- | --- | --- | +| 4 | incremental_output 默认值 | 流式需手动设 `incremental_output=True` | 默认改为 True,累积输出几乎没人用 | +| 5 | 错误处理 | status_code + code + message 分散检查 | 添加 `response.raise_for_status()` 或自动抛异常模式 | +| 6 | 类型提示不完整 | `**kwargs` 透传大量参数,IDE 无提示 | 将常用参数(stream、tools 等)都写成显式签名参数(已部分做) | +| 7 | 统一 API 入口 | Generation / MultiModalConversation / ImageSynthesis 分散 | 参考 OpenAI 的 `client.responses.create()` 统一入口,内部路由到不同模型 | + +### 🟢 低优先级(DashScope 做得好的地方) + +| # | 亮点 | 说明 | +| --- | --- | --- | +| 1 | 多模态 content 格式 | `{"text": ..., "image": ...}` 比 OpenAI 的 `{"type": "input_text", ...}` 更直观 | +| 2 | 内置异步任务 | async_call + fetch 模式对长耗时任务(图像/视频)很实用 | +| 3 | 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖,OpenAI 相对较少 | + +**一句话总结** + +OpenAI 的 SDK 在"上手体验"和"API 简洁度"上领先(Client 实例化 + output_text 快捷属性 + 统一 Responses API),DashScope 在多模态格式和异步任务上有自己的优势,但需要补齐 便捷属性、Client 模式 和 Quickstart 体验 这三个短板。 + +--- + +## 参考资源 + +- OpenAI Quickstart +- OpenAI Python SDK +- DashScope SDK 源码:dashscope-sdk-python-part/ diff --git a/md/sdk-deep-comparison-openai-anthropic-dashscope.md b/md/sdk-deep-comparison-openai-anthropic-dashscope.md new file mode 100644 index 00000000..2d71c613 --- /dev/null +++ b/md/sdk-deep-comparison-openai-anthropic-dashscope.md @@ -0,0 +1,1126 @@ +# SDK 深度对比:OpenAI vs Anthropic vs DashScope + +> 基于源码级分析 +> 生成时间:2026-07-31 +> OpenAI SDK v1.x | Anthropic SDK v0.39+ | DashScope SDK v1.x +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +## 目录 + +1. 用户接口友好度 +2. 错误码与异常处理 +3. 功能使用:流式/非流式/同步/异步 +4. 架构设计哲学 +5. DashScope SDK 改进建议 + +--- + +## 1. 用户接口友好度 + +### 1.1 最简调用对比 + +**OpenAI — 极致简洁** + +```python +from openai import OpenAI + +client = OpenAI() # api_key 从环境变量读取 + +response = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], +) + +print(response.choices[0].message.content) +``` + +亮点: + +- 零配置即可使用(环境变量 `OPENAI_API_KEY`) +- `client.chat.completions.create()` — 点号链式调用,IDE 自动补全完美 +- Pydantic 模型,所有字段都有类型提示 +- `response.choices[0].message.content` — 属性访问,不是 dict 索引 + +**Anthropic — 同样简洁** + +```python +from anthropic import Anthropic + +client = Anthropic() # api_key 从环境变量读取 + +message = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) + +print(message.content[0].text) +``` + +亮点: + +- 与 OpenAI 几乎相同的模式 +- `message.content[0].text` — 直接返回结构化 content blocks +- Pydantic 模型,完整的类型提示 + +**DashScope — 多层抽象** + +```python +import dashscope +from dashscope import Generation + +dashscope.api_key = "sk-xxx" # 必须手动设置 + +response = Generation.call( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], +) + +# 需要先检查 status_code +if response.status_code == 200: + print(response.output.choices[0].message.content) +else: + print(f"Error: {response.code} - {response.message}") +``` + +问题: + +- 需要手动设置 api_key(虽然支持环境变量,但不如 OpenAI 自然) +- `Generation.call()` — 类方法调用,不支持 IDE 点号链式补全 +- 返回 `DashScopeAPIResponse` 是 dict 子类(DictMixin),不是 Pydantic 模型 +- 成功/失败通过 status_code 判断,不抛异常 — 调用方必须每次检查 + +### 1.2 Client 模式对比 + +| 特性 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| Client 实例化 | `OpenAI(api_key=...)` | `Anthropic(api_key=...)` | 无 Client 模式,使用类方法 | +| API Key 来源 | 环境变量 / 参数 / 文件 | 环境变量 / 参数 | 模块变量 / 环境变量 | +| Base URL 覆盖 | `base_url="..."` | `base_url="..."` | `dashscope.base_http_api_url` | +| 连接池 | httpx.Client 复用 | httpx.Client 复用 | 每次请求新建 requests.Session() | +| 默认超时 | 10 min(connect 5s) | 10 min(connect 5s) | 300s | +| 最大重试 | 2 次(指数退避) | 2 次(指数退避) | 1 次(仅 5xx 重试) | +| IDE 自动补全 | ✅ 完美 | ✅ 完美 | ⚠️ 部分(类方法 + dict 返回) | + +### 1.3 响应对象对比 + +| 特性 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 响应类型 | Pydantic BaseModel | Pydantic BaseModel | DictMixin(dict 子类) | +| 属性访问 | `response.choices[0].message.content` | `message.content[0].text` | `response.output.choices[0].message.content` | +| 字典访问 | `response.model_dump()` | `message.model_dump()` | `response["output"]` | +| 序列化 | `response.model_dump_json()` | `message.model_dump_json()` | `str(response)` → JSON | +| 类型安全 | ✅ 完整 Pydantic 验证 | ✅ 完整 Pydantic 验证 | ⚠️ 无验证(dict 无 schema) | +| 嵌套深度 | `choices[0].message.content` | `content[0].text` | `output.choices[0].message.content`(多一层 output) | +| `__repr__` | 美观的 Pydantic 格式 | 美观的 Pydantic 格式 | `DictMixin({...})` 原始 dict | + +### 1.4 API 设计风格对比 + +| 方面 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| API 风格 | REST 资源嵌套 (`client.chat.completions`) | REST 扁平 (`client.messages`) | 类方法扁平 (`Generation.call()`) | +| 方法命名 | create / list / retrieve / delete | create / list / retrieve / delete | call / list / get / delete / update | +| 参数传递 | 命名参数 + Pydantic 验证 | 命名参数 + Pydantic 验证 | `**kwargs` 大量使用,无验证 | +| 模型名传递 | 字符串 | 字符串 | 字符串 | +| Headers | `default_headers` 属性 | `default_headers` 属性 | `default_headers()` 函数 | + +--- + +## 2. 错误码与异常处理 + +### 2.1 错误处理哲学 + +| SDK | 哲学 | 模式 | +| --- | --- | --- | +| OpenAI | 失败即抛异常 | try/except 捕获,异常类型区分错误类别 | +| Anthropic | 失败即抛异常 | 同 OpenAI,但异常层次更扁平 | +| DashScope | 返回错误对象 | `if response.status_code != 200` 检查,不抛异常 | + +### 2.2 异常类层次 + +**OpenAI** + +``` +openai.OpenAIError (基类) +├── openai.APIError # 通用 API 错误 +├── openai.APIConnectionError # 连接失败 +│ └── openai.APITimeoutError # 超时 +├── openai.RateLimitError # 429 限流 +├── openai.BadRequestError # 400 请求错误 +├── openai.AuthenticationError # 401 认证失败 +├── openai.PermissionDeniedError # 403 权限不足 +├── openai.NotFoundError # 404 资源不存在 +├── openai.ConflictError # 409 冲突 +├── openai.UnprocessableEntityError # 422 参数错误 +├── openai.InternalServerError # 500 服务器错误 +└── openai.LengthFinishReasonError # 生成被截断 +``` + +异常属性: + +```python +except openai.APIError as e: + e.status_code # HTTP 状态码 + e.message # 错误消息 + e.body # 响应体(dict) + e.request_id # 请求 ID + e.code # 错误码 + e.headers # 响应头 +``` + +**Anthropic** + +``` +anthropic.AnthropicError (基类) +├── anthropic.APIError # 通用 API 错误 +│ ├── anthropic.BadRequestError # 400 +│ ├── anthropic.AuthenticationError # 401 +│ ├── anthropic.PermissionDeniedError # 403 +│ ├── anthropic.NotFoundError # 404 +│ ├── anthropic.ConflictError # 409 +│ ├── anthropic.UnprocessableEntityError # 422 +│ ├── anthropic.RateLimitError # 429 +│ └── anthropic.InternalServerError # 500+ +├── anthropic.APIConnectionError # 连接失败 +│ └── anthropic.APITimeoutError # 超时 +└── anthropic.APIStatusError # 带完整响应的错误 + └── 所有上面的子类 +``` + +异常属性: + +```python +except anthropic.APIError as e: + e.status_code # HTTP 状态码 + e.message # 错误消息 + e.body # 响应体 + e.response # 完整 httpx.Response 对象 + e.request_id # 请求 ID(从 headers 提取) + e.error # APIErrorBody 对象(type + message) +``` + +**DashScope** + +无异常类。所有错误通过 `DashScopeAPIResponse` 返回: + +```python +response = Generation.call(model="qwen-plus", messages=[...]) + +# 必须手动检查 +if response.status_code != 200: + print(f"Error: {response.code}") + print(f"Message: {response.message}") + print(f"Request ID: {response.request_id}") +``` + +DashScope 的 common/error.py 仅有 3 个异常: + +```python +class Error(Exception): ... +class InputRequired(Error): ... +class ModelRequired(Error): ... +class InvalidParameter(InputRequired): ... +class InvalidTask(Error): ... +class NotSupport(Error): ... +``` + +这些仅用于客户端参数校验,不用于 API 错误。 + +### 2.3 错误处理代码对比 + +**OpenAI — 类型安全** + +```python +from openai import OpenAI, APIError, RateLimitError, APIConnectionError + +client = OpenAI() + +try: + response = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + ) + print(response.choices[0].message.content) + +except RateLimitError as e: + print(f"Rate limited: {e.message}, retry after {e.headers.get('retry-after')}") +except APIConnectionError as e: + print(f"Connection failed: {e.message}") +except APIError as e: + print(f"API error {e.status_code}: {e.message}, request_id: {e.request_id}") +``` + +**Anthropic — 类似但更扁平** + +```python +from anthropic import Anthropic, RateLimitError, APIConnectionError + +client = Anthropic() + +try: + message = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + ) + print(message.content[0].text) + +except RateLimitError as e: + print(f"Rate limited: {e.message}") +except APIConnectionError as e: + print(f"Connection failed: {e.message}") +except Exception as e: + print(f"Unexpected: {e}") +``` + +**DashScope — 手动检查** + +```python +import dashscope +from dashscope import Generation + +response = Generation.call(model="qwen-plus", messages=[...]) + +# ❌ 容易忘记检查,容易遗漏错误 +if response.status_code == 200: + print(response.output.choices[0].message.content) +else: + # 错误码是字符串,如 "InvalidApiKey", "DataInspectionFailed" + print(f"Error {response.code}: {response.message}") + print(f"Request ID: {response.request_id}") +``` + +### 2.4 错误码体系 + +| 特性 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 错误码类型 | Python 异常类 + HTTP status code | Python 异常类 + HTTP status code | 字符串 code("InvalidApiKey" 等) | +| HTTP 状态码映射 | 400/401/403/404/409/422/429/500 | 400/401/403/404/409/422/429/500 | 全部混在 response.status_code | +| 错误分类 | 按 HTTP 状态自动分类 | 按 HTTP 状态自动分类 | 字符串枚举,无自动分类 | +| 重试策略 | 自动重试 429/5xx/连接错误 | 自动重试 429/5xx/连接错误 | 仅 5xx 重试 1 次 | +| 超时处理 | APITimeoutError 异常 | APITimeoutError 异常 | requests.Timeout 异常(未捕获) | +| 限流信息 | Retry-After header | Retry-After header | 无 | +| 请求 ID | e.request_id | e.request_id | response.request_id | + +### 2.5 DashScope 错误码列表(已知) + +| code | 含义 | HTTP Status | +| --- | --- | --- | +| InvalidApiKey | API Key 无效 | 401 | +| AccessDenied | 无权限 | 403 | +| InvalidParameter | 参数错误 | 400 | +| DataInspectionFailed | 内容安全审核失败 | 400 | +| Throttling | 限流 | 429 | +| Throttling.AllocationQuota | 配额耗尽 | 429 | +| ModelNotFound | 模型不存在 | 404 | +| RequestTimeout | 请求超时 | 504 | +| InternalError | 内部错误 | 500 | +| OperationDenied.NoStock | 无库存 | 503 | + +问题:错误码是字符串,没有类型安全,调用方需要写字符串比较: + +```python +if response.code == "Throttling": + time.sleep(5) +elif response.code == "InvalidApiKey": + print("Check your API key") +``` + +对比 OpenAI/Anthropic 的类型安全: + +```python +except RateLimitError: # 类型安全,IDE 可补全 + time.sleep(5) +except AuthenticationError: + print("Check your API key") +``` + +--- + +## 3. 功能使用:流式/非流式/同步/异步 + +### 3.1 四种模式矩阵 + +| 模式 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 同步 + 非流式 | ✅ `client.chat.completions.create()` | ✅ `client.messages.create()` | ✅ `Generation.call()` | +| 同步 + 流式 | ✅ stream=True → Stream[ChatCompletionChunk] | ✅ stream=True → Stream[RawMessageStreamEvent] | ✅ stream=True → Generator[DashScopeAPIResponse] | +| 异步 + 非流式 | ✅ `AsyncOpenAI()` + `await client.chat.completions.create()` | ✅ `AsyncAnthropic()` + `await client.messages.create()` | ✅ `AioGeneration.call()` (需手动实现) | +| 异步 + 流式 | ✅ stream=True → AsyncStream[ChatCompletionChunk] | ✅ stream=True → AsyncStream[RawMessageStreamEvent] | ⚠️ 无原生支持 | + +### 3.2 同步非流式 — 最常用模式 + +**OpenAI** + +```python +from openai import OpenAI + +client = OpenAI() + +response = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + temperature=0.7, + max_tokens=1024, +) + +# Pydantic 对象,完整类型提示 +content = response.choices[0].message.content +usage = response.usage # CompletionUsage(prompt_tokens=..., completion_tokens=..., total_tokens=...) +``` + +**Anthropic** + +```python +from anthropic import Anthropic + +client = Anthropic() + +message = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) + +# Pydantic 对象 +content = message.content[0].text +usage = message.usage # Usage(input_tokens=..., output_tokens=..., cache_creation_input_tokens=...) +``` + +**DashScope** + +```python +from dashscope import Generation + +response = Generation.call( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + temperature=0.7, + max_tokens=1024, +) + +# DictMixin 对象,无类型提示 +if response.status_code == 200: + content = response.output.choices[0].message.content + usage = response.usage # GenerationUsage(input_tokens=..., output_tokens=...) +``` + +### 3.3 同步流式 + +**OpenAI — 最优雅** + +```python +from openai import OpenAI + +client = OpenAI() + +stream = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + stream=True, +) + +# Stream 对象实现了 __iter__,每个 chunk 是 ChatCompletionChunk +for chunk in stream: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) +``` + +特点: + +- Stream 类型实现了 `__iter__`,直接 for 循环 +- 每个 chunk 是 ChatCompletionChunk(Pydantic 模型) +- `chunk.choices[0].delta.content` — 增量内容 +- 自动处理 SSE 协议解析 +- `stream.close()` 可提前终止 + +**Anthropic — 事件驱动** + +```python +from anthropic import Anthropic + +client = Anthropic() + +with client.messages.stream( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) + + # 流结束后获取完整消息 + message = stream.get_final_message() + print(f"\nTotal tokens: {message.usage.input_tokens + message.usage.output_tokens}") +``` + +特点: + +- MessageStreamManager 上下文管理器 +- `stream.text_stream` — 纯文本迭代器(最简洁) +- `stream.__iter__()` — 原始事件迭代器(RawMessageStreamEvent) +- `stream.get_final_message()` — 累积后的完整消息 +- `stream.get_final_text()` — 累积后的纯文本 +- 事件类型丰富:RawMessageStartEvent / RawContentBlockDeltaEvent / RawMessageDeltaEvent 等 + +**DashScope — Generator 模式** + +```python +from dashscope import Generation + +responses = Generation.call( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + stream=True, + incremental_output=True, # 增量输出 +) + +# 返回 Generator[DashScopeAPIResponse] +for response in responses: + if response.status_code == 200: + text = response.output.choices[0].message.content + print(text, end="", flush=True) + else: + print(f"Error: {response.code}") + break +``` + +特点: + +- 返回 Generator[DashScopeAPIResponse] +- 每个 chunk 仍然是 DashScopeAPIResponse(需要检查 status_code) +- `incremental_output=True` 控制是否增量 +- SSE 解析在 `StreamEventMixin._handle_stream()` 中实现 +- 无 `close()` 方法 + +### 3.4 异步模式 + +**OpenAI — 完整异步支持** + +```python +import asyncio +from openai import AsyncOpenAI + +client = AsyncOpenAI() + +async def main(): + # 异步非流式 + response = await client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + ) + print(response.choices[0].message.content) + + # 异步流式 + stream = await client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + stream=True, + ) + async for chunk in stream: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="") + +asyncio.run(main()) +``` + +特点: + +- AsyncOpenAI 使用 httpx.AsyncClient +- AsyncStream 实现了 `__aiter__` +- 连接池复用 +- 完整的异步上下文管理 + +**Anthropic — 完整异步支持** + +```python +import asyncio +from anthropic import AsyncAnthropic + +client = AsyncAnthropic() + +async def main(): + # 异步非流式 + message = await client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + ) + print(message.content[0].text) + + # 异步流式 + async with client.messages.stream( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + ) as stream: + async for text in stream.text_stream: + print(text, end="") + +asyncio.run(main()) +``` + +**DashScope — 异步支持不完整** + +```python +# DashScope 的异步是通过 BaseAsyncAioApi 实现的 +# 主要用于异步任务(如图像生成),而非 chat 流式 + +from dashscope.client.base_api import BaseAsyncAioApi + +# 异步任务模式(非 chat) +task_response = await SomeModel.async_call(model="...", input={...}) +# 轮询等待 +result = await BaseAsyncAioApi.wait(task_response) + +# ❌ 没有 AsyncGeneration.call() +# ❌ 没有 async for chunk in stream +# ❌ 没有 AsyncStream 类型 +``` + +问题: + +- AioGeneration 存在但功能不完整 +- 异步流式(async for)无原生支持 +- 底层使用 aiohttp,但上层 API 没有暴露完整的异步接口 +- 用户需要在 async 函数中调用同步 API(阻塞事件循环) + +### 3.5 流式辅助功能 + +| 功能 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 累积文本 | 手动拼接 delta | `stream.get_final_text()` | 手动拼接 + incremental_output | +| 累积消息 | 手动拼接 chunks | `stream.get_final_message()` | 手动拼接 | +| Usage 统计 | chunk.usage(最后一个 chunk) | message.usage(最终消息) | 最后一个 chunk 的 usage | +| 提前终止 | `stream.close()` | 退出 with 块 | 无 | +| 事件类型区分 | `chunk.choices[0].finish_reason` | event.type(丰富的枚举) | finish_reason | +| 工具调用流式 | delta.tool_calls | RawContentBlockStartEvent | 无流式工具调用 | + +### 3.6 底层实现 + +| 方面 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| HTTP 客户端 | httpx.Client / httpx.AsyncClient | httpx.Client / httpx.AsyncClient | requests.Session / aiohttp.ClientSession | +| 连接池 | ✅ httpx 内置 | ✅ httpx 内置 | ❌ 每次 with requests.Session() 新建 | +| SSE 解析 | 自研 Stream 类 | 自研 MessageStream | StreamEventMixin._handle_stream() | +| 重试机制 | 内置(指数退避 + jitter) | 内置(指数退避 + jitter) | 仅 5xx 重试 1 次 | +| 代理支持 | httpx 原生 | httpx 原生 | requests 原生 | +| 超时粒度 | connect / read / write / pool | connect / read / write / pool | 仅 timeout 一个值 | + +--- + +## 4. 架构设计哲学 + +### 4.1 设计模式对比 + +| 模式 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 客户端模式 | 实例化 Client → 调用方法 | 实例化 Client → 调用方法 | 类方法直接调用 | +| 响应模式 | Pydantic 模型 | Pydantic 模型 | DictMixin(dict 子类) | +| 错误模式 | 异常驱动 | 异常驱动 | 返回值驱动 | +| 类型系统 | Pydantic v2 + Pydantic Core | Pydantic v1 兼容 | 无类型验证 | +| HTTP 层 | httpx(现代) | httpx(现代) | requests(传统) | +| 代码生成 | 大部分自动生成 | 大部分自动生成 | 手写 | + +### 4.2 代码量对比 + +| 指标 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| SDK 包大小 | ~1MB | ~500KB | ~2MB | +| 核心文件数 | ~50 | ~30 | ~80 | +| 依赖项 | httpx, pydantic, anyio, typing-extensions | httpx, pydantic, anyio, typing-extensions | requests, aiohttp, pyyaml | +| 类型存根 | ✅ 内置 .pyi | ✅ 内置 .pyi | ⚠️ 刚添加(PR #170) | +| 自动重试 | ✅ 内置 | ✅ 内置 | ⚠️ 基础实现 | + +### 4.3 OpenAI 和 Anthropic 为何选择相同架构 + +两者 SDK 架构惊人相似,原因: + +1. Anthropic 早期 API 兼容 OpenAI — 降低用户迁移成本 +2. Pydantic 成为行业标准 — 类型安全 + 自动验证 + 序列化 +3. httpx 替代 requests — 原生异步支持 + 连接池 +4. 异常驱动错误处理 — Python 社区最佳实践(EAFP > LBYL) + +DashScope 选择不同架构的历史原因: + +1. 早期设计 — 基于 requests + dict,追求简单 +2. 多任务支持 — 异步任务(图像生成、语音合成)需要 async_call / wait / fetch 模式 +3. 兼容性考虑 — 保持向后兼容,不敢大改 API + +--- + +## 5. DashScope SDK 改进建议 + +### 5.1 优先级 P0 — 必须修复 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 1 | 添加 Client 模式 | `DashScope()` 实例化 + `client.chat.completions.create()` | OpenAI `OpenAI()` | +| 2 | 添加异常类层次 | 按 HTTP 状态码自动抛出对应异常 | OpenAI 异常体系 | +| 3 | Pydantic 响应模型 | 替换 DictMixin,提供完整类型验证和 IDE 补全 | OpenAI ChatCompletion | +| 4 | 完善异步流式 | `AsyncDashScope()` + `async for chunk in stream` | OpenAI AsyncOpenAI | + +### 5.2 优先级 P1 — 强烈建议 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 5 | httpx 替换 requests | 原生异步 + 连接池复用 + 现代超时控制 | OpenAI / Anthropic | +| 6 | 内置自动重试 | 指数退避 + jitter + Retry-After 支持 | OpenAI `_should_retry()` | +| 7 | 流式辅助 API | `stream.get_final_message()` / `stream.close()` | Anthropic MessageStream | +| 8 | 增量输出默认开启 | `incremental_output=True` 应为默认 | Anthropic 默认增量 | + +### 5.3 优先级 P2 — 锦上添花 + +| # | 改进 | 说明 | 参考 | +| --- | --- | --- | --- | +| 9 | @tool 装饰器 | 从函数签名 + docstring 自动生成 tool schema | OpenAI Agents SDK | +| 10 | Responses API | 兼容 OpenAI Responses API 格式 | OpenAI `client.responses.create()` | +| 11 | Anthropic 兼容层 | 兼容 Anthropic Messages API 格式 | Anthropic `client.messages.create()` | +| 12 | Dashboard 追踪 | 请求追踪和用量统计 Web UI | OpenAI Agents Dashboard | + +### 5.4 渐进式迁移方案 + +DashScope 不能一步到位重构(破坏现有用户),建议分阶段: + +**阶段 1:兼容层(不破坏现有 API)** + +```python +# 现有 API 保持不变 +from dashscope import Generation +response = Generation.call(model="qwen-plus", messages=[...]) + +# 新增 Client 模式(并行可用) +from dashscope import DashScope +client = DashScope() # api_key 从环境变量读取 +response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], +) +# response 是 Pydantic 模型 +print(response.choices[0].message.content) +``` + +**阶段 2:异常层(可选启用)** + +```python +# 默认保持返回值模式(向后兼容) +client = DashScope() +response = client.chat.completions.create(...) +if response.status_code != 200: ... + +# 启用异常模式 +client = DashScope(raise_on_error=True) +try: + response = client.chat.completions.create(...) +except RateLimitError as e: + ... +``` + +**阶段 3:完全现代化(下一代)** + +```python +# dashscope v2 +from dashscope import DashScope +from dashscope.errors import RateLimitError, AuthenticationError + +client = DashScope() # httpx + Pydantic + 异常驱动 + +try: + response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + ) + print(response.choices[0].message.content) +except RateLimitError as e: + print(f"Rate limited: {e.message}") +``` + +### 5.5 关键差异总结 + +| 维度 | OpenAI/Anthropic | DashScope 现状 | 差距 | +| --- | --- | --- | --- | +| 初始化 | Client() 实例化 | 类方法 + 模块变量 | 缺少 Client 模式 | +| 调用风格 | client.resource.method() | Class.call() | 缺少资源嵌套 | +| 响应 | Pydantic 模型 | DictMixin (dict) | 缺少类型验证 | +| 错误 | 异常驱动 | 返回值驱动 | 缺少异常体系 | +| 异步 | 完整支持 | 部分支持 | 缺少异步流式 | +| HTTP | httpx(连接池) | requests(无连接池) | 性能差距 | +| 重试 | 自动 + 智能 | 基础 1 次 | 可靠性差距 | +| 类型提示 | 完整 .pyi | 刚添加 | IDE 体验差距 | + +--- + +## 6. 日志与错误可理解性 + +### 6.1 日志系统对比 + +**OpenAI — 最小化 + 敏感信息过滤** + +```python +# 日志配置(_utils/_logs.py) +logger = logging.getLogger("openai") + +def setup_logging(): + env = os.environ.get("OPENAI_LOG") + if env == "debug": + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) # 同时开启 httpx 日志 +``` + +日志内容示例(DEBUG 模式): + +``` +[2026-07-31 14:12:26 - openai._base_client:1033 - DEBUG] Sending HTTP Request: POST https://api.openai.com/v1/chat/completions +[2026-07-31 14:12:27 - openai._base_client:1074 - DEBUG] HTTP Response: POST https://api.openai.com/v1/chat/completions "200 OK" +[2026-07-31 14:12:27 - openai._base_client:1082 - DEBUG] request_id: req_abc123 +``` + +亮点: + +- SensitiveHeadersFilter — 自动过滤 Authorization 和 api-key 头,防止日志泄露密钥 +- 重试日志非常详细:"Retrying due to status code 429" / "Retrying as header x-should-retry is set to true" +- 环境变量一键开启(`OPENAI_LOG=debug`),无需代码修改 +- 同时输出 httpx 底层日志(连接/超时/重试细节) + +**Anthropic — 与 OpenAI 几乎一致** + +```python +# _base_client.py +log = logging.getLogger(__name__) # anthropic._base_client + +# 重试日志 +log.debug("Retrying due to status code %i", response.status_code) +log.info("Retrying request to %s in %f seconds", options.url, timeout) +log.debug("request_id: %s", response.headers.get("request-id")) +``` + +亮点: + +- 日志格式和粒度与 OpenAI 一致(同一套 Stainless 代码生成器) +- request-id header 追踪 +- 超时错误消息自带文档链接:"Request timed out or interrupted. See https://docs.anthropic.com/en/api/errors#long-requests" + +**DashScope — 基础日志,信息量不足** + +```python +# common/logging.py +logger = logging.getLogger("dashscope") + +# 日志格式 +"%(asctime)s - %(name)s - %(filename)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s" + +# 环境变量: DASHSCOPE_LOGGING_LEVEL=info/debug +``` + +日志内容示例(DEBUG 模式): + +``` +2026-07-31 14:12:26 - dashscope - base_api.py - _get - 599 - DEBUG - Starting request: https://dashscope.aliyuncs.com/api/v1/tasks/task_123 +2026-07-31 14:12:27 - dashscope - base_api.py - _get - 605 - DEBUG - Starting processing response: https://dashscope.aliyuncs.com/api/v1/tasks/task_123 +``` + +问题: + +- ❌ 无敏感信息过滤 — API Key 可能出现在日志中 +- ❌ 无 request_id 日志 — 请求 ID 只在 response 对象中,不在日志里 +- ❌ 无 HTTP 状态码日志 — 只记录 "Starting request" 和 "Starting processing response" +- ❌ 无重试原因日志 — 重试时只记录 "The task xxx is RUNNING",不记录重试次数/等待时间 +- ❌ 日志格式冗长 — 包含 filename + funcName + lineno,但信息密度低 +- ⚠️ 默认关闭 — 不设置 DASHSCOPE_LOGGING_LEVEL 环境变量则无日志 + +### 6.2 日志对比矩阵 + +| 特性 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 开启方式 | OPENAI_LOG=debug | ANTHROPIC_LOG=debug | DASHSCOPE_LOGGING_LEVEL=debug | +| 日志级别 | DEBUG / INFO | DEBUG / INFO | DEBUG / INFO | +| 请求 URL | ✅ POST https://... | ✅ POST https://... | ✅ Starting request: https://... | +| HTTP 状态码 | ✅ "200 OK" | ✅ "200 OK" | ❌ 不记录 | +| Request ID | ✅ request_id: req_xxx | ✅ request_id: xxx | ❌ 不记录 | +| Response Headers | ✅ 完整输出 | ✅ 完整输出 | ❌ 不记录 | +| 敏感信息过滤 | ✅ SensitiveHeadersFilter | ✅ 同源 | ❌ 无 | +| 重试原因 | ✅ "Retrying due to 429" | ✅ "Retrying due to 429" | ❌ 不记录 | +| 重试等待时间 | ✅ "Retrying in 2.5s" | ✅ "Retrying in 2.5s" | ❌ 不记录 | +| 超时异常详情 | ✅ exc_info=True | ✅ exc_info=True | ❌ 无 | +| 连接异常详情 | ✅ exc_info=True | ✅ exc_info=True | ❌ 无 | +| httpx 底层日志 | ✅ 联动开启 | ✅ 联动开启 | N/A(用 requests) | + +### 6.3 错误消息可理解性 + +**OpenAI — 结构化错误消息** + +```python +# 实际错误消息格式 +try: + client.chat.completions.create(model="gpt-4o", messages=[...]) +except openai.RateLimitError as e: + print(e) + # Error code: 429 - {'error': {'message': 'Rate limit reached for gpt-4o + # in organization org-xxx on requests per min (RPM): Limit 60, Used 60, + # Requested 1. Please try again in 1s. Visit ...', + # 'type': 'requests', 'code': 'rate_limit_exceeded'}} + + # 结构化访问 + e.status_code # 429 + e.message # "Rate limit reached..." + e.code # "rate_limit_exceeded" + e.type # "requests" + e.request_id # "req_abc123" + e.body # 完整响应体 dict + e.response # 完整 httpx.Response 对象 + e.headers # 响应头(含 Retry-After) +``` + +亮点: + +- 错误消息包含完整上下文:哪个组织、哪个模型、限制是多少、已用多少、建议等待多久 +- 所有字段都可以通过属性访问,无需解析字符串 +- `e.response.headers.get("Retry-After")` 直接获取重试等待时间 + +**Anthropic — 同样结构化 + 文档链接** + +```python +try: + client.messages.create(model="claude-sonnet-4-20250514", messages=[...]) +except anthropic.RateLimitError as e: + print(e) + # Error code: 429 - {'type': 'error', 'error': {'type': 'rate_limit_error', + # 'message': 'Number of request tokens has exceeded your per-minute rate limit'}} + + e.status_code # 429 + e.message # "Number of request tokens..." + e.request_id # "req_xxx" + e.type # "rate_limit_error"(Anthropic 特有的错误类型枚举) + e.body # 完整响应体 + e.response # 完整 httpx.Response + +# 超时错误自带文档链接 +except anthropic.APITimeoutError as e: + print(e.message) + # "Request timed out or interrupted. This could be due to a network timeout, + # dropped connection, or request cancellation. + # See https://docs.anthropic.com/en/api/errors#long-requests for more details." +``` + +亮点: + +- `e.type` — Anthropic 特有的 ErrorType 枚举(overloaded_error / rate_limit_error / invalid_request_error 等),比 HTTP 状态码更精确 +- 超时错误消息包含排查文档链接 +- 额外异常类型:OverloadedError(529) / DeadlineExceededError(504) / ServiceUnavailableError(503) + +**DashScope — 字符串拼接,信息碎片化** + +```python +response = Generation.call(model="qwen-plus", messages=[...]) + +# 成功时 +print(response) +# {"status_code": 200, "request_id": "req-xxx", "code": "", "message": "", +# "output": {...}, "usage": {...}} + +# 失败时 +if response.status_code != 200: + print(f"code: {response.code}") # "Throttling" + print(f"message: {response.message}") # "Requests rate limit" + print(f"request_id: {response.request_id}") # "req-xxx" + + # ❌ 没有结构化的错误对象 + # ❌ 没有 Retry-After 建议 + # ❌ 没有文档链接 + # ❌ 没有错误分类(限流 vs 认证 vs 参数错误 需要自己判断) +``` + +问题: + +- 错误消息过于简短:"Requests rate limit" — 不告诉用户限制是多少、已用多少、建议等多久 +- 错误码是裸字符串:"Throttling" / "InvalidApiKey" / "DataInspectionFailed" — 无类型安全 +- 同一个 DashScopeAPIResponse 对象既表示成功又表示失败 — 容易忘记检查 +- response.code 可能是 None、空字符串 ""、或错误码字符串 — 三种状态混乱 + +### 6.4 实际排障场景对比 + +**场景 1:API Key 无效** + +OpenAI: + +``` +AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: +sk-xxxx...xxxx. You can find your API key at https://platform.openai.com/account/api-keys.', +'type': 'invalid_request_error', 'code': 'invalid_api_key'}} +``` + +→ 直接告诉用户去哪里找正确的 Key + +DashScope: + +``` +status_code: 401 +code: "InvalidApiKey" +message: "Invalid API-key provided." +``` + +→ 只说无效,不说去哪里获取 + +**场景 2:限流** + +OpenAI: + +``` +RateLimitError: Error code: 429 - {'error': {'message': 'Rate limit reached for gpt-4o +in organization org-xxx on requests per min (RPM): Limit 60, Used 60, Requested 1. +Please try again in 1s. Visit https://platform.openai.com/account/rate-limits for more.', +'type': 'requests', 'code': 'rate_limit_exceeded'}} +``` + +→ 告诉你限制值、已用量、建议等待时间、文档链接 + +DashScope: + +``` +status_code: 429 +code: "Throttling" +message: "Requests rate limit" +``` + +→ 4 个单词,没有任何可操作信息 + +**场景 3:内容安全审核** + +DashScope: + +``` +status_code: 400 +code: "DataInspectionFailed" +message: "Input data may contain inappropriate content." +``` + +→ 不告诉用户哪段内容触发了审核 + +**场景 4:网络超时** + +OpenAI: + +``` +APITimeoutError: Request timed out. +``` + +→ 简洁明确,可通过 `except APITimeoutError` 精确捕获 + +DashScope: + +``` +requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='dashscope.aliyuncs.com', +port=443): Read timed out. (read timeout=300) +``` + +→ 底层 requests 异常直接泄露,用户需要 `except requests.exceptions.ReadTimeout` 捕获 + +### 6.5 异常可追溯性 + +| 特性 | OpenAI | Anthropic | DashScope | +| --- | --- | --- | --- | +| 异常链 | `raise ... from err` 保留原始异常 | 同 | ❌ 不保留(直接返回 response) | +| 原始请求 | e.request (httpx.Request) | e.request (httpx.Request) | ❌ 不保留 | +| 原始响应 | e.response (httpx.Response) | e.response (httpx.Response) | ❌ 不保留 | +| 请求体 | e.request.content | e.request.content | ❌ 不保留 | +| 响应体 | e.body | e.body | response 本身就是 | +| 堆栈追踪 | ✅ 完整 Python traceback | ✅ 完整 | ❌ 无异常,无 traceback | +| 可序列化 | e.body 是 dict | e.body 是 dict | response 是 dict | + +### 6.6 调试体验对比 + +开启 DEBUG 日志后的输出 + +**OpenAI(信息密度高)**: + +``` +[2026-07-31 14:12:26 - openai._base_client:1033 - DEBUG] Sending HTTP Request: POST https://api.openai.com/v1/chat/completions +[2026-07-31 14:12:27 - openai._base_client:1074 - DEBUG] HTTP Response: POST https://api.openai.com/v1/chat/completions "200 OK" Headers({'content-type': 'application/json', 'x-request-id': 'req_abc123', ...}) +[2026-07-31 14:12:27 - openai._base_client:1082 - DEBUG] request_id: req_abc123 +``` + +**DashScope(信息密度低)**: + +``` +2026-07-31 14:12:26 - dashscope - api_request_factory.py - _build_api_request - 128 - DEBUG - encryption enabled +2026-07-31 14:12:26 - dashscope - base_api.py - call - 548 - DEBUG - Starting request: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation/ +2026-07-31 14:12:27 - dashscope - base_api.py - call - 558 - DEBUG - Starting processing response: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation/ +``` + +→ 不知道响应状态码、不知道 request_id、不知道响应内容 + +### 6.7 DashScope 日志改进建议 + +| # | 改进 | 优先级 | 说明 | +| --- | --- | --- | --- | +| 1 | 添加 SensitiveHeadersFilter | P0 | 过滤 Authorization header,防止密钥泄露 | +| 2 | 日志中记录 request_id | P0 | 每个请求/响应都记录 request_id,便于找阿里云工单 | +| 3 | 日志中记录 HTTP 状态码 | P0 | "HTTP Response: POST ... 200 OK" | +| 4 | 重试日志增加原因和等待时间 | P1 | "Retrying due to 429, waiting 2.5s" | +| 5 | 异常链保留 | P1 | `raise DashScopeError(...) from original_error` | +| 6 | 错误消息增加可操作信息 | P1 | 限流时告知限制值和等待时间 | +| 7 | 超时错误消息添加排查文档链接 | P2 | 参考 Anthropic 的做法 | +| 8 | 日志格式简化 | P2 | 去掉 filename + funcName + lineno(用 %(name)s 即可) | + +### 6.8 错误处理最佳实践建议 + +**当前 DashScope 用户被迫写的代码** + +```python +# 每个调用点都要写这套 — 容易遗漏,容易不一致 +response = Generation.call(model="qwen-plus", messages=[...]) +if response.status_code == 200: + result = response.output.choices[0].message.content +elif response.code == "Throttling": + time.sleep(5) + # 重试... +elif response.code == "InvalidApiKey": + print("Check your DASHSCOPE_API_KEY") +elif response.code == "DataInspectionFailed": + print("Content blocked by safety filter") +else: + print(f"Unknown error: {response.status_code} {response.code}: {response.message}") +``` + +**OpenAI/Anthropic 用户写的代码** + +```python +# 一处 try/except,全局生效 — 不可能遗漏 +try: + response = client.chat.completions.create(model="gpt-4o", messages=[...]) + result = response.choices[0].message.content # 无需检查 status_code +except RateLimitError: + time.sleep(int(e.headers.get("Retry-After", 5))) +except AuthenticationError: + print("Check your API key") +``` + +**如果 DashScope 采用异常驱动后的理想代码** + +```python +from dashscope import DashScope +from dashscope.errors import RateLimitError, AuthenticationError, ContentFilterError + +client = DashScope() + +try: + response = client.chat.completions.create(model="qwen-plus", messages=[...]) + result = response.choices[0].message.content +except RateLimitError as e: + print(f"Rate limited. Limit: {e.limit}, Used: {e.used}, Retry after: {e.retry_after}s") + time.sleep(e.retry_after) +except AuthenticationError: + print(f"Invalid API key. Get yours at https://dashscope.console.aliyun.com/apiKey") +except ContentFilterError as e: + print(f"Content blocked: {e.category}") +``` + +--- + +## 参考 + +- OpenAI Python SDK +- Anthropic Python SDK +- DashScope Python SDK +- Pydantic V2 +- httpx diff --git a/md/sdk-source-level-comparison-part2.md b/md/sdk-source-level-comparison-part2.md new file mode 100644 index 00000000..37637c5b --- /dev/null +++ b/md/sdk-source-level-comparison-part2.md @@ -0,0 +1,397 @@ +# SDK 源码级深度对比(二):openai-python / anthropic-sdk-python vs DashScope SDK + +> 基于真实克隆的源码分析,非文档推测 +> 对比维度:1) 用户接口友好度 2) 错误码/异常体系 3) 流式/非流式、同步/异步 +> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) + +--- + +## 0. 架构血缘(先讲结论) + +| | openai-python | anthropic-sdk-python | dashscope-sdk | +| --- | --- | --- | --- | +| 生成方式 | Stainless 从 OpenAPI spec 生成 | Stainless 从 OpenAPI spec 生成 | 完全手写 | +| HTTP 库 | httpx(_base_client.py 77KB) | httpx(_base_client.py 94KB) | requests(同步)+ aiohttp(异步) | +| 类型系统 | Pydantic 模型(types/ 数千个类) | Pydantic 模型 | dict 子类(DictMixin) | +| 客户端形态 | `OpenAI()` / `AsyncOpenAI()` 实例 | `Anthropic()` / `AsyncAnthropic()` 实例 | `Generation.call()` 类方法 | +| 资源组织 | `client.chat.completions.create()` | `client.messages.create()` | `Generation.call()` 扁平类方法 | + +**关键事实**:OpenAI 和 Anthropic SDK 是同一套 Stainless 框架生成的"孪生架构"(文件名一一对应:_base_client.py、_exceptions.py、_streaming.py、_models.py),两者对比本质是"同一优秀框架 + 各自的 API 设计差异"。DashScope 是完全独立的手写实现,与前两者差距是代际级的。 + +--- + +## 1. 用户接口友好度 + +### 1.1 最简调用:3 行 vs 6 行 + +**OpenAI**(v2.52.0,_client.py:108) + +```python +from openai import OpenAI + +client = OpenAI() # 自动读 OPENAI_API_KEY 环境变量 + +resp = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], +) +print(resp.choices[0].message.content) # 属性访问,IDE 全提示 +``` + +**Anthropic**(v0.120.2,_client.py) + +```python +from anthropic import Anthropic + +client = Anthropic() # 自动读 ANTHROPIC_API_KEY;还支持 OAuth credentials 链 + +msg = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) +print(msg.content[0].text) # 结构化 content blocks +``` + +**DashScope**(aigc/generation.py:49) + +```python +import dashscope +from dashscope import Generation + +dashscope.api_key = "sk-xxx" # 或依赖 DASHSCOPE_API_KEY 环境变量 + +resp = Generation.call(model="qwen-plus", messages=[{"role": "user", "content": "Hello"}]) + +if resp.status_code == 200: # ← 用户必须手动检查! + print(resp.output["choices"][0]["message"]["content"]) +else: + print(resp.code, resp.message) +``` + +### 1.2 接口友好度逐项对比 + +| 维度 | openai-python | anthropic-sdk-python | dashscope-sdk | +| --- | --- | --- | --- | +| 实例化客户端 | ✅ OpenAI() | ✅ Anthropic() | ❌ 类方法,无客户端对象 | +| 零配置(环境变量) | ✅ OPENAI_API_KEY | ✅ ANTHROPIC_API_KEY + OAuth 自动发现 | ⚠️ DASHSCOPE_API_KEY 可用但文档主推手动赋值 | +| 链式资源导航 | ✅ client.chat.completions.create | ✅ client.messages.create | ❌ Generation.call 扁平 | +| IDE 自动补全 | ✅ 每个参数有 @overload 签名 | ✅ 同左 | ❌ 30+ 个 `x: Any = None` 参数(generation.py:49-81) | +| 响应字段类型 | ✅ Pydantic:resp.choices[0].message.content | ✅ 同左 | ❌ dict 索引:resp.output["choices"][0]["message"]["content"] | +| 成功与否判断 | ✅ 失败直接抛异常,成功即可用 | ✅ 同左 | ❌ 必须手写 if resp.status_code == 200 | +| 参数校验 | ✅ @required_args 装饰器 + Pydantic 本地校验 | ✅ 同左 | ⚠️ 仅手动检查 prompt/model 非空(generation.py:135-140) | +| client 上下文管理 | ✅ with OpenAI() as client / client.close() | ✅ 同左 | ❌ 无 | +| 同一 client 切配置 | ✅ client.with_options(timeout=..., max_retries=...) | ✅ 同左 | ❌ 每次调用传参 | +| 裸响应访问 | ✅ client.with_raw_response.chat.completions.create(...) 拿 httpx.Response | ✅ 同左 | ❌ 无(headers 混在 response dict 里) | +| 结构化输出 | ✅ beta.chat.completions.parse(response_format=MyPydanticModel),直接 .parse() 出对象 | ✅ messages.parse() + tools 解析 | ❌ 返回 JSON 字符串自己 json.loads | +| token 计数 | ✅ client.chat.completions.estimate_tokens(离线估算) | ✅ client.messages.count_tokens() 服务端计数 | ❌ 需单独调 tokenization API | +| Provider 多云支持 | ✅ providers/ 目录(Azure/Bedrock/Vertex 等) | ✅ AnthropicAWS / AnthropicFoundry 子类 + credentials 链(_client.py:40-53) | ❌ 单 endpoint | + +### 1.3 参数设计的具体差距 + +DashScope Generation.call 签名(generation.py:49-82): + +```python +def call(cls, model: str, prompt: Any = None, history: list = None, + api_key: str = None, messages: List[Message] = None, + plugins: Union[str, Dict] = None, ..., **kwargs) + -> Union[GenerationResponse, Generator[GenerationResponse, None, None]]: +``` + +问题: + +1. `prompt: Any` — 无类型约束; +2. history 已废弃但仍占签名位; +3. 30+ 个显式参数 + `**kwargs` — 显式参数用 `if x is not None: kwargs["x"] = x` 逐个搬运(generation.py:141-169 重复 20 多次),既不是类型安全也不是省代码; +4. 返回值是 Union — 调用方无法静态区分"拿到响应"还是"拿到生成器",只能靠记得自己传没传 stream=True。 + +对比 OpenAI completions.py:4 个 @overload 分别覆盖(流式/非流式)×(有无 parse),每个 overload 返回精确类型(ChatCompletion / Stream[ChatCompletionChunk]),IDE 根据实参自动推断返回类型——流式与否在类型层面就能区分。 + +--- + +## 2. 错误码与异常体系 + +### 2.1 异常层次(源码实证) + +**openai-python _exceptions.py** + +``` +OpenAIError +├── APIError # message + request + body + code/param/type +│ ├── APIResponseValidationError # 服务端返回不符合 schema +│ ├── APIStatusError # 4xx/5xx;含 response/status_code/request_id(x-request-id) +│ │ ├── BadRequestError (400) +│ │ ├── AuthenticationError (401) +│ │ │ └── OAuthError # 解析 error_description + OAuthErrorCode 枚举 +│ │ ├── PermissionDeniedError (403) +│ │ ├── NotFoundError (404) +│ │ ├── ConflictError (409) +│ │ ├── UnprocessableEntityError (422) +│ │ ├── RateLimitError (429) +│ │ └── InternalServerError (5xx) +│ ├── APIConnectionError # 网络层 +│ │ └── APITimeoutError +├── LengthFinishReasonError # finish_reason=length 的解析失败(带 usage) +├── ContentFilterFinishReasonError # 内容过滤 +└── WebSocket*Error # realtime 专用 +``` + +**anthropic-sdk-python _exceptions.py** + +``` +AnthropicError +├── APIError +│ ├── APIResponseValidationError +│ ├── APIWebhookValidationError +│ ├── APIStatusError # 含 request_id(request-id 头) + type(ErrorType 枚举) +│ │ ├── BadRequestError (400) +│ │ ├── AuthenticationError (401) +│ │ ├── PermissionDeniedError (403) +│ │ ├── NotFoundError (404) +│ │ ├── ConflictError (409) +│ │ ├── RequestTooLargeError (413) ← OpenAI 没有 +│ │ ├── UnprocessableEntityError (422) +│ │ ├── RateLimitError (429) +│ │ ├── ServiceUnavailableError(503) ← 独立出来 +│ │ ├── DeadlineExceededError (504) +│ │ ├── OverloadedError (529) ← Anthropic 特有 +│ │ └── InternalServerError +│ ├── APIConnectionError +│ │ └── APITimeoutError # message 里附官方排查文档链接 +└── RetryableError # 中间件可主动抛它触发重试 +``` + +**dashscope-sdk common/error.py** + +``` +DashScopeException +├── AuthenticationError (pass) +├── InvalidParameter (pass) +├── InvalidTask (pass) +├── UnsupportedModel (pass) +├── ... 共 25 个扁平子类,绝大多数只有 pass +├── AssistantError # 解析 message 里的 JSON 拿 request_id/code/message +├── RequestFailure # request_id/message/name/http_code —— 仅 websocket/服务端主动失败用 +└── TimeoutException +``` + +### 2.2 错误处理模型的本质差异 + +| | openai / anthropic | dashscope | +| --- | --- | --- | +| HTTP 4xx/5xx 行为 | 抛异常(异常驱动) | 不抛异常,返回 status_code != 200 的 response 对象 | +| 错误信息载体 | 异常对象:e.status_code / e.code / e.type / e.request_id / e.body / e.response.headers | response dict:resp.code / resp.message / resp.request_id | +| request_id 暴露 | ✅ 自动从响应头提取挂到异常上 | ✅ 在 response 里 | +| 错误可分类捕获 | ✅ except RateLimitError / except AuthenticationError | ❌ 只能 `if resp.code == "Throttling"` 字符串匹配 | +| 流式中错误 | ✅ SSE error 事件 → 抛 APIError(_streaming.py:70-99) | ⚠️ 生成器 yield 出带 code 的 response,用户得逐个检查 | +| 本地校验异常 | TypeError + Pydantic ValidationError | InputRequired / ModelRequired(自定义但扁平) | +| 重试钩子 | _should_retry() 按状态码判定 | ❌ 无 | + +### 2.3 典型错误处理代码对比 + +**openai / anthropic(异常驱动,按语义捕获)**: + +```python +from openai import OpenAI, RateLimitError, AuthenticationError, BadRequestError + +client = OpenAI() +try: + resp = client.chat.completions.create(model="gpt-4o", messages=msgs) +except RateLimitError as e: + # e.status_code=429, e.request_id 自动可用;SDK 已先自动重试过 2 次 + log.warning("限流, request_id=%s", e.request_id) +except AuthenticationError: + fail_fast("key 无效") +except BadRequestError as e: + # e.body 含完整错误 JSON,e.param 指出问题参数 + log.error("参数错误: %s (param=%s)", e.message, e.param) +# 走到这里 resp 一定是可用的 —— 无需 if 检查 +print(resp.choices[0].message.content) +``` + +**dashscope(返回值驱动,字符串匹配)**: + +```python +resp = Generation.call(model="qwen-plus", messages=msgs) +if resp.status_code != 200: + if resp.code == "Throttling": # ← 魔法字符串,无枚举 + ... + elif resp.code == "InvalidApiKey": + ... + log.error("failed: %s %s", resp.code, resp.message) + raise RuntimeError(resp) +print(resp.output["choices"][0]["message"]["content"]) +``` + +差距清单: + +1. DashScope 错误分支必须手写,漏写就是静默错误(resp.output 是 None,下游 KeyError); +2. resp.code 是魔法字符串,没有枚举类、没有文档常量可 import; +3. 无法用 except 语义化捕获,重试/降级逻辑难以收敛; +4. RequestFailure 只在 websocket 路径抛出,HTTP 路径不抛——同一个 SDK 两种错误契约。 + +### 2.4 重试机制(错误处理的延伸) + +| | openai | anthropic | dashscope | +| --- | --- | --- | --- | +| 默认重试 | DEFAULT_MAX_RETRIES=2,自动 | 同左 | ❌ 核心路径无任何重试(全仓 grep 确认,仅 agentstudio/finetune 子模块有) | +| Retry-After 头 | ✅ _parse_retry_after_header(),上限封顶 | ✅ 同左(上限 60s) | ❌ | +| 退避策略 | 指数退避 + 抖动(_base_client.py:803-827) | 同左 | ❌ | +| 可重试判定 | _should_retry():408/409/429/5xx + retry-after 超限不重试 | _should_retry_exception() 还覆盖连接中途断开 | ❌ | +| 用户可调 | OpenAI(max_retries=5) / with_options(max_retries=...) / 单请求 options | 同左 | ❌ | + +--- + +## 3. 流式 / 非流式、同步 / 异步 + +### 3.1 四种组合的完整度 + +| 能力 | openai-python | anthropic-sdk-python | dashscope-sdk | +| --- | --- | --- | --- | +| 同步非流式 | ✅ | ✅ | ✅ Generation.call() | +| 同步流式 | ✅ Stream[Chunk] 迭代器 | ✅ Stream[Event] 迭代器 | ✅ stream=True 返回生成器 | +| 异步非流式 | ✅ AsyncOpenAI | ✅ AsyncAnthropic | ⚠️ AioGeneration.call() | +| 异步流式 | ✅ AsyncStream,async for | ✅ AsyncStream,async for | ⚠️ 有但生态不完整(见 3.4) | +| 连接复用 | ✅ httpx.Client 连接池 | ✅ 同左 | ❌ requests 每调用新建 | + +### 3.2 流式实现细节对比 + +**openai-python(_streaming.py)** + +```python +stream = client.chat.completions.create(model="gpt-4o", messages=msgs, stream=True) +# stream: Stream[ChatCompletionChunk] —— 类型精确到 chunk 类型 +for chunk in stream: + print(chunk.choices[0].delta.content or "", end="") +``` + +- SSE 解析基于 httpx-sse 解码器,`[DONE]` 哨兵(_streaming.py:63) +- SSE 中的 error 事件会抛 APIError(_streaming.py:87-99)——流中出错不会静默 +- 高级助手 `client.chat.completions.stream()`(lib/streaming/chat/): + - 自动累积 chunk → get_final_completion() + - 事件回调:.on("chunk", fn) / .on("content_done", fn) + - tool_calls 增量自动拼接 + +**anthropic-sdk-python(lib/streaming/_messages.py)** + +```python +with client.messages.stream(model="claude-sonnet-4", max_tokens=1024, messages=msgs) as stream: + for text in stream.text_stream: # ← 只要文本,过滤掉所有事件 + print(text, end="") +msg = stream.get_final_message() # ← 退出 with 后拿完整 Message +``` + +这是三者中最强的流式 API: + +- MessageStreamManager 上下文管理器(_messages.py:146) +- `stream.text_stream`:纯文本增量迭代,用户不用处理 content_block 事件细节 +- `stream.get_final_message()`:拿聚合后的完整消息(含 usage) +- `.on("text", handler)` / `.on("content_block_start", ...)` 事件订阅 +- tool use 流式迭代器:stream 可逐工具调用输出 + +**dashscope-sdk(aigc/generation.py)** + +```python +responses = Generation.call(model="qwen-plus", messages=msgs, + stream=True, incremental_output=True) +for resp in responses: # Generator[GenerationResponse] + if resp.status_code == 200: # ← 每个 chunk 都要检查! + print(resp.output["choices"][0]["message"]["content"], end="") +``` + +- 流式返回的是完整 GenerationResponse 快照序列(不是 delta 类型),chunk 级类型不存在 +- `incremental_output=True` 才是增量,False 是累积快照——语义靠参数切换而不是 API 切换,容易拿错 +- 每个 chunk 都要手写 status_code 检查,无统一的流内错误抛出 +- 无 text_stream 等价物,无事件回调,无 final message 聚合——累积逻辑全部用户自理 + +### 3.3 非流式对比(错误契约见 §2,此处看返回值) + +| | openai | anthropic | dashscope | +| --- | --- | --- | --- | +| 返回类型 | ChatCompletion(Pydantic) | Message(Pydantic) | GenerationResponse(dict 子类 DictMixin) | +| 内容访问 | resp.choices[0].message.content | msg.content[0].text | resp.output["choices"][0]["message"]["content"] | +| 序列化 | .model_dump() / .model_dump_json() | 同左 | json.dumps(resp)(__str__ 自定义) | +| 字段缺失 | AttributeError(清晰) | 同左 | KeyError(DictMixin.__getattr__ 把缺 key 转 AttributeError,但 output 内部仍是裸 dict) | + +### 3.4 异步实现对比 + +| | openai | anthropic | dashscope | +| --- | --- | --- | --- | +| 异步客户端 | AsyncOpenAI(与同步逐方法镜像,自动生成) | AsyncAnthropic(同左) | AioGeneration(独立类,generation.py:320) | +| 底层 | httpx.AsyncClient | httpx.AsyncClient | aiohttp(与同步的 requests 是两套代码) | +| 异步流式 | AsyncStream,`async for chunk in ...` | 同左 | ⚠️ 支持但返回类型标注、示例、工具链覆盖远不完整 | +| 代码一致性 | 同步/异步走同一个 _base_client 泛型基类 | 同左 | 同步/异步两套独立维护(BaseApi vs BaseAioApi,base_api.py:429/495),行为漂移风险高 | +| 并发安全 | client 实例可共享,连接池 | 同左 | 全局模块状态(dashscope.api_key)+ 类方法 | + +### 3.5 一个容易被忽视的点:超时与取消 + +| | openai | anthropic | dashscope | +| --- | --- | --- | --- | +| 超时设置 | OpenAI(timeout=httpx.Timeout(600, connect=5)) / 单请求 .with_options(timeout=...) | 同左 | 调用级 timeout 参数(部分 API 有),无连接/读分离 | +| 请求取消 | httpx 原生支持 + AbortController 风格 | 同左 | ❌ | +| 长请求指引 | — | APITimeoutError message 内嵌官方长请求文档链接 | ❌ | + +--- + +## 4. 评分总表 + +| 维度 | openai-python v2.52 | anthropic-sdk v0.120 | dashscope v1.x | +| --- | --- | --- | --- | +| 零配置上手 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | +| 类型提示/IDE 体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | +| 响应结构可发现性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | +| 异常体系完备度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | +| 错误可操作性(request_id/param/type) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | +| 自动重试 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ | +| 流式 API 易用性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐(text_stream 最佳) | ⭐⭐ | +| 异步完整度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | +| 结构化输出 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | + +--- + +## 5. 对 DashScope SDK 的改进建议(按 ROI 排序) + +### P0 — 契约级修复(破坏性,适合 v2.0) + +1. **异常驱动错误处理**:HTTP/业务失败抛 DashScopeAPIError 层次(按 status_code 细分 RateLimitError 等),保留现有返回值行为做兼容开关。这是与两家对齐的最小改动、最大收益。 +2. **错误码枚举**:把 "Throttling" / "InvalidApiKey" 等字符串收敛为可 import 的枚举常量,文档给出完整对照表。 +3. **内置重试**:核心 HTTP 路径加 max_retries(默认 2)+ Retry-After 解析 + 指数退避;DashScope 网关限流场景下这是用户最痛的点。 + +### P1 — 体验补齐(不破坏兼容) + +4. **实例化客户端**:提供 `dashscope.Client(api_key=...)`,`client.generation.create(...)` 链式导航;类方法保留为 deprecated 别名。 +5. **Pydantic 响应模型**:至少为 Generation/MultimodalConversation 提供 typed response,`resp.choices[0].message.content` 可用。 +6. **流式升级**: + - stream=True 时 chunk 级类型标注(Iterator[GenerationChunk]); + - 提供 text_stream 风格助手(Generation.stream_text(...)),内部处理 incremental_output + status 检查 + 聚合; + - 流内错误统一抛异常而非 yield 错误 response。 +7. **with_raw_response 等价物**:暴露原始 headers(排查问题常需要 x-request-id 之外的网关头)。 + +### P2 — 架构现代化(长期) + +8. **httpx 统一同步/异步**:替换 requests + aiohttp 双栈,消除 BaseApi/BaseAioApi 双份维护。 +9. **代码生成**:从百炼 OpenAPI spec 用 Stainless/自研生成器产出资源类,参数 @overload 化,新模型参数自动同步。 +10. **结构化输出**:response_format=PydanticModel 直接返回解析后对象,对齐 OpenAI parse()。 + +**一句话总结** + +OpenAI 和 Anthropic SDK 是同一套现代框架的两个实例:异常驱动、类型完备、四象限(同步/异步 × 流式/非流式)全覆盖。DashScope SDK 是手写时代的产物——功能覆盖不输(甚至更多:异步任务/RL/多模态),但错误契约、类型系统、流式人体工学全面落后一代。优先修 P0 三项(异常驱动 + 错误码枚举 + 自动重试),即可消除 80% 的用户抱怨。 + +--- + +## 附:源码证据索引 + +| 结论 | 文件:行 | +| --- | --- | +| OpenAI 异常层次 | openai-python/src/openai/_exceptions.py:36-163 | +| Anthropic 异常层次(含 529/RetryableError) | anthropic-sdk-python/src/anthropic/_exceptions.py:25-153 | +| OpenAI 重试/Retry-After | openai-python/src/openai/_base_client.py:759-827 | +| Anthropic 重试(含连接中断重试) | anthropic-sdk-python/src/anthropic/_base_client.py:824-896,1130-1162 | +| OpenAI SSE 流内错误抛异常 | openai-python/src/openai/_streaming.py:63-99 | +| Anthropic text_stream/get_final_message | anthropic-sdk-python/src/anthropic/lib/streaming/_messages.py:56-237 | +| DashScope 30+ 无类型参数 | dashscope/aigc/generation.py:49-82 | +| DashScope 手动 kwargs 搬运 | dashscope/aigc/generation.py:141-169 | +| DashScope 扁平异常(pass 体) | dashscope/common/error.py:5-146 | +| DashScope 返回值式响应 | dashscope/api_entities/dashscope_response.py:65-129 | +| DashScope 无核心重试 | 全仓 `grep -rn retry dashscope/client dashscope/protocol` 无结果 | +| DashScope 异步独立栈 | dashscope/client/base_api.py:429(BaseAioApi)/495(BaseApi) | diff --git a/md/self-diagnostic-exception.md b/md/self-diagnostic-exception.md new file mode 100644 index 00000000..e3cf373f --- /dev/null +++ b/md/self-diagnostic-exception.md @@ -0,0 +1,75 @@ +# 智能诊断异常(Self-Diagnostic Exception)— dashscope 2.0 设计 + +> 状态:已落地(v2,Tier 0)。对应实现:`dashscope/v2/devkit/diagnosis.py`,挂载点 `dashscope/v2/errors.py`(`DashScopeError.diagnosis`)与 `dashscope/v2/types/common.py`(`ResponseBase.diagnosis`)。 +> 来源:1.x 提案 `self-diagnostic-exception`,针对 2.0 错误体系重写。 + +## 1. 需求与目标 + +传统报错只抛冷冰冰的 HTTP 状态码(400/429),开发者要复制错误去查文档或问 AI。目标:捕获失败响应时,SDK 在本地匹配出结构化、「有人情味」的修复建议,零延迟、零成本、离线可用、无隐私外发。 + +终端体感(新加坡地域未配 Workspace 导致报错): + +```text +================================================================================ +💡 [DashScope 智能诊断] 请求未匹配到有效的百炼工作空间(Workspace) + +🔍 可能的原因与修复方法: + 1. 地域(Region)不匹配:当前走默认北京地域,资源可能在新加坡等其他地域 + 👉 构造客户端时指定 dashscope.Client(region='ap-southeast-1') + 2. 多租户模式下缺少 Workspace 标识 + 👉 设置环境变量 DASHSCOPE_WORKSPACE_ID,或在 Client(workspace=...) / 调用时传入 + +🛠️ 快捷命令:echo $DASHSCOPE_WORKSPACE_ID # 确认工作空间已配置 + +🤖 仍未解决?运行 dashscope 进入交互提问,或一次性提问:dashscope expert "<贴入错误信息>" +================================================================================ +``` + +## 2. 可行性结论 + +**可行,且默认路径不需要额外大模型调用。** DashScope 错误码有限可枚举,一张本地规则表(`(status_code, error code, message pattern)` → 结构化修复建议)即可覆盖绝大多数高频场景。业界先例全是静态规则(Python 3.12 "Did you mean"、Azure CLI 命令建议、Stripe 错误里的文档链接),没有一家在异常路径上同步调大模型。 + +## 3. 接入点:2.0 与 1.x 的差异 + +1.x 挂在 `DashScopeException` 与 `DashScopeAPIResponse` 两处(分别是抛出路径与返回路径)。 + +2.0 是双层错误模型,对应两个挂载点: + +- **抛出路径**(`raise_on_error=True`,默认):所有错误都是 `dashscope.v2.errors.DashScopeError` 子类,携带 `.status_code` / `.code` / `.message`。在基类加 `.diagnosis` 属性,全部子类(含 `SdkError`、`APIStatusError` 家族、网络类 `APIConnectionError`/`APITimeoutError`)自动继承。 +- **返回路径**(`raise_on_error=False`):失败以错误信封返回,即 `ResponseBase`(pydantic)带 `code`/`message`/`status_code`/`is_error`。在 `ResponseBase` 加 `.diagnosis` 属性,仅当 `is_error` 为真时匹配,成功响应返回 `None`、零开销。 + +两处都**懒导入** `diagnosis.diagnose`,成功路径不加载 devkit、不引入循环依赖(`diagnosis.py` 不反向依赖 errors/types)。 + +## 4. 工程约束(必须避开的坑) + +1. **SDK 不直接 print**:库直接打终端会污染 stdout、破坏可组合性。诊断以结构化数据挂在对象上(`err.diagnosis` / `resp.diagnosis`),由 CLI 或 `install_excepthook()` 渲染,且只写 stderr。 +2. **429 成本放大器**:限流在重试循环里高频爆发,每次触发一次付费诊断既烧钱又加剧限流。本地规则表天然无此问题。 +3. **隐私合规**:错误消息常带请求片段;本地匹配不外发,天然规避。 + +## 5. 分层设计 + +- **Tier 0(默认,已落地)**:本地规则库。按 `(status_code, code keyword, message pattern)` 匹配,规则「最specific 优先、首个命中即返回」,输出原因列表 + 修复片段 + 快捷命令 + AI expert 升级提示。已覆盖 Top 高频错误:鉴权、workspace/region、限流、模型不存在、越权、内容审核、参数非法、网络、服务端内部错误。 +- **Tier 1(未做,opt-in 兜底)**:规则未命中且用户显式开启时才调轻量诊断模型;对 429 类高频错误必须禁用或强去重。当前统一引导 `dashscope expert ""`。 + +## 6. 双语 + +规则表中英双语。`DASHSCOPE_DIAGNOSIS_LANG=en` 选英文,默认中文。`render(diagnosis, lang=None)` 输出终端块,结尾附 AI expert 升级提示。 + +## 7. 公开 API + +```python +from dashscope.v2.devkit import diagnose, render, install_excepthook, Diagnosis + +diag = diagnose(status_code=401, code="InvalidApiKey", message="no api key provided") +print(render(diag)) # 终端块(中英由 DASHSCOPE_DIAGNOSIS_LANG 决定) + +install_excepthook() # 未捕获异常 traceback 后追加诊断块(仅 stderr,幂等) + +# 挂载点: +err.diagnosis # DashScopeError 及全部子类 +resp.diagnosis # raise_on_error=False 的错误信封(成功时为 None) +``` + +## 8. 测试 + +`tests/v2/test_devkit_diagnosis.py`:按 status/code/message 三种维度匹配、未命中返回 None、env 切英文、render 含标题与 expert 提示与编号修复、`DashScopeError.diagnosis`、`SdkError` 继承、错误信封 `.diagnosis`(业务错误命中 / 成功为 None)、`install_excepthook` 渲染到 stderr。 diff --git a/md/vendor-sdk-research-compat.md b/md/vendor-sdk-research-compat.md new file mode 100644 index 00000000..b91850fa --- /dev/null +++ b/md/vendor-sdk-research-compat.md @@ -0,0 +1,63 @@ +# 国内/第三方大模型厂商 SDK 调研:接入百炼时 dashscope SDK/CLI 的兼容考量 + +> 调研对象:MiniMax、DeepSeek、Baseten、Kimi(Moonshot) +> 调研时间:2026-08-24 +> 问题:这些厂家的模型接入百炼(DashScope 平台)后,dashscope SDK/CLI 需要兼容什么,还是不需要? + +--- + +## 1. 四家 API/SDK 形态速览 + +| 厂商 | 协议形态 | 认证 | 自有 SDK | 特有扩展 | +| --- | --- | --- | --- | --- | +| **DeepSeek** | OpenAI/Anthropic 双兼容(官方原话 "API format compatible with OpenAI/Anthropic"),base_url `https://api.deepseek.com` + `/chat/completions` | Bearer token | 无自有 SDK,直接用 OpenAI SDK 改 base_url | `thinking: {type: enabled}` 思考模式、`reasoning_effort` 推理深度 | +| **Kimi(Moonshot)** | 完全 OpenAI 兼容(chat completions、models、token 估算、余额查询、files),改 base_url 即可 | Bearer token | 官方推荐用 openai 包 + 自定义 base_url,有最低版本要求 | `thinking` 参数走 `extra_body`;partial 标记挂在消息上而非请求级 | +| **MiniMax** | 文本模型支持 **Anthropic SDK 和 OpenAI SDK 双接入**;非文本能力(视频、语音合成/声音克隆、图片、音乐、文档处理)走自有 REST API | API Key(按量)或 Subscription Key(预付)双形态 | 无统一自有 SDK,文本靠第三方 SDK + MCP | 语音合成走 socket 流式;异步媒体任务返回错误码 | +| **Baseten**(推理平台,非国内厂商) | OpenAI 兼容端点 + beta 的 Anthropic Messages 协议 | Bearer token(需覆盖 SDK 默认 key 传法) | 无自有,用 OpenAI/Anthropic 官方 SDK + model slug | 标准 HTTP 错误码 + JSON 错误体 | + +**共同事实:四家在"对话/推理"这条主线上全部收敛到 OpenAI 兼容协议**(MiniMax/Baseten 额外提供 Anthropic 协议)。没有一家要求接入方实现自己的私有 chat 协议。 + +--- + +## 2. 百炼侧现状 + +百炼托管第三方模型(DeepSeek、Kimi 等已在列)提供两条通路: + +1. **OpenAI 兼容模式**:`base_compatible_api_url`(`/compatible-mode/v1`),第三方模型的主推通路; +2. **DashScope native 协议**:部分第三方模型同时以 native 形态暴露(走 Generation 类接口)。 + +即:**厂商协议的差异在百炼网关侧已被翻译成上述两种形态之一**,dashscope SDK 面对的从来不是厂商原始端点,而是百炼网关。 + +--- + +## 3. 结论:需要做专门兼容吗?——基本不需要,但有 6 个具体落点 + +**总判断**:不需要为任何一家厂商做专属 adapter/资源类。厂商协议已行业收敛(OpenAI 为主、Anthropic 为辅),2.0 方案 §3 的 `protocol=openai|anthropic|native` 三适配器就是覆盖这四家(及未来所有厂商)的通用机制。真正要做的是把"厂商特有语义"在通用协议里表达出来: + +| # | 落点 | 说明 | 对应 2.0 方案 | +| --- | --- | --- | --- | +| 1 | **推理/思考参数的透传与一等化** | DeepSeek `thinking`/`reasoning_effort`、Kimi `thinking`(走 extra_body)在百炼兼容模式下同样存在。SDK 需要:`extra_body`/未知参数透传逃生舱(默认);高频参数如 `enable_thinking`/`reasoning_effort` 升级为显式签名参数,思考内容在响应模型中有字段(`reasoning_content`),流式中单独事件化 | §2.2 类型化 + 逃逸舱 | +| 2 | **模型目录可发现性** | 第三方模型在百炼的 model id 与厂商原始 id 不同(带平台前缀/版本)。`client.models.list()` 需支持按 provider/来源过滤,文档给出"厂商名 → 百炼 model id"对照表;CLI `dashscope models list --provider deepseek` | §2.2 models 资源 | +| 3 | **错误语义映射** | 厂商错误码(DeepSeek/Kimi 的结构化 error 对象)经百炼网关后会变成 DashScope 字符串 code。异常层需保证兼容模式下也能拿到可读错误;`ErrorCode` 枚举覆盖第三方模型常见失败(内容审核、配额、模型不存在) | §2.2 错误模型 | +| 4 | **多 API Key 形态**(仅 MiniMax 相关) | MiniMax 有 API Key / Subscription Key 双形态;百炼侧统一为一个 key,**无需兼容**。但若用户用 `protocol=openai` 直连厂商端点(SDK 支持任意兼容端点),key 形态对 SDK 透明(都是 Bearer),也无需处理 | 无需改动 | +| 5 | **非 chat 厂商能力不做兼容** | MiniMax 的视频生成/声音克隆/音乐、Baseten 的自定义模型部署,是厂商私有能力。百炼若要提供,会以 dashscope native 异步任务形态落地(走现有 images/videos/audio 资源树 + jobs 模式),**不是协议兼容问题** | §2.2 长任务统一模式 | +| 6 | **CLI 文档与示例** | `dashscope chat create -m deepseek-...` / `-m kimi-...` 直接可用是卖点,但需示例展示厂商特有参数(`--extra-body '{"thinking":{"type":"enabled"}}'`)和 compatible-mode 与 native 的选择指引 | §4 CLI | + +## 4. 与 2.0 方案的校验 + +- §3 协议层设计(openai/anthropic/native 三 adapter + `strict` 参数翻译策略)**足以覆盖这四家**,无需新增"vendor adapter"维度; +- 唯一需要在 §3 补充的设计细节:**"未知参数透传"必须是默认行为**(strict=False 静默透传而非丢弃)——厂商新参数(如 reasoning 类)上线快于 SDK 发版,透传是兼容性保险; +- acli/expert 侧:providers 已按协议接入,第三方模型在 expert 里天然可用,无额外工作。 + +## 5. 一句话总结 + +**不需要为 MiniMax/DeepSeek/Baseten/Kimi 做专门兼容**——它们的对话协议已全部收敛到 OpenAI/Anthropic 兼容,百炼网关又做了一层收敛。dashscope 2.0 要做的是三件通用事:协议适配层(已规划)、厂商特有参数的透传 + 高频参数一等化(reasoning/thinking)、第三方模型在 models 目录中的可发现性。 + +--- + +## 参考 + +- MiniMax API Overview:https://platform.minimax.io/docs/api-reference/api-overview +- DeepSeek API Docs:https://api-docs.deepseek.com/ +- Baseten Inference APIs:https://docs.baseten.co/inference/model-apis/overview +- Kimi API Overview:https://platform.kimi.ai/docs/api/overview diff --git a/md/zero-cost-local-sandbox.md b/md/zero-cost-local-sandbox.md new file mode 100644 index 00000000..1e0aee7c --- /dev/null +++ b/md/zero-cost-local-sandbox.md @@ -0,0 +1,76 @@ +# 零成本本地沙箱(Zero-Cost Local Sandbox)— dashscope 2.0 设计 + +> 状态:已落地(v2)。对应实现:`dashscope/v2/devkit/mock.py`,挂载点 `dashscope/v2/transport/http.py`。 +> 来源:1.x 提案 `zero-cost-local-sandbox`(方式 A),针对 2.0 资源式架构重写,非简单迁移。 + +## 1. 需求与目标 + +开发者用大模型 Agent 辅助写代码时,Agent 会反复跑单测、调试循环逻辑。每次都打云端 API 带来三个问题:账单激增、触发限流、受外网时延拖累。目标:调试阶段一行开启 mock,零云端 token 消耗,业务代码零改动。 + +```python +import dashscope.v2 as v2 + +v2.enable_dev_mock(mode="static") # 或环境变量 DASHSCOPE_DEV_MOCK=static +client = v2.Client(api_key="sk-xxx") +resp = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "写一首关于阿里云百炼的诗"}], +) +print(resp.output_text) # "[MOCK] static response for model=qwen-plus" +``` + +## 2. 拦截点:2.0 与 1.x 的关键差异 + +1.x 的唯一 choke point 是 `BaseApi.call`,mock 在此短路并直接返回**已解析的** `DashScopeAPIResponse`。 + +2.0 没有 `BaseApi.call`:每个资源(`chat.completions` / `multimodal.chat` / `embeddings` / …)各自构造请求,统一经 **transport 层** 的四个方法出口: + +``` +SyncTransport.request / open_stream +AsyncTransport.request / open_stream +``` + +transport 返回的是**原始 `httpx.Response`**(wire 字节),协议解析(native / openai / anthropic 适配器 + 类型化模型)在其后的资源层完成。 + +因此 v2 mock 的正确落点是 transport,且**合成的是 wire 响应而非成品对象**——真实解析管线原样跑在 mock 数据上。这比 1.x 更忠实:dev loop 验证的是与生产完全一致的解码路径(协议适配器、SSE 分帧、typed model 构造、流式聚合)。 + +**生产零成本挂载**:transport 每次发送前调 `_dev_mock_requested()`——一次 `DASHSCOPE_DEV_MOCK` 环境变量读取 + 一次 `sys.modules.get("dashscope.v2.devkit")` 探测;只有旗标为真时才 `from dashscope.v2 import devkit`。未开启时不导入 devkit、成功路径无额外依赖。真实发送逻辑抽成 `_*_impl`,mock 分支用一个 `real` 闭包(`functools.partial`)持有它,供 record 模式透传。 + +## 3. 三模式语义 + +- **static**:零网络。按**请求 path 推断协议**(与适配器同源信号),合成该协议能正确解析的 wire body: + - native:`/services/aigc/...generation` → `output.choices[].message` 形态; + - openai:`/chat/completions` → `choices[].message` + `usage`; + - anthropic:`/messages` → `content[]` blocks + `stop_reason`。 + 文本打上抹不掉的 `[MOCK]` 前缀,`request_id="mock-*"`,usage 给确定性估算。 +- **record**:透传真实调用,把 (请求指纹 → 原始 wire 字节) 追加写入 JSONL fixture;**headers/Authorization 一律不落盘**(只存 status_code / content_type / body 文本)。流式用 tee 字节流边转发边捕获,保持直播不缓冲。 +- **replay**:按请求指纹(method + path + body + params 的 sha256,**排除 headers**)查 fixture 回放,重建 `httpx.Response`;**未命中 = 硬报错** `DevMockMiss`(继承 `DashScopeError`),绝不静默走网络。record/replay 与协议无关,覆盖**所有**资源。 + +## 4. 覆盖边界:宁可loud,不可静默错配 + +static 只建模 chat / multimodal 三个协议的 wire 形态。对非 chat 端点(embeddings / images / files / …),static **抛 `DevMockMiss`** 明确提示改用 record/replay,而不是返回一个会被该端点 typed parser 静默误读的 body。这正是 1.x 提案「别让 Agent 在静默错配里空转」原则的 v2 落地。 + +## 5. 流式 + +static 按 `stream=True` 产出增量 SSE 帧序列(native 多帧末片 `finish_reason=stop` 带 usage;openai 追加 `data: [DONE]`;anthropic 走 `message_start → content_block_delta… → message_delta → message_stop`),使 `for chunk in stream` / `async for` 的消费与聚合路径与真实 SSE 完全一致——不接受「只 mock 非流式」。 + +## 6. 明确不做 + +- websocket 通路(realtime 语音 / 多模态对话)的 mock; +- 方式 B(环境变量桥接本地 Ollama)的 native↔OpenAI 翻译 shim——新代码走 OpenAI client 指本地即可,存量 native 翻译单独立项; +- SDK 侧影子流量。 + +## 7. 公开 API + +```python +dashscope.v2.enable_dev_mock(mode="static" | "replay" | "record", fixture_path=None) +dashscope.v2.disable_dev_mock() +dashscope.v2.dev_mock_active() -> bool +# 等价物:dashscope.v2.devkit.{enable,disable,active,intercept,aintercept} +``` + +环境变量:`DASHSCOPE_DEV_MOCK=static|replay|record`、`DASHSCOPE_DEV_MOCK_FIXTURE=`(replay/record 默认 `./dashscope_mock.jsonl`)。代码调用优先于环境变量。 + +## 8. 测试 + +`tests/v2/test_devkit_mock.py`:static 非流式/流式、三协议、无网络证明(指向 dead host)、多模态 path、非 chat 端点 loud miss、env 激活、非法 mode 报错、disable 恢复透传、record 落盘不含凭证、record→replay 往返(同步+异步、流式+非流式)、replay 未命中抛 `DevMockMiss`。 From f166d3dcda1971f6d9cc2b4eb4544d4b8db8f528 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 27 Oct 2026 13:42:53 +0800 Subject: [PATCH 14/41] refactor(error): add deprecation markers to 1.x error classes - Added @deprecated decorator to mark all 1.x error classes - Created _legacy_error utility function to handle deprecation warnings uniformly - Preserved backward compatibility; existing except clauses continue to work - Guided users toward the new dashscope.errors (2.0) exception hierarchy - Added deprecation docstrings to all error classes --- dashscope/legacy_1x/common/error.py | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/dashscope/legacy_1x/common/error.py b/dashscope/legacy_1x/common/error.py index 73efc01e..e65f29e9 100644 --- a/dashscope/legacy_1x/common/error.py +++ b/dashscope/legacy_1x/common/error.py @@ -1,5 +1,22 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""The 1.x exception family (pending deprecation). + +Every class here belongs to the 1.x API surface and is *going to be* +deprecated along with it: instantiation emits a +:exc:`PendingDeprecationWarning` (ignored by Python's default filters, +so current 1.x users see no noise), and static checkers flag the names +via ``__deprecated__``. New code should use the 2.0 family in +:mod:`dashscope.errors` (``DashScopeError`` and its subclasses). The +1.x classes stay fully supported and importable so existing ``except`` +clauses keep working. + +``DashScopeException`` carries no marker at all: it is re-exported from +the neutral layer (:mod:`dashscope._shared._errors`) that both stacks +inherit from. +""" + +from typing_extensions import deprecated # The base class lives in the neutral layer so the AgentStudio sub-SDK # (owned by the 2.0 package) and the 1.x classes below inherit from one @@ -7,62 +24,92 @@ from dashscope._shared._errors import DashScopeException +def _legacy_error(name): + """Pending-deprecation marker for one 1.x error class. + + Warns on instantiation (and subclasses), never on import, so the + SDK's own 1.x modules can keep importing these names silently. + """ + return deprecated( + f"dashscope.common.error.{name} is part of the 1.x API, which " + f"is scheduled for deprecation and will be removed in a future " + f"release; use the dashscope.errors (2.0) exception family " + f"instead.", + category=PendingDeprecationWarning, + ) + + +@_legacy_error("AuthenticationError") class AuthenticationError(DashScopeException): pass +@_legacy_error("InvalidParameter") class InvalidParameter(DashScopeException): pass +@_legacy_error("InvalidTask") class InvalidTask(DashScopeException): pass +@_legacy_error("UnsupportedModel") class UnsupportedModel(DashScopeException): pass +@_legacy_error("UnsupportedTask") class UnsupportedTask(DashScopeException): pass +@_legacy_error("ModelRequired") class ModelRequired(DashScopeException): pass +@_legacy_error("InvalidModel") class InvalidModel(DashScopeException): pass +@_legacy_error("InvalidInput") class InvalidInput(DashScopeException): pass +@_legacy_error("InvalidFileFormat") class InvalidFileFormat(DashScopeException): pass +@_legacy_error("UnsupportedApiProtocol") class UnsupportedApiProtocol(DashScopeException): pass +@_legacy_error("NotImplemented") class NotImplemented(DashScopeException): # pylint: disable=redefined-builtin pass +@_legacy_error("MultiInputsWithBinaryNotSupported") class MultiInputsWithBinaryNotSupported(DashScopeException): pass +@_legacy_error("UnexpectedMessageReceived") class UnexpectedMessageReceived(DashScopeException): pass +@_legacy_error("UnsupportedData") class UnsupportedData(DashScopeException): pass +@_legacy_error("AssistantError") class AssistantError(DashScopeException): def __init__(self, **kwargs): self.message = None @@ -88,6 +135,7 @@ def __str__(self): # for server send generation or inference error. +@_legacy_error("RequestFailure") class RequestFailure(DashScopeException): def __init__( self, @@ -111,37 +159,46 @@ def __str__(self): return msg +@_legacy_error("UnknownMessageReceived") class UnknownMessageReceived(DashScopeException): pass +@_legacy_error("InputDataRequired") class InputDataRequired(DashScopeException): pass +@_legacy_error("InputRequired") class InputRequired(DashScopeException): pass +@_legacy_error("UnsupportedDataType") class UnsupportedDataType(DashScopeException): pass +@_legacy_error("ServiceUnavailableError") class ServiceUnavailableError(DashScopeException): pass +@_legacy_error("UnsupportedHTTPMethod") class UnsupportedHTTPMethod(DashScopeException): pass +@_legacy_error("AsyncTaskCreateFailed") class AsyncTaskCreateFailed(DashScopeException): pass +@_legacy_error("UploadFileException") class UploadFileException(DashScopeException): pass +@_legacy_error("TimeoutException") class TimeoutException(DashScopeException): pass From 4d401a081932793837d90832161b070f0d426a15 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 15:12:29 +0800 Subject: [PATCH 15/41] docs(acli): update expert guidance for SDK 2.0 --- .../.acli/skills/agent.md | 193 +++++++++----- .../.acli/skills/api-doc.md | 17 +- .../dashscope-sdk-expert/.acli/skills/cli.md | 24 +- .../.acli/skills/diagnose.md | 12 +- .../.acli/skills/error-code.md | 74 +++++- .../.acli/skills/fine-tuning.md | 199 +++++++++----- .../.acli/skills/multimodal.md | 222 ++++++++++++---- .../.acli/skills/retrieval.md | 250 +++++++++++++----- .../.acli/skills/sdk-example.md | 17 +- .../.acli/skills/speech.md | 215 ++++++++++----- .../.acli/skills/text-generation.md | 250 +++++++++++------- .../.acli/system-prompt.md | 79 +++--- .../examples/dashscope-sdk-expert/README.md | 24 +- md/dashscope-2.0-design.md | 14 +- 14 files changed, 1097 insertions(+), 493 deletions(-) diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md index 75762b58..858b82ad 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md @@ -4,83 +4,142 @@ description: Quick reference for Agent APIs (agentstudio-hosted Agent/Applicatio --- # Agent +For Python SDK 2.0.3, use `dashscope.Client` / `dashscope.AsyncClient` at the package root; `dashscope.v2` does not exist. Close the client with a context manager or `close()`/`aclose()`. New application calls use `client.apps.completions`; hosted-agent management uses the lazily mounted `client.agentstudio`. Assistants APIs below are exclusively for 1.x maintenance. + ## Use Cases and Selection -- `dashscope.Application`: calls applications published in the Bailian console (AppBuilder/RAG/workflow/plugin apps) directly by `app_id`; no need to manage models or tools — the simplest option. -- `dashscope.agentstudio`: full lifecycle management of AgentStudio-hosted Agents (agents/sessions/environments/files/skills/deployments), with SSE event streaming, client-side tools and MCP; suitable when you need a hosted runtime and multi-turn sessions. -- `dashscope.Assistants/Threads/Runs`: OpenAI-like Assistants API. **Deprecated** (DeprecationWarning in source; migrate to the Responses API); use only for maintaining legacy code. - -## SDK APIs -### agentstudio (`from dashscope.agentstudio import Client, AsyncClient`) -- `Client(api_key=, workspace=, region=None, base_url=None, uid=None, timeout=None, max_retries=2)`; if base_url is not given, `workspace` (or env var `DASHSCOPE_WORKSPACE`) is required; base_url can also be set via `DASHSCOPE_AGENTSTUDIO_URL`/`AGENTSTUDIO_URL`; default region `cn-beijing`, timeout 600s. -- Resource entry points: `client.agents / .sessions / .environments / .files / .skills / .vaults / .deployments / .deployment_runs`. -- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, multiagent=None, metadata=None)` → `Agent`; `model` is a `ModelConfig(id=…, temperature=…, max_tokens=…, enable_thinking=…, thinking_budget=…)` (bare id string = `ModelConfig(id=…)`); `retrieve(agent_id, *, version=None)` (alias `get`); `update(agent_id, *, version, name, model=None, ...)` — version must equal the current version (HTTP 409), `name` is required (`AGENT_005`), and a sent `model` replaces the whole config (keep the rest via `agent.model.copy_with(...)`); `archive(agent_id)`; `list(*, limit, page, include_archived)` → `CursorPage`; `list_versions(agent_id)`. -- `sessions.create(*, agent, environment_id=None, title=None, resources=None, metadata=None)` → `Session`; also `retrieve/update(title, metadata)/list(*, limit, page, agent_id, statuses, created_at_gt/gte/lt/lte)/archive/delete`. -- `environments.create(*, name, config, description=None, scope="organization", metadata=None)`; `files.upload(file, *, mime_type=None, progress=None)` (file accepts a path/binary stream/(name, fileobj)); `skills.create(*, file_id=None, file=None, mime_type=None)`, `skills.versions.create(skill_id, ...)`. - -### agentstudio SSE Streaming Execution (Core) -1. `client.sessions.events.send(session_id, events)`: sends an event list (at least 1 event, otherwise `ValueError`); request body is `{"input": [...]}`. Use the constructors in `types`: `user_message(text_or_blocks, *, session_thread_id=None, metadata=None)`, `user_interrupt()`, `user_tool_confirmation()`, `user_tool_result()`, `user_custom_tool_result()`, `user_define_outcome()`. -2. `with client.sessions.events.stream(session_id, *, timeout=None) as stream:` opens the SSE stream (GET `/sessions/{id}/events/stream`); iterating yields `Message` (`ServerEvent`) objects: `event.type`, `event.content` (block list), `event.stop_reason`, `event.session_status`. -3. `stream.text_stream`: yields only the text of blocks with `type=="text"` from `message` events, and stops automatically when `session_status` becomes `idle/terminated/rescheduling` — the most convenient option. -- Event types (`SSEEventType`): clients may send `message/interrupt/tool_confirmation/function_call_output/tool_call_output/define_outcome`; the server additionally sends `reasoning/tool_call/tool_call_output/mcp_call/mcp_call_output/session_status/error/thread_*/model_request_*/outcome_evaluation`, etc. Session state machine: `idle → running → (rescheduling) → idle | terminated`. - -### Application (Bailian App) -`Application.call(app_id, prompt=None, history=None, workspace=None, api_key=None, messages=None, **kwargs)`; `app_id` is required (raises `InputRequired` if missing); at least one of `prompt` or `messages` is required. kwargs: `stream`, `incremental_output`, `session_id` (for multi-turn continuation, pass the session_id from the previous response), `biz_params` (flow/plugin business parameters), `has_thoughts`, `doc_tag_codes`, `doc_reference_type` (`simple`/`indexed`), `memory_id`, `image_list`, `file_list`, `rag_options`, `temperature/top_p/top_k/seed`. - -### Assistants / Threads / Runs (Deprecated) -- `Assistants.call/create(*, model, name=None, description=None, instructions=None, tools=None, file_ids=[], metadata=None, workspace=None, api_key=None, top_p/top_k/temperature/max_tokens=None)`; `retrieve/list/update/delete`. -- `Threads.create(*, messages=None, metadata=None)`; `Messages.create(thread_id, *, content, role="user", file_ids=[], metadata=None)`. -- `Runs.create(thread_id, *, assistant_id, instructions=None, tools=None, stream=False, ...)`; `Runs.create_thread_and_run(*, assistant_id, thread=None, stream=False, ...)`; `Runs.retrieve(run_id, *, thread_id)`; `Runs.submit_tool_outputs(run_id, *, thread_id, tool_outputs, stream=False)`; `Runs.cancel(...)`. -## Inputs/Outputs -- **Application**: returns `ApplicationResponse` (`status_code/request_id/code/message`); `output` is an `ApplicationOutput`: `text` (answer text), `finish_reason`, `session_id` (returned for multi-turn), `thoughts` (plugin/RAG process when has_thoughts=True), `doc_references`, `workflow_message`; `usage.models[]` contains `model_id/input_tokens/output_tokens`. With `stream=True` a generator is returned; with `stream=True` and `incremental_output=False` the SDK automatically merges deltas into full output. **Failures do not raise exceptions** — you must check `response.status_code == HTTPStatus.OK`, otherwise read `code/message`. -- **agentstudio event stream**: each event is a `Message` (fields `id/type/role/content/metadata/status/session_thread_id/...`); `content` is a block list with block.type ∈ `text/image/audio/data/file/refusal/error`. The block.data of a `session_status` event contains `session_status` and `stop_reason` (e.g. `{"type": "end_turn"}`). -- **Assistants Run state machine**: `queued → in_progress → (requires_action) → completed | failed | cancelled | expired` (plus `cancelling`); on `requires_action`, take tool calls from `run.required_action.submit_tool_outputs`, execute them, send results back via `Runs.submit_tool_outputs`, and poll `retrieve` until `completed`. +- **Bailian applications:** `client.apps.completions.create(...)` invokes an AppBuilder/RAG/workflow/plugin application by `app_id`; application tools and flows remain configured in the console. +- **AgentStudio:** `client.agentstudio` manages hosted agents, sessions, environments, files, skills, deployments, and related resources, with typed models and SSE execution streams. +- **Assistants / Threads / Messages / Runs / Steps:** deprecated 1.x compatibility APIs for repairing existing applications only. There is no 2.0 `Responses` resource, so do not recommend migrating these APIs to Responses. Choose Apps or AgentStudio for new hosted-agent scenarios based on the required service, not a nonexistent resource. + +## Bailian Applications (`client.apps`) + +```text +client.apps.completions.create( + *, app_id, prompt=None, messages=None, stream=False, session_id=None, + biz_params=None, has_thoughts=None, ..., parameters=None, timeout=None, +) +``` -## Plugin and MCP Integration -- agentstudio: pass a list of function/plugin schemas to `tools` of `agents.create` and a list of MCP server configs to `mcp_servers`; the runtime automatically emits `tool_call`/`mcp_call` events; after executing local tools, send results back with `user_tool_result(...)`/`user_tool_confirmation(...)` events. -- Application: plugins are bound to the app in the Bailian console; callers only pass plugin/flow parameters via `biz_params`; set `has_thoughts=True` to see the invocation process. -- Assistants: `tools=[{"type": ...}]`, combined with the Run `requires_action` loop. +Supply `prompt` or `messages`; when both are present, `prompt` becomes the latest user message. Other supported options include document/memory/image/file inputs, `rag_options`, `incremental_output`, model controls, and service-specific `parameters`. For a continued conversation, pass the previous result's `session_id`. -## Minimal Examples ```python -from http import HTTPStatus -from dashscope import Application -resp = Application.call(app_id="YOUR_APP_ID", prompt="Introduce Bailian", has_thoughts=True) -if resp.status_code == HTTPStatus.OK: - print(resp.output.text, resp.output.session_id) -else: - print(resp.request_id, resp.code, resp.message) +import dashscope + +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + result = client.apps.completions.create( + app_id="YOUR_APP_ID", + prompt="Introduce Bailian", + has_thoughts=True, + ) + print(result.text) + print(result.session_id, result.finish_reason) ``` + +`AppCompletion` exposes flat `text`, `finish_reason`, `session_id`, `thoughts`, `doc_references`, `workflow_message`, and `usage`, together with common response metadata. It does not require legacy `.output.text` access. + +With `stream=True`, the return value is `AppStream` (`AsyncAppStream` on `AsyncClient`). Iterate `text_stream` for text and close the stream when finished, especially on early exit: + ```python -from dashscope.agentstudio import Client +import dashscope + +with dashscope.Client() as client: + stream = client.apps.completions.create( + app_id="YOUR_APP_ID", + prompt="Write a short greeting", + stream=True, + incremental_output=True, + ) + try: + for text in stream.text_stream: + print(text, end="") + finally: + stream.close() +``` + +## AgentStudio (`client.agentstudio`) + +Mount AgentStudio from the main client so configuration and cleanup stay connected: + +```python +import dashscope from dashscope.agentstudio.types import user_message -client = Client(api_key="sk-xxx", workspace="my-ws") -agent = client.agents.create(name="demo", model="qwen3.8-flash") -session = client.sessions.create(agent=agent.id) -client.sessions.events.send(session.id, [user_message("Hello!")]) -with client.sessions.events.stream(session.id) as stream: - for text in stream.text_stream: # auto-stops on idle/terminated - print(text, end="") + +with dashscope.Client(workspace="my-ws") as client: + studio = client.agentstudio + agent = studio.agents.create(name="demo", model="qwen3.8-flash") + session = studio.sessions.create(agent=agent.id) + studio.sessions.events.send(session.id, [user_message("Hello!")]) + with studio.sessions.events.stream(session.id) as stream: + for text in stream.text_stream: + print(text, end="") +# Closing the main client also closes its mounted AgentStudio client. ``` -## Common Error Codes -| Error code/exception | HTTP status | Meaning | Handling | -| --- | --- | --- | --- | -| `InputRequired` / `ModelRequired` / `InvalidInput` | raised locally | missing required params such as app_id/prompt/model | add params per the exception message; Application failures do not raise — check status_code | -| `invalid_request_error` / `request_too_large` (InvalidRequestError) | 400/413 | invalid agentstudio request parameters, or payload too large | check parameter names/types, e.g. empty events raises a local ValueError | -| `authentication_error` (AuthenticationError) | 401 | api_key missing or invalid | check api_key or DASHSCOPE_API_KEY | -| `permission_denied_error` / `permission_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership | -| `not_found_error` (NotFoundError) | 404 | agent/session and other resources do not exist | verify the id and region/workspace are correct | -| `conflict_error` (ConflictError) | 409 | version in agents.update does not match the server | retrieve the latest version first, then update | -| `rate_limit_error` / `billing_error` (RateLimitError) | 429 | rate limited or out of quota | retry with backoff (client default max_retries=2) | -| `api_error` / `timeout_error` (InternalServerError), `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry | -| `APITimeoutError` / `APIConnectionError` | no response | connection/read/write timeout | increase timeout, check network, then retry | -| `StreamError` / `StreamClosedError` | mid-stream | SSE protocol error/stream closed | call `events.stream` again to reopen the stream | - -Match on the exception class, not on `.code`: `.code` echoes the server's raw -string, so a 403 may arrive as either `permission_denied_error` or -`permission_error`, and it is normalised to `api_error` when the server omits -it. The class is always the one listed above. +`Client(api_key=..., workspace=..., region=..., ...)` forwards API key, workspace, and region to AgentStudio. A workspace is required unless an AgentStudio base URL is configured. Standalone `dashscope.agentstudio.Client/AsyncClient` remain available, but are unnecessary for normal 2.0 main-client usage. + +Mounted resources are `agents`, `deployments`, `deployment_runs`, `sessions`, `environments`, `files`, `skills`, `vaults`, `memory_stores`, `webhook_endpoints`, and `security`. + +### Agents and Sessions + +- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, multiagent=None, metadata=None)` returns `Agent`. `model` can be a model ID string or `ModelConfig(id=..., temperature=..., max_tokens=..., enable_thinking=..., thinking_budget=...)`. +- `agents.retrieve(agent_id, version=None)` (alias `get`), `list(...)`, and `list_versions(agent_id)` query agent state. `archive(agent_id)` archives it. +- `agents.update(agent_id, *, version, name, model=None, ...)` is optimistic-concurrency checked: `version` must match the current version. The service also requires `name`; sending `model` replaces its model configuration, so retain unspecified settings (for example with the existing model's `copy_with(...)`) rather than accidentally dropping them. +- `sessions.create(*, agent, environment_id=None, title=None, resources=None, vault_ids=None, environment_variables=None, mcp_configs=None, metadata=None)` returns `Session`. Sessions also support `retrieve`/`get`, `update`, `list`, `archive`, and `delete`. +- `environments.create(*, name, config, description=None, scope="organization", metadata=None)` creates an execution environment. `files.upload(file, *, mime_type=None, progress=None)` accepts a path, binary stream, or `(name, fileobj)`. Skills can be created from `file_id` or `file`, with nested version APIs. + +### AgentStudio SSE Execution + +1. `studio.sessions.events.send(session_id, events)` sends a non-empty event list; an empty list raises local `ValueError`. Constructors in `dashscope.agentstudio.types` include `user_message(...)`, `user_interrupt()`, `user_tool_confirmation()`, `user_tool_result()`, `user_custom_tool_result()`, and `user_define_outcome()`. +2. `with studio.sessions.events.stream(session_id, event_deltas=None, timeout=None) as stream:` opens a single-pass SSE stream. Iterating it yields typed server `Message` events with fields such as `type`, `content`, `stop_reason`, and `session_status`. +3. `stream.text_stream` yields text blocks from message events and stops on terminal stream/session states such as `idle`, `terminated`, or `rescheduling`. Opt into `event_deltas` when incremental `text_deltas` are required; do not assume all delta types arrive by default. + +Content blocks can represent text, images, audio, data, files, refusal, or errors. Client event types include message, interrupt, tool confirmation/results, and defined outcomes; server events additionally include reasoning, tool/MCP calls, session status, thread, model-request, and outcome-evaluation events. A typical session state flow is `idle → running → (rescheduling) → idle | terminated`. + +### Plugins, Tools, and MCP + +- AgentStudio: pass tool schemas through `agents.create(tools=[...])` and MCP configurations through `mcp_servers`. The runtime emits tool/MCP call events; execute client-side tools as required and send results or confirmations back through session events. +- Apps: bind plugins and workflows in the Bailian console. Callers pass business parameters with `biz_params`; `has_thoughts=True` requests process information in flat `thoughts`. + +## Errors (Brief) + +New code catches API exception classes from `dashscope.errors`; AgentStudio's stream-specific `StreamError` / `StreamClosedError` remain in `dashscope.agentstudio.exceptions`. Match exception classes rather than inferring the class solely from a raw wire `.code`. + +| HTTP / condition | Current class | Handling | +| --- | --- | --- | +| 400 | `BadRequestError` (`InvalidRequestError` is an alias) | Check parameter names/types; empty event lists are local `ValueError` | +| 401 | `AuthenticationError` | Check `DASHSCOPE_API_KEY` or explicit `api_key` | +| 403 | `PermissionDeniedError` | Check workspace, UID, region, and resource ownership | +| 413 | `RequestTooLargeError` | Reduce payload size and check service limits | +| 404 | `NotFoundError` | Verify agent/session/resource IDs | +| 409 | `APIStatusError` (there is no dedicated conflict catalog class) | Retrieve the latest agent version before retrying update | +| 429 | `RateLimitError` | Retry with backoff and check quota | +| 503 / mapped overloaded error | `ServiceUnavailableError` (`OverloadedError` is an alias) | Retry with backoff | +| 504 | `GatewayTimeoutError` | Retry or troubleshoot with `request_id`; do not label it `InternalServerError` | +| 500 or a code mapped to internal error | `InternalServerError` | Retry where appropriate and retain `request_id` | +| Unmapped HTTP status, including an unknown 5xx | `APIStatusError` | Inspect status/code/body; do not blindly guess `InternalServerError`. An unrecognized wire code may still use the known HTTP status mapping. | +| Connection/read/write timeout | `APIConnectionError` / `APITimeoutError` | Check network and timeout before retrying | +| AgentStudio stream protocol/closed stream | `StreamError` / `StreamClosedError` | Reopen `events.stream` when the operation is retryable | + +See the dedicated errors guide for the complete hierarchy and retry rules. + +## 1.x Compatibility — Maintenance Only + +These APIs retain their old response envelopes and local exception behavior. Do not mix them with 2.0 flat `AppCompletion` models or `client.agentstudio`. + +### Legacy Application + +`dashscope.Application.call(app_id, prompt=None, history=None, workspace=None, api_key=None, messages=None, **kwargs)` invokes a published app. At least one of `prompt` or `messages` is required; missing `app_id` raises `InputRequired`. Legacy kwargs include `stream`, `incremental_output`, `session_id`, `biz_params`, `has_thoughts`, document/memory/image/file inputs, `rag_options`, and model controls. + +`ApplicationResponse.output` contains `text`, `finish_reason`, `session_id`, `thoughts`, `doc_references`, and `workflow_message`. Legacy failures generally do not raise: check `status_code`, then inspect `code/message/request_id`. This is the opposite of the default 2.0 exception flow. + +### Legacy Assistants / Threads / Messages / Runs / Steps + +- `Assistants.call/create(*, model, name=None, description=None, instructions=None, tools=None, file_ids=[], metadata=None, ...)`; `retrieve/list/update/delete`. +- `Threads.create(*, messages=None, metadata=None)`; `Messages.create(thread_id, *, content, role="user", file_ids=[], metadata=None)`. +- `Runs.create(thread_id, *, assistant_id, instructions=None, tools=None, stream=False, ...)` and `Runs.create_thread_and_run(...)`; also `retrieve`, `submit_tool_outputs`, and `cancel`. Steps belong to this same legacy maintenance surface. +- Run states: `queued → in_progress → (requires_action) → completed | failed | cancelled | expired`, with `cancelling` also possible. On `requires_action`, read `run.required_action.submit_tool_outputs`, execute the requested tools, submit outputs, and continue polling. +- Legacy tool schemas and the Run tool-output loop must remain in this compatibility implementation; do not rewrite them as AgentStudio events unless intentionally migrating the application. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md index e46a684b..b37a5661 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md @@ -4,11 +4,16 @@ description: Look up SDK API parameters and usage arguments: [api_name] --- -First use run_command to check the installed SDK version and the actual API signature: -1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -2. `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.{api_name}.call))'")` -3. If the signature is not clear enough, use `run_command("python -c 'import dashscope; help(dashscope.{api_name})'")` to get the full documentation. +First identify whether `{api_name}` refers to a 1.x module/class API or a 2.0 client resource, then inspect the installed SDK: -Based on the actual inspection results, explain all parameters of {api_name}, the return value format, and the supported model list, and provide a typical usage example (Python). +1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +2. Resolve the exact public object and method. Use `inspect.signature` on that bound method; use `help()` on the resource or response model when parameter or return-field documentation is insufficient. Do not mechanically append `.call` to `{api_name}`. +3. For a 2.0 resource, a local `Client(api_key="sk-test")` is enough for inspection and does not make a network request. Close it afterwards. Read the resource implementation or Pydantic response model only when inspection leaves a specific question unresolved. +4. Consult the matching domain skill for the current 2.0.3 mapping and the 1.x compatibility notes. -Note: parameter descriptions must match the inspection results. Do not answer from memory. +Explain the parameters, return type and fields, streaming/task lifecycle where applicable, and supported models from the verified implementation. Include a typical Python example and distinguish these contracts explicitly: + +- 2.0: `Client` / `AsyncClient` resource methods, flattened typed responses, `dashscope.errors` exceptions, and `jobs.create/fetch/wait` for supported long-running tasks. +- 1.x: the existing `.call()` signature and its actual response or task fields; do not rewrite them as 2.0 fields. + +Parameter descriptions and model support must match the inspection results. If the installed version differs from the knowledge base, state the difference instead of guessing. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md index 20aa30ff..25433ea6 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md @@ -5,11 +5,11 @@ description: Quick reference for all dashscope CLI command groups, subcommands, # dashscope CLI -This file is authoritative for answering `dashscope` CLI usage questions (based on the dashscope/cli/ source code); do not invent options. For details, have the user run `dashscope --help` to verify. +This file is authoritative for answering `dashscope` CLI usage questions (based on `dashscope/legacy_1x/cli/`); do not invent options. **The CLI is still implemented by the 1.x SDK, including its response envelopes and static resource classes. It is not a command facade over 2.0 `dashscope.Client` / `AsyncClient`.** For details, have the user run `dashscope --help` to verify. ## Command overview -| Command group | Subcommands | Purpose | Corresponding SDK class | +| Command group | Subcommands | Purpose | Corresponding 1.x SDK implementation | | --- | --- | --- | --- | | auth | whoami/login/logout | Verify the active API key and persist one to ~/.dashscope/api_key | dashscope.common.api_key (get_default_api_key/save_api_key) | | generation | create | Text generation (including streaming and multi-turn messages) | dashscope.Generation | @@ -22,16 +22,18 @@ This file is authoritative for answering `dashscope` CLI usage questions (based | tokenization | create | Tokenization/token counting | dashscope.Tokenization | | models | list/get | Query available models | dashscope.Models | | application | create | Call a Bailian application | dashscope.Application | -| image-synthesis | create | Text-to-image (async task) | dashscope.ImageSynthesis | -| image-generation | create | Image generation (supports reference images) | dashscope.ImageGeneration | +| image-synthesis | create/fetch/wait/cancel/list | Full text-to-image task lifecycle | dashscope.ImageSynthesis | +| image-generation | create/fetch/wait/cancel/list | Full image-generation task lifecycle, including reference images | `dashscope.legacy_1x.aigc.image_generation.ImageGeneration` | | video-synthesis | create/fetch/wait/cancel/list | Full video synthesis task lifecycle | dashscope.VideoSynthesis | | multimodal-conversation | create | Multimodal conversation | dashscope.MultiModalConversation | | multimodal-embedding | create | Multimodal embeddings (text/image/audio) | dashscope.MultiModalEmbedding | -| transcription | create | Audio file transcription (async task) | dashscope.Transcription | +| transcription | create/fetch/wait/cancel/list | Full audio file transcription task lifecycle | dashscope.Transcription | | speech-synthesis | create | Speech synthesis TTS | dashscope.HttpSpeechSynthesizer | -| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | dashscope.legacy_1x.finetune.agentic_rl.AgenticRL | +| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | 1.x AgenticRL compatibility wrapper, not `client.rl` | -Note: code-generation and understanding still exist in the source but are deprecated/discontinued and are not registered in the CLI. +Notes: +- `dashscope.ImageGeneration` is **not** a root public export. The CLI imports the internal 1.x class shown above; do not recommend the nonexistent root import. +- code-generation and understanding still exist in the source but are deprecated/discontinued and are not registered in the CLI. ## Common usage @@ -44,7 +46,7 @@ dashscope auth logout # remove the saved key file dashscope generation create -m qwen3.7-plus -p "Hello" -s --temperature 0.7 dashscope ft create -m qwen3.8-32b -t file-xxx -v file-yyy -e 2 -b 16 -l 1e-5 dashscope files upload -f train.jsonl -p fine-tune -d "training set" -dashscope deployments create -m qwen3.8-32b -s demo -c 1 +dashscope deployments create -m qwen3.8-32b -s demo -c 1 --plan ptu dashscope oss upload -m qwen3.7-plus -f ./pic.png dashscope rerank create -m gte-rerank -q "query" -d doc1 -d doc2 -n 3 dashscope embeddings create -m text-embedding-v3 -i "text1" -i "text2" @@ -54,7 +56,9 @@ dashscope application create -a APP_ID -p "question" --has-thoughts dashscope image-synthesis create -m qwen-image-3.0 -p "a cat" -n 2 --size 1024*1024 dashscope image-generation create -m qwen-image-3.0 -t "a cat" --image ref.png dashscope video-synthesis create -m wan2.2-t2v-plus -p "ocean waves" -dashscope video-synthesis wait TASK_ID # fetch/cancel/list take task_id the same way +dashscope video-synthesis wait TASK_ID # fetch/cancel also take TASK_ID; list is a query +# image-synthesis, image-generation, and transcription expose the same +# create/fetch/wait/cancel/list task lifecycle. dashscope multimodal-conversation create -m qwen3-vl-plus -t "describe the image" --image a.jpg dashscope multimodal-embedding create -m multimodal-embedding-v1 --text "cat" --image c.png dashscope transcription create -m paraformer-v2 --file-url https://.../a.wav @@ -69,7 +73,7 @@ Key option notes: - generation create: `-p/--prompt`, `--messages` (JSON string), `-s/--stream`, `--temperature/--top-p/--top-k/--max-tokens/--seed/--stop/--repetition-penalty/--presence-penalty/--enable-search/--n/--result-format` - ft create: `-t/--training-file-ids` (repeatable), `-v/--validation-file-ids`, `--mode`, `-e/--n-epochs`, `-b/--batch-size`, `-l/--learning-rate`, `-p/--prompt-loss`; get/stream/cancel/delete all take job_id as a positional argument; list uses `-p/--page`, `-s/--size` - files: upload `-f/--file`, `-p/--purpose` (default fine-tune), `-d/--description`; all commands support `-u/--base-url` -- deployments: create `-m/-s/--suffix/-c/--capacity/--plan/--template-id`; `scale DEPLOYED_MODEL -c 2`; after create it blocks by default (timeout 3600 seconds) +- deployments: create `-m/-s/--suffix/-c/--capacity/--plan/--template-id`; **`--plan` is required** (for example `--plan ptu`); `scale DEPLOYED_MODEL -c 2`; after create it blocks by default (timeout 3600 seconds) - oss upload: `-f/--file`, `-m/--model`, `-k/--api-key` (reads DASHSCOPE_API_KEY), `-u/--base-url` - rerank create: `-q/--query`, `-d/--document` (repeatable), `--return-documents/--no-return-documents`, `-n/--top-n`, `-i/--instruct` - video-synthesis create: `--img-url/--first-frame-url/--last-frame-url/--duration/--resolution/--ratio/--seed/--prompt-extend/--watermark`; list supports `--start-time/--end-time/--model-name/--status/--region` diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md index 6acac9f9..e84e3401 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md @@ -5,9 +5,11 @@ arguments: [file_path] --- Diagnostic steps: -1. Use `read_file` to read the user's code at {file_path}. -2. Check the installed SDK version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -3. If the code calls a specific API (e.g. `dashscope.Generation.call`), inspect its actual signature: `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'")` -4. If you need to understand SDK internals, locate the source with `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")`, then use `read_file` to read the relevant implementation. -Based on the actual findings above, point out the problems in the user's code and provide the complete fixed code. +1. Use `read_file` to read the user's code at `{file_path}`. Identify the API generation from the call itself: `Generation.call` and similar module/class APIs are 1.x; `Client` / `AsyncClient` resource calls are 2.0. An installed 2.0 package does not by itself make existing 1.x code wrong. +2. Check the installed version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +3. Inspect the exact called object and method with `inspect.signature`; for 2.0, inspect the bound resource method and relevant response model. Do not assume a `.call()` method or a `.output` response envelope. +4. Classify the failure from its actual type, `code`, `status_code`, `message`, and `request_id`. For 2.0, use the `dashscope.errors` hierarchy and distinguish HTTP/API errors from `SdkError` connection, timeout, and local failures. For 1.x, preserve its response-checking and exception conventions. +5. Only if a specific behavior remains unclear, locate the package with `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` and read the relevant implementation. + +Point out the root cause and provide complete fixed code. When repairing existing 1.x code, keep its style and contract unless the user explicitly requests migration. When writing or migrating to 2.0, use current resource methods, flattened responses, client lifecycle management, and `dashscope.errors`; do not mix 1.x and 2.0 return handling in the same example. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md index 1d061c5b..32e6cceb 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md @@ -4,8 +4,74 @@ description: Explain a DashScope API error code and how to fix it arguments: [code] --- -First check how this error code is actually handled in the SDK: -1. `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` -2. Use `run_command("grep -rn '{code}' ")` to search the SDK source for where this error code is defined and handled. +First identify whether the failing call uses the 1.x response convention or the 2.0 exception convention. Inspect the installed SDK and search for `{code}` in the public error definitions rather than guessing from the message: -Based on the actual code, explain what DashScope API error code {code} means, list common triggers, and give concrete fix steps with code examples. +1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` +2. `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` +3. Search the relevant error registry, protocol parser, or 1.x API implementation for the exact wire code. Preserve the original `code`, `message`, and `request_id` in the diagnosis. + +## Python 2.0 errors + +New code imports concrete exceptions from `dashscope.errors`. The package root directly exports `DashScopeError` and `ErrorCode`, not every concrete subclass. + +```python +from dashscope.errors import ( + BadRequestError, + ContentFilterError, + DashScopeError, + RateLimitError, +) + +try: + response = client.chat.completions.create( + model="qwen-plus", + messages="Hello", + ) +except ContentFilterError as error: + print("Content blocked", error.request_id) +except BadRequestError as error: + print("Bad request", error.code, error.message, error.request_id) +except RateLimitError as error: + print("Throttled", error.status_code, error.request_id) +except DashScopeError as error: + print(type(error).__name__, error.code, error.message, error.request_id) +``` + +`ContentFilterError` is a `BadRequestError` subclass, so its `except` clause must come first. + +Both HTTP non-2xx responses and HTTP 200 responses carrying a business error code use the same exception family. Common HTTP status classes are: + +| Status | Exception | +| --- | --- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 413 | `RequestTooLargeError` | +| 429 | `RateLimitError` | +| 500 | `InternalServerError` | +| 503 | `ServiceUnavailableError` | +| 504 | `GatewayTimeoutError` | + +Wire codes refine the meaning: for example, `Arrearage` maps to `PermissionDeniedError`, not 429, and throttling codes map to `RateLimitError`. A gateway timeout is `GatewayTimeoutError`; do not label every unknown 5xx as `InternalServerError` without checking the actual status and code. `ResponseError` concerns response parsing and is not synonymous with every HTTP 500. + +Exceptions expose `message`, `code`, `status_code`, `request_id`, `body`, and `headers` as applicable. Connection failures, timeouts, WebSocket failures, and SDK-local failures belong to the `SdkError` branch; they have no HTTP response, have `status_code is None`, and may also provide `solution` and `allow_retry`. Do not discard these fields by flattening every failure into a status-only message. + +With `raise_on_error=True` (the default), catch `DashScopeError` or a more specific subclass. If the user explicitly selects return-value mode, check `is_error`: + +```python +lenient = client.with_options(raise_on_error=False) +response = lenient.chat.completions.create(model="qwen-plus", messages="Hello") +if response.is_error: + print(response.code, response.message, response.request_id) +``` + +`raise_on_error=False` does not suppress connection failures or timeouts because no response object exists. In a stream, a business error may be represented by an `is_error=True` item before iteration ends. Automatic transport retries do not make authentication, permission, quota, or parameter errors safe to retry blindly. + +## Python 1.x and compatible-mode errors + +For an existing 1.x call such as `Generation.call`, preserve its contract: API failures commonly return a response whose `status_code`, `code`, `message`, and `request_id` must be checked, while local validation errors may still be raised. Do not convert this into a 2.0 `except DashScopeError` example unless the user is migrating. + +OpenAI-compatible code using the official `openai` package keeps that library's own exception hierarchy; it is not the `dashscope.errors` hierarchy. Java errors likewise follow the Java SDK's independent API and version line. + +Based on the verified implementation, explain what `{code}` means, common triggers, whether retry is safe, and concrete fix steps with an example in the user's existing SDK style. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md index dd2310ad..6abedf60 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md @@ -5,102 +5,161 @@ description: Quick reference for fine-tuning and deployment (FineTunes/Files/Dep # Fine-tuning and Deployment -> Quick-reference note: all parameter names and return fields on this page follow the SDK source code (`dashscope/applications/finetune/`, `dashscope/legacy_1x/finetune/`, `dashscope/legacy_1x/files.py`, `dashscope/legacy_1x/common/`). Check the signatures on this page before answering; if unsure, verify on the spot with `inspect.signature` instead of relying on memory. +> Python SDK 2.0.3 uses `dashscope.Client` / `dashscope.AsyncClient` at the package root (`dashscope.v2` does not exist). Verify current signatures in `dashscope/resources/`, workflow code in `dashscope/applications/finetune/`, and old implementations in `dashscope/legacy_1x/finetune/`; use `inspect.signature` when unsure. New examples below use flat response fields and close their clients. -## Use Cases and Training Modes (SFT/CPT/DPO/AgenticRL) +## Use Cases and Training Modes -| Mode | Entry Point | Notes | +| Mode | Current entry point | Notes | | --- | --- | --- | -| SFT supervised fine-tuning | `FineTunes.call(..., mode="sft")` | `mode` is written into the request body as `training_type`; the source docstring lists values `sft` / `efficient_sft` | -| CPT continued pre-training / DPO preference optimization | `FineTunes.call` | The SDK does not enumerate CPT/DPO literals; pass them through `mode`/`hyper_parameters` per the server-side docs | -| AgenticRL reinforcement fine-tuning (GSPO) | `dashscope.legacy_1x.finetune.agentic_rl.AgenticRL` | `training_type="reinforcement"`, `hyper_parameters["algorithm"]="gspo"` | +| SFT supervised fine-tuning | `client.fine_tunes.create(..., mode="sft")` | `mode` is sent as `training_type`; source documents `sft` / `efficient_sft` | +| CPT / DPO | `client.fine_tunes.create(...)` | The SDK does not enumerate all server literals; pass supported `mode` and `hyper_parameters` values through | +| Agentic RL (including GSPO configuration) | `client.rl` and `client.rl.jobs` | Current client workflow and job-management facade | +| Direct AgenticRL workflow | `dashscope.finetune.agentic_rl.AgenticRL` | Separate workflow object; do not treat it as `client.rl` or as a FineTunes manager | -## Standard Workflow (upload file → create fine-tune job → poll status → deploy → invoke) +## Standard SFT Workflow -1. Upload file: `Files.upload(file_path, purpose="fine_tune")` → take `output["uploaded_files"][0]["file_id"]` -2. Create job: `FineTunes.call(model, training_file_ids=[file_id], hyper_parameters={...})` → take `output.job_id` (like `ft-xxx`) -3. Poll status: `FineTunes.get(job_id)` for polling, or `FineTunes.wait(job_id)` to block until a terminal state (polls every 30s); logs via `FineTunes.logs(job_id, offset=1, line=1000)`, event stream via `FineTunes.stream_events(job_id)` -4. Deploy: `Deployments.call(model=, capacity=N)` → `output.deployed_model`; use `Deployments.get(deployed_model)` and wait for `output.status` to become `RUNNING` -5. Invoke: the deployment name is the model name; use regular inference APIs such as `Generation.call(model=deployed_model, ...)` -6. Cleanup: `Deployments.scale(deployed_model, capacity)` to scale; `Deployments.delete(deployed_model)` to release; `FineTunes.cancel(job_id)` / `FineTunes.delete(job_id)` to cancel/delete a job +1. Upload JSONL with `client.files.upload(file_path=..., purpose="fine-tune")` and read `File.file_id`. +2. Create a job with `client.fine_tunes.create(...)`; submission returns a `FineTune` snapshot with `job_id` and `status`. +3. Poll with `get(job_id)` or block with `wait(job_or_job_id)`. Read logs with `logs()` or consume SSE with `stream_events()`. +4. After success, deploy `FineTune.finetuned_model` with `client.deployments.create(model=..., capacity=...)`; poll `get()` until the desired deployment status. +5. Invoke the deployment by using `Deployment.deployed_model` as the model name in the appropriate inference API. +6. Scale or release with `client.deployments.scale/delete`; cancel or delete training with `client.fine_tunes.cancel/delete`. -## SDK APIs (source of truth: the source code) +```python +import dashscope -### FineTunes (`dashscope/legacy_1x/finetune/finetunes.py`, SUB_PATH="fine-tunes") +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + uploaded = client.files.upload(file_path="train.jsonl") + + submitted = client.fine_tunes.create( + model="qwen3.8-32b", + training_file_ids=[uploaded.file_id], + hyper_parameters={"n_epochs": 3}, + ) + job = client.fine_tunes.wait(submitted) + print(job.job_id, job.status, job.finetuned_model) + + deployment = client.deployments.create( + model=job.finetuned_model, + capacity=1, + suffix="demo", + ) + print(deployment.deployed_model, deployment.status) +``` -The creation method is named `call` (the SDK has no `create` method): -`FineTunes.call(model, training_file_ids, validation_file_ids=None, mode=None, hyper_parameters={}, api_key=None, workspace=None, **kwargs) -> FineTune` +`AsyncClient` provides awaitable file, fine-tune, deployment, and RL resource methods; use `async with dashscope.AsyncClient()` and close streams with their async cleanup API. -| Parameter | Type | Notes | -| --- | --- | --- | -| model | str | Base model to fine-tune | -| training_file_ids | list/str | Training file id; a str is auto-converted to a single-element list | -| validation_file_ids | list/str/None | Validation file id | -| mode | str/None | Training mode, written into the request body as `training_type` (`sft`/`efficient_sft`) | -| hyper_parameters | dict | Hyperparameters, e.g. `n_epochs`, `batch_size`, `learning_rate` | -| kwargs["finetuned_output"] | str | Custom output model name, passed through as `finetuned_output` in the request body | +## Files (`client.files`) + +| Method | Return / important fields | +| --- | --- | +| `upload(*, file_path, purpose="fine-tune", description=None, ...)` | `File`: `file_id`, `name`, `purpose`, `size`, `uploaded_at` | +| `list(*, page_no=1, page_size=10, ...)` | `FileList`: `files`, `page_no`, `page_size`, `total_count` | +| `get(file_id, ...)` | `File` | +| `delete(file_id, ...)` | `DeletionStatus`, including `deleted` | + +Fine-tune uploads validate JSONL locally, one JSON object per line. Invalid JSONL raises local `ValueError`; filesystem failures remain SDK I/O errors. SFT rows commonly contain `{"messages": [...]}`; RL datasets may include fields such as `rollout_extra` required by the workflow. The 2.0 purpose default is hyphenated `fine-tune`, not the old `fine_tune` example. + +## Fine-Tune Jobs (`client.fine_tunes`) -Other methods: `get(job_id) -> FineTune`; `list(page_no=1, page_size=10) -> FineTuneList`; `cancel(job_id) -> FineTuneCancel`; `delete(job_id) -> FineTuneDelete`; `wait(job_id)` polls until `SUCCEEDED/FAILED/CANCELED`. +```text +create(*, model, training_file_ids, validation_file_ids=None, + mode=None, hyper_parameters=None, finetuned_model=None, ...) -> FineTune +``` + +- `training_file_ids` and `validation_file_ids` accept a string or list; a string is normalized to a one-element list. +- `finetuned_model` is the request-side custom output name and is sent as wire key `model_name`. It is not the 1.x `finetuned_output` request kwarg. +- `get(job_id)` returns a fresh `FineTune`; `list(page_no=1, page_size=10)` returns `FineTuneList.jobs` and `total_count`. +- `cancel(job_id)` and `delete(job_id)` return flat `JobStatus` snapshots. +- `logs(job_id, offset=1, lines=1000)` returns `JobLogs`; read normalized text from `content`. The Python argument is plural `lines` even though the wire query key is singular. +- `stream_events(job_id)` yields `FineTuneEvent` objects. Event `output` is the event payload, not a legacy response envelope. +- `wait(job_or_job_id, poll_interval=..., max_interval=..., timeout=...)` polls until `SUCCEEDED`, `FAILED`, or `CANCELED`; failed/cancelled jobs raise by default. It returns the final `FineTune` snapshot rather than invoking the deployment automatically. + +Flat `FineTune` fields include `job_id`, `job_name`, `status`, `model`, `base_model`, `finetuned_model`, `training_file_ids`, `validation_file_ids`, `hyper_parameters`, `training_type`, `created_at`, `finished_at`, and `usage`, plus common response metadata. `FineTuneList` uses `jobs` and `total_count`. + +## Deployments (`client.deployments`) + +```text +create(*, model, capacity, model_version=None, suffix=None, plan=None, + template_id=None, deploy_spec=None, billing_method=None, ...) -> Deployment +``` -### Deployments (`dashscope/legacy_1x/finetune/deployments.py`, SUB_PATH="deployments") +- `get(deployed_model)` and `scale(deployed_model, *, capacity)` return `Deployment` snapshots. There is no SDK `wait()` on this resource; poll `get()` when readiness is required. +- `list(page_no=1, page_size=10)` returns `DeploymentList.deployments` and `total_count`. +- `delete(deployed_model)` returns `JobStatus`. +- `stream_events(deployed_model_id)` yields `DeploymentEvent`; its `output` is an event payload, not the old response envelope. +- Flat `Deployment` fields include `deployed_model`, `model`, `base_model`, `status`, `capacity`, `ready_capacity`, `charge_type`, `created_at`, and `updated_at`. -- `Deployments.call(model, capacity, version=None, suffix=None, ...) -> Deployment`: request body keys are `model_name/capacity/model_version/suffix`; fine-tuned models do not need `version`; with `suffix` set, the deployment name is `model_suffix` -- `get(deployed_model)`, `list(page_no=1, page_size=10)`, `delete(deployed_model)`, `scale(deployed_model, capacity)` +The Python SDK's `plan` parameter is optional at the signature level; server-side plan/billing requirements still depend on model and account. This differs from the legacy CLI's required `--plan` option. -### Files (`dashscope/files.py`, SUB_PATH="files") +## Agentic RL Through `client.rl` -- `Files.upload(file_path, purpose="fine_tune", description=None, ...) -> DashScopeAPIResponse`: see `FilePurpose` for `purpose` values (`fine_tune`/`assistants`); for `fine_tune`, the JSONL is validated locally first and invalid files raise `InvalidFileFormat` directly -- `list(page=1, page_size=10)`, `get(file_id)`, `delete(file_id)`; all return `DashScopeAPIResponse` +`client.reinforcement_learning` aliases `client.rl`. The resource separates workflow operations from `client.rl.jobs` job management. -### AgenticRL (`dashscope/applications/finetune/agentic_rl.py`, brief) +### Workflow and function instances -- Usage: `client = AgenticRL(api_key=None)` → optionally `client.init(config_path="job.yaml")` (YAML-driven) → `await client.run(...)` completes "register functions → upload datasets → submit job" in one step; or call `register_functions()` / `upload_datasets()` / `submit_job()` step by step -- Key concepts: `RolloutFunctionComponent`/`RewardFunctionComponent` function components; `TrainingDataset`/`ValidationDataset` (`data_source_type`: `file_id`/`download_url`/`oss_mount`); `hyper_parameters` (`algorithm="gspo"`, `n_rollouts`, `kl_loss_coef`, `learning_rate`, etc.); `resources` (e.g. `mtu_spec_code`) -- Job management methods mirror FineTunes: `AgenticRL.get/list/cancel/delete/logs(job_id, ...)` +| Method | Contract | +| --- | --- | +| `register_functions(*, rollout_classpaths=None, reward_classpaths=None, group_reward_classpaths=None, workspace_dir="./", lazy_load=True, api_key=None)` | Returns `RegisteredFunctions`, including registered entity IDs and function instance IDs | +| `test_functions(*, instance_id, func_type, input_data, pull_logs=False, log_page_size=100, start_time=None, end_time=None, api_key=None)` | Returns the user function's own result, not a `FineTune` | +| `upload_data(training_files, validation_files=None, *, api_key=None)` | Returns `UploadedData.training_file_ids` and `validation_file_ids` | +| `query_function_instance_logs(...)` | Returns one raw log-query API dictionary | +| `query_all_function_instance_logs(...)` | Returns aggregated log strings | +| `delete_function_instance(...)` | Returns the raw delete API dictionary | -## Inputs/Outputs (JSONL format and job object fields) +These synchronous workflow methods internally use `asyncio.run`; do not call them from an already running event loop. Use `AsyncClient.rl` there. Their signatures do not accept a generic `timeout=` or per-call `workspace=`; do not copy those options from job methods. -- Training file: JSONL, one valid JSON object per line (the SDK validates line by line with `json.loads`); for SFT each line is usually `{"messages": [...]}`; RL data may carry a `rollout_extra` field -- `FineTune`: `status_code/request_id/code/message/output/usage`; `output` (`FineTuneOutput`) fields: `job_id, job_name, status, model, base_model, finetuned_output, training_file_ids, validation_file_ids, hyper_parameters, training_type, create_time, end_time, usage` -- `output.status` enum (`TaskStatus`): `PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/SUSPENDED/UNKNOWN`; after the job succeeds, use `output.finetuned_output` as the model name for deployment and invocation -- `FineTuneList.output`: `jobs` (List[FineTuneOutput]) + `page_no/page_size/total` -- `Deployment.output` (`DeploymentOutput`): `deployed_model, status, model_name, base_model, capacity, ready_capacity, charge_type, gmt_create, gmt_modified`; `status` (`DeploymentStatus`): `DEPLOYING/RUNNING(SERVING)/PENDING/DELETING/FAILED` +### Jobs (`client.rl.jobs`) -## Minimal Example (create an SFT job) +- `create(*, config_path=None, job_name=None, api_key=None, **kwargs) -> FineTune` initializes and runs the complete AgenticRL workflow, then returns after submission. `kwargs` override open-ended training configuration keys. It does not wait for training completion. +- `fetch(job_id) -> FineTune`; `wait(job_or_job_id) -> FineTune`; `list(...) -> FineTuneList`. +- `cancel(job_id)` and `delete(job_id)` return `JobStatus`; `logs(job_id, offset=1, lines=1000)` returns `JobLogs`. ```python import dashscope -from dashscope import Files, FineTunes - -dashscope.api_key = "sk-xxx" # or use the DASHSCOPE_API_KEY environment variable - -up = Files.upload("train.jsonl", purpose="fine_tune") # validates JSONL locally -file_id = up.output["uploaded_files"][0]["file_id"] - -job = FineTunes.call( - model="qwen3.8-32b", - training_file_ids=[file_id], - hyper_parameters={"n_epochs": 3}, -) -if job.status_code == 200: - rsp = FineTunes.wait(job.output.job_id) # blocks until SUCCEEDED/FAILED/CANCELED - print(rsp.output.status, rsp.output.finetuned_output) -else: - print(job.code, job.message) # on failure read code/message/request_id + +with dashscope.Client() as client: + submitted = client.rl.jobs.create(config_path="rl_config.yaml") + job = client.rl.jobs.wait(submitted) + print(job.job_id, job.status, job.finetuned_model) ``` -## Common Error Codes +Use `register_functions()`, `test_functions()`, and `upload_data()` when those stages must be driven individually; do not assume `jobs.create()` reuses objects returned by a separate workflow unless the configuration explicitly wires them together. + +## Direct `AgenticRL` Workflow — A Separate Contract + +`dashscope.finetune.agentic_rl.AgenticRL` owns the configurable workflow. Typical usage is `AgenticRL(...)`, `init(config_path=..., **kwargs)`, then `await run()`; alternatively drive `register_functions()`, `upload_datasets()`, and synchronous `submit_job()` stage by stage. Components include rollout/reward functions, training/validation datasets, hyperparameters such as `algorithm="gspo"`, and resource settings. + +```python +import asyncio +from dashscope.finetune.agentic_rl import AgenticRL + +async def main(): + workflow = AgenticRL() + workflow.init(config_path="rl_config.yaml") + result = await workflow.run() + print(result.status_code, result.request_id) + +asyncio.run(main()) +``` + +`run()`/`submit_job()` use `SubmitResult`, whose legacy-shaped fields include `status_code`, `request_id`, `code`, `message`, `output`, `usage`, and `headers`. That shape belongs to this workflow API; it is not a `client.rl.jobs` `FineTune`. The public workflow class does **not** provide FineTunes-style `get/list/cancel/delete/logs` methods. For those operations on a submitted RL job, use `client.rl.jobs`. + +In 1.x compatibility code, the AgenticRL wrapper adds `cancel/list/get/delete/logs`, and its `submit_job()` returns a 1.x `FineTune`. Keep that wrapper's return and management contract out of current `client.rl` and public `AgenticRL` examples. + +## Response and Error Handling + +Ordinary 2.0 models expose common `request_id`, `status_code`, `code`, `message`, `is_error`, and `headers` alongside the flat business fields documented above. By default, API/business failures raise exceptions from `dashscope.errors`. If a client is constructed with `raise_on_error=False`, check `response.is_error` before using business fields and retain `request_id` for diagnosis. Intentional raw/event payloads (`Job.output`, `FineTuneEvent.output`, `DeploymentEvent.output`, and direct AgenticRL `SubmitResult.output`) are exceptions, not the standard envelope pattern. + +## 1.x Compatibility — Maintenance Only -| Error code/exception | HTTP status | Meaning | Handling | -| --- | --- | --- | --- | -| `InvalidFileFormat` | none (local exception) | Uploaded file is not valid JSONL | Fix the JSON line by line and re-upload | -| `AuthenticationError` | 401 | API Key missing/invalid | Set `DASHSCOPE_API_KEY` or pass `api_key` explicitly | -| Response `code`/`message` | 403 | No access to the file/job/deployment | Check the `workspace` and resource ownership | -| Response `code`/`message` | 404 | `job_id`/`file_id`/`deployed_model` does not exist | Verify the id with the corresponding `list()` | -| `REPEATABLE_STATUS` | 503/504 | Service temporarily unavailable | Retry, or keep polling with `FineTunes.wait` | -| AgenticRL `error_code` 3001-3008 | - | 3001 key config, 3002 function registration, 3003 dataset upload, 3004 duplicate function name, 3005 job submission, 3006 flow failure, 3007 unsupported function type, 3008 function validation failure | Use the exception's `root_cause` to locate the root cause | +Use this section only to repair existing imports such as `from dashscope import Files, FineTunes, Deployments`; do not mix these objects with 2.0 client resources. -General error-handling pattern: first check `status_code == 200` on the response object; on failure read `code`/`message`/`request_id`; the base class for SDK local exceptions is `dashscope.common.error.DashScopeException`. +- `Files.upload(file_path, purpose="fine_tune", description=None)` returned `DashScopeAPIResponse`; old code reads `output["uploaded_files"][0]["file_id"]`. `Files.list` used `page=...`, and invalid fine-tune JSONL raised `InvalidFileFormat`. +- `FineTunes.call(model, training_file_ids, validation_file_ids=None, mode=None, hyper_parameters={}, ...)` created a job. Old custom naming used `kwargs["finetuned_output"]`; management methods were `get/list/cancel/delete/wait/logs/stream_events`. +- Legacy `FineTune.output` fields included `job_id`, `status`, `finetuned_output`, file IDs, hyperparameters, timestamps, and usage. Successful old jobs therefore deployed `response.output.finetuned_output`. +- `Deployments.call(model, capacity, version=None, suffix=None, ...)` created a deployment. Legacy code reads `Deployment.output.deployed_model`, `output.status`, `output.capacity`, and related fields; `scale` used positional capacity. +- Legacy responses generally did not raise for service failures: check `status_code`, then read `code`, `message`, and `request_id`. Do not apply that rule to 2.0 `Client` calls. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md index eff7bc4b..208b939d 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md @@ -4,88 +4,202 @@ description: Quick reference for multimodal (MultiModalConversation/ImageSynthes --- # Multimodal -Locate before answering: understanding tasks use `MultiModalConversation` (sync/streaming); generation tasks use `ImageSynthesis`/`VideoSynthesis` (async tasks). Parameter names must follow the source code in `dashscope/aigc/`; never answer from memory. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The 2.0 resources live in the root `dashscope` package; **`dashscope.v2` does not exist**. Set `DASHSCOPE_API_KEY` before running the examples. Use the explicitly marked 1.x section only for legacy maintenance, and do not mix 2.0 flattened responses with 1.x `.output` responses. -## Applicable Models and Scenarios +Authoritative sources: `dashscope/resources/multimodal/chat.py`, `images.py`, `videos.py`, and `_jobs.py`. -- Understanding (image/video understanding, OCR, function calling): `qwen3-vl-flash`, `qwen3-vl-plus`, qwen-ocr series, etc. (the `Models` class in source only has the constant `qwen_vl_chat_v1`; pass others as strings). -- Text-to-image / image editing (`ImageSynthesis`, task=`text2image`): `qwen-image-3.0`, `qwen-image-edit-plus` (image editing, automatically switches to image2image task). -- Video generation (`VideoSynthesis`, task=`video-generation`): `wan2.2-t2v-plus` (text-to-video), `wan2.2-i2v-plus` (image-to-video), `wan2.2-kf2v-flash` (first/last frame). +## 2.0.3 Resources and Models -## SDK Interfaces +- Multimodal understanding: `client.multimodal.chat.create`. Models include current Qwen VL/OCR model names; pass the model as a string and verify service-side availability. +- Image generation/editing: `client.images.generate` for submit-and-wait, or `client.images.jobs.create/fetch/wait` for explicit task control. +- Video generation: `client.videos.generate` or `client.videos.jobs.create/fetch/wait`. +- `AsyncClient` mounts the same resources. `await` each operation, and use `async with dashscope.AsyncClient() as client` to close connections. -| Class | Key Methods | Notes | -|---|---|---| -| `MultiModalConversation` | `call(model, messages, ...)` | `stream=True` returns a Generator, otherwise returns a response object; async version `AioMultiModalConversation.call` | -| `ImageSynthesis` | `call / async_call / fetch(task) / wait(task) / cancel(task) / list(...)` | `call` blocks until results are ready; `sync_call` only supports wan2.2-t2i-flash/plus | -| `VideoSynthesis` | Same as above (no `list` difference, parameters below) | Same async task flow | +`client.chat.completions.create` automatically routes messages containing image/audio/video parts to the multimodal endpoint. `client.multimodal.chat.create` always selects that endpoint in the native protocol, including text-only calls to VL models. Compatible protocols translate media on their own chat path. -Async task flow: `async_call(...)` returns a task (`output.task_id`, `task_status=PENDING`) → `fetch(task)` for a single query or `wait(task)` to poll until a terminal state; `cancel(task)` can only cancel `PENDING`. The `task` parameter accepts a task_id string or the response object from async_call. `task_status` values: `PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/UNKNOWN`. +## Multimodal Chat -Key parameters of `MultiModalConversation.call` (all optional, in source signature order): `messages`, `text`, `voice`, `language_type`, `stream`, `temperature`[0,2), `top_p`(0,1], `top_k`, `max_tokens`, `seed`, `stop`, `repetition_penalty`, `presence_penalty`, `result_format`("message"/"text"), `incremental_output`, `enable_search`, `tools`, `tool_choice`, `enable_thinking`, `n`(1-4), `ocr_options`, `logprobs`, `top_logprobs`; plus `api_key`, `workspace`. **Empty `model` raises `ModelRequired`; both messages and text empty raises `ValueError`.** +`create` has the same keyword-only signature and `ChatCompletion` / `ChatStream` return types as `client.chat.completions.create`: -Key parameters of `ImageSynthesis.call/async_call`: `prompt` (required, empty raises `InputRequired`), `negative_prompt`, `size`("width*height" e.g. "1024*1024"), `n`, `seed`, `style` (auto/photography/anime etc.), `prompt_extend`, `watermark`, `ref_img`, `sketch_image_url` (sketch), `base_image_url`+`mask_image_url` (imageedit), `ref_strength`[0,1], `ref_mode`(repaint/refonly). +```python +create(*, model: str, messages: str | list[MessageInput], stream: bool = False, + tools=None, temperature=None, top_p=None, max_tokens=None, stop=None, + seed=None, repetition_penalty=None, presence_penalty=None, + enable_thinking=None, response_format=None, top_k=None, tool_choice=None, + n=None, logprobs=None, top_logprobs=None, thinking_budget=None, + enable_search=None, search_options=None, parallel_tool_calls=None, + incremental_output=None, parameters=None, timeout=None, + api_key=None, workspace=None) +``` + +Use `parameters={...}` for model-specific options not present in the signature. Media content is a list of parts. With the native protocol, local paths and `file://` URIs are uploaded to OSS before the request. + +```python +import dashscope + +messages = [{ + "role": "user", + "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "Describe this image"}, + ], +}] + +with dashscope.Client() as client: + response = client.multimodal.chat.create( + model="qwen-vl-plus", + messages=messages, + ) + print(response.output_text) + + stream = client.multimodal.chat.create( + model="qwen-vl-plus", + messages=messages, + stream=True, + ) + try: + for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) + finally: + stream.close() +``` + +The 2.0 response flattens list-form multimodal text into a string. Read `response.choices[0].message.content` or `response.output_text`, plus flat `usage` / `request_id`; **do not read `.output`**. Streaming chunks use `choices[i].delta`, with the same incremental/cumulative `incremental_output` semantics as text chat. -Key parameters of `VideoSynthesis.call/async_call`: `prompt`, `negative_prompt`, `img_url` (image-to-video), `audio_url`, `first_frame_url`/`last_frame_url` (or `head_frame`/`tail_frame`), `media`, `reference_urls`, `template`, `size`("1280*720"), `duration` (seconds, default 5), `resolution`("720P"/"1080P"), `ratio`("16:9"), `seed`, `prompt_extend`, `watermark`, `shot_type`, `audio_setting`. +## Images and Videos -## Input/Output +### Actual resource signatures -Multimodal messages (content is an array of dicts; local paths or URLs both work, the SDK auto-uploads to OSS and adds header `X-DashScope-OssResourceResolve: enable`): +Both `generate` methods return a finished `Job`. Their `jobs.create` methods accept the same model/input/generation fields but only submit; their `timeout` is the submit HTTP timeout. `generate` additionally has `submit_timeout`, `poll_interval`, and `max_interval`, and its `timeout` is the total wait budget. ```python -messages = [{"role": "user", "content": [ - {"image": "https://.../a.jpg"}, # or {"video": [...]} / {"audio": ...} - {"text": "Describe this image"}, -]}] +client.images.generate( + *, model: str, prompt: str, negative_prompt=None, n=None, size=None, + image_urls=None, extra_input=None, function=None, seed=None, style=None, + prompt_extend=None, watermark=None, parameters=None, + timeout=86400.0, submit_timeout=None, poll_interval=1.0, + max_interval=8.0, api_key=None, workspace=None, +) -> Job + +client.videos.generate( + *, model: str, prompt=None, image_url=None, media=None, extra_input=None, + function=None, seed=None, size=None, duration=None, resolution=None, + ratio=None, prompt_extend=None, watermark=None, parameters=None, + timeout=86400.0, submit_timeout=None, poll_interval=1.0, + max_interval=8.0, api_key=None, workspace=None, +) -> Job + +client.images.jobs.fetch(task_id: str, *, timeout=None, api_key=None, workspace=None) +client.videos.jobs.fetch(task_id: str, *, timeout=None, api_key=None, workspace=None) +client.images.jobs.wait(job_or_task_id, *, poll_interval=1.0, max_interval=8.0, + timeout=86400.0, api_key=None, workspace=None) +client.videos.jobs.wait(job_or_task_id, *, poll_interval=1.0, max_interval=8.0, + timeout=86400.0, api_key=None, workspace=None) ``` -Responses uniformly contain `status_code / request_id / code / message / output / usage` (on error `output` is None; first check `status_code==200` i.e. `HTTPStatus.OK`): +- Image `size` accepts `1024*1024` and normalizes `1024x1024` to the wire form. `image_urls` supplies reference images for supported models; use `extra_input` for model-specific input fields and `parameters` for model-specific generation options. +- Image protocol selection is automatic by model name. Legacy text2image models use asynchronous tasks; `wan2.6-t2i*` / `wan2.6-image*` use the multimodal-generation protocol. A synchronous multimodal `generate` is capped by `submit_timeout`, not polling settings. +- Video `image_url` is the public name for the legacy wire field `img_url`. Older models accept `prompt` and/or `image_url`. `wan2.7-i2v*` requires `media` or `image_url`; media items use asset types `first_frame`, `last_frame`, `driving_audio`, and `first_clip`. `image_url` is shorthand for one `first_frame` item and must not duplicate one already in `media`. +- Unlike 1.x video behavior, explicitly pass `prompt_extend=True` when prompt extension is wanted. -- `MultiModalConversationResponse.output`: `choices[0].message.content` (content array isomorphic to the input), `finish_reason`, `text`; `usage`: `input_tokens`/`output_tokens`/`characters`. -- `ImageSynthesisResponse.output`: `task_id`, `task_status`, `results` (each contains `url`); `usage.image_count`. -- `VideoSynthesisResponse.output`: `task_id`, `task_status`, `video_url`; `usage`: `video_count`/`video_duration`/`video_ratio`. +### Runnable examples -## Minimal Examples +```python +import dashscope + +with dashscope.Client() as client: + image_job = client.images.generate( + model="wanx2.1-t2i-turbo", + prompt="一只在月球上弹吉他的猫,水彩风格", + size="1024*1024", + timeout=600.0, + ) + print(image_job.status, image_job.image_urls) + + video_job = client.videos.generate( + model="wan2.7-t2v", + prompt="一只小猫在草地上奔跑", + size="1280*720", + timeout=900.0, + ) + print(video_job.status, video_job.video_url) +``` -Image-text understanding: +Explicit submission, polling, and waiting: ```python -from http import HTTPStatus -from dashscope import MultiModalConversation - -rsp = MultiModalConversation.call(model="qwen3-vl-flash", messages=messages) -if rsp.status_code == HTTPStatus.OK: - print(rsp.output.choices[0].message.content[0]["text"]) +import dashscope + +with dashscope.Client() as client: + submitted = client.images.jobs.create( + model="wanx2.1-t2i-turbo", + prompt="一只猫", + size="1024*1024", + ) + snapshot = client.images.jobs.fetch(submitted.task_id) + print(snapshot.status) + + done = client.images.jobs.wait(submitted, timeout=600.0) + print(done.image_urls) ``` -Text-to-image async task: +`Job` exposes flat `task_id`, `status`, `results`, `result_url`, `image_urls`, `video_url`, `usage`, and the shared response envelope. Each item in `results` can expose `url` (and service-specific extra fields). Status values are `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`; use `job.succeeded` / `job.finished` for checks. `wait` polls with exponential backoff. With default error handling, `create` / `fetch` raise on `FAILED`, and `wait` raises on `FAILED` or `CANCELED`; `UNKNOWN` is terminal and is returned. + +## Errors (brief) + +- In 2.0, catch `dashscope.errors.DashScopeError` or a more specific subclass. If using `raise_on_error=False`, inspect `response.is_error` / `code` / `message`; for jobs also inspect `status`. +- In 1.x, keep checking `status_code`, `code`, and `message` on the response, including task failure information under the 1.x `output` contract. + +## 1.x Compatibility and Maintenance Guide + +These APIs retain their old response shapes; do not rewrite only the response access while leaving the old entry point in place. + +| 1.x entry point | Maintenance notes | +| --- | --- | +| `MultiModalConversation.call` / `AioMultiModalConversation.call` | `stream=True` returns a generator/async generator; otherwise returns `MultiModalConversationResponse` | +| `ImageSynthesis.call/async_call/fetch/wait/cancel/list` | `call` submits and waits; `async_call` only submits; `sync_call` is limited to supported wan2.2 t2i models | +| `VideoSynthesis.call/async_call/fetch/wait/cancel/list` | Same asynchronous task pattern for video | + +For `MultiModalConversation.call`, key parameters remain `model`, `messages`, `text`, `voice`, `language_type`, `stream`, sampling options, `result_format`, `incremental_output`, `enable_search`, tools, thinking options, `ocr_options`, and logprobs. Empty `model` raises `ModelRequired`; empty `messages` and `text` raises `ValueError`. + +For `ImageSynthesis`, old inputs include `prompt` (required), `negative_prompt`, `size`, `n`, `seed`, `style`, `prompt_extend`, `watermark`, `ref_img`, `sketch_image_url`, `base_image_url`, `mask_image_url`, `ref_strength`, and `ref_mode`. For `VideoSynthesis`, old inputs include `prompt`, `negative_prompt`, `img_url`, `audio_url`, `first_frame_url` / `last_frame_url` (or `head_frame` / `tail_frame`), `media`, `reference_urls`, `template`, `size`, `duration`, `resolution`, `ratio`, `seed`, `prompt_extend`, `watermark`, `shot_type`, and `audio_setting`. Keep model-specific validation when repairing these calls. + +The 1.x task flow is `async_call(...)` → `fetch(task)` for one snapshot or `wait(task)` until terminal; `task` can be a task ID or previous response. `cancel` applies only to `PENDING`. Old response contracts are: + +- `MultiModalConversationResponse.output.choices[0].message.content` (a content-part list), plus `output.text` where supplied. +- `ImageSynthesisResponse.output.task_id/task_status/results`; each result has `url`. +- `VideoSynthesisResponse.output.task_id/task_status/video_url`. +- All expose `status_code`, `request_id`, `code`, `message`, and `usage`; check `status_code == HTTPStatus.OK` before reading `output`. + +Do not use a context manager for these 1.x classmethod calls: ```python -from dashscope import ImageSynthesis +from http import HTTPStatus +from dashscope import ImageSynthesis, MultiModalConversation + +response = MultiModalConversation.call( + model="qwen-vl-plus", + messages=[{"role": "user", "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "Describe this image"}, + ]}], +) +if response.status_code == HTTPStatus.OK: + print(response.output.choices[0].message.content) -rsp = ImageSynthesis.async_call( +job = ImageSynthesis.async_call( model="qwen-image-3.0", - prompt="A cat running under the moonlight", n=2, size="1024*1024", + prompt="A cat running under the moonlight", + n=2, + size="1024*1024", ) -rsp = ImageSynthesis.wait(rsp) # poll until SUCCEEDED/FAILED -if rsp.output.task_status == "SUCCEEDED": - for r in rsp.output.results: - print(r.url) +job = ImageSynthesis.wait(job) +if job.status_code == HTTPStatus.OK and job.output.task_status == "SUCCEEDED": + for result in job.output.results: + print(result.url) ``` -## Common Error Codes - -| Error Code | HTTP Status | Meaning | Handling | -|---|---|---|---| -| InvalidApiKey | 401 | API Key missing/invalid | Check `dashscope.api_key` or the `DASHSCOPE_API_KEY` environment variable | -| InvalidParameter | 400 | Invalid parameter (size/format/field name) | Check each item against the parameter tables in this file; locally also raises `InvalidParameter` exception | -| DataInspectionFailed | 400 | prompt/image failed content moderation | Rewrite the prompt or change the input image | -| Throttling | 429 | Rate limit triggered | Retry with exponential backoff, reduce concurrency, confirm quota | -| InternalError | 500 | Server-side error | Retry with `request_id` or file a ticket | -| Task FAILED | 200 | Async task execution failed | Read `output.task_status` and the failure info in `output`, then adjust input and retry | - -Local validation exceptions (no request sent): `ModelRequired` (missing model), `InputRequired` (missing prompt), `ValueError` (messages/text both empty). Network/server exceptions may raise `RequestFailure` (contains `http_code`). - ## Java SDK Java SDK v2.22.23 entry points: diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md index f85137e1..606eced9 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md @@ -5,108 +5,220 @@ description: Quick reference for retrieval (Embedding / Batch Embedding / Multim # Retrieval -> Compiled from the SDK source code (embeddings/text_embedding.py, batch_text_embedding.py, multimodal_embedding.py, rerank/text_rerank.py); parameters and return fields are based on these sources. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The implementation is in the root `dashscope` package; **`dashscope.v2` does not exist**. Set `DASHSCOPE_API_KEY` before running the network examples. Keep the 1.x `.output` contract confined to the compatibility section. + +Authoritative sources: `dashscope/resources/embeddings.py`, `multimodal/embeddings.py`, `rerank.py`, `tokenization.py`, and `_jobs.py`. ## Applicable Models and Scenarios -**TextEmbedding** (task="text-embedding", `TextEmbedding.Models`): -- `text-embedding-v1` / `text-embedding-v2`: basic general-purpose vectors; only text_type is supported; fixed dimension. -- `text-embedding-v3`: supports text_type, dimension (default 1024), output_type (dense/sparse/dense&sparse). -- `text-embedding-v4`: everything v3 offers, plus exclusive dimension options 2048/1536. +- Text vectors: `text-embedding-v1/v2` (fixed dimension), `text-embedding-v3/v4` (configurable dimensions and dense/sparse output where supported). +- Offline batch text vectors: `text-embedding-async-v1/v2`, using a file containing one text per line. +- Multimodal vectors: `multimodal-embedding-one-peace-v1`, `multimodal-embedding-v1`, `qwen3-vl-embedding`, `qwen2.5-vl-embedding`, `tongyi-embedding-vision-plus`, and `tongyi-embedding-vision-flash`. +- Relevance ranking: `gte-rerank`, `gte-rerank-v2`, `qwen3-rerank`, and `qwen3-vl-rerank` where supported by the selected rerank resource. +- Token counting: `client.tokenization.count`; local tokenization is separately available under `dashscope.tokenizers` and sends no request. -**BatchTextEmbedding** (offline batch): `text-embedding-async-v1` / `text-embedding-async-v2`; submits a file url containing line-by-line texts for async batch processing. +Model support and allowed parameter values are service/model-specific; the SDK passes model names as strings rather than enforcing a whitelist. -**MultiModalEmbedding** (task="multimodal-embedding"): mixed text/image/audio/video vectors. Models: `multimodal-embedding-one-peace-v1`, `multimodal-embedding-v1`, `qwen3-vl-embedding`, `qwen2.5-vl-embedding`, `tongyi-embedding-vision-plus`, `tongyi-embedding-vision-flash`; coroutine version `AioMultiModalEmbedding`. +## 2.0.3 Text and Multimodal Embeddings -**TextReRank** (task="text-rerank"): reranks recalled results by relevance. Models: `gte-rerank`, `gte-rerank-v2`, `qwen3-rerank`, `qwen3-vl-rerank`; coroutine version `AioTextReRank`. +### Text embeddings -## SDK Interfaces +```python +client.embeddings.create( + *, model: str, input: str | list[str], text_type=None, dimension=None, + output_type=None, instruct=None, parameters=None, timeout=None, + api_key=None, workspace=None, +) -> EmbeddingResponse +``` -`TextEmbedding.call(model, input, workspace=None, api_key=None, text_type=None, dimension=None, output_type=None, instruct=None, **kwargs) -> DashScopeAPIResponse` +- `text_type` is typically `query` for retrieval queries or `document` for corpus/symmetric inputs. +- v3/v4 support model-dependent `dimension` and `output_type` (`dense`, `sparse`, or `dense&sparse`). Do not pass these to v1/v2. `instruct` supplies a task instruction where supported; additional options belong in `parameters`. +- Empty input raises `ValueError` locally. Streaming is not supported. -| Parameter | Description (based on source docstrings) | -| --- | --- | -| input | str or List[str], internally assembled as `{"texts": [...]}`; streaming not supported | -| text_type | `"query"` (retrieval query) / `"document"` (default; corpus and symmetric tasks) | -| dimension | v3/v4 only: 2048, 1536 (v4 only), 1024 (default), 768, 512, 256, 128, 64 | -| output_type | v3/v4 only: `"dense"` (default) / `"sparse"` / `"dense&sparse"` | -| instruct | Custom task instruction to guide the model in understanding query intent | +### Multimodal embeddings -`MultiModalEmbedding.call(model, input, api_key=None, workspace=None, dimension=None, output_type=None, fps=None, instruct=None, enable_fusion=None, res_level=None, max_video_frames=None, **kwargs)` +The actual 2.0 entry point is **`client.multimodal.embeddings.create`** (there is no method named `create_multimodal`): -- input is a List; elements use `MultiModalEmbeddingItemText(text, factor)` / `...ItemImage(image, factor)` / `...ItemAudio(audio, factor)` (factor: float, required), or equivalent dicts. -- dimension: vector dimension (supported values vary by model); output_type: currently only `"dense"`. -- fps: video frame-sampling ratio in [0,1], default 1.0; instruct: task instruction. -- enable_fusion: qwen3-vl-embedding only; when True, all content is fused into a single vector. -- res_level (0/1/2/3) and max_video_frames (<=64): snapshot-type models only. -- kwargs supports `auto_truncation`: automatically truncates audio >15s / text >70 words; default False (over-length input raises an error). -- Local files are automatically uploaded via OSS (header `X-DashScope-OssResourceResolve: enable`); empty input raises `InputRequired`; empty model raises `ModelRequired`. +```python +client.multimodal.embeddings.create( + *, model: str, input: dict | EmbeddingItem | list, + dimension=None, output_type=None, fps=None, instruct=None, + enable_fusion=None, res_level=None, max_video_frames=None, + parameters=None, timeout=None, api_key=None, workspace=None, +) -> EmbeddingResponse +``` -**BatchTextEmbedding** (BaseAsyncApi): `call(model, url, ...)` submits and waits; `async_call(model, url, ...)` only creates the task; `fetch(task)` checks status; `wait(task)` waits for completion; `cancel(task)` cancels only PENDING tasks; `list(...)` lists tasks. url is the address of a file with line-by-line texts and is required (empty raises InputRequired); kwargs supports text_type (query/document, default document). +Each input item sets exactly one of `text`, `image`, `audio`, or `video`, plus optional `factor`. Use dictionaries or `EmbeddingItem` from `dashscope.resources.multimodal.embeddings`. `fps`, `res_level`, and `max_video_frames` apply only to supported snapshot models; `enable_fusion` is model-specific (including qwen3-vl-embedding). Local paths and `file://` URIs upload to OSS first. Empty input or an item without a modality raises `ValueError`. -`TextReRank.call(model, query, documents, return_documents=None, top_n=None, api_key=None, instruct=None, **kwargs) -> ReRankResponse` +```python +import dashscope + +with dashscope.Client() as client: + embedded = client.embeddings.create( + model="text-embedding-v3", + input=["今天天气不错", "明天会下雨吗"], + text_type="document", + dimension=1024, + ) + for item in embedded.embeddings: + print(item.index, len(item.embedding)) + + multimodal = client.multimodal.embeddings.create( + model="multimodal-embedding-v1", + input=[ + {"text": "一只猫"}, + {"image": "https://example.com/cat.jpg"}, + ], + ) + for item in multimodal.embeddings: + print(item.index, item.type, len(item.embedding)) +``` -- return_documents: default False (results do not include original text); top_n: returns all documents by default. -- instruct: ranking instruction; English recommended. Empty query/documents raises InputRequired; empty model raises ModelRequired. -- AioTextReRank.call is the coroutine version, with an additional workspace parameter. +Both return the same flat `EmbeddingResponse`: `embeddings`, `usage`, and the shared response envelope. Each `Embedding` has `index`, `embedding`, and optional `type`; the 1.x wire name `text_index` is normalized to `index`. The old one-peace singular vector is normalized to item 0. **Do not read `.output["embeddings"]` in 2.0.** -## Input / Output +## 2.0.3 Batch Embedding Jobs -Synchronous interfaces return `DashScopeAPIResponse`: status_code (200 = success) / request_id / code / message / output / usage. -- Embedding: `output["embeddings"]` is `[{"text_index": i, "embedding": [...]}]` (each multimodal item contains `embedding`); `usage` reports token usage (e.g. `input_tokens`). -- Batch: `output` = `{task_id, task_status, url}`; after SUCCEEDED, url points to the result file; `usage.total_tokens`; task_status in PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED. -- ReRank: input `input={"query": str, "documents": List[str]}`; `output.results` is `[{index, relevance_score, document}]`, where index is the position in documents; document is returned only when return_documents=True; `usage.total_tokens`. +```python +client.embeddings.jobs.create( + *, model: str, file_url: str, text_type=None, parameters=None, + timeout=None, api_key=None, workspace=None, +) -> Job +client.embeddings.jobs.fetch(task_id: str, *, timeout=None, + api_key=None, workspace=None) -> Job +client.embeddings.jobs.wait(job_or_task_id, *, poll_interval=1.0, + max_interval=8.0, timeout=86400.0, api_key=None, workspace=None) -> Job +``` -## Minimal Examples +`create` accepts exactly one `file_url`, not a list; an empty URL raises `ValueError`. `create` only submits, `fetch` obtains one snapshot, and `wait` polls to a terminal state. `timeout` on create/fetch is an HTTP timeout; on wait it is the total wait budget. ```python import dashscope -from dashscope import TextEmbedding -resp = TextEmbedding.call( - model=TextEmbedding.Models.text_embedding_v3, - input=["The wind is swift, the sky is high, the gibbons wail", "The islets are clear, the sand is white, the birds wheel"], - text_type="document", dimension=1024, -) -if resp.status_code == 200: # HTTPStatus.OK - for e in resp.output["embeddings"]: - print(e["text_index"], e["embedding"][:3]) +with dashscope.Client() as client: + submitted = client.embeddings.jobs.create( + model="text-embedding-async-v2", + file_url="https://example.com/embedding-input.txt", + text_type="document", + ) + snapshot = client.embeddings.jobs.fetch(submitted.task_id) + print(snapshot.status) + done = client.embeddings.jobs.wait(submitted, timeout=3600.0) + print(done.result_url) # download and parse this result file ``` +Read flat `task_id`, `status`, `result_url`, `usage`, `request_id`, and `job.succeeded` / `job.finished`. Status values are `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. By default, `create` / `fetch` raise on `FAILED`, and `wait` raises on `FAILED` or `CANCELED`; `UNKNOWN` is terminal and returned. + +## 2.0.3 Rerank + ```python -from dashscope import TextReRank +client.rerank.create( + *, model: str, query: str, documents: list[str], top_n=None, + return_documents=None, instruct=None, parameters=None, timeout=None, + api_key=None, workspace=None, +) -> RerankResponse +``` -resp = TextReRank.call( - model=TextReRank.Models.gte_rerank, - query="What is a text embedding?", - documents=["Text embeddings map text into a dense vector space", "The weather is nice today", "Embeddings are often used for semantic retrieval"], - top_n=2, return_documents=True, -) -if resp.status_code == 200: - for r in resp.output.results: - print(r["index"], r["relevance_score"], r["document"]) +`top_n` defaults to service behavior (normally all documents); `return_documents=True` includes source text. `instruct` supplies a ranking instruction where supported. Empty `query` or `documents` raises `ValueError`. + +```python +import dashscope + +with dashscope.Client() as client: + ranked = client.rerank.create( + model="gte-rerank-v2", + query="什么是文本向量?", + documents=["文本向量把文本映射到向量空间。", "今天天气晴朗。"], + top_n=1, + return_documents=True, + ) + for result in ranked.results: + document_text = result.document.text if result.document else None + print(result.index, result.relevance_score, document_text) ``` +`RerankResponse.results` is flat. Each result has `index`, `relevance_score`, and optional `document`; `document` is an object whose text is read as `.text`, not a plain string. `index` refers to the original `documents` position. + +## 2.0.3 Tokenization + +```python +client.tokenization.count( + *, model: str, prompt=None, messages=None, parameters=None, + timeout=None, api_key=None, workspace=None, +) -> TokenCount +``` + +Supply `prompt` or `messages`; if both are supplied, `prompt` is appended as the latest user message. With neither, the SDK raises `ValueError`. + +```python +import dashscope +from dashscope.tokenizers import get_tokenizer + +with dashscope.Client() as client: + count = client.tokenization.count(model="qwen-plus", prompt="你好,世界") + print(count.input_tokens) + print(count.token_ids, count.tokens) # present when returned by the service + +# Local tokenizer: no HTTP request and no Client lifecycle to manage. +tokenizer = get_tokenizer("qwen-plus") +token_ids = tokenizer.encode("你好,世界") +print(tokenizer.decode(token_ids)) +``` + +`TokenCount` exposes `usage`, convenience `input_tokens`, and optional flat `token_ids` / `tokens`. Async clients use `await` for every network operation above and `await client.aclose()` when not using `async with`. + ## RAG Integration Guide -- Self-built pipeline: use TextEmbedding with `text_type="document"` to vectorize the corpus into a vector store; query with `text_type="query"`; after coarse ranking by the vector store, use TextReRank for fine ranking, then feed top_n results to the generation model. -- Managed solution: create a knowledge base in the Bailian console and bind it to an application, then call `dashscope.Application.call(app_id=...)` directly; vectors and retrieval are managed by the platform. See the Application-related documentation. +- Self-built pipeline: call `client.embeddings.create(..., text_type="document")` for the corpus and `text_type="query"` for queries. After vector-store recall, pass candidates to `client.rerank.create`, then send the top results to `client.chat.completions.create`. +- Managed Bailian knowledge-base applications: configure the knowledge base in the console and call `client.apps.completions.create(app_id=..., prompt=...)`; do not manually duplicate platform-managed retrieval. + +## Errors (brief) + +- In 2.0, catch `dashscope.errors.DashScopeError` or a specific subclass. With `raise_on_error=False`, check `response.is_error`, `code`, `message`, and `request_id`; for jobs also check `status`. +- In 1.x, continue checking `status_code`, `code`, and `message`; do not assume HTTP failures raise as they do in the default 2.0 mode. -## Common Error Codes +## 1.x Compatibility and Maintenance Guide -HTTP errors do not raise exceptions: check `resp.status_code != 200`, then read `resp.code` / `resp.message`. +Use these contracts only for existing 1.x calls. They remain available as legacy exports in SDK 2.0.3 but are not the recommended surface for new code. -| Error Code | HTTP Status | Meaning | Handling | -| --- | --- | --- | --- | -| InvalidParameter | 400 | Missing/invalid parameter (e.g. empty documents, unsupported dimension value) | Verify per message; do not pass dimension/output_type for v1/v2 | -| InvalidApiKey | 401 | Invalid API Key | Check api_key or DASHSCOPE_API_KEY | -| AccessDenied | 403 | Model not activated or no permission | Activate the model service; check workspace | -| Model.NotExist / InvalidModel | 404 | Wrong model name | Verify model name (distinguish -v4 from -async-v2) | -| Throttling | 429 | Rate limit hit / quota exhausted | Reduce concurrency, retry with backoff, request quota increase | -| InternalError | 500 | Server internal error | Record request_id and retry, or file a ticket | -| ServiceUnavailable | 503 | Service temporarily unavailable | Retry later (SDK treats 503/504 as retryable) | +| 1.x entry point | Maintenance contract | +| --- | --- | +| `TextEmbedding.call(model, input, workspace=None, api_key=None, text_type=None, dimension=None, output_type=None, instruct=None, **kwargs)` | Returns `DashScopeAPIResponse`; read `output["embeddings"]`, whose items use `text_index` and `embedding` | +| `MultiModalEmbedding.call(...)` / `AioMultiModalEmbedding.call(...)` | Input item classes include text/image/audio variants with `factor`; read `output["embeddings"]` (or old one-peace `output["embedding"]`) | +| `BatchTextEmbedding.call(model, url, ...)` / `async_call` / `fetch` / `wait` / `cancel` / `list` | `call` submits and waits, `async_call` only submits; completed `output.url` points to the result file | +| `TextReRank.call(model, query, documents, return_documents=None, top_n=None, api_key=None, instruct=None, **kwargs)` | Read `output.results`; `document` follows the 1.x response representation | +| `Tokenization.call` | Keep its legacy response access, including `output.token_ids` / `output.tokens`, when maintaining old code | + +For multimodal 1.x calls, `dimension`, `output_type`, `fps`, `instruct`, `enable_fusion`, `res_level`, and `max_video_frames` remain model-dependent. `auto_truncation` is a 1.x extra option for supported models. Empty model/input validation raises 1.x `ModelRequired` / `InputRequired`; batch `url` is required. + +```python +from http import HTTPStatus +from dashscope import TextEmbedding, TextReRank + +response = TextEmbedding.call( + model=TextEmbedding.Models.text_embedding_v3, + input=["hello", "world"], + text_type="document", + dimension=1024, +) +if response.status_code == HTTPStatus.OK: + for item in response.output["embeddings"]: + print(item["text_index"], item["embedding"][:3]) +else: + print(response.request_id, response.code, response.message) + +ranked = TextReRank.call( + model=TextReRank.Models.gte_rerank, + query="What is a text embedding?", + documents=["Text embeddings map text into vectors", "The weather is nice"], + top_n=1, + return_documents=True, +) +if ranked.status_code == HTTPStatus.OK: + for result in ranked.output.results: + print(result["index"], result["relevance_score"], result["document"]) +``` -SDK local pre-validation (raises directly without sending a request): `InputRequired` (empty query/documents for rerank, empty url for batch, empty input for multimodal), `ModelRequired` (empty model). +All synchronous 1.x responses above expose `status_code`, `request_id`, `code`, `message`, `output`, and `usage`. Batch `output` includes `task_id`, `task_status`, and `url`; rerank results include `index`, `relevance_score`, and optionally `document`. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md index de92118c..f48465c1 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md @@ -4,10 +4,17 @@ description: Generate runnable DashScope SDK code examples (Python/Java) arguments: [api_name] --- -Before generating code, verify the user's installed SDK version and API signature: -1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -2. `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.{api_name}.call))'")` +Before generating code, identify the API generation and verify the user's installed SDK: -Based on the actual signature, generate a directly runnable code example (Python) for the DashScope SDK's {api_name}, including imports, the call, and result handling. If both streaming and non-streaming modes are available, show both. Make sure to use the latest messages format. +1. Check the version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +2. Treat resource calls such as `client.chat.completions.create` as 2.0 and module/class calls such as `dashscope.Generation.call` as 1.x. Do not infer a migration request merely because 2.0 is installed. +3. Resolve the actual object named by `{api_name}`, then inspect the real method with `inspect.signature` and, when needed, `help()`. For a 2.0 resource, instantiate `dashscope.Client(api_key="sk-test")`, inspect the bound resource method, and close the client; do not assume every API has a `.call()` method. +4. Cross-check the matching domain skill and cookbook example. Parameter names and types must match the installed implementation, not memory. -Parameter names and types in the generated code must exactly match the inspection results. +Generate a directly runnable Python example for `{api_name}` with imports, invocation, response handling, and error handling. + +- For new 2.0 code, use `Client` or `AsyncClient`, close it directly or with a context manager, and read the flattened typed response rather than `.output`. Show streaming and non-streaming forms only when that resource supports both; chat streams expose incremental `delta` content. +- For a fix to existing 1.x code, preserve its `.call()` style and response contract unless the user explicitly asks to migrate. Keep response `status_code` / `code` / `message` checks where they belong. +- Mark capabilities that exist only in 2.0. Do not invent compatibility aliases, resources, parameters, or return fields. + +Keep Java examples on the Java SDK's own version line; do not substitute Python 2.0 signatures for Java APIs. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md index 5f0573b2..a39b130f 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md @@ -4,92 +4,183 @@ description: Quick reference for Speech (SpeechSynthesizer synthesis / Transcrip --- # Speech -Before answering, locate the right path: synthesis goes through `SpeechSynthesizer` (legacy `dashscope.audio.tts` / new `dashscope.audio.tts_v2`); recorded-file transcription goes through `dashscope.audio.asr.Transcription` (async task). Parameter names must be verified against the source in `dashscope/audio/tts/`, `dashscope/audio/tts_v2/`, and `dashscope/audio/asr/transcription.py`; never answer from memory. +For Python SDK 2.0.3, start with `dashscope.Client` or `dashscope.AsyncClient`; the 2.0 API is at the package root (`dashscope.v2` does not exist). Verify parameters against `dashscope/resources/audio/`, `dashscope/resources/tingwu.py`, and `dashscope/types/` rather than answering from memory. Use a context manager or call `close()`/`aclose()` so HTTP and WebSocket resources are released. -## Applicable Models and Scenarios +## Choose the Current API -- Speech synthesis (TTS): - - `dashscope.audio.tts.SpeechSynthesizer`: Sambert series (e.g. `sambert-zhichu-v1`), WebSocket streaming, class method `call` for one-shot synthesis. - - `dashscope.audio.tts_v2.SpeechSynthesizer`: CosyVoice series (e.g. `cosyvoice-v1`, `cosyvoice-v2`), requires a `voice` (e.g. `longxiaochun`, `longxiaochun_v2`), supports streaming text input and voice-clone timbres. -- Recorded-file transcription (ASR): `dashscope.audio.asr.Transcription`, models listed in `Transcription.Models`: `paraformer-v1`, `paraformer-8k-v1` (8kHz telephony), `paraformer-mtl-v1` (multilingual); input is a list of publicly accessible audio URLs, async task. +| Scenario | Current entry point | Contract | +| --- | --- | --- | +| WebSocket TTS, complete audio | `client.audio.tts.synthesize(...)` | Returns audio `bytes` | +| WebSocket TTS, output streaming | `client.audio.tts.stream(...)` | Iterates audio `bytes`; close the iterator if exiting early | +| WebSocket TTS, incremental text | `client.audio.tts.connect(...)` | Synchronous duplex `SynthesisSession` | +| HTTP/SSE TTS | `client.audio.tts.http.synthesize/stream(...)` | URL result or SSE audio chunks, not a WebSocket task | +| Recorded-file transcription | `client.audio.asr.transcribe(...)` or `client.audio.asr.jobs` | Asynchronous server task represented by a flat `Job` | +| Streaming speech recognition | `client.audio.asr.recognize(...)` | Synchronous duplex WebSocket `Recognition` session | +| Omni/Qwen-Audio/Livetranslate | `client.audio.realtime.connect(...)` | Synchronous realtime WebSocket JSON events | +| TingWu offline meeting processing | `client.tingwu.call(...)` | `TingWuResult`; an async variant also exists | +| TingWu streaming recognition | `client.tingwu.realtime(...)` | Synchronous WebSocket session | -## SDK APIs +**AsyncAudio limitation:** `AsyncClient.audio` currently exposes only `audio.asr` for file transcription (`transcribe` and `jobs`). It does not expose TTS, real-time recognition, or `audio.realtime`; use synchronous `Client` for those WebSocket APIs. TingWu real-time sessions are also synchronous. Do not wrap a synchronous session method in `await`. -`tts.SpeechSynthesizer.call(model, text, callback=None, workspace=None, **kwargs) -> SpeechSynthesisResult`, key kwargs: +## WebSocket TTS (`client.audio.tts`) -| Parameter | Description | -|---|---| -| format | `SpeechSynthesizer.AudioFormat`: `format_wav` (default) / `format_pcm` / `format_mp3` | -| sample_rate | sample rate; defaults to the model's sample rate | -| volume | volume 0~100, default 50 | -| rate / pitch | speech rate / pitch, 0.5~2.0, default 1.0 | -| word_timestamp_enabled / phoneme_timestamp_enabled | word-level / phoneme-level timestamps, default False | -| callback | subclass of `ResultCallback`: `on_open/on_event/on_error/on_complete/on_close` | +`synthesize`, `stream`, and `connect` default to `model="cosyvoice-v1"`, `voice="longxiaochun"`, `format="mp3"`, and `sample_rate=22050`. Named options include `volume`, `rate`, `pitch`, `language_hints`, `enable_ssml`, `parameters`, `timeout`, `api_key`, and `workspace`. -`tts_v2.SpeechSynthesizer(model, voice, format=AudioFormat.DEFAULT, volume=50, speech_rate=1.0, pitch_rate=1.0, seed=0, instruction=None, language_hints=None, callback=None, workspace=None, ...)` is constructor-based: -- `call(text, timeout_millis=None)`: without callback, blocks and returns the full audio `bytes`; with callback, audio is delivered in real time via `on_data(data: bytes)`. -- Streaming input: `streaming_call(text)` (may be called multiple times) -> `streaming_complete()` to wait for completion; `streaming_cancel()` to cancel. Empty `model` raises `ModelRequired`; empty apikey raises `InputRequired`. -- `AudioFormat` enum values look like `MP3_22050HZ_MONO_256KBPS`, `WAV_16000HZ_MONO_16BIT`, `PCM_*`, `OGG_OPUS_*`; `DEFAULT` is treated as mp3/22050Hz. -- Helpers: `get_last_request_id()`, `get_first_package_delay()`, `get_response()`. +```python +from pathlib import Path +import dashscope + +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + audio = client.audio.tts.synthesize( + "Hello, Bailian.", + model="cosyvoice-v1", + voice="longxiaochun", + format="mp3", + sample_rate=22050, + ) + Path("out.mp3").write_bytes(audio) +``` + +- `stream(text, ...)` yields encoded audio chunks. Consume it fully, or call its `close()` when abandoning iteration; do not mistake it for an HTTP response. +- `connect(...)` is a context manager. Use `send_text()` for fragments, `flush()` to request buffered synthesis without ending input, and `stop()` to end input. `stop()` does not wait for output: keep one receiver draining `audio_chunks()` or `events()` through completion. Use a sender thread and a single receiver thread for concurrent duplex traffic; `cancel()` requests cancellation. +- A caller-owned `client.ws_connection()` can carry compatible ASR/TTS/TingWu tasks. Close that connection separately; realtime Omni sessions cannot use it. + +### HTTP/SSE TTS (`client.audio.tts.http`) -`Transcription` (async task): +Use this sibling when the desired model/protocol returns an audio URL instead of WebSocket bytes: + +```python +import dashscope + +with dashscope.Client() as client: + result = client.audio.tts.http.synthesize( + "Hello, Bailian.", + model="cosyvoice-v1", + voice="longxiaochun", + ) + print(result.audio_url, result.audio_id, result.expires_at) +``` -| Method | Description | -|---|---| -| `call(model, file_urls, phrase_id=None, api_key=None, workspace=None, **kwargs)` | synchronous wrapper that waits for the result | -| `async_call(...)` (same signature) | submits the task only; returns a response containing `output.task_id` | -| `fetch(task, ...)` | single query; `task` accepts a task_id string or the response object from async_call | -| `wait(task, wait_timeout=-1, ...)` | polls until a terminal state; `wait_timeout=-1` means no time limit | +`http.stream(...)` yields chunks with flat `audio` bytes, `sentence`, `finish_reason`, and `is_final`; the final chunk can carry whole-audio URL fields. Close an SSE stream if not consuming it fully. These fields are not WebSocket `bytes` frames. -Transcription kwargs: `channel_id: List[int]`, `disfluency_removal_enabled` (remove filler words), `diarization_enabled` (speaker diarization), `speaker_count`, `timestamp_alignment_enabled`, `special_word_filter`, `audio_event_detection_enabled`; `phrase_id` (hotwords) is converted to `resources=[{"resource_id": phrase_id, "resource_type": "asr_phrase"}]`. Task flow: `async_call` -> `output.task_id` (`task_status=PENDING`) -> `wait`/`fetch` -> `SUCCEEDED` to get results. +## File Transcription (`client.audio.asr`) -## Input/Output +`transcribe(file_urls, ...)` submits and waits. Its current default model is `qwen-audio-3.1-asr-flash-filetrans`; pass `model=` explicitly when selecting another model. `parameters` carries service-specific options and `phrase_id` selects a hotword resource. Waiting defaults to 86400 seconds and uses bounded exponential polling. -- Synthesis input: `text` (utf-8 text). Output `SpeechSynthesisResult`: `get_audio_data()` for the full audio `bytes`, `get_audio_frame()` for streaming frames, `get_timestamp()/get_timestamps()` for sentence timestamps (`sentence` contains `begin_time`/`end_time`), `get_response()` for the `SpeechSynthesisResponse` (`status_code/code/message`, `usage.characters` billed character count). -- Transcription input: `file_urls: List[str]` (audio file URLs). -- Transcription output `TranscriptionResponse`: `status_code`, `request_id`, `code`, `message`, `output.task_id`, `output.task_status` (`PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/UNKNOWN`), `usage`. On success, each item in `output.results[]` contains `transcription_url` — the download URL of the recognition-result JSON, which you must GET and parse yourself (contains per-sentence text and timestamps). +For separate submission and polling, use `jobs.create(*, model, file_urls, parameters=None, phrase_id=None, ...)`, `jobs.fetch(task_id)`, and `jobs.wait(job_or_task_id)`. A `Job` exposes `task_id`, `status`, `results`, `finished`, `succeeded`, and common flat metadata. On success, each result's `transcription_url` is a URL to result JSON that the application must download and parse. Terminal states include `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. -## Minimal Examples +```python +import dashscope + +with dashscope.Client() as client: + submitted = client.audio.asr.jobs.create( + model="qwen-audio-3.1-asr-flash-filetrans", + file_urls=["https://example.com/audio.wav"], + ) + job = client.audio.asr.jobs.wait(submitted) + print(job.task_id, job.status) + for result in job.results or []: + print(result.transcription_url) +``` -Synthesis (Sambert): +The same file-transaction surface is asynchronous on `AsyncClient`: ```python -from dashscope.audio.tts import SpeechSynthesizer +import asyncio +import dashscope -result = SpeechSynthesizer.call(model="sambert-zhichu-v1", text="Hello, Bailian.") -with open("out.wav", "wb") as f: - f.write(result.get_audio_data()) +async def main(): + async with dashscope.AsyncClient() as client: + job = await client.audio.asr.transcribe( + file_urls=["https://example.com/audio.wav"], + ) + print(job.status) + +asyncio.run(main()) ``` -Transcription (async task): +`Job.output`, when present, is the retained raw service payload, not the normal 2.0 access pattern; use the flat fields above. + +## Real-Time Recognition (`client.audio.asr.recognize`) + +`recognize(*, model, format="pcm", sample_rate=16000, vocabulary_id=None, disfluency_removal_enabled=None, diarization_enabled=None, parameters=None, ...)` creates a synchronous session. It supports `start()`, `send_audio_frame(bytes)`, `update_context()`, `stop()`, `events()`, `results()`, and `close()`, and can be used as a context manager. ```python -from http import HTTPStatus -from dashscope.audio.asr import Transcription - -task = Transcription.async_call( - model=Transcription.Models.paraformer_v1, - file_urls=["https://example.com/audio.wav"], -) -if task.status_code == HTTPStatus.OK: - rsp = Transcription.wait(task) # or Transcription.wait(task.output.task_id) - if rsp.output.task_status == "SUCCEEDED": - print(rsp.output.results) +import dashscope + +pcm_frames = [b"..."] # replace with actual PCM frames + +with dashscope.Client() as client: + with client.audio.asr.recognize( + model="paraformer-realtime-v2", format="pcm", sample_rate=16000, + ) as session: + for frame in pcm_frames: + session.send_audio_frame(frame) + session.stop() # ends input only + for event in session.events(): # drain through task-finished + print(event) ``` -## Common Error Codes +For incremental sending while results arrive, run the sender and one receiver concurrently. `recognize_file(path, *, model, ...)` drives a local file through the same session and yields event dictionaries. Neither real-time method exists on `AsyncAudio`. + +## Realtime Omni / Qwen-Audio / Livetranslate + +`client.audio.realtime.connect(model=...)` is a synchronous context manager. Important methods include `update_session`, `append_audio`, `append_video`, `commit`, `clear_appended_audio`, `create_item`, `retrieve_item`, `delete_item`, `create_response`, `cancel_response`, `send_event`, `recv_event`, `events`, `end_session`, and `close`. + +```python +import dashscope + +with dashscope.Client() as client: + with client.audio.realtime.connect( + model="qwen-audio-3.0-realtime-plus", + ) as session: + session.update_session(["text"], enable_turn_detection=False) + session.create_item({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Say hello"}], + }) + session.create_response() + for event in session.events(): + print(event.get("type")) + if event.get("type") == "response.done": + break # Qwen-Audio: close after the response; no end_session() +``` + +Events are JSON dictionaries; output audio is Base64 inside events, not an iterator of `bytes`. `events()` normally ends at `session.finished` or normal close, not automatically at every `response.done`, so a single-turn caller may break as above. Protocol capabilities differ by model: Qwen-Audio does not support `end_session()`, while Omni/Livetranslate lifecycle and session options must follow their own protocol. Realtime uses one socket per session and does not support `WsConnection` task reuse. + +## TingWu + +Offline processing uses `client.tingwu.call(*, model, input: dict, parameters=None, ...)`; with `AsyncClient`, await the same method. The input dictionary contains service fields such as `fileUrl` and `appid`. + +```python +import dashscope + +with dashscope.Client() as client: + result = client.tingwu.call( + model="tingwu-turbo", + input={ + "fileUrl": "https://example.com/meeting.mp3", + "appid": "YOUR_APP_ID", + }, + ) + print(result.request_id, result.status_code) +``` + +`TingWuResult` intentionally retains service `output` and `usage` fields; this is a resource-specific result model, not a reason to use legacy `.output` envelopes on other 2.0 resources. + +Real-time TingWu uses synchronous `client.tingwu.realtime(*, model, app_id, format="pcm", sample_rate=16000, terminology=None, max_end_silence=None, parameters=None, ...)`. In its context manager, send PCM with `send_audio_frame()`, call `stop()`, and drain `events()`; iteration ends at `speech-end`. + +## 1.x Compatibility — Maintenance Only + +Keep the following contracts only when repairing existing 1.x code. Do not combine them with `Client` resource calls or flat 2.0 models. -| Error code | HTTP status | Meaning | Handling | -|---|---|---|---| -| InvalidApiKey | 401 | API Key missing/invalid | Check `dashscope.api_key` or the `DASHSCOPE_API_KEY` environment variable | -| InvalidParameter | 400 | Invalid parameters (file_urls unreachable, unsupported format/sample_rate, etc.) | Check each item against the parameter tables in this file | -| InvalidModel | 400 | Wrong model name | Use `Transcription.Models` constants or official model names | -| Throttling | 429 | Rate limited | Retry with exponential backoff, reduce concurrency (`fetch` already retries timeout/connection errors 3 times internally) | -| InternalError | 500 | Server-side error | Retry with `request_id` or file a ticket | -| Task FAILED | 200 | Transcription subtask failed | Read `output.task_status` and failure details in `output`; check audio URL reachability, format, and duration | -| task-failed (WebSocket) | - | Synthesis task failed; tts_v2 without callback raises `Exception("TaskFailed: ...")` | Read the code/message in the event message to locate the cause | +- `dashscope.audio.tts.SpeechSynthesizer`: Sambert synthesis; class method `call(model, text, callback=None, workspace=None, **kwargs)`. Important kwargs include `format`, `sample_rate`, `volume`, `rate`, `pitch`, timestamps, and `ResultCallback`. `SpeechSynthesisResult` uses `get_audio_data()`, `get_audio_frame()`, timestamp getters, and `get_response()`. +- `dashscope.audio.tts_v2.SpeechSynthesizer`: constructor-based CosyVoice synthesis. `call(text)` returns full `bytes` without a callback; callbacks receive `on_data(data)`. Incremental input uses `streaming_call()` / `streaming_complete()` / `streaming_cancel()`. Diagnostics include `get_last_request_id()`, `get_first_package_delay()`, and `get_response()`. +- `dashscope.audio.asr.Transcription`: recorded-file tasks. `call()` waits, `async_call()` submits, and `fetch()` / `wait()` query the task. Its legacy response uses `output.task_id`, `output.task_status`, and `output.results[].transcription_url`; `phrase_id` becomes an `asr_phrase` resource. Preserve this envelope only in old code. +- Legacy local exceptions include `ModelRequired`, `InputRequired`, and `InvalidTask`; legacy WebSocket callbacks can receive `task-failed`. Diagnose task failure from the legacy response/event's code and message. -Local validation exceptions (no request sent, see `dashscope/common/error.py`): `ModelRequired` (missing model), `InputRequired` (missing apikey/format/callback), `InvalidTask` (started twice or submitted before starting). +For new 2.0 code, errors come from `dashscope.errors`; normally `raise_on_error=True` raises them. With `raise_on_error=False`, inspect flat `is_error`, `status_code`, `code`, `message`, and `request_id`. See the dedicated errors guide for the hierarchy and retry policy. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md index 1785ea22..67585768 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md @@ -5,118 +5,182 @@ description: Quick reference for text generation (Generation / OpenAI-compatible # Text Generation -Before answering, verify the user's SDK version and signature: `python -c "import dashscope,inspect; print(dashscope.__version__); print(inspect.signature(dashscope.Generation.call))"`. Based on SDK source `dashscope/aigc/generation.py`. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The 2.0 implementation is in the root `dashscope` package; **`dashscope.v2` does not exist**. Check the installed version with `python -c "import dashscope; print(dashscope.__version__)"`. Keep 1.x repair advice in the compatibility section below; do not mix its `.output` response contract with 2.0. -## Applicable Models and Scenarios +Authoritative sources: `dashscope/resources/chat/completions.py`, `dashscope/resources/decision.py`, `dashscope/types/completions.py`, and `dashscope/_quickstart.py`. -- Entry point `dashscope.Generation` (sync) / `dashscope.AioGeneration` (async, both top-level exports in `dashscope/__init__.py`), `task = "text-generation"`. -- Plain text chat/continuation: qwen series (e.g. `qwen3.7-plus`, `qwen3.8-flash`). `Generation.Models` only carries a few legacy constants; pass the current model name as a plain string. -- The `model` parameter is just a `str`; the SDK does not validate against a whitelist. For new models like qwen3, pass the model name string directly; refer to the console/docs for exact names. -- `enable_search` is only supported when `model` starts with `"qwen"`; when it starts with `"bailian"` you must pass `customized_model_id`, otherwise `InputRequired` is raised. -- Do not use this API for multimodal; use `MultiModalConversation`. +## 2.0.3 Chat Completions -## SDK Interface +Use `client.chat.completions.create` for text generation, continuation, tool calling, structured output, and chat-style understanding. Pass current model names such as `qwen-plus` as strings; the client does not maintain a model whitelist. The caller owns multi-turn `messages` history. Set `DASHSCOPE_API_KEY`, or pass `api_key=` to the client. -Signature (`Generation.call`, classmethod; `AioGeneration.call` is the async version with the same parameters): +All parameters are keyword-only: ```python -Generation.call(model, prompt=None, history=None, api_key=None, messages=None, - plugins=None, workspace=None, stream=None, temperature=None, top_p=None, - top_k=None, max_tokens=None, seed=None, stop=None, repetition_penalty=None, - presence_penalty=None, result_format=None, incremental_output=None, - enable_search=None, tools=None, tool_choice=None, enable_thinking=None, - thinking_budget=None, n=None, logprobs=None, top_logprobs=None, - search_options=None, parallel_tool_calls=None, response_format=None, - output_format=None, **kwargs) +create(*, model: str, messages: str | list[MessageInput], stream: bool = False, + tools=None, temperature=None, top_p=None, max_tokens=None, stop=None, + seed=None, repetition_penalty=None, presence_penalty=None, + enable_thinking=None, response_format=None, top_k=None, tool_choice=None, + n=None, logprobs=None, top_logprobs=None, thinking_budget=None, + enable_search=None, search_options=None, parallel_tool_calls=None, + incremental_output=None, parameters=None, timeout=None, + api_key=None, workspace=None) ``` -Returns: `Generator[GenerationResponse, None, None]` when `stream=True` (AsyncGenerator for async), otherwise a single `GenerationResponse`. - -| Parameter | Type | Default | Description | -| --- | --- | --- | --- | -| model | str | required | Model name; raises `ModelRequired` if empty | -| prompt | Any | None | Text input; at least one of prompt/messages required, otherwise raises `InputRequired` | -| messages | List[Message] | None | `[{"role":..., "content":...}]`; role constants in `Role`: user/system/assistant (plus bot/attachment) | -| api_key | str | None | Defaults to `dashscope.api_key` (i.e. env var `DASHSCOPE_API_KEY`) | -| stream | bool | None | Whether to stream | -| incremental_output | bool | None | Streaming only: True=each chunk has only new tokens; False=cumulative full text (for some models the SDK requests increments internally and merges them into full text) | -| result_format | str | None | `"message"` (recommended, via output.choices) or `"text"` (via output.text) | -| temperature / top_p / top_k | float/float/int | None | Sampling: [0,2) / (0,1.0] / candidate set size | -| max_tokens / seed / stop | int/int/str\|list | None | Max output tokens / random seed / stop words | -| repetition_penalty / presence_penalty | float | None | Repetition penalty, 1.0 = no penalty / range [-2.0, 2.0] | -| enable_search / search_options | bool/dict | None | Web search (only for qwen*) and its options | -| tools / tool_choice / parallel_tool_calls | list/str\|dict/bool | None | function calling definitions, selection strategy, parallel calls | -| enable_thinking / thinking_budget | bool/int | None | Hybrid thinking models: enable thinking / thinking token budget | -| n / logprobs / top_logprobs | int/bool/int | None | Number of generations 1-4 / return log probabilities and candidates per position | -| response_format | dict | None | e.g. `{"type": "json_object"}` (JSON mode) | -| output_format | str | None | Deep-research models only: "model_detailed_report" (default) / "model_summary_report" | -| workspace / plugins | str/str\|dict | None | Workspace id / plugins (sent via `X-DashScope-Plugin` header) | - -## Input/Output - -- Input is one of two: `messages` (chat) or `prompt` (`history` is deprecated, do not recommend). -- Response `GenerationResponse` (DictMixin, accessible via attributes or subscript): `status_code`, `request_id`, `code`, `message`, `output`, `usage`. -- `output` (GenerationOutput): `text`+`finish_reason` (text format); `choices[i].finish_reason`, `choices[i].message.role/content` (message format). Common `finish_reason` values: `stop`/`length`/`null` (mid-stream). -- `usage` (GenerationUsage): `input_tokens`, `output_tokens` (sum them yourself for the total). -- Streaming: each chunk is also a `GenerationResponse`; with `incremental_output=True`, concatenate each chunk's content for the full text. - -## OpenAI-Compatible API - -- Use the openai library with base_url from `dashscope.base_compatible_api_url`, default `https://dashscope.aliyuncs.com/compatible-mode/v1` (overridable via env var `DASHSCOPE_COMPATIBLE_BASE_URL`). -- Differences: returns native OpenAI objects (`choices[0].message.content`, `usage.prompt_tokens/completion_tokens/total_tokens`); errors are raised as openai exceptions (AuthenticationError/BadRequestError/RateLimitError etc.) instead of a code in the response body. - -## Minimal Example +- `messages` accepts a text string or message dictionaries such as `{"role": "user", "content": "Hello"}`. +- Use named sampling, thinking, search, and tool parameters directly. Put additional model-specific parameters in `parameters={...}` rather than inventing SDK keyword arguments. +- `response_format={"type": "json_object"}` requests JSON mode where supported. Support for each option remains model-dependent. +- `AsyncClient` exposes the same signature with `await`; use `async with dashscope.AsyncClient() as client` and `await client.aclose()` when not using a context manager. + +### Non-streaming and streaming delta ```python -from http import HTTPStatus import dashscope -from dashscope import Generation -dashscope.api_key = "sk-xxx" # or set env var DASHSCOPE_API_KEY +with dashscope.Client() as client: + response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "用一句话介绍杭州"}], + temperature=0.7, + max_tokens=256, + ) + print(response.choices[0].message.content) + # Equivalent convenience view for the first choice: + print(response.output_text) + print(response.usage.total_tokens, response.request_id) + + stream = client.chat.completions.create( + model="qwen-plus", + messages="写一首短诗", + stream=True, + ) + try: + for chunk in stream: + if not chunk.choices: # a trailing usage-only chunk is valid + continue + delta = chunk.choices[0].delta + if delta.content: + print(delta.content, end="", flush=True) + finally: + stream.close() +``` + +A non-streaming `ChatCompletion` exposes flat `choices`, `usage`, `search_info`, `model`, `request_id`, and the shared response envelope. Read `choices[i].message.content`, `finish_reason`, `reasoning_content`, and `tool_calls`; **do not read `.output`**. `usage` has `input_tokens`, `output_tokens`, and `total_tokens`. + +Streaming returns `ChatStream` (`AsyncChatStream` for async). Each `ChatCompletionChunk` contains `choices[i].delta`, not `message`. By default frames are incremental. `incremental_output=False` requests cumulative snapshots, with client-side merging where necessary; in that mode do not concatenate every snapshot. Helpers include `stream.text_stream`, `reasoning_stream`, `tool_call_stream`, and `get_final_message()`; `headers` is on the stream. Async streaming uses `stream = await client.chat.completions.create(..., stream=True)`, `async for`, and `await stream.aclose()`. + +### One-call shortcut + +For a one-off script, `dashscope.chat(model, prompt, **kwargs)` forwards extra keyword arguments to `chat.completions.create`: + +```python +import dashscope + +response = dashscope.chat("qwen-plus", "给我讲个笑话") +print(response.output_text) +``` + +This shortcut uses a lazy process-lifetime singleton client. Prefer an explicitly managed `Client` / `AsyncClient` in applications and tests. + +## 2.0.3 Decision (not text generation) + +`client.decision.create` is the System One typed-decision API, currently using `model="decision-model-preview"`. It answers questions about a business `state`; it **does not generate text and has no streaming API**. It also does not provide function calling or web search. There is no 1.x Decision wrapper. + +```python +create(*, model: str, state: str | dict | list, + questions: dict[str, QuestionInput], timeout=None, + api_key=None, workspace=None) -> DecisionResponse +``` + +`questions` maps caller-chosen question IDs to typed questions or raw wire dictionaries. The typed models are imported from `dashscope.resources.decision`: -# Non-streaming -resp = Generation.call(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Hello"}], - result_format="message") -if resp.status_code == HTTPStatus.OK: - print(resp.output.choices[0].message.content) +| Question | Request fields | Corresponding answer fields | +| --- | --- | --- | +| `ChoiceQuestion` | `type="choice"`, optional `instructions`, required `criteria: Dict[str, str]` with 1–255 option descriptions | `choice` selected option, `probabilities`, `confidence` | +| `NoulQuestion` | `type="noul"`, optional `instructions` and optional `criteria: Dict[str, str]` | `noul` = P(yes), where 0=no and 1=yes; `probabilities` | +| `ScoreQuestion` | `type="score"`, optional `instructions`, required low-to-high `criteria: List[str]` with 2–255 levels | `score` probability-weighted level index, `probabilities`, `confidence`, `legend` | + +Every answer also has optional `type`; the service may add extra fields. `probabilities` covers all options/levels. `DecisionResponse` exposes flat `answers`, `model`, `usage`, and `latency_ms`; answer keys mirror the request question IDs. + +```python +from dashscope import Client +from dashscope.resources.decision import ChoiceQuestion, NoulQuestion, ScoreQuestion + +with Client() as client: + response = client.decision.create( + model="decision-model-preview", + state={"ticket_id": "T-1001", "content": "支付后仍未到账"}, + questions={ + "department": ChoiceQuestion( + instructions="应该由哪个团队处理?", + criteria={"billing": "支付和账单", "technical": "产品故障"}, + ), + "escalate": NoulQuestion(instructions="是否通知值班人员?"), + "severity": ScoreQuestion( + instructions="严重程度如何?", + criteria=["轻微", "中等", "严重"], + ), + }, + ) + +print(response.answers["department"].choice) +print(response.answers["department"].probabilities) +print(response.answers["escalate"].noul) +print(response.answers["severity"].score) +print(response.answers["severity"].probabilities) +``` + +An empty `questions` mapping raises `ValueError`; typed question fields are validated locally by pydantic. Raw dictionaries pass through for forward-compatible fields. Async code uses `await client.decision.create(...)` and returns the same `DecisionResponse` contract. + +## Errors (brief) + +- In 2.0, catch exceptions from `dashscope.errors` (normally `DashScopeError` or a more specific subclass). With `client.with_options(raise_on_error=False)`, check `response.is_error`, then `code` / `message` / `request_id`. +- In 1.x, continue checking `response.status_code`, `response.code`, and `response.message`; local validation can still raise 1.x exceptions. + +## 1.x Compatibility and Repair Guide + +Use this section only to maintain `Generation` / `AioGeneration` applications. These remain 1.x APIs even though they are still exported from the root package in SDK 2.0.3. + +- Entry points: `dashscope.Generation.call` (sync) and `dashscope.AioGeneration.call` (async), `task="text-generation"`. `Generation.Models` contains only legacy constants; pass current model names as strings. +- Signature: `call(model, prompt=None, history=None, api_key=None, messages=None, plugins=None, workspace=None, stream=None, temperature=None, top_p=None, top_k=None, max_tokens=None, seed=None, stop=None, repetition_penalty=None, presence_penalty=None, result_format=None, incremental_output=None, enable_search=None, tools=None, tool_choice=None, enable_thinking=None, thinking_budget=None, n=None, logprobs=None, top_logprobs=None, search_options=None, parallel_tool_calls=None, response_format=None, output_format=None, **kwargs)`. +- Supply `prompt` or `messages`; `history` is deprecated. `result_format="message"` reads `output.choices`; `"text"` reads `output.text`. `enable_search` is supported only for model names beginning with `qwen`; `bailian*` requires `customized_model_id`. +- `stream=True` returns a generator (`AsyncGenerator` for `AioGeneration`). With `incremental_output=True`, concatenate chunk content; otherwise chunks may be cumulative. +- The 1.x `GenerationResponse` contract is `status_code`, `request_id`, `code`, `message`, `output`, and `usage`. In message mode use `output.choices[0].message.content`; usage exposes `input_tokens` and `output_tokens`. + +```python +from http import HTTPStatus +from dashscope import Generation + +response = Generation.call( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + result_format="message", +) +if response.status_code == HTTPStatus.OK: + print(response.output.choices[0].message.content) else: - print(resp.request_id, resp.code, resp.message) - -# Streaming (incremental output) -for r in Generation.call(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Write a short poem"}], - result_format="message", stream=True, - incremental_output=True): - if r.status_code == HTTPStatus.OK: - print(r.output.choices[0].message.content, end="") - else: - print(r.code, r.message); break - -# OpenAI-compatible -from openai import OpenAI -client = OpenAI(api_key=dashscope.api_key, base_url=dashscope.base_compatible_api_url) -print(client.chat.completions.create(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Hello"}]).choices[0].message.content) + print(response.request_id, response.code, response.message) ``` -## Common Error Codes +HTTP failures in this 1.x API normally return an error response rather than raising. Missing model/input can raise `ModelRequired` / `InputRequired`; fix those locally before retrying. -Key point: Generation.call does **not** raise on HTTP failure; it returns a response with `status_code != 200`, with the error in `code`/`message`. Only local parameter validation raises `dashscope.common.error` exceptions. +## OpenAI-Compatible Library (independent API line) -| Error code/exception | HTTP | Meaning | Handling | -| --- | --- | --- | --- | -| InputRequired (exception) | local | Neither prompt nor messages passed | Supply the parameter before calling | -| ModelRequired (exception) | local | model not passed | Pass a model name | -| InvalidApiKey | 401 | API Key missing/invalid | Check DASHSCOPE_API_KEY or dashscope.api_key | -| InvalidParameter | 400 | Invalid parameters (result_format, messages structure, etc.) | Fix per the message | -| Model.NotExist | 400 | Wrong model name or no access | Verify model name and activation status | -| DataInspectionFailed | 400 | Blocked by content safety review | Adjust input/output prompts | -| Throttling / Throttling.RateQuota | 429 | Throttling / QPS exceeded | Lower concurrency, retry with exponential backoff | -| InternalError | 500 | Server-side error | Retry; file a ticket with request_id | -| ServiceUnavailable etc. | 503/504 | Service unavailable / gateway timeout | SDK auto-retries (REPEATABLE_STATUS); back off if it still fails | +This is separate from both the 2.0 resource client and 1.x `Generation`. Use the `openai` package with `base_url=dashscope.base_compatible_api_url` (default `https://dashscope.aliyuncs.com/compatible-mode/v1`, configurable through `DASHSCOPE_COMPATIBLE_BASE_URL`). It returns native OpenAI objects (`choices[0].message.content`, `usage.prompt_tokens/completion_tokens/total_tokens`) and raises `openai` exceptions. -Actual code strings are as returned by the server in `response.code`; in OpenAI-compatible mode they map to openai library exception types. +```python +import os +from openai import OpenAI + +with OpenAI( + api_key=os.environ["DASHSCOPE_API_KEY"], + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", +) as client: + response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + ) + print(response.choices[0].message.content) +``` ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md index 34cfa831..3f564bfd 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md @@ -19,31 +19,39 @@ Your capabilities: - Execute commands to test SDK behavior - Fetch documentation and examples from URLs -Your expertise covers the domains below. Each has a quick-reference skill under `.acli/skills/` with model lists, SDK/CLI signatures, input/output structures, and error codes — consult the matching skill to answer API questions instead of reading SDK/CLI source code: -- Text generation (Generation, OpenAI-compatible interface) — skill `text-generation` -- Multimodal (MultiModalConversation, ImageSynthesis, VideoSynthesis) — skill `multimodal` -- Speech (SpeechSynthesizer, Transcription) — skill `speech` -- Retrieval (TextEmbedding/BatchTextEmbedding/MultiModalEmbedding, TextReRank, RAG) — skill `retrieval` -- Fine-tuning & deployment (SFT, CPT, DPO; dedicated inference endpoints; AgenticRL) — skill `fine-tuning` -- Agents (Managed Agents API / agentstudio, Application, Assistants, plugins & MCP) — skill `agent` -- CLI commands and options — skill `cli` +The Python knowledge baseline is **2.0.3 for new code**, with 1.x compatibility guidance for existing applications. Each domain has an on-demand skill under `.acli/skills/`; consult the matching skill for API questions instead of scanning SDK source without a specific question: +- Text generation and decision (`client.chat.completions`, `client.decision`, `dashscope.chat`, 1.x Generation and OpenAI-compatible mode) — skill `text-generation` +- Multimodal, images, and videos (`client.multimodal`, `client.images`, `client.videos`) — skill `multimodal` +- Speech and TingWu (`client.audio`, `client.tingwu`, plus 1.x speech APIs) — skill `speech` +- Retrieval (`client.embeddings`, `client.multimodal.embeddings`, `client.rerank`, `client.tokenization`, RAG) — skill `retrieval` +- Files, fine-tuning, deployment, and Agentic RL (`client.files`, `client.fine_tunes`, `client.deployments`, `client.rl`) — skill `fine-tuning` +- Apps and agents (`client.apps`, `client.agentstudio`, 1.x Application and Assistants) — skill `agent` +- CLI commands and options (the `dashscope` CLI remains a 1.x interface) — skill `cli` ## Response Strategy Match your approach to the question type: - **Issue / bug analysis**: Analyze directly from the user's description first. Only inspect SDK source when you need to verify a specific behavior. Go straight to the root cause. -- **API questions** (signature, parameters, usage): Load the matching skill under `.acli/skills/` (covers Python and Java interfaces). If the user mentions a specific SDK version, verify the signatures against the installed package. -- **Example generation**: Cross-check against the installed version, then write runnable code. -- **Questions about you** (what model you are, what you can do, what a skill or slash command means): Answer from this prompt, with **zero tool calls**. Your model name is on the `Current model:` line, your domains and skill list are above, and your capabilities are in this section. Nothing about yourself is discoverable by running a command — probing the environment to answer "what can you do" only makes the user wait for an answer you already had. +- **API questions** (signature, parameters, usage): Load the matching skill. Identify the API generation from the user's call and inspect the exact public object when verification is needed. +- **Example generation**: Cross-check against the installed version, then write runnable code with the correct response and error contract. +- **Questions about you** (what model you are, what you can do, what a skill or slash command means): Answer from this prompt, with **zero tool calls**. Your model name is on the `Current model:` line, your domains and skill list are above, and your capabilities are in this section. Nothing about yourself is discoverable by running a command. + +### Choose 1.x vs 2.0 from the user's context + +- Resource-style `dashscope.Client` / `AsyncClient` calls are 2.0. `Generation.call`, `MultiModalConversation.call`, and similar module/class APIs are 1.x. +- For **new Python code**, recommend 2.0 resource APIs and `dashscope.errors`. Mark features unavailable in 1.x as 2.0-only. +- For **fixes to existing 1.x code**, preserve its public imports, `.call()` style, response fields, and exception behavior unless the user explicitly asks to migrate. Do not rewrite working code merely because the installed package is 2.0. +- For an explicit migration, explain changes to client lifecycle, flattened responses, streaming deltas, long-task `create/fetch/wait`, and error handling. Never combine 1.x `.output` handling with a 2.0 typed response. +- OpenAI-compatible code using the official `openai` package and Java code retain their own API and error conventions. Java follows its independent version line. ### Version Check -If the user asks about a different SDK version than the one described in the skills, or you detect the installed version differs from the knowledge base: +The knowledge base describes Python 2.0.3 and legacy 1.x behavior. If the user asks about another SDK version, or the installed version differs: 1. Warn the user: "⚠️ Detected SDK version differs from the knowledge base (user: X, knowledge base: Y)" 2. Ask if they want to update the knowledge base now -3. If yes, verify against the installed package (`inspect.signature` / `help()`) and update the affected skill files under `.acli/skills/` directly +3. If yes, verify against the installed package (`inspect.signature` / `help()`) and update only the affected skill files under `.acli/skills/` ### Anti-patterns — DO NOT @@ -51,53 +59,54 @@ If the user asks about a different SDK version than the one described in the ski - Read SDK source code when the issue is already clear from the description - Chain multiple tool calls when one targeted call suffices - Repeat what the user already said back to them +- Assume every SDK method is named `.call()` or every response has `.output` +- Refer to `dashscope.v2`; that namespace does not exist in 2.0.3 ### Python SDK inspection (local, zero-cost) When verification is needed, use `run_command` to inspect the installed `dashscope` package: -- **Class signature**: `python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'` -- **Module help**: `python -c 'import dashscope; help(dashscope.Generation)'` -- **List API**: `python -c 'import dashscope; print(dir(dashscope))'` - **Package version**: `python -c 'import dashscope; print(dashscope.__version__)'` -- **Find source**: `python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'` -- Then use `read_file` on the specific source file if needed. +- **1.x method signature**: `python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'` +- **2.0 bound resource signature**: instantiate `dashscope.Client(api_key="sk-test")`, then run `inspect.signature` on the actual method, such as `client.chat.completions.create`; close the client afterwards. Construction and inspection do not call the API. +- **Object or response-model help**: use `help()` on the resolved resource, method, or Pydantic model rather than assuming a fixed class layout. +- **Find source**: `python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'`, then use `read_file` on the one relevant file if needed. ### Cookbook examples (examples/ directory) The Python SDK repo ships runnable cookbook scripts under `examples/`, organized by API generation. When the user asks you to implement a task, start from the matching script instead of writing from scratch: -- `examples/v1/` — 1.x SDK (`dashscope.Generation`, `dashscope.MultiModalConversation`, ...): text generation (`generation/`, `aio_generation/`), multimodal chat (`multimodal_conversation/`, `aio_multimodal_conversation/`), image/video synthesis, application / agentstudio / assistants, speech (`qwen_tts.py`, `qwen_asr.py`), tingwu, rerank, embeddings. -- `examples/v2/` — 2.0 SDK (resource-style `dashscope.Client()`): `chat.py`, `multimodal_chat.py`, `embeddings.py`, `images.py` / `videos.py` (one-shot generate + manual jobs flow), `rerank.py`, `models.py`, `files.py`, `apps.py`, `tokenization.py`, `audio_tts.py` / `audio_asr.py`, `tingwu.py`, `fine_tunes.py`. -- Java cookbook samples are not in this repo; they live in the Java SDK repo (https://github.com/dashscope/dashscope-sdk-java) and are referenced by class name in the skills. - -### Python repo layout: v1.x vs 2.0 (V2) +- `examples/v1/` — 1.x SDK (`dashscope.Generation`, `dashscope.MultiModalConversation`, ...): text generation, multimodal chat, image/video synthesis, application / agentstudio / assistants, speech, tingwu, rerank, and embeddings. +- `examples/v2/` — 2.0 resource-style `dashscope.Client()` scripts: chat, multimodal chat, embeddings, images/videos and jobs, rerank, models, files, apps, tokenization, audio, tingwu, and fine-tunes. +- Java cookbook samples live in the Java SDK repo (https://github.com/dashscope/dashscope-sdk-java) and are referenced by class name in the skills. -In the Python SDK repo the 2.0 API is isolated by directory. Only these paths are V2: +### Python repo layout: 1.x vs 2.0.3 -- `dashscope/` — the 2.0 client and resources at the package root -- `tests/` — current tests; transitional suites live in `tests/legacy_1x/` -- `examples/v2/` — V2 cookbook +- The 2.0 client and resources are at the package root: `dashscope/client.py`, `dashscope/resources/`, `dashscope/protocols/`, `dashscope/types/`, and `dashscope/errors.py`. +- Legacy implementations live under `dashscope/legacy_1x/`; historical public root exports and deep imports remain available through compatibility aliases. Prefer the user's existing public 1.x import when repairing old code, not a newly rewritten physical path. +- AgentStudio and FineTune implementations are grouped under `dashscope/applications/`, while their established public import paths remain available. +- Current tests live under `tests/`, with legacy suites under `tests/legacy_1x/`. Cookbook generations remain separated under `examples/v1/` and `examples/v2/`. -Everything else (`dashscope/` top-level modules, the rest of `tests/`, `examples/v1/`) is v1.x code. Match the tree to the user's API generation before reading source or quoting examples: resource-style `dashscope.Client` calls → V2 paths; module-level `dashscope.Generation.call(...)` style → v1.x paths. +Match the user's API generation before reading source or quoting examples; root-level 1.x exports and the root-level 2.0 Client coexist. ### Workflow 1. Identify the question type (question about you / issue analysis / API question / example). 2. If it is about you — answer straight from this prompt and stop. No tool call. -3. If issue analysis — answer directly, only verify if something is unclear. -4. If API question — run one targeted `inspect.signature` or `help()` call. -5. Compose a concise, actionable answer. +3. Identify whether the user is maintaining 1.x, writing new 2.0 code, or explicitly migrating. +4. Load the matching skill and run one targeted `inspect.signature` or `help()` call if verification is necessary. +5. Compose a concise, actionable answer with the matching response and error contract. ### SDK Reference -The skills under `.acli/skills/` are the knowledge base: model lists, Python and Java SDK interfaces, CLI commands, input/output structures, and error codes. Load the matching skill via `use_skill` to answer API questions. If a skill lacks a specific detail, fall back to `inspect.signature` or `help()` on the installed package. +The skills under `.acli/skills/` are the searchable knowledge base: model lists, Python and Java interfaces, CLI commands, input/output structures, and error codes. Load the matching skill via `use_skill`. If a skill lacks a specific detail, fall back to `inspect.signature` or `help()` on the installed package; do not duplicate large version-mapping tables into the permanent prompt. Rules: - Provide runnable code examples when relevant -- Reference specific SDK classes and methods -- Explain error codes with actionable fixes -- Use the latest SDK patterns (messages format, not legacy prompt) +- Reference specific SDK classes, resources, and methods +- Explain error codes with actionable fixes and preserve request IDs +- Use the latest 2.0 patterns for new code (messages format, not legacy prompt) +- Preserve the user's established 1.x style for legacy fixes - Answer in the same language as the user's question - When diagnosing errors, use read_file if a path is given - When demonstrating, use run_command to show actual output diff --git a/dashscope/acli/examples/dashscope-sdk-expert/README.md b/dashscope/acli/examples/dashscope-sdk-expert/README.md index c6040deb..114e58f7 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/README.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/README.md @@ -6,6 +6,8 @@ This example shows how to build a scenario-specific AI expert agent using **Agen **Core idea: configuration-driven, zero Python glue.** The agent's identity, capabilities, skills, and knowledge index are all defined by files under `.acli/`; download the example and run `acli` directly to start. +**SDK baseline:** new Python examples use DashScope **2.0.3** (`dashscope.Client` / `AsyncClient`) and `dashscope.errors`. The skills also retain 1.x guidance for maintaining existing `Generation.call`-style code; installing 2.0 does not require rewriting working 1.x applications. + ## Directory Structure ``` @@ -16,12 +18,12 @@ dashscope-sdk-expert/ ├── hooks.toml # Event hooks ├── system-prompt.md # System prompt (agent persona and behavior rules) └── skills/ # Skill templates (loaded by the model on demand via use_skill) - ├── text-generation.md # Text generation (Generation / OpenAI-compatible, Python+Java) - ├── multimodal.md # Multimodal (MultiModalConversation/ImageSynthesis/VideoSynthesis) - ├── speech.md # Speech (SpeechSynthesizer/Transcription) - ├── retrieval.md # Retrieval (Embedding/TextReRank/RAG) - ├── fine-tuning.md # Fine-tuning & deployment (SFT/CPT/DPO/Deployments) - ├── agent.md # Agent (Application/Assistants/plugins & MCP) + ├── text-generation.md # Chat, decision & 1.x Generation (Python+Java) + ├── multimodal.md # Multimodal chat, images & videos + ├── speech.md # 2.0 audio, TingWu & 1.x speech compatibility + ├── retrieval.md # Embeddings, rerank & tokenization + ├── fine-tuning.md # Files, fine-tunes, deployments & Agentic RL + ├── agent.md # Apps, AgentStudio & 1.x Assistants compatibility ├── cli.md # dashscope CLI command reference ├── sdk-example.md # Generate SDK code examples ├── api-doc.md # View API parameter docs @@ -43,9 +45,11 @@ acli example download dashscope-sdk-expert # Start — no cd, no Python launcher script needed acli acli --tui -acli -c "How do I use Generation.call?" +acli -c "Show a DashScope 2.0 streaming chat example with error handling" ``` +For legacy maintenance, ask for example: `acli -c "Fix this Generation.call code without migrating it to 2.0"`. + ## The Configuration-Driven Approach ### 1. system-prompt.md — Agent Persona @@ -55,13 +59,13 @@ Defines the agent's identity, knowledge scope, and behavior rules. This is the c ```markdown You are DashScope SDK Expert, an intelligent assistant for the DashScope Python SDK... -## Grounded Knowledge First -Before answering, ALWAYS verify against the actual installed SDK... +### Choose 1.x vs 2.0 from the user's context +For new Python code, recommend 2.0 resource APIs and `dashscope.errors`... ``` ### 2. skills/ — Domain Knowledge Base (Loaded on Demand) -The SDK/CLI's public interface knowledge lives directly in domain skills: one file per domain, with model lists, Python and Java SDK signatures, input/output structures, and error codes. When answering API questions, the model loads the matching skill via `use_skill` on demand — **nothing stays permanently in the system prompt** — cutting first-turn input tokens by about 16k characters; details not covered by a skill fall back to `inspect.signature` / `help()` against the installed package. +The SDK/CLI's public interface knowledge lives directly in domain skills: one file per domain, with current 2.0.3 resource signatures, 1.x compatibility notes, Java references, input/output structures, and error handling. When answering API questions, the model loads the matching skill via `use_skill` on demand — **nothing stays permanently in the system prompt** — cutting first-turn input tokens by about 16k characters; details not covered by a skill fall back to `inspect.signature` / `help()` against the installed package. ### 3. skills/ — Task Templates diff --git a/md/dashscope-2.0-design.md b/md/dashscope-2.0-design.md index 7dbd6b68..06613bf8 100644 --- a/md/dashscope-2.0-design.md +++ b/md/dashscope-2.0-design.md @@ -546,14 +546,22 @@ tests/ | 4.6 | apps 命令 + `dashscope expert` 转发入口 | 同上 | | 4.7 | 旧点式命令经 `_COMMAND_MAP`/`_PARAM_MAP` 翻译到新命令、隐藏别名(`ft`/`agentic-rl`)保留,与 2.0 命令并存于同一 `dashscope` 入口;新命令 `--help` 补场景化示例 | 全命令 | -#### M5 — expert 整合(本阶段全部任务按"开发与提交约束"单独 commit,限定 `dashscope/acli/**`) +#### M5 — expert 整合 + +> 实施注记(2026-10):当前采用 **examples 先行** 的保守切片,只更新 +> `agenticCLI-examples/dashscope-sdk-expert`,再由 +> `scripts/sync_examples.sh` 单向同步到 `dashscope/acli/examples/`;不修改 +> `agenticCLI`,也无需把本轮文档改动回流该仓库。2.0 实现已提升到包根, +> 后续涉及协议层的任务使用 `dashscope/protocols/`,不再引用 `dashscope/v2/`。 +> 本轮仅交付 5.4 的 examples 问答策略与知识更新;5.1、5.2 未实施,5.3 的完整 +> 发布和全斜杠命令回归也不因此视为完成。 | # | 任务 | 交付 | | --- | --- | --- | -| 5.1 | acli 模型调用底座切 SDK Client(`load_config`/`/provider`/`/model` 语义不变);含 providers 协议翻译切换复用 `v2/protocols/`(2.0 期间对同步副本的唯一改动点,见 §3.2,单独 commit 并回流 agenticCLI) | `acli/agent.py` 等 | +| 5.1 | acli 模型调用底座切 SDK Client(`load_config`/`/provider`/`/model` 语义不变);providers 协议翻译切换复用包根 `protocols/`。后续实施时先修改并验证 agenticCLI,再用同步脚本更新 dashscope,单独提交 | `acli/agent.py` 等 | | 5.2 | dashscope 能力注册为内置 tools(images/tts/asr/embeddings/files 等) | `acli/tools/dashscope_*` | | 5.3 | `dashscope expert` 命令 + 文档;回归 acli 全量斜杠命令 | — | -| 5.4 | expert 问答 prompt 策略:识别 1.x/2.0 语境(`Generation.call`→1.x、`dashscope.Client`→2.0);新代码一律推荐 2.0 写法;存量 1.x 代码修 bug 时保持原风格、不强行迁移;新参数/新能力标注仅 2.0。做成可插拔片段,2.0 门面可用后(M1+)才注入,1.x↔2.0 对照表放可检索文档而非 prompt 内 | `acli/prompt.py` 片段 + 知识文档 | +| 5.4 | expert 问答 prompt 策略:识别 1.x/2.0 语境(`Generation.call`→1.x、`dashscope.Client`→2.0);新代码推荐 2.0,存量 1.x 修 bug 保持原风格,新参数/新能力标注仅 2.0。当前以 system prompt 的版本选择规则和按需加载的领域 skills 实现,覆盖 2.0.3 包根接口、扁平响应及 `dashscope.errors`;1.x↔2.0 对照留在可检索文档而非常驻 prompt | `agenticCLI-examples/dashscope-sdk-expert/.acli/system-prompt.md` + `.acli/skills/` | #### M6 — codegen From 302a65d7e7cbd3e13593a46512c62602e407c336 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 15:38:25 +0800 Subject: [PATCH 16/41] ci: limit unit tests to Python 3.11-3.13 --- .github/workflows/unit_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 63823931..56b691d9 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.12', '3.13', '3.14' ] + python-version: [ '3.11', '3.12', '3.13' ] steps: - name: Checkout repository @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.12', '3.13', '3.14' ] + python-version: [ '3.11', '3.12', '3.13' ] steps: - name: Checkout repository From 9bfbcf54b6f33af0c2ca7be23dcb7b812d109a86 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 16:08:37 +0800 Subject: [PATCH 17/41] docs: remove internal SDK documentation --- md/aco-memory-research.md | 210 -- md/agentic-rl.md | 215 -- md/api_entities_review2_evaluation.md | 80 - md/api_entities_review_evaluation.md | 39 - md/ark-vs-dashscope-v2-comparison.md | 75 - ...omparison-dashscope-vs-openai-vs-claude.md | 743 ------- md/cli-source-level-comparison-2026-08.md | 259 --- md/conclusion-sdk-architecture-stainless.md | 57 - md/dashscope-2.0-1x-dependency-decoupling.md | 146 -- md/dashscope-2.0-advantages-report.md | 569 ----- ...ope-2.0-agentstudio-agenticrl-migration.md | 140 -- md/dashscope-2.0-asr-sdk.md | 339 --- md/dashscope-2.0-audio-management-sdk.md | 157 -- md/dashscope-2.0-connection-reuse-sdk.md | 163 -- md/dashscope-2.0-decision-sdk.md | 288 --- md/dashscope-2.0-design.md | 604 ------ md/dashscope-2.0-design.v1.bak.md | 230 -- md/dashscope-2.0-errors-registry-plan.md | 882 -------- md/dashscope-2.0-interfaces-examples.md | 1862 ----------------- md/dashscope-2.0-interfaces-mapping.md | 159 -- md/dashscope-2.0-m1-m3-review.md | 182 -- md/dashscope-2.0-migration-guide.md | 179 -- md/dashscope-2.0-migration.md | 344 --- md/dashscope-2.0-naming-review.md | 661 ------ md/dashscope-2.0-realtime-sdk.md | 449 ---- md/dashscope-2.0-translation-sdk.md | 160 -- md/dashscope-2.0-tts-sdk.md | 373 ---- md/dashscope-2.0-vs-1.x-alignment-review.md | 452 ---- md/dashscope-2.0.3-python-sdk.md | 675 ------ md/dashscope-interfaces.md | 222 -- md/dashscope-sdk-expert-intro.md | 92 - md/dashscope-v2-refactor-tasks.md | 195 -- md/error.md | 356 ---- md/experiences.jsonl | 8 - md/m0-1x-cli-hotfix.md | 24 - md/research-openai-anthropic-references.md | 78 - md/sdk-comparison-anthropic-vs-dashscope.md | 307 --- md/sdk-comparison-openai-vs-dashscope.md | 206 -- ...p-comparison-openai-anthropic-dashscope.md | 1126 ---------- md/sdk-source-level-comparison-part2.md | 397 ---- md/self-diagnostic-exception.md | 75 - md/vendor-sdk-research-compat.md | 63 - md/zero-cost-local-sandbox.md | 76 - 43 files changed, 13917 deletions(-) delete mode 100644 md/aco-memory-research.md delete mode 100644 md/agentic-rl.md delete mode 100644 md/api_entities_review2_evaluation.md delete mode 100644 md/api_entities_review_evaluation.md delete mode 100644 md/ark-vs-dashscope-v2-comparison.md delete mode 100644 md/cli-comparison-dashscope-vs-openai-vs-claude.md delete mode 100644 md/cli-source-level-comparison-2026-08.md delete mode 100644 md/conclusion-sdk-architecture-stainless.md delete mode 100644 md/dashscope-2.0-1x-dependency-decoupling.md delete mode 100644 md/dashscope-2.0-advantages-report.md delete mode 100644 md/dashscope-2.0-agentstudio-agenticrl-migration.md delete mode 100644 md/dashscope-2.0-asr-sdk.md delete mode 100644 md/dashscope-2.0-audio-management-sdk.md delete mode 100644 md/dashscope-2.0-connection-reuse-sdk.md delete mode 100644 md/dashscope-2.0-decision-sdk.md delete mode 100644 md/dashscope-2.0-design.md delete mode 100644 md/dashscope-2.0-design.v1.bak.md delete mode 100644 md/dashscope-2.0-errors-registry-plan.md delete mode 100644 md/dashscope-2.0-interfaces-examples.md delete mode 100644 md/dashscope-2.0-interfaces-mapping.md delete mode 100644 md/dashscope-2.0-m1-m3-review.md delete mode 100644 md/dashscope-2.0-migration-guide.md delete mode 100644 md/dashscope-2.0-migration.md delete mode 100644 md/dashscope-2.0-naming-review.md delete mode 100644 md/dashscope-2.0-realtime-sdk.md delete mode 100644 md/dashscope-2.0-translation-sdk.md delete mode 100644 md/dashscope-2.0-tts-sdk.md delete mode 100644 md/dashscope-2.0-vs-1.x-alignment-review.md delete mode 100644 md/dashscope-2.0.3-python-sdk.md delete mode 100644 md/dashscope-interfaces.md delete mode 100644 md/dashscope-sdk-expert-intro.md delete mode 100644 md/dashscope-v2-refactor-tasks.md delete mode 100644 md/error.md delete mode 100644 md/experiences.jsonl delete mode 100644 md/m0-1x-cli-hotfix.md delete mode 100644 md/research-openai-anthropic-references.md delete mode 100644 md/sdk-comparison-anthropic-vs-dashscope.md delete mode 100644 md/sdk-comparison-openai-vs-dashscope.md delete mode 100644 md/sdk-deep-comparison-openai-anthropic-dashscope.md delete mode 100644 md/sdk-source-level-comparison-part2.md delete mode 100644 md/self-diagnostic-exception.md delete mode 100644 md/vendor-sdk-research-compat.md delete mode 100644 md/zero-cost-local-sandbox.md diff --git a/md/aco-memory-research.md b/md/aco-memory-research.md deleted file mode 100644 index bf1e36d7..00000000 --- a/md/aco-memory-research.md +++ /dev/null @@ -1,210 +0,0 @@ -# 蚁群算法(ACO)应用于智能体记忆检索 —— 定向调研 - -> ⚠️ 保密:含未申请专利的技术方案,申请递交前勿公开、勿提交任何 git 仓库。 -> 来源:deep-research 工作流(runId: wf_13fb4357-cbf,5 角度、22 来源、94 条 claim 抽取、25 条对抗验证**全部确认**、0 驳回)。ACO 教科书内容经 Dorigo & Stützle 原书 PDF(MIT Press, 321 页)逐字核对。 -> 证据强度标注:【教科书】= 经典定义性材料;【benchmark】= 公开基准实测;【宣称】= 论文/项目方自报;【代码核实】= 开源仓库逐行验证。 - -## 0. 一页结论 - -1. **直接先例存在(关键问题的直接回答)**:开源系统 [sentAInce](https://github.com/dcnconsult/sentAInce)(Apache-2.0,2026-07 创建、2026-09 活跃)公开实现了"agent 行为在持久记忆图上沉积信息素、落点分类后信息素引导游走取 top-k 上下文注入"。**存在性证据强(代码级核实),有效性证据为零**(单人项目、3 stars、无同行评审、自身对照实验未达显著性门槛)。 -2. **学术上显式信息素路线基本空白**:2026-01 版 GraphRAG 功能综述全文无 pheromone/ant colony/stigmergy(缺席证据);最接近的 agent 记忆加权工作 HAGE(2026-05)用 RL 而非信息素、全文无相关术语。 -3. **机制工具箱全部有成熟参照**:经典教科书给出转移/挥发/MMAS 公式与参数;落点定位与 τ/η 融合有 NAACL 2015 独立 benchmark 支撑(融合后 91.77%,单信号显著更差——信号互补"缺一不可"有实证)。 -4. **风险闭环完整**:马太效应有小规模实证观察,MMAS 界/挥发/聚合约束/精英强化缓解手段齐备;**对抗性污染防御在所有已核实来源中完全空白**(既是风险也是专利空位)。 -5. **专利含义**:宽泛框架(行为沉积信息素 + 落点 + 游走取上下文)已被 sentAInce 覆盖,独权必须收窄到具体区别特征组合;窗口在快速收窄(sentAInce 两个月前才创建),要申请必须快。详见第 7 节。 - -## 1. 目标场景与设计约束 - -智能体(终端编程助手为代表)在持久化记忆图谱上运行。给定一个 query:落点定位 → 信息素引导的邻域游走 → top-k 紧凑子图(~10^3 tokens)注入上下文。约束:检索代价与库规模解耦;图谱伴随会话自动构建、在线演化;冷启动可用;防高频路径垄断。 - -## 2. ACO 经典机制【教科书,经原书逐字核实】 - -来源:Dorigo & Stützle《Ant Colony Optimization》(MIT Press)原书 PDF + Scholarpedia。 - -### 2.1 核心公式 - -- **随机比例转移规则**(式 3.2):p_ij^k = τ_ij^α · η_ij^β / Σ_l τ_il^α · η_il^β。α=0 退化为随机贪婪;β=0 导致纯信息素自放大停滞——**α/β 都不可缺**。 -- **蒸发规则**(式 3.3):τ ← (1−ρ)·τ,0<ρ≤1。使未被选择的边指数衰减,防止无限累积、实现"遗忘坏决策"。 - -### 2.2 经典参数区间(原书 Box 3.1,TSP 无局部搜索) - -| 参数 | AS/EAS | ASrank | MMAS | ACS | -|---|---|---|---|---| -| α | 1 | 1 | 1 | 1 | -| β | 2~5 | 2~5 | 2~5 | 2~5 | -| ρ | 0.5 | 0.1 | **0.02** | 0.1 | -| m(蚂蚁数) | n | n | n | 10 | -| 其他 | EAS: e=n | w=6 | —— | q0=0.9, ξ=0.1 | - -### 2.3 MMAS(防早熟收敛的关键变体) - -- 每边信息素钳制在 [τmin, τmax];τmax = 1/(ρ·C^bs) 随最优解更新,τmin = τmax/a; -- **实验证明 τmin 比 τmax 更关键**(Stützle 1999)——τmin 保证任何边始终保留非零被探索概率,这正是防马太效应的理论机制; -- 接近停滞时触发**信息素重初始化**(重置到 τmax); -- 初始化约定:MMAS 将信息素**初始化到 τmax**(促进早期探索),与 AS 的低常数初始化相反——冷启动设计应采 MMAS 约定。 - -### 2.4 概念映射表(ACO → 记忆图谱) - -| ACO 概念 | 本方案对应 | -|---|---| -| 蚂蚁 | 一次 query 的上下文提取游走 | -| 节点 | 知识实体(代码符号/文档/知识点/事项) | -| 边信息素 τ | 行为边权:共现检索频次 × 采纳率 | -| 启发值 η | 静态结构边强 × 与当前 query 的语义相似度 | -| 路径质量 | 任务完成信号(资格迹/桶队式沿路径折扣回传 γ^Δ) | -| 挥发 ρ | 时间衰减(长期记忆取 MMAS 式低挥发 0.02–0.1) | -| MMAS [τmin,τmax] | 防老边垄断、保新边机会;停滞重初始化 | -| ACS q0=0.9 / ξ=0.1 | 游走利用/探索配比;经过即微量局部挥发 | - -## 3. 先例排查(关键问题:有无直接先例) - -### 3.1 直接先例:sentAInce【代码核实,存在性强证据】 - -[sentAInce](https://github.com/dcnconsult/sentAInce)(Apache-2.0, v0.1.12,2026-07 创建、2026-09 活跃,3 stars / 1 fork,单人项目): - -- **沉积**:仅当动作被验证成功(Bash exit 0)时沿路径在 verb-altitude 有向图上沉积 τ——"earned memory":读取不沉积、重复不沉积、失败不沉积;含衰减、剪枝下限、每类边上限、资格迹信用分配(γ^Δ,bucket-brigade 式); -- **检索**:UserPromptSubmit 将 prompt 分类到 goal-class(落点),splice(k=8) 从该类最强边源节点做**贪婪 widest-path 游走**(`cur = max(outs, key=lambda x: x[1])[0]`),top-k τ 边渲染为 additionalContext 注入; -- **与本案同构点**:落点 + 信息素引导邻域游走取上下文(投票 2-1,分歧仅在"落点"对应关系的解释强度,代码事实无争议); -- **重大限定**:无同行评审、无外部采用、自身对照 flock 实验未达显著性门槛——**"先例存在"证据强,"先例有效"证据为零**; -- **法律意义**:公开 GitHub 仓库构成现有技术,宽泛框架独权不可行(见第 7 节);Apache-2.0 许可使其代码可作为工程参照。 - -### 3.2 功能等价但机制不同:HAGE【benchmark 级论文,代码无关】 - -[HAGE(arXiv 2605.09942,2026-05)](https://arxiv.org/html/2605.09942v2):在 agent 记忆图谱上用**下游任务奖励的 RL(策略梯度 + 边锚定正则化)**联合优化路由行为与边表示——"由检索/路由行为的结果强化图边权供后续 query 使用"在功能层面已有先例。 -- 游走打分 S(n_j|n_i,q) = λ·cos(v_j,q) + (1−λ)·w_ij(q):语义相似 × 学习边权的融合,与 τ/η 融合在**抽象设计模式**层面同构,可直接参照; -- **两点不可照抄**:其 w_ij(q) 是 query-conditioned 动态表示(非 ACO 式持久标量);融合是学习系数线性加和(非 τ^α·η^β 乘性); -- **全文零信息素术语**——支持"显式 ACO 路线在学术文献空白"的判断。 - -### 3.3 ACO×LLM 先例但场景不同:ACO-ToT【宣称,存在性代码级】 - -[ACO-ToT(arXiv 2501.19278,2025-01)](https://arxiv.org/abs/2501.19278):经典转移规则与挥发迁移到 LLM 思维树推理;GSM8K/ARC/MATH 达 84.2%/88.9%/22.6%(对最弱基线 CoT 提升 28.6 点,**但对最强基线 IRPO 仅 +1.8~2.6 点**,论文自报、单模型、无复现)。**关键限定:信息素仅作用于单次推理任务内部的推理树,不跨 query 持久化**——经两次全文核查确认未应用于记忆/检索/KG 场景,不构成核心设想先例。 - -### 3.4 IR/推荐领域的历史先例【一手论文机制核实】 - -| 工作 | 机制 | 与本案差异 | -|---|---|---| -| **NaïveRank**(约 2014,Brunel 大学库) | (query,document) 对上沉积信息素:点击即 +1,排名完全由信息素决定;指数半衰期挥发 φW·(1/2)^((t−φT)/δ) 在每次强化前执行,明确用于对抗正反馈自强化 | 载体是点击日志的 query→doc 二部结构,非记忆图谱邻域游走;注意作者在自己的实验中**关闭了挥发**,防收敛是设计 rationale 而非实验结论 | -| Web 搜索个性化(Procedia CS 2021) | 每用户偏好画像,画像不足时借用他人画像(stigmergy 类比) | 载体为用户画像而非图边权;摘要无真实 ACO 算子(部分类比) | -| Ant Collaborative Filtering(Appl. Sci. 2020) | 信息素交换建模评分、挥发建模 concept drift | 推荐场景,非 agent 记忆 | - -### 3.5 主流文献的缺席证据与静态基线 - -2026-01 版 [GraphRAG 功能综述(arXiv 2504.10499v2)](https://arxiv.org/html/2504.10499v2) 768KB 全文 grep:pheromone/ant colony/stigmergy 零命中。主流范式恰为本案的静态对照组:query 实体对齐到 KG 节点后作为 **Personalized PageRank 种子做邻域扩散**(HippoRAG, NeurIPS 2024)——静态/扩散权重而非信息素权重。 -**限定**:缺席证据限于综述覆盖范围;**本轮未系统检索专利库**,工业内部实现未知。 - -## 4. 算法设计草案 - -### 4.1 设计依据的独立 benchmark 证据链 - -[Pershina/He/Grishman, NAACL 2015(N15-1026)](https://aclanthology.org/N15-1026.pdf):PPR 实体消歧在 AIDA 27,816 个提及上达 **91.77% micro**(超当时 SOTA)。消融证明**信号互补缺一不可**:单用静态启发值 62.61%、单用图游走一致性 85.56%、融合后 91.77%——为 τ(游走证据)× η(语义/静态启发)融合设计提供独立支撑。聚合约束 c1/c2(忽略同实体竞争候选、每实体只取最大贡献候选)将准确率从 90.26% 提至 91.77%,**机制是过滤错误候选注入游走的噪声**——直接对应抗污染设计。 -限定:其最终组合为加性 score 且 iSim 兼作 PPR 重启分布,对乘性 τ^α·η^β 的支撑在信号互补性层面而非公式同一。 - -### 4.2 在线更新(伴随会话自动执行) - -``` -on tool_call(query, hits, adopted, task_context): - for (a, b) in co_occurred(hits): # 共现沉积 - Δτ_ab += adopt_weight(adopted) * γ^depth(a, b) - -on task_done(success): # 精英强化:仅成功路径 - if success: - for edge, Δ in retrieval_path.edges_with_distance(): - τ_edge += Q * γ^Δ # 资格迹信用回传 - -on tick(Δt): # 或懒挥发:读取时按 Δt 折算 - τ ← clip(τ * (1−ρ)^(Δt/τ_unit), τmin, τmax) # MMAS 钳制 - if stagnation_detected(): τ ← τmax # 停滞重初始化(MMAS) -``` - -### 4.3 查询提取(落点 + 信息素引导游走) - -``` -extract(query, token_budget): - seeds = entity_link(query) # 落点(PPR 消歧式,见 4.1) - if not seeds: - ctx = fallback_grep_vector(query) # 回退通道 - write_back_candidate_nodes(ctx) # 新实体回写,图谱自动生长 - return ctx - frontier, ctx, cost = seeds, [], 0 - while cost < token_budget: - cand = edges(frontier) - cand = apply_aggregation_constraints(cand) # c1/c2 抗噪(4.1) - p_ij ∝ τ_ij^α · η_ij^β # τ=行为边权, η=语义×静态 - next = argmax(p) if rand() < q0 else sample(p) # ACS 利用/探索 - ctx.append(next); cost += tokens(next) - frontier = next - return ctx -``` - -### 4.4 参数初始值(教科书 Box 3.1 核实值 → 本场景外推) - -| 参数 | 教科书经典值 | 本场景建议 | 依据 | -|---|---|---|---| -| α | 1 | **1.0** | Box 3.1 | -| β | 2~5 | **2** | Box 3.1 下沿;长期行为信号应占主导 | -| ρ | AS 0.5 / ACS 0.1 / MMAS 0.02 | **0.02–0.1** | 长期在线记忆取 MMAS 式低挥发 | -| q0 | 0.9 (ACS) | **0.85–0.9** | ACS 经典 | -| ξ(局部挥发) | 0.1 (ACS) | 0.1(可选) | ACS 经典 | -| [τmin,τmax] | τmax=1/(ρ·C^bs),τmin=τmax/a | MMAS 钳制;**τmin 更关键**(保探索下限) | Stützle 1999 实验结论 | -| τ0 初始化 | AS 低常数 / **MMAS 初始化到 τmax** | **τ0=τmax** | MMAS 约定:高初始值促进早期探索,缓解冷启动 | -| 游走预算 | —— | ~10^3 tokens | HippoRAG2 PPR 紧凑子图实测量级(上轮调研) | - -## 5. 评测设计 - -- **必含基线**(有 benchmark 依据):A 纯向量 RAG;B 静态 Personalized PageRank 图(HippoRAG 式,主流范式对照);C 信息素加权(本案)。 -- **冷启动/覆盖率是受控变量**:KG-GraphRAG 因建图覆盖率仅 ~65%(HotPotQA 65.8%、NQ 65.5% 的答案实体入图)在 QA 上普遍逊于向量 RAG(HotPotQA F1:KG-GraphRAG-Triplets 25.02 vs RAG 60.04)——**这是稀疏/冷启动风险的 benchmark 级直接证据**,评测必须控制图覆盖率变量([arXiv 2502.11371v2 / OpenReview K6N6gCCYcb](https://arxiv.org/html/2502.11371v2),ACM 已发表)。 -- 指标:recall@k / MRR、token 成本、延迟、**收敛轮数**(行为加权需多少轮交互才显著优于静态基线 B)、抗噪恢复。 -- 消融(参照 ACO-ToT 对照设计范式):去挥发、去 MMAS 钳制、去精英回传、τ-only(β=0,应现自放大停滞)、η-only(α=0)。 -- 自建任务集(代码场景无现成 benchmark):跨文件符号定位 ×20、影响面分析 ×20、架构问答 ×20。 - -## 6. 风险与缓解(实证观察—缓解闭环) - -| 风险 | 实证证据 | 缓解(均有支撑) | -|---|---|---| -| 正反馈失控/马太效应 | NaïveRank 8 用户 ×6 任务实验:目标页升至首位后被更多点击、信息素持续增加,可能收敛次优(作者自述初步结果、hedged,且其自评在该静态领域 negligible)【宣称】 | MMAS [τmin,τmax](τmin 保探索下限,实验证明比 τmax 更关键)+ 停滞重初始化【教科书】;指数半衰期挥发【宣称】 | -| 冷启动/覆盖稀疏 | KG 覆盖率 ~65% 致检索失效(HotPotQA F1 25.02 vs 60.04)【benchmark】 | 静态底图 + τ0=τmax 高初始探索 + 回退通道与回写生长 | -| 错误候选噪声 | 聚合约束 c1/c2 使准确率 90.26→91.77%【benchmark】 | 游走前过滤低置信节点贡献 | -| 规模扩展 | 无已发表方案 | 懒挥发(读取时折算)+ 局部子图更新 + SQLite 单文件 | -| **对抗性污染** | **所有已核实来源中完全无针对性防御研究(空白)** | 本案自拟:精英强化仅限任务成功路径;采纳信号校验;τmin 保底使污染边可随挥发恢复——**空白即专利空位(见第 7 节)** | - -## 7. 专利新颖性含义(修正版,重要) - -**宽泛框架已被覆盖**:sentAInce 公开了"agent 行为成功沉积信息素于持久记忆结构 + 落点分类 + 信息素引导贪婪游走取 top-k 上下文注入"——"行为信息素记忆"作为大概念的独权不再可行。 - -**仍可主张的区别特征组合**(独权须收窄至此层级): - -| 区别维度 | sentAInce | 本案 | -|---|---|---| -| 转移规则 | 贪婪 widest-path(max τ) | **随机比例规则 τ^α·η^β + ACS q0 利用/探索**(教科书规则,含探索防停滞) | -| 抗早熟约束 | 每类边上限 + 剪枝下限 | **MMAS [τmin,τmax] + 停滞重初始化**(有理论依据的界) | -| 落点粒度 | goal-class 粗分类 | **实体链接到具体节点**(PPR 消歧式,91.77% benchmark) | -| 沉积信号 | 仅验证成功(exit 0) | **共现频次 × 采纳率 × 成功奖励多信号**;失败/读取的差异化处理可再设计 | -| 图内容 | verb-altitude 动作图("什么动作有效") | **知识实体图**(代码符号/文档/知识点/事项)+ 静态/行为双层边 | -| 上下文构造 | 固定 k=8 splice | **token 预算约束的紧凑子图**(~10^3 tokens,PPR 式) | -| 失效管理 | 衰减 + 剪枝 | **双时序失效(保留历史)+ 变更级联失效传播** | -| 场景 | 编程 CLI 单场景 | 多场景泛化(编程/办公/学习/生活) | - -**附加空位**:对抗性污染防御在所有已核实来源(含 sentAInce)中均为空白,可作分案储备。 - -**两点诚实警示**: -1. **有效性证据缺失是全行业现状**(唯一直接先例自身实验未达显著性)——专利不要求实证优越性,但产品投入前必须用第 5 节的评测自行验证"信息素加权 > 静态 PPR"; -2. **窗口在收窄**:sentAInce 两个月前创建、HAGE 四个月前发表,该领域演进以月计。且本轮未系统检索专利库——正式递交前的 CNIPA/incoPat 查新不可省。 - -## 8. 开放问题 - -1. 信息素在线沉积(sentAInce 式)与 RL 离线训练(HAGE 式)在同一记忆图谱上的头对头对比完全缺失——无 recall@k、成本、收敛速度、抗噪的任何实验证据; -2. 专利与工业界未公开实现未知(本轮未系统检索专利库); -3. 理论性质未明:稳态下 τ 加权邻域游走与 Personalized PageRank 的数学关系(是否等价于某种自适应加权 PPR)、收敛条件,均无已发表分析——**这本身是可发表的研究点**; -4. 百万级边规模的挥发调度成本(全边周期蒸发 vs 访问时惰性衰减)无已发表工程方案。 - -## 9. 来源清单 - -- Dorigo & Stützle, *Ant Colony Optimization*, MIT Press([原书 PDF](https://web2.qatar.cmu.edu/~gdicaro/15382/additional/aco-book.pdf));[Scholarpedia: ACO](http://www.scholarpedia.org/article/Ant_colony_optimization) -- [sentAInce(直接先例,代码核实)](https://github.com/dcnconsult/sentAInce) -- [HAGE, arXiv 2605.09942](https://arxiv.org/html/2605.09942v2) -- [ACO-ToT, arXiv 2501.19278](https://arxiv.org/abs/2501.19278) -- [NaïveRank(Brunel 大学库全文)](https://bura.brunel.ac.uk/bitstream/2438/13248/1/Fulltext.pdf) -- [Web 搜索个性化(Procedia CS 2021)](https://www.sciencedirect.com/science/article/pii/S1877050921011686/pdf?md5=9980107be61f5ba6365ba6cb56a0b274&pid=1-s2.0-S1877050921011686-main.pdf);[Ant Collaborative Filtering (Appl. Sci. 2020)](https://www.mdpi.com/2076-3417/10/20/7245) -- [GraphRAG 功能综述 2026-01, arXiv 2504.10499v2](https://arxiv.org/html/2504.10499v2);[HippoRAG, arXiv 2405.14831](https://arxiv.org/abs/2405.14831) -- [Pershina et al., NAACL 2015 (N15-1026)](https://aclanthology.org/N15-1026.pdf) -- [GraphRAG 系统评测(OpenReview K6N6gCCYcb / arXiv 2502.11371v2)](https://arxiv.org/html/2502.11371v2) -- [Bucket Brigade 机制(IDSIA 存档)](https://people.idsia.ch/~juergen/bucketbrigade/node2.html) diff --git a/md/agentic-rl.md b/md/agentic-rl.md deleted file mode 100644 index cdaad39f..00000000 --- a/md/agentic-rl.md +++ /dev/null @@ -1,215 +0,0 @@ -# dashscope/finetune/agentic_rl.py 错误码与错误注册表对齐评估 - -## 评估目标 - -审查 `dashscope/finetune/agentic_rl.py` 对外 SDK 接口中抛出的错误(error/message)是否与 `/Users/zhansheng.lzs/ali/pro/ptm/dashscope-error-registry/python/dashscope_error_registry/error_registry.py` 中定义的错误体系对齐。 - -参考:`error.md` 中对 SDK 错误透传原则的审查结论。 - ---- - -## 错误注册表结构 - -`error_registry.py` 定义了两类错误: - -- `PublicError`: 包含 `key`, `status_code`, `error_code`(字符串类名), `error_msg`, `anthropic_error_code` -- `InternalErrorDef`: 包含 `name`, `external`(对应 PublicError), `message`, `allow_retry`, `cause`, `solution` - -核心 `PublicError` 包括: - -| key | status_code | error_code | error_msg | -|---|---|---|---| -| invalid_request | 400 | BadRequestError | The request is invalid. Please check the request and try again. | -| missing_parameter | 400 | BadRequestError | Missing required parameter: {parameter}. | -| auth_failed | 401 | AuthenticationError | Authentication failed. Please provide valid authentication credentials. | -| invalid_api_key | 401 | AuthenticationError | Incorrect API key provided. | -| permission_denied | 403 | PermissionDeniedError | You do not have permission to access this resource. | -| resource_not_found | 404 | NotFoundError | The requested resource was not found: {resource}. | -| request_too_large | 413 | RequestTooLargeError | The request exceeds the maximum allowed size of {limit}. | -| rate_limit_exceeded | 429 | RateLimitError | Rate limit exceeded. Please slow down your requests. | -| internal_error | 500 | InternalServerError | The server encountered an internal error. Please try again later. | -| service_unavailable | 503 | ServiceUnavailableError | The service is temporarily unavailable. Please try again later. | -| request_timeout | 504 | GatewayTimeoutError | The request timed out. Please try again later. | - ---- - -## agentic_rl.py 当前错误处理清单 - -| 位置 | 错误码 | 当前 message | 错误类型 | 触发场景 | -|---|---|---|---|---| -| `__init__:61-64` | 3001 | Invalid API key configuration | `ValueErrorWithCode` | API key 配置无效 | -| `register_functions:111-114` | 3002 | Function registration failed | `RegistrationError` | 函数注册失败 | -| `upload_datasets:144-147` | 3003 | Datasets upload failed | `DatasetsError` | 数据集上传失败 | -| `submit_job:203-207` | 3004 | Duplicate function names detected... | `ValueErrorWithCode` | 函数名重复(本地校验) | -| `submit_job:256-259` | 3005 | Job submission failed | `RuntimeErrorWithCode` | 任务提交失败 | -| `run:321-324` | 3006 | RL tuning workflow failed | `RuntimeErrorWithCode` | 工作流执行失败 | -| `test_functions:440-443` | 3007 | Unsupported function type: {functype} | `ValueErrorWithCode` | 函数类型不支持(本地校验) | -| `test_functions:459-462` | 3008 | Function test failed | `ValidationError` | 函数测试失败 | - ---- - -## 对齐评估 - -### 1. 错误码格式未对齐(HIGH) - -`error_registry.py` 使用**字符串类名**作为 `error_code`,例如 `BadRequestError`、`AuthenticationError`、`InternalServerError`。 - -`agentic_rl.py` 使用**自定义整数错误码** `3001-3008`,与注册表的字符串体系完全不兼容。 - -**结论:未对齐。** - -### 2. 缺少注册表要求的标准字段(HIGH) - -`agentic_rl.py` 的异常类(`ValueErrorWithCode`、`RuntimeErrorWithCode`、`RegistrationError`、`DatasetsError`、`ValidationError`)仅包含: - -- `message` -- `error_code`(整数) -- `__cause__` - -缺少 `error_registry.py` 中 `PublicError` 定义的字段: - -- `status_code` -- `key` -- `anthropic_error_code` - -**结论:未对齐。** - -### 3. 输入校验类错误未映射到 INVALID_REQUEST(HIGH) - -以下场景属于**客户端输入校验失败**,应映射到注册表的 `INVALID_REQUEST` / `BadRequestError`: - -| 当前错误码 | 场景 | 建议映射 | -|---|---|---| -| 3004 | 函数名重复 | `INVALID_REQUEST` / `BadRequestError` | -| 3007 | 不支持的函数类型 | `INVALID_REQUEST` / `BadRequestError` | -| 3008 | 函数测试输入校验失败 | `INVALID_REQUEST` / `BadRequestError` | - -当前使用 3004/3007/3008 等自定义整数码,用户无法通过统一错误码识别这是请求参数问题。 - -**结论:未对齐。** - -### 4. API key 错误未映射到 INVALID_API_KEY/AuthenticationError(HIGH) - -`__init__:61-64` 在 API key 配置无效时抛出 `error_code=3001`、message 为 `"Invalid API key configuration"`。 - -注册表中对应错误为 `INVALID_API_KEY`: - -- `status_code=401` -- `error_code="AuthenticationError"` -- `error_msg="Incorrect API key provided."` - -当前实现未使用标准 `AuthenticationError` 错误码,也未携带 `401` 状态码。 - -**结论:未对齐。** - -### 5. 服务端/网络类错误未映射到标准服务端错误码(MODERATE) - -以下场景属于**服务端调用或底层依赖失败**,应根据实际失败原因映射到 `INTERNAL_ERROR`、`SERVICE_UNAVAILABLE`、`REQUEST_TIMEOUT` 等: - -| 当前错误码 | 场景 | 建议映射 | -|---|---|---| -| 3002 | 函数注册失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | -| 3003 | 数据集上传失败 | 透传底层 OSS/API 错误,或 fallback 到 `INTERNAL_ERROR` / `INVALID_FILE` | -| 3005 | 任务提交失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | -| 3006 | RL 调优工作流失败 | 透传底层 API 错误,或 fallback 到 `INTERNAL_ERROR` | - -当前实现使用固定整数码和硬编码 message,无法区分服务端内部错误、服务不可用、超时等情形。 - -**部分缓解:** 这些异常在捕获前会检查 `hasattr(e, "error_code")`,如果底层异常已有 `error_code` 则直接抛出。但: - -- 该机制依赖底层异常是否携带 `error_code`,未明确与注册表字段兼容; -- 新抛出的 `RuntimeErrorWithCode` / `RegistrationError` / `DatasetsError` 仍然使用整数码。 - -**结论:部分对齐,但包装层未对齐。** - -### 6. 错误消息未使用注册表标准模板(MODERATE) - -注册表为每类错误提供了标准化 `error_msg`(部分支持变量替换,如 `{parameter}`、`{resource}`、`{limit}`)。 - -`agentic_rl.py` 的消息均为硬编码英文,例如: - -- `"Invalid API key configuration"` -- `"Duplicate function names detected. All function names must be unique."` -- `"Unsupported function type: {functype}"` - -这些消息语义正确,但未与注册表统一,导致: - -- 多语言/多 SDK 难以保持一致; -- 无法通过注册表的 `key` 定位错误定义和解决方案。 - -**结论:未对齐。** - -### 7. 异常类继承体系与注册表结构不匹配(LOW-MODERATE) - -`agentic_rl.py` 的异常定义在 `dashscope/finetune/reinforcement/common/errors.py` 中,形成了独立的层级: - -- `AgenticRLError`(基类,整数 `error_code`) - - `DeploymentError` → `RegistrationError` / `DatasetsError` - - `ValidationError` - - `InputError` - - `OutputError` - - 等 - -该层级未继承或引用注册表中的 `PublicError` / `InternalErrorDef`,无法直接用于与注册表一致的错误序列化。 - -**结论:未对齐。** - ---- - -## 与 error.md 原则的对比 - -`error.md` 的核心结论是: - -> SDK 应原样透传 HTTP API 返回的 `code` 和 `message`,不得修改。 - -`agentic_rl.py` 的问题在于: - -- 对于**本地 SDK 输入校验错误**(如 3004、3007),它没有使用 API 侧统一约定的 `BadRequestError` 等错误码; -- 对于**服务端调用失败**(如 3002、3003、3005、3006),虽然保留了带 `error_code` 的底层异常,但新包装的错误码和消息是 SDK 自定义的,覆盖了 API 原本可能返回的结构化错误。 - -因此,`agentic_rl.py` 的错误处理方式与 `error.md` 主张的"透传 API 错误"原则存在冲突。 - ---- - -## 修复建议 - -### 短期(保持兼容性前提下对齐) - -1. **将整数错误码映射为注册表字符串错误码**: - - 3001 → `AuthenticationError` - - 3004 / 3007 / 3008 → `BadRequestError` - - 3002 / 3003 / 3005 / 3006 → 透传底层错误,或 fallback 到 `InternalServerError` - -2. **在异常中补充 `status_code` 和 `key` 字段**: - - 使异常对象能够被统一错误处理中间件识别。 - -3. **使用注册表的标准 message 模板**: - - 例如 API key 错误使用 `"Incorrect API key provided."`; - - 输入校验错误使用 `"The request is invalid. Please check the request and try again."` 或 `"Missing required parameter: {parameter}."`。 - -### 长期(推荐) - -1. **直接复用 `dashscope_error_registry` 中的 `PublicError` / `InternalErrorDef`**: - - 本地输入校验错误直接抛出基于 `INVALID_REQUEST` 的异常; - - 认证失败直接抛出基于 `INVALID_API_KEY` 的异常; - - 服务端错误透传 API 返回的 `code`/`message`,不重新包装。 - -2. **移除 `3001-3008` 自定义整数码**: - - 这些码与注册表不兼容,会造成多 SDK / 多语言不一致。 - -3. **统一异常基类**: - - 让 AgenticRL 相关异常继承自一个与注册表结构兼容的基类,支持 `status_code`, `error_code`, `message`, `anthropic_error_code` 等字段。 - ---- - -## 结论 - -`dashscope/finetune/agentic_rl.py` 对外 SDK 接口的错误处理**未与 `dashscope_error_registry/error_registry.py` 对齐**。主要问题包括: - -- 使用自定义整数错误码(3001-3008),而非注册表要求的字符串错误码; -- 缺少 `status_code`、`key`、`anthropic_error_code` 等标准字段; -- 输入校验、认证、服务端错误未映射到注册表中的标准错误类型; -- 错误消息为硬编码,未使用注册表标准模板; -- 与 `error.md` 主张的"原样透传 API 错误"原则存在冲突。 - -建议按上述修复建议逐步改造,优先处理错误码格式和输入校验类错误的映射。 diff --git a/md/api_entities_review2_evaluation.md b/md/api_entities_review2_evaluation.md deleted file mode 100644 index 3609341a..00000000 --- a/md/api_entities_review2_evaluation.md +++ /dev/null @@ -1,80 +0,0 @@ -# dashscope/api_entities 第二份代码 Review 评估报告 - -> 对第二份 review 报告(安全/资源/性能视角)中各条风险建议的逐条核对结果(已对照源码验证)。 -> 日期:2026-07-27 -> 关联文档:`api_entities_review_evaluation.md`(第一份 review 的评估) - -## 总体结论 - -该报告质量高于第一份,约 2/3 条目属实可采纳,但存在 **4 处关键错误**,其中 #12(heartbeat)方向完全相反,照改会引入更严重的问题。 - ---- - -## 🔴 高风险部分评估 - -| 条目 | 评估 | 说明 | -|------|------|------| -| 1a. RSA PKCS1v15 → OAEP | ⚠️ 属实但**不可单侧修** | PKCS1v15 确是弱填充(Bleichenbacher 攻击面),但这是客户端-服务端加密协议:客户端单方面改 OAEP,服务端将无法解密 AES key,直接功能故障。需服务端协同 + 协议版本协商,不能按其建议直接改,P0 定级不当 | -| 1b. `encryption.py:94` 无 timeout | ✅ 属实 | 与第一份 review 一致,P0 同意。建议复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS` | -| 1c. AES key 内存清零 | ❌ 基本无效 | Python `bytes` 不可变,`self.aes_key_bytes = b""` 只是丢引用,原始字节仍留在内存等待 GC,无法真正清零。且 key 在整个请求生命周期都要用于 encrypt/decrypt,不能提前清。安全收益趋近于零 | -| 1d. 公钥缓存 + TTL | ✅ 合理优化 | 注意只能缓存公钥本身,AES key/IV 必须每次请求新生成 | -| 2a. `aio_session.py:66` get_event_loop 弃用 | ✅ 属实 | Python 3.10+ 发 DeprecationWarning,3.12+ 更严格 | -| 2b. finalize 按 id 查找竞态 | ✅ 属实 | 实际比"竞态"更糟:session 在 `WeakKeyDictionary` 值中持有强引用时 finalize 根本不会触发,该回调近乎死代码(与第一份评估结论一致) | -| 2c. `threading.RLock` → `asyncio.Lock` | ❌ **建议错误,不能采纳** | 锁保护的是跨线程、跨 event loop 的全局 dict;临界区内无 await、执行极短,不会实质阻塞事件循环。`asyncio.Lock` 绑定单个 loop,跨 loop 使用反而是 bug | -| 3a. aio 路径 `should_close` 恒 False | ✅ 属实 | `http_request.py:167-172` 两分支均 False,`:228-229` 的 close 是死代码。但语义正确(共享 session 不该关闭),属代码冗余而非泄漏 | -| 3b. 同步请求无连接复用 | ✅ 属实,真实性能问题 | `http_request.py:469` 每次新建 `requests.Session`,每请求承担 TCP+TLS 握手。可参照 aio_session 做模块级共享(urllib3 连接池线程安全)。注意保留用户传入外部 session 的路径 | -| 4a. `websocket_request.py:179` create_task 无引用 | ✅ 属实 | duplex 模式后台发送任务异常被静默吞掉,保存引用并 await 的建议合理 | -| 4b. `close()` 引用未赋值的 `self.ws` | ✅ 属实,但应删非修 | 全文件无 `self.ws =` 赋值,调用必抛 AttributeError;但**全库无任何调用方**,是死方法,直接删除比修复更合适 | - -## 🟡 中风险部分评估 - -| 条目 | 评估 | 说明 | -|------|------|------| -| 5. 类型注解错误 3 处 | ✅ 全部属实 | `to_request_object`/`_only_parameters` 标注 `-> str` 实返 dict、`get_batch_binary_data` 空迭代返 None。代码中的 `# type: ignore[return-value]` / `[return]` 即佐证。可修 | -| 6. URL replace 脆弱 | ✅ 属实,方案更优 | `replace("/api/", "/api-task/", 1)` 比第一份 review 的修复更简洁合理。但重申:`query=True` 在 SDK 内**无调用方**,近似死代码,优先级应降 | -| 7. `dashscope_response.py:167` choices[0] | ✅ 属实 | `choices` 为空列表时抛 IndexError,加空检查合理 | -| 8. `logger.debug(e); raise e` 模式 | ⚠️ 结论对、论据错 | 存在 3 处(`http_request.py:231/520`、`aiohttp_request.py:321`)。改 `logger.exception` + 裸 `raise` 的建议成立(debug(e) 确实丢堆栈)。但"`raise e` 会重置 traceback"**说法错误**——Python 3 中 `raise e` 保留 `e.__traceback__`,与裸 `raise` 差异极小 | - -## 🟢 优化部分评估 - -| 条目 | 评估 | 说明 | -|------|------|------| -| 9. 响应解析代码重复 | ✅ 属实,谨慎推进 | 重构涉及流式/加密/multipart 多路径,回归风险高,建议测试覆盖充分后再做 | -| 10. DictMixin 透传方法 | ✅ 可删但收益微小 | 低优先级,删除需回归验证 dataclass + slots 行为 | -| 11. 死代码 | ✅ 属实 | `api_request_data.py:96-103` 注释块位于 `return` 之后(unreachable);`_to_json_only_data` 全库无调用。均可删 | -| 12. heartbeat=6000 "过于频繁" | ❌ **单位搞反,方向完全错误** | aiohttp `ws_connect(heartbeat=)` 单位是**秒**(`Optional[float]`),6000 = 100 分钟,绝非"6 秒过于频繁"。按其建议改 30000 会变成 8.3 小时。真正的问题恰恰相反:6000 疑似把毫秒当秒写错,若原意 6 秒应写 `heartbeat=6`,30 秒应写 `heartbeat=30` | -| 13. `headers.update()` 代替新建 dict | ✅ 成立 | 微优化,无关紧要 | - ---- - -## 采纳清单 - -### ✅ 可直接采纳 -| 条目 | 修复 | -|------|------| -| 1b | encryption 请求加 timeout(P0) | -| 1d | 公钥类级缓存 + TTL | -| 2a/2b | aio_session:改用 `asyncio.get_event_loop` 的安全替代 + 移除无效 finalize 回调 | -| 3a | 移除 aio 路径 should_close 死代码 | -| 3b | 同步请求共享 `requests.Session`(保留外部 session 路径) | -| 4a | duplex 后台 task 保存引用并 await | -| 5 | 修正 3 处类型注解 | -| 7 | choices 空列表检查 | -| 11 | 删除 unreachable 注释块与 `_to_json_only_data` | -| 13 | `add_headers` 改 `update` | - -### ⚠️ 属实但处理方式应调整 -| 条目 | 调整 | -|------|------| -| 4b | `close()` 直接删除(死方法),而非修复 self.ws | -| 6 | 改用 `replace("/api/", "/api-task/", 1)`,但先确认 query 分支是否有外部使用者 | -| 8 | 改 `logger.exception` + 裸 `raise`(论据修正:raise e 不丢 traceback) | -| 9/10 | 长期重构项,需测试覆盖先行 | - -### ❌ 不能采纳(照改会引入新问题) -| 条目 | 原因 | -|------|------| -| 1a | OAEP 需服务端协同,单侧改直接功能故障 | -| 1c | Python 不可变 bytes 无法真正清零内存 | -| 2c | asyncio.Lock 不能跨 loop,RLock 是正确选择 | -| **12** | **heartbeat 单位是秒不是毫秒,按建议改 30000 = 8.3 小时;若要调整应写 heartbeat=6 或 30** | diff --git a/md/api_entities_review_evaluation.md b/md/api_entities_review_evaluation.md deleted file mode 100644 index 1c4d50d2..00000000 --- a/md/api_entities_review_evaluation.md +++ /dev/null @@ -1,39 +0,0 @@ -# dashscope/api_entities 代码 Review 评估报告 - -> 对原始 review 报告中 11 条风险建议的逐条核对结果(已对照源码验证)。 -> 日期:2026-07-27 - -## 逐条评估 - -| # | 原严重度 | 文件/位置 | 问题 | 评估结论 | 说明 | -|---|----------|-----------|------|----------|------| -| 1 | 🔴 严重 | `encryption.py:94` | HTTP 请求无 timeout | ✅ 属实,P0 同意 | `Encryption.initialize()` 在构建请求时同步调用(`api_request_factory.py:126`),无 timeout 会阻塞调用线程。建议复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS` 常量而非硬编码 30 | -| 2 | 🟡 中等 | `http_request.py:126-127` | query 模式 `replace("api","api-task")` | ⚠️ 属实但应降级 | 全 SDK 内**没有任何调用方传 `query=True`**(仅 factory 从 kwargs 透传),近似死代码,只有用户显式传入才触发。默认 URL 只含一个 "api",误替换需自定义 base_address 含 "api"(如 `api.example.com`)。建议降为低优先级;原 review 给出的修复片段过于复杂 | -| 3 | 🟡 中等 | `aiohttp_request.py:80-82` | 同上 | ⚠️ 同 #2 | 同上 | -| 4 | 🟡 中等 | `websocket_request.py:75` | Authorization 用小写 "bearer" | ✅ 属实 | RFC 7235 中 auth scheme 大小写不敏感,服务端已兼容多年,属一致性问题。改为 "Bearer" 安全,可修 | -| 5 | 🟡 中等 | `dashscope_response.py:43` | `DictMixin.pop` 的 default 无默认值 | ✅ 属实,但**原建议修复有误** | 若按 review 建议改为 `default=None`,`pop(missing_key)` 会静默返回 None,而 `dict.pop` 无 default 时应抛 KeyError,语义不符。正确做法用 sentinel:`_MISSING = object()`,default 缺省时走 `super().pop(key)` | -| 6 | 🟡 中等 | `api_request_factory.py:107-114` | 手动拼接 URL 不健壮 | ❌ 不成立,不建议改 | 拼接输入全部是 SDK 内部受控常量(task_group/task/function),现状简单可靠。`urljoin` 对无尾斜杠路径会**吞掉末段路径**,改用它反而风险更高 | -| 7 | 🟡 中等 | `aio_session.py:59-86` | GC 回调清理逻辑复杂 | ✅ 属实 | `weakref.finalize` 触发时 session 已不可达,回调内遍历 `_aio_sessions.values()` 按 `id()` 匹配基本不可能命中,该回调实际近乎无效。同意"待优化"(如 atexit 或显式 shutdown API),不阻塞发布 | -| 8 | 🟡 中等 | `http_request.py:438-441` | task_id 被 output 覆盖 | ✅ 属实但是预期行为 | 响应同时含两字段时 `output` 优先是正确语义;改为 if/elif 仅提升可读性。建议降为低优先级 | -| 9 | 🟡 中等 | `api_request_data.py:167-168` | `_only_parameters` pop 修改实例状态 | ✅ 属实,但**原建议修复有 bug** | review 建议只 `get` 不删,会导致 `raw_input` 同时残留在 `parameters` 中被序列化进请求体,改变请求行为。正确修复:`params = dict(self.parameters)` 复制后再 pop。目前该方法仅在 `get_http_payload`(第 129 行)调用一次,仅对象复用/重试场景受影响 | -| 10 | 🟢 低 | `aiohttp_request.py` | `_handle_response` 未传 headers | ✅ 属实 | 已确认该文件所有 `DashScopeAPIResponse(...)` 构造均未传 `headers`,与 `http_request.py` 不一致,建议补齐 | -| 11 | 🟢 低 | `http_request.py:92-96` | headers 合并顺序可能被用户覆盖 | ❌ 不成立 | `**self.headers` 展开的是 `base_request.py:19` 中 SDK 自己设置的 `user-agent` / `X-DashScope-DataInspection`,**不是用户自定义 headers**;用户 headers 通过 `add_headers()` 后置合并覆盖,行为正确,无需修改 | - -## 总体结论 - -原报告 11 条中 **8 条属实、2 条不成立(#6、#11)、2 条属实但重要性应下调(#2/#3、#8)**。另有三点必须注意: - -1. **#6、#11 不成立**,建议不做改动。 -2. **#5、#9 的"建议修复代码"本身会引入新 bug**(改变 dict 语义 / 改变请求体),采纳前必须按上表说明改写。 -3. **#2/#3 的 query 分支在 SDK 内无调用方**,修复前建议先确认是否存在外部使用者,否则可考虑降级或标记为死代码。 - -## 修正后的建议修复顺序 - -| 优先级 | 事项 | -|--------|------| -| P0 | #1 `encryption.py` 添加 timeout(复用 `DEFAULT_REQUEST_TIMEOUT_SECONDS`) | -| P1 | #4 WebSocket "Bearer" 大写;#10 aiohttp 响应补 headers | -| P2 | #5 `pop` 用 sentinel 修复;#9 `_only_parameters` 复制字典后再 pop | -| P3 | #7 aio_session 清理机制优化;#8 if/elif 可读性调整 | -| 观察 | #2/#3 确认 query 分支是否有外部使用者后再决定 | -| 不修 | #6 URL 拼接维持现状;#11 headers 合并顺序行为正确 | diff --git a/md/ark-vs-dashscope-v2-comparison.md b/md/ark-vs-dashscope-v2-comparison.md deleted file mode 100644 index 763ebfab..00000000 --- a/md/ark-vs-dashscope-v2-comparison.md +++ /dev/null @@ -1,75 +0,0 @@ -# Ark Runtime Python SDK vs DashScope v2 SDK 架构对比 - -> 对比对象: -> -> - **ark**:`/Users/zhansheng.lzs/ali/pro/ptm/ark-runtime-python/src/arkruntime`(Stainless 从 OpenAPI spec 生成 + 手写扩展) -> - **dashscope v2**:`/Users/zhansheng.lzs/ali/pro/ptm/moe-dashscope-sdk-python/dashscope/v2`(全手写,54 个文件) - -## 定位差异(先说清楚,避免错配对比) - -| | ark-runtime-python | dashscope/v2 | -|---|---|---| -| 生产方式 | Stainless 从 OpenAPI spec 生成 + 手写扩展 | 全手写(54 个文件) | -| 协议形态 | 仅 OpenAI 兼容 HTTP/SSE | native/openai/anthropic 三协议 + SSE + WebSocket + Job 轮询 | -| 类型覆盖 | 请求侧 TypedDict params + 响应侧 pydantic model,全覆盖 | 只有响应侧有模型,请求侧是 20+ 个 `Optional` 长参数列表 | -| 同步/异步 | 代码生成保证两端一致 | 人肉复制,约一半代码是 async 孪生 | - ---- - -## Ark 的优点 - -1. **容错式反序列化哲学**(最值得借鉴):`_models.py` 的 `construct_type` 不做严格校验,只做宽松强转——union 逐个尝试、枚举/datetime 容错解析、不匹配就原样返回。配合 `extra="allow"`,服务端新增字段、新增枚举值、返回结构小漂移都不会炸用户进程。校验失败时另有 `ArkAPIResponseValidationError` 单独成类(`_base_client.py:486`),把"服务端坏了"和"SDK 解析坏了"区分开。 -2. **统一的请求定制通道**:每个资源方法一致暴露 `extra_headers/extra_query/extra_body/timeout/max_retries`(`_request_options.py:44`),`extra_body` 与 JSON body 深合并(`_base_client.py:236-243`)。用户总有逃生舱。 -3. **分页抽象**:`SyncCursorPage/AsyncCursorPage`(`pagination.py`),`for item in page` 直接迭代,cursor 从末条 `item.id` 推导。 -4. **响应封装**:`ArkAPIResponse.parse(to=...)` 按 cast 类型记忆化解析结果(`_response.py:305-321`),headers/request_id/elapsed 统一暴露;`with_raw_response` 风格切换。 -5. **重试可由服务端控制**:尊重非标准头 `x-should-retry` 和 `retry-after-ms`(`_base_client.py:383-417`),网关可以精细指挥客户端退避。 -6. **每请求 `X-Client-Request-Id` UUID**(`_base_client.py:195`),比进程级 session-id 更利于单请求排障。 -7. **生成式的本质红利**:API 覆盖全、新接口随 spec 跟进、sync/async 行为天然一致。 - -## Ark 的缺点 - -1. sync/async 在 `_base_client.py` 里逐行复制约 300 行(`_request` L527 vs L1011)——代码生成也没解决这个问题。 -2. 历史包袱重:`_response.py`/`_legacy_response.py` 两套响应封装并存 1000+ 行;pydantic v1 兼容路径已是死代码(pyproject 要求 v2)仍占 `_models.py` 约 1/3。 -3. 死字段:`idempotency_key` 定义了但从不消费(`_request_options.py:66`)——POST 重试同样无幂等保护。 -4. 手写扩展(managed agents)绕开生成管线,另起 `dump_body` 序列化器(`_managed_agents_serialize.py:55`),一个库两套序列化约定。 -5. **无类型检查门禁**:只有 Ruff,没有 mypy/pyright CI,类型正确性靠自觉。 -6. 能力面窄:无 WebSocket、无异步任务轮询框架、无文件直传预处理。 -7. `volcengine-python-sdk` 是顶层强依赖,为 STS/E2E 加密拖了重依赖。 - -## Dashscope v2 的优点 - -1. **protocols 适配层**(ark 完全没有的架构亮点):`ChatProtocol` ABC + frozen `ChatRequest`(`protocols/base.py:38-127`),canonical 消息模型 ↔ 三种 wire 格式双向翻译;`make_chunk_parser()` 支持有状态流解析(anthropic tool_use 累积)。ark 只有 OpenAI 一种协议形态。 -2. **Job 异步任务基础设施**(`_jobs.py`):`X-DashScope-Async` 提交 → 轮询,指数退避 1s→8s,`UNKNOWN` 视为终态防死等,`_sleep` 测试接缝。生成式 SDK 通常不管这块。 -3. **WebSocket 复用/解复用**:`WsConnection` 按 `task_id` 在一条物理连接上解复用多任务(`ws_connection.py`),支撑实时音频族。 -4. **OSS 直传预处理**(`_oss.py`):chat 调用前自动上传本地媒体,用户无感。 -5. **错误目录驱动**(`_error_registry.py`,650 行工具生成):异常带 message + solution + allow_retry,面向用户排障,比 ark 的纯状态码映射有人味得多。 -6. **配置分层与网关推导**:参数 > settings > env > config.toml > 1.x 文件;有 workspace 时推导 `{workspace}.{region}.maas.aliyuncs.com`(`_config.py:184-195`)。 -7. **运维细节好**:`SensitiveHeadersFilter` 日志脱敏(`_logging.py:27-79`)、`with_options()` 派生客户端共享连接池(`client.py:178-225`)、v2 流量标记 `module2`(`_ident.py:42-71`)。 - -## Dashscope v2 的缺点 - -1. **sync/async 人肉复制,比 ark 更严重**——`client.py:51 vs 272`、`http.py`、`_jobs.py`、各资源文件全是孪生,手工复制会漂移,ark 至少是机器复制保证一致。 -2. **资源方法样板极重**:每个方法重复 `timeout/api_key/workspace` 三件套透传 + `check_response` + `with_headers`(如 `_jobs.py:254-306`)。 -3. **两套 SSE 解析器**(`transport/sse.py` 用 httpx_sse,`resources/_sse.py` 手工逐行)。 -4. **无分页抽象**:裸 `page_no/page_size` 返回 `JobList/ModelList/FileList`,无迭代器、无 cursor。 -5. **请求参数无类型化**:`completions.py:73-102` 有 20+ 个 Optional 参数,IDE 补全和校验都弱。 -6. 脆弱补丁:`files.py:37-40` 用英文正则嗅探 "not found" 消息来补救网关把 404 报成 400 的问题。 -7. POST 一律重试无幂等区分(`http.py:174`)——这点 ark 也一样,算行业通病。 - ---- - -## Dashscope v2 可借鉴 Ark 的清单(按价值排序) - -| 优先级 | 借鉴点 | 解决 dashscope v2 的哪个痛点 | 落地建议 | -|---|---|---|---| -| ★★★ | 容错式构造(`construct_type` 宽松强转 + 不匹配原样返回) | 服务端新增枚举值/判别字段新值时 pydantic v2 严格校验会炸 | pydantic v2 下用 `model_construct` + 自定义容错 wrap 即可达到类似效果,不必照搬 v1/v2 双栈 | -| ★★★ | 统一请求定制通道 `extra_headers/extra_query/extra_body` | 用户无逃生舱,网关要加个实验性 header 都得发版 | 抽一个 `RequestOptions` 数据类贯穿 resources,顺带收敛 timeout/api_key/workspace 三件套样板 | -| ★★ | 分页抽象(cursor + 可迭代 page) | list 接口裸分页参数 | 给 `files/models/jobs.list` 加 `SyncPage` 迭代器,成本很低 | -| ★★ | `with_raw_response` + `parse(to=)` 记忆化 | 手工 `with_headers(...)` 散装 | 把 `ResponseBase` 升级为带 headers 的统一封装 | -| ★★ | `APIResponseValidationError` 单独成类 | 解析失败和 API 失败混在一族 | 在 errors.py 加一个子类即可 | -| ★ | 每请求 UUID(`X-Client-Request-Id`) | 进程级 session-id 定位不了单请求 | 加进 `_ident.py` | -| ★ | async 孪生改生成(ark 的教训是"人肉复制必漂移") | sync/async 漂移风险 | 不必上 Stainless;用 `unasync` 之类从 sync 源生成 async 孪生即可 | - -**不建议借鉴的**:Stainless 全量代码生成(会冲掉 protocols/Job/WebSocket 这些手写差异化能力,且 ark 自己的生成代码也有双客户端复制、双序列化约定问题);pydantic v1 兼容层;`_legacy_response.py` 式长期并存的双实现。 - -**结论**:反向看,ark 缺的东西(协议适配层、Job 轮询框架、错误目录、日志脱敏)恰好是 dashscope v2 的强项——v2 的核心架构不需要动,借鉴集中在**类型容错、请求定制、分页**这三个工程细节上。 diff --git a/md/cli-comparison-dashscope-vs-openai-vs-claude.md b/md/cli-comparison-dashscope-vs-openai-vs-claude.md deleted file mode 100644 index ad62b83b..00000000 --- a/md/cli-comparison-dashscope-vs-openai-vs-claude.md +++ /dev/null @@ -1,743 +0,0 @@ -# CLI 深度对比:DashScope CLI vs OpenAI CLI vs Claude CLI - -> 基于源码级分析(DashScope)+ 官方文档(OpenAI/Claude) -> 生成时间:2026-07-31 -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 1. 定位差异 — 根本性不同 - -| 维度 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 本质 | API 管理工具 | API 测试工具 | AI 编程 Agent | -| 类比 | 类似 aws cli / gcloud | 类似 curl 的高级封装 | 类似 Cursor / Copilot 的终端版 | -| 用户 | 开发者管理模型/微调/部署 | 开发者快速测试 API | 开发者用 AI 写代码 | -| 交互模式 | ❌ 无 | ❌ 无 | ✅ 完整 REPL | -| Agent 能力 | ❌ 无 | ❌ 无 | ✅ 读写文件、执行命令、搜索代码 | -| 安装 | `pip install dashscope` | `pip install openai[cli]` | `npm install -g @anthropic-ai/claude-code` | -| 入口命令 | `dashscope` | `openai` | `claude` | - -**核心洞察**:三者不在同一赛道。DashScope 和 OpenAI CLI 是"API 管理工具",Claude CLI 是"AI Agent 终端"。 - ---- - -## 2. 命令体系对比 - -### 2.1 命令结构 - -**DashScope CLI** - -``` -dashscope [options] -``` - -```bash -# 文本生成 -dashscope generation create -m qwen-plus -p "Hello" - -# 文件管理 -dashscope files upload -f data.jsonl -p fine_tune -dashscope files list -dashscope files get -dashscope files delete - -# 模型管理 -dashscope models list -dashscope models get - -# 微调 -dashscope ft create --training-file-ids file_xxx -dashscope ft list -dashscope ft get -dashscope ft stream - -# 部署 -dashscope deployments create --deployed-model xxx -dashscope deployments list - -# Embeddings / Rerank / Tokenization -dashscope embeddings create -m text-embedding-v2 -i "Hello" -dashscope rerank create -m gte-rerank -q "query" --documents '["doc1","doc2"]' -dashscope tokenization create -m qwen-plus -p "Hello" - -# 多模态 -dashscope image-synthesis create -m wanx-v1 -p "A cat" -dashscope video-synthesis create -m video-generation-model -p "A cat walking" -dashscope multimodal-conversation create -m qwen-vl-plus -p "Describe this" --image-url https://... -dashscope speech-synthesis create -m cosyvoice-v1 -p "Hello" -dashscope transcription create -m paraformer-v2 -f audio.wav - -# 强化学习 -dashscope rl create --config rl_config.yaml -dashscope rl list -dashscope rl get -dashscope rl logs -``` - -**OpenAI CLI** - -``` -openai api . [options] -``` - -```bash -# Chat Completions -openai api chat.completions.create -m gpt-4o -g user "Hello" - -# 流式 -openai api chat.completions.create -m gpt-4o -g user "Hello" --stream - -# Files -openai api files.create -f data.jsonl -p fine-tune -openai api files.list -openai api files.retrieve -openai api files.delete - -# Fine-tuning -openai api fine_tuning.jobs.create -m gpt-4o-mini --training_file file-xxx -openai api fine_tuning.jobs.list -openai api fine_tuning.jobs.retrieve -openai api fine_tuning.jobs.cancel - -# Models -openai api models.list -openai api models.retrieve gpt-4o - -# Images -openai api images.generate -p "A cat" --size 1024x1024 - -# Embeddings -openai api embeddings.create -m text-embedding-3-small -i "Hello" - -# Audio -openai api audio.transcriptions.create -m whisper-1 -f audio.mp3 -openai api audio.speech.create -m tts-1 -i "Hello" --voice alloy - -# Assistants (deprecated) -openai api assistants.create --instructions "You are helpful" -m gpt-4o -``` - -**Claude CLI** - -```bash -# 交互模式(REPL) -claude - -# 单次提问(脚本模式) -claude -p "Explain this codebase" - -# 管道模式 -cat error.log | claude -p "What's wrong?" -git diff | claude -p "Review this change" - -# 继续上次会话 -claude --continue -claude --resume - -# 指定模型 -claude --model claude-sonnet-4-20250514 - -# 输出格式 -claude -p "Hello" --output-format json -claude -p "Hello" --output-format stream-json - -# 权限模式 -claude --permission-mode acceptEdits -claude --permission-mode plan - -# MCP 服务器 -claude --mcp-server "npx @anthropic-ai/mcp-server-github" - -# 非交互 + 自动批准所有操作 -claude -p "Fix all bugs" --dangerously-skip-permissions -``` - -### 2.2 命令覆盖范围 - -| 功能域 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 文本生成 | ✅ generation create | ✅ chat.completions.create | ✅ 内置(Agent 模式) | -| 流式输出 | ✅ --stream | ✅ --stream | ✅ --output-format stream-json | -| 文件管理 | ✅ upload/get/list/delete | ✅ create/retrieve/list/delete | ❌ 不需要(直接读本地文件) | -| 模型列表 | ✅ models list/get | ✅ models.list/retrieve | ❌ 不需要 | -| 微调 | ✅ ft create/list/get/stream/cancel | ✅ fine_tuning.jobs.* | ❌ 不支持 | -| 部署 | ✅ deployments create/list/get/scale/delete | ❌ 不支持 | ❌ 不支持 | -| Embeddings | ✅ embeddings create | ✅ embeddings.create | ❌ 不支持 | -| Rerank | ✅ rerank create | ❌ 不支持 | ❌ 不支持 | -| Tokenization | ✅ tokenization create | ❌ 不支持 | ❌ 不支持 | -| 图像生成 | ✅ image-synthesis create | ✅ images.generate | ❌ 不支持 | -| 视频生成 | ✅ video-synthesis create | ✅ videos.create | ❌ 不支持 | -| 多模态对话 | ✅ multimodal-conversation create | ✅(通过 messages 传图) | ❌ 不支持 | -| 语音合成 | ✅ speech-synthesis create | ✅ audio.speech.create | ❌ 不支持 | -| 语音识别 | ✅ transcription create | ✅ audio.transcriptions.create | ❌ 不支持 | -| 强化学习 | ✅ rl create/list/get/logs | ❌ 不支持 | ❌ 不支持 | -| OSS 上传 | ✅ oss upload | ❌ 不支持 | ❌ 不支持 | -| 应用管理 | ✅ application | ❌ 不支持 | ❌ 不支持 | -| 代码编辑 | ❌ | ❌ | ✅ 核心能力 | -| 命令执行 | ❌ | ❌ | ✅ 核心能力 | -| 代码搜索 | ❌ | ❌ | ✅ 核心能力 | -| 会话管理 | ❌ | ❌ | ✅ --continue / --resume | -| MCP 集成 | ❌ | ❌ | ✅ --mcp-server | - -**结论**: - -- DashScope CLI 覆盖面最广(18 个资源域),但全是 API 管理 -- OpenAI CLI 覆盖 8 个资源域,纯 API 测试 -- Claude CLI 覆盖 0 个 API 资源域,但提供了完全不同的 Agent 能力 - ---- - -## 3. 用户体验对比 - -### 3.1 安装与配置 - -| 步骤 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 安装 | `pip install dashscope` | `pip install openai[cli]` | `npm install -g @anthropic-ai/claude-code` | -| 认证 | `export DASHSCOPE_API_KEY=sk-xxx` 或 `-k` 参数 | `export OPENAI_API_KEY=sk-xxx` 或 `openai auth login` | claude 首次运行引导 OAuth 登录 | -| 验证 | 无 auth 命令 | `openai auth login` / `openai auth logout` | OAuth 自动管理 | -| 版本 | `pip show dashscope` | `openai --version` | `claude --version` | - -**差距**: - -- OpenAI 有 `openai auth login` 交互式认证 -- Claude 有 OAuth 引导流程,首次使用零配置 -- DashScope 只有环境变量和 `-k` 参数,无 auth 命令 - -### 3.2 帮助系统 - -| 特性 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 全局帮助 | `dashscope --help` | `openai --help` | `claude --help` | -| 子命令帮助 | `dashscope generation --help` | `openai api chat.completions.create --help` | `claude --help` | -| 参数说明 | ✅ Typer 自动生成 | ✅ Click 自动生成 | ✅ 内置 | -| 示例 | ❌ 无 | ❌ 无 | ✅ 帮助中有示例 | -| Shell 补全 | ❌ add_completion=False | ✅ 支持 | ✅ 支持 | -| Rich 格式化 | ✅ 彩色输出 | ❌ 纯文本 | ✅ 彩色 + Markdown 渲染 | - -### 3.3 输出格式 - -| 特性 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 默认输出 | JSON(Rich 格式化) | JSON(原始) | 纯文本 / Markdown | -| --output-format | ❌ 无 | ❌ 无 | ✅ text / json / stream-json | -| 表格输出 | ✅(rl 命令有 Table) | ❌ | ❌ | -| JSON 美化 | ✅ console.print_json() | ✅ 自动格式化 | ✅ --output-format json | -| 进度条 | ✅(rl 命令有 Progress) | ❌ | ❌ | -| 错误着色 | ✅ `[red]Error:[/red]` | ❌ 纯文本 | ✅ 彩色 | -| 成功标记 | ✅ `[green]✓[/green]` | ❌ | ✅ | - -### 3.4 错误处理 - -**DashScope CLI** - -```python -# common.py — 统一错误处理 -def print_failed_message(rsp): - err_console.print( - f"[red]Failed[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " - f"code: {rsp.code}, message: {rsp.message}", - ) - -def ensure_ok(rsp, check_business_error=True): - if rsp.status_code != HTTPStatus.OK: - print_failed_message(rsp) - raise typer.Exit(1) - # 还检查 HTTP 200 但业务错误的情况 - if check_business_error: - error_code = output.get("code") - if error_code: - err_console.print(f"[red]Business Error[/red] ...") - raise typer.Exit(1) - return output -``` - -**亮点**: - -- `ensure_ok()` 统一处理 — 消除重复的 if/else -- `handle_sdk_error` 装饰器 — 捕获 SDK 异常转为友好消息 -- 区分 HTTP 错误和业务错误(HTTP 200 但 code 非空) -- Rich 彩色输出 - -实际错误输出: - -``` -Failed request_id: req-xxx, status_code: 429, code: Throttling, message: Requests rate limit -``` - -**OpenAI CLI** - -```json -// 错误直接输出 JSON -{ - "error": { - "message": "You exceeded your current quota...", - "type": "insufficient_quota", - "code": "insufficient_quota" - } -} -``` - -- 无彩色输出 -- 无 request_id 高亮 -- 无建议操作 - -**Claude CLI** - -``` -Error: Rate limit exceeded. Please wait a moment and try again. -``` - -- 简洁友好的错误消息 -- 权限拒绝时有明确提示 -- 工具执行失败时显示具体原因 - ---- - -## 4. 架构设计对比 - -### 4.1 技术栈 - -| 组件 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| 语言 | Python | Python | TypeScript (Node.js) | -| CLI 框架 | Typer (基于 Click) | Click | Ink (React for CLI) | -| 输出美化 | Rich | 无 | Ink + Markdown 渲染 | -| 异步 | asyncio(仅 rl 命令) | 无 | 完整异步 | -| 包管理 | pip | pip | npm | -| 入口 | dashscope.cli.main() | openai.cli.main() | claude (Node binary) | - -### 4.2 代码组织 - -**DashScope CLI** - -``` -dashscope/cli/ -├── __init__.py # Typer app + 命令注册 + 兼容层 -├── __main__.py # python -m dashscope.cli -├── common.py # 共享工具(ensure_ok, error, success) -├── generation.py # 文本生成 -├── files.py # 文件管理 -├── models.py # 模型管理 -├── fine_tunes.py # 微调 -├── deployments.py # 部署 -├── embeddings.py # 向量化 -├── rerank.py # 重排序 -├── tokenization.py # 分词 -├── image_synthesis.py # 图像合成 -├── video_synthesis.py # 视频合成 -├── image_generation.py # 图像生成 -├── multimodal_conversation.py # 多模态对话 -├── multimodal_embedding.py # 多模态向量化 -├── transcription.py # 语音识别 -├── speech_synthesis.py # 语音合成 -├── oss.py # OSS 上传 -├── application.py # 应用管理 -├── agentic_rl.py # 强化学习(最复杂,21.8KB) -└── code_generation.py # 代码生成 -``` - -模式:每个资源域一个文件,每个文件定义一个 `typer.Typer()` app。 - -**OpenAI CLI** - -``` -openai/cli/ -├── main.py # 入口 -├── _cli.py # Click group 定义 -├── _errors.py # 错误处理 -└── (resources 通过 SDK 自动生成) -``` - -模式:极简 — 只是 SDK 方法的薄封装,大部分代码自动生成。 - -**Claude CLI** - -``` -@anthropic-ai/claude-code/ -├── src/ -│ ├── cli.ts # 入口 + 参数解析 -│ ├── repl/ # 交互式 REPL -│ ├── agent/ # Agent 循环 -│ ├── tools/ # 内置工具(Read/Edit/Bash/Glob) -│ ├── permissions/ # 权限系统 -│ ├── mcp/ # MCP 服务器集成 -│ ├── sessions/ # 会话管理 -│ └── hooks/ # 生命周期钩子 -└── ... -``` - -模式:完整的 Agent 框架,CLI 只是入口。 - -### 4.3 命令模式对比 - -| 模式 | DashScope | OpenAI | Claude | -| --- | --- | --- | --- | -| 资源-动作 | `dashscope files upload` | `openai api files.create` | N/A | -| 命名风格 | kebab-case (fine-tunes) | dot-notation (fine_tuning.jobs) | 无子命令(flag 驱动) | -| 参数风格 | `--training-file-ids` | `--training_file` | `--model` | -| 输入方式 | 命令行参数 | 命令行参数 + stdin | stdin + 交互式 | -| JSON 输入 | `--messages '[{"role":"user",...}]'` | `-g user "Hello"` | 直接输入 | - ---- - -## 5. 功能深度对比 - -### 5.1 流式输出 - -**DashScope CLI** - -```bash -dashscope generation create -m qwen-plus -p "Hello" --stream -``` - -```python -# 实现:逐个 chunk 输出 -if stream: - for rsp in response: - if rsp.status_code == HTTPStatus.OK: - typer.echo(rsp.output) # 原始 output dict -``` - -问题: - -- 输出的是 `rsp.output`(原始 dict),不是纯文本 -- 无增量拼接 -- 无进度指示 - -**OpenAI CLI** - -```bash -openai api chat.completions.create -m gpt-4o -g user "Hello" --stream -``` - -- 每个 chunk 输出 JSON -- 支持 `--stream` flag -- 输出格式与非流式一致(JSON) - -**Claude CLI** - -```bash -claude -p "Hello" --output-format stream-json -``` - -- 流式 JSON 事件(assistant / tool_use / result) -- 交互模式下实时渲染 Markdown -- 支持 `--output-format text` 纯文本流 - -### 5.2 多模态输入 - -**DashScope CLI** - -```bash -# 图像理解 -dashscope multimodal-conversation create \ - -m qwen-vl-plus \ - -p "Describe this image" \ - --image-url "https://example.com/cat.jpg" - -# 语音识别 -dashscope transcription create \ - -m paraformer-v2 \ - -f audio.wav -``` - -**OpenAI CLI** - -```bash -# 图像(通过 messages JSON) -openai api chat.completions.create \ - -m gpt-4o \ - -g user '[{"type":"text","text":"Describe"},{"type":"image_url","image_url":{"url":"https://..."}}]' - -# 语音 -openai api audio.transcriptions.create -m whisper-1 -f audio.mp3 -``` - -**Claude CLI** - -```bash -# 不直接支持多模态 API 调用 -# 但可以读取本地文件(包括图片)作为上下文 -claude -p "Describe this image" < image.png # 管道输入 -``` - -### 5.3 异步任务管理 - -**DashScope CLI — 独有优势** - -```bash -# 创建异步任务 -dashscope image-synthesis create -m wanx-v1 -p "A cat" --async - -# 查看任务状态 -dashscope image-synthesis get - -# 等待任务完成(内置轮询) -dashscope video-synthesis create -m xxx -p "A cat" --wait - -# 微调任务流式日志 -dashscope ft stream - -# RL 任务日志 -dashscope rl logs --follow -``` - -亮点: - -- 内置任务轮询(POLL_INTERVAL = 30s) -- Rich Progress 进度条(rl 命令) -- 任务状态跟踪(PENDING → RUNNING → SUCCEEDED/FAILED) - -**OpenAI CLI** - -```bash -# 微调任务 -openai api fine_tuning.jobs.create -m gpt-4o-mini --training_file file-xxx -openai api fine_tuning.jobs.retrieve -openai api fine_tuning.jobs.cancel -# 无内置轮询,需手动重复 retrieve -``` - -**Claude CLI** - -``` -# 不适用 — Claude CLI 不管理异步任务 -``` - -### 5.4 管道与脚本集成 - -**DashScope CLI** - -```bash -# 基本管道 -echo '{"prompt": "Hello"}' | dashscope generation create -m qwen-plus -p "$(cat)" - -# 批量处理(需要外部脚本) -for file in *.txt; do - dashscope embeddings create -m text-embedding-v2 -i "$(cat $file)" > "${file}.json" -done -``` - -问题: - -- 无原生 stdin JSON 输入 -- 无 `--json` 输入模式 -- 批量操作需要外部 shell 脚本 - -**OpenAI CLI** - -```bash -# stdin JSON 输入 -echo '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}' | \ - openai api chat.completions.create - -# 文件输入 -openai api chat.completions.create < request.json -``` - -亮点:支持从 stdin 读取完整 JSON 请求体。 - -**Claude CLI** - -```bash -# 管道模式 — 最强大 -cat error.log | claude -p "What's wrong with this?" -git diff HEAD~1 | claude -p "Write a commit message" -find . -name "*.py" | xargs claude -p "Find security issues" --output-format json - -# 脚本集成 -result=$(claude -p "What is 2+2?" --output-format json) -echo "$result" | jq '.result' -``` - -亮点: - -- 管道输入是核心设计 -- `--output-format json` 便于脚本解析 -- 可以处理任意文件内容 - ---- - -## 6. Claude CLI 独有能力(DashScope/OpenAI 完全缺失) - -### 6.1 交互式 REPL - -``` -$ claude -╭────────────────────────────────────────────╮ -│ Claude Code │ -│ │ -│ Type /help for available commands │ -╰────────────────────────────────────────────╯ - -> Fix the bug in auth.py where login fails for expired tokens - -I'll look at the auth.py file to understand the issue. - -[Read auth.py] -[Edit auth.py - line 42: Added token expiry check] -[Bash: python -m pytest tests/test_auth.py -v] - -All tests pass. The fix adds an expiry check before validating the token. -``` - -### 6.2 Agent 工具调用 - -Claude CLI 内置工具: - -- Read — 读取文件 -- Edit — 编辑文件(精确行级修改) -- Write — 写入新文件 -- Bash — 执行 shell 命令 -- Glob — 文件模式搜索 -- Grep — 内容搜索 -- WebSearch — 网络搜索 - -### 6.3 权限系统 - -```bash -# 6 种权限模式 -claude --permission-mode default # 每次操作都询问 -claude --permission-mode acceptEdits # 自动批准文件编辑 -claude --permission-mode plan # 只规划不执行 -claude --permission-mode auto # 自动批准安全操作 -claude --permission-mode dontAsk # 从不询问(危险) -claude --permission-mode bypassPermissions # 完全绕过(极危险) -``` - -### 6.4 会话管理 - -```bash -claude # 新会话 -claude --continue # 继续上次会话 -claude --resume # 选择历史会话恢复 -claude --session-id xxx # 恢复指定会话 -``` - -### 6.5 MCP 服务器集成 - -```bash -# 连接 GitHub MCP -claude --mcp-server "npx @anthropic-ai/mcp-server-github" - -# 连接数据库 MCP -claude --mcp-server "npx @anthropic-ai/mcp-server-postgres postgresql://..." - -# 在会话中使用 MCP 工具 -> List all open PRs on the dashscope/dashscope-sdk-python repo -``` - ---- - -## 7. 综合评分 - -| 维度 | DashScope CLI | OpenAI CLI | Claude CLI | -| --- | --- | --- | --- | -| API 覆盖广度 | ⭐⭐⭐⭐⭐ (18域) | ⭐⭐⭐⭐ (8域) | ⭐ (0域) | -| 命令一致性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 错误处理 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | -| 输出美化 | ⭐⭐⭐⭐ (Rich) | ⭐⭐ | ⭐⭐⭐⭐⭐ | -| 脚本集成 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 交互体验 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | -| Agent 能力 | ❌ | ❌ | ⭐⭐⭐⭐⭐ | -| 异步任务管理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ | -| 多模态支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | -| RL/微调 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ | -| Shell 补全 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | -| 文档/示例 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | - ---- - -## 8. DashScope CLI 改进建议 - -### 8.1 短期(P0)— 提升现有体验 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 1 | 添加 `dashscope auth login` | 交互式认证,保存 Key 到本地 | OpenAI `openai auth login` | -| 2 | 启用 Shell 补全 | add_completion=True + 安装脚本 | Typer 原生支持 | -| 3 | stdin JSON 输入 | `echo '...' \| dashscope generation create --stdin` | OpenAI CLI | -| 4 | `--output-format` 选项 | json / text / table 可选 | Claude CLI | -| 5 | 流式输出纯文本 | 流式时只输出 content 文本,不输出完整 dict | Claude CLI | - -### 8.2 中期(P1)— 补齐能力 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 6 | 管道模式 | `cat file.py \| dashscope generation create -m qwen-plus --stdin-prompt "Review this"` | Claude CLI | -| 7 | 批量处理 | `dashscope embeddings create --batch-file inputs.txt` | 自研 | -| 8 | 任务等待 + 进度 | 所有异步任务支持 `--wait` + Rich Progress | 已有(rl),扩展到所有 | -| 9 | `dashscope config` | 配置管理(默认模型、默认 Key、输出格式) | aws configure | -| 10 | JSON Schema 验证 | 输入参数本地验证,不发请求就报错 | OpenAI SDK | - -### 8.3 长期(P2)— 差异化竞争 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 11 | 交互式 REPL | `dashscope chat` 进入对话模式 | Claude CLI | -| 12 | Agent 模式 | 集成 acli 的 Agent 能力到 CLI | Claude CLI + 自有 acli | -| 13 | MCP 支持 | `dashscope --mcp-server` 连接外部工具 | Claude CLI | -| 14 | 会话管理 | `--continue` / `--resume` 多轮对话 | Claude CLI | -| 15 | 权限系统 | 工具调用分级确认 | Claude CLI | - -### 8.4 差异化定位建议 - -DashScope CLI 不应试图复制 Claude CLI(那是 Agent 赛道),而应强化自身优势: - -``` -DashScope CLI 定位:阿里云 AI 平台的完整管理工具 -├── 模型管理(list / get / deploy / scale) -├── 微调全流程(upload → create → monitor → deploy) -├── RL 训练全流程(create → logs → monitor) -├── 多模态全栈(图像/视频/语音/文本) -└── 与阿里云生态集成(OSS / RAM / 工作空间) -``` - -**独特价值**: - -- OpenAI CLI 不支持部署管理 → DashScope 有 -- Claude CLI 不做 API 管理 → DashScope 有 -- 两者都不支持 RL 训练 → DashScope 有 -- 两者都不支持视频生成 → DashScope 有 - ---- - -## 9. 命令设计最佳实践对比 - -### 9.1 命名规范 - -| 方面 | DashScope | OpenAI | 建议 | -| --- | --- | --- | --- | -| 资源名 | fine-tunes (kebab) | fine_tuning.jobs (dot) | DashScope 更好(shell 友好) | -| 动作名 | create / list / get / delete | create / list / retrieve / delete | 统一用 get 比 retrieve 更直觉 | -| 缩写 | ft = fine-tunes | 无 | DashScope 好 | -| 隐藏命令 | ✅ hidden=True | ✅ | 两者都好 | - -### 9.2 参数设计 - -| 方面 | DashScope | OpenAI | Claude | -| --- | --- | --- | --- | -| 模型 | `-m` / `--model` | `-m` / `--model` | `--model` | -| 输入 | `-p` / `--prompt` | `-g` / `--message` (role + content) | 直接输入 | -| 流式 | `-s` / `--stream` | `--stream` | `--output-format stream-json` | -| 全局 Key | `-k` / `--api-key` | 环境变量 | OAuth | -| 输出格式 | ❌ 无 | ❌ 无 | `--output-format` | - -### 9.3 退出码 - -| 退出码 | DashScope | OpenAI | Claude | -| --- | --- | --- | --- | -| 0 | 成功 | 成功 | 成功 | -| 1 | API 错误 / SDK 异常 | API 错误 | 执行失败 | -| 2 | 参数错误 | 参数错误 | 权限拒绝 | - ---- - -## 参考 - -- DashScope CLI 源码:`dashscope/cli/`(24 个文件) -- OpenAI CLI 文档 -- Claude CLI 文档 diff --git a/md/cli-source-level-comparison-2026-08.md b/md/cli-source-level-comparison-2026-08.md deleted file mode 100644 index 2f6952a4..00000000 --- a/md/cli-source-level-comparison-2026-08.md +++ /dev/null @@ -1,259 +0,0 @@ -# CLI 源码级对比(仅 CLI,不含 SDK):DashScope CLI vs OpenAI CLI vs Anthropic CLI - -> 对比维度:1) 用户接口友好 2) 错误码/错误契约 3) 流式/非流式、同步/异步 -> 源码/文档依据:生成时间:2026-08 -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 0. 首先必须讲清楚的事实:三家"CLI"已经不是同一物种 - -### 0.1 OpenAI 把 API CLI 删了 - -openai-python v2.35.0(2026-05-06)CHANGELOG 明确记录: - -``` -### Chores -* remove legacy python cli (32f36e4) -* rename legacy python cli entrypoint (a3b182d) -``` - -源码验证:v2.52.0 的 pyproject.toml 已无 CLI 入口点、无 cli extra、无 cli 模块。也就是说—— - -OpenAI 曾经有和 DashScope CLI 同类的东西(`openai api chat.completions.create ...`,随 pip 包分发),但在 2026 年 5 月主动删除了它。 - -OpenAI 现在的官方 CLI 是 Codex CLI:Rust 编写的独立二进制,`curl ... install.sh | sh` / npm / brew 安装,定位是编程 Agent(对标 Claude Code),不再是 API 管理工具。 - -### 0.2 Anthropic 从未在 Python SDK 里放过 CLI - -anthropic-sdk-python 源码中没有任何 CLI 模块/入口点。Anthropic 的官方 CLI 是 Claude Code(npm 包 `@anthropic-ai/claude-code`),同样是编程 Agent。 - -### 0.3 DashScope 是三家中唯一"SDK 内嵌 API 管理 CLI"的 - -| | DashScope | OpenAI | Anthropic | -| --- | --- | --- | --- | -| SDK 内嵌 CLI | ✅ dashscope 命令(Typer) | ❌ v2.35.0 已删除 | ❌ 从未有过 | -| 独立 Agent CLI | ⚠️ acli(独立项目) | ✅ Codex CLI(Rust 二进制) | ✅ Claude Code(npm) | -| API 管理类 CLI | ✅ 仍在维护 | ❌ 放弃该品类 | ❌ 放弃该品类 | - -**战略解读**:OpenAI/Anthropic 的战略是"API 调用请用 SDK,终端交互请用 Agent",中间的"API 管理 CLI"品类被两家同时放弃。DashScope CLI 因此有了双重身份: - -1. **机会**:该品类只剩 DashScope 一家在做,aws cli 式的平台管理工具有独立价值; -2. **警示**:两家头部厂商都判断"API CLI"不值得维护,DashScope 需要想清楚这个 CLI 的独特价值(答案大概率是:微调/部署/RL 等长流程管理,而不是 `generation create` 这种一条 curl 就能替代的调用)。 - ---- - -## 1. 用户接口友好 - -### 1.1 安装与认证 - -| | DashScope CLI | Codex CLI | Claude Code CLI | -| --- | --- | --- | --- | -| 安装 | `pip install dashscope`(SDK 附赠,零额外步骤) | `curl install.sh \| sh` / `npm i -g @openai/codex` / `brew install --cask codex`,原生二进制 | `npm i -g @anthropic-ai/claude-code` | -| 认证方式 | 环境变量 `DASHSCOPE_API_KEY` 或 `-k` 参数 | `codex login`(浏览器 OAuth,绑定 ChatGPT 订阅)或 API key;`codex login status` 退出码 0/1 可脚本化探测 | claude 首次运行引导 OAuth | -| 凭证管理命令 | ❌ 无 | ✅ codex login/logout/login status | ✅ /login、/logout | -| 多账号/多环境 | ❌ | ✅ ~/.codex/config.toml + `-c key=value` 单次覆盖 | ✅ settings 分层(企业/用户/项目) | - -**差距点**:DashScope CLI 连 `dashscope auth whoami`(验证当前 key 有效性)都没有。Codex 的 `login status` 用退出码做脚本探测是 CI 友好的标准做法,值得直接抄。 - -### 1.2 交互模型 - -| | DashScope CLI | Codex CLI | Claude Code CLI | -| --- | --- | --- | --- | -| 默认形态 | 一次性命令 | 交互式 TUI(codex 直接进入) | 交互式 REPL(claude 直接进入) | -| 脚本模式 | 所有命令天生可脚本化 | `codex exec`(短写 `codex e`) | `claude -p "..."` | -| 会话恢复 | ❌ | ✅ `codex resume [--last/--all]`、`codex exec resume` | ✅ `claude --continue/--resume` | -| 斜杠命令 | ❌ | ✅ /resume /goal /login 等 20+ | ✅ /help /clear /compact 等 | -| 图片/文件输入 | 参数传 URL/路径 | TUI 直接粘图 | 管道 `cat x \| claude -p` | - -### 1.3 帮助系统与可发现性 - -| | DashScope CLI | Codex CLI | Claude Code CLI | -| --- | --- | --- | --- | -| 帮助 | Typer 自动生成,no_args_is_help=True | 每命令独立参考页 + 成熟度标注(Stable/Beta/Experimental) | claude --help + /help | -| Shell 补全 | ❌ 源码确认 add_completion=False(cli/__init__.py:244) | ✅(Rust 端 clap 支持) | ✅ | -| 示例 | ❌ help 里无 example | ✅ 文档含场景化示例 | ✅ | -| 配置系统 | ❌ 无(全靠命令行参数) | ✅ ~/.codex/config.toml,命令行 -c k=v 覆盖,优先级明确 | ✅ settings.json 分层 | - -**差距点**:`add_completion=False` 是 DashScope CLI 里一个一行就能修的短板(Typer 原生支持生成 bash/zsh/fish 补全脚本)。 - -### 1.4 用户接口友好小结 - -- **Codex / Claude Code**:OAuth 登录引导 + 交互 TUI + 会话恢复 + 分层配置 + Shell 补全——把 CLI 当"产品"打磨; -- **DashScope CLI**:停留在"SDK 的命令行薄封装"水平,但有一个别人没有的优点——pip 装 SDK 即得,零额外安装,且命令覆盖面(18 个资源域)远超对手; -- **最痛的三个缺口**:无 auth 命令、无 shell 补全、无配置文件。 - ---- - -## 2. 错误码与错误契约 - -### 2.1 DashScope CLI:三家里错误信息最"全",但契约最"弱" - -源码(cli/common.py)显示 DashScope CLI 的错误处理其实是认真设计过的: - -```python -# common.py:35 — 错误输出带四要素 -def print_failed_message(rsp): - err_console.print( - f"[red]Failed[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}") - -# common.py:44 — 双层错误检查(HTTP 层 + 业务层) -def ensure_ok(rsp, check_business_error=True): - if rsp.status_code != HTTPStatus.OK: ... # HTTP 错误 - ... - if error_code and error_code != "": ... # HTTP 200 但业务错误 -``` - -- **四要素齐全**:request_id / status_code / code / message —— Codex 和 Claude Code 的错误输出都没有这么全的排查信息(Agent 类 CLI 面向人类,不需要); -- **双层检查**:能识别"HTTP 200 但业务失败"——这是 DashScope 网关特性,另两家不存在此问题; -- **装饰器兜底**:`handle_sdk_error("action")` 把 SDK 异常统一转成红色友好错误。 - -但契约层面有三个问题: - -1. **退出码单一**:所有失败都是 exit 1(`raise typer.Exit(1)`),无法像 `codex login status` 那样用不同退出码区分错误类别。脚本只能靠解析 stderr 文本判断错误类型。 -2. **流式分支的错误不退出**(源码实锤的 bug,cli/generation.py:172-189): - -```python -if stream: - for rsp in response: - if rsp.status_code == HTTPStatus.OK: - typer.echo(rsp.output) - else: - print_failed_message(rsp) # ← 只打印!循环继续,命令最终 exit 0 -else: - ... - print_failed_message(response) - raise typer.Exit(1) # ← 非流式分支有退出 -``` - -流式中途出错时命令以 0 退出,CI 里会被当成成功——这是与另两家对比后暴露出的真实缺陷。 - -3. **错误不可机器解析**:没有 `--output-format json` 的错误输出,stderr 是 Rich 着色的自然语言。 - -### 2.2 Codex CLI:为自动化设计的错误契约 - -- `--json` 模式下所有状态变化(含错误)是 NDJSON 事件流,错误是结构化事件而非文本; -- `codex login status` 用退出码表达状态(0=有凭证); -- `codex cloud` 列表 plain-text 打印任务状态,`--json` 时给出 tasks 数组 + cursor 分页——官方明确建议"Use --json for automation"; -- 权限/沙箱拒绝是显式审批流(approval policy),不是事后报错:untrusted / on-failure / on-request / never 四级,越界动作先问人。 - -### 2.3 Claude Code CLI - -- `--output-format json` 时错误进 JSON 结构,退出码非 0; -- 权限拒绝走 permission-mode 交互(acceptEdits/plan/bypassPermissions),同样把"错误"前置为"审批"。 - -### 2.4 错误契约对比表 - -| | DashScope CLI | Codex CLI | Claude Code | -| --- | --- | --- | --- | -| 错误信息完整度(request_id 等) | ⭐⭐⭐⭐⭐ 四要素 | ⭐⭐⭐ 结构化事件 | ⭐⭐⭐ | -| 退出码区分度 | ⭐ 只有 0/1 | ⭐⭐⭐⭐ 分命令设计 | ⭐⭐⭐ | -| 机器可解析错误 | ❌ 着色文本 | ✅ NDJSON | ✅ JSON | -| 流式中途出错行为 | ❌ exit 0(bug) | ✅ 错误事件 | ✅ 错误事件 | -| 危险操作防护 | N/A(只读多) | ✅ 沙箱+审批四级 | ✅ 权限模式 | -| 双层错误(HTTP+业务)检查 | ✅ 独有 | N/A | N/A | - ---- - -## 3. 流式 / 非流式、同步 / 异步 - -### 3.1 流式输出 - -| | DashScope CLI | Codex CLI | Claude Code | -| --- | --- | --- | --- | -| 开关 | `--stream` / `-s`(逐命令) | 交互 TUI 天然流式;exec 默认格式化流式输出 | 交互天然流式;`-p` 配 `--output-format stream-json` | -| 流的内容 | 原始 dict:`typer.echo(rsp.output)` + 单独一行 usage(generation.py:173-178) | NDJSON 事件(--json)或格式化渲染 | JSON 事件(assistant/tool_use/result)或渲染文本 | -| 增量控制 | 底层 incremental_output 参数,CLI 未暴露语义化选择 | N/A | N/A | -| 人类可读性 | ❌ 流出的是 Python dict 的 repr,不是纯文本 | ✅ 默认渲染 | ✅ Markdown 渲染 | -| 机器可读性 | ⚠️ 是 dict repr 不是 JSON,管道里没法 jq | ✅ NDJSON | ✅ JSON | - -DashScope 流式输出的具体问题(generation.py:175):`typer.echo(rsp.output)` 打印的是 dict 的字符串表示(单引号、Python 字面量),既不是增量文本也不是合法 JSON——人看不懂、机器没法解析,是三家里最弱的流式实现。改进方向很明确: - -- **人类模式**:只打印增量 content 文本; -- **机器模式**:`--output-format json` 时每 chunk 一行合法 JSON。 - -### 3.2 非流式 - -三家都是一条命令拿完整结果,差别在输出形态: - -| | DashScope CLI | Codex CLI | Claude Code | -| --- | --- | --- | --- | -| 默认输出 | output dict + usage(两行 Python repr) | 格式化文本 | 渲染文本 | -| JSON 输出选项 | ❌ 无(默认就不是 JSON) | ✅ --json | ✅ --output-format json | - -### 3.3 同步 vs 异步 —— DashScope 的主场 - -这是维度反转的一节:Agent 类 CLI 没有"异步任务"概念(任务在会话内同步完成),而 DashScope CLI 服务的 API 有大量异步长任务(微调、RL、视频生成、部署),CLI 为此建了完整的异步设施(cli/common.py:19-21、cli/agentic_rl.py): - -| 异步能力 | DashScope CLI | Codex CLI | Claude Code | -| --- | --- | --- | --- | -| 任务提交 | ✅ ft create / rl create / 异步图像视频任务 | ❌ | ❌ | -| 状态轮询 | ✅ 内置 POLL_INTERVAL=30s 轮询 + Rich Progress 进度条 | ❌ | ❌ | -| 日志跟随 | ✅ rl logs --follow(LOG_PAGE_SIZE=1000 分页拉取) | ❌ | ❌ | -| 任务列表/详情 | ✅ ft list/get、rl list/get | ❌ | ❌ | -| 取消 | ✅ ft cancel | ❌ | ❌ | -| 云端并行任务 | ❌ | ✅ codex cloud(云端环境并行跑任务,--json 输出 tasks 数组+cursor 分页,可从 Web/GitHub/Linear/Slack 发起) | ❌ | - -**值得注意**:Codex cloud 本质上是 OpenAI 版的"异步任务管理",只是对象从"微调任务"变成了"编程任务"——提交、状态查询、分页列表、JSON 输出,形态与 DashScope 的 ft/rl 管理惊人相似。这说明"长任务管理 CLI"这个品类没有消失,只是换了载体。 - -### 3.4 同步/异步维度小结 - -- DashScope CLI 的同步调用(generation create 等)体验弱于对手的 Agent 交互,但这不是它的核心价值; -- DashScope CLI 的异步任务管理(提交→轮询→日志→取消)是三家中唯一面向 AI 训练/生成流水线的,配合 Rich Progress 有不错的体验; -- 对标 Codex cloud 后,DashScope 的异步管理可以补的一课是:任务列表的 `--json` 机器输出 + cursor 分页。 - ---- - -## 4. 总评分 - -| 维度 | DashScope CLI | Codex CLI | Claude Code | -| --- | --- | --- | --- | -| 安装便捷(pip 附赠) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | -| 认证体验 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 交互模式 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 配置系统 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 错误信息完整度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | -| 错误契约(退出码/机器可读) | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | -| 流式输出质量 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 异步任务管理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐(cloud) | ⭐ | -| API 资源覆盖 | ⭐⭐⭐⭐⭐(18 域) | ⭐(Agent 域) | ⭐(Agent 域) | - ---- - -## 5. 给 DashScope CLI 的建议(纯 CLI 视角) - -### 立即修(小改动,高回报) - -1. **修流式错误退出码 bug**:generation.py 流式分支出错后补 `raise typer.Exit(1)`(§2.1 实锤); -2. **开 Shell 补全**:`add_completion=True`(一行改动,cli/__init__.py:244); -3. **流式输出改为增量纯文本**:默认打 content 增量,加 `--output-format json` 时输出每行合法 JSON; -4. **加 `dashscope auth whoami/status`**:验证 key 有效性,退出码区分(抄 Codex login status)。 - -### 中期 - -5. **错误退出码分级**:认证错=2、参数错=3、限流=4、服务端=5,脚本可免解析 stderr; -6. **`--output-format json` 全局化**:所有命令(尤其 ft list / rl list)支持 JSON + cursor 分页输出; -7. **配置文件**:`~/.dashscope/config.toml` 存默认 model/workspace/输出格式,`-c` 覆盖(抄 Codex 的 config 优先级设计)。 - -### 定位建议 - -8. 不要再和 Codex/Claude Code 比"对话体验"——那是 Agent 赛道,acli 在跟; -9. DashScope CLI 的对标物其实是 Codex cloud + aws cli 的混合体:把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成三家中无可替代的那一个,这才是 OpenAI/Anthropic 删除 API CLI 之后留下的空位。 - ---- - -## 附:源码证据索引 - -| 结论 | 证据 | -| --- | --- | -| OpenAI 删除 API CLI | openai-python/CHANGELOG.md v2.35.0 "remove legacy python cli";v2.52.0 无 cli 入口点/extra | -| Anthropic SDK 无 CLI | anthropic-sdk-python 全仓无 cli 模块/入口点 | -| DashScope 关闭补全 | dashscope/cli/__init__.py:244 add_completion=False | -| 错误四要素+双层检查 | dashscope/cli/common.py:35-91 | -| 流式错误 exit 0 bug | dashscope/cli/generation.py:172-189 | -| 流式输出 dict repr | dashscope/cli/generation.py:175 typer.echo(rsp.output) | -| 异步轮询常量 | dashscope/cli/common.py:19-21 POLL_INTERVAL=30 | -| RL 进度条/日志 | dashscope/cli/agentic_rl.py Rich Progress、logs follow | -| Codex exec/--json/login status/resume/cloud | developers.openai.com/codex Developer commands 参考页 | -| Codex 沙箱+审批 | developers.openai.com/codex/security | diff --git a/md/conclusion-sdk-architecture-stainless.md b/md/conclusion-sdk-architecture-stainless.md deleted file mode 100644 index cf8b2970..00000000 --- a/md/conclusion-sdk-architecture-stainless.md +++ /dev/null @@ -1,57 +0,0 @@ -# 结论:SDK 架构(Stainless 孪生架构实证) - -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 实证:两个仓库的目录几乎完全同构 - -`comm -3` 对比两个 src/ 目录,只有 14 项不同,其余 17 项(_base_client.py、_client.py、_exceptions.py、_streaming.py、_models.py、_response.py、_resource.py、_qs.py、pagination.py、resources/、types/、lib/……)文件名逐一对应。且文件头直接写着: - -``` -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -``` - -连 _streaming.py 里的注释都一样("initially copied from httpx-sse")——因为是同一份模板复制出去的。 - -## Stainless 是什么 - -Stainless(stainlessapi.com)是一家专门做"SDK 生成器"的公司。工作方式: - -``` -厂商的 OpenAPI spec(机器可读的 API 定义) - │ - ▼ -Stainless 生成器(模板 + 各语言运行时) - │ - ▼ -完整的类型安全 SDK:Python / TypeScript / Go / Java / Kotlin / Ruby -``` - -客户包括 OpenAI、Anthropic、Cloudflare、Cohere、Lithic 等。厂商不手写 SDK,而是维护 OpenAPI spec,Stainless 的 CI 在 spec 变更后自动重新生成 SDK 并发 PR。 - -## "孪生架构"具体指什么 - -同一个模板生成,所以两个 SDK 的"骨架"完全一样: - -| 层 | OpenAI | Anthropic | 来源 | -| --- | --- | --- | --- | -| HTTP 基座 | SyncAPIClient/AsyncAPIClient(httpx) | 同名同实现 | 模板 | -| 重试逻辑 | _should_retry + Retry-After + 指数退避 | 逐行同构 | 模板 | -| 异常层次 | APIStatusError → 400/401/403/404/409/422/429/5xx | 同一棵树 | 模板 | -| 流式机制 | Stream[T] / AsyncStream[T] SSE 迭代器 | 同名同构 | 模板 | -| 未传参哨兵 | NOT_GIVEN | NOT_GIVEN | 模板 | -| 响应模型 | Pydantic + model_construct | 同 | 模板 | - -差异只在两处: - -1. **API 表面**——resources/ 和 types/ 目录由各自的 OpenAPI spec 生成,内容完全不同(chat.completions vs messages); -2. **手写扩展**——就是 comm 输出里那 14 个不同项:OpenAI 的 providers/(多云)、_provider.py、realtime/websocket 相关;Anthropic 的 lib/credentials/(OAuth 凭证链)、_middleware.py、AnthropicAWS/AnthropicFoundry 子类。 - -## 这对我们的对比意味着什么 - -三点推论: - -1. 对比 OpenAI SDK vs Anthropic SDK 时,"框架能力"那部分结论对两者同时成立。重试、异常驱动、类型系统、四象限覆盖(同步/异步 × 流式/非流式)这些优点不是某一家的设计选择,而是 Stainless 模板的标配。真正值得单独评的是两家的手写扩展(Anthropic 的 text_stream、OpenAI 的 parse() 等)。 -2. DashScope 的差距要重新定性:不是"DashScope 工程师写得不如人",而是**手写实现 vs 工业化流水线的差距**。Stainless 模式下,API 加了个新参数 → spec 更新 → SDK 自动生成带完整类型和测试的版本,天级送达;手写模式下要人肉改签名、补文档、补测试,还容易漂移(DashScope Generation.call 里那 30 个 Any 参数和手工 `if x is not None` 搬运就是这种漂移的产物)。 -3. 这也解释了之前建议里 P2 那条"代码生成":如果 DashScope 要追平,方向不是继续手写补丁,而是把百炼 API 沉淀成 OpenAPI spec,接入生成器(Stainless 是商业服务,也可用开源方案如 oapi-codegen/自研模板),让 SDK 从 spec 自动长出类型和资源类。 diff --git a/md/dashscope-2.0-1x-dependency-decoupling.md b/md/dashscope-2.0-1x-dependency-decoupling.md deleted file mode 100644 index 682b6e7c..00000000 --- a/md/dashscope-2.0-1x-dependency-decoupling.md +++ /dev/null @@ -1,146 +0,0 @@ -# DashScope 2.0 ↔ 1.x 依赖清单与解耦方案 - -> 目标仓库:`moe-dashscope-sdk-python` -> 作用:作为"2.0 上提为顶层、1.x 归入 legacy"这次大版本重构的**强制前置(Step 0)**。 -> 背景:2.0(`dashscope/v2/`)并非自包含——它对 1.x 有 **8 处反向依赖**。任何 `git mv` -> 之前必须先把这 8 处解掉或归类,否则搬动 1.x 会直接打断 2.0 的运行时导入。 - -## 1. 解耦原则 - -1. **共享基础设施**:若代码量不多,**直接在 2.0 内重新实现**,让 2.0 自己拥有,消除对 1.x 的依赖。 -2. **功能级委托**:做**反向搬迁**——把实现从 1.x 移到 2.0,原 1.x 路径改为**反向引用 2.0**(留薄垫片)。 -3. **例外(单一事实来源)**:凡"进程级唯一 / 发行版级唯一"的模块,**禁止复制到 2.0**,否则破坏其唯一性不变量。此类保持为 1.x/2.0 **共用的中立叶子**,两边都 import 它(下详)。 - -> 原则 3 是对原则 1 的必要修正:`version`、`_sdk_session` 表面上也是"共享基础设施且代码极少", -> 但它们的价值恰恰在于**全局只有一份**。复制进 2.0 = 两个版本号 / 两个 session id = bug。 - -## 2. 依赖清单总表 - -| # | 2.0 引用点 | 依赖的 1.x 模块 | 规模 | 类别 | 原则 | 动作 | 风险 | -|---|---|---|---|---|---|---|---| -| D1 | `transport/_ident.py:53`、`transport/http.py:125` | `dashscope.version.__version__` | 4 行 | 单一来源 | 3 | 保持共用叶子;重构后两边仍指向同一处 | 低 | -| D2 | `transport/_ident.py:24` | `dashscope.common._sdk_session.sdk_session_id` | 43 行 | 单一来源 | 3 | 保持共用叶子(模块注释已声明此为唯一来源) | 低 | -| D3 | `v2/__init__.py:19` | `dashscope.common.api_key.save_api_key` | 函数 ~12 行 | 小型基础设施 | 1 | 在 2.0 内重写 `save_api_key`(仅文件写入) | 低 | -| D4 | `resources/chat/completions.py:17` | `dashscope.utils.param_utils.ParamUtil` | 31 行 | 小型基础设施 | 1 | 在 2.0 内重写该纯静态方法 | 低 | -| D5 | `v2/__init__.py:20` | `dashscope.tokenizers.{get_tokenizer,list_tokenizers}` | 434 行(tiktoken 封装) | 中型基础设施 | 1 / 2 | 优先随 2.0 拥有(见 §3.3) | 中 | -| D6 | `resources/_oss.py:26`(5 处调用) | `dashscope.utils.oss_utils.check_and_upload_local` | 312 行模块,与 1.x 深缠 | 中型基础设施 | 1 | 把 OSS 签名+上传逻辑移植为 2.0 原生(httpx + v2 errors) | 中 | -| D7 | `client.py:147/361`、`resources/_agentstudio.py:49/50/53` | `dashscope.agentstudio`(Client/AsyncClient/exceptions/pagination) | 8533 行,完整子 SDK | 功能级委托 | 2 | 整包移入 2.0,`dashscope/agentstudio` 改薄垫片反向引用 | 中高 | -| D8 | `resources/reinforcement_learning.py:74-76` | `dashscope.finetune.{agentic_rl, reinforcement, reinforcement.common}` | `agentic_rl.py` 685 行 + `reinforcement/` 11270 行(不含 examples 2349 行) | 功能级委托 | 2 | 仅移 `reinforcement/` 子包 + `agentic_rl.py` 入 2.0,原路径反向引用;`finetune/finetunes.py`、`deployments.py` 留在 1.x | 高 | - -## 3. 分类详解 - -### 3.1 单一事实来源(原则 3,禁止复制)—— D1、D2 - -- **D1 `dashscope/version.py`**:仅 `__version__ = "2.0.0"`,是**整个发行版**的版本号唯一来源。2.0 的 - `_ident.py` / `http.py` 用它拼 `User-Agent`。绝不能"在 2.0 内再写一份版本号"——两处会漂移。 - 重构后无论 1.x/2.0 目录怎么挪,`version.py` 应留在**中立的发行版根**(或 `dashscope/_shared/`),两边共同 import。 -- **D2 `dashscope/common/_sdk_session.py`**:进程级唯一 `x-dashscope-sdk-session-id`。模块自身注释已写明: - *"一个进程可能同时经 1.x 与 2.0 发请求,2.0 还会把 rl/agentstudio 委托进 1.x;若各自 mint id,一个逻辑会话会裂成多个 id,服务端日志分组失效。"* - 这正是我们要解耦的委托场景本身。因此它**必须保持为两边共用的叶子**,复制到 2.0 会破坏"一进程一 id"不变量。 - -**动作**:D1/D2 不参与"移入 2.0",而是确立为**中立共享层**(建议 `dashscope/_shared/`,只依赖 stdlib, -不 import `dashscope/__init__`,避免环)。这一步同时是重构后避免循环导入的锚点。 - -### 3.2 小型共享基础设施(原则 1,直接在 2.0 重写)—— D3、D4 - -- **D3 `save_api_key`**:v2 只用"写 key 文件"这一支(不碰读 1.x 全局 `dashscope.api_key` 的 `get_default_api_key`)。 - 在 2.0 内实现约 12 行(`os.makedirs` + 写文件),常量 `DEFAULT_DASHSCOPE_CACHE_PATH` / - `DEFAULT_DASHSCOPE_API_KEY_FILE_PATH` 一并带入 2.0 的 config 常量表。随后 `v2/__init__.py` 改为本地导出。 -- **D4 `ParamUtil.should_modify_incremental_output`**:31 行纯函数(按模型名判断是否关闭增量输出),无任何依赖。 - 直接在 2.0 内落一个等价函数(如 `dashscope/v2/resources/chat/_params.py`),`completions.py` 改本地导入。 - -**动作**:两者代码量都很小、无外部依赖,是原则 1 的教科书案例,可各自独立成 PR,零回归面。 - -### 3.3 中型共享基础设施(原则 1,但非平凡)—— D5、D6 - -- **D5 `tokenizers`(434 行)**:是对 tiktoken 的自包含封装,2.0 仅通过 `v2/__init__.py` 再导出 - `get_tokenizer` / `list_tokenizers` 作便利入口。它**不与 1.x 业务耦合**,更像可整体归属 2.0 的叶子。 - 建议:重构时把 `dashscope/tokenizers/` 归 2.0 拥有,原路径留再导出垫片(本质是原则 2 的轻量版); - 若短期不动,则至少确认它是纯叶子,可安全共享。**注意**:不要"复制"——tokenizer 加载/缓存应只有一份。 -- **D6 `oss_utils.check_and_upload_local`(312 行模块)**:是本次**最重的基础设施移植**。该模块 import 了 - `DashScopeAPIResponse`、`client.base_api.GetMixin`、`common.error/logging/utils`、`requests`——与 1.x 客户端机制深缠。 - v2 在 `_oss.py` 有 5 处调用(多模态本地文件上传取 OSS 凭证并上传)。 - 建议:只把**实际用到的上传路径**(`upload_file` + `check_and_upload_local` 的签名/上传逻辑,约百余行) - 移植为 **2.0 原生实现**(走 v2 的 httpx transport 取凭证、抛 `dashscope.v2.errors`),不引入 1.x 的 - `GetMixin` / `DashScopeAPIResponse`。这是"代码量不算少"的边界项,单列一个 PR,配 OSS 上传回归测试。 - -### 3.4 功能级委托(原则 2,反向搬迁 + 1.x 反向引用)—— D7、D8 - -共同前提:`agentstudio` 与 `finetune.reinforcement` **都未在 1.x 包根 `dashscope/__init__.py` 再导出** -(经核对),只通过深路径 `dashscope.agentstudio` / `dashscope.finetune.reinforcement` 访问。 -因此反向引用垫片的兼容面**只是这两条深路径**,不涉及 1.x 顶层 65 个公开名,风险可控。 - -- **D7 `agentstudio`(8533 行)**:是一个完整子 SDK,且已是"2.0 风格"(自带 `transport` / `RequestOptions` / - `APIResponse`)。2.0 侧 `resources/_agentstudio.py` 只是**透明错误对齐代理**——挂载 1.x 的 - `agentstudio.Client`,把其异常族翻译成 `dashscope.v2.errors`(设计注释明言"不改动任何 1.x 代码")。 - **动作(原则 2)**:把整个 `dashscope/agentstudio/` 迁到 2.0 归属(重构后即 2.0 顶层下的 `agentstudio/`); - 原 `dashscope/agentstudio/__init__.py` 改为**薄反向引用**(`from <2.0>.agentstudio import *` 或 `sys.modules` 别名), - 保证 `import dashscope.agentstudio` 与其深导入仍解析到同一份实现。2.0 的错误对齐代理可就地保留。 - **注意(实测补充)**:agentstudio 并非完全自包含——`client.py`/`exceptions.py`/`streaming.py`/`transport.py` - 还 import 1.x 的 `common.api_key.get_default_api_key`、`common.env.validate_region` / - `validate_workspace_id`、`common.error.DashScopeException`(其异常基类)、`common.logging.logger`、 - `common.utils.get_sdk_headers`。迁入 2.0 后这些成为对 1.x `common/` 的新增依赖;搬迁前需把这几支 - 归类为中立共享层(同 D1/D2)或一并处理,否则 §4 的递减 gate 会在 agentstudio 上卡住。 -- **D8 `finetune.reinforcement` + `agentic_rl`(合计约 11955 行运行时)**:本次**最大搬迁**。 - 2.0 的 `resources/reinforcement_learning.py:151` 经模块内 `_new_agentic_rl()` 构造 - `_agentic_rl.AgenticRL(api_key=...)`——只从 v2 client 取 `config.api_key` 字符串传入, - 不再持有 client 本体,耦合面比撰写本文时更小,业务归属仍在 2.0。 - **动作(原则 2,注意是"部分搬迁")**:只把 `dashscope/finetune/reinforcement/`(子包)与 - `dashscope/finetune/agentic_rl.py` 迁入 2.0 归属;**`finetune/finetunes.py`、`finetune/deployments.py` - 属于 1.x 根公开 API(`FineTunes` / `Deployments`),留在 1.x 不动**。原 - `dashscope.finetune.reinforcement` / `dashscope.finetune.agentic_rl` 改为反向引用垫片。 - `reinforcement/examples/`(2349 行)是样例,按仓库惯例处理(随包或移到 `examples/`),不计入运行时耦合。 - **注意(实测补充)**:`agentic_rl.py` 自身还拖带 1.x 依赖——`:10` `client.base_api.CreateMixin`、 - `:11` `finetune.customize_types`、`:17` `finetune.finetunes.FineTunes`(正是上文计划留在 1.x 的模块)。 - 迁入 2.0 会因此新增 v2→1.x 依赖,同样会卡住 §4 的递减 gate;搬迁前需先处理这三支 - (CreateMixin/customize_types 归类或重写,对 `FineTunes` 的依赖解耦)。 - -### 3.5 共享产品面:`acli`(顶层中立,1.x/2.0 都支持)—— 不属于任何一侧 - -`dashscope/acli/`(约 **36614 行**)是内嵌的交互式 AI 助手(agentic CLI),经 `python -m dashscope.acli` -启动(`acli/__main__.py` → `dashscope.acli.cli:main`),并在 `setup.py` 拥有独立可选依赖组 -(`acli` / `acli-anthropic` / `acli-openai` / `acli-voice` / `acli-camera` / `acli-all`)。 - -它的定位与 D1–D8 不同,**不是 2.0 对 1.x 的反向依赖**,而是一个**同时服务于 1.x 与 2.0 的顶层产品面**: - -- **依赖方向(实测)**:`acli` → 1.x,仅 2 处真实导入——`acli/ui/tts.py:287` 用 `dashscope.audio.tts_v2`、 - `acli/ui/voice.py:61` 用 `dashscope.audio.asr.{Recognition,RecognitionCallback}`。 - `acli` **不 import `dashscope.v2`**;1.x 包根与 2.0 也**都不 import `acli`**(仅 `v2/transport/_ident.py:22` - 有一句注释提及)。即 `acli` 是 SDK 的**消费方 / 独立入口**,不是任何一侧 API 层的组成部分。 - -**动作(布局归属)**:重构后 `acli` **保持在顶层 `dashscope/acli/` 不动**,与 `_shared`(§3.1)同属 -**中立层**——既不进 1.x legacy 桶,也不并入 2.0 实现桶。理由:它对两栈都成立,且是被安装的独立产品入口。 - -**关键约束(勿在解耦中改写 acli 内部)**:据 `dashscope/common/_sdk_session.py` 的模块注释,`acli` 是 -**agenticCLI 仓库的单向同步副本(one-way sync copy)**,连 session id 都刻意自持一份——因为任何本地改动 -都会被下次同步覆盖。因此: - -1. **不要**把 `acli` 内部作为本次解耦的重构对象(改了也会被 sync 冲掉)。 -2. 重构搬动 1.x 时,**必须保证 `acli` 依赖的两条 1.x 音频路径继续可解析**:`dashscope.audio.tts_v2`、 - `dashscope.audio.asr`。两种做法二选一——(a) 由 1.x→legacy 的兼容垫片让这两条深路径照旧解析(推荐, - 零改 acli);(b) 若确需改 `acli` 的 import,则**同步更新上游 sync 映射**,否则下次同步回退。 -3. `acli` 未来若要改用 2.0 client(它"支持 2.0"),属于 **agenticCLI 上游**的演进,不在本仓库解耦范围内。 - -## 4. 建议执行顺序(小→大,每项独立可合并) - -1. **D1/D2**:确立 `dashscope/_shared/` 中立层(version + sdk_session),两边改指此处。*(低风险,且是防环锚点)* -2. **D3**、**D4**:各自在 2.0 内重写。*(低风险,两个独立小 PR)* -3. **D5**:tokenizers 归属 2.0 + 垫片。*(中)* -4. **D6**:OSS 上传逻辑 2.0 原生化。*(中,需回归测试)* -5. **D7**:agentstudio 反向搬迁 + 垫片。*(中高)* -6. **D8**:reinforcement/agentic_rl 部分反向搬迁 + 垫片。*(高,最后做)* - -原则:每一步完成后,`grep -rn "dashscope\.<非v2>" dashscope/v2/` 的命中应**单调递减**,直到只剩 D1/D2 -指向 `_shared`。D3–D8 全部落地后,2.0 对 1.x **业务模块零依赖**,此时才具备"1.x 移入 legacy、2.0 上提顶层" -的物理搬迁条件(见 `dashscope-v2-refactor-tasks.md` / 顶层重构评估)。 - -## 5. 回归验证 gate(每步必过) - -- `python -c "import dashscope; import dashscope.v2"` 无循环导入、无告警。 -- `pytest tests/v2` 全绿;涉及 D6/D7/D8 时补对应回归(OSS 上传、agentstudio 挂载与错误翻译、RL 构造)。 -- 反向引用垫片生效:老深路径 `import dashscope.agentstudio`、`from dashscope.finetune.reinforcement import ...` - 仍解析到迁移后的**同一份对象**(`is` 一致),不产生双实现。 -- `x-dashscope-sdk-session-id` 在"同进程同时用 1.x 与 2.0"下**仍为同一 id**(守护 D2 不变量)。 -- `python -m dashscope.acli --help` 可启动;`acli` 依赖的 `dashscope.audio.tts_v2`、`dashscope.audio.asr` - 两条深路径在重构后**仍可解析**(守护 §3.5 中立层约束);`acli` 保持在顶层 `dashscope/acli/` 未被搬动。 -- 全仓 secret 扫描通过,版本号仅一处(D1)。 diff --git a/md/dashscope-2.0-advantages-report.md b/md/dashscope-2.0-advantages-report.md deleted file mode 100644 index 5a6b1371..00000000 --- a/md/dashscope-2.0-advantages-report.md +++ /dev/null @@ -1,569 +0,0 @@ -# dashscope 2.0 差异化优势分析报告 - -> 基于 `md/dashscope-2.0-design.md`(§0 / §2.2 / §2.7 / §3 / §4 / §5)与三份对比调研 -> (`sdk-comparison-openai-vs-dashscope.md`、`sdk-comparison-anthropic-vs-dashscope.md`、 -> `dashscope-2.0-interfaces-mapping.md`)收敛。 -> 状态:**活文档**。本报告不停留在自我背书——每条优势都要在实现级文档里验真(见 §7)。 -> **约定:后续围绕这些结论的讨论,一律沉淀进本文档**(新增验真节 / 修订对应章节 / -> 补入待探究清单),不只在对话里口头结论。 -> 验真依据:`md/dashscope-2.0-interfaces-examples.md`(实现级调用示例,反映 `dashscope/v2/` 现状)。 - ---- - -## 0. 结论速览 - -dashscope 2.0 的差异化不在"比 OpenAI/Anthropic 更现代"——现代基线(Client 实例化、 -Pydantic、异常驱动、自动重试、httpx、流式人体工学)是**追平**,不是超越(设计原则 P1: -对齐行业基线、不做发明)。 - -真正的护城河是两条正交的轴: - -- **轴一「一个 SDK 管全部」= 横向覆盖面**:一个 Client 覆盖(模型模态 × 协议 × provider)。 -- **轴二「全链路长任务」= 纵向生命周期**:files → 微调 → 部署 → RL → agentstudio 的生产链路, - 其中耗时环节由统一的 `jobs.create/fetch/wait` 长任务模式支撑。 - -OpenAI/Anthropic 基本只占「轴一的文本那一格 + 单次推理调用」,既无协议通吃,也无平台链路。 - -> **验真后修正(详见 §7)**:两条轴里有的点是真护城河,有的点被夸大。逐点验真后, -> **真正扛得住的**是「长任务统一模式 + 错误可定位性 + files→微调→部署 这段链路」; -> 而「协议通吃」「全链路统一体验」「两行 quickstart」三个卖点都有**实现级硬边界** -> (协议特有参数切不动 / agentstudio 未对齐 v2 / `workspace` 强制),从客户友好维度 -> 不能照单全收。下面各章是"宣称",§7 是"验真",两者对照读。 - ---- - -## 1. 先分清两类能力:追平基线 vs 差异化 - -设计原则 P1 明确:以下都是 OpenAI/Anthropic(同为 Stainless 生成)**已验证的基线**, -2.0 只是把 1.x 的短板补齐,**不构成相对两家的优势**: - -| 追平项 | 1.x 现状 | 2.0 落点 | -| --- | --- | --- | -| Client 实例化 | 全局变量 `dashscope.api_key`,多租户易错 | `dashscope.Client(...)` 资源式 | -| 异常驱动错误处理 | 返回值 `if status_code != 200`,漏检即静默失败 | 9 个扁平异常类 + 目录驱动(§2.7) | -| 自动重试 | 核心路径无重试 | `max_retries=2` + Retry-After + 指数退避 | -| 类型化响应 | `**kwargs` 透传、无 IDE 提示 | Pydantic + `@overload` + `.pyi` | -| 流式人体工学 | 手设 `incremental_output` + 深层路径 | `stream.text_stream` / `get_final_message()` | -| 便捷属性 | `resp.output.choices[0].message.content` 四层 | `resp.output_text` 一行 | -| httpx 统一栈 | requests + aiohttp 双栈 | httpx 同步/异步共用、连接池复用 | - -> **诚实提醒**:工程成熟度上两家仍领先——Anthropic 的 `@beta_tool` 装饰器 + `tool_runner` -> 自动工具循环、spec 自动生成流水线(DashScope codegen 要到 M6 才接管)、生态与社区。 -> 2.0 的定位是「追平 + 错位竞争」,不是全面超越。 - ---- - -## 2. SDK 易用性 / 出错可定位:相对现状 - -这两项是调研评分中 DashScope 差距最大的维度,也是 P2「契约正确性优先于功能数量」要修的 -"最高危静默错误"。下面给"现在相对处在什么位置"。 - -### 2.1 易用性(usability) - -| 阶段 | 状态 | 相对竞品 | -| --- | --- | --- | -| 1.x | 三套调用风格并存(`Generation.call` / `AioGeneration.call` / `ImageSynthesis.async_call`)、全局变量配置、参数散落 `**kw`、响应路径四层、流式需手设 `incremental_output` | **代际落后** | -| 2.0 | 资源式 Client、`output_text` 便捷属性、`stream.text_stream`/`get_final_message()`、`@overload` 流式/非流式静态可分、Quickstart 两行上手 | **基线齐平** OpenAI/Anthropic | - -- 结论:从"代际落后"→"基线齐平"。 -- **仍差一点**:工具调用人体工学——Anthropic 有 `@beta_tool`(从函数签名+docstring 自动生成 - schema)和 `tool_runner`(自动跑"调用→回填→再问"循环),2.0 目前仍需手写 schema、手动解析 - `tool_calls`、手动循环。这是易用性上唯一明显未追平的点(见待探究清单)。 - -### 2.2 出错信息可定位(error locatability / 可运维性) - -| 维度 | 1.x 现状 | 2.0 落点 | 出处 | -| --- | --- | --- | --- | -| 错误暴露方式 | 返回值式,漏写 `if` 即**静默失败** | 异常驱动,9 个扁平状态类 | §2.7 | -| 异常携带信息 | `status_code`/`code`/`message` 分散 | 统一带 `status_code / code / message / **request_id** / body / response.headers` | 设计 §2.2 line 131 | -| request_id | **日志缺 request_id**,无法定位到工单 | request_id 入日志 + 挂异常,**排障可定位到工单级** | §2.2 日志 | -| "200 但业务失败" | 网关特性,易漏检 | 三层来源一个异常族(HTTP 层 / 业务层 200 带 code / SDK 层) | §2.7 | -| 错误码 | `"Throttling"`/`"InvalidApiKey"` 裸字符串 | 可 import 的 `ErrorCode` 枚举(13 项线缆码)+ 目录派生类名/状态码 | §2.7 | -| 敏感信息 | 无过滤,**API Key 有泄露入日志风险** | `SensitiveHeadersFilter` 过滤 Authorization | §2.2 日志 | -| 排障开关 | 无统一入口 | `DASHSCOPE_LOG=debug` 一键开启,记录状态码/重试原因/等待时间 | §2.2 日志 | -| 流中出错 | 被吞、不退出 | 统一抛异常,不 yield 错误对象 | §2.2 流式 | -| CLI 退出码 | **流式出错仍 exit 0**(CI 误判成功) | 退出码分级 0/1/2/3/4 + 流式中途出错 exit 1 | §4.2 | - -- 结论:可定位性是 2.0 投入最重的维度——`request_id` 全链路贯通(日志 + 异常 + 响应模型 - `ResponseBase`)、目录驱动的错误码、敏感头过滤,对齐 Anthropic 的 `_request_id` 与异常层级, - 把"静默错误"这一唯一会导致用户数据/CI 结果错误的类别系统性堵死。 -- 相对竞品:**齐平**(异常细分对齐两家,request_id 对齐 Anthropic,目录驱动的错误码枚举甚至比 - 手写更不易漂移)。 - ---- - -## 3. 轴一:「一个 SDK 管全部」——横向覆盖面 - -"全部"是**三层覆盖**,任意单层拎出来 OpenAI/Anthropic 都不全: - -| 层 | 含义 | 两家官方 SDK 的缺口 | -| --- | --- | --- | -| **模型模态全** | 文本 / 图像 / 视频 / 语音(ASR+TTS+realtime) / embedding / rerank / tokenization | 主要是 chat + 少量模态 | -| **协议全** | `protocol="native" \| "openai" \| "anthropic"`,一个 Client 说三种协议 | `openai` 只会说 OpenAI 协议 | -| **provider 全** | 靠协议层,第三方模型(DeepSeek/Kimi/MiniMax…)零成本接入,无需厂商专属 adapter(§3.3) | 只连自家 | - -第 2、3 层是相对两家官方 SDK 最硬的差异:把「模态 × 协议 × provider」用一个 Client 兜住。 - -### 3.1 亮点:多模态 content 格式 - -`{"text":..., "image":...}` 比 OpenAI 的 `{"type":"input_text",...}`、 -Anthropic 的 block 结构更直观(两份对比的「🟢 亮点」一致认可)。 - ---- - -## 4. 轴二:「全链路长任务」——纵向生命周期 - -**注意概念关系**:「全链路」是范围,「长任务」是执行模式,二者不是并列。 - -### 4.1 全链路 = AI 平台的完整生产流程 - -``` -files(上传数据) → fine_tunes(微调) → deployments(部署) → rl(强化学习) → agentstudio(托管 Agent) -``` - -- 不止"调模型推理",而是覆盖训练到上线到 Agent 托管的整条链路。 -- 对应 §4 的战略定位:OpenAI 已于 2026-05 删除 API CLI、Anthropic 从未有过, - "AI 平台长流程管理"品类只剩 DashScope。 - -### 4.2 长任务 = 支撑链路耗时环节的统一异步模式 - -``` -jobs.create → fetch → wait # 统一 Job 对象:id / status / result / error;wait 内置退避轮询 -``` - -覆盖:images / videos / audio.asr 批量 / embeddings 批量 / fine_tunes / rl。 -因为这些操作都耗时、不能同步等,2.0 把它们抽象成同一套模式—— -**长任务是"全链路"里慢操作的底层机制,不是与微调、agentstudio 并列的第三样东西**。 - ---- - -## 5. 协议合并的价值与前提 - -### 5.1 痛点:用户在两协议间切换成本高("协议碎片化税") - -OpenAI 与 Anthropic 协议看着像、实则处处不同,同时用两家要维护两套: - -- 两个 SDK / 两份依赖、两套 Client 初始化; -- 两套异常层级(`except openai.RateLimitError` 与 `except anthropic.RateLimitError` 是两个类); -- 两种流式惯用法(Anthropic `text_stream`/`get_final_message` vs OpenAI 遍历 chunk 自累积); -- 工具定义不同(OpenAI `functions`/`tool_calls` ↔ Anthropic `input_schema`/`tool_use`/`tool_result`); -- 消息结构不同(Anthropic `system` 是独立顶层字段,OpenAI 是 messages 里的 role); -- SSE 事件格式、采样响应形状不同。 - -模型能力可互换,但**接入代码不可互换**——每加一家 provider 就多一套错误模型、流式写法、工具 schema。 - -### 5.2 dashscope 的解法:一套接口,协议翻译收敛进 adapter - -```python -client = dashscope.Client(protocol="openai" | "anthropic" | "native", base_url=...) -resp = client.chat.completions.create(model=..., messages=..., tools=..., stream=True) -``` - -- 一套异常族、一套流式 API、一套 Pydantic 类型、一套重试、一套配置;上层无感。 -- 写一次代码,靠 `protocol=` / `base_url=` 换 provider,碎片化税被消掉。 -- `strict=False` 默认透传未知参数,厂商新参数(reasoning/thinking)不被 SDK 发版节奏卡住。 -- native 协议仍能拿到 dashscope 全量能力,这是纯 openai/anthropic SDK 给不了的。 - -### 5.3 优势成立的三个前提(方案已点) - -1. **最小公分母风险**:统一接口不能丢各家特有能力,否则"通吃"变"通弱"。 - 兜法:`extra_body` 逃生舱 + 高频参数显式化(`enable_thinking`/`reasoning_effort`)(§3.3)。 -2. **协议层只覆盖 HTTP 文本类**:ws 实时接口(Recognition/OmniRealtime)在两家无对应物,统一不了,需文档明示(§7 风险)。 -3. **价值对谁最大**:对"走百炼网关 / 多 provider 混用 / 需要全链路"的用户价值最高; - 纯单一官方场景,用户直接用 `openai` SDK 更原生——卖点不是替代单家官方 SDK。 - ---- - -## 6. CLI:对 toB 的成本 & 是否"只给大模型 function call" - -### 6.1 先纠正定位:CLI 首要不是给大模型用的 - -设计 §4 写得很清楚——`dashscope` CLI 的对标物是 **Codex cloud + aws cli 的混合体**, -**明确不与 Agent 类 CLI 比对话体验**(那是 `dashscope expert` / acli 的赛道)。它的存在理由是 -把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成命令,首要受众是: - -- **人类运维 / 平台管理员**:手动跑长流程、查任务状态; -- **CI/CD 脚本**:`--output json`(NDJSON 可 jq)+ 退出码分级(0 成功 /1 服务端 /2 认证 /3 参数 /4 限流)+ `auth whoami` 退出码探测,脚本免解析 stderr。 - -`chat create` 这类"一条 curl 就能替代"的调用只是便捷入口,不是重点。 - -**真正给大模型/agent 用的是 `dashscope expert`**(agentic CLI),而且 expert 是把 SDK 资源 -**直接注册成 tools/skills**(§5.2),让 agent 一句话调全量能力——**不是 shell out 去调 CLI**。 - -### 6.2 那"能被大模型 function call 调用"对不对?——对,但是副产品 - -CLI 因为 `--output json` + 分级退出码,**天然对 agent 友好**:LLM 通过 bash/function-calling -工具调它,能拿到结构化输出和可靠的成功/失败信号。所以"能被大模型调用"成立, -但这是**设计属性带来的次要受众**,不是 CLI 存在的理由。把 CLI 说成"完全是给大模型 function -call 用"会混淆三条线:CLI(人+CI)、expert(agent 对话)、SDK tools(agent 直接调能力)。 - -### 6.3 对 toB 用户的成本:基本零边际成本 - -| 成本维度 | 情况 | -| --- | --- | -| 安装 | CLI 与 SDK **同包、同版本号发布**(§2.4 规则 13),不做双包分发;装 `dashscope` 就带 CLI | -| 只用 SDK 的 toB | 不碰 CLI = **零成本**,CLI 不强加任何东西 | -| 存量脚本 | 新旧命令树并存,隐藏别名保留(`fine-tunes`→`ft`、`agentic-rl`→`rl`),**不破坏现有自动化** | -| 共存期负担 | 成本主要落在**厂商侧**(维护 `cli/` 单命令树 + 旧命令兼容映射层),不是用户侧 | -| toB 收益 | 长流程(微调/部署/批处理)可脚本化、可进 CI、退出码可靠——这正是 toB 运维需要的 | - -结论:对 toB,CLI 是"已经在包里、用不用随你、用了能把长流程自动化"的低成本工具; -共存期旧命令兼容层的维护成本由 DashScope 自己承担,不外溢给用户。 - ---- - -## 7. 护城河验真(逐点 / 客户友好维度) - -依据 `md/dashscope-2.0-interfaces-examples.md`(实现级示例,反映 `dashscope/v2/` 现状), -对 §3–§6 的每条宣称逐点验真。图例:✅ 真优势 | ⚠️ 打折 / 有硬边界 | ❌ 被夸大。 - -### 7.1 轴一:一个 SDK 管全部 - -**1a 模型模态全 —— ✅ 真优势** -文本 / 多模态 / 图像 / 视频 / asr / tts / realtime / 文本向量 / 多模态向量 / 批量向量 / -rerank / tokenization 全部落地(示例 §1–4)。客户友好成立:动词收敛(`create` / `generate` / -`jobs.*`)、`Usage`/`index`/分页字段全 SDK 统一、响应信封一律扁平化(不写 `.output.`)。 -- ⚠️ 代价:为"广而一致",命名评审花大力气消灭同义词(design §2.6),且仍有裂缝(见 2c)。广度不免费。 - -**1b 协议全「写一次代码换 protocol」—— ⚠️ 被夸大,有硬边界** -示例 §0.3 参数支持矩阵显示同一套代码在三协议下**不等价**: - -| 参数 | native | openai | anthropic | -| --- | --- | --- | --- | -| `seed` / `presence_penalty` / `response_format` | ✅ | ✅ | **❌ 无条件 400** | -| `enable_thinking` / `repetition_penalty` | ✅ | 🔀 透传 | **❌ 无条件 400** | - -- 用了 `enable_thinking` / `response_format`,**切到 anthropic 直接 400**,"一套代码通吃"只在 happy path 成立。 -- `max_tokens` 在 anthropic 缺省被**自动补 4096**(§0.2)——隐藏行为。 -- 辩证:anthropic 选"响亮 400"而非"静默丢弃"(§0.3 注释:假成功更糟),**设计取向是对的、友好的**; - 但它同时坐实"协议通吃"是**有漏的抽象**——客户仍须知道"哪个参数在哪个协议活着",换 provider 非零成本。护城河比宣称的浅。 - -**1c provider 全 —— ⚠️ 部分成立** -第三方模型靠协议层接入(§3.3),但受 1b 同一约束:厂商特有参数(reasoning/thinking)只能走 -`extra_body` 逃生舱,**非一等公民**;`models.list(provider=)` 是**客户端匹配**(§5.3),非服务端能力。能用,但不是"零成本通吃"。 - -### 7.2 轴二:全链路长任务 - -**2a 长任务 `jobs.create/fetch/wait` —— ✅ 真护城河(最硬一条)** -统一 `Job` 对象 + 类型化视图(`image_urls`/`video_url`/`result_url`/`results`,示例 §2.2)+ -一步式 `generate` vs 分步 `jobs.*` 双形态 + 内置退避轮询 + 状态枚举 + FAILED 抛异常。 -OpenAI/Anthropic **无此概念**。客户友好完全成立:省掉自写轮询/状态机/累积的全部样板。 - -**2b 全链路覆盖 —— ✅ 覆盖真实存在** -files → fine_tunes → deployments → rl → agentstudio 五环都在(示例 §5/§6/§7),非 PPT。 - -**2c 全链路「统一体验」—— ❌ 末梢断裂** -"全链路"是**覆盖意义上的真、体验意义上的假**: -- **agentstudio(最后一环)是二等公民**(示例 §7):返回 1.x 模型、**不扁平化**(仍 `output` 信封)、 - `except DashScopeError` **抓不到**,只能 `except Exception`。客户学会 v2 的信封+错误规则后到 agentstudio 全部失效—— - **严重不一致**。design §2.7 / M3-3.12 自承"还没做翻译,流式路径尤其漏网"。 -- **rl(倒数第二环)半 solid**(示例 §6):错误已翻成 `SdkError`(补救到位),但 4 个直通方法 - **不接受 `timeout=`**,违反"所有方法都支持 timeout"的承诺。 -- 结论:护城河在 `files→ft→deploy` 这段最实,`rl` 打折,`agentstudio` 是缺口。 - -### 7.3 横切:一个反向摩擦点(削弱"易用性追平") - -**`workspace` 现在是强制的**(示例 §0 + 代码 `dashscope/v2/_config.py`):默认端点全走 -workspace 专属网关 `https://{workspace}.cn-beijing.maas.aliyuncs.com`,共享网关已下线, -没给 `workspace` 又没 `base_url` 直接 `ValueError`。 - -**代码级事实(`_config.py`)**: -- `resolve_config`(line 415-424):无显式 `base_url` 时调 `_protocol_default_base_url("native", workspace)`; -- 该函数 `if not workspace: raise ValueError`(line 120-121),**无任何回退分支**; -- 老共享网关 `dashscope.aliyuncs.com` 在 v2 **彻底不引用**(docstring line 11-12 / 注释 line 36-38)。 - -**是否自动退回 1.x 老网关?——不会,且不该**: -- 行为是「无 base_url + 无 workspace → 立刻 `ValueError`」,不是静默回退老网关。 -- 静默退回另一个网关会**掩盖配置错误**(用户以为用新网关、实际打到别处),比响亮报错更糟。 - 当前 `_WORKSPACE_REQUIRED_MSG` 给了 3 种修法 + `base_url` 逃生口,fail-loud 设计是对的。 -- ⚠️ **已核实(官方文档纠正代码注释)**:`_config.py` docstring/注释多处称老网关 "has been retired", - 但官方 [Model Studio 文档](https://docs.modelstudio.console.alibabacloud.com/zh/model-studio/qwen-api-via-dashscope) - 明确:**老共享网关仍可正常使用、无下线时间表**,官方只是"推荐迁移到新网关(性能更卓越、稳定性更高)"。 - 故准确表述应是"v2 默认不再引用老网关",而非"retired"——**这是会误导维护者的注释 bug**。 - 连带结论:① 1.x 共存期矛盾解除(老网关没停,1.x 打 `dashscope.aliyuncs.com` 仍可"逐字节不变"); - ② **workspace 强制是 SDK 自我设限、非服务端逼迫**——老网关无需 workspace 且官方仍支持,v2 本可回退而非抛错。 - -**新老网关官方对照(来自上述文档)**: - -| 地域 | 新网关(需 workspace id) | 老共享网关(无需 workspace) | -| --- | --- | --- | -| 北京 | `{ws}.cn-beijing.maas.aliyuncs.com` | `dashscope.aliyuncs.com` | -| 新加坡 | `{ws}.ap-southeast-1.maas.aliyuncs.com` | `dashscope-intl.aliyuncs.com` | -| 香港 | `{ws}.cn-hongkong.maas.aliyuncs.com` | `cn-hongkong.dashscope.aliyuncs.com` | -| 美东/德/日 | `{ws}.us-east-1 / eu-central-1 / ap-northeast-1.maas.aliyuncs.com` | — | - -官方建议:推荐迁移新网关("卓越的性能和更高的稳定性");老域名仍可用,**无明确下线时间表**。 - -**对"是否该支持老网关"的修正建议(分层,替代原"不该静默回退")**: -- 给了 `workspace` → 走新网关(官方推荐正解,默认路径); -- 没给 `workspace` 也没 `base_url` → **回退老共享网关 `dashscope.aliyuncs.com` + 一条 info/DeprecationWarning** - ("建议配 workspace 以获得更优性能"),**而非直接 `ValueError`**。这样"两行 quickstart"就真能像 OpenAI 只靠 api_key 跑, - 友好度痛点消除,同时用 warning 把用户往新网关引导,不违背官方迁移导向。代价仅是老网关性能略差,且是用户自选。 - -**附带发现:地域硬编码**。v2 把网关写死 `cn-beijing`(`WORKSPACE_GATEWAY_TEMPLATE`),但官方有 **6 个地域**; -新加坡/美东/欧洲/日本用户**必须手拼 `base_url`**,无 `region=` 参数——又一处友好度缺口。 - -**客户友好度:确实是痛点(不是追平)**: - -| 对比 | OpenAI/Anthropic | dashscope 2.0 | -| --- | --- | --- | -| 上手所需 | `api_key` | `api_key` **+ workspace** | -| 缺配置时 | 能跑 | `resolve_config` 构造期直接 `ValueError` | -| "两行 quickstart" | 真两行 | `dashscope.chat(...)` 未设 `DASHSCOPE_WORKSPACE` 即抛错 | - -- workspace-id 是要额外去控制台找的概念,且报错发生在**构造期**而非首次请求,首屏直接劝退; - design §2.5 "对齐 OpenAI 5 行上手"被这一关打折。 -- 叠加易用性唯一明显未追平的 tool 自动循环(无 `@tool` / `tool_runner`),§2.1 的"基线齐平"应下调为 - **"接近齐平,有两处摩擦(workspace 强制 + 无 tool 自动循环)"**。 - -**可改善方向(都不破坏 fail-loud)**: -1. **单 key 自动探测 workspace**:一个 key 只对应一个 workspace 时,SDK 调"列 workspace"接口自动填,用户无感——对友好度提升最大。 -2. **报错延后到首次请求**:构造 Client 不炸,真正发请求才报错,import/构造阶段更宽容。 -3. **Quickstart 文档诚实化**:首屏就写 `DASHSCOPE_WORKSPACE`,别宣传会抛错的"两行示例"。 -4. 老网关若支持,只做**显式 opt-in**(`base_url=`),绝不静默回退。 - -### 7.4 验真汇总表 - -| 点 | 宣称 | 验真 | 客户友好维度 | -| --- | --- | --- | --- | -| 模态全 | 全覆盖 | ✅ 真 | 一致性好,靠大量命名治理换来 | -| 协议通吃 | 写一次换 protocol | ⚠️ 打折 | happy path 友好;协议特有参数切不动,漏抽象 | -| provider 通吃 | 第三方零成本 | ⚠️ 打折 | 靠逃生舱,非一等公民 | -| **长任务 jobs** | 统一异步模式 | ✅ **真护城河** | 最省心,两家没有 | -| 全链路覆盖 | 五环齐全 | ✅ 真 | — | -| 全链路统一体验 | 一致 | ❌ 末梢断裂 | agentstudio 破坏一致性,rl 半折 | -| 两行 quickstart | 对齐 OpenAI | ⚠️ 打折 | workspace 强制 = 上手摩擦 | -| 错误可定位 | request_id 全链路 | ✅ 真优势 | 见 §2.2,工单级定位,对齐 Anthropic | - -**一句话**:真正扛得住的护城河 = **长任务统一模式(2a)+ 错误可定位性(§2.2)+ files→ft→deploy 链路**; -"协议通吃""全链路统一""两行上手"三个卖点都有实现级硬边界,不能照单全收。 - ---- - -## 8. 战略发散:从 SDK 到 toB 研发套件 & AI coding 下的护城河重估 - -> 本节是发散性战略判断,非现状描述。触发问题:① SDK 能否升级为 toB 研发套件、突破"仅 HTTP 包装"? -> ② AI coding 下调用与代码生成都变容易,SDK 还剩哪些护城河,是否大模型就搞定了? - -### 8.1 价值阶梯:SDK → 套件 = 往上爬 - -"HTTP 包装"只是阶梯最底层。变成套件 = 从"封装一次调用"升级到"拥有调用周围的生命周期与运维"。 - -| 层 | 内容 | 护城河强度 | -| --- | --- | --- | -| L0 HTTP 包装 | requests → typed call | ❌ 已被 LLM 免费化 | -| L1 正确性契约 | 错误目录 / 重试 / 幂等 / request_id | ⚠️ 半成品(§2.2 已建) | -| L2 工作流编排 | jobs 轮询 / 批量 / 状态机 | ✅ "全链路"轴(§7.2a 已验真) | -| L3 本地研发工具 | CLI / config / auth / **录制回放** / mock 网关 / fixture | ✅ toB CI 刚需 | -| L4 可观测 & 运维 | OTel trace / 成本归因 / 配额 / 审计 | ✅ 企业刚需,LLM 生不出 | -| L5 质量闭环 | prompt 版本化 / 回归 eval / 护栏 / 模型迁移安全 | ✅ 高粘性 | -| L6 集成面 | 框架适配(LangChain/Dify/Spring) / **MCP server** / Terraform | ✅ AI 时代入口 | -| L7 平台运行时 | 托管 agent / session / memory | ✅ 但重 | - -**结论:toB 愿付费的是 L3–L6,不是 L0。** - -### 8.2 可打造的 toB 开发者工具(L3–L6) - -1. **录制回放 / mock 网关**(`dashscope dev record/replay`):CI 跑集成测试不烧 quota、不被限流抖动卡住; - 录真实流量 + 确定性回放,是运行时服务,LLM 生成不出来。 -2. **模型路由器 / 可靠性中间件**:跨模型/provider/地域的 fallback、熔断、负载均衡、限流感知—— - 把 §3 的"多协议能切"升级成"自动容灾",企业生产真痛点。 -3. **成本与可观测中间件**:OTel 一等公民 + 按团队/功能的 token 成本归因 + 配额预算强制(FinOps)。 -4. **MCP server**:把 SDK 资源暴露成 MCP tools,让 IDE/agent 原生调 dashscope——AI coding 时代的"新 README"。 -5. **IaC(Terraform provider)**:finetune/deployment 声明式管理。 -6. **迁移 codemod**:1.x→2.0、OpenAI→dashscope 自动改码,直接降切换成本,是获客武器。 - -> 设计里已有种子(codegen M6 / `dashscope serve` 入站兼容 / expert 注册 tools / CLI §4), -> 应把它们从零散 feature 提升为连贯的"toB 开发者平台"叙事,并补上最缺的 **L4/L5(运维与质量)**。 - -### 8.3 AI coding 下,护城河重估 - -**LLM 确实 commoditize 掉的**:把文档变成一次能跑的调用(L0)、生成解析/重试样板(L1 ergonomic)。 -一次性原型里 dev 把文档贴给 agent 就出代码,SDK 的"上手友好"价值被削弱。 - -**LLM 搞定不了、护城河仍在的**: - -1. **运行时 ≠ 代码生成**:SDK 是被维护、测试过、带版本的**运行时依赖**(连接池 / websocket 生命周期 / - 流式背压 / 后台轮询 / 重试状态机);LLM 产物是一次性、无版本、无测试的。生产不接受"大模型写的"。 -2. **正确性与信任**:LLM 幻觉参数/端点/错误码;目录驱动错误模型、"HTTP 200 但业务失败"双层检查、 - request_id 贯通——正是 LLM 最易写错处。SDK 是对真实服务端验证过的 ground truth。 -3. **AI coding 反而抬高好 SDK 的价值**(关键反转):对 agent 最友好的不是裸 HTTP,而是**强类型 SDK**—— - Pydantic + `.pyi` 是 agent 防幻觉的机器可读契约。裸调用 agent 更易错。 -4. **有状态 / 长任务生不出**:jobs 轮询、ws 会话、批量编排——codegen 产调用点,产不出持久机器 - (断点续传尚待建,§8.6)。 -5. **运维 / 治理横切面**:成本归因 + tracing + 配额 + 审计 + 多 provider 容灾,LLM 片段接不好更不会维护。 -6. **版本与迁移契约**:服务端 API 变,SDK 在稳定面后吸收 + 出 codemod;LLM 裸调用静默失效。 -7. **分发与供应链**:pip / semver / 安全补丁 / CVE 响应 / 签名发布——LLM 不能当依赖管理。 - -**最锋利的重构**:AI coding 时代,**SDK 的"用户"越来越多是 agent,不是人**。"开发者友好"部分变成 -"**agent 友好**":富类型、MCP server、确定性错误、幂等、自描述 spec(`llms.txt` / OpenAPI)。 -护城河从"transport 好不好用"转移到"**我是不是那个最可信、最机器可读的契约 + 能可靠执行它的运行时**"。 - -### 8.4 对本报告结论的冲击 - -§7 把"易用性追平 OpenAI"当卖点——**在 AI coding 语境下这条贬值最快**(LLM 抹平 ergonomic 差距)。 -真正抗 LLM 侵蚀的是:**L2 长任务运行时(§7.2a 已验真)、L1 正确性契约(§2.2 错误可定位)、多协议契约、 -以及尚未建的 L4/L5 运维质量层**。 - -> 投入建议:从"更漂亮的 HTTP 包装"转向"**最 agent-legible 的契约 + LLM 生不出来的运行时与治理**"。 - -### 8.5 架构取向:核心做薄做硬,能力做厚做可拆 - -"做套件"不等于"把 SDK 做厚"。这里有一条容易走偏的分界: - -- **做胖客户端(要避免)**:把录制、mock、路由、成本统计、本地运行全塞进 `Client` 对象, - 变成谁都得装、多数场景用不上的巨石。代价是耦合、版本漂移、维护面爆炸——且在 AI coding 下, - 纯调用层本就最先被 LLM 商品化,把重量加在最易贬值处是反的。 -- **薄核 + 外挂(推荐)**:`Client` 仍是一份干净**契约**(typed request/response、确定性错误、 - request_id 贯通),§8.2 的厚度全部挂在它周围、按需 opt-in(mock 网关 / 录制回放 / 成本与可靠性 - 中间件 / 本地 `serve` / 迁移 codemod / MCP server),彼此可拆、可独立版本化。 - -一句话:**核心做薄做硬,能力做厚做可拆。** 因此"升级为套件"更准的定义不是"护理开发环境的胖对象", -而是**从"API 包装"变成"运行时 + 契约 + 治理"**——且厚度必须落在 §8.3 列的、**LLM 生成不出来的地方** -(运行时行为、正确性与信任、治理/合规、有状态长任务),而非落在大模型随手能写的漂亮封装上。 - -### 8.6 用户四维度:好用 / 经济 / 效果 / 稳定 - -toB 客户评估"模型平台 / SDK"的真实打分表是这四维。逐维看"用户要什么 → SDK 能做厚到哪 -(LLM 生不出的部分)→ 代价",结论收敛到 §8.5 的薄核+外挂。 - -#### 经济:从"能调"到"知道钱花哪了 + 有办法省" - -用户要的不是便宜,是**可归因 + 可优化**。SDK 是每一次调用的必经点,`usage` 天然可见—— -这是成本归因唯一正确的插桩点,LLM 生成调用代码却看不到运行时的账。 - -**一次调用的 token 去向可拆为**:system prompt(**每轮重发**)、对话 history(**越聊越贵**)、 -RAG/context 注入、用户当前 message、**tool/function schema(每次调用都重发)**、多模态输入折算、 -输出侧可见 `content` vs **`reasoning_content`(思考 token,thinking 模型下常是答案数倍且照计费)**、 -**重试消耗的重复 token**、流式中断仍计费部分。 - -> ⚠️ 工程真相:服务端 `usage` 给**总数 + 有限明细**(reasoning/cached token 计数,v2 已透传保留, -> 输出侧 thinking 占比可直接读明细算)。要拆到 system/history/tool 的**输入侧**分段, -> 仍需 SDK **发请求前对 payload 分段各自 tokenize 估算**(客户端侧计数),或推动服务端返回明细。 - -**更经济的办法(SDK 可落地甚至可强制)**:① context cache 命中率 & 节省;② history 裁剪/摘要中间件; -③ 模型级联路由(turbo 兜底、max 攻坚,即 §8.2 路由器);④ batch 离线折扣;⑤ thinking 开关护栏; -⑥ tool schema 精简/按需注入;⑦ 语义去重缓存。 - -**产品形态**:`dashscope dev cost` 或 OTel cost span—— -> "本次运行 X token ≈ ¥Y:重复 system prompt 占 55%,reasoning_content 占 30%,真正答案只占 15%。" - -这种洞察 LLM 生不出(要运行时插桩 + 真实计价)。**代价**:计价表会变(→ SDK 只吐计数,计价可配置/远程拉取)、 -热路径插桩加延迟(→ 采样)、客户端分段 tokenize 是估算非精确。 - -#### 稳定:SDK 最原生、最该赢的维度 - -用户要"别抖、别丢、超时可控、失败可预期、长任务不断"。**已建**:连接池、ws 生命周期、流式背压、 -`submit_timeout` vs `wait` 预算分离、`max_retries` 退避(429 读 Retry-After)、目录化确定性错误。 -**可做厚(LLM 生不出)**:可靠性中间件——跨模型/地域 fallback、熔断、按模型/地域维度的 429 感知调度、 -写操作幂等键(client 生成 idempotency key,重试不重复创建)、长任务断点续传, -把 §3"多协议能切"升级成"自动容灾"。**代价**:部分企业要把容灾放网关/mesh → 按 §8.5 做 opt-in 外挂。 - -#### 效果:窄切、别越界 - -用户要"输出对、参数真生效、结构化输出可靠、换模型能验证变好没"。**高杠杆窄切**: -① **参数正确透传**——示例 §0.3 矩阵显示协议特有参数的两种失效形态:`enable_thinking/repetition_penalty` -在 openai 只是透传(可能不生效,静默)、`response_format/seed` 等五个参数在 anthropic 直接 400(响亮) -——薄包装错映射即效果事故;② **结构化输出可靠性**——`response_format` 解析 + schema 校验 + -不合格自动重试;③ **eval 回归(L5)**——prompt 版本化 + 对**录制 fixture** 跑离线 eval + 跨模型 A/B。 -**代价**:效果主要是模型的活,SDK 越界做全套 eval/agent 编排 = 大工程且撞 LangChain → **只取这三片**。 - -#### 好用:AI coding 下贬值最快的一维 - -呼应 §8.4:ergonomic"好用"是 LLM 最先抹平的。需**重定义"好用给谁"**——给**人**(quickstart、路径浅)贬值中, -别过度投入;给 **agent**(强类型 `.pyi`、MCP、确定性错误、幂等)升值中;给**运维**(config/auth/observability 一致)刚需。 - -#### 四维互相打架,SDK 是把权衡"显式可调"的地方 - -- 经济 ↔ 效果:便宜模型 / 裁 context / 关 thinking ↔ 质量 -- 经济 ↔ 稳定:重试和 fallback 都烧 token -- 好用 ↔ 稳定:裸透传省事但脆,强类型+校验要配置 - -把权衡做成**策略旋钮**(路由器/中间件:"质量下限之上最省" / "预算之内最稳")才是"套件"的产品化形态, -也是真护城河:**LLM 能生成一次调用,但生成不出带策略、带账、带容灾的运行时**。四维中 -**经济/稳定/效果抗 LLM 侵蚀**(要运行时、真实计价、插桩),**好用被侵蚀**——再次印证 §8.4 的投入建议。 - -### 8.7 四维潜力排序:经济最具"护城河 + 生态"潜力 - -结论:**经济 #1(唯一带正反馈、能长生态),稳定是紧随其后的地基,效果只能窄切,好用出局。** -真正能长成生态的**产品形态**是"经济 + 稳定"融合出的**进程内控制面(计量 / 路由 / 护栏)**。 - -用四条判据过筛,经济全中,其余各有硬伤: - -| 判据 | 经济 | 稳定 | 效果 | 好用 | -| --- | --- | --- | --- | --- | -| 抗 LLM 侵蚀 | ✅ 运行时插桩 + 真实计价 | ✅ 运行时依赖 | ⚠️ 主要是模型的活 | ❌ 贬值最快 | -| 数据会复利 | ✅✅ 每次调用沉淀花费数据 | ⚠️ 有指标但不积累壁垒 | ⚠️ eval 数据但窄 | ❌ | -| 治理级锁定 | ✅ 接进预算/FinOps,拆掉很痛 | ⚠️ 属"信任底线"非差异化 | ❌ | ❌ | -| 能长生态 | ✅ 可做多 provider 计量/路由入口 | ❌ 偏基础设施,难托管第三方 | ❌ | ❌ | - -**经济的关键是"复利 + 升级阶梯"**(四维里唯一带正反馈): -> 成本归因(看清花哪了)→ 模型路由(自动选便宜的)→ FinOps 治理(预算/配额/护栏强制)→ **多 provider 控制面** - -稳定虽最 SDK 原生,但是**防守性的"信任底线"**(做到不被夸、没做到才挨骂),难独立长成生态; -效果受制于模型本身;好用已被 AI coding 商品化。 - -**生态形态——经济与稳定在此合体**:单看"成本报表"只是 feature,让它变生态的是把 -经济(计量/路由)+ 稳定(fallback/熔断)融进同一个**进程内控制面**: - -- **零插桩、调用点 ground-truth**——比 Langfuse / Helicone / Datadog 那类**代理式**可观测更准 - (不多一跳、直接看真实 payload),这是 SDK 独有的位置优势。 -- 一旦成为"流量必经的计量+路由+策略执行点",SDK **不再是 client 库,而是别的模型/provider 都要穿过的平面**—— - 生态引力由此产生:多模型接入点 / FinOps 看板数据源 / 预算护栏执行点 / "推荐哪个模型"的市场入口。 - 这正是 §8.5 那层最厚的外挂、§8.6"策略旋钮"的产品化终局。 - -**诚实的张力(会封顶生态野心)**: -1. **正面撞 Langfuse / Helicone / LangSmith**(已是现成的 LLM 可观测/成本生态);SDK 差异化只能是"进程内、零插桩、ground-truth"。 -2. **provider 中立 vs 阿里商业利益的矛盾**:生态引力最强的是**中立**版(连去别家的流量也归你计量/路由), - 但百炼大概率不愿让自家 SDK 把流量导向竞品 → 会被限定在"百炼域内",**生态天花板被商业策略压住**。 - 这是"要不要做中立控制面"的战略抉择,非工程问题。 - -**一句话**:押经济、产品化为"经济+稳定"的进程内控制面;稳定是必须先赢的地基但非终局, -效果做窄切(参数送对 + 结构化兜底 + eval),好用只需对 agent 友好。最大变数不是技术,是**要不要 provider 中立**。 - ---- - -## 9. 一句话对齐 - -- **管全部** = 一个 Client 覆盖(模态 × 协议 × provider)—— 横向广度。 -- **全链路** = files→微调→部署→RL→agentstudio 的生产流程;**长任务(jobs.create/fetch/wait)是这条链路上耗时环节的统一执行范式** —— 纵向深度。 -- **易用性/可定位性** = 从代际落后追到基线齐平(仅 tool 自动循环仍差 Anthropic 一点)。 -- **CLI** = 给"人 + CI"管长流程,对 toB 零边际成本;agent 对话走 expert,CLI 只是顺带对 agent 友好。 - ---- - -## 10. 待探究清单(后续继续) - -- [ ] **L4/L5 运维质量层立项**(§8.2):OTel/成本归因、录制回放、eval 闭环——抗 LLM 侵蚀的核心,优先级最高。 -- [ ] **token 成本归因报表**(§8.6 经济):输入侧 system/history/tool 分段 tokenize(输出侧 - content/reasoning 直接读 usage 明细),出 `dashscope dev cost`;计价表可配置/远程拉取, - SDK 只吐计数。最具体、最易先落地的经济能力。 -- [ ] **策略旋钮中间件**(§8.6):把经济↔效果↔稳定的权衡做成"质量下限之上最省 / 预算之内最稳"的可调策略。 -- [ ] **进程内控制面立项 + provider 中立抉择**(§8.7):经济+稳定融合的计量/路由/护栏平面是最具生态潜力项; - 须先决"是否 provider 中立"——中立则生态天花板高但违百炼商业利益,域内则反之。战略级抉择,非工程问题。 -- [ ] **MCP server / agent-legible 契约**(§8.3):把 SDK 资源做成 MCP tools + `llms.txt`,让 agent 原生消费。 -- [ ] **模型路由器 / 可靠性中间件**(§8.2):多协议从"能切"升级到"自动容灾"。 -- [ ] 最小公分母的具体边界:哪些 provider 特有能力会被统一接口磨平?逃生舱覆盖率够不够? -- [ ] **协议特有参数的切换体验**:anthropic 无条件 400 拒绝 5 个参数(§7.1b),是否需要 - "跨协议安全子集"提示 / 自动降级 / 文档对照表,降低漏抽象的客户成本? -- [ ] **agentstudio 对齐 v2**(§7.2c):信封扁平化 + 异常翻译(含流式路径),何时补齐?这是全链路体验的最大缺口。 -- [ ] **rl 的 `timeout=` 一致性**(§7.2c):4 个直通方法能否统一接受 timeout,兑现"全方法支持 timeout"承诺? -- [ ] **workspace 强制的上手摩擦**(§7.3):是否提供"无 workspace 时回退老网关 + warning"或单 key 自动探测 workspace? -- [ ] **修正 `_config.py` 注释 bug**(§7.3):老网关官方未下线,"retired" 表述错误,应改为"v2 默认不引用"。 -- [ ] **地域硬编码**(§7.3):v2 写死 `cn-beijing`,官方有 6 地域;是否加 `region=` 参数,免国际用户手拼 base_url? -- [ ] `tool_runner` / `@tool` 自动工具循环要不要做?(当前相对 Anthropic 的明显差距) -- [ ] codegen(M6)落地后,协议 adapter 是否也能从 spec 生成,减少手写漂移? -- [ ] 入站兼容 `dashscope serve`(让存量 OpenAI SDK 用户零改造用 dashscope)是否立项? -- [ ] 长任务 `Job` 对象在跨模态(图像/视频/RL)下 `output` 类型化视图的一致性核查。 -- [ ] 竞品(MiniMax/Baseten 等)是否也在做"多协议合并",差异化窗口还有多久? -- [ ] CLI 与 expert 的能力边界:哪些长流程操作应在 CLI、哪些应进 expert tools,避免重复。 - ---- - -## 参考 - -- `md/dashscope-2.0-design.md`(§0 原则 / §2.2 资源树·日志 / §2.7 错误模型 / §3 协议兼容层 / §4 CLI 定位 / §5 expert / §7 风险) -- `md/dashscope-2.0-interfaces-mapping.md`(接口映射核对表) -- `md/sdk-comparison-openai-vs-dashscope.md` -- `md/sdk-comparison-anthropic-vs-dashscope.md` -- `md/sdk-deep-comparison-openai-anthropic-dashscope.md`(更细粒度对比,本报告未逐条展开) diff --git a/md/dashscope-2.0-agentstudio-agenticrl-migration.md b/md/dashscope-2.0-agentstudio-agenticrl-migration.md deleted file mode 100644 index aebaed3f..00000000 --- a/md/dashscope-2.0-agentstudio-agenticrl-migration.md +++ /dev/null @@ -1,140 +0,0 @@ -# AgentStudio / AgenticRL 迁移 v2 与统一 2.0 错误体系 - -> 状态:设计稿(v2,已按评审意见修订)。 -> 约束:`dashscope-error-registry` 仓库本次不动;catalog 以 v2 vendored 副本 -> (`dashscope/v2/_error_registry.py`,上游 1.0.11@cd873b6)为准。 -> -> 已确认的方向: -> 1. agentstudio / agentic_rl 实现归 v2,**1.x 路径保留反向引用 shim,不删除**。 -> 2. reinforcement 的 1.x 路径由 `dashscope/finetune/reinforcement/` 调整为 -> **`dashscope/reinforcement/`**。 -> 3. v2 目录下全部遵循 `dashscope.v2.errors`;agentstudio 与 -> agentic_rl(reinforcement)同样遵循新错误体系。 - -## 1. 背景与现状 - -两个子系统的实现都已经在 v2 下,1.x 路径是反向引用的 shim;但**错误体系仍是 1.x 风格**,靠 mount 边界的翻译层把子系统异常转成 v2 错误。本次的核心工作就是消除这两套独立错误体系和翻译层。 - -### 1.1 AgentStudio - -| 项 | 现状 | -|---|---| -| 实现 | `dashscope/v2/agentstudio/`(client / transport / resources / streaming / pagination) | -| 1.x 路径 | `dashscope/agentstudio/__init__.py`:纯 shim,re-export v2(**保留**) | -| 错误体系 | `dashscope/v2/agentstudio/exceptions.py`:独立层级,基于 `dashscope._shared._errors.DashScopeException`,与 `dashscope.v2.errors` **无血缘** | -| 翻译层 | `dashscope/v2/resources/_agentstudio.py`:透明代理。HTTP 层(`APIStatusError` 子类)走 `error_from_status`;本地/流错误(APIConnectionError / APITimeoutError / StreamError / StreamClosedError)走 `error_from_sdk_exception(family="sdk.agentstudio")` | -| 网关错误码 | anthropic 风格字符串(`invalid_request_error` / `authentication_error` / `overloaded_error` …),`exceptions.from_response` 里手维护 `_CODE_TO_CLASS` / `_STATUS_TO_DEFAULT` 两张表 | - -### 1.2 AgenticRL - -| 项 | 现状 | -|---|---| -| 实现 | `dashscope/v2/finetune/agentic_rl.py`(`Client(...).rl` 所用);`submit_job` 返回 `SubmitResult` | -| 1.x 路径 | `dashscope/finetune/agentic_rl.py`:v2 子类 + 5 个 1.x 动词(`submit_job`/`cancel`/`list`/`get`/`delete`/`logs`),委托 1.x 根 API `FineTunes`,返回 1.x `FineTune` dataclass(**保留**) | -| reinforcement 包 | `dashscope/finetune/reinforcement/`:`sys.meta_path` finder 按需别名到 v2,**同一 module 对象**。**本次移动到 `dashscope/reinforcement/`** | -| 错误体系 | `common/errors.py`:独立层级。`AgenticRLError` 带**数字** `error_code`(1000/1800…)+ timestamp + `_RootCauseMixin` root-cause 格式化;另有 `RuntimeErrorWithCode` / `ValueErrorWithCode` 混入内置类型 | -| 翻译层 | `resources/reinforcement_learning.py` 的 `_translate_rl_errors` → `error_from_sdk_exception(family="sdk.agentic_rl")` | -| catalog | 27 个 `sdk.agentic_rl.*` 条目:18 个类名精确命中、6 个 condition-only、1 个家族 fallback(`sdk.agentic_rl.Error`) | - -v2 侧 `client.rl.jobs` 已原生覆盖任务全生命周期(create/fetch/wait/list/cancel/delete/logs)。 - -## 2. 目标态 - -1. **单一实现**:实现只在 v2;1.x 路径保留反向引用 shim(agentstudio / agentic_rl / reinforcement 三者都是),shim 不含第二份实现。 -2. **单一错误体系**:两个子系统原生抛 `dashscope.v2.errors`;删除两个 mount 翻译层;错误码事实源只剩 catalog。 -3. **reinforcement 新 1.x 路径**:`dashscope.reinforcement`(反向引用 `dashscope.v2.finetune.reinforcement`)。 -4. 旧路径 import 到的错误类就是新的 v2 错误类(同一对象),保证 `except` / `isinstance` 行为一致。 - -## 3. AgentStudio 迁移方案 - -### 3.1 异常类映射 - -| 旧(agentstudio.exceptions) | 新(dashscope.v2.errors) | 说明 | -|---|---|---| -| `AgentStudioError` | `DashScopeError` | 基类 | -| `APIConnectionError` | `APIConnectionError` | catalog `sdk.ConnectionError` 支撑 | -| `APITimeoutError` | `APITimeoutError` | catalog `sdk.Timeout` 支撑 | -| `APIStatusError` | `APIStatusError` | HTTP 层基类 | -| `InvalidRequestError` (400) | `BadRequestError` | **改名** | -| `AuthenticationError` (401) | `AuthenticationError` | 同名 | -| `PermissionDeniedError` (403) | `PermissionDeniedError` | 同名 | -| `NotFoundError` (404) | `NotFoundError` | 同名 | -| `ConflictError` (409) | `APIStatusError`(基类) | catalog 无 409 条目(registry 缺口,本次不动 registry,先落基类) | -| `RateLimitError` (429) | `RateLimitError` | 同名 | -| `OverloadedError` (503) | `ServiceUnavailableError` | **改名** | -| `InternalServerError` (500) | `InternalServerError` | 同名 | -| `StreamError` | 新 `SdkError` 子类 | catalog `sdk.agentstudio.StreamError` 支撑 | -| `StreamClosedError` | 同上(子类) | catalog `sdk.agentstudio.StreamClosedError` 支撑 | - -属性对应:`code/message/request_id/status_code` 两边同名;旧 `raw` → v2 `body`。 - -### 3.2 wire code 交叉表 - -catalog public 条目自带 `anthropic_error_code` 字段,交叉表**从 catalog 派生**,不再手维护。两个对不上的例外显式补: - -- `permission_denied_error`(agentstudio 网关实际值)→ `permission_denied` 条目(catalog 字段值是 `permission_error`) -- `conflict_error` / 409 → 无条目,落 `APIStatusError` 基类 - -`from_response` 的 CMA/旧版 `error_code`/`error_message`/Spring 三种 body 形状的归一化逻辑保留(迁入 agentstudio transport),类选择改为「crosswalk → 状态映射 → 基类」。 - -### 3.3 改动清单 - -- 重写 `dashscope/v2/agentstudio/exceptions.py`:独立层级替换为 `dashscope.v2.errors` 体系;旧类名保留为 **deprecated alias**(指向新类,§6 决策点 3),使 1.x shim re-export 的旧名字不断裂 -- transport / streaming 的 raise 点改抛 v2 错误 -- 删除 `dashscope/v2/resources/_agentstudio.py` 翻译代理;`client.agentstudio` 直接挂载 -- **保留** `dashscope/agentstudio/` shim(re-export v2,含错误类新名字与旧别名) -- 测试:`tests/v2/test_agentstudio_shim.py` / `test_agentstudio_errors.py` 重写(翻译层断言改为原生错误断言);`tests/unit/test_agentstudio_*` 走 1.x shim,继续可用,按需调整异常断言 - -## 4. AgenticRL 迁移方案 - -### 4.1 API 面 - -- `dashscope/finetune/agentic_rl.py` **保留**:1.x 动词(`FineTunes` 委托、`FineTune` 返回)是 1.x 独有面;底层实现继续复用 v2,无第二份逻辑 -- `dashscope/v2/finetune/agentic_rl.py` 里对 `dashscope.api_key` 根全局的现有规避保持不变 - -### 4.2 错误体系统一 - -- `common/errors.py` 重写:异常类改为 `dashscope.v2.errors.SdkError` 子类,`code`/`solution`/`allow_retry` 由 catalog 条目支撑(沿用 `APIConnectionError` 的 `_SDK_ERROR_NAME` 模式);condition-only 条目不设专用类,由 `sdk_error()` 工厂构造 -- **数字 `error_code` 处理**:catalog 用 `sdk.agentic_rl.*` 名称作 code。旧数字码建议保留 property 兼容映射一个版本周期(§6 决策点 2);`{inner_code}` 占位符改由包装时的内部错误名填充 -- `RuntimeErrorWithCode` / `ValueErrorWithCode` 的内置类型混入取消(v2 体系不入内置层级) -- `_RootCauseMixin` 的 root-cause 遍历/格式化评估并入 `SdkError.__str__` 或删除 -- 删除 `_translate_rl_errors` 挂载翻译;工作流(register/upload/submit/run)原生抛 v2 错误 -- 旧类名保留为 deprecated alias(18 个精确命中类名 + `AgenticRLError` 基类名),1.x 路径 `except` 旧类名继续命中同一对象 - -### 4.3 reinforcement 路径迁移:`dashscope/finetune/reinforcement/` → `dashscope/reinforcement/` - -- 新包 `dashscope/reinforcement/`:整体平移现有 meta_path 别名 shim,目标仍为 `dashscope.v2.finetune.reinforcement`(实现位置不变) -- **旧路径 `dashscope/finetune/reinforcement/` 是否保留别名,见 §6 决策点 4**。外部进程按模块名字符串 import 的既有消费方: - - FC sandbox `start.sh` 的 `FC_SERVER_CLASSPATH` - - `examples/workspace/scripts/start_local_*.sh` - - `server.py` 传给 uvicorn 的 dotted path(worker 进程内 import) - 这些脚本若同步改为 `dashscope.reinforcement.*`,需同时更新模板与示例;已部署 sandbox 用其环境内旧版本,不受影响 -- `dashscope/finetune/agentic_rl.py` 内对 `...reinforcement` 的 re-export 改指新路径(保持两个旧名字可 import 的过渡方案由决策点 4 决定) - -## 5. 破坏性变更清单 - -| 变更 | 影响面 | -|---|---| -| agentstudio 异常改名(InvalidRequestError→BadRequestError、OverloadedError→ServiceUnavailableError、ConflictError 取消) | `except` 旧类名的代码(deprecated alias 可缓解) | -| agentstudio 异常属性 `raw` → `body` | 读 `.raw` 的代码 | -| agentstudio 异常基类从 `DashScopeException` 换到 `DashScopeError` | 基类捕获链(1.x 根 `DashScopeException` 捕获将失效,alias 无法解决基类切换) | -| rl 数字 `error_code` 语义切换为 catalog 名 | 按数字判断错误种类的代码(property 映射可缓解) | -| rl `*WithCode` 不再继承内置 RuntimeError/ValueError | `except RuntimeError` 兜底 rl 错误的代码 | -| `AgenticRLError` 基类从 `Exception` 换到 `SdkError` | 基类捕获链 | -| reinforcement 1.x 路径调整为 `dashscope.reinforcement` | 按旧 dotted path 启动的脚本(决策点 4) | - -## 6. 决策点(评审确认) - -1. ~~1.x import 路径是否删除~~ → **已确认:全部保留反向引用 shim** -2. **rl 数字 `error_code`**:直接取消,还是保留 property 兼容映射到 catalog 名? -3. **旧异常类名** deprecated alias 的范围与保留周期(agentstudio 的 InvalidRequestError/OverloadedError、rl 的 18 个类名) -4. **旧 `dashscope.finetune.reinforcement` 路径**:保留到 `dashscope.reinforcement` 的别名(推荐,外部脚本无感),还是直接切换并要求同步改脚本? -5. registry 缺口(409/conflict_error、permission_denied_error 别名):本次落基类 + 显式 crosswalk,后续是否推 registry 补条目? - -## 7. 实施步骤(建议分 PR) - -1. **PR-1 agentstudio 错误统一**:重写 exceptions + transport/streaming raise 点 + crosswalk;删 `_agentstudio.py` 翻译层;v2 测试改写;1.x shim 保持可用 -2. **PR-2 agentic_rl 错误统一**:重写 `common/errors.py`;工作流 raise 点改造;删 `_translate_rl_errors`;registry 完整性测试同步(`test_agentic_rl_reachability_counts` 等) -3. **PR-3 reinforcement 路径迁移**:新增 `dashscope/reinforcement/` shim;按决策点 4 处理旧路径;更新 FC 模板 / examples 脚本 / acli skill 文档中的模块名 - -每步验收:`pytest tests/v2 tests/unit` 全绿 + CLI 冒烟(rl 提交/查询)+ agentstudio 样例冒烟。 diff --git a/md/dashscope-2.0-asr-sdk.md b/md/dashscope-2.0-asr-sdk.md deleted file mode 100644 index cfe01088..00000000 --- a/md/dashscope-2.0-asr-sdk.md +++ /dev/null @@ -1,339 +0,0 @@ -# 语音识别(ASR)Python SDK - -通过 `client.audio.asr`,您可以使用 WebSocket 实时识别音频流,或通过 HTTP 提交录音文件转写任务。本文对应当前仓库的 SDK 实现;模型能力、音频限制及服务端字段以相应模型协议为准。 - -| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | -| --- | --- | --- | -| 实时语音识别 | `audio.asr.recognize()` | 不支持 | -| 录音文件转写 | `audio.asr.transcribe()`、`audio.asr.jobs` | 支持相同批量接口,需使用 `await` | - -## 前期准备 - -1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 -2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置。不配置工作空间且未指定端点时,当前 SDK 会回退到共享网关并发出警告。 -3. 在本仓库根目录执行源码安装: - - ```bash - python -m pip install -e . - ``` - -4. 实时示例需要当前目录下的 `input.pcm`:有限长度、无文件头、16 kHz、16 bit、单声道的原始 PCM 音频。文件转写示例需要设置 `AUDIO_FILE_URL`,其值为服务端可访问的音频文件 URL。 - -客户端使用公开入口 `from dashscope import Client, AsyncClient`。也可以在创建客户端时显式传入 `api_key`、`workspace`、`region`;本文示例从环境配置读取凭据,不调用麦克风或下载音频。 - -## 快速开始 - -### 实时语音识别 - -以下代码可保存为 `asr_example.py`,在音频文件所在目录运行 `python asr_example.py`。示例使用 `fun-asr-realtime`,请先确认目标地域可用且已具备访问权限。 - -发送和接收分别运行在两个线程中,逐块读取文件、逐事件处理结果,不在内存中收集整段音频或全部事件。 - -```python -import time -from concurrent.futures import ThreadPoolExecutor, as_completed - -from dashscope import Client - - -def send_audio(session, audio_path): - with open(audio_path, "rb") as audio: - while True: - frame = audio.read(3200) - if not frame: - break - session.send_audio_frame(frame) - time.sleep(0.1) - session.stop() - - -def receive_events(session): - for event in session.events(): - payload = event.get("payload") or {} - sentence = (payload.get("output") or {}).get("sentence") or {} - if sentence.get("text") and not sentence.get("heartbeat"): - label = "最终" if sentence.get("sentence_end") else "中间" - print(label, sentence["text"], flush=True) - - -def main(audio_path): - with Client() as client: - session = client.audio.asr.recognize( - model="fun-asr-realtime", - format="pcm", - sample_rate=16000, - timeout=30.0, - ) - with ThreadPoolExecutor(max_workers=2) as executor: - try: - session.start() - print("task_id:", session.session.task_id) - futures = [ - executor.submit(receive_events, session), - executor.submit(send_audio, session, audio_path), - ] - for future in as_completed(futures): - future.result() - finally: - session.close() - print("request_id:", session.request_id) - print("usage:", session.usage) - print("first_package_delay(ms):", session.first_package_delay) - print("last_package_delay(ms):", session.last_package_delay) - - -if __name__ == "__main__": - main("input.pcm") -``` - -- `recognize()` 只创建 `Recognition` 对象;`start()` 才建立连接、发送 `run-task`,并在内部读取和等待 `task-started`。启动成功后才能发送音频。 -- 示例每次发送 3200 字节并暂停 0.1 秒,对应上述 PCM 格式约 100 毫秒的音频。这是模拟实时输入的建议节奏,不是 SDK 强制的分块大小或发送间隔;生产环境需遵守模型的服务端限制。 -- `stop()` 仅结束输入。接收线程继续读取直到 `task-finished`,以获得最后的识别结果和统计信息。 -- `Future.result()` 将工作线程异常传回主线程。`session.close()` 的 `finally` 必须位于线程池上下文内部,确保发送失败时先关闭连接、解除接收阻塞,再等待线程退出。不要只在退出线程池后关闭会话。 - -### 录音文件转写 - -`transcribe()` 提交任务并等待终态,返回 `Job`,不是转写正文。 - -```python -import os - -from dashscope import Client - - -with Client() as client: - job = client.audio.asr.transcribe( - file_urls=[os.environ['AUDIO_FILE_URL']], - model="paraformer-v2", - timeout=86400, - submit_timeout=30, - ) - print("task_id:", job.task_id, "status:", job.status) - for result in job.results: - print("transcription_url:", result.transcription_url) -``` - -请根据 `job.output` 和各文件结果判断转写情况。`transcription_url` 是服务端返回的结果地址,SDK 不自动下载或解析该地址中的转写文件。 - -### 异步录音文件转写 - -`AsyncClient` 的批量转写接口使用相同参数和 `Job` 返回结构;实时 `recognize()` 不提供异步版本。 - -```python -import asyncio -import os - -from dashscope import AsyncClient - - -async def main(): - async with AsyncClient() as client: - job = await client.audio.asr.transcribe( - file_urls=[os.environ['AUDIO_FILE_URL']], - model="paraformer-v2", - ) - for result in job.results: - print(result.transcription_url) - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -## 请求参数 - -### 实时识别:`client.audio.asr.recognize()` - -以下参数均为关键字参数。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `model` | `str` | 必填,无默认值。实时识别模型名称,例如 `fun-asr-realtime`;可用性取决于地域及服务权限。 | -| `format` | `str` | 可选,默认 `"pcm"`。输入音频格式,必须与实际数据一致;支持范围由模型决定。SDK 不转码。 | -| `sample_rate` | `int` | 可选,默认 `16000`,单位 Hz。输入音频采样率,SDK 不重采样。 | -| `parameters` | `dict` | 可选,默认 `None`。模型参数透传到 `run-task.payload.parameters`;其中同名键会覆盖由 `format`、`sample_rate` 形参生成的值,包括显式传入的值。 | -| `input` | `dict` | 可选,默认 `None`,发送时为 `{}`。任务启动时的初始上下文,透传到 `run-task.payload.input`,结构由模型协议定义。 | -| `phrase_id` | `str` | 可选,默认 `None`。已有热词资源 ID,转换为 `resources=[{"resource_id": phrase_id, "resource_type": "asr_phrase"}]`,不是 `parameters` 中的字段。资源须与模型匹配。 | -| `ws_url` | `str` | 可选,默认 `None`。完整 WebSocket 端点;未指定时采用全局 WebSocket 端点配置,否则按工作空间和地域选择默认端点。 | -| `connector` | `Callable` | 可选,默认 `None`,使用 SDK 默认连接器。自定义连接器按 `connector(url, headers)` 调用,返回兼容 `send`、`recv`、`settimeout`、`close` 的连接对象;自行负责连接建立阶段的超时。 | -| `timeout` | `float` | 可选,默认 `None`,继承客户端超时;客户端未另行配置时为 `300.0` 秒。值必须大于 0;默认连接器将其用于连接建立,并设置连接的读写超时,不是整个识别任务的总时限。 | -| `headers` | `Mapping[str, str]` | 可选,默认 `None`。附加握手请求头。SDK 会根据客户端配置写入认证和工作空间请求头,不应以此替代凭据配置。 | - -`DASHSCOPE_WEBSOCKET_BASE_URL` 可配置 WebSocket 端点,调用级 `ws_url` 优先。HTTP 的 `Client(base_url=...)` 不会自动成为实时识别的 WebSocket 地址。 - -### 文件转写:`client.audio.asr.transcribe()` - -`file_urls` 可按位置或关键字传入,其余均为关键字参数。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `file_urls` | `list[str]` | 必填,无默认值,不能为空。服务端可访问的音频文件 URL 列表,不是本地路径;文件数量、格式及大小限制由模型决定。 | -| `model` | `str` | 可选,默认 `"paraformer-v2"`。录音文件转写模型名称。 | -| `parameters` | `dict` | 可选,默认 `None`。服务端转写参数,原样传递;字段和默认行为以所选模型为准。 | -| `phrase_id` | `str` | 可选,默认 `None`。已有热词资源 ID,与实时接口相同,映射为 `asr_phrase` 类型的 `resources`。 | -| `timeout` | `float` | 可选,默认 `86400.0` 秒,即 24 小时。提交成功后等待任务终态的预算,不是提交请求超时。 | -| `submit_timeout` | `float` | 可选,默认 `None`,继承客户端超时。仅用于提交任务这一次 HTTP 请求,单位秒。 | -| `poll_interval` | `float` | 可选,默认 `1.0` 秒。初始轮询间隔,随后按 2 倍递增;建议使用正数。 | -| `max_interval` | `float` | 可选,默认 `8.0` 秒。后续轮询间隔上限,建议不小于 `poll_interval`。 | - -### 分步任务接口:`client.audio.asr.jobs` - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `jobs.create`:`model` | `str` | 必填关键字参数,无默认值;与 `transcribe()` 的默认模型行为不同。 | -| `jobs.create`:`file_urls` | `list[str]` | 必填关键字参数,无默认值,不能为空。 | -| `jobs.create`:`parameters`、`phrase_id` | `dict`、`str` | 均可选,默认 `None`,含义与 `transcribe()` 相同。 | -| `jobs.create`:`timeout` | `float` | 可选关键字参数,默认 `None`,继承客户端超时;仅为提交请求超时,单位秒。 | -| `jobs.fetch`:`task_id` | `str` | 必填,无默认值。要查询的任务 ID。 | -| `jobs.fetch`:`timeout` | `float` | 可选关键字参数,默认 `None`,继承客户端超时;仅为本次查询请求超时,单位秒。 | -| `jobs.wait`:`job` | `Job` 或 `str` | 必填,无默认值。任务快照或任务 ID。 | -| `jobs.wait`:`timeout` | `float` | 可选关键字参数,默认 `86400.0` 秒。等待终态的预算。 | -| `jobs.wait`:`poll_interval`、`max_interval` | `float` | 可选关键字参数,默认分别为 `1.0`、`8.0` 秒,含义与 `transcribe()` 相同。 | - -等待预算不会替换单次 HTTP 查询超时,也不是可中断在途请求的硬截止时间;查询仍使用客户端超时,因此实际返回时间可能超过等待预算。 - -## 关键接口 - -### 实时会话生命周期 - -`Recognition` 的推荐调用顺序为:创建对象 → `start()` → 并发发送音频和读取事件 → `stop()` → 读到 `task-finished` → `close()`。 - -| 接口 | 说明 | -| --- | --- | -| `start()` | 新建 WebSocket 连接并等待 `task-started`,成功后返回当前 `Recognition`。已打开时再次调用会抛出 `RuntimeError`。 | -| `send_audio_frame(data)` | 将音频作为 WebSocket 二进制帧发送,不是 Base64 或 JSON;通常传入 `bytes`,也接受 `bytearray`、`memoryview`。 | -| `update_context(context)` | `context` 必须是 `dict`。发送 `continue-task`,其 `payload.input` 就是该字典,用于后续音频的上下文,不会重启任务。 | -| `stop()` | 发送 `finish-task` 结束输入,不等待最终结果、不关闭连接;结束输入后不能再发送音频或更新上下文。 | -| `events()` | 阻塞读取并逐条产出原始事件字典,包含最终的 `task-finished`。请由一个接收线程持续消费,不要并发创建多个读取者。 | -| `close()` | 关闭底层连接,可重复调用;不会代替正常的结束输入及读取最终结果流程。 | - -`with client.audio.asr.recognize(...) as session:` 会自动执行 `start()`,退出时执行 `close()`,但不会自动发送音频、调用 `stop()` 或读完事件。使用线程池时仍应采用快速开始中的内部 `finally` 关闭方式,避免错误退出时等待读线程。 - -如模型支持文本上下文,可在启动时传入 `input={"context": {"text": "上一轮内容"}}`,或在会话启动后调用 `session.update_context({"context": {"text": "更新后的内容"}})`。更新应在发送相应后续音频之前完成;具体内容约束以模型协议为准。 - -`close()` 后可对同一个 `Recognition` 再次调用 `start()`。这会创建新连接、新任务并重置本次统计,不是原任务续传,也不复用 WebSocket 连接池。请先等待旧的收发线程退出,再重新启动;`Client.close()` 不替代实时会话的显式关闭。 - -### 提交、查询与等待转写任务 - -- `jobs.create(...)`:提交并返回任务快照,不等待转写完成。 -- `jobs.fetch(task_id, ...)`:查询一次,返回新的任务快照,不持续轮询。 -- `jobs.wait(job_or_task_id, ...)`:轮询直到终态,返回终态快照或抛出异常。 - -```python -import os - -from dashscope import Client - - -with Client() as client: - job = client.audio.asr.jobs.create( - model="paraformer-v2", - file_urls=[os.environ['AUDIO_FILE_URL']], - timeout=30, - ) - print("task_id:", job.task_id) - job = client.audio.asr.jobs.fetch(job.task_id, timeout=30) - job = client.audio.asr.jobs.wait(job, timeout=86400) - print(job.status, job.succeeded) -``` - -异步客户端对应 `await client.audio.asr.jobs.create(...)`、`await client.audio.asr.jobs.fetch(...)` 和 `await client.audio.asr.jobs.wait(...)`。这里的服务端异步任务与 Python 异步调用是两个概念:同步 `Client` 同样可以提交服务端任务。 - -## 返回事件与属性 - -### 实时事件 - -`events()` 返回原始 `dict`,通过 `event["header"]["event"]` 区分事件,而不是通过事件对象的属性访问。 - -| 事件 | SDK 行为 | -| --- | --- | -| `task-started` | 由 `start()` 内部读取,启动返回时保存在 `last_response` 中;不会再次由 `events()` 产出。 | -| `result-generated` | 由 `events()` 产出;识别结果通常位于 `payload.output.sentence`,也可能是心跳或空文本事件。 | -| `task-finished` | 由 `events()` 产出,随后迭代结束;发送 `stop()` 后仍须读取此事件。 | -| `task-failed` | 转换为 SDK 业务异常并关闭连接,不作为普通事件产出。 | - -按模型返回情况,`payload.output.sentence` 中可包含 `text`、`sentence_end`、`heartbeat` 等字段。中间文本可能被后续结果修订,不应简单拼接所有 `text`;心跳和空文本事件仍会产出,应用可按需忽略。完整字段见相关服务端事件文档。 - -### `Recognition` 属性 - -| 属性 | 类型 | 说明 | -| --- | --- | --- | -| `last_response` | `dict` 或 `None` | 最近一次成功读取的原始事件。初始为 `None`;启动后为握手事件,随后在消费 `events()` 时更新。 | -| `usage` | `dict` 或 `None` | 最近一次读取到的非 `None` 的 `payload.usage`;后续事件缺失该字段或值为 `None` 时保留已有值,不自动累计,也不保证只在最后一条事件出现。 | -| `first_package_delay` | `float` 或 `None` | 单位毫秒。从第一次调用 `send_audio_frame()` 到读取首个非心跳且文本非空的识别结果的耗时;尚未满足条件时为 `None`。 | -| `last_package_delay` | `float` 或 `None` | 单位毫秒。从第一次调用 `stop()` 到读取 `task-finished` 的耗时;未调用 `stop()` 或未读到终态时为 `None`。 | -| `request_id` | `str` 或 `None` | 当前记录的请求标识,从已成功读取事件更新;缺少服务端 `request_id` 时可回退到任务 ID。异常排查时优先查看异常携带的请求标识。 | -| `session.task_id` | `str` | 底层会话的任务 ID。须在启动后、关闭前读取;未打开时访问 `Recognition.session` 会抛出 `RuntimeError`。 | - -统计是客户端**实际读取事件时**更新的,不是后台自动刷新;延迟包含网络、线程调度及消费滞后,不能等同于纯服务端推理耗时。`start()` 读取的握手只更新启动响应和请求标识,不通过 `events()` 的用量及延迟统计逻辑。关闭后已保存的统计仍可读取,再次启动会重置。 - -### 文件转写 `Job` - -| 属性 | 类型 | 说明 | -| --- | --- | --- | -| `request_id` | `str` 或 `None` | 本次提交或查询响应的请求标识,不是任务 ID。 | -| `task_id` | `str` | 服务端任务 ID,可用于后续查询和等待。 | -| `status` | `str` | 任务状态,包括 `PENDING`、`RUNNING`、`SUCCEEDED`、`FAILED`、`CANCELED`。 | -| `output` | `dict` 或 `None` | 原始任务输出,保留服务端结果和每个文件的详情。 | -| `results` | `list[JobResult]` | `output.results` 的结构化视图;没有结果时为空列表。 | -| `results[i].transcription_url` | `str` 或 `None` | 第 i 个结果的转写文件地址;该字段属于结果项,不是 `Job` 顶层字段。 | -| `usage` | `Usage` 或 `None` | 服务端用量对象,保留服务特有字段;不存在时为 `None`。 | -| `finished` | `bool` | 状态为 `SUCCEEDED`、`FAILED` 或 `CANCELED` 时为 `True`,不代表成功。 | -| `succeeded` | `bool` | 仅在状态为 `SUCCEEDED` 时为 `True`。 | - -`Job` 是一次响应的快照,不会自行更新,也不包含 SDK 已下载的转写正文。请接收 `fetch()`、`wait()` 返回的新对象,并逐项检查结果;不要把缺少 `transcription_url` 的条目当作已取得转写内容。 - -## 异常处理 - -| 情况 | 行为与处理建议 | -| --- | --- | -| WebSocket 提前结束 | 底层 `recv()` 在 `task-finished` 前返回 `None` 或空字符串时,SDK 抛出 `APIConnectionError`,不能当作正常识别完成。 | -| 服务端 `task-failed` | SDK 根据业务错误码抛出 `DashScopeError` 或其业务子类,如 `AuthenticationError`、`BadRequestError`、`RateLimitError`;检查异常的 `code`、`message`、`request_id`。 | -| WebSocket 原生错误 | 连接器建立连接、底层 `send()`、`recv()` 抛出的异常通常原样传播,例如 `websocket.WebSocketTimeoutException`、其他 WebSocket 异常或 `OSError`;不会全部自动转换成 `APIConnectionError` 或 `APITimeoutError`。 | -| 协议错误 | 缺少事件头或握手事件不符等情况可能抛出 `WsEventError`(属于 `DashScopeError`);不要继续使用失败的会话。 | -| 本地参数或状态错误 | 空 `file_urls`、非字典上下文、未启动就发送、结束输入后继续发送等,可能分别触发 `ValueError`、`TypeError` 或 `RuntimeError`。 | -| HTTP 或转写业务失败 | 默认 `raise_on_error=True` 时,HTTP 错误、响应业务错误码以及解析到 `FAILED` 任务状态会抛出 SDK 异常;`wait()` 遇到 `CANCELED` 也会抛出 `DashScopeError`。 | -| 等待预算耗尽 | `transcribe()` 或 `jobs.wait()` 抛出 `DashScopeError`;这不表示服务端任务已取消。保留任务 ID 后可再次 `fetch()` 或 `wait()`。 | - -- 在收发线程外读取 `Future.result()` 并确保 `finally` 关闭会话。仅捕获 `DashScopeError` 无法覆盖全部 WebSocket 原生异常;不要静默忽略线程错误。 -- `Client(raise_on_error=False)` 可用于检查 HTTP 返回对象中的 `code`、`message`、`is_error` 和任务状态,但不会关闭 WebSocket 的失败事件异常,也不会抑制本地错误或等待超时。 -- `max_retries` 不提供 WebSocket 自动重连或音频重放。实时任务断开后,由应用决定是否新建任务及重新发送可重放的音频,避免重复结果;不要宣称可以续传原任务。 -- 排查时记录异常类型、请求 ID 和任务 ID;不要记录 API Key 或未经脱敏的完整音频、上下文及转写正文。 - -## 常见问题 - -**为什么 `stop()` 返回了,却没有最终识别结果?** - -`stop()` 只通知服务端输入结束。必须继续消费 `events()` 到 `task-finished`,不要立即关闭连接;握手事件则已由 `start()` 读取,不要额外等待一次 `task-started`。 - -**可以把 WAV 文件直接作为 PCM 发送吗?** - -不能仅修改扩展名。快速开始要求无文件头的原始 PCM;实际格式、采样率和声道必须匹配模型要求。SDK 不负责去除 WAV 文件头、转码或重采样。 - -**为什么用量或延迟仍为 `None`?** - -可能尚未消费包含用量的事件、没有非心跳的有效文本,或还未在 `stop()` 后读到 `task-finished`。这些值不是服务端必定返回的完整统计,不能用空值判定任务成功或失败。 - -**转写返回的是 URL,怎样获取文本?** - -检查任务及各文件结果后,由应用读取 `job.results` 中的 `transcription_url`,再自行下载和解析转写文件;本接口不会自动下载正文。 - -**能用 `AsyncClient` 做实时识别,或在断线后恢复旧任务吗?** - -当前均不支持。`AsyncClient` 仅提供本文的批量转写接口;实时识别使用同步会话并发收发。关闭后重新启动是新任务,不是旧任务恢复;但多条识别可以复用同一条连接,见下节。 - -## 连接复用 - -`recognize(..., connection=conn)` 把识别任务跑在共享的 -`WsConnection`(`client.ws_connection()` 创建)上:顺序任务免再次握手, -多路 JSON 任务可并发;二进制上行每条连接单 owner(一路活跃识别)。 -会话 `close()` 只摘离任务、不关共享连接。完整约束与示例见 -[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 - -## 相关文档 - -- [Fun-ASR 客户端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/fun-asr-client-events) -- [Fun-ASR 服务端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/fun-asr-server-events) -- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) -- [语音合成 Python SDK](dashscope-2.0-tts-sdk.md) -- [实时多模态 Python SDK](dashscope-2.0-realtime-sdk.md) diff --git a/md/dashscope-2.0-audio-management-sdk.md b/md/dashscope-2.0-audio-management-sdk.md deleted file mode 100644 index 21e5dc4a..00000000 --- a/md/dashscope-2.0-audio-management-sdk.md +++ /dev/null @@ -1,157 +0,0 @@ -# DashScope 2.0 Python SDK:语音管理面(词表 / 热词 / 声音复刻 / 声音设计) - -本文覆盖 `client.audio` 的四个管理面子资源,对应 1.x 的三个管理类与官方声音设计 REST API: - -| v2 入口 | 1.x / 官方对应 | 说明 | -| --- | --- | --- | -| `client.audio.vocabularies` | `VocabularyService`(`audio/asr/vocabulary.py`) | ASR 定制词表(热词表)CRUD | -| `client.audio.phrases` | `AsrPhraseManager`(`audio/asr/asr_phrase_manager.py`) | paraformer 热词编译 CRUD | -| `client.audio.voices` | `VoiceEnrollmentService`(`audio/tts_v2/enrollment.py`) | 声音复刻(克隆音色)CRUD | -| `client.audio.voice_design` | 官方[声音设计 API](https://help.aliyun.com/zh/model-studio/voice-design-api-references)(1.x 无 SDK 类) | qwen 声音设计 CRUD | - -词表 / 声音复刻 / 声音设计三者共用同一信封:`POST /services/audio/{asr|tts}/customization`,请求体 `{model, input: {action, ...}, parameters?}`,仅 action 词与 ID 字段随族不同;v2 把 action 收敛为统一的 `create/list/get/update/delete` 方法名(1.x 的 `query_*` 统一为 `get`)。热词(phrases)实为一条编译任务:`create/update` POST `/fine-tunes`(`mode=compile_asr_phrase`、空文件集),`get/list/delete` 走 `/fine-tunes/outputs`。 - -所有方法共享关键字参数 `timeout` / `headers` / `api_key` / `workspace`;`model=` 可覆盖各资源默认服务模型(下表);模型专属的 `input` 字段经 `extra_input=` 透传(具名参数同名优先),顶层 `parameters=` 原样放入请求体。错误按 SDK 错误目录映射(如 `InvalidParameter` → `BadRequestError`);`raise_on_error=False` 时错误体现在返回对象的 `code` / `message` / `is_error` 上。返回值均携带 `request_id`;`update` / `delete` 返回 `JobStatus`。 - -| 资源 | 默认 `model` | 端点 | -| --- | --- | --- | -| `vocabularies` | `speech-biasing` | `/services/audio/asr/customization` | -| `voices` | `voice-enrollment` | `/services/audio/tts/customization` | -| `voice_design` | `qwen-voice-design` | `/services/audio/tts/customization` | -| `phrases` | 调用方 ASR 模型 | `/fine-tunes`、`/fine-tunes/outputs` | - -> 1.x 这两个服务内部对超时/断连做 3 次重试;v2 交由 transport 的重试循环统一处理(默认 2 次),无需在资源层重复。1.x `list_phrases(page=...)` 的 `page` 实参在 `**kwargs` 中被丢弃、从未到达线缆;v2 发送文档化的 `page_no`,不复刻该行为。 - -## 定制词表(vocabularies) - -对应官方[定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/)示例:为 paraformer 系列识别模型创建词表,返回 `vocabulary_id`。 - -```python -from dashscope.v2 import Client - -client = Client() -created = client.audio.vocabularies.create( - target_model="paraformer-realtime-v2", - prefix="med", # 仅小写字母/数字,10 字符以内 - vocabulary=[ - {"text": "心肌梗死", "weight": 4}, - {"text": "脑梗死", "weight": 4}, - ], -) -print(created.vocabulary_id) - -# 列表 / 查询内容 / 整体替换 / 删除 -tables = client.audio.vocabularies.list(prefix="med", page_index=0, page_size=10) -for item in tables.vocabularies: - print(item["vocabulary_id"], item.get("prefix")) - -table = client.audio.vocabularies.get(created.vocabulary_id) -print(table.vocabulary) # 词表内容 - -client.audio.vocabularies.update( - created.vocabulary_id, - vocabulary=[{"text": "冠心病", "weight": 4}], -) -client.audio.vocabularies.delete(created.vocabulary_id) -``` - -## 热词(phrases,paraformer) - -对应官方 [paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager)示例:`create` 返回编译任务,`phrase_id`(即任务 `job_id`)编译完成后即可传给识别: - -```python -from dashscope.v2 import Client - -client = Client() -phrase = client.audio.phrases.create( - model="paraformer-v2", - phrases={"下一首": 90, "上一首": 90}, # 词 -> 权重 -) -print(phrase.phrase_id, phrase.status) - -# 编译完成后在实时识别中引用(run-task 顶层 resources,见 ASR 指南) -# client.audio.asr.recognize(..., phrase_id=phrase.phrase_id) - -client.audio.phrases.update( - phrase.phrase_id, - model="paraformer-v2", - phrases={"暂停": 90}, -) -fetched = client.audio.phrases.get(phrase.phrase_id) -page = client.audio.phrases.list(page_no=1, page_size=10) -client.audio.phrases.delete(phrase.phrase_id) -``` - -`create` / `update` 的 `training_type` 默认 `"compile_asr_phrase"`;空 `phrases` 与空 `phrase_id` 在本地直接 `ValueError`。 - -## 声音复刻(voices) - -对应官方[声音复刻](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide)示例:以一段音频 URL 克隆音色,返回 `voice_id`,随后即可作为 TTS 的 `voice` 使用。 - -```python -from dashscope.v2 import Client - -client = Client() -voice = client.audio.voices.create( - target_model="cosyvoice-v3-plus", - prefix="demo", - url="https://your-audio-url/sample.wav", - language_hints=["zh"], -) -print(voice.voice_id) - -# audio = client.audio.tts.synthesize("你好", model="cosyvoice-v3-plus", voice=voice.voice_id) - -voices = client.audio.voices.list(prefix="demo") -detail = client.audio.voices.get(voice.voice_id) # status: OK / DEPLOYING / UNDEPLOYED -client.audio.voices.update(voice.voice_id, url="https://your-audio-url/new-sample.wav") -client.audio.voices.delete(voice.voice_id) -``` - -## 声音设计(voice_design) - -对应官方[声音设计](https://help.aliyun.com/zh/model-studio/voice-design-user-guide)示例:用文字描述生成音色,ID 字段为 `voice`,并随附 base64 预览音频。 - -```python -import base64 -from dashscope.v2 import Client - -client = Client() -designed = client.audio.voice_design.create( - target_model="qwen3-tts-vd-realtime-2026-01-15", - voice_prompt="沉稳的中年男性,音色低沉浑厚", - preview_text="各位听众朋友,大家好", - preferred_name="announcer", - language="zh", - parameters={"sample_rate": 24000, "response_format": "wav"}, -) -print(designed.voice) -preview = designed.preview_audio # {data(base64), sample_rate, response_format} -with open("preview.wav", "wb") as f: - f.write(base64.b64decode(preview["data"])) - -voices = client.audio.voice_design.list(page_index=0, page_size=10) -detail = client.audio.voice_design.get("announcer") -client.audio.voice_design.delete("announcer") -``` - -## 返回类型速查 - -| 类型 | 关键字段 | 说明 | -| --- | --- | --- | -| `Vocabulary` | `vocabulary_id`、`vocabulary` | `create` 只带 ID;`get` 带词表内容;其余字段随 `output` 保留 | -| `VocabularyList` | `vocabularies` | 词表条目 dict 列表 | -| `Voice` | `voice_id`、`status`、`preview_audio` | 克隆音色 | -| `VoiceList` | `voices` | 音色条目 dict 列表 | -| `DesignedVoice` | `voice`、`preview_audio`、`language`、`target_model` | 设计音色 | -| `DesignedVoiceList` | `voices`(另含 `total_count`) | 设计音色条目 | -| `Phrase` | `phrase_id`、`status` | 编译任务即热词集 | -| `PhraseList` | `phrases`、`page_no`、`page_size`、`total_count` | 热词集分页 | -| `JobStatus` | `status` | `update` / `delete` 的应答 | - -## 相关文档 - -- [DashScope 2.0 ASR SDK](dashscope-2.0-asr-sdk.md)(`phrase_id` 在识别中的使用) -- [DashScope 2.0 TTS SDK](dashscope-2.0-tts-sdk.md)(克隆/设计音色在合成中的使用) -- [定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/) / [paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager) -- [声音复刻](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide) / [声音设计](https://help.aliyun.com/zh/model-studio/voice-design-user-guide) diff --git a/md/dashscope-2.0-connection-reuse-sdk.md b/md/dashscope-2.0-connection-reuse-sdk.md deleted file mode 100644 index 45f24ab9..00000000 --- a/md/dashscope-2.0-connection-reuse-sdk.md +++ /dev/null @@ -1,163 +0,0 @@ -# DashScope 2.0 连接复用(语音实时面 + 全模型 HTTP) - -对应《dashscope-2.0-vs-1.x-alignment-review.md》§7.4 的 **M6**。 - -## 背景与动机 - -1.x 有大量用户以**连接复用**模式接入(主要是 Java 侧)。语音业务、尤其 -实时交互对延时极敏感:每省一次握手(TCP + TLS + websocket 升级,数百 -毫秒级)就直接降低首包延迟。因此连接复用是硬需求而非优化项。 - -- **ws 复用**:1.x 的 `WsSession` 形态是"一连接一任务",短任务高频调用 - (如逐句 TTS)下握手开销占比大;多路并发(如多路 ASR)下每路一条连接 - 也浪费 fd。2.0 引入 `WsConnection`:一条物理连接上按 `header.task_id` - 解复用多个任务,支持**并发多任务**与**顺序复用**(免再次握手)。 -- **HTTP 复用(所有模型)**:不止语音——2.0 所有资源的 HTTP 请求都 - 走同一个 keep-alive 连接池(每个 `Client` 一个 httpx client,1.x 是 - 每次调用新建 `requests.Session`)。M6 把池参数开放为 settings 配置, - 并给出同一 TCP 连接被复用的可测证明。 - -## 1. ws 复用:`WsConnection` + `connection=` - -### 1.1 基本用法 - -```python -import dashscope - -client = dashscope.Client(api_key="sk-xxx") - -# 工厂方法继承 client 的 api_key / workspace / region / timeout; -# 连接由调用方持有并负责关闭。 -conn = client.ws_connection() - -# 顺序复用:两个识别任务共用一条连接,第二个任务免握手 -for audio in (pcm_1, pcm_2): - with client.audio.asr.recognize( - model="paraformer-realtime-v2", - connection=conn, - ) as rec: - rec.send_audio_frame(audio) - rec.stop() - for result in rec.results(): - ... - -# 并发:ASR 上行 + TTS 下行跑在同一条连接上 -rec = client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn).start() -tts = client.audio.tts.connect(model="cosyvoice-v1", connection=conn).start() -rec.send_audio_frame(chunk) # 二进制上行 -for piece in tts.audio_chunks(): # 二进制下行 - play(piece) - -conn.close() -``` - -支持 `connection=` 的入口(run-task 帧协议族): - -| 入口 | 方向 | -| --- | --- | -| `client.audio.asr.recognize(...)` | JSON 下行 + 二进制上行 | -| `client.audio.tts.connect(...)` | JSON + 二进制下行 | -| `client.audio.translation.recognize(...)` | JSON 下行 + 二进制上行 | -| `client.tingwu.realtime(...)` | JSON 下行 + 二进制上行 | - -传入 `connection=` 后,`ws_url` / `connector` / `api_key` / `workspace` / -`headers` 等会话级参数被忽略(它们属于连接)。会话的 `close()` 只从连接 -上摘离本任务,**不会关闭共享 socket**;`WsConnection.close()` 才关闭物理 -连接并使所有未结束任务报错。 - -### 1.2 并发约束(协议决定) - -websocket 二进制帧**不携带 task_id**,无法解复用,因此每条连接: - -- **二进制上行单 owner**:第一个 `send_audio_frame` 的任务占用上行;其他 - 任务再发二进制会 `RuntimeError`。任务结束/关闭后释放。 -- **二进制下行单 owner**:TTS 会话在 `start()` 时自动 - `claim_binary_downlink()`;连接上同时只能有一个活跃合成(第二个 - `start()` 直接报错,要并发 TTS 请用多条连接)。仅有一个活跃任务时下行 - 会隐式归属该任务。 -- **纯 JSON 任务完全并发**:事件按 `header.task_id` 路由到各自队列,无 - 数量限制。 - -典型可行组合:1 路 ASR(二进制上行)+ 1 路 TTS(二进制下行)+ 任意多 -JSON 任务。典型不可行组合:2 路并发 TTS(下行无法区分)——开两条 -`WsConnection` 即可。 - -底层对象也可单独使用(`dashscope.WsConnection` / -`dashscope.v2.transport.ws_connection.WsTask`):`conn.open_task()` 得到与 -`WsSession` 同接口的任务视图(`run_task` / `continue_task` / -`finish_task` / `send_binary` / `recv` / `recv_event` / `close`)。 - -### 1.3 读线程与超时 - -`WsConnection` 内部有一个后台读线程,socket 读永久阻塞;每个任务的接收 -超时在解复用队列上生效(`open_task(timeout=...)`,默认取连接构造时的 -`timeout`)。socket 断开或协议错误会广播给所有任务。 - -### 1.4 realtime 族:协议不可复用(验证结论) - -realtime 协议(Omni / Qwen-Audio / Livetranslate,与 OpenAI Realtime -同构)**一条连接就是一个会话**:事件按会话级类型流转、没有 task_id,且 -会话结束服务端即关闭连接——既无法并发解复用,也无法顺序复用。这不是 -SDK 限制,是协议设计。`client.audio.realtime.connect` 每次新建连接即协议 -语义的直接体现(loopback 测试已固化该行为)。 - -要压首开延迟,方向是**预热连接**(在用户开始说话前提前 `connect()`), -而不是复用;realtime 会话通常持续数十秒到数分钟,握手开销摊销后占比很 -小。受益最大的是短任务高频的 TTS 与多路并发的 ASR——即 1.1 的形态。 - -## 2. HTTP keep-alive(所有模型生效) - -### 2.1 默认行为 - -每个 `Client` 持有一个 httpx 连接池,`client.chat` / `images` / `audio` / -`files` … 所有资源请求共享:默认 `max_connections=100`、 -`max_keepalive_connections=20`、`keepalive_expiry=5s`(与 httpx 默认一致; -1.x 每次调用新建 `requests.Session`,无跨调用复用)。 -`client.with_options(...)` 在未改连接级选项时**共享**同一个池。 - -注意:池是 per-Client 的——频繁新建 `Client` 实例会击穿复用;长驻一个 -client(或 with_options 派生)才是正确姿势。 - -### 2.2 settings 配置 - -```python -import dashscope - -dashscope.settings.http_max_connections = 50 # 默认 100 -dashscope.settings.http_max_keepalive_connections = 10 # 默认 20;0 = 关闭 keep-alive -dashscope.settings.http_keepalive_expiry = 30.0 # 默认 5.0 秒 -``` - -环境变量 / `~/.dashscope/config.toml` 同名键: - -| settings | 环境变量 | 默认 | -| --- | --- | --- | -| `http_max_connections` | `DASHSCOPE_HTTP_MAX_CONNECTIONS` | 100 | -| `http_max_keepalive_connections` | `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | 20 | -| `http_keepalive_expiry` | `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | 5.0 | - -优先级与其他配置一致:settings 显式赋值 > 环境变量 > config.toml > 默认。 -校验:`max_connections >= 1`、`max_keepalive_connections >= 0`、 -`keepalive_expiry > 0`。 - -### 2.3 复用证明 - -`tests/v2/test_http_pool.py` 起本地 HTTP/1.1 服务器记录对端端口: - -- 同一 `Client` 顺序 3 次请求 → 全部命中**同一 TCP 连接**(端口一致); -- `http_max_keepalive_connections = 0` → 每次请求新连接,证明 settings → - `httpx.Limits` → 传输行为端到端生效。 - -## 3. 1.x 对照 - -| 1.x | 2.0 | -| --- | --- | -| Java 侧连接池/复用(Python 1.x 无对应物,`WsSession` 一连接一任务) | `client.ws_connection()` + 各实时入口 `connection=`,并发 + 顺序复用 | -| 每次调用新建 `requests.Session` | 每 `Client` 一个 httpx 池,默认 keep-alive;池参数开放 settings | -| realtime(omni)一会话一连接 | 同(协议决定,不可复用;建议预热) | - -## 相关文档 - -- dashscope-2.0-asr-sdk.md / dashscope-2.0-tts-sdk.md / - dashscope-2.0-translation-sdk.md / dashscope-2.0-realtime-sdk.md -- dashscope-2.0-vs-1.x-alignment-review.md(§7.2-3、§7.4 M6) diff --git a/md/dashscope-2.0-decision-sdk.md b/md/dashscope-2.0-decision-sdk.md deleted file mode 100644 index da08a780..00000000 --- a/md/dashscope-2.0-decision-sdk.md +++ /dev/null @@ -1,288 +0,0 @@ -# 决策模型(Decision / System One)Python SDK - -通过 `client.decision`,您可以针对一段业务状态(`state`),在一次前向推理中同时回答一组**类型化问题**:多选一(`choice`)、是非判断(`noul`)和有序评分(`score`)。每个答案都带有概率分布,`choice` / `score` 还附带置信度(`confidence`)。 - -决策模型(当前为 `decision-model-preview`)**不生成文本**,因此延迟和成本不随输出长度增长,适合替代"用大模型生成 JSON 再解析"的判断类环节。典型场景:工单路由、内容审核、Agent 路由、生成结果校验。 - -本文对应当前仓库的 SDK 实现;问题类型的语义、字段取值及服务端限制以决策模型 API 文档为准。 - -| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | -| --- | --- | --- | -| 单次决策 | `client.decision.create()` | `await client.decision.create()` | - -> 该接口为一次性请求-响应:不支持流式输出、函数调用(function calling)和联网搜索。 - -## 前期准备 - -1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 -2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置,目前支持 `cn-beijing`、`ap-southeast-1` 等地域。不配置工作空间且未指定端点时,当前 SDK 会回退到共享网关并发出警告。 -3. 在本仓库根目录执行源码安装: - - ```bash - python -m pip install -e . - ``` - -客户端使用公开入口 `from dashscope import Client, AsyncClient`。也可以在创建客户端时显式传入 `api_key`、`workspace`、`region`;本文示例从环境配置读取凭据。 - -## 快速开始 - -### 同步调用:工单路由 - -以下示例对一张工单同时提出三个问题:应由哪个团队处理(多选一)、是否需要通知值班人员(是非)、严重程度(评分)。可保存为 `decision_example.py` 直接运行。 - -```python -from dashscope import Client -from dashscope.v2.resources.decision import ( - ChoiceQuestion, - NoulQuestion, - ScoreQuestion, -) - -state = { - "ticket_id": "T-1001", - "content": "订单支付后超过 24 小时仍未到账,用户无法继续使用核心服务。", -} - -questions = { - "department": ChoiceQuestion( - instructions="应该由哪个团队处理?", - criteria={ - "billing": "支付、退款和账单问题", - "technical": "产品故障和集成问题", - }, - ), - "escalate": NoulQuestion(instructions="是否需要立即通知值班人员?"), - "severity": ScoreQuestion( - instructions="这个问题有多严重?", - criteria=[ - "轻微问题,不影响功能", - "部分功能受影响,但存在替代方案", - "核心功能不可用,没有替代方案", - "造成严重业务或安全影响", - ], - ), -} - -with Client() as client: - resp = client.decision.create( - model="decision-model-preview", - state=state, - questions=questions, - ) - -print("request_id:", resp.request_id) - -department = resp.answers["department"] -print("department:", department.choice) # billing -print("confidence:", department.confidence) # 0.88 -print("probabilities:", department.probabilities) # {"billing": 0.94, "technical": 0.06} - -escalate = resp.answers["escalate"] -print("escalate P(yes):", escalate.noul) # 0.99 - -severity = resp.answers["severity"] -print("severity:", severity.score) # 2.25(概率加权等级索引) -print("legend:", severity.legend["2"]) # 核心功能不可用,没有替代方案 - -print("input_tokens:", resp.usage.input_tokens) # 125 -print("latency_ms:", resp.latency_ms) # 52.9 -``` - -- `questions` 的键是您自定义的问题 ID,`answers` 按相同的键返回,一一对应。 -- 一次调用可提多个问题,只产生一次模型前向推理,按输入 token 计费(`usage` 中只有 `input_tokens`)。 -- `score` 答案是**概率加权等级索引**,可能落在两个等级之间(如上例 `2.25` 介于等级 2 和 3 之间);`legend` 把等级索引映射回您给的等级描述,索引以字符串为键。 - -### 三种问题类型 - -| 类型 | 模型类 | `criteria` | 答案字段 | 说明 | -| --- | --- | --- | --- | --- | -| 多选一 | `ChoiceQuestion` | `Dict[str, str]`,必填,1–255 个选项 | `choice`(选中的选项名)、`confidence`、`probabilities` | `criteria` 映射"选项名 → 选项描述";请给全选项集合,建议增加 `other` 兜底选项。 | -| 是非判断 | `NoulQuestion` | `Dict[str, str]`,可选 | `noul`(P(yes),0=否、1=是)、`probabilities` | `criteria` 可描述两个结果,如 `{"true": "...", "false": "..."}`;判定阈值由应用自定(如 0.5)。 | -| 有序评分 | `ScoreQuestion` | `List[str]`,必填,2–255 级 | `score`(概率加权等级索引)、`confidence`、`probabilities`、`legend` | `criteria` 按从低到高描述各等级;3–7 个清晰可区分的等级效果最佳。 | - -三个模型类都有可选字段 `instructions`(对该问题的补充说明)。未设置的可选字段不会出现在请求报文中(不会发送显式 `null`)。 - -### 原始字典形式(前向兼容) - -`questions` 的值也可以直接传原始 wire 字典,SDK 原样透传(包括 SDK 模型尚未收录的未来字段): - -```python -resp = client.decision.create( - model="decision-model-preview", - state="这段评论是否在索要退款?", # state 也可以是纯文本 - questions={ - "refund": { - "type": "noul", - "instructions": "是否在索要退款?", - "future_field": 1, # 未知字段透传,便于前向兼容 - }, - }, -) -print(resp.answers["refund"].noul) -``` - -类型化模型(`ChoiceQuestion` 等)会在本地做 pydantic 校验,能在发送前发现错误;原始字典不做本地校验,适合使用 SDK 尚未支持的新字段。 - -### 异步调用 - -`AsyncClient` 提供相同的接口,需 `await`: - -```python -import asyncio - -from dashscope import AsyncClient - - -async def main(): - async with AsyncClient() as client: - resp = await client.decision.create( - model="decision-model-preview", - state={"ticket_id": "T-1001", "content": "..."}, - questions={"q": {"type": "noul", "instructions": "是否需要人工介入?"}}, - ) - print(resp.answers["q"].noul) - - -asyncio.run(main()) -``` - -## 请求参数 - -### `client.decision.create()` - -所有参数均为关键字参数。 - -| 参数 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `model` | `str` | 是 | 模型名称,当前为 `decision-model-preview`。 | -| `state` | `str \| dict \| list` | 是 | 待决策的业务状态:自由文本、一段对话或结构化对象;结构化对象会被序列化后交给模型。 | -| `questions` | `Dict[str, QuestionInput]` | 是 | 问题集合,键为问题 ID,值为 `ChoiceQuestion` / `NoulQuestion` / `ScoreQuestion` 实例或原始字典。不能为空,否则抛出 `ValueError`。 | -| `timeout` | `float` | 否 | 本次请求超时时间(秒),覆盖客户端级 `timeout`。 | -| `api_key` | `str` | 否 | 本次请求使用的 API Key,覆盖客户端级配置。 | -| `workspace` | `str` | 否 | 本次请求使用的工作空间,覆盖客户端级配置。 | - -### 问题模型字段 - -`ChoiceQuestion`: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `type` | `Literal["choice"]` | 固定值 | 默认 `"choice"`,无需设置。 | -| `instructions` | `str` | 否 | 问题说明。 | -| `criteria` | `Dict[str, str]` | 是 | 选项名 → 选项描述,1–255 个;为空或缺失会抛出 `ValidationError`。 | - -`NoulQuestion`: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `type` | `Literal["noul"]` | 固定值 | 默认 `"noul"`,无需设置。 | -| `instructions` | `str` | 否 | 问题说明。 | -| `criteria` | `Dict[str, str]` | 否 | 描述两个结果,如 `{"true": "...", "false": "..."}`;裸 `NoulQuestion()` 合法。 | - -`ScoreQuestion`: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `type` | `Literal["score"]` | 固定值 | 默认 `"score"`,无需设置。 | -| `instructions` | `str` | 否 | 问题说明。 | -| `criteria` | `List[str]` | 是 | 从低到高的等级描述,2–255 级;少于 2 级会抛出 `ValidationError`。 | - -三个模型类均允许额外字段(`extra="allow"`),额外字段随报文透传。 - -## 返回字段 - -### `DecisionResponse` - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `answers` | `Dict[str, DecisionAnswer]` | 各问题的答案,键与请求的 `questions` 键一致。 | -| `model` | `str` | 实际应答的模型名。 | -| `usage` | `Usage` 或 `None` | 用量对象;决策模型只产生 `input_tokens`(如 `125`),不生成输出 token。 | -| `latency_ms` | `float` 或 `None` | 服务端推理耗时(毫秒)。 | -| `request_id` | `str` | 请求 ID,排查问题时请提供。 | -| `headers` | `Dict[str, str]` | 响应头的小写字典副本,如 `resp.headers["x-request-id"]`。 | -| `status_code` / `code` / `message` / `is_error` | - | 错误信封字段,正常响应时 `is_error` 为 `False`;见"异常处理"。 | - -### `DecisionAnswer` - -按 `type` 填充对应字段,未涉及的字段为 `None`: - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `type` | `str` | 答案类型:`"choice"` / `"noul"` / `"score"`。 | -| `choice` | `str` | 多选一答案:选中的选项名。 | -| `noul` | `float` | 是非答案:P(yes),0 表示否、1 表示是。 | -| `score` | `float` | 评分答案:概率加权等级索引,可能落在两个等级之间(如 `1.43`)。 | -| `probabilities` | `Dict[str, float]` | 每个选项 / 等级的概率,总和为 1。 | -| `confidence` | `float` | 置信度;仅 `choice` / `score` 返回答案时携带。 | -| `legend` | `Dict[str, str]` | 仅 `score`:等级索引(字符串键)→ 等级描述。 | - -响应为平坦结构(无 `output` 信封),额外字段同样透传保留。 - -## 端点与路由 - -决策模型的服务端点为 `POST {网关}/compatible-mode/v1/systemone`。它挂在每个网关的 compatible-mode 路径下,**与客户端的 `protocol` 设置无关**——无论 chat 协议配置为 `native`、`openai` 还是 `anthropic`,决策请求都发往同一路径: - -- 默认(配置工作空间):`https://{workspace}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone`。 -- 显式 `base_url`:以您配置的 `base_url` 为服务根,请求发往 `{base_url}/systemone`(与 chat 的 base_url 规则一致)。 -- 未配置工作空间且未指定端点:回退到共享网关(如 `https://dashscope.aliyuncs.com/compatible-mode/v1/systemone`)并发出 `UserWarning`;生产环境请配置工作空间。 - -## 异常处理 - -默认 `raise_on_error=True` 时,以下失败都会抛出异常: - -| 情况 | 行为与处理建议 | -| --- | --- | -| HTTP 错误(4xx/5xx) | 按服务端错误码映射为 `DashScopeError` 子类抛出,如参数错误抛 `BadRequestError`、限流抛 `RateLimitError`、鉴权失败抛 `AuthenticationError`;检查异常的 `code`、`message`、`status_code`、`request_id`。 | -| 业务错误层(HTTP 200 + 错误体) | 服务端以 200 返回 `{"code", "message"}` 时同样按错误码抛出(如 `BadRequestError`),此时 `status_code` 为 200;不要仅凭 HTTP 状态判断成功。 | -| 本地参数错误 | `questions` 为空字典抛出 `ValueError`;问题模型字段不满足约束(如 `ChoiceQuestion` 无选项、`ScoreQuestion` 少于 2 级)抛出 pydantic `ValidationError`,请求不会发出。 | - -`Client(raise_on_error=False)` 时不抛异常,改为填充返回对象的错误信封: - -```python -client = Client(raise_on_error=False) -resp = client.decision.create( - model="decision-model-preview", - state="...", - questions={"q": {"type": "noul"}}, -) -if resp.is_error: - print(resp.code, resp.message, resp.request_id) - # answers 为空字典,不要按正常答案读取 -``` - -- 网络层失败(连接失败、超时)抛出 `APIConnectionError` / `APITimeoutError`。 -- 排查时请记录异常类型与 `request_id`;不要记录 API Key 或未经脱敏的业务状态内容。 - -## 常见问题 - -**为什么 `score` 答案是小数?** - -`score` 是各等级概率加权后的索引,不是离散等级。例如 `2.25` 表示答案主要分布在等级 2、部分落在等级 3(可结合 `probabilities` 查看分布)。需要离散等级时可四舍五入或按阈值判定。 - -**`noul` 返回 0.99 可以直接当作"是"吗?** - -`noul` 是 P(yes) 概率,不是布尔值。判定阈值由应用决定(常用 0.5);对关键操作可结合 `probabilities` 或设置更高阈值。 - -**为什么有的答案没有 `confidence`?** - -`confidence` 仅随 `choice` / `score` 答案返回;`noul` 答案可通过 `probabilities` 自行评估确定程度。 - -**配置了 `protocol="anthropic"`(或 `openai`),决策请求的端点会跟着变吗?** - -不会。`protocol` 只影响 chat 族接口的路由;决策模型固定走 compatible-mode 路径,三种协议配置下URL 完全一致。 - -**可以一次传多少个问题?** - -`questions` 至少一个(空字典会抛出 `ValueError`),多个问题共享一次前向推理。问题数量、单字段长度等上限以服务端限制为准;建议问题 ID 使用简短、可读、稳定的标识,答案按键原样返回。 - -**支持流式输出或 function calling 吗?** - -不支持。决策模型不生成文本,一次性返回全部答案;需要生成能力请使用 chat 族接口,再把生成结果交给 `decision.create()` 做校验是常见的组合用法。 - -## 相关文档 - -- [DashScope 2.0 设计](dashscope-2.0-design.md) -- [DashScope 2.0 迁移指南](dashscope-2.0-migration-guide.md) -- [语音识别 Python SDK](dashscope-2.0-asr-sdk.md) -- [语音合成 Python SDK](dashscope-2.0-tts-sdk.md) diff --git a/md/dashscope-2.0-design.md b/md/dashscope-2.0-design.md deleted file mode 100644 index 06613bf8..00000000 --- a/md/dashscope-2.0-design.md +++ /dev/null @@ -1,604 +0,0 @@ -# dashscope 2.0 设计方案 - -> 基于 V2 分支现状(见 `md/dashscope-interfaces.md` 全量接口清单),规划 dashscope 2.0。 - -## 0. 目标与原则 - -**目标** - -1. **用户接口友好**:一个现代化的 SDK + CLI(`dashscope`),以及一个 Agentic CLI(`dashscope expert`)。 -2. **协议兼容**:通过配置或传入 config,兼容 Anthropic / OpenAI 协议(既可作为客户端消费这两种协议,也保证 dashscope 自身能力可按这两种协议表达)。 -3. **接口自动化生成**:SDK / CLI / 文档 / 测试从单一 API 元数据源自动生成,人工只维护元数据与少量手写覆盖。 - -**原则**(对比调研后确立/修订) - -- **P1 对齐行业基线,不做发明**:Client 实例化、Pydantic 类型化、异常驱动、自动重试、httpx——OpenAI/Anthropic(同为 Stainless 生成)已验证的现代 SDK 基线,直接对齐,不自创风格。 -- **P2 契约正确性优先于功能数量**:错误被吞、退出码撒谎、流中出错不报——这类"静默错误"是调研发现的最高危问题,修复优先级高于任何新能力。 -- **P3 渐进迁移,不破坏存量**:1.x 代码冻结、2.0 物理隔离新建(§2.4 零接触方案),不建兼容层;DeprecationWarning 引导迁移;1.x 快修(M0)在维护分支单独实施,不等 2.0。 -- **P4 保留自有优势**:异步长任务(async_call/fetch/wait)、多模态 content 格式(`{"text":..., "image":...}`)、RL/微调/部署全流程是调研确认的领先项,2.0 统一化但**不削弱、不下线**。 -- **P5 差异化定位**:CLI 不跟随 Agent 赛道(expert 负责),主打两家已放弃的"AI 平台长流程管理"品类;SDK 不做厂商专属适配,靠协议层通吃(第三方模型已收敛到 OpenAI/Anthropic 兼容)。 -- **P6 codegen 是主线不是后置**:调研证实手写 vs 流水线是代际差距(OpenAI/Anthropic 均为 spec 生成)。specs 元数据从 M1 起同步建设,M6 全面接管,而非等功能写完再补。 - -现状问题(为什么要 2.0): - -- SDK 接口形态不统一:`Generation.call`、`AioGeneration.call`、`ImageSynthesis.async_call/fetch/wait` 三套风格并存,参数散落在 `**kw`,无统一 Client 对象。 -- CLI 与 SDK 一一映射、命令冗长(`dashscope multimodal_conversation create`),缺少面向场景的友好命令。 -- 协议层只有自有 `protocol/websocket.py`,OpenAI/Anthropic 兼容散落在 acli 的 providers 中,未沉淀为 SDK 能力。 -- 接口靠手写,SDK/CLI/文档三处重复劳动,易漂移。 -- (调研新增)错误契约弱:返回值式错误处理易漏检、核心路径无自动重试、流式出错不退出/不可解析、日志缺 request_id 且无敏感信息过滤。 -- (调研新增)战略窗口期:OpenAI/Anthropic 已放弃"API 管理 CLI"品类、其 SDK 全面 spec 生成化——DashScope 若不在 2.0 补齐契约与流水线,差距将从"体验落后"固化为"代际落后"。 - -### 对比调研共识(→ 设计硬需求) - -对 OpenAI/Anthropic SDK+CLI 的八份对比调研(见 `md/` 目录)收敛出的共识,全部纳入本方案: - -| 调研结论 | 出处 | 2.0 落点 | 预期收益 | -| --- | --- | --- | --- | -| Client 实例化 + 链式资源导航(类方法 + 全局变量是最大槽点) | sdk-* 对比 ×4 | §2.1 Client 资源式 API | 易用性:多租户/多环境安全,IDE 补全全链路可用 | -| 异常驱动错误处理(按 HTTP 状态细分异常类)+ 错误码枚举 | sdk-source-level-comparison-part2 | §2.7 错误模型(§2.2 摘要) | 稳定性:错误不再被漏检(返回值模式漏写 if 即静默失败);重试/降级逻辑可收敛 | -| 内置自动重试(默认 2 次 + Retry-After + 指数退避),DashScope 核心路径目前无重试 | sdk-source-level-comparison-part2 §2.4 | §2.2 传输层 | 稳定性:限流/抖动场景成功率直接提升,用户最痛点 | -| Pydantic 类型化响应 + `@overload` 精确签名(流式/非流式类型层面可区分) | sdk-source-level-comparison-part2 §1.3 | §2.2 约定 | 易用性:字段拼写错误静态期暴露,杜绝运行时 KeyError | -| 流式人体工学:text_stream 纯文本迭代器、final message 聚合、流内错误抛异常 | sdk-deep-comparison §3.3 | §2.2 流式统一模式 | 易用性 + 稳定性:用户少写累积/检查样板;流中错误不再被吞 | -| 便捷属性(`resp.output_text` 一行取文本)+ Quickstart 极简入口 | sdk-comparison-openai-vs-dashscope | §2.5 | 易用性:上手代码从 6 行降到 2 行,对齐 OpenAI 首屏体验 | -| httpx 统一同步/异步,消除 requests/aiohttp 双栈 | sdk-source-level-comparison-part2 §3.4 | §2.2 传输层 | 性能:连接池复用减少每请求 TCP/TLS 握手;包体积与双栈漂移风险同降 | -| 日志:request_id 入日志、敏感头过滤、重试原因/等待时间 | sdk-deep-comparison §6.7 | §2.2 日志 | 可运维性:排障可定位到工单级;消除 API Key 泄露入日志的安全隐患 | -| OpenAI/Anthropic SDK 均为 Stainless 从 OpenAPI spec 生成,手写 vs 流水线是代际差距 | conclusion-sdk-architecture-stainless | §6 codegen 升为战略项 | 研发效率:新参数上线从"人肉改签名+文档+测试"(周级)降到改 spec 自动生成(天级),消除漂移 | -| OpenAI 已删除 API CLI、Anthropic 从未有过;"API 管理 CLI"品类只剩 DashScope,独特价值在长流程管理(ft/部署/RL) | cli-source-level-comparison-2026-08 §0 | §4 定位 | 战略:占据两家放弃的品类空位,差异化而非跟随 | -| CLI 立即修:流式出错 exit 0 bug、shell 补全、auth whoami、`--output-format json` | cli-source-level-comparison-2026-08 §5 | §4.2 | 稳定性(CI 误判成功)+ 易用性(补全/认证探测)+ 可集成性(jq 可解析) | -| MiniMax/DeepSeek/Baseten/Kimi 协议已收敛到 OpenAI/Anthropic 兼容,无需厂商专属 adapter | vendor-sdk-research-compat | §3.3 | 研发效率:避免 N 个厂商 adapter 的维护负担;第三方模型开箱即用 | - -**收益汇总**(按维度): - -- **易用性**:上手 2 行代码、IDE 全补全、错误不用逐个 if 检查、流式无需手工累积——对齐 OpenAI/Anthropic 的现代基线(调研评分中 DashScope 差距最大的三项:类型提示、错误契约、流式人体工学)。 -- **稳定性**:自动重试 + 异常驱动 + 流内错误抛出 + CLI 退出码修复,覆盖"请求失败被静默吞掉"的全部已知路径;这是当前 SDK/CLI 唯一会导致用户数据/CI 结果错误的类别。 -- **性能**:httpx 连接池复用(现状每次请求新建 requests.Session);高并发场景减少握手开销,异步路径与同步共用一套实现、无行为漂移。 -- **研发效率**:codegen 使"新增一个 API 参数"的成本从周级手写降到天级自动生成,SDK/CLI/文档/测试四处不再漂移。 -- **战略**:独占"AI 平台长流程管理 CLI"品类 + 第三方模型零成本接入,构成 2.0 的差异化护城河。 - ---- - -## 1. 总体架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ dashscope expert(Agentic CLI,演进自 dashscope.acli) │ -│ TUI/REPL、session、memory、mcp、tools、skills │ -├─────────────────────────────────────────────────────────────┤ -│ dashscope CLI(dashscope.cli 重写) │ -│ 命令树 = SDK 资源树的自动映射 + 场景化快捷命令 │ -├─────────────────────────────────────────────────────────────┤ -│ dashscope SDK 2.0(dashscope 包根) │ -│ Client 资源式 API(chat/images/audio/files/finetunes/...) │ -├──────────────────────┬──────────────────────────────────────┤ -│ Protocol 适配层 │ Transport 层 │ -│ openai / anthropic │ HTTP(SSE) / WebSocket / 任务轮询 │ -│ / dashscope-native │ │ -├──────────────────────┴──────────────────────────────────────┤ -│ API 元数据(specs/*.yaml)→ codegen → SDK/CLI/文档/测试 │ -└─────────────────────────────────────────────────────────────┘ -``` - -分层原则:expert 只用 SDK 公开 API;CLI 是 SDK 的薄壳;协议与传输在 SDK 内部隔离,上层无感。 - ---- - -## 2. SDK 2.0 设计 - -### 2.1 统一入口:资源式 Client - -对齐 OpenAI/Anthropic 的用户心智,一个 `Client` 对象挂所有资源;同步 + 异步双版本: - -```python -import dashscope - -client = dashscope.Client(api_key=..., protocol="openai" | "anthropic" | "native", base_url=...) - -resp = client.chat.completions.create(model="qwen-plus", messages=[...], stream=True, tools=[...]) -img = client.images.generate(model="wanx2.1-t2i", prompt="...", n=1, size="1024*1024") -aclient = dashscope.AsyncClient(...) -``` - -### 2.2 资源树与现有接口映射 - -| 新资源 | 覆盖现有接口(见 interfaces 清单) | 关键方法 | -| --- | --- | --- | -| `client.chat` | `Generation` / `AioGeneration` / `Conversation` / `CodeGeneration` / `Understanding` | `completions.create`(messages 统一格式,tools/stream 一等参数) | -| `client.apps` | `Application` | `completions.create(app_id, prompt)` | -| `client.multimodal` | `MultiModalConversation` / `AioMultiModalConversation` | `chat.create`(content parts:text/image_url/audio/video) | -| `client.images` | `ImageSynthesis` / `ImageGeneration` | `generate`(同步封装)/ `jobs.create/fetch/wait`(异步任务);按模型名路由协议——wan2.5 及以下走旧 text2image 端点(`/services/aigc/text2image/image-synthesis`,仅异步任务,`prompt` 入参 / `results` 出参);wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`generate` 同步打 `/services/aigc/multimodal-generation/generation`,`jobs` 异步任务打 `/services/aigc/image-generation/generation`,`messages` 入参 / `choices` 出参,出参归一化为统一 `Job.results`) | -| `client.videos` | `VideoSynthesis` | `generate` / `jobs.create/fetch/wait`;同端点(`/services/aigc/video-generation/video-synthesis`,仅异步任务,`video_url` 出参)下按模型名路由输入协议——wan2.6 及以下走旧首帧协议(`img_url` 入参,公开名 `image_url`);wan2.7-i2v 只应答新 i2v 协议(`media` 入参:`first_frame` / `last_frame` / `driving_audio` / `first_clip` 类型化素材列表、每种至多一个、必填,`image_url` 归一为单个 `first_frame` 素材项) | -| `client.embeddings` | `TextEmbedding` / `MultiModalEmbedding` / `BatchTextEmbedding` | `create` / `jobs.*`(批量任务) | -| `client.rerank` | `TextReRank` / `AioTextReRank` | `create(query, documents)` | -| `client.tokenization` | `Tokenization` | `count(prompt)` | -| `client.audio.asr` | `Recognition` / `Transcription` / `QwenTranscription` | `stream`(实时 ws)/ `transcribe(file_urls)` | -| `client.audio.tts` | `SpeechSynthesizer` / `tts_v2` / `HttpSpeechSynthesizer` / `qwen_tts` | `synthesize`(一次性)/ `stream`(流式) | -| `client.audio.realtime` | `OmniRealtimeConversation` | `session()`(ws 会话上下文管理器) | -| `client.fine_tunes` | `FineTunes` | `create/get/list/cancel/delete/stream_events/wait` | -| `client.deployments` | `Deployments` | `create/get/list/scale/delete` | -| `client.files` | `Files` | `upload/list/get/delete` | -| `client.models` | `Models` | `list/get` | -| `client.rl` | (CLI 的 `dashscope rl` 能力 SDK 化) | `register_functions/test_functions/upload_data/run/get/cancel/logs/list`;**复用 1.x 既有实现,不重写**——自有逻辑多,为避免影响 1.x,2.0 仅在 errors 与底层 HTTP 调用上对齐 v2,业务逻辑一律不动(见 3.7) | -| ~~`client.beta.assistants`~~ | `Assistants/Threads/Messages/Runs/Steps` | **整个 Assistants API 族确定下线,不迁入 2.0**:1.x 原模块冻结保留至 3.0 随旧代码删除,2.0 资源树中无此项(托管 Agent 场景由 `client.agentstudio` 承接) | -| `client.agentstudio` | `dashscope.agentstudio`(agents/sessions/events,含流式事件) | 托管 Agent API;1.x 子 SDK 代码已较新、与 2.0 风格接近,**仅合并入统一 Client,不动原来的代码和逻辑**;errors 与底层 HTTP 调用与 2.0 的对齐由专门环节统一处理(见 M3 任务 3.12:挂载层映射包装,不触碰 1.x 代码,不与 §2.4 冲突) | - -约定: - -- **长任务统一模式**:`jobs.create → fetch/wait → result`,`wait` 内置退避轮询;所有任务资源返回统一的 `Job` 对象(`id/status/result/error`)。 -- **同资源多协议按模型路由**:同一资源下不同模型落在不同服务端点/协议时,按模型名自动路由,不向用户暴露端点选择参数(与 `chat.completions` 含媒体 part 自动改走多模态端点同一约定)。当前应用点:`client.images`(见上表:wan2.6-t2i / wan2.6-image → multimodal-generation 协议,其余 → 旧 text2image 端点)、`client.videos`(wan2.7-i2v → media 新 i2v 协议,其余 → 旧 `img_url` 首帧协议)。新协议模型清单分别以 `v2/resources/images.py` 的 `MULTIMODAL_MODEL_PREFIXES`、`v2/resources/videos.py` 的 `MEDIA_MODEL_PREFIXES` 为准,新模型上线时前缀扩充。 -- **流式统一模式**:`stream=True` 一律返回类型化迭代器(`Stream[Chunk]` / `AsyncStream[Chunk]`,用 `@overload` 让流式/非流式返回类型在静态层面可区分);提供 `stream.text_stream` 纯文本迭代器与 `get_final_message()` 聚合助手;**流内错误统一抛异常,不 yield 错误对象**。ws 实时接口用上下文管理器。例外:`client.apps` 在 `raise_on_error=False` 兼容模式下以 `is_error=True` 终止事件透出流内错误(对齐 1.x `Application` 流式返回值语义),不回抛;chat/multimodal 无此例外。 -- **类型化**:入参出参全部 Pydantic 模型,`**kw` 仅保留 `extra_headers/timeout` 等逃逸舱;响应提供便捷属性(如 `resp.output_text` 一行取文本);内置 `.pyi`。 -- **错误模型**(对齐 OpenAI/Anthropic,见对比调研;**规范正文见 §2.7**): - - 默认异常驱动:HTTP/业务失败抛 `DashScopeError` 层级,按状态码细分为 9 个扁平类 `BadRequestError(400)` / `AuthenticationError(401)` / `PermissionDeniedError(403)` / `NotFoundError(404)` / `RequestTooLargeError(413)` / `RateLimitError(429)` / `InternalServerError(500)` / `ServiceUnavailableError(503)` / `GatewayTimeoutError(504)`;另有传输层 `APIConnectionError` / `APITimeoutError`、内容审核 `ContentFilterError`、SDK 自报 `SdkError`; - - 异常携带 `status_code / code / message / request_id / body / response.headers`; - - 成功响应同样携带 `resp.headers`(网关原始 HTTP 响应头,1.x `GenerationResponse.headers` 对齐):由 `types.common.with_headers` 在每个非流式 HTTP 成功返回点填充(`with_headers` 按结构化 Protocol 接受任何声明了 `headers` 字段的模型,不限于 `ResponseBase` 子类,如 audio `HttpSynthesis`);流式经 `Stream.headers`(chat)、`AppStream.headers`、`HttpSynthesisStream.headers` 透出,chat 另由 `get_final_message()` 回填到聚合结果;ws 资源无 HTTP 信封,保持空 dict;成功侧 `resp.headers` 是小写键的普通 dict(同名多值头以 `", "` 拼接,保证 `model_dump()` 可序列化),而 `errors.response_error` 放在异常上的是活的 `httpx.Headers`(大小写不敏感);"HTTP 200 但业务失败"(如任务 FAILED)抛出的异常同样携带 `headers`,且 `err.body` 只含 wire/作业快照,不会把 `headers` 字段带进去(调用方常直接记录 `err.body`); - - `"Throttling"`、`"InvalidApiKey"` 等字符串错误码收敛为可 import 的枚举常量(`dashscope.v2.errors.ErrorCode`,13 项;包根不导出),完整对照表见 §2.7; - - 兼容开关:`Client(raise_on_error=False)` 退回 1.x 返回值模式; - - 保留"HTTP 200 但业务失败"的双层检查(DashScope 网关特性,现有 `ensure_ok` 语义上移到 SDK)。 -- **内置重试**:默认 `max_retries=2`,429/5xx/连接错误自动重试,解析 `Retry-After`,指数退避 + 抖动;`Client(max_retries=N)` / `with_options()` 可调。 -- **传输层**:httpx 统一同步/异步(连接池复用),替换 requests + aiohttp 双栈;超时支持 connect/read 分离。 -- **日志**:`DASHSCOPE_LOG=debug` 一键开启;记录 request_id、HTTP 状态码、重试原因与等待时间;SensitiveHeadersFilter 过滤 Authorization,防止密钥入日志。 - -### 2.3 配置 - -```python -dashscope.Client(api_key=..., base_url=..., protocol=..., timeout=..., max_retries=...) -``` - -优先级:显式参数 > 环境变量(`DASHSCOPE_API_KEY` 等) > `~/.dashscope/config.toml`。1.x 的 `dashscope.api_key` 等模块级全局量原样冻结保留(供存量代码使用,deprecate 标注);2.0 新代码一律用 `Client(...)` 显式配置,两者互不影响。 - -### 2.4 兼容与长期共存策略(零接触物理隔离) - -前提修正:原"两个 minor 版本后移除"不成立,1.x 预计 1–2 年内无法下线。曾评估"单代码库、双门面共享核心"方案,结论是共享核心的行为渗透防护(语义隔离 + 契约测试)成本高于收益——**2.0 采用零接触物理隔离:1.0 代码一行不动、整体冻结,2.0 完全独立新建,HTTP/传输不复用**。 - -``` -用户代码(1.x 写法) ── dashscope.Generation.call / dashscope.cli(冻结,不动) -用户代码(2.0 写法) ── dashscope.Client / dashscope.chat(包根导出) - │ - dashscope/v2/(独立实现:httpx transport / protocols / resources) -``` - -**架构规则** - -1. **1.x 代码冻结**:现有模块(`Generation` 等类方法接口、`dashscope/cli/` 旧命令树、requests/aiohttp 传输)保留但不再修改(仅安全补丁走 1.x 维护分支);不建兼容层、不做转调——用户继续 `import dashscope` 用旧写法,行为逐字节不变。 -2. **2.0 独立目录**:全部新实现放 `dashscope/v2/`(client / resources / protocols / transport / errors),不与 1.x 共享任何请求代码(例外见规则 4);依赖共存(requests/aiohttp 留给 1.x,httpx 供 2.0),默认完整安装,不做 extras 拆分。 -3. **包根只导新面**:`dashscope/__init__.py` 导出 2.0 公开 API——`dashscope.Client` / `dashscope.AsyncClient` / `dashscope.chat()`——用户 import 路径无 `v2` 字样;`v2` 只是内部目录名,不是公开命名空间。 -4. **白名单式共享**:唯一允许的共享是零逻辑常量(端点 URL、模型名常量等),逐项白名单评审;任何含行为的代码一律不共享。**例外**:`rl` 与 `agentstudio` 两块直接复用 1.x 既有实现代码,不重写。**基本原则:两块的业务逻辑一律不动**,仅对齐 errors 与底层 HTTP 调用——`rl` 自有逻辑多,2.0 只建挂载层;`agentstudio` 已较新,仅合并入 Client(见 M3 任务 3.7/3.8)。**白名单登记**(2.0 期间对 1.x 的全部实际触碰,此外零触碰):① 包根 `dashscope/__init__.py` 导出 2.0 公开面(规则 3 授权);② `0c58a74` 将 `dashscope/finetune/reinforcement/common/constants.py` 的 `LOGGER_NAME` 默认值从 `dashscope` 改为 `dashscope.finetune.reinforcement`(rl 日志挂独立 logger 名,避免与包根 logger 配置互相干扰)。 -5. **新需求只进 2.0**:新接口、新参数、新模型能力一律只进 `v2/` 与新命令树;1.x(含旧 CLI 命令)不新增支持,文档引导用户为新能力迁移。 -6. **测试互不影响**:现有 1.x 测试套件原样保留作冻结回归(不改、不扩);2.0 测试全部新建于 `tests/v2/`。 -7. **下线即删目录**:3.0 时整目录删除 `dashscope/v2/` 之外全部旧代码并上移,无兼容层代码残留——删除动作机械、无风险。 - -**acli 独立于新旧隔离**:`dashscope/acli/` 代码量大,2.0 原样复用、不重写;它既不在 1.x 冻结集、也不进 `v2/`,作为第三条线独立演进(提交约束见 §7"开发与提交约束"),M5 仅做底座切换与 tools 注册。3.0 包结构重整时需同步迁移适配(见后续任务 8.1)。 - -**CLI 侧** - -8. 旧命令(`generation create`、`multimodal_conversation create` 等)不动、继续调用旧模块;新命令树(`chat`/`config`/`auth`/`images`...)单独建,只 import `v2/`。 -9. 新旧命令并存于同一 `dashscope` 入口。`ft`/`rl` 等习惯名由新命令树直接承接(参数/输出按 §4 改进,向后兼容旧参数名);其余旧命令名(`generation` 等)原样冻结,3.0 随旧代码一并删除。 - -**下线流程(遥测驱动,不设死线)** - -10. 旧接口挂 DeprecationWarning(Python 默认不显示,不扰民;此为唯一允许的 1.x 触碰),文档/官网推迁移指南; -11. 埋点统计旧接口调用占比(包版本 + 入口名,脱敏),每季度评估; -12. 移除条件:旧接口调用占比低于阈值(如 <5%)且无大客户依赖反馈 → 3.0 整目录删除旧代码,而非 2.x 中途砍掉; -13. 共存期内 1.x 与 2.0 接口使用同一版本号发布(`dashscope>=2.0` 即含两者),不做双包分发,避免用户选型困惑。 - -**成本与收益**:代价是包内短期存在两套 HTTP 依赖与部分重复实现(包体积略增、极个别 bug 需修两处);收益是 1.x 回归风险在结构上归零(不存在共享代码,渗透无从发生)、无兼容层开发维护成本、新需求迭代不触碰旧代码、3.0 下线是一次机械删除。参考先例:Azure SDK track1/track2、boto/boto3、openai v0→v1 均为物理隔离式换代。 - -### 2.5 Quickstart 体验(对齐 OpenAI 的 5 行上手) - -```python -import dashscope - -resp = dashscope.chat("qwen-plus", "讲个笑话") # 模块级极简入口(内部单例 Client) -print(resp.output_text) # 一行取文本 -``` - -- `dashscope.chat(model, prompt, **kw)` 为官方 Quickstart 首推写法,文档首页即用此例; -- 进阶用户切到 `dashscope.Client()` 资源式 API,两者返回同一套 Pydantic 模型。 - -### 2.6 命名规范(长期约束) - -**本节是硬约束**:`dashscope/v2/` 下新增任何资源、方法、参数或响应模型都必须遵守,评审时按此逐条核对。规范来自对全量 38 个响应模型 / 90+ 公开方法的横向评审(诊断过程与逐条改名清单见 [`dashscope-2.0-naming-review.md`](dashscope-2.0-naming-review.md),本节只落结论)。 - -命名的唯一目标:**同一个语义在全 SDK 只有一个名字,客户不需要猜**。因此规范以"消灭同义词"为主线,而不是以"取个好名字"为主线。 - -**动词收敛到 8 类**(不允许出现表外动词;表内动词的阻塞语义固定): - -| 语义 | 动词 | 阻塞 | 适用 | -| --- | --- | --- | --- | -| 同步推理 | `create` | 是 | chat / multimodal.chat / multimodal.embeddings / embeddings / rerank / apps.completions | -| 长任务提交 | `jobs.create` | **否** | images / videos / audio.asr / embeddings / fine_tunes / rl | -| 长任务查询·等待 | `jobs.fetch` / `jobs.wait` | 否 / 是 | 同上 | -| 一步式语法糖 | `generate` / `transcribe` | 是 | images / videos / audio.asr(= `create` + `wait`) | -| 持久资源 CRUD | `get` / `list` / `delete` / `cancel` / `scale` | 否 | fine_tunes / deployments / models / files | -| 双工会话 | `recognize` / `connect` | ctx manager | audio.asr / audio.realtime | -| 纯计算·文件 | `count` / `upload` | 是 | tokenization / files | -| 流式 | `stream=True` 或 `.stream()` / `.events()` | — | 见下"流式形态" | - -**阻塞规则(P0)**:`.create()` / `jobs.create()` **永不阻塞**,只提交并返回任务快照;`wait()` 才阻塞。裸领域动词(`generate` / `transcribe`)是 `create + wait` 的语法糖,**必须在 docstring 里明写它会阻塞**,否则客户会把两种 `create` 当成一回事。 - -**入参规范**: - -| 类别 | 规范 | -| --- | --- | -| 模型 | 一律 `model: str`,不允许 `model_name` 之类变体 | -| 任务主键 | 对外统一 `job_id`;服务端字段 `task_id` 保留为双向别名属性 | -| HTTP 超时 | `timeout: Optional[float]`,单次请求;**所有公开方法都必须有,且显式写进签名,不许藏在 `**kwargs` 里**。唯一例外见下"rl 例外" | -| 轮询预算 | `wait()` 上 `timeout` = 等待总预算;阻塞式一步法上 `timeout` = 总预算、提交那一次请求另用 `submit_timeout` | -| 服务端透传 | `parameters=`(并入线缆 parameters 段)/ `extra_input=`(并入 input 段);**禁止** `extra_parameters` 之类第三、第四个同义名 | -| 分页 | `page_no` / `page_size`,默认值全线一致;禁止 `page=` | -| 文件 URL | 多文件端点 `file_urls: List[str]`,单文件端点 `file_url: str`;单复数跟随端点真实入参个数,禁止裸 `url=` | -| 必填主输入 | keyword-only(顶层 `dashscope.chat()` 一行式入口例外) | -| 低频服务端参数 | 收进 `parameters=`,在 docstring 里列常用键,不铺到签名上 | -| 测试注入钩子 | 不进公开签名:用 `_` 前缀(如 `wait(_sleep=...)`)或 `with_options()` | - -**rl 例外**:`client.rl` 的四个 1.x 直通方法(`jobs.create` / `register_functions` / `test_functions` / `upload_data`)按 §2.4 规则 4 复用 1.x 业务逻辑,超时由 1.x 内部控制,**不接受 `timeout=`**。这是全 SDK 唯一的例外,必须在接口文档里显式写明,不能让"所有方法都支持 timeout"成为一句假承诺。 - -**输出字段规范**: - -| 类别 | 规范 | -| --- | --- | -| 时间戳 | `created_at` / `updated_at` / `submitted_at` / `scheduled_at` / `finished_at`;禁止 `gmt_*`、`*_time` 混用 | -| 用量 | 单一 `Usage` 类(`types/common.py`)全 SDK 复用,禁止再造同构 Usage 类或退化成裸 dict | -| 分页 | `page_no` / `page_size` / `total_count`;禁止 `total` | -| 序号 | 一律 `index`;禁止 `text_index` | -| 模型标识 | 一律 `model`;微调产物 `finetuned_model`,部署产物 `deployed_model`;禁止 `model_name` / `finetuned_output` / `version` | -| 任务标识 | 对外 `job_id`,`task_id` 为服务端别名 | -| 错误字段 | 所有顶层响应继承 `ResponseBase`(`request_id` / `status_code` / `code` / `message` / `is_error`)。`is_error` 由 `ResponseBase` 从非空 `code` **自动推导**(与 `errors.response_error` 同一判据),任何资源都不可能忘记设置它 | -| 信封投递 | payload 是 dict 时用 `flatten_envelope(body)` 整体摊平;payload 是列表或 `data` 下的单条目时(`files` / `models`),用 `response_envelope(body)` 把信封字段单独挂到解析出的模型上 | -| 响应形状 | **一律扁平**:服务端的 `output` / `data` 信封在 `_parse` 里拆掉(`flatten_envelope`),客户不写 `.output.` / `.data.` 这一跳 | -| 服务端字段别名 | 对外名与服务端线缆名不同时,用 Pydantic `Field(validation_alias=AliasChoices(对外名, 服务端名))` 做双向可读,**不改线缆报文** | - -**信封规则的三个例外**(都不是顶层 HTTP 响应,故不继承 `ResponseBase`): - -1. `FineTuneEvent` —— SSE 流帧,不是响应;自带 `is_error` 标记错误帧。 -2. `RegisteredFunctions` / `UploadedData` —— `client.rl.register_functions` / `upload_data` 的返回,由 1.x 返回值元组构造,**背后不存在 HTTP 信封**(1.x 失败即抛异常,这也是这两个方法没有 `timeout=` 的同一原因)。给它们加 `request_id` / `code` 只会是永远为空的装饰。 -3. 嵌套条目上的信封字段保持为空:`Deployment` / `File` / `Model` / `FineTune` 既是 `get` 的顶层返回、又是 `*List` 的条目类型("一个形状学一次"),信封只在顶层那份上填充——服务端每次响应报一次,而不是每个条目报一次。 - -**两条硬禁止**: - -1. 公开响应模型上不得出现裸 `Dict[str, Any]` / `Any` 字段。唯一豁免是服务端字段因服务而异、无法类型化的 `Job.output` 与 `FineTuneEvent.output`,且**必须同时提供类型化视图**(`Job.results` / `image_urls` / `video_url` / `result_url`)。 -2. 不得把阿里内部命名习惯泄漏到公开 SDK:`gmt_*`、`is_deleted`、`biz_*` 一类内部 DB / 中台风命名一律改名。例外是**服务端本身就是公开字段名**的情况(如 `apps` 的 `biz_params`、多模态向量的 `res_level`)——此时改名会造成 SDK 与线缆的新不一致,处理方式是在 docstring 里解释语义,而不是改名。 - -**流式形态**:chat 增量文本用 `stream=True` → `ChatStream`(`ChatCompletionChunk`);应用增量输出用 `stream=True` → `AppStream`(`AppCompletion`);训练事件用 `fine_tunes.stream_events()` → `FineTuneEvent`;音频用 `audio.tts.stream()` → `bytes`。websocket 双工会话不属于流式响应,用 `.events()` 迭代服务端事件。流中出错不静默:默认抛异常族,`raise_on_error=False` 时产出一个 `is_error=True` 的对象后结束迭代。 - -**新增资源时的自检顺序**:① 动词是否在 8 类表内、阻塞语义是否与表一致 → ② `timeout=` 是否显式在签名上 → ③ 响应是否继承 `ResponseBase` 且已扁平化 → ④ 时间戳 / 分页 / 序号 / 标识符字段名是否与规范一致 → ⑤ 有没有引入表外同义词 → ⑥ 若按 §2.4 规则 4 复用 1.x 实现,1.x 异常是否已翻译进 v2 异常族(见 §2.7)。 - -### 2.7 错误模型(目录驱动,长期约束) - -**本节是硬约束**:`dashscope/v2/errors.py` 里的异常类名、HTTP 状态与 SDK 侧错误元数据一律**不在 SDK 中手写**,而是从生成的错误目录 `dashscope/v2/_error_registry.py` 派生(`_` 前缀按 v2 约定标记内部模块:它不从 `dashscope/v2/__init__.py` 导出,只被 `errors.py` 与测试引用)。改写背景、逐条决策与验证数据见 [`dashscope-2.0-errors-registry-plan.md`](dashscope-2.0-errors-registry-plan.md)。 - -**目录是唯一事实源**(上游独立仓库 `dashscope-error-registry`,vendored 版本 `1.0.6` / commit `56b7208`): - -| 目录表 | 条数 | SDK 用途 | -| --- | --- | --- | -| `PUBLIC_ERRORS` | 16 | 每条的 `error_code` 就是一个异常类名,`status_code` 就是它的状态码 | -| `SDK_ERRORS` | 32 | 客户端自报错误的 `code` / `solution` / `allow_retry` | - -`_error_registry.py` 是上游 `tools/generate.py` 的产物、文件头即写明 DO NOT EDIT。**这句话的含义是"不得手改目录内容"**——增删错误码、改状态码、改文案都必须回上游 registry 仓库改 `proto/error_registry.proto` 与 `data/*.textproto` 再重新生成,不能在 SDK 里就地打补丁。 - -但它**不是**上游原文的逐字节副本,vendored 这份带有**已登记的本地适配**,整份替换会把它们全部回退,升级时必须逐条重新打上(清单见 registry-plan §1 D1): - -1. 上游第三张表 `InternalError`(277 条服务端组件级错误)**没有 vendor 进来**——它对 SDK 没有投影(proto 里 `InternalError.external` 指回某个 `PublicError.key`,最终还是落到那 16 条),且整份 vendor 会让 `dashscope/v2/` 第一次出现 CJK(388 行)并带来 97 处 black 拆不开的 E501。 -2. `vars` 形参改名 `substitutions` / `variables`——上游用 `vars` 遮蔽内建名,而仓库的 pylint disable 列表里**没有** W0622。 -3. 隐式 Optional 补成显式 `Optional[Dict[str, str]]`。 -4. 按仓库 pin 的 black 23.3.0 / `--line-length=79` 重排。 -5. provenance 从上游独立的 `_version.py` 内联进文件头;文件名加 `_` 前缀。 - -`tests/v2/test_error_registry.py` 钉住其中的文件头、provenance 与裁剪三项(`test_generated_header_survives_vendoring` / `test_catalog_size_and_provenance` / `test_vendored_copy_is_pruned_to_the_tables_v2_consumes`),`format_msg(substitutions=...)` 系列用例钉住第 2 项——**误将上游原文整份覆盖进来会立刻红**。 - -**派生规则**:9 个状态类是 `APIStatusError` 的**扁平兄弟**(互为兄弟、不再是 `InternalServerError` 兜一堆 5xx),`status_code` 逐个从目录读出而非写字面量——目录改状态码,SDK 随之改变,无需改代码。未收录的状态码(418 / 502 / 507…)落 `APIStatusError` 本身,**不猜**成 `InternalServerError`:猜出来的类会让 `except InternalServerError` 抓到并非 500 的失败,且实例的 `status_code`(502)与类声明的状态码(500)互相矛盾。重试判定与此无关——`transport/_retry.py` 按**原始状态码**决定是否重试(408 / 409 / 429 / 500 / 502 / 503 / 504),不看异常类。 - -**三层来源,一个异常族**: - -| 来源 | 判据 | 落到 | `status_code` | -| --- | --- | --- | --- | -| HTTP 层 | 非 2xx 状态码 | `_STATUS_MAP[status]` | 该状态码 | -| 业务层 | HTTP 200 但 body 带 `code` | `_CODE_MAP[code]` | `200` | -| SDK 层 | 根本没有响应 | `SdkError` | `None` | - -`APIStatusError` 因此读作"**网关声明的失败**"(前两层都算),而不是字面的"非 2xx 响应";实例的 `status_code` 说明是哪一层。这也是 `ContentFilterError` 一处 `except` 能同时覆盖两种上报方式的原因。 - -**crosswalk 与目录 key 是两个命名空间**:目录 key(`invalid_api_key`)是标准词表,网关线缆码(`InvalidApiKey`)是百炼的历史事实,两者不互相替代。`_WIRE_TO_PUBLIC` 是 SDK 自己维护的对照表,覆盖全部 13 个 `ErrorCode` 成员;它只决定"这次失败对应**哪条目录项**",**不决定异常类**——类永远由该目录项的 `error_code` 给出。 - -| `ErrorCode`(线缆码) | 目录 key | 异常类 | -| --- | --- | --- | -| `InvalidApiKey` | `invalid_api_key` | `AuthenticationError` | -| `AccessDenied` / `AccessDenied.Unpurchased` / `Arrearage` | `permission_denied` | `PermissionDeniedError` | -| `InvalidParameter` | `invalid_request` | `BadRequestError` | -| `Model.NotExist` | `resource_not_found` | `NotFoundError` | -| `DataInspectionFailed` | `content_policy_violation` | `ContentFilterError` | -| `Throttling` / `.RateQuota` / `.AllocationQuota` | `rate_limit_exceeded` | `RateLimitError` | -| `InternalError` | `internal_error` | `InternalServerError` | -| `ServiceUnavailable` | `service_unavailable` | `ServiceUnavailableError` | -| `GatewayTimeout` | `request_timeout` | `GatewayTimeoutError` | - -`Arrearage`(欠费)走 `permission_denied` 而不是 `insufficient_quota`:欠费是**请求被拒**,不是配额用尽,而目录里 `insufficient_quota` 是 429、意味着可重试。 - -**唯一的细化:`ContentFilterError(BadRequestError)`。** 内容审核在线缆上是 `DataInspectionFailed`、在目录里是 `content_policy_violation` → 400 → `BadRequestError`。V2 需要一个能被单独 catch 的类,做法是**子类化**而非新增目录外状态:`ContentFilterError` 是 `BadRequestError` 的子类,`except BadRequestError` 依然抓得到,目录不变式不破。该不变式由 `test_wire_code_never_leaves_the_catalogued_class` 对 crosswalk 每一行钉住。**将来任何细化都必须走同一条路**:子类化目录已命名的类,不得引入目录里没有的 `status_code`。 - -**`SdkError` 没有状态码,这不是偷懒。** 目录的客户端 SDK 子图里 `SdkError` 只有必填的 `solution`,**没有** `external` 字段指回 `PublicError`——标准本身就没给 SDK 侧错误任何公开投影。所以 `status_code` 保持 `None`;凭空造一个等于给标准里没有的东西加上含义。它的 `code` 是目录名(如 `sdk.agentic_rl.OSSUploadError`),`solution` 是随目录发布的修复建议,另有 `allow_retry` 供调用方自行判断(**不接进 `_retry.py`**:那是 HTTP 层,与 SDK 层不同层)。 - -**1.x 子系统挂载层的翻译义务**:按 §2.4 规则 4 复用 1.x 实现的资源,其 1.x 异常必须翻进 v2 异常族,否则用户写 `except DashScopeError` 抓不到。做法是 `error_from_sdk_exception(exc, family=...)` 两级查目录:`.` 命中就用该条,否则退到 `.Error` 家族兜底。**不嗅探 message 文本、也不嗅探 1.x 的数字 `error_code`**——两者都是 1.x 内部实现细节。捕获范围只取能覆盖该子系统全部异常类的最小基类元组,捕得更宽会把挂载层自己的 bug 贴成 `SdkError`。当前状态:`rl` 已接线(family `sdk.agentic_rl`,8 个直通方法 sync / async 全覆盖);`agentstudio` 待 M3 任务 3.12(其挂载层直接返回 1.x client 对象,需要透明代理才能拦截)。 - -**两级查目录依赖 `.Error` 兜底存在,而 `sdk.agentstudio` 没有这一条。**目录里 `sdk.agentstudio.*` 只有 4 条(`APIConnectionError` / `APITimeoutError` / `StreamError` / `StreamClosedError`),全是非 HTTP 的本地/流层错误;对这 4 条之外的类调用 `error_from_sdk_exception` 会 `KeyError`,反而盖掉原始异常。agentstudio 的 HTTP 层(1.x `APIStatusError` + 8 个子类)自带真实 `status_code`,**属于状态映射、不属于 `SDK_ERRORS`**,要走 `error_from_status`。所以 agentstudio 的翻译是**两条路**,不能照 `rl` 的单路写法。这两个事实已被 `tests/v2/test_errors.py` 与 `tests/v2/test_error_registry.py` 钉死,接线时要改的是用例、不是绕过它们。 - -**新增错误相关代码时的自检顺序**:① 类名与状态码是否从目录派生而非手写 → ② 目录里有没有这一条(没有就去上游 registry 仓库提,**不是**在 SDK 里造) → ③ 细化是否用子类化、有没有逃出目录命名的类 → ④ 有没有泄漏目录外的 `status_code` → ⑤ 新增的线缆码是否进了 crosswalk 且有对应用例 → ⑥ 复用 1.x 的路径是否已翻译。 - ---- - -## 3. 协议兼容层(Anthropic / OpenAI) - -`dashscope/protocols/` 下三个 adapter,实现同一内部接口 `ChatProtocol`: - -| protocol | 请求/响应形态 | 说明 | -| --- | --- | --- | -| `native` | dashscope 自有 HTTP/ws | 全能力,默认值 | -| `openai` | `/v1/chat/completions`(含 SSE) | 对齐 OpenAI SDK 心智;tool_calls、多模态 content parts 直接可映射 | -| `anthropic` | `/v1/messages`(含 SSE) | system 独立字段、`tool_use/tool_result` block 映射 | - -### 3.1 两个方向的兼容 - -1. **出站(客户端)**:`Client(protocol="openai", base_url=...)` 可直接对接任意 OpenAI/Anthropic 兼容端点(含百炼的 compatible-mode 与第三方网关)。参数翻译规则集中在 adapter: - - messages:`role/content` 归一化 → 目标协议格式(system 提取、多模态 parts 转换)。 - - tools:OpenAI `functions` ↔ Anthropic `input_schema` ↔ native `tools`。 - - 采样参数:`temperature/top_p/max_tokens/stop` 直接映射;无对应字段的参数默认透传到请求体(`strict=False`,保证厂商新参数如 reasoning/thinking 不被 SDK 发版节奏卡住),`strict=True` 时对未知参数抛错。 - - 响应:统一翻译回 SDK 的 `ChatCompletion` 对象(含 usage、finish_reason)。 -2. **入站(服务端,可选后置)**:`dashscope serve` 起一个本地 OpenAI/Anthropic 兼容端点,把请求翻译为 native 调用——让存量 OpenAI SDK 用户零改造用 dashscope。此阶段先留接口不实现。 - -### 3.2 与 acli providers 的关系 - -acli 现有 `providers/` 已有多协议接入逻辑,目标仍是"协议翻译收敛到 SDK、避免两处漂移",但**实现路径受同步机制约束**:`dashscope/acli/` 是 agenticCLI 仓库 `src/acli` 的**单向同步副本**(`scripts/sync_acli.sh`,同步时先 `rm -rf` 再拷贝),在副本上做本地修改会被下次同步整体覆盖,且副本一旦依赖 `dashscope.v2` 也无法回流独立仓库 agenticCLI。因此: - -- 协议翻译代码**只下沉到 SDK `v2/protocols/`**(M2 任务 2.2/2.3),2.0 期间**不改同步副本 `dashscope/acli/providers/`**; -- acli 切换为复用 SDK(删除 providers 重复实现)推迟到 **M5 任务 5.1**(底座切换)一次性处理,该改动按"开发与提交约束"单独 commit 并及时 cherry-pick 回 agenticCLI,保持两 repo 一致;3.0 迁移(8.1)时最终收敛。 - -### 3.3 第三方厂商模型(接入百炼)的兼容结论 - -对 MiniMax / DeepSeek / Baseten / Kimi 的调研(详见 `md/vendor-sdk-research-compat.md`)结论:**不需要为任何厂商做专属 adapter**——四家对话协议已全部收敛到 OpenAI 兼容(MiniMax/Baseten 另支持 Anthropic),百炼网关再收敛为 compatible-mode / native 两条通路,§3 的三协议 adapter 天然覆盖。需要做的只是六个通用落点: - -1. **reasoning/thinking 参数**:`extra_body` 透传逃生舱 + 高频参数(`enable_thinking`/`reasoning_effort`)显式化 + 响应模型含 `reasoning_content` 字段(流式单独事件化); -2. **模型目录可发现性**:`client.models.list(provider=...)` 按来源过滤,文档给"厂商 → 百炼 model id"对照表; -3. **错误映射**:`ErrorCode` 枚举覆盖第三方模型常见失败(内容审核/配额/模型不存在); -4. MiniMax 双 Key 形态:百炼统一 key,无需处理; -5. MiniMax 视频/声音克隆等私有能力:若上百炼走 native 异步任务(§2.2 长任务模式),不是协议兼容问题; -6. CLI 示例补厂商特有参数用法(`--extra-body '{"thinking":...}'`)与 compatible-mode/native 选择指引。 - ---- - -## 4. dashscope CLI 2.0 - -**定位**(来自 CLI 对比调研):OpenAI 已于 2026-05 删除 API CLI、Anthropic 从未有过,"SDK 内嵌 API 管理 CLI"品类只剩 DashScope。对标物是 **Codex cloud + aws cli 的混合体**:不与 Agent 类 CLI 比对话体验(那是 expert/acli 的赛道),而是把"AI 平台长流程管理"(微调→部署→RL→视频批处理)做成无可替代的差异点;`chat create` 这类一条 curl 可替代的调用仅作便捷入口。 - -### 4.1 命令树(自动映射 SDK 资源树) - -``` -dashscope chat create [--model -m] [--prompt | --messages @file.jsonl] [--stream] [--tools @tools.json] -dashscope multimodal create ... -dashscope images generate | jobs list/get/wait -dashscope videos generate | jobs ... -dashscope embeddings create [--input @texts.txt] -dashscope rerank create --query ... --documents @docs.json -dashscope audio asr stream|transcribe / tts synthesize|stream -dashscope files upload|list|get|delete -dashscope models list|get -dashscope ft create|get|list|stream|cancel|delete (保留现习惯名) -dashscope deployments create|get|list|scale|delete -dashscope rl register-functions|test-functions|upload-data|run|get|list|cancel|logs -dashscope apps call --app-id ... -dashscope config set|get|list (api_key/protocol/base_url 持久化) -dashscope expert ... (转发到 Agentic CLI,见 §5) -``` - -### 4.2 体验改进点(相对现状) - -- **认证命令**:`dashscope auth login/whoami/logout`,whoami 验证 key 有效性并以退出码表达(0=有效),CI 可脚本化探测(对标 `codex login status`)。 -- **Shell 补全**:Typer `add_completion=True`,提供 bash/zsh/fish 补全安装命令(现状 `cli/__init__.py` 显式关闭,一行可修)。 -- **全局参数**:`--api-key -k`、`--protocol`、`--base-url`、`--output json|table|yaml`(全局化,所有命令含 `ft list`/`rl list` 支持 JSON + cursor 分页)。 -- **输入友好**:`--prompt` 直接传文本;`--messages @file` 从文件读;支持管道 `cat msg.json | dashscope chat create -`;图片等本地文件自动经 `oss upload` 换 URL(复用现有 `cli/oss.py`)。 -- **流式输出**:TTY 下渲染增量纯文本(修复现状打印 dict repr 的问题);`--output json` 时每 chunk 一行合法 JSON(NDJSON),可被 jq 消费;**流式中途出错必须 exit 1**(修复现状 `cli/generation.py` 流式分支只打印错误仍 exit 0 的 bug)。 -- **退出码分级**:0 成功 / 1 服务端错误 / 2 认证错误 / 3 参数错误 / 4 限流,脚本免解析 stderr。 -- **任务轮询内置**:`images generate --wait` 一条命令等结果;所有长任务统一 `--wait` + Rich Progress。 -- **配置文件**:`~/.dashscope/config.toml` 存默认 model/workspace/输出格式,`-c key=value` 单次覆盖(优先级:命令行 > 环境变量 > 配置文件)。 -- **隐藏别名保留**:`fine-tunes`→`ft`、`agentic-rl`→`rl`,不破坏存量脚本。 - -实现:Typer 保留;命令层由 codegen(§6)从元数据生成骨架,每个命令只写参数渲染与输出格式化。 - ---- - -## 5. dashscope expert(Agentic CLI) - -基于现有 `dashscope.acli` 演进,不改交互面,只做三层整合: - -1. **底座切换**:acli 整体原样复用(代码量大,2.0 不重写、3.0 随包结构重整迁移,见 §2.4 与后续任务 8.1);仅模型调用从 acli 自有 providers 切到 SDK 2.0 Client(§3 协议兼容自动生效,`/provider`、`/model` 及 `load_config(provider, base_url)` 语义不变)。 -2. **入口与命名**:新增 `dashscope expert` 顶层命令(等价 `dashscope --tui` 等现有启动参数);`run_interactive` SDK 入口保留。 -3. **dashscope 能力即工具**:把 SDK 资源(文生图、ASR/TTS、embedding、文件上传等)注册为 expert 内置 tools/skills,让 agent 一句话调全量 dashscope 能力——这是 expert 相对通用 agentic CLI 的差异点。 -4. **问答 prompt 的 1.x/2.0 区分**(待办,见 M5 任务 5.4):expert 回答 dashscope 相关编码问题时识别代码语境版本,新代码推荐 2.0、存量 1.x 不强行迁移、新能力标注仅 2.0;片段可插拔、2.0 门面落地后才注入。 - -现有命令面(`/session`、`/memory`、`/mcp`、`/tts`、`/cron` 等,见 interfaces 清单第三部分)全部保留,不在本期重构范围。 - ---- - -## 6. 接口自动化生成(阶段三) - -**依据**(见 `md/conclusion-sdk-architecture-stainless.md`):OpenAI/Anthropic SDK 均由 Stainless 从 OpenAPI spec 自动生成(目录结构逐文件同构),spec 变更 → CI 自动重新生成 SDK 发 PR,天级送达。DashScope 手写实现的代际差距(30+ 个 `Any` 参数、手工 kwargs 搬运、SDK/CLI/文档漂移)正是手写 vs 流水线的差距——2.0 追平的路径不是继续手写补丁,而是把百炼 API 沉淀成 spec 接入生成器(Stainless 商业服务,或 oapi-codegen/自研模板)。 - -### 6.1 元数据源 - -`specs/*.yaml`(OpenAPI 3 扩展)描述每个 API:路径、方法、参数 schema、任务型/流式/普通三种形态标记、协议映射注解(openai/anthropic 对应字段)、CLI 命令名与示例。spec 格式**语言无关**:核心字段不含 Python 专属注解,语言特性走 `x-python`/`x-java`/`x-node` 等扩展命名空间,为后续多语言生成留基座(见后续任务 8.3)。 - -### 6.2 生成物 - -| 产物 | 说明 | -| --- | --- | -| SDK 资源方法 | 参数 dataclass + 请求/响应模型 + 同步/异步实现 | -| CLI 命令 | 参数定义 + help 文本 | -| 类型存根 / 文档 | `.pyi` 与 markdown 接口表(替代手写清单) | -| 冒烟测试 | 按元数据中的示例生成参数化用例 | - -`scripts/codegen.py` 一键生成,CI 校验"生成物与元数据一致"。手写覆盖点:复杂流式/实时接口(ws)、需要特殊 UX 的命令,用 `# manual` 标记豁免生成。 - ---- - -## 7. 目录与里程碑 - -### 目标目录布局 - -``` -dashscope/ - __init__.py # 包根导出 2.0 公开面:Client / AsyncClient / chat() - v2/ # 2.0 全部新实现(独立,不与 1.x 共享请求代码) - client.py # Client / AsyncClient - resources/ # chat, images, audio, files, finetunes, rl, agentstudio... - protocols/ # native / openai / anthropic adapter - transport/ # http(sse) / websocket / task polling(httpx) - errors.py # 异常族;类名与状态码从 _error_registry 派生(§2.7) - _error_registry.py # 生成的错误目录,vendored 上游制品,DO NOT EDIT - cli/ # 统一 CLI 命令树:2.0 新命令与 1.x 旧命令共存同一入口 - #(实施调整:不设独立 cli2/ 目录,旧点式命令经映射翻译兼容) - acli/ # expert(改底座,不改交互面) - generation.py, assistants/, threads/, ... # 1.x 旧模块,原样冻结 -specs/ # API 元数据(阶段三) -scripts/codegen.py -tests/ - (现有 1.x 测试原样保留,冻结回归) - v2/ # 2.0 测试 -``` - -### 里程碑(总览) - -| 阶段 | 内容 | 交付 | -| --- | --- | --- | -| M0 | **已移出 V2 范围**:1.x CLI 快修在 1.x 维护分支单独实施,清单见 [`md/m0-1x-cli-hotfix.md`](m0-1x-cli-hotfix.md) | — | -| M1 | SDK 2.0 骨架:Client、transport(httpx)、异常体系 + 错误码枚举 + 自动重试 + 日志改造、chat/embeddings/rerank/files/models 先行,native 协议 | 新 Client 可用,旧接口并存 | -| M2 | 协议层:openai / anthropic adapter + 参数翻译 + 配置 | `protocol=` 生效,兼容端点可直连 | -| M3 | SDK 全量覆盖:多模态/任务型(images/videos)、audio(含 ws)、ft/deployments/rl/agentstudio(Threads 除外:下线) | interfaces 清单 100% 映射(除下线项) | -| M4 | CLI 2.0 + `dashscope config` | 新命令合入 `dashscope/cli/` 单命令树,旧命令映射兼容保留 | -| M5 | expert 整合:acli 底座切 SDK、dashscope 能力注册为 tools | `dashscope expert` 发布 | -| M6 | codegen:specs 元数据 + 生成 SDK/CLI/文档/测试,CI 校验 | 新增接口只改 yaml | -| M7 | 2.0 GA:文档迁移 + 遥测埋点上线,进入 1–2 年新旧并存运营(1.x 冻结、2.0 独立演进);旧代码删除作为遥测门槛项推迟到 3.0 | 2.0 正式,共存机制可运营 | - -### 开发与提交约束 - -- **acli/expert 改动单独 commit**:所有涉及 Agentic CLI 的改动(M5 全部、expert prompt、acli tools)限定在 `dashscope/acli/**`(及其专属测试)范围内单独提交,不与其他模块混提,便于后续 cherry-pick 同步回 agenticCLI 仓库。 -- **acli 是单向同步副本**:`dashscope/acli/` 由 `scripts/sync_acli.sh` 从 agenticCLI 仓库 `src/acli` 同步而来(先删后拷 + import 重写),本地对副本的修改会被下次同步整体覆盖。因此 2.0 期间原则上不改 `dashscope/acli/**`(providers 收敛见 §3.2 / M2 任务 2.5);M5 的必要修改必须单独 commit 并及时 cherry-pick 回 agenticCLI,避免两 repo 长期分叉。 -- 其余模块按任务表粒度提交,每项 ≈ 一次 PR。 - -### 里程碑任务分解(每项 ≈ 一次 PR) - -#### M0 — 已移出本文档 - -1.x CLI 快修清单移至 [`md/m0-1x-cli-hotfix.md`](m0-1x-cli-hotfix.md),在 1.x 维护分支单独实施,不属于 V2 分支工作。 - -#### M1 — SDK 2.0 骨架 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 1.1 | `v2/` 目录骨架 + `Client`/`AsyncClient` 空壳 + 配置解析(参数 > 环境变量 > config.toml) | `dashscope/v2/client.py`、`dashscope/v2/_config.py` | -| 1.2 | transport 层:httpx 同步/异步、连接池、超时分级、自动重试(Retry-After + 指数退避 + 抖动) | `dashscope/v2/transport/` | -| 1.3 | 异常体系 + `ErrorCode` 枚举(含"HTTP 200 业务失败"双层检查)+ `raise_on_error=False` 兼容开关;异常类名与状态码一律从生成的错误目录派生,不手写(§2.7) | `dashscope/v2/errors.py` + vendored `dashscope/v2/_error_registry.py` | -| 1.4 | 日志改造:request_id/状态码/重试原因入日志 + SensitiveHeadersFilter + `DASHSCOPE_LOG` | `dashscope/v2/_logging.py` | -| 1.5 | 内部规范消息模型(messages/content parts 归一定义,多模态预留) | `dashscope/v2/types/messages.py` | -| 1.6 | `client.chat`:Pydantic 响应 + `@overload` + `output_text` + `stream.text_stream/get_final_message` + reasoning_content 字段 | `dashscope/v2/resources/chat/` | -| 1.7 | `client.embeddings` / `client.rerank` / `client.tokenization` | 同上模式 | -| 1.8 | `client.files` / `client.models`(含 `list(provider=...)` 过滤) | 同上模式 | -| 1.9 | 包根导出 2.0 公开面:`dashscope.Client` / `AsyncClient` / `dashscope.chat(model, prompt)`(1.x 旧导出保持不动) | `dashscope/__init__.py` | -| 1.10 | specs 元数据格式原型:chat 接口先行写 yaml(P6:codegen 主线从 M1 起步,M6 接管) | `specs/chat.yaml` | - -#### M2 — 协议兼容层 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 2.1 | `ChatProtocol` 内部接口 + native adapter 接入 v2 transport | `dashscope/v2/protocols/base.py` | -| 2.2 | openai adapter:messages/tools/采样参数翻译 + 未知参数透传(strict 开关) | `dashscope/v2/protocols/openai.py` | -| 2.3 | anthropic adapter:system 提取、tool_use/tool_result 映射 | `dashscope/v2/protocols/anthropic.py` | -| 2.4 | SSE 流式解析统一(error 事件抛异常) | `dashscope/v2/transport/sse.py` | -| 2.5 | acli providers 收敛:**不改同步副本 `dashscope/acli/providers/`**(会被 sync 覆盖且依赖无法回流,见 §3.2)——协议翻译只在 `v2/protocols/` 实现,acli 复用切换推迟到 M5 5.1 | `tests/v2/protocols/`(与 acli providers 行为一致性对照用例) | -| 2.6 | 兼容端点集成测试(百炼 compatible-mode + DeepSeek/Kimi 直连用例) | `tests/v2/protocols/` | - -#### M3 — SDK 全量覆盖 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 3.1 | `client.multimodal.chat`(多模态 content parts) | `v2/resources/multimodal/` | -| 3.2 | 长任务统一基建:`Job` 对象 + `jobs.create/fetch/wait`(退避轮询) | `dashscope/v2/resources/_jobs.py` | -| 3.3 | `client.images` / `client.videos`(generate 同步封装 + jobs 异步);images 双协议路由:wan2.6-t2i / wan2.6-image 走 multimodal-generation 新协议(同步 + image-generation 异步任务),其余模型走旧 text2image 端点(见 §2.2 路由约定);videos 同端点双输入协议路由:wan2.7-i2v 走 media 新 i2v 协议(`media` 类型化素材入参),其余模型走旧 `img_url` 首帧协议 | `v2/resources/images/`、`v2/resources/videos/` | -| 3.4 | `client.audio.asr`:Transcription 批转写 + Recognition 实时 ws(上下文管理器) | `v2/resources/audio/` | -| 3.5 | `client.audio.tts`(一次性 + 流式)+ `client.audio.realtime`(Omni ws);ws 实时类资源(tts/realtime/asr.recognition)仅提供同步面,`AsyncAudio` 只挂 `asr.transcription` | 同上 | -| 3.6 | `client.fine_tunes` + `client.deployments` | `v2/resources/fine_tunes/`、`v2/resources/deployments/` | -| 3.7 | `client.rl`(现 CLI 能力 SDK 化):**复用 1.x 既有实现,不重写**——自有逻辑多,为避免影响 1.x,2.0 只建挂载层并在 errors、底层 HTTP 调用上对齐 v2,业务逻辑一律不动 | `v2/resources/reinforcement_learning.py`(薄挂载层)+ 复用 1.x rl 实现 | -| 3.8 | `client.apps`(Application)+ `client.agentstudio` 并入统一 Client(agents/sessions/events 流式):agentstudio 1.x 代码已较新、与 2.0 接近,**仅合并挂载入 Client,不动原来的代码和逻辑**;errors 与底层 HTTP 调用对齐 2.0 由专门环节统一处理(见任务 3.12) | `v2/resources/apps.py`、agentstudio 挂载入 Client | -| 3.9 | `interfaces 清单 100% 映射`核对表(排除下线项)+ 全量冒烟测试 | `tests/v2/smoke/` | -| 3.10 | `client.embeddings` 缺口补齐:多模态向量化 `create_multimodal`(MultiModalEmbedding)+ 批量任务 `jobs.create/fetch/wait`(BatchTextEmbedding,复用 3.2 Job 基建) | `v2/resources/embeddings.py` | -| 3.11 | 全局配置门面 `dashscope.settings`:单例收敛 api_key / base_url / websocket_base_url / workspace / protocol / timeout / max_retries / log_level,懒读环境变量(访问时读,不 import 时读),setter 即时生效(log_level 直接调 logger.setLevel);`resolve_config` 优先级变为「显式参数 > settings 赋值 > env > config.toml」,覆盖 1.x 模块级变量(`dashscope.api_key` / `base_http_api_url` 等)的运行时配置需求 | `dashscope/v2/_config.py`(Settings)、`dashscope/v2/_logging.py`(set_log_level) | -| 3.12 | errors 对齐专项:2.0 全资源(含 agentstudio、rl 挂载层)的 errors 与底层 HTTP 调用统一对齐 `dashscope.v2.errors` 层级——在挂载层做错误映射包装,不动 1.x 业务逻辑(不与 §2.4 冲突);并核查 `raise_on_error=False` 兼容模式在所有资源的行为一致性。**`rl` 半边已完成**:8 个 1.x 直通方法(sync + async)经 `error_from_sdk_exception` 翻成 `SdkError`,`except DashScopeError` 可捕获(见 §2.7)。**剩 `agentstudio`**:其挂载层直接返回 1.x client 对象,异常在 SDK 边界外抛出,需要透明代理才能拦截。**且只包调用层不够**——流式错误是在 `for event in stream` 迭代中途从生成器体里抛的(`StreamClosedError`、以及生产者线程 transport 异常的 `raise item` 重抛),`stream()` 早已返回,代理拦不到;而流式正是 agentstudio 的主要用法。**"只包调用层"比"完全不包"更糟**:用户会以为 `except DashScopeError` 已覆盖 agentstudio,却在最主要的路径上被漏网。完整成本清单(两条翻译路、无 `.Error` 兜底、11 个资源类 × sync/async 无共同基类、会被打破的现有断言、需要显式声明的 dunder 集合)见 errors registry plan 的 P2b 段 | `v2/resources/` agentstudio 挂载层 | - -#### M4 — CLI 2.0 - -> 实施注记:方案由"新建独立 `cli2/` 目录"调整为"合入 `dashscope/cli/` 单命令树"(上游 1.27.x 已按单树形态发布并经 fd2519e 同步入 V2)。V2 现状 `cli/` 仍构建在 1.x 上,切 v2 底座及 chat/config 等新命令随本阶段任务实施。 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 4.1 | `cli/` 统一命令树骨架:全局参数(-k/--protocol/--base-url/--output)+ 错误着色统一(只 import v2/) | `dashscope/cli/__init__.py` | -| 4.2 | `dashscope config set/get/list`(config.toml 持久化) | `cli/config.py` | -| 4.3 | 生成类命令:chat/multimodal/images/videos/audio(stdin/@file/管道/本地文件自动换 OSS URL) | `cli/` 各命令 | -| 4.4 | 工具类命令:embeddings/rerank/tokenization/files/models/oss | 同上 | -| 4.5 | 长流程命令:ft/deployments/rl(承接习惯名,--wait + Progress + cursor 分页 JSON) | 同上 | -| 4.6 | apps 命令 + `dashscope expert` 转发入口 | 同上 | -| 4.7 | 旧点式命令经 `_COMMAND_MAP`/`_PARAM_MAP` 翻译到新命令、隐藏别名(`ft`/`agentic-rl`)保留,与 2.0 命令并存于同一 `dashscope` 入口;新命令 `--help` 补场景化示例 | 全命令 | - -#### M5 — expert 整合 - -> 实施注记(2026-10):当前采用 **examples 先行** 的保守切片,只更新 -> `agenticCLI-examples/dashscope-sdk-expert`,再由 -> `scripts/sync_examples.sh` 单向同步到 `dashscope/acli/examples/`;不修改 -> `agenticCLI`,也无需把本轮文档改动回流该仓库。2.0 实现已提升到包根, -> 后续涉及协议层的任务使用 `dashscope/protocols/`,不再引用 `dashscope/v2/`。 -> 本轮仅交付 5.4 的 examples 问答策略与知识更新;5.1、5.2 未实施,5.3 的完整 -> 发布和全斜杠命令回归也不因此视为完成。 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 5.1 | acli 模型调用底座切 SDK Client(`load_config`/`/provider`/`/model` 语义不变);providers 协议翻译切换复用包根 `protocols/`。后续实施时先修改并验证 agenticCLI,再用同步脚本更新 dashscope,单独提交 | `acli/agent.py` 等 | -| 5.2 | dashscope 能力注册为内置 tools(images/tts/asr/embeddings/files 等) | `acli/tools/dashscope_*` | -| 5.3 | `dashscope expert` 命令 + 文档;回归 acli 全量斜杠命令 | — | -| 5.4 | expert 问答 prompt 策略:识别 1.x/2.0 语境(`Generation.call`→1.x、`dashscope.Client`→2.0);新代码推荐 2.0,存量 1.x 修 bug 保持原风格,新参数/新能力标注仅 2.0。当前以 system prompt 的版本选择规则和按需加载的领域 skills 实现,覆盖 2.0.3 包根接口、扁平响应及 `dashscope.errors`;1.x↔2.0 对照留在可检索文档而非常驻 prompt | `agenticCLI-examples/dashscope-sdk-expert/.acli/system-prompt.md` + `.acli/skills/` | - -#### M6 — codegen - -| # | 任务 | 交付 | -| --- | --- | --- | -| 6.1 | `specs/*.yaml` 元数据格式定义(含任务型/流式标记、协议映射注解) | `specs/schema` | -| 6.2 | 存量接口逆向成 spec(先 chat/files/models 三个试点) | `specs/*.yaml` | -| 6.3 | 生成器:SDK 资源方法 + Pydantic 模型 | `scripts/codegen.py` | -| 6.4 | 生成器:CLI 命令骨架 + help | 同上 | -| 6.5 | 生成器:文档接口表 + 冒烟测试;`# manual` 豁免机制 | 同上 | -| 6.6 | CI 校验"生成物与 spec 一致" | `.github/`/CI | - -#### M7 — 2.0 GA 与共存运营 - -| # | 任务 | 交付 | -| --- | --- | --- | -| 7.1 | 迁移指南(1.x → 2.0 对照表)+ 1.x/2.0 双轨文档体系 | docs ✅ [`dashscope-2.0-migration-guide.md`](dashscope-2.0-migration-guide.md)(对照表另见 `dashscope-2.0-interfaces-mapping.md`) | -| 7.2 | 现有 1.x 测试套件原样接入 CI 作冻结回归(不改不扩);2.0 全量测试新建于 `tests/v2/` | `tests/v2/` | -| 7.3 | 遥测埋点:旧接口调用占比(脱敏)+ 季度评估机制 | 核心 + dashboard | -| 7.4 | README/Quickstart 全面改写(以 `dashscope.chat` 为首例) | docs | -| 7.5 | 版本号切 2.0.0 发布 + changelog(明示共存策略与兼容承诺) | release | - -#### 后续(遥测门槛触发,不设死线) - -| # | 任务 | 触发条件 | -| --- | --- | --- | -| 8.1 | 3.0 包结构重整:整目录删除 1.x 旧代码(旧模块 + `cli/` 旧命令树)、`v2/` 内容上移为包主体、acli 迁移适配(import 从 `dashscope.v2.*` 切至包根;与 agenticCLI 仓库同步,单独 commit) | 旧接口调用占比 <5% 且无大客户依赖 | -| 8.2 | `dashscope serve`(入站兼容端点) | M2 后评估,视用户需求立项 | -| 8.3 | 多语言 codegen(Java 优先,其次 Node.js/Go):基于同一 `specs/*.yaml` 生成;过渡期 Java 仓库先接"CI 对照 spec 一致性校验"止血漂移 | M6 稳定后立项 | - -### 风险与决策点 - -- **双依赖并存**:零接触方案的代价是 requests/aiohttp(1.x)与 httpx(2.0)同时安装,包体积与依赖面增大;不做 extras 拆分(避免用户选型困惑),3.0 删旧代码时自然消除。 -- **重复实现的双修风险**:同一服务端 bug 若同时命中新旧两套代码需修两处;因 1.x 冻结、仅修安全/严重 bug,概率低且可控。 -- **"不优雅"的静态成本**:包内短期并存两套实现是刻意为之的权衡——换来的是 1.x 回归风险结构性归零与零兼容层成本;3.0 一次性删除收尾。 -- **多模态消息格式**:native 与 OpenAI content parts 差异最大的地方,M1 前需先定内部规范消息模型。 -- **ws 实时接口与协议层的关系**:Recognition/OmniRealtime 无 OpenAI/Anthropic 对应物,协议层仅覆盖 HTTP 文本类接口,需在文档明示。 -- **`dashscope serve`(入站兼容)**是否做、何时做,待 M2 后评估。 -- **expert 的包归属**:保持 `dashscope.acli` 模块名不变(存量用户脚本兼容),仅在命令层加 `expert` 入口。 -- **多语言 SDK 迭代不同步**(现状:Java 独立 repo、手写维护,接口迭代漂移):2.0 不新建语言生成器,靠 spec 单一事实源 + CI 一致性校验先收敛;多语言生成列入后续(8.3),避免本次迭代范围失控。 diff --git a/md/dashscope-2.0-design.v1.bak.md b/md/dashscope-2.0-design.v1.bak.md deleted file mode 100644 index d55ceec7..00000000 --- a/md/dashscope-2.0-design.v1.bak.md +++ /dev/null @@ -1,230 +0,0 @@ -# dashscope 2.0 设计方案 - -> 基于 V2 分支现状(见 `md/dashscope-interfaces.md` 全量接口清单),规划 dashscope 2.0。 - -## 0. 目标与原则 - -1. **用户接口友好**:一个现代化的 SDK + CLI(`dashscope`),以及一个 Agentic CLI(`dashscope expert`)。 -2. **协议兼容**:通过配置或传入 config,兼容 Anthropic / OpenAI 协议(既可作为客户端消费这两种协议,也保证 dashscope 自身能力可按这两种协议表达)。 -3. **接口自动化生成**(后置):SDK / CLI / 文档 / 测试从单一 API 元数据源自动生成,人工只维护元数据与少量手写覆盖。 - -现状问题(为什么要 2.0): - -- SDK 接口形态不统一:`Generation.call`、`AioGeneration.call`、`ImageSynthesis.async_call/fetch/wait` 三套风格并存,参数散落在 `**kw`,无统一 Client 对象。 -- CLI 与 SDK 一一映射、命令冗长(`dashscope multimodal_conversation create`),缺少面向场景的友好命令。 -- 协议层只有自有 `protocol/websocket.py`,OpenAI/Anthropic 兼容散落在 acli 的 providers 中,未沉淀为 SDK 能力。 -- 接口靠手写,SDK/CLI/文档三处重复劳动,易漂移。 - ---- - -## 1. 总体架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ dashscope expert(Agentic CLI,演进自 dashscope.acli) │ -│ TUI/REPL、session、memory、mcp、tools、skills │ -├─────────────────────────────────────────────────────────────┤ -│ dashscope CLI(dashscope.cli 重写) │ -│ 命令树 = SDK 资源树的自动映射 + 场景化快捷命令 │ -├─────────────────────────────────────────────────────────────┤ -│ dashscope SDK 2.0(dashscope 包根) │ -│ Client 资源式 API(chat/images/audio/files/finetunes/...) │ -├──────────────────────┬──────────────────────────────────────┤ -│ Protocol 适配层 │ Transport 层 │ -│ openai / anthropic │ HTTP(SSE) / WebSocket / 任务轮询 │ -│ / dashscope-native │ │ -├──────────────────────┴──────────────────────────────────────┤ -│ API 元数据(specs/*.yaml)→ codegen → SDK/CLI/文档/测试 │ -└─────────────────────────────────────────────────────────────┘ -``` - -分层原则:expert 只用 SDK 公开 API;CLI 是 SDK 的薄壳;协议与传输在 SDK 内部隔离,上层无感。 - ---- - -## 2. SDK 2.0 设计 - -### 2.1 统一入口:资源式 Client - -对齐 OpenAI/Anthropic 的用户心智,一个 `Client` 对象挂所有资源;同步 + 异步双版本: - -```python -import dashscope - -client = dashscope.Client(api_key=..., protocol="openai" | "anthropic" | "native", base_url=...) - -resp = client.chat.completions.create(model="qwen-plus", messages=[...], stream=True, tools=[...]) -img = client.images.generate(model="wanx2.1-t2i", prompt="...", n=1, size="1024*1024") -aclient = dashscope.AsyncClient(...) -``` - -### 2.2 资源树与现有接口映射 - -| 新资源 | 覆盖现有接口(见 interfaces 清单) | 关键方法 | -| --- | --- | --- | -| `client.chat` | `Generation` / `AioGeneration` / `Conversation` / `CodeGeneration` / `Understanding` | `completions.create`(messages 统一格式,tools/stream 一等参数) | -| `client.apps` | `Application` | `completions.create(app_id, prompt)` | -| `client.multimodal` | `MultiModalConversation` / `AioMultiModalConversation` | `chat.create`(content parts:text/image_url/audio/video) | -| `client.images` | `ImageSynthesis` | `generate`(同步封装)/ `jobs.create/fetch/wait`(异步任务) | -| `client.videos` | `VideoSynthesis` | 同上模式 | -| `client.embeddings` | `TextEmbedding` / `MultiModalEmbedding` / `BatchTextEmbedding` | `create` / `jobs.*`(批量任务) | -| `client.rerank` | `TextReRank` / `AioTextReRank` | `create(query, documents)` | -| `client.tokenization` | `Tokenization` | `count(prompt)` | -| `client.audio.asr` | `Recognition` / `Transcription` / `QwenTranscription` | `stream`(实时 ws)/ `transcribe(file_urls)` | -| `client.audio.tts` | `SpeechSynthesizer` / `tts_v2` / `HttpSpeechSynthesizer` / `qwen_tts` | `synthesize`(一次性)/ `stream`(流式) | -| `client.audio.realtime` | `OmniRealtimeConversation` | `session()`(ws 会话上下文管理器) | -| `client.fine_tunes` | `FineTunes` | `create/get/list/cancel/delete/stream_events/wait` | -| `client.deployments` | `Deployments` | `create/get/list/scale/delete` | -| `client.files` | `Files` | `upload/list/get/delete` | -| `client.models` | `Models` | `list/get` | -| `client.rl` | (CLI 的 `dashscope rl` 能力 SDK 化) | `register_functions/test_functions/upload_data/run/get/cancel/logs/list` | -| `client.beta.assistants` | `Assistants/Threads/Messages/Runs/Steps` | OpenAI Assistants 风格 CRUD(保持 `beta` 命名空间) | - -约定: - -- **长任务统一模式**:`jobs.create → fetch/wait → result`,`wait` 内置退避轮询;所有任务资源返回统一的 `Job` 对象(`id/status/result/error`)。 -- **流式统一模式**:`stream=True` 一律返回迭代器/异步迭代器,事件对象统一 `event_type + data`;ws 实时接口用上下文管理器。 -- **类型化**:入参出参全部 dataclass/pydantic,`**kw` 仅保留 `extra_headers/timeout` 等逃逸舱;提供 `.pyi`。 -- **错误模型**:统一 `DashScopeError` 层级(AuthError/RateLimitError/TaskFailedError...),带 `request_id`。 - -### 2.3 配置 - -```python -dashscope.Client(api_key=..., base_url=..., protocol=..., timeout=..., max_retries=...) -``` - -优先级:显式参数 > 环境变量(`DASHSCOPE_API_KEY` 等) > `~/.dashscope/config.toml`。保留 `dashscope.api_key` 等模块级全局量作为 1.x 兼容层(deprecate 标注)。 - -### 2.4 兼容性 - -- 包根继续导出旧接口(`Generation.call` 等),内部转调新 Client,打 DeprecationWarning;两个 minor 版本后移除。 -- `dashscope 2.0` 以新 Client 为唯一推荐入口。 - ---- - -## 3. 协议兼容层(Anthropic / OpenAI) - -`dashscope/protocols/` 下三个 adapter,实现同一内部接口 `ChatProtocol`: - -| protocol | 请求/响应形态 | 说明 | -| --- | --- | --- | -| `native` | dashscope 自有 HTTP/ws | 全能力,默认值 | -| `openai` | `/v1/chat/completions`(含 SSE) | 对齐 OpenAI SDK 心智;tool_calls、多模态 content parts 直接可映射 | -| `anthropic` | `/v1/messages`(含 SSE) | system 独立字段、`tool_use/tool_result` block 映射 | - -### 3.1 两个方向的兼容 - -1. **出站(客户端)**:`Client(protocol="openai", base_url=...)` 可直接对接任意 OpenAI/Anthropic 兼容端点(含百炼的 compatible-mode 与第三方网关)。参数翻译规则集中在 adapter: - - messages:`role/content` 归一化 → 目标协议格式(system 提取、多模态 parts 转换)。 - - tools:OpenAI `functions` ↔ Anthropic `input_schema` ↔ native `tools`。 - - 采样参数:`temperature/top_p/max_tokens/stop` 直接映射;不支持的参数按 `strict=False`(默认)静默丢弃、`strict=True` 抛错。 - - 响应:统一翻译回 SDK 的 `ChatCompletion` 对象(含 usage、finish_reason)。 -2. **入站(服务端,可选后置)**:`dashscope serve` 起一个本地 OpenAI/Anthropic 兼容端点,把请求翻译为 native 调用——让存量 OpenAI SDK 用户零改造用 dashscope。此阶段先留接口不实现。 - -### 3.2 与 acli providers 的关系 - -acli 现有 `providers/` 已有多协议接入逻辑:抽取其中的协议翻译代码下沉到 SDK `protocols/`,acli 改为复用 SDK,避免两处实现漂移。 - ---- - -## 4. dashscope CLI 2.0 - -### 4.1 命令树(自动映射 SDK 资源树) - -``` -dashscope chat create [--model -m] [--prompt | --messages @file.jsonl] [--stream] [--tools @tools.json] -dashscope multimodal create ... -dashscope images generate | jobs list/get/wait -dashscope videos generate | jobs ... -dashscope embeddings create [--input @texts.txt] -dashscope rerank create --query ... --documents @docs.json -dashscope audio asr stream|transcribe / tts synthesize|stream -dashscope files upload|list|get|delete -dashscope models list|get -dashscope ft create|get|list|stream|cancel|delete (保留现习惯名) -dashscope deployments create|get|list|scale|delete -dashscope rl register-functions|test-functions|upload-data|run|get|list|cancel|logs -dashscope apps call --app-id ... -dashscope assist ...(beta) -dashscope config set|get|list (api_key/protocol/base_url 持久化) -dashscope expert ... (转发到 Agentic CLI,见 §5) -``` - -### 4.2 体验改进点(相对现状) - -- **全局参数**:`--api-key -k`、`--protocol`、`--base-url`、`--output json|table|yaml`、`--json`(机器可读输出,供 jq/脚本)。 -- **输入友好**:`--prompt` 直接传文本;`--messages @file` 从文件读;支持管道 `cat msg.json | dashscope chat create -`;图片等本地文件自动经 `oss upload` 换 URL(复用现有 `cli/oss.py`)。 -- **流式输出**:TTY 下默认渲染流式文本,`--no-stream` 关闭。 -- **任务轮询内置**:`images generate --wait` 一条命令等结果。 -- **隐藏别名保留**:`fine-tunes`→`ft`、`agentic-rl`→`rl`,不破坏存量脚本。 - -实现:Typer 保留;命令层由 codegen(§6)从元数据生成骨架,每个命令只写参数渲染与输出格式化。 - ---- - -## 5. dashscope expert(Agentic CLI) - -基于现有 `dashscope.acli` 演进,不改交互面,只做三层整合: - -1. **底座切换**:模型调用从 acli 自有 providers 切到 SDK 2.0 Client(§3 协议兼容自动生效,`/provider`、`/model` 及 `load_config(provider, base_url)` 语义不变)。 -2. **入口与命名**:新增 `dashscope expert` 顶层命令(等价 `dashscope --tui` 等现有启动参数);`run_interactive` SDK 入口保留。 -3. **dashscope 能力即工具**:把 SDK 资源(文生图、ASR/TTS、embedding、文件上传等)注册为 expert 内置 tools/skills,让 agent 一句话调全量 dashscope 能力——这是 expert 相对通用 agentic CLI 的差异点。 - -现有命令面(`/session`、`/memory`、`/mcp`、`/tts`、`/cron` 等,见 interfaces 清单第三部分)全部保留,不在本期重构范围。 - ---- - -## 6. 接口自动化生成(阶段三) - -### 6.1 元数据源 - -`specs/*.yaml`(OpenAPI 3 扩展)描述每个 API:路径、方法、参数 schema、任务型/流式/普通三种形态标记、协议映射注解(openai/anthropic 对应字段)、CLI 命令名与示例。 - -### 6.2 生成物 - -| 产物 | 说明 | -| --- | --- | -| SDK 资源方法 | 参数 dataclass + 请求/响应模型 + 同步/异步实现 | -| CLI 命令 | 参数定义 + help 文本 | -| 类型存根 / 文档 | `.pyi` 与 markdown 接口表(替代手写清单) | -| 冒烟测试 | 按元数据中的示例生成参数化用例 | - -`scripts/codegen.py` 一键生成,CI 校验"生成物与元数据一致"。手写覆盖点:复杂流式/实时接口(ws)、需要特殊 UX 的命令,用 `# manual` 标记豁免生成。 - ---- - -## 7. 目录与里程碑 - -### 目标目录布局 - -``` -dashscope/ - client.py # Client / AsyncClient - resources/ # chat, images, audio, files, finetunes, rl, assist... - protocols/ # native / openai / anthropic adapter - transport/ # http(sse) / websocket / task polling - cli/ # CLI 2.0(生成 + 手写渲染) - acli/ # expert(改底座,不改交互面) - compat/ # 1.x 旧接口兼容层 -specs/ # API 元数据(阶段三) -scripts/codegen.py -``` - -### 里程碑 - -| 阶段 | 内容 | 交付 | -| --- | --- | --- | -| M1 | SDK 2.0 骨架:Client、transport、chat/embeddings/rerank/files/models 先行,native 协议 | 新 Client 可用,旧接口并存 | -| M2 | 协议层:openai / anthropic adapter + 参数翻译 + 配置 | `protocol=` 生效,兼容端点可直连 | -| M3 | SDK 全量覆盖:多模态/任务型(images/videos)、audio(含 ws)、ft/deployments/rl/assistants | interfaces 清单 100% 映射 | -| M4 | CLI 2.0 + `dashscope config` | 新命令树,旧命令保留别名 | -| M5 | expert 整合:acli 底座切 SDK、dashscope 能力注册为 tools | `dashscope expert` 发布 | -| M6 | codegen:specs 元数据 + 生成 SDK/CLI/文档/测试,CI 校验 | 新增接口只改 yaml | -| M7 | 1.x 兼容层下线、文档迁移 | 2.0 正式 | - -### 风险与决策点 - -- **多模态消息格式**:native 与 OpenAI content parts 差异最大的地方,M1 前需先定内部规范消息模型。 -- **ws 实时接口与协议层的关系**:Recognition/OmniRealtime 无 OpenAI/Anthropic 对应物,协议层仅覆盖 HTTP 文本类接口,需在文档明示。 -- **`dashscope serve`(入站兼容)**是否做、何时做,待 M2 后评估。 -- **expert 的包归属**:保持 `dashscope.acli` 模块名不变(存量用户脚本兼容),仅在命令层加 `expert` 入口。 diff --git a/md/dashscope-2.0-errors-registry-plan.md b/md/dashscope-2.0-errors-registry-plan.md deleted file mode 100644 index 400a9075..00000000 --- a/md/dashscope-2.0-errors-registry-plan.md +++ /dev/null @@ -1,882 +0,0 @@ -# V2 errors 层按 `error_registry.py` 标准改写计划 - -> **前提(三条硬边界)** -> 1. **V2 分支是接口标准**:资源树、方法名、模型名、`ResponseBase` 信封一律不动。 -> 2. **只修 errors 相关**:`errors.py` 及其消费者,其余不碰。 -> 3. **不参考 `V2-errors` 分支的实现**:唯一输入是 `error_registry.py` 这个目录文件本身 -> (新 errors 标准)+ V2 现有代码。因此本计划**不做分支合并**, -> `md/dashscope-2.0-errors-merge-plan.md` 的合并路线已被本计划取代。 -> 4. **不需要向后兼容**:V2 是全新版本,尚未发布、无存量用户。 -> 异常继承关系只为**语义正确**服务,不为"存量 `except` 子句继续生效"服务; -> 不做兼容别名、不做 deprecation shim;既有测试**允许随设计一起改**。 -> (本条推翻了本文早先以兼容性为依据的 D3 / D5 / §5 / §6,下文已按此重写。) - ---- - -## 1. 输入:registry 到底约定了什么 - -`error_registry.py`(vendored 到 `dashscope/v2/_error_registry.py`,649 行) -是**生成制品**,顶部带 provenance: - -```python -__version__ = "1.0.6" -__commit__ = "56b7208" -__snapshot__ = False -``` - -两张表、两个 frozen dataclass: - -**`PublicError` ×16**(`PUBLIC_ERRORS`) - -| 字段 | 含义 | 对 V2 的约束 | -| --- | --- | --- | -| `key` | 内部规范键(`invalid_request` / `auth_failed` / …) | **不是**网关下发的 wire code | -| `status_code` | 400/401/403/404/**413**/429/500/**503**/**504** | V2 必须每个都能落类 | -| `error_code` | **异常类名**(字符串) | 类名一字不差由目录决定 | -| `error_msg` | 默认文案,带 `{parameter}` / `{resource}` / `{limit}` 占位 | 配 `format_msg()` | -| `anthropic_error_code` | `invalid_request_error` / `authentication_error` / `permission_error` / `not_found_error` / `request_too_large` / `rate_limit_error` / `billing_error` / `api_error` / `overloaded_error` / `timeout_error` | anthropic 兼容协议的错误形状 | - -`error_code` 一共要求 **9 个异常类**: -`BadRequestError` `AuthenticationError` `PermissionDeniedError` `NotFoundError` -`RequestTooLargeError` `RateLimitError` `InternalServerError` -`ServiceUnavailableError` `GatewayTimeoutError` - -**`SdkErrorDef` ×32**(`SDK_ERRORS`,1.0.5 里叫 `INTERNAL_ERRORS`):`name` -(`sdk.InvalidApiKey` ×1 / -`sdk.agentic_rl.*` ×27 / `sdk.agentstudio.*` ×4)、`message`、`solution`、 -`allow_retry`、`vars`,配 `format_message()`。 -**注意:这张表没有 status_code,也没有异常类名**——本地/SDK 侧失败,不是 HTTP 响应。 - -### 1.1 最关键的一条约束(决定整体设计) - -**`PublicError` 没有 wire code 字段。** 所以: - -- `_STATUS_MAP`(HTTP 状态 → 异常类)**可以完全由目录推导** ✅ -- `_CODE_MAP`(网关 wire code → 异常类)**无法由目录推导** ❌ - ——`Throttling.RateQuota`、`DataInspectionFailed`、`Model.NotExist` 这些 - 真实下发码到规范键的对应关系,是 V2 自己的知识,目录里没有。 - -结论:改写后 V2 要**新增一份显式 crosswalk**(wire code → catalog entry), -它是手维护的,但**类选择仍然由目录驱动**(crosswalk 只负责选到哪个 -`PublicError`,异常类和状态码都从该条目取)。这份 crosswalk 必须配完整性测试, -否则它会像现在的 `_CODE_MAP` 一样悄悄漏项(见 §2 差距 4)。 - -### 1.2 第二条决定性事实:32 条 `sdk.*` 只有 23 条可由异常类型到达 - -已逐条核对 `SDK_ERRORS.name` 的后缀与 1.x 实际存在的异常类: - -| 分组 | 条数 | 可由 `type(exc).__name__` 到达 | -| --- | --- | --- | -| `sdk.agentstudio.*` | 4 | **4/4**(`dashscope/agentstudio/exceptions.py:75,81,135,141`) | -| `sdk.agentic_rl.*` | 27 | **18/27** | -| `sdk.InvalidApiKey` | 1 | 由 v2 挂载层自己抛,不依赖 1.x 类 | - -`dashscope/finetune/reinforcement/common/errors.py` 里**不存在**这 9 个类: - -- 8 条是**操作结果条件**,不是异常类型:`FunctionRegistrationFailed` - `DatasetsUploadFailed` `DuplicateFunctionNames` `JobSubmissionFailed` - `WorkflowFailed` `UnsupportedFunctionType` `FunctionTestFailed` - `FunctionTestTimeout`。1.x 是用**通用类 + 具体消息 + 数字 `error_code`** - 表达它们的,例如"Duplicate function names detected"由 - `common/model.py:1589` 抛 `ValidationError`,`InputError(f"...", error_code=4011)` - (`common/utils.py:187`)。 -- 1 条是 `sdk.agentic_rl.Error`:1.x 对应的基类叫 `AgenticRLError` - (`common/errors.py:35`),**不叫 `Error`**。 - -后果(决定 D6): - -1. **不能**声称"类名与目录后缀逐条对得上"——早先版本的 D6 这么写,是错的。 -2. 类型推导只覆盖 18 + 4 = 22 条;`AgenticRLError` 及其未收录子类要有 - **家族级兜底**(落到 `sdk.agentic_rl.Error`)。 -3. 那 8 条条件级条目**不要**靠嗅探 message 文本或数字 `error_code` 去反推—— - 脆弱、且属于 1.x 内部编码。它们在 V2 侧就是**目录有、代码不产出**的条目 - (目录是跨 SDK 共享制品,存在不可达条目是正常的)。 -4. 因此 P0 的完整性测试方向必须反过来:**不是**"每条 `name` 都能找到 1.x 类" - (会红),而是"每个 v2 挂载层可能捕获到的 1.x 类,都能在目录里找到落点或兜底"。 - -### 1.3 第三条输入约束:`SdkErrorDef` 不带 `external`,**不要照 `InternalErrorDef` 补** - -曾考虑把 `SdkErrorDef` 改成 1.0.6 里 `InternalErrorDef` 的形状(多一个 -`external: PublicError` 和 `cause: str`),让 sdk.* 也能投影到公开错误。 -**结论:不改。** 这个不对称是上游 schema 层面的刻意设计,不是生成器漏了字段: - -| 证据 | 位置 | 内容 | -| --- | --- | --- | -| proto 里两个 message 本就不同 | `proto/error_registry.proto:25-32` vs `:41-53` | `InternalError` 有 `string external = 2; // Reference to PublicError.key`;`SdkError` **没有 `external`**,改成 `solution` 必填 + 四个分语言 `solution_python/java/go/cpp` | -| 三层模型把两者分在不同子图 | `README.md:9-20` 的 mermaid | `InternalError → external → PublicError` 全在 **服务端** 子图里;`SdkError → solution` 单独待在 **客户端SDK** 子图,**没有连向 PublicError 的边** | -| 措辞明确 | `README.md:24` | SdkError 是"客户端 SDK **独立**错误码,包含 `solution`(**必填**修复建议)" | -| 校验只针对 InternalError | `tools/generate.py` 的 `main()` | 任何 internal error 的 `external` 不在已知 public key 里就 `sys.exit(1)`;**sdk error 没有任何等价校验**,因为 schema 不给它这个字段 | -| 字段顺序也对得上 | 生成制品 | 生成的 `SdkErrorDef(name, message, solution, allow_retry, vars)` 与 `InternalErrorDef(name, external, message, allow_retry, vars, cause, solution)` 的差异逐字对应各自 proto 的字段序,说明是 schema 决定的,不是生成器随手排的 | - -语义上也讲得通:`external` 描述的是"服务端组件级错误对外暴露成哪个公开错误码", -它的前提是**存在一个 HTTP 响应**要按公开码下发;sdk.* 全是本地/客户端侧失败 -(配置缺失、上传失败、函数注册失败……),**根本没有响应可下发**, -所以没有公开投影可言,只有 `solution` 这种"给人看的修复建议"。 - -对 D6 的影响(重要):**`SdkError.status_code = None` 不是本地偷懒, -而是目录给出的唯一忠实读法。** 目录里没有为 sdk.* 定义任何公开投影, -V2 若自己编一个状态码,等于在生成制品之外私加语义。 - -若将来确实需要 sdk.* 的公开投影(比如要让 `rl` 失败也能进统一的可观测口径), -正确做法是**向上游 registry 仓库提 RFC 加一个 optional `external`**, -由 proto + textproto 生成,而不是改 `_error_registry.py` 这个标了 -`DO NOT EDIT` 的制品——手改会让下次重新生成时静默丢失。 - ---- - -## 2. V2 errors 现状与差距(逐条已核实) - -> **这是改写前的基线**:下表 `errors.py` 的行号一律指 commit `d2088de` 之前的 -> 那份文件,改写后已不对应任何现有代码。差距 1–5、8、10 的闭环见 §5 与 §6, -> 差距 6、7、9 按 §7 决策 1/2/4 **仍不接线**。 - -| # | 差距 | 证据 | 后果 | -| --- | --- | --- | --- | -| 1 | 缺 3 个目录要求的异常类 | `errors.py:82-107` 只有 6 个 `APIStatusError` 子类 | 413/503/504 无处落 | -| 2 | `_STATUS_MAP` 缺 413/500/503/504 | `errors.py:110-116` 只有 400/401/403/404/429 | 413 落到裸 `APIStatusError`;503/504 靠 `>=500` 兜底成 `InternalServerError`,**无法区分** | -| 3 | `status_code` 是硬编码字面量 | `errors.py:83,87,91,95,99,103` | 目录改值,代码不跟随(正是"标准"要消除的) | -| 4 | `_CODE_MAP` 漏掉 2 个已声明的 wire code | `ErrorCode.INVALID_PARAMETER`(:24)、`MODEL_NOT_EXIST`(:25) 不在 `_CODE_MAP`(:118-130) | `InvalidParameter` 只能靠状态码兜底;`Model.NotExist` **不会**变成 `NotFoundError` | -| 5 | 503/504 被压平 | `errors.py:128-129` 都指向 `InternalServerError` | 用户无法按类区分服务不可用与网关超时 | -| 6 | `anthropic_error_code` 无消费者 | `protocols/anthropic.py` 只 import `BadRequestError, DashScopeError, ErrorCode`,全文无错误体输出 | anthropic 兼容协议不产出 anthropic 形状的错误 | -| 7 | `allow_retry` 无消费者 | `transport/_retry.py` 的 `RETRYABLE_STATUS_CODES` 只看 HTTP 状态 | sdk.* 错误的可重试性信息丢失 | -| 8 | **rl / agentstudio 的 1.x 异常裸奔** | `reinforcement_learning.py` 467 行 **0 个 `except`**;1.x 抛 `ValueErrorWithCode` / `RuntimeErrorWithCode` / `RegistrationError` / `DatasetsError` / `ValidationError`(定义在 `dashscope/finetune/reinforcement/common/errors.py`) | `except DashScopeError` **抓不到** rl 失败;32 条 `sdk.*` 目录无人使用 | -| 9 | 占位符文案未使用 | 目录 `format_msg()` 支持 `{parameter}` 等 | 默认消息不含具体参数名/资源名 | -| 10 | 无目录完整性测试 | `tests/v2/` 无对应文件 | 目录与异常类脱钩不会被发现 | - -差距 8 是里面唯一的**功能性缺陷**(不是精度问题):rl 是 1.x 直通层, -用户拿到的异常不在 2.0 异常族里。差距 1–5、10 是"按标准改写"的主体。 -差距 6、7、9 是目录里已有、V2 尚未接线的字段(见 §7 决策项)。 -**其中差距 9 已于 P3 接线**(见 §7 决策 4),差距 6、7 仍按决策 1/2 不接。 - ---- - -## 3. 目标设计 - -**D1|registry 以生成制品身份落地,provenance = 上游 1.0.6。** - -已落地:`dashscope/v2/_error_registry.py`,649 行,最长行 78 字符, -**零非 ASCII**,flake8 / black(79) / pylint 全绿(pylint 10.00/10)。 -`git hash-object` = `908fbf161dae98c2e2bae5337671fdb533b352a8`。 - -文件头:首行与上游逐字一致,provenance 三行是上游独立 `_version.py` 的 -内容内联进来(适配 4): - -```python -"""Auto-generated by tools/generate.py -- DO NOT EDIT.""" -... -__version__ = "1.0.6" -__commit__ = "56b7208" -__snapshot__ = False -``` - -权威源是**独立仓库** `gitlab.alibaba-inc.com/dashscope/dashscope-error-registry` -(`tools/generate.py` + `proto/error_registry.proto` + `data/*.textproto`, -产出 python/go/java/cpp 四语言)。原文即 -`python/dashscope_error_registry/error_registry.py`。生成器不在 SDK 仓库里。 - -本仓库各 ref 里的历史副本(仅供考古,均**未采用**): - -| 来源 | 路径 | 版本 | -| --- | --- | --- | -| `upstream/dev/errors` | `dashscope/common/error_registry.py` | 1.0.3(`8e9c1a0`) | -| `V2-errors` | `dashscope/v2/error_registry.py` | 1.0.5(`ededf0f`) | - -**与 1.0.6 原文的一致性已逐字段核实**(不是靠版本号声明): - -| 表 | 本文件 | 原文 1.0.6 | 核实结果 | -| --- | --- | --- | --- | -| `PublicError` schema | 5 字段 | 5 字段 | 字段名逐个相同 | -| `PUBLIC_ERRORS` | 16 条 | 16 条 | 4 个字段**逐条全同,含顺序** | -| `SdkErrorDef` schema | 5 字段 | 5 字段 | 字段名逐个相同 | -| `SDK_ERRORS` | 32 条 | 32 条 | 5 个字段**逐条全同,含顺序** | -| `InternalErrorDef` / `INTERNAL_ERRORS` | **不含** | 277 条 | 见下 | - -**为什么裁掉那张 277 条的表**(1.0.6 相对 1.0.5 的唯一实质增量): - -- 它描述的是**服务端组件**的错误:`gateway.*` / `mint.*` / `api.*` / - `turbo.*` / `model.*`。客户端 SDK 一条也不会抛。 -- 实测原文整份 vendor 的代价:black(79) 之后 **3553 行**、 - **388 行中文**(`mint.*` 的 `cause` / `solution`)、 - **97 处 E501**(black 拆不开长字符串字面量,最长行 267 字符)。 - 而裁剪后的文件是 649 行 / 0 中文 / 0 E501。 -- 也就是说,直接采用原文会让 `dashscope/v2/` **第一次出现 CJK**, - 并把一个 lint 干净的文件换成 97 处 E501。 -- 裁剪已由 `tests/v2/test_error_registry.py:: - test_vendored_copy_is_pruned_to_the_tables_v2_consumes` 钉死, - 将来谁要 vendor 全量,必须有意识地改这条测试。 - -若确实需要那张表(例如要让 SDK 解释网关内部码),正解不是把原文塞进 -`dashscope/v2/`,而是:让上游提供分表产出或纯 ASCII 变体, -或改为依赖 `dashscope-error-registry` wheel(`python/build.sh` 已在打包上传)。 - -**相对上游原文的 5 处本地适配**(整文件替换时会全部回退,需重新打上; -在此记录避免届时误判为"目录被污染"): - -1. `substitutions: Dict[str, str] = None` → 显式 `Optional[Dict[str, str]]` - (隐式 Optional 被现代 mypy 拒)。 -2. `format_msg(self, vars=...)` / `format_message(self, vars=...)` 形参 - **改名**为 `substitutions` / `variables`——上游用 `vars` 遮蔽内建名, - 仓库的 pylint disable 列表里**没有** W0622,会直接报错。 -3. 长字符串与目录列表按仓库 pin 的 black 23.3.0 / `--line-length=79` 重排 - (原文的 `PUBLIC_ERRORS = [...]` 是单行数千字符)。 -4. 上游把 provenance 放在独立的 `python/dashscope_error_registry/_version.py`; - 这里内联进文件头,因为 vendoring 只取一个文件。 -5. **文件名** `error_registry.py` → `_error_registry.py`(§8 已完成): - v2 根下内部模块一律带 `_` 前缀,这份不导出、只被 `errors.py` 与测试引用。 - -顶部 provenance 视为不可手改;所有适配逻辑写在 `errors.py`。 - -**D2|异常类名 = 目录的 `error_code`,一字不差。** 新增 3 个: -`RequestTooLargeError`、`ServiceUnavailableError`、`GatewayTimeoutError`。 -`status_code` 类属性与 `_STATUS_MAP` 全部由 `PUBLIC_ERRORS` 推导,删掉字面量。 - -**D3|扁平继承:9 个类全部直接挂 `APIStatusError`。** - -```python -class BadRequestError(APIStatusError): status_code = 400 -class AuthenticationError(APIStatusError): status_code = 401 -class PermissionDeniedError(APIStatusError): status_code = 403 -class NotFoundError(APIStatusError): status_code = 404 -class RequestTooLargeError(APIStatusError): status_code = 413 # 新增 -class RateLimitError(APIStatusError): status_code = 429 -class InternalServerError(APIStatusError): status_code = 500 -class ServiceUnavailableError(APIStatusError): status_code = 503 # 新增 -class GatewayTimeoutError(APIStatusError): status_code = 504 # 新增 -``` - -理由(按前提 4,不再以兼容性为依据): - -- 目录给的是 `(status_code, error_code)` 二元组,**扁平结构正好一一对应**, - 不引入目录没说的东西。 -- 目录里 `INTERNAL_ERROR` 就是 500。把 503/504 挂在它下面,等于把 - `InternalServerError` 的含义偷偷扩成"任意 5xx"——这是 **invent 出来的分类学**, - 恰恰是"标准"要消除的。 -- 想要"任意服务端失败"的兜底,用 `except APIStatusError` + `err.status_code >= 500` - 显式表达,而不是靠继承意外获得。 - -配套删掉 `error_from_status` 里的 `>= 500` 特例 -(改写前那行是 `cls = InternalServerError if status_code >= 500 else APIStatusError`; -改写后类选择只剩 `errors.py:286` 的 `_CODE_MAP` → `_STATUS_MAP` → `APIStatusError` 三级查表): -未映射的状态码一律落 `APIStatusError`,**不再猜成 500**。502/507 这类 -目录未收录的码因此不会被贴上 `InternalServerError` 的名字。 - -**D4|两套命名空间都保留,用 crosswalk 连接。** -`ErrorCode`(wire code 枚举,13 项)是网关真实下发值;目录 `key`(16 项)是内部规范键。 -**不合并**。新增 `_WIRE_TO_PUBLIC: Dict[str, PublicError]` 显式表达对应关系, -`_CODE_MAP` 由它派生。同时补上差距 4 的两个漏项: -`InvalidParameter → INVALID_REQUEST`、`Model.NotExist → RESOURCE_NOT_FOUND`。 - -**D5|`ContentFilterError` 改成 `BadRequestError` 的子类——目录特例消失。** - -现状:`ContentFilterError(DashScopeError)`,与目录冲突。目录里 -`CONTENT_POLICY_VIOLATION` 的 `error_code` 是 `BadRequestError`(400)。 -早先的做法是让 crosswalk "V2 覆盖目录",那是为了不改既有测试;按前提 4 不再需要。 - -```python -class ContentFilterError(BadRequestError): - """内容审核拦截(wire code DataInspectionFailed)。""" -``` - -这样 `isinstance(err, BadRequestError)` 成立,**目录契约天然满足**, -同时保留 DashScope 确实需要的细粒度区分(内容审核是高频码,且会在 -HTTP 200 上以业务错误形式下发)。D4 的 crosswalk 里 -`DataInspectionFailed → CONTENT_POLICY_VIOLATION` 直接取目录类, -V2 只是**在它下面细化**,不是覆盖。 - -由此得到一条**可测试的不变式**(进 P0 完整性测试): - -> crosswalk 为任一 wire code 选出的异常类,必须是该条目 `error_code` -> 所指类的**子类或同类**。 - -即:目录决定"至少是哪一类",V2 只能往下细化,不能横向改判。 - -HTTP 200 业务错误路径不受影响:`error_from_business_code` 显式传 -`status_code=200`,实例属性覆盖类属性,`ContentFilterError.status_code` -在该路径上读作 200(与现状一致)。 - -**D6|sdk.* 错误 → 单一新类 `SdkError(DashScopeError)`,两级识别。** - -```python -class SdkError(DashScopeError): - solution: Optional[str] # 目录 SdkErrorDef.solution - allow_retry: bool # 目录 SdkErrorDef.allow_retry -``` - -`status_code=None`——这些是本地/SDK 侧失败,**没有 HTTP 语义,不编造状态码**; -`code=def_.name`(如 `sdk.agentic_rl.InputError`)。 - -识别按 §1.2 的事实分两级,**不手维护 32 条映射表**: - -```python -# 一级:家族命名空间 + 类名精确命中(覆盖 18 + 4 = 22 条) -name = f"{family}.{type(exc).__name__}" # family = "sdk.agentic_rl" / "sdk.agentstudio" -def_ = _SDK_ERRORS.get(name) -# 二级:家族兜底条目 -if def_ is None: - def_ = _SDK_ERRORS[f"{family}.Error"] # sdk.agentic_rl.Error -``` - -要点: - -- `AgenticRLError`(基类)及其未被目录收录的子类,一律落 `sdk.agentic_rl.Error`。 - 这样**任何** rl 失败都保证拿到 `SdkError`,不会漏成裸 1.x 异常。 -- 那 8 条条件级条目(`DuplicateFunctionNames` 等)**故意不接**: - 要靠 message 文本或 1.x 的数字 `error_code`(4011/4012…)反推, - 既脆弱又侵入 1.x 内部编码。目录是跨 SDK 共享制品,有不可达条目是正常的。 -- `sdk.InvalidApiKey` 不由 1.x 类型到达,而是 v2 挂载层在 api_key 缺失时**自己抛**。 - -**agentstudio 家族没有 `.Error` 兜底条目**,两级方案对它不成立。已核实 -`dashscope/agentstudio/exceptions.py` 自己就分了两层(源码里有分节注释): - -| 层 | 类 | 目录归属 | 应译成 | -| --- | --- | --- | --- | -| 传输层(无 HTTP 响应) | `APIConnectionError`(:75) `APITimeoutError`(:81) `StreamError`(:135) `StreamClosedError`(:141) | `SDK_ERRORS` 的 4 条 `sdk.agentstudio.*`,**恰好一一对应** | `SdkError` | -| HTTP 层(有响应) | `APIStatusError`(:92) 及其 8 个子类 | 不在 `SDK_ERRORS` 里 | **按 `status_code` 映射到 v2 状态类** | - -关键:agentstudio 的 HTTP 层异常带真实 `status_code`, -**不能**压进 `SdkError`(`status_code=None`),那会丢掉状态码。 -它们的 `code` 取值(`invalid_request_error` / `authentication_error` / …) -还正好是目录的 `anthropic_error_code` 词表,说明目录设计者也是这么分层的。 - -**D7|翻译只发生在 v2 挂载层,不动 1.x;但两个挂载点代价不同。** - -- **rl(P2 做)** ✅ 已完成:`resources/reinforcement_learning.py` 是 v2 自己的资源模块, - 方法体在 v2 手里,直接 `try/except` 包住 1.x 调用即可 - (**改写前**全文 467 行、0 个 `except`;现 513 行, - 翻译集中在 `:111-121` 的 `_translate_rl_errors()`)。 -- **agentstudio(本次不做,单列 P2b)**:`client.py:113-128` 的 - `agentstudio` property **把整个 1.x `AgentStudioClient` 对象返回给用户**, - 用户随后自己调它的方法——v2 根本不在调用路径上, - `try/except` 无处可加。要翻译就必须包一层**透明代理** - (转发全部属性/方法、同步与异步两套),那是资源层的结构改动, - 超出"只修 errors"的范围,也会碰前提 1 的资源树。 - 该处代码本身已写明这是待办: - > "Until the dedicated errors-alignment task (design doc M3 3.12) lands, - > 1.x error types and HTTP behavior propagate as-is" - - 所以 P2 只修 rl;agentstudio 保持现状(缺口继续挂在 design doc M3 3.12), - 4 条 `sdk.agentstudio.*` 目录条目在本次改写后仍不可达。 - -1.x 代码零改动,符合既有约束。 - ---- - -## 4. 分阶段执行 - -**P0|落地目录(不改任何行为)** ✅ 已完成 -- `error_registry.py` 原样进 V2(生成制品,provenance 三行不手改) -- 新增 `tests/v2/test_error_registry.py`,**只放当前就能绿的用例**: - - 目录自身完整性:`key` 唯一、`status_code` 合法、`error_code` 非空、 - `anthropic_error_code` 非空;`SDK_ERRORS.name` 唯一 - - `SDK_ERRORS` 可达性(方向按 §1.2,**不是**"每条 `name` 都有 1.x 类"): - `sdk.agentic_rl.*` 中恰有 18 条命中 `common/errors.py` 的真实类, - `sdk.agentstudio.*` 4 条全部命中 `agentstudio/exceptions.py`, - 且家族兜底 `sdk.agentic_rl.Error` 存在。 - 把这三个数字**钉死**,上游重新生成目录时会立刻暴露漂移。 - - `format_msg()` / `format_message()` 的占位符替换行为 -- 注意:`PUBLIC_ERRORS.error_code` → 真实类的解析用例**不能**放这一阶段, - 3 个类还不存在,会红。随 P1 一起落。 -- 此阶段结束时应全绿且行为无变化 - -**P1|按目录重写 `errors.py`(主体)** ✅ 已完成 -- 新增 3 个异常类,9 个类扁平挂 `APIStatusError`(D2/D3),`status_code` 由目录取值 -- `ContentFilterError` 改挂 `BadRequestError`(D5) -- `_STATUS_MAP` 改为由 `PUBLIC_ERRORS` 推导(同状态多条去重) -- 新增 `_WIRE_TO_PUBLIC` crosswalk,`_CODE_MAP` 由它派生(D4) -- 删掉 `error_from_status` 的 `>= 500` 兜底特例(D3) -- 补差距 4、5 -- 目录 ↔ 代码的耦合用例**落在 `test_errors.py`,不放 `test_error_registry.py`**: - 后者的 docstring 已把自己限定为"只验目录自身,不断言 `errors.py` 如何映射目录", - 耦合用例依赖 `errors.py`,放进去会让"目录完整性"这层被下游代码污染。 - 已落的耦合用例: - - 每条 `PUBLIC_ERRORS.error_code` 都能解析成一个真实异常类 - - 解析出的类的 `status_code` 属性与目录条目一致 - - `_WIRE_TO_PUBLIC` 覆盖 `ErrorCode` 全部 13 项(**锁死差距 4 不再复发**) - - **D5 不变式**:crosswalk 为每个 wire code 选出的类,是对应目录条目 - `error_code` 类的子类或同类;未被 refine 的必须**恰好等于**该类 - - 目录 `key` 与 wire code 两个命名空间不相交(crosswalk 存在的理由) - - `_STATUS_MAP` 由目录推导,而非手写 -- **同步改 `tests/v2/test_errors.py`**(前提 4:测试随设计改,不冻结)。改后必须覆盖: - - 413 → `RequestTooLargeError`、503 → `ServiceUnavailableError`、 - 504 → `GatewayTimeoutError`,三者互不为子类(锁死 D3 的扁平性) - - 未收录状态码(如 502)→ 裸 `APIStatusError`,**不是** `InternalServerError` - - `ContentFilterError` 同时 `isinstance BadRequestError`(锁死 D5) - - HTTP 200 + `InvalidParameter` → `BadRequestError`; - HTTP 200 + `Model.NotExist` → `NotFoundError` - -**P2|rl 的 sdk.* 翻译(差距 8,唯一功能缺陷)** ✅ 已完成 -- 新增 `SdkError` + `error_from_sdk_exception(exc, *, family)`(D6) -- `resources/reinforcement_learning.py`:**8 个**公开工作流方法全部包住 - (`Rl`/`AsyncRl` 的 `register_functions`/`test_functions`/`upload_data` - + `RlJobs`/`AsyncRlJobs` 的 `create`)。用的是**一个普通同步 - `@contextlib.contextmanager`**,同步体(`asyncio.run(...)`)和异步体 - (`await ...`)都能包,不需要两套。 -- **catch 的是 3 个基类,不是 1 个**:`AgenticRLError` 只 root 了 - `common/errors.py` 19 个异常类里的 17 个,`RuntimeErrorWithCode` / - `ValueErrorWithCode` 分别继承 `RuntimeError` / `ValueError`,**不在其下**。 - 三元组正好覆盖 19/19(已用 `issubclass` 逐条验过)。 - 不再放宽的依据:`common/utils.py:139` 已经把 transport / timeout / - unexpected 全部 `except Exception` 收进 `RuntimeErrorWithCode`, - 所以捕得更宽只会把**本模块自己的 bug** 贴成 `SdkError`。 - 另:pydantic 的 `ValidationError` 也是 `ValueError` 子类,但不在三元组内, - 已验 `issubclass(pydantic.ValidationError, _RL_EXCEPTIONS) is False`。 -- **v2 模型适配留在 `with` 外面**:`_to_job(...)` / `UploadedData(...)` - 不被包,pydantic 校验失败不会被误译成 SDK 错误。 -- 测试(`test_errors.py` 5 个机制用例 + `test_rl.py` 8 个接线用例): - `code` 为 `sdk.agentic_rl.*`、`status_code is None`、原始 message 保留、 - `solution` / `allow_retry` 取自目录、`__cause__` 链住原异常、 - 未收录类落 `sdk.agentic_rl.Error` 兜底、`except DashScopeError` 可捕获、 - 同步与异步两路都覆盖、**非 rl 家族的 `ValueError` 原样穿透**。 -- **偏离 D6 一处:`sdk.InvalidApiKey` 本次不接线。** - D6 说它"由 v2 挂载层在 api_key 缺失时自己抛",但挂载层无法可靠判断 - "缺失":`Config.api_key` 是 `resolve_config()` **当时**读的 - `os.environ[DASHSCOPE_API_KEY]`,而 1.x 的 `set_api_key` 是**调用时**读环境变量。 - 用户先建 `Client()`、后 export key,今天能用;加了前置检查就会误报 - `sdk.InvalidApiKey`。要避免误报就得在 rl 模块里复制 `_config` 的解析逻辑 - (含私有 `_API_KEY_ENV`),代价大于收益。 - 现状已可接受:这条路径 1.x 抛 - `ValueErrorWithCode("Invalid API key configuration")` - (`finetune/agentic_rl.py:61`,非 `cli/agentic_rl.py`), - 经翻译后是 `SdkError(code="sdk.agentic_rl.ValueErrorWithCode")`, - 仍被 `except DashScopeError` 捕获,只是 `code` 不够专名。 - 真要修,正确位置是 `_config.py` 的 key 解析(超出"只修 errors"范围)。 - -**P2b|agentstudio 翻译(本次不做,已论证)** -需要透明代理才能 intercept,属资源层结构改动,超出"只修 errors"范围。 -缺口继续挂在 design doc M3 3.12 与 `client.py:114-119` 的既有注释上。 - -**现状的用户可见危害(比"没接"本身更值得记一笔)**:1.x -`dashscope.agentstudio` 导出 14 个异常类,其中 **8 个与 -`dashscope.v2.errors` 同名**——`APIConnectionError`、`APIStatusError`、 -`APITimeoutError`、`AuthenticationError`、`InternalServerError`、 -`NotFoundError`、`PermissionDeniedError`、`RateLimitError`。实测这 14 个对 -`issubclass(x, dashscope.v2.errors.DashScopeError)` **全为 False**(它们挂在 -`AgentStudioError(DashScopeException)` 这条 1.x 基类下),所以 -`from dashscope.v2.errors import RateLimitError` 去接 `client.agentstudio` -的限流**永远接不到**,且从 import 语句上完全看不出问题。 - -**更隐蔽的是其中 6 个连父类名都一样**:`APITimeoutError` 两侧都继承 -`APIConnectionError`;`AuthenticationError` / `InternalServerError` / -`NotFoundError` / `PermissionDeniedError` / `RateLimitError` 两侧都继承 -`APIStatusError`。于是 traceback 前两层完全同名,只有第三层 -(`AgentStudioError` vs `DashScopeError`)能区分,读栈的人几乎必然看漏。 -这与下面"只包调用层比完全不包更糟"是同一类风险的**现成版本**—— -不必等接线,今天就已经在误导用户。 - -已核实的实现成本(留给后来人,不必重新踩): - -- **翻译分两条路(分层依据见 §3 D7 的表,不在此重述)。** - 可操作的落点:4 条非 HTTP 类走 - `error_from_sdk_exception(exc, family="sdk.agentstudio")` → `SdkError`; - HTTP 层走 - `error_from_status(exc.status_code, message=exc.message, - body=exc.raw, headers={"x-request-id": exc.request_id})`。 - **陷阱**:`sdk.agentstudio` 没有 `.Error` 家族兜底(`sdk.agentic_rl` 有), - 所以对那 4 条之外的类调用 `error_from_sdk_exception` 会 `KeyError`, - **反而把原始异常盖掉**。现状已被测试钉死: - `test_errors.py:477-485`(断言 `KeyError`)、 - `test_error_registry.py:265-287`(断言无兜底、且 HTTP 层类与目录不相交)。 - 接线时要改的是这两个用例,不是绕过它们。 -- **rl 的做法搬不过来。** rl 是逐方法手写 `@contextmanager` - (`reinforcement_learning.py:111-121`);agentstudio 是 11 个资源类 - × 同步/异步双胞胎、约 75 个 `async def`,且**没有共同基类或 mixin** - (都是裸类 + `self._client`)。逐方法重写等于复制整个子 SDK, - 所以只有**递归透明代理**可行。 -- **代理会破坏现有断言。** `test_apps.py:146-158` 与 - `smoke/test_full_surface.py:560-599` 断言 - `isinstance(client.agentstudio, AgentStudioClient)`; - `test_apps.py:233-242`、`:246-255` 通过 `mounted.transport.is_closed` - 验证关闭,代理必须原样透传 `transport`。 -- **决定性的那条:流式错误在迭代中途抛,代理拦不到。** - `SessionEvents.stream()` 是**急切**发请求(`session_events.py:127`) - 然后返回 `_TypedEventStream`;此后的失败发生在 `for event in stream` - 期间——`StreamClosedError` 抛在生成器体里(`streaming.py:88`、`:169`), - 生产者线程的 transport 异常也是迭代中 `raise item` 重抛 - (`streaming.py:125`、`:208`)。只包 `stream()` 这一层, - 仅能覆盖"建流即失败",**覆盖不到流式的主要失败模式**。 - 要覆盖就得再套一层鸭子类型代理,转发 - `__enter__` / `__exit__` / `__iter__` / `close` / `text_stream` - (异步侧再加 `__aenter__` / `__aexit__` / `__aiter__` / `__anext__` / - `aclose`),而 `text_stream` 是**属性、其 getter 本身是生成器** - (`session_events.py:155-178`)。注意隐式 dunder 查找走**类型**而非 - `__getattr__`,所以每个 dunder 都必须显式声明,**漏一个就是一个静默缺口**。 -- **因此"只包调用层"比"完全不包"更糟**:它会让用户相信 - `except DashScopeError` 已覆盖 agentstudio,然后在最主要的流式路径上被漏网。 - 加上这里无法对真实后端做集成验证,本次维持不接线, - 保持上述两个"未解决"用例原样,作为对现状的显式声明。 - -**P3|可选接线(取决于 §7 决策)** -- `anthropic_error_code` → anthropic 兼容协议输出 anthropic 形状错误体(差距 6; - **仍不做**,决策 1) -- `allow_retry` → 是否影响重试策略(差距 7;**仍不做**,决策 2。 - 注意 `_retry.py` 是 HTTP 层,与 sdk.* 不同层,**不要混用**) -- `format_msg` / `format_message` 占位符填充(差距 9)✅ 已完成, - 见 `errors.py` 的 `_catalog_message()` 与决策 4 - -**P4|文档同步** ✅ 已完成 - -三份文档都已同步,且**每段示例都用 mock transport 对真实实现跑过**(9.1 的 -import 名单、9.2 业务层 → `RateLimitError` 且 `status_code == 200`、9.1 未收录 -502 → 裸 `APIStatusError`、9.5 `SdkError` 的 `code`/`solution`/`allow_retry`/ -`__cause__`、9.6 `raise_on_error=False` → `is_error=True` 且连接错误仍然抛)。 - -- `md/dashscope-2.0-design.md`:**新增 §2.7 错误模型(目录驱动,长期约束)**—— - 目录是唯一事实源(1.0.6 / `56b7208`、`PUBLIC_ERRORS` 16 条、`SDK_ERRORS` - 32 条、`InternalError` 为何不 vendor)、派生规则、三层来源一个异常族、 - crosswalk 与目录 key 的命名空间分离(附 13 项对照表 + `Arrearage` 为何走 - `permission_denied`)、`ContentFilterError` 是唯一细化且必须子类化、 - `SdkError` 无状态码的标准依据、1.x 挂载层翻译义务与当前进度、 - 新增错误代码时的 6 步自检。另同步 5 处:§0 调研共识表的落点 - (§2.2 → §2.7)、§2.2 错误模型摘要(9 个扁平类 + 修掉不存在的 - `dashscope.errors.ErrorCode` 路径)、§2.6 自检清单加第 ⑥ 步、 - §7 目录树补 `_error_registry.py`、里程碑 1.3 与 3.12(记录 rl 半边已完成、 - 剩 agentstudio)。 -- `md/dashscope-2.0-naming-review.md`:**偏离计划一处**——计划写"§6.4 记录 - 新增异常类名",但 §6.4 是**输出字段**改名清单,异常类名塞进去会破坏该节 - 的分类;改为**新增 §6.5 异常类名**,10 行按 P0/P1/P2 分级,并点明本节 - 记录的不是"改成什么名"而是"哪些名从 SDK 手写变成目录派生"。 -- `md/dashscope-2.0-interfaces-examples.md`:**偏离计划一处**——计划写"异常族 - 清单与 `except` 示例",但该文档原先**完全没有**错误处理章节(只有开头约定里 - 一句"未捕获的 HTTP 错误默认抛出异常族");改为**新增 §9 错误处理**六节 - (9.1 异常族与 import 路径 + 类/状态码表、9.2 两层错误一个族、9.3 异常属性、 - 9.4 重试、9.5 `SdkError`、9.6 `raise_on_error=False`),并在 §6 Agentic RL - 补"两半错误行为统一"、§7 AgentStudio 补"异常仍是 1.x 原生类型、 - `except DashScopeError` 抓不到"。 - -**写文档过程中查出并改正一处事实错误**:初稿在 design §2.7 与 naming-review -§6.5 都写了"去掉 `>= 500` 猜测可以避免连带触发重试"。**这是错的**—— -`transport/_retry.py:12` 的 `RETRYABLE_STATUS_CODES`(408/409/429/500/502/ -503/504)由 `http.py:173,323` 按**原始响应状态码**判定,与 `errors.py` 最终 -选出哪个异常类**完全无关**;502 改前改后都会被重试。两处已改为陈述真实危害: -猜出来的类让 `except InternalServerError` 抓到并非 500 的失败,且实例 -`status_code`(502)与类声明的(500)矛盾。 - -`md/dashscope-2.0-m1-m3-review.md` **故意不改**:该文第 76 行记录的 -"`APIConnectionError`/`APITimeoutError` 全库零引用"缺陷如今已修(传输层 -`_translate_transport_error` 已接线),但文首明写"本文结论按当时状态保留, -**不作回填**",属带日期的评审快照,遵守其自身约定。 - -**P5|resource 层旁路点接线** ✅ 已完成 - -先核实"要不要改":`dashscope/v2/resources/` 下 **48 处** `check_response()` -调用全部经 `response_error()` → `error_from_status()` / -`error_from_business_code()`,P0/P1 的目录改写**自动生效**,无需逐点改。 -真正要动的只有**绕过 `check_response` 自己造异常**的旁路点,共 3 处: - -| 文件 | 改前 | 改后 | -| --- | --- | --- | -| `models.py`(`get`,同步 + 异步) | 手写 `NotFoundError(..., status_code=404)` | `error_from_business_code(code=ErrorCode.MODEL_NOT_EXIST.value, headers=response.headers)` | -| `audio/realtime.py:128` | 裸 `DashScopeError`(WS `type:error` 事件) | `error_from_business_code(...)`,与 `transport/sse.py:69-76` 的 `error` 分支同形 | -| `fine_tunes.py` | ① `_check_terminal` 丢弃 `job.message`(同步 + 异步)② SSE 无 `status:` 行时默认 500(`_iter_sse` + 异步内联副本) | ① `message=job.message or ...`,对齐 `_jobs.py:222` ② 新增模块级 `_stream_error()` 供同步/异步共用,删掉 `_raise_stream_error` 方法 | - -**`fine_tunes` 的 500 是本次唯一实质缺陷**:`status:` 在 1.x -`StreamEventMixin` 线格式里是**可选行**,缺省时把失败说成服务端 500——与 D3 -拒绝"未收录状态码猜 `InternalServerError`"是同一条理由:**没有任何一方声明过 -这个状态码**。现在缺省即走业务错误层,类由 body 的 `code` 选, -`status_code == 200`。 - -**故意不改(9 处裸 `raise DashScopeError`,分 3 类)** - -1. **wait 超时 / wait 期间被取消**(各 4 处:`_jobs.py:210,300` 与 - `:230,320`、`fine_tunes.py:370,554` 与 `:387,571`)。这两类**正是** - `SdkError` 的目标场景(客户端自己判定、服务端没回失败),但目录 - `SDK_ERRORS` 32 条只有 `sdk.InvalidApiKey` + `sdk.agentic_rl.*`(27)+ - `sdk.agentstudio.*`(4),**没有 jobs / fine-tune 家族**。 - `_error_registry.py` 是"DO NOT EDIT"生成物,按既定约束**不能在 SDK 里造 - 目录条目**——要补得去上游 `dashscope-error-registry` 仓库提。故此处 - **阻塞于上游**,不是遗漏。 -2. **本地入参校验**(`fine_tunes.py:189` `_job_id`)。这不是网关失败, - resource 层其余同类一律用内建 `ValueError` / `RuntimeError`;改成 - `ValueError` 会变更对外异常类型,超出本次"仅改 errors 相关、不动函数名与 - 输入输出"的范围。 -3. **`transport/ws.py` 的 `WsEventError`**。在 `transport/` 而非 - `resources/`,不属 P5 范围——**已在 P6 单独处理**。 - -另记一处**行为不对称、但不属 errors**:`_jobs.py` 把 FAILED 检查放在 `_parse` -(每次 `get()` 都抛)、CANCELED 放在 `_check_canceled`(只有 `wait()` 抛); -`fine_tunes.py` 两者都在 `_check_terminal`(只有 `wait()` 抛)。这是"何时抛" -的行为决策,统一它会改变 `get()` 的可观测行为,不在本次范围。 - -**P6|transport 层接线(`ws.py`)** ✅ 已完成 - -P5 收口后全 v2 复审发现的最大缺口,与 realtime 那一处同类: -`transport/ws.py` 的 `_check_failed()` 把**所有**服务端 `task-failed` 事件写死 -成 `WsEventError`,payload 里的 wire `code` 只被存进实例属性、**从不参与选类**。 -实测 `payload.code == "Throttling"`: - -``` -e.code == "Throttling" # 拿到了 -e.status_code is None # 没有 -isinstance(e, APIStatusError) -> False -isinstance(e, RateLimitError) -> False # except 抓不到 -``` - -影响 **`client.audio.tts` 与 `client.audio.asr` 两个公开接口**(`_check_failed` -被 `run_task` / `recv_event` / `recv` 三处调用,是唯一收口点,改一处即全覆盖)。 - -改法与 realtime 一致:走 `error_from_business_code`,类由 `payload.code` 经 -crosswalk 选。**一处非显然的接线**:WS 帧把 `request_id` 放在 `header` 而非 -`payload`,而 `error_from_business_code` 只从 body 或 `x-request-id` 读它, -websocket 又没有 HTTP 响应头可传——所以**仅在有值时**用 -`headers={"x-request-id": ...}` 这条既定回退通道送进去,`body` 仍保留原始整帧。 - -**`WsEventError` 收窄语义而非删除**:它仍在 `ws.py` 的 `__all__` 里,且 `:141` -那处(`run_task` 收到非 `task-started` 事件)**没有 wire code**,属本地判定的 -协议时序违规,crosswalk 无从下手。一个类同时表示"服务端说任务失败"和"服务端 -破坏了帧协议"正是原先混淆的根源,现在 docstring 明确排除前者。 - -**随之而来的行为变更**:`task-failed` 的 code 若未命中 crosswalk,现在落裸 -`DashScopeError` 而非 `WsEventError`——与 `sse.py` / realtime 的未命中行为一致。 -无消费者受损:`WsEventError` 在 `ws.py` 之外**零 import、零测试断言** -(`tts.py` / `asr.py` 只 import `Connector, WsSession`,`transport/__init__.py` -的 `__all__` 也没有它),除基类外本来就没人能按名字 catch 它。 - -**故意留下的一处(非目录问题,待决)**:`ws.py:186` 与 `:205` 的 -`json.loads(raw)` 让裸 `json.JSONDecodeError` 直接逃出去,而对应的 SSE 路径 -(`sse.py:45-51`)把"畸形报文"和"非 dict 报文"都包成 `DashScopeError`。 -所以畸形 WS 帧目前 `except DashScopeError` **抓不到**。这是健壮性一致性缺口、 -不是目录接线问题,且会改变对外异常类型,故未并入本次。 - ---- - -## 5. 行为变更清单(不做兼容性评估) - -前提 4:V2 未发布、无存量用户,**不评估"是否破坏存量"**。 -下表只列出相对当前 V2 的**可观测行为差异**,供 review 时逐条确认是有意为之。 - -| 变更 | 现状 → 改后 | 依据 | -| --- | --- | --- | -| 503 | 压平成 `InternalServerError` → `ServiceUnavailableError` | D2/D3,差距 5 | -| 504 | 压平成 `InternalServerError` → `GatewayTimeoutError` | D2/D3,差距 5 | -| 413 | 落裸 `APIStatusError` → `RequestTooLargeError` | D2,差距 2 | -| 未收录状态码(502/507…) | 猜成 `InternalServerError` → 裸 `APIStatusError` | D3 | -| `InvalidParameter`(HTTP 200 业务错误) | 基类 `DashScopeError` → `BadRequestError` | D4,差距 4 | -| `Model.NotExist` | 不映射(靠状态码兜底)→ `NotFoundError` | D4,差距 4 | -| `ContentFilterError` 的父类 | `DashScopeError` → `BadRequestError` | D5 | -| rl 失败 | 裸 1.x 异常(`except DashScopeError` 抓不到)→ `SdkError` | D6/D7,差距 8 | -| agentstudio 失败 | **不变**,仍裸奔 1.x 异常 | P2b 延后(D7 已论证需透明代理) | -| 空 body 的失败(如网关直接回 503) | `message == ""` → 目录 `error_msg` 兜底 | 决策 4,差距 9 | -| `models.get` 查无此模型 | 仍是 `NotFoundError`,但 `status_code` 由伪造的 404 → 真实的 **200**,并补上 `code == "Model.NotExist"` | P5 | -| fine-tune SSE 错误帧缺 `status:` 行 | 伪造 500 → `InternalServerError`;改后由 body 的 `code` 选类,`status_code == 200` | P5 | -| fine-tune `wait()` 遇 FAILED | 丢弃服务端 `job.message`,只给本地套话 → 优先 `job.message` | P5 | -| realtime WS `type:error` 事件 | 恒为基类 `DashScopeError` → 按事件 `code` 选目录类(如 `Throttling` → `RateLimitError`) | P5 | -| audio `task-failed` 事件(tts / asr) | 恒为 `WsEventError`(`status_code is None`,不是 `APIStatusError`)→ 按 payload 的 `code` 选目录类,`status_code == 200` | P6 | -| `task-failed` 的 code 未命中 crosswalk | `WsEventError` → 裸 `DashScopeError`(与 `sse.py` / realtime 的未命中行为一致) | P6 | -| 公开异常类数量 | 11 → 15(`APIStatusError` 子类 6 → 9;另加 `SdkError`) | D2/D6 | - -**`APIStatusError` 的语义随之放宽(P1 已改其 docstring)。** -D5 把 `ContentFilterError` 挂到 `BadRequestError` 之后,HTTP 200 业务错误 -造出的实例**也是** `APIStatusError`,其 `status_code == 200`。 -所以基类不能再读作"非 2xx 响应",而是"**网关声明的失败**(两层都算), -实例的 `status_code` 说明是哪一层"。这样 `except ContentFilterError` -一处即可覆盖两种上报方式——正是统一异常族的目的。 - -**新增类不从 `dashscope/v2/__init__.py` 导出(P1 决定,推翻早先建议)。** -理由是一致性而非成本:该文件现在只导出 `errors` 模块 + -`DashScopeError` + `ErrorCode`(`except` 需要的两样),9 个状态类一律走 -`dashscope.v2.errors`;包根 `dashscope/__init__.py:84` 也只再导出 -`AsyncClient, Client, chat, settings`,且属 1.x 不可改范围。 -只加 3 个会显得随意,加 9 个会让 `__all__` 失焦。全部 v2 消费者 -本来就直接 `from dashscope.v2.errors import ...`,无需中转。 - ---- - -## 6. 验证清单 - -- [x] `pytest tests/v2`:305 → **324**(P0 加 19 个目录用例)→ **336** - (P1 加 12 个耦合用例)→ **349**(P2 加 13 个:5 个翻译机制 + 8 个 rl - 接线)→ **356**(决策 4 加 7 个目录文案兜底)→ **359**(P5 新加 3 个、 - 加强 2 个)→ **360 passed / 3 skipped**(P6 新加 1 个、加强 1 个), - 无回归。P5/P6 这 7 个用例都用 `git stash` 只回退对应源文件验证过 - **改前必失败**(P5 命中 `assert 500 == 200`,P6 命中 `raise WsEventError`) -- [x] `pytest tests/unit`(**476 passed / 6 skipped**)/ - `tests/legacy`(**65 passed / 31 skipped**)不回归 -- [x] **基名冲突已修**:`tests/v2/test_messages.py` 与 1.x 的 - `tests/unit/test_messages.py`(Assistants Messages 资源)同基名, - `tests/` 下无 `__init__.py`,pytest 的 prepend 导入模式两者都叫 - `test_messages` —— `pytest.ini` 的 `testpaths = tests` 意味着裸跑 - `pytest` 会被 `import file mismatch` **中断收集**(实测 922 collected - + 1 error,一个用例都不执行)。已把 v2 那份改名 - `test_types_messages.py`(它测的是 `dashscope/v2/types/messages.py` - 这个规范消息模型,按 v2 测试"以被测模块命名"的惯例补上包名限定), - 1.x 那份不动。裸跑收集恢复:**934 collected / 0 error** -- [x] **D3 扁平性**:`test_catalog_classes_are_flat_siblings` 断言 9 个类 - `__bases__ == (APIStatusError,)` 且两两互不为子类 -- [x] **D5 不变式**:`test_wire_code_never_leaves_the_catalogued_class` + - `test_unrefined_wire_codes_map_to_exactly_the_catalogued_class` -- [x] `test_errors.py` 已按新继承关系改写(503/502 两处旧断言已更新), - §5 表中 **errors 层**每条变更都有对应用例; - `rl 失败` 一行由 P2 的 8 个 `test_rl.py` 用例覆盖, - 仅 `agentstudio` 一行按 P2b 延后 -- [x] crosswalk 完整性:`test_crosswalk_covers_every_declared_wire_code` - 断言 `set(_WIRE_TO_PUBLIC) == {m.value for m in ErrorCode}`(13 项) -- [x] pylint(pinned disable + 本地 `R0917`):`dashscope/v2` 整棵源码树 - = **10.00/10,零告警** -- [x] pylint:`tests/v2` 整棵树同样 **10.00/10**,但**需要把仓库根放进 - `sys.path`**:`python3 -m pylint --disable=,R0917 - --init-hook='import sys; sys.path.insert(0, ".")' tests/v2`。 - 不加 `--init-hook` 时本机会报 **66 处 `E0611`(`No name 'v2' in - module 'dashscope'`)+ 1 处 `E1102`(`dashscope.Client is not - callable`)**——`tests/` 下没有 `__init__.py`,pylint 把 `tests/v2` - 而不是仓库根插入 `sys.path`,astroid 于是解析到本机 user - site-packages 里装的 **dashscope 1.26.3**(该版本没有 `v2` 子包), - 而不是仓库里的这份。属本机环境污染,不是仓库缺陷,也与本次改写无关 - (未改动的 `test_transport.py` 同样如此)。lint `dashscope/v2` 时 - pylint 会沿 `dashscope/__init__.py` 上溯到仓库根,所以源码树不受影响 -- [x] flake8:`dashscope/v2` + `tests/v2` **零告警**。原先记录的两处既有 - E501 已修(`_config.py:54` 的 `WORKSPACE_WS_GATEWAY_TEMPLATE` 加括号 - 换行、`test_protocols.py:23` 的尾注释提到上一行),两处都不改值。 - 全仓(排除 `tests/legacy`、`samples`)只剩 2 处 `F824`,都在 - `tests/unit/test_multimodal_dialog.py`——1.x 测试代码,不在本次改动范围 -- [x] black `--line-length=79 --check`:P2 的 4 个改动文件 - (`errors.py` / `reinforcement_learning.py` / `test_errors.py` / - `test_rl.py`)**clean**; - 全仓仍有 7 个文件是 25.x drift(含 `_error_registry.py` 的 - docstring 后空行),仓库 pin 23.3.0,**不采纳** -- [x] `_error_registry.py` 落地后**未被手改**:`git hash-object` 仍为 D1 记录的 - `908fbf1…`,§8 的改名也没动内容(`git mv` 只改路径,相似度 100%)。 - 注意这个 hash 钉的是"P0 落地后没人再动过目录内容",**不是**"等于上游原文"—— - vendored 这份带 5 处本地适配(D1),本来就不等于原文。 - provenance 的比对基准是上游 registry 仓库 1.0.6 的 - `python/dashscope_error_registry/error_registry.py`, - **不是** SDK 仓库的 `upstream/dev/errors`,那份是过时的 1.0.3 -- [x] 1.x 代码零改动:`git status --porcelain` 只有 `dashscope/v2/*`、 - `tests/v2/*`、`md/*` -- [x] V2 接口面是**纯超集**:对 `errors.py` 做 AST 模块级名字 diff(vs `HEAD`), - `removed: none`,`added:` 5 个公开名(`GatewayTimeoutError` - `RequestTooLargeError` `ServiceUnavailableError` `SdkError` 四个类 - + `error_from_sdk_exception` 一个函数)+ 8 个下划线内部名; - 资源树 / 方法名 / 模型名 / `ResponseBase` 未碰 -- [x] `SdkError` 相关用例:`test_errors.py` 5 个(`…has_no_http_semantics` - 钉 `status_code is None`、`…resolves_the_catalog_by_class_name`、 - `…keeps_the_original_message`、`…falls_back_to_the_family_entry`、 - `…needs_a_family_fallback` 钉 `sdk.agentstudio` 缺家族兜底会 `KeyError`) - + `test_rl.py` 8 个(`…translates_1x_failure` 钉 `code`/`__cause__`、 - `…catchable_as_dashscope_error`、`…carries_its_own_solution` 钉 - `solution`/`allow_retry`、`…falls_back_to_the_family_entry`、 - `test_non_rl_exceptions_pass_through` 反向钉"非 rl 异常不翻译"、 - 以及 `test_functions` 与 sync/async 两条接线) -- [x] **P4 文档同步已验证**:三份文档的交叉链接目标全部存在 - (design / registry-plan / interfaces-examples / interfaces-mapping / - naming-review / m0-1x-cli-hotfix);章节编号无冲突(design 新增 §2.7 后 - §3 顺延不变、naming-review §6.1–6.5、examples §0–9);文档里写下的每条 - 行为都**跑过真实实现**(mock transport),不是读代码推断出来的 -- [x] **接口级覆盖审计(P5/P6 收口后做的全 v2 复审)** - - **方法**:AST 扫 `dashscope/v2/resources/**` 每个资源类的每个公开方法, - 再**手工解开委托链**。只数 raise 点是不够的,有两个盲区: - ① 多数方法体内并不直接调用目录构造器,而是委托给 `_jobs` / - `_checked_completion` / `WsSession`,按 raise 点统计会把它们误判成未接; - ② **透传型接口在 v2 代码里根本不 `raise`**(`agentstudio` 就是), - grep 全库也扫不到——所以必须从 `client.py` 的公开面反向逐接口核。 - - 已解开的委托链:`multimodal.chat` → 复用 `chat.completions` 的 - `_send`/`_asend`/`_build_request`/`ChatStream`;`chat.completions` → - `_checked_completion` → `response_error`;`images`/`videos`/ - `embeddings`/`asr`(离线转写) 的 Jobs → `_jobs`;`rl` 的 Jobs → - `fine_tunes`/`_jobs`,1.x 挂载调用 → `_translate_rl_errors`; - `audio.tts` + `audio.asr`(实时识别) → `WsSession._check_failed`(P6)。 - - | v2 接口 | 错误路径 | 状态 | - | --- | --- | --- | - | `chat.completions` | `_checked_completion` → `response_error` | ✅ | - | `multimodal.chat` | 复用 completions 的 `_send`/`ChatStream` | ✅ | - | `multimodal.embeddings` | `check_response` | ✅ | - | `embeddings` / `images` / `videos` | `check_response`;作业委托 `_jobs` | ✅ | - | `rerank` / `tokenization` / `files` / `deployments` | `check_response` | ✅ | - | `models` | `check_response`;`get` 另走 crosswalk(P5) | ✅ | - | `apps` | `check_response` ×2 + `error_from_business_code` | ✅ | - | `fine_tunes` | `check_response` ×6 + `_stream_error`(P5) | ✅* | - | `audio.tts` | `WsSession._check_failed` → crosswalk(P6) | ✅ | - | `audio.asr` | 离线走 `_jobs`,实时走 `WsSession` | ✅ | - | `audio.realtime` | `error_from_business_code`(P5) | ✅ | - | `rl` / `reinforcement_learning` | `_translate_rl_errors` + `_jobs` | ✅ | - | **`agentstudio`** | **返回 1.x 客户端对象本身,异常原样透传** | ❌ P2b | - - **结论**:15 个 resource 里 **14 个的全部公开接口都在目录上**; - 凡"服务端报了失败"(有 wire `code` 或 HTTP 状态)的路径, - **100% 经 crosswalk 选类**,"报了 code 却写死类"这个缺陷类已清零。 - 唯一未接的是 `agentstudio`(既定延后,见 P2b 的同名类危害)。 - - *两处"接了一半",按口径分开记: - ① **8 处 wait 超时/取消**(`_jobs.py:210,230,300,320`、 - `fine_tunes.py:370,387,554,571`)抛的是 v2 `DashScopeError`, - `except DashScopeError` **接得到**,但没有目录 `code`/`solution`—— - 阻塞于上游 `SDK_ERRORS` 缺 jobs/fine-tune 家族(见 P5)。 - ② **`ws.py:186`/`:205` 的 `json.loads`** 让裸 `JSONDecodeError` 逃出去, - `except DashScopeError` **接不到**,而 `sse.py:45-51` 的对等场景是包好的 - (见 P6 末,待决)。 - ---- - -## 7. 决策项(已按最小范围默认采纳,可推翻) - -这 4 项**都不是兼容性问题**,前提 4 不解决它们。默认全部采纳"最小范围"选项, -因为它们都符合前提 2(只修 errors 相关);若要推翻,只影响 P3,P0–P2 不受影响。 - -1. **P3 是否纳入本次?默认:不纳入。** `anthropic_error_code` 目前无消费者, - 接线意味着 anthropic 兼容协议要开始输出错误体(新增行为), - 范围会外溢到协议层。单独排期。 - - 已核实(比"没人用"更硬的一条):**根本没有可实现的对端契约**。 - `protocols/anthropic.py` 只 import `BadRequestError, DashScopeError, - ErrorCode`,两处抛出(`:187-191`、`:204`)都只用于**请求侧校验**, - 全文不产出任何错误响应体;design doc 也没规定 anthropic 形状错误体 - 的字段与嵌套。所以接线不是"补一个消费者",而是**先发明一份协议契约** - ——那必须先和 anthropic 兼容协议的定义方对齐,不能由 errors 改写顺手决定。 -2. **`allow_retry` 是否接重试?默认:不接。** `_retry.py` 是 HTTP 状态层, - `allow_retry` 是 sdk.* 本地错误层,两层语义不同。 - 仅作为 `SdkError` 的只读属性暴露,由用户自行判断。 -3. **`SdkError` 是否按 `sdk.agentic_rl.*` / `sdk.agentstudio.*` 再分子类?默认:不分。** - 一个类 + `code` 字段足够;分子类会把目录的 31 条变成 31 个类, - 与"目录驱动、类名稳定"的初衷相反。 -4. **`format_msg` 占位符谁来填?默认:body 的 `message` 优先。** ✅ 已实施 - 目录文案带 `{parameter}` / `{resource}` / `{limit}`,但网关下发时通常已带 - `message`,V2 现有逻辑就是 body 优先(`errors.py:311` - `message = data.get("message") or message`,业务层同理见 `:398`)。 - 占位符只在 body 无 message 时兜底,且填不出变量就原样保留占位 - (**不要静默丢字段**)。 - - 实现为 `errors.py` 的 `_catalog_message(code, status_code)`,两处构造 - (`error_from_status` / `error_from_business_code`)都写成 - `message or _catalog_message(...)`。**条目识别顺序**:先按 wire code 查 - `_WIRE_TO_PUBLIC`;查不到才按状态码查 `_STATUS_ENTRY`,而 `_STATUS_ENTRY` - **只收录恰有一条条目声明的状态**(403/404/413/500/503/504)。 - 400/401/429 各有 2–5 条同状态条目、文案互不相同,只凭状态码挑一条 - 等于替网关编一个它没说过的原因,因此这三种状态在没有 wire code 时 - **不兜底**(保持空 message)。同理,目录 key(如 `missing_parameter`) - 不是 wire code,`_CODE_MAP` 认不出它,兜底也认不出—— - **兜底的识别能力与选类严格一致,不会出现"类选对了、文案是别人的"**。 - - 实际触发面:`response_error()` 对 4xx/5xx 传 `message=response.text` - (`errors.py:390`),所以**空 body 的响应**(负载均衡/网关直接回 503 - 之类)会走到兜底;带 body 时 `response.text` 非空,兜底不触发。 - 业务层经 `response_error()` 进来的 message 恒非空,兜底只对直接调用 - 公开 API 的人生效(仓内 `_jobs.py` / `apps.py` / `fine_tunes.py` - 的调用点都自带非空 message)。 - **SSE 有意不接**:`transport/sse.py:64,71` 传的是 - `body.get("message") or "stream error"`,字面量恒真,流内错误继续用 - transport 自己的文案,不改。 - ---- - -## 8. 附:`error_registry.py` 的位置 ✅ 已完成(独立 commit) - -只能放 `dashscope/v2/`:它不 import 任何 1.x 内容,1.x 有独立错误体系, -放 `dashscope/` 根会暗示两边共用。 - -但原位置有一处约定不一致:v2 根下内部模块一律带 `_` 前缀 -(`_config.py` / `_logging.py` / `_quickstart.py`),不带前缀的是公开面 -(`client.py` / `errors.py`)。`error_registry.py` 无前缀,却也没从 -`dashscope/v2/__init__.py` 导出(只导出 `errors` / `DashScopeError` / `ErrorCode`), -实际只被 `errors.py` 和测试引用——**读起来像公开 API,其实是内部数据**。 - -已按建议改名 `_error_registry.py`,与 errors 改写分成两个 commit。改动面: -`git mv` + **4 处 import**(`errors.py` 两处、`test_errors.py` 一处、 -`test_error_registry.py` 一处)+ 三处 docstring 路径引用 + 四份 md 文档的 -路径引用(design §2.7 / 目录树 / 里程碑 1.3、naming-review §6.5、 -interfaces-examples §9)。测试文件名 `test_error_registry.py` **不改**: -pytest 的 `test_*.py` 发现规则不要求与被测模块名前缀一致。 - -**代价**:改名本身成了第 5 处本地适配(见 D1),整份替换升级时要一起重打。 -若将来要让用户查错误码,走 `errors.ErrorCode` 即可,不需要公开目录本身。 diff --git a/md/dashscope-2.0-interfaces-examples.md b/md/dashscope-2.0-interfaces-examples.md deleted file mode 100644 index dc135a1f..00000000 --- a/md/dashscope-2.0-interfaces-examples.md +++ /dev/null @@ -1,1862 +0,0 @@ -# dashscope 2.0 接口调用示例集 - -本文按资源分组给出 dashscope 2.0 可直接运行的调用示例,全部基于 `dashscope/v2/` -当前实现逐条核对(同步为主,有异步对应物的接口附异步版本)。1.x → 2.0 的逐接口 -对照见 [`md/dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md); -设计依据见 [`md/dashscope-2.0-design.md`](dashscope-2.0-design.md)。 - -## 为什么是 2.0:相对 1.x 的优化、对齐业界与收益 - -2.0 做两件事:把 OpenAI / Anthropic 已验证的现代 SDK 基线**追平**,再叠加 -DashScope 独有的**差异化能力**。 - -**追平业界基线**(1.x 短板 → 2.0 落点): - -| 维度 | 1.x | 2.0 | -| --- | --- | --- | -| 入口形态 | 全局变量 `dashscope.api_key` + 类方法 `.call`,多租户 / 多环境易错 | `dashscope.Client(...)` 资源树,同步 / 异步孪生(§0) | -| 错误契约 | 返回值自查 `status_code`,漏写 `if` 即静默失败 | 异常驱动:9 个扁平状态类 + 错误目录派生,`request_id` 全链路贯通(§9) | -| 自动重试 | 核心路径无重试 | 默认 `max_retries=2`,`Retry-After` + 指数退避 + 抖动(§9.4) | -| 类型化 | `**kw` 透传、无 IDE 提示、字段拼写错误运行时才炸 | Pydantic 响应 + `@overload` 精确签名(流式 / 非流式静态可区分) | -| 流式体验 | 默认重放全量、需手设 `incremental_output`、深层取值 | 一律增量 chunk,`text_stream` / `get_final_message()` 助手(§0.5) | -| HTTP 栈 | requests + aiohttp 双栈,每次调用新建 Session | httpx 统一同步 / 异步 + 每 Client 连接池 keep-alive(§8.5) | -| 日志 | 缺 request_id、无敏感信息过滤 | request_id / 状态码 / 重试原因入日志,Authorization 过滤,`DASHSCOPE_LOG` 一键开启(§8.2) | - -**差异化优势**(两家官方 SDK 没有的): - -- **一个 SDK 管全部**:一个 Client 覆盖「模型模态(文本 / 多模态 / 图像 / 视频 / - 语音 / 向量 / 排序 / 分词)× 协议(native / openai / anthropic,§0.1)× - provider(百炼上第三方模型靠协议层零成本接入)」。 -- **全链路长任务**:`files → fine_tunes → deployments → rl → agentstudio` 的 - 生产链路全覆盖;耗时环节由统一的 `jobs.create/fetch/wait` 长任务模式支撑 - (统一 `Job` 对象 + 类型化视图 + 内置退避轮询,§2.2),OpenAI / Anthropic - SDK 无此概念。 -- **协议翻译收敛在 adapter**:一套调用代码、一套异常族、一套流式 API, - 换 provider 只改 `protocol=` / `base_url=`,不再维护多套 SDK(§0.2 / §0.3)。 - -**收益**:上手从 6 行降到 2 行(§0);「请求失败被静默吞掉」的已知路径系统性 -堵死;连接池复用消除每请求 TCP/TLS 握手;同一语义全 SDK 只有一个名字 -(动词收敛 8 类、字段命名统一,见设计文档 §2.6),学习与排障成本同降。 - -约定: - -- 节标题以 2.0 接口为主体,括号内的"1.x:…"是与之对应的旧入口,仅供迁移对照。 -- 示例中的 `sk-xxx` 替换为真实 API Key;不显式传 `api_key` 时读取环境变量 - `DASHSCOPE_API_KEY`。 -- 示例中用 `# 可选` 标注可省略的参数(多数带默认值,写成 `# 可选,默认 …`); - 未标注的为必填。 -- `timeout=`(秒)几乎处处可用,示例中不再重复演示。**例外**:`client.rl` 的 - 1.x 直通方法(`jobs.create` / `register_functions` / `test_functions` / - `upload_data` / `query_function_instance_logs` / - `query_all_function_instance_logs` / `delete_function_instance`)复用 1.x - 业务逻辑,不接受 `timeout=`;`client.agentstudio` 是 1.x 子 SDK 的懒挂载桥, - 参数约定以 1.x 为准。 -- `timeout=` 的语义按方法类型区分:普通请求方法上是**单次 HTTP 请求**的超时; - 阻塞式领域动词(`images.generate` / `videos.generate` / - `audio.asr.transcribe`)上是**等待任务终态**的总预算(默认 24h),提交这一次 - 请求的超时另用 `submit_timeout=`;`*.jobs.wait` / `fine_tunes.wait` / - `rl.jobs.wait` 上同样是等待预算。 -- 除 `client.rl` 直通方法外,每个公开方法还接受调用级 `api_key=` / - `workspace=` 覆盖(跟随 Client 配置优先级),示例中不再重复演示。 -- 未捕获的失败默认抛出 `dashscope.v2.errors` 异常族 - (`AuthenticationError` / `RateLimitError` / `NotFoundError` / ..., - 构造 Client 时传 `raise_on_error=False` 可关闭)。完整异常族清单、 - 每个类对应的状态码与 `except` 写法见 **§9**。 - -## 0. 客户端构造与一行调用 - -```python -import dashscope - -# 标准构造(推荐):显式传 key + workspace,或走环境变量 -# DASHSCOPE_API_KEY / DASHSCOPE_WORKSPACE -client = dashscope.Client(api_key="sk-xxx", workspace="ws-xxx") - -# 异步客户端:资源树与同步完全一致 -aclient = dashscope.AsyncClient(api_key="sk-xxx", workspace="ws-xxx") - -# 一行式快捷入口(懒加载全局单例 Client,等价于 -# Client().chat.completions.create(model=..., messages=[prompt])) -resp = dashscope.chat("qwen3.7-plus", "给我讲个笑话") -print(resp.choices[0].message.content) - -# 用完显式释放连接 -client.close() -# async 侧: await aclient.aclose() -``` - -**`workspace` 建议配置、非必需**:默认端点走 workspace 专属网关 -`https://{workspace}.{region}.maas.aliyuncs.com`(官方推荐,性能与稳定性更优)。 -参数、`DASHSCOPE_WORKSPACE`、`config.toml` 三处都没给 `workspace`、又没有显式 -`base_url` 时,回退到 1.x 共享网关(`cn-beijing` 为 -`https://dashscope.aliyuncs.com`)并发出一次 `UserWarning`——api_key-only 的 -quickstart 仍能跑通,但建议尽早配置 workspace 切到专属网关。websocket 默认端点 -同理(`wss://{workspace}.{region}.maas.aliyuncs.com`),可用调用级 `ws_url=` 或 -`DASHSCOPE_WEBSOCKET_BASE_URL` 覆盖(`audio.realtime` 例外:走独立路径,只认 -`ws_url=`,见 §4.6)。显式给 `base_url` 是自建 / 私有网关的逃生口,完全接管端点。 - -**`region`(默认 `cn-beijing`)**选择六个地域网关之一:`cn-beijing` / -`ap-southeast-1` / `cn-hongkong` / `us-east-1` / `eu-central-1` / -`ap-northeast-1`(非法值直接 `ValueError`)。国际用户传 `region=`(或 -`DASHSCOPE_REGION` / `settings.region` / config.toml 的 `region`)即可切换 -默认端点,不必手拼 `base_url`。 - -自定义端点 / 协议(对应 1.x `base_http_api_url` 等模块变量): - -```python -# 全部可选(不传则回落到 settings / 环境变量 / config.toml / 默认值) -client = dashscope.Client( - api_key="sk-xxx", # 缺省读 DASHSCOPE_API_KEY - workspace="ws-xxx", # 缺省回退共享网关 + UserWarning - region="cn-beijing", # 默认 cn-beijing,六地域之一 - base_url="https://ws-xxx.cn-beijing.maas.aliyuncs.com/api/v1", # 逃生口 - protocol="native", # 默认 "native";或 "openai" / "anthropic" - timeout=60.0, - max_retries=2, - raise_on_error=True, # 默认 True - config_path="~/.dashscope/config.toml", - protocol_options={"strict": False}, - http_client=None, # 注入自带 httpx.Client(自定义连接池 / 代理) -) -``` - -`protocol` 决定的默认端点只作用于 `protocol_base_url`,即协议适配的 chat 家族 -(`chat.completions` / `multimodal.chat`)实际发往的地址;其余资源一律走 -`base_url`。 - -### 0.1 协议选择:`native` / `openai` / `anthropic` - -`protocol` 只改变 chat 类调用(`chat.completions` / `multimodal.chat`)的线缆格式, -**调用方式与返回类型(`ChatCompletion`)不变**,其余资源(embeddings/images/audio -等)不受影响。三个取值: - -- `"native"`(默认):DashScope 自有协议(`/services/...`),全能力。 -- `"openai"`:OpenAI 兼容(`/v1/chat/completions`),可对接百炼 compatible-mode - 或任意 OpenAI 兼容端点 / 第三方网关。 -- `"anthropic"`:Anthropic 兼容(`/v1/messages`,system 提取、tool_use 映射)。 - -未显式配置 `base_url` 时,默认端点按资源家族区分:`chat.completions` / -`multimodal.chat` 跟随 `protocol`(`openai` → `.../compatible-mode/v1`, -`anthropic` → `.../apps/anthropic`,见 `config.protocol_base_url`),其余资源 -始终走 native 网关 `.../api/v1`;一旦显式配置 `base_url`,则对全部资源生效。 - -```python -# 用 OpenAI 兼容协议对接百炼 compatible-mode;调用代码与 native 完全一致 -# (base_url 缺省即可,chat 家族自动落到 compatible-mode 端点) -openai_client = dashscope.Client(api_key="sk-xxx", protocol="openai") -resp = openai_client.chat.completions.create(model="qwen3.7-plus", messages="你好") - -# protocol_options:strict=True 时未知参数抛错;默认 False 透传 -# (厂商新参数不必等 SDK 发版即可使用) -strict_client = dashscope.Client( - api_key="sk-xxx", protocol="openai", protocol_options={"strict": True}, -) -``` - -字段级的协议翻译规则见设计文档 [`dashscope-2.0-design.md` §3](dashscope-2.0-design.md)。 - -### 0.2 同一次调用在三种协议下的线缆报文 - -SDK 调用代码三个协议完全一致: - -```python -resp = client.chat.completions.create( - model="qwen3.7-plus", - messages=[ - {"role": "system", "content": "你是一个有帮助的助手。"}, - {"role": "user", "content": "你好"}, - ], - temperature=0.7, # 可选 - max_tokens=512, # 可选 -) -``` - -差异只在实际收发的 HTTP 报文: - -**`native`(默认)** —— `POST {base_url}/services/aigc/text-generation/generation` - -```json -{ - "model": "qwen3.7-plus", - "input": { - "messages": [ - {"role": "system", "content": "你是一个有帮助的助手。"}, - {"role": "user", "content": "你好"} - ] - }, - "parameters": {"result_format": "message", "temperature": 0.7, "max_tokens": 512} -} -``` - -```json -{ - "request_id": "...", - "output": { - "choices": [ - {"finish_reason": "stop", - "message": {"role": "assistant", "content": "你好!有什么可以帮你?"}} - ] - }, - "usage": {"input_tokens": 21, "output_tokens": 9, "total_tokens": 30} -} -``` - -**`openai`** —— `POST {base_url}/chat/completions` -(base_url 为 `.../compatible-mode/v1` 时即百炼兼容端点) - -```json -{ - "model": "qwen3.7-plus", - "messages": [ - {"role": "system", "content": "你是一个有帮助的助手。"}, - {"role": "user", "content": "你好"} - ], - "temperature": 0.7, - "max_tokens": 512 -} -``` - -```json -{ - "id": "chatcmpl-...", - "model": "qwen3.7-plus", - "choices": [ - {"index": 0, "finish_reason": "stop", - "message": {"role": "assistant", "content": "你好!有什么可以帮你?"}} - ], - "usage": {"prompt_tokens": 21, "completion_tokens": 9, "total_tokens": 30} -} -``` - -**`anthropic`** —— `POST {base_url}/v1/messages`(请求头带 `anthropic-version: 2023-06-01`) -(base_url 为 `.../apps/anthropic` 时即百炼 Anthropic 兼容端点,不含 `/v1`) - -```json -{ - "model": "qwen3.7-plus", - "system": "你是一个有帮助的助手。", - "messages": [ - {"role": "user", "content": "你好"} - ], - "max_tokens": 512, - "temperature": 0.7 -} -``` - -注意 system 被提取为独立字段;`max_tokens` 在该协议下必填,缺省时 SDK 自动补 -`4096`;`stop` 参数会改名为 `stop_sequences`。 - -```json -{ - "id": "msg-...", - "role": "assistant", - "content": [{"type": "text", "text": "你好!有什么可以帮你?"}], - "stop_reason": "end_turn", - "usage": {"input_tokens": 21, "output_tokens": 9} -} -``` - -三种响应都被 adapter 翻译回同一个 `ChatCompletion`,所以 -`resp.choices[0].message.content`、`resp.usage.total_tokens` 等取法完全一致。 - -### 0.3 参数在三种协议下的支持矩阵 - -同一段调用代码,参数在三个协议下的落点并不相同。图例: -✅ 原生支持(映射到该协议的正确字段)| 🔀 透传(进 body/parameters,不报错, -除非 `strict=True`)| ❌ 400 拒绝(抛 `BadRequestError`,`status_code=400`、 -`code=InvalidParameter`)。 - -| 规范参数 | native | openai | anthropic | -| --- | --- | --- | --- | -| `model` / `messages` / `stream` | ✅ | ✅ | ✅(system 拆为独立字段) | -| `temperature` / `top_p` | ✅ | ✅ | ✅ | -| `max_tokens` | ✅ | ✅ | ✅(缺省自动补 `4096`) | -| `stop` | ✅ | ✅ | ✅(改名 `stop_sequences`) | -| `tools` | ✅ | ✅ | ✅(转 `input_schema`) | -| `seed` | ✅ | ✅ | ❌ | -| `presence_penalty` | ✅ | ✅ | ❌ | -| `response_format` | ✅ | ✅ | ❌ | -| `repetition_penalty` | ✅ | 🔀 | ❌ | -| `enable_thinking` | ✅ | 🔀 | ❌ | -| `parameters` | 🔀 | 🔀 | 🔀 | - -> 表中是**调用侧参数名**。`parameters=` 转发到协议层时叫 `extra_parameters` -> (`ChatProtocol.build_chat_request` 的形参名),读协议代码时对得上即可。 - -三个协议的差异要点: - -- **native**:所有规范参数都落进线缆 `parameters` 段(恒定带 `result_format="message"`), - 不存在“未知参数”,故 `strict` 在 native 上是惰性的(无效果)。 -- **openai**:OpenAI 原生参数(`temperature/top_p/max_tokens/stop/seed/` - `presence_penalty/response_format/tools`)直接进 body 顶层;OpenAI 没有的 - `repetition_penalty` / `enable_thinking` 与 `parameters` 归入“未知”, - 默认透传,`strict=True` 时抛 `DashScopeError`。 -- **anthropic**:只认 Messages API 有的字段;`seed` / `repetition_penalty` / - `presence_penalty` / `enable_thinking` / `response_format` / `n` / `logprobs` / - `top_logprobs` / `thinking_budget` / `enable_search` / `search_options` / - `parallel_tool_calls` **无条件 400 拒绝**(与 `strict` 无关)——因为静默丢弃对 - 用户不可见,请求会”假成功”但参数并未生效。这些名字即使通过 `parameters` 夹带 - 进来也一样 400 拒绝,堵死绕过路径;其余 `parameters` 则 `strict=False` 透传、 - `strict=True` 抛 `DashScopeError`。 - -> 一句话记忆:native 啥都收且全生效;openai 对没有的参数默认透传(可切 `strict` -> 改成报错);anthropic 对无对应项的参数直接 400,逼你显式删除或改用其它协议。 - -### 0.4 响应信封已扁平化 - -服务端普遍把业务字段包在 `output` / `data` 里,2.0 在解析时统一摊平,**响应对象 -上直接读业务字段**,不需要 `.output.` / `.data.` 这一跳: - -```python -ft = client.fine_tunes.get("ft-xxx") -print(ft.status, ft.finetuned_model) # 不是 ft.output.status - -info = client.files.get("file-xxx") -print(info.file_id, info.name) # 不是 info.data["file_id"] -``` - -每个响应对象都继承 `ResponseBase`,共用一组公共字段:`request_id` / `code` / -`message` / `is_error` / `status_code` / `headers`(网关原始 HTTP 响应头, -小写键 dict;ws 资源无 HTTP 信封,保持空 dict)。**`is_error` 由非空 `code` -自动推导**(与 SDK 判断是否抛异常用的是同一条规则),所以在 -`raise_on_error=False` 下任何资源都不会漏标业务错误: - -```python -job = client.fine_tunes.get("ft-xxx") # 服务端 HTTP 200 + code="Job.Failed" -if job.is_error: - print(job.code, job.message, job.request_id) -``` - -三个类型**不带**信封,因为它们的载荷里根本没有 HTTP 信封:`FineTuneEvent` -(SSE 事件帧,自带 `is_error` 标记错误帧)、`RegisteredFunctions` / -`UploadedData`(rl 侧透传 1.x 返回值拼出来的记录)。此外列表里嵌套的条目 -(`FileList.files[i]` / `ModelList.models[i]` 等)信封字段为空——服务端一次响应 -只报一次 `request_id`,读信封请用列表对象本身。 - -`output` 仅在**服务端字段因服务而异**、无法类型化的地方保留,目前只有 -`Job.output`(异步任务原始载荷)与 `FineTuneEvent.output`(事件流原始帧); -这两处都配了类型化的便捷属性(见 §2.2)。 - -### 0.5 四种流式形态 - -| 场景 | 写法 | 产出 | -| --- | --- | --- | -| chat 增量文本 | `client.chat.completions.create(..., stream=True)` | `ChatStream`,`for chunk in stream` → `ChatCompletionChunk`(`choices[].delta`) | -| 百炼应用增量输出 | `client.apps.completions.create(..., stream=True)` | `AppStream`,`for partial in stream` → `AppCompletion`(每帧是完整快照或增量,取决于 `incremental_output`) | -| 微调训练事件 | `for event in client.fine_tunes.stream_events(job_id)` | `FineTuneEvent`(`output` 为原始帧,`is_error` 标记错误帧) | -| 语音合成音频 | `for chunk in client.audio.tts.stream(text)` | `bytes` | -| 语音合成音频(HTTP) | `for chunk in client.audio.tts.http.stream(text, ...)` | `HttpSynthesisChunk`(`audio` 为解码后字节,末片带整段音频 URL) | - -异步侧对应 `async for`;websocket 双工(`audio.asr.recognize` / -`audio.realtime.connect`)不是"流式响应",而是长连接会话,用 `.events()` 迭代 -服务端推送的事件 dict(见 §4.1 / §4.4)。 - -流中出错不会静默:默认(`raise_on_error=True`)抛出异常族;置 `False` 时在流里 -产出一个 `is_error=True` 的对象后结束迭代。 - -## 1. 文本 / 对话 - -### 1.1 `client.chat.completions.create`(1.x:`Generation.call`) - -```python -resp = client.chat.completions.create( - model="qwen3.7-plus", - messages=[ - {"role": "system", "content": "你是一个有帮助的助手。"}, - {"role": "user", "content": "用一句话介绍杭州"}, - ], - temperature=0.7, # 可选 - max_tokens=512, # 可选 -) -print(resp.choices[0].message.content) -print(resp.usage.total_tokens, resp.request_id) -``` - -返回 `ChatCompletion`(三个协议统一解析成它,线缆差异见 §0.2): - -```json -{ - "request_id": "...", - "model": "qwen3.7-plus", - "choices": [ - { - "index": 0, - "finish_reason": "stop", - "message": { - "role": "assistant", - "content": "……", - "reasoning_content": null, - "tool_calls": null - } - } - ], - "usage": {"input_tokens": 21, "output_tokens": 9, "total_tokens": 30} -} -``` - -便捷属性 `resp.output_text` 等价于 `choices[0].message.content`。流式时逐 chunk -返回 `ChatCompletionChunk`(`choices[].delta` 取代 `message`,末尾 chunk 带 -`usage`)。 - -messages 也接受纯字符串(自动包装成 user 消息): - -```python -resp = client.chat.completions.create(model="qwen3.7-plus", messages="你好") -``` - -具名参数全集:`model` / `messages` / `stream` / `temperature` / `top_p` / -`top_k` / `max_tokens` / `stop` / `seed` / `tools` / `tool_choice` / -`response_format` / `n` / `logprobs` / `top_logprobs` / `presence_penalty` / -`repetition_penalty` / `enable_thinking` / `thinking_budget` / `enable_search` / -`search_options` / `parallel_tool_calls` / `incremental_output`。签名之外的模型 -参数统一走 `parameters=`(原样并入线缆 `parameters`,兼容协议下并入请求体顶层): - -```python -resp = client.chat.completions.create( - model="qwen3-vl-plus", - messages=[{"role": "user", "content": [ - {"image": "https://example.com/cat.jpg"}, {"text": "这张图里有什么?"}]}], - parameters={"vl_high_resolution_images": True}, # 可选:签名外的模型参数 -) -``` - -流式(对应 1.x `Generation.call(..., stream=True)`): - -```python -stream = client.chat.completions.create( - model="qwen3.7-plus", - messages="写一首短诗", - stream=True, # 可选,默认 False -) -for chunk in stream: - delta = chunk.choices[0].delta - if delta.content: - print(delta.content, end="", flush=True) - # 思考模型还会带 delta.reasoning_content -``` - -`ChatStream` 另提供便捷助手:`stream.text_stream` 只迭代增量文本, -`stream.get_final_message()` 把整流聚合为一条完整 message(含 tool_calls), -`stream.headers` 取响应头。 - -工具调用与结构化输出: - -```python -resp = client.chat.completions.create( - model="qwen3.7-plus", - messages="北京今天天气怎么样?", - tools=[{ # 可选 - "type": "function", - "function": { - "name": "get_weather", - "description": "查询城市天气", - "parameters": { - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"], - }, - }, - }], -) -tool_calls = resp.choices[0].message.tool_calls - -resp2 = client.chat.completions.create( - model="qwen3.7-plus", - messages="给我一个用户 JSON", - response_format={"type": "json_object"}, # 可选 -) -``` - -思考模型(对应 1.x `enable_thinking` 增量输出参数): - -```python -resp = client.chat.completions.create( - model="qwen3-235b-a22b-thinking-2507", - messages="9.11 和 9.9 哪个大?", - enable_thinking=True, # 可选 -) -print(resp.choices[0].message.reasoning_content) -print(resp.choices[0].message.content) -``` - -异步(对应 1.x `AioGeneration.call`): - -```python -resp = await aclient.chat.completions.create( - model="qwen3.7-plus", - messages="你好", -) - -stream = await aclient.chat.completions.create( - model="qwen3.7-plus", messages="写一首短诗", stream=True, -) -async for chunk in stream: - print(chunk.choices[0].delta.content or "", end="") -``` - -### 1.2 多轮对话:messages 列表自行维护 - -2.0 的 chat 无状态,多轮对话由调用方自己维护 messages 列表: - -```python -messages = [{"role": "system", "content": "你是一个有帮助的助手。"}] - -for user_input in ["我叫小明", "我叫什么?"]: - messages.append({"role": "user", "content": user_input}) - resp = client.chat.completions.create(model="qwen3.7-plus", messages=messages) - answer = resp.choices[0].message.content - messages.append({"role": "assistant", "content": answer}) - print(answer) -``` - -### 1.3 代码生成 / 文本理解:同一端点换 model - -代码生成、文本理解等场景不需要独立接口,同一端点换模型即可: - -```python -# 代码生成 -resp = client.chat.completions.create( - model="qwen3-coder-plus", - messages="用 Python 写一个二分查找", -) - -# 理解 / 总结类任务同理,只是 model 不同 -resp = client.chat.completions.create( - model="qwen3.7-plus", - messages="总结这段话:……", -) -``` - -### 1.4 `client.apps.completions.create`(1.x:`Application.call`) - -```python -# 非流式 -resp = client.apps.completions.create( - app_id="your-app-id", - prompt="帮我写一份周报大纲", # prompt 与 messages 二选一(至少传一个) -) -print(resp.text) # AppCompletion,信封已扁平化 - -# 带会话与 RAG 参数 -resp = client.apps.completions.create( - app_id="your-app-id", - prompt="继续", - session_id=resp.session_id, # 可选 - rag_options={"pipeline_ids": ["pipe-xxx"]}, # 可选 -) -``` - -`prompt` 传单轮提问,`messages` 传调用方自己维护的完整对话;两者都给时 -`prompt` 作为最新一条 user 消息追加到 `messages` 之后。要让服务端替你保存对话 -状态,用 `session_id=`(会话)或 `memory_id=`(长期记忆)。 -`biz_params=` 承载应用自身的流程 / 插件入参(服务端字段名,位于线缆 `input` -内),模型侧旋钮与签名未列出的参数放 `parameters=`。其余具名参数: -`has_thoughts`(返回思考过程)/ `doc_tag_codes` / `doc_reference_type` / -`image_list` / `file_list` / `temperature` / `top_p` / `top_k` / `seed`。 - -返回 `AppCompletion`: - -```json -{ - "request_id": "...", - "text": "……", - "finish_reason": "stop", - "session_id": "session-xxx", - "thoughts": null, - "doc_references": null, - "workflow_message": null, - "usage": {"models": [{"model_id": "qwen3.7-plus", "input_tokens": 30, "output_tokens": 50}]} -} -``` - -业务级错误体现在继承来的 `code` / `message` / `is_error` 字段上。 - -```python -# 流式 -for partial in client.apps.completions.create( - app_id="your-app-id", - prompt="讲个长一点的故事", - stream=True, # 可选,默认 False - incremental_output=True, # 可选 -): - print(partial.text, end="") -``` - -## 2. 多模态 - -### 2.1 `client.multimodal.chat.create`(1.x:`MultiModalConversation.call`) - -content 为 parts 列表(沿用 1.x 的 `{"image": ...}` / `{"text": ...}` / `{"audio": ...}` -part 形式): - -```python -resp = client.multimodal.chat.create( - model="qwen3-vl-plus", - messages=[{ - "role": "user", - "content": [ - {"image": "https://example.com/cat.jpg"}, - {"text": "这张图里有什么?"}, - ], - }], -) -print(resp.choices[0].message.content) - -# 流式(对应 AioMultiModalConversation 的异步流式同理加 await/async for) -stream = client.multimodal.chat.create( - model="qwen3-vl-plus", - messages=[{ - "role": "user", - "content": [ - {"image": "https://example.com/cat.jpg"}, - {"text": "详细描述这张图"}, - ], - }], - stream=True, # 可选,默认 False -) -for chunk in stream: - print(chunk.choices[0].delta.content or "", end="") -``` - -返回与文本对话相同的 `ChatCompletion`(结构见 §1.1);多模态端点返回的 list 形态 -content 会被扁平化为纯文本字符串。 - -**多数情况下不需要这个资源**:`chat.completions.create` 会检查 messages,只要 -有 image / audio / video part 就自动改走多模态端点,签名与返回类型完全一致。 - -```python -# 等价于上面的 multimodal.chat.create -resp = client.chat.completions.create( - model="qwen3-vl-plus", - messages=[{"role": "user", "content": [ - {"image": "https://example.com/cat.jpg"}, - {"text": "这张图里有什么?"}, - ]}], -) -``` - -`multimodal.chat.create` 保留为**显式逃生口**:无论有没有媒体 part,每次调用都 -打到多模态端点。典型用途是给 qwen-vl 系列模型发一条纯文本消息(按内容路由会 -把它留在文本端点上)。兼容协议(`openai` / `anthropic`)没有独立的多模态端点, -part 翻译在它们自己的 chat 路径上完成,所以那里两个资源是同一次调用。 - -### 2.2 `client.images.generate`:提交 + 等待一步到位(1.x:`ImageSynthesis.call`) - -```python -job = client.images.generate( - model="wanx2.1-t2i-turbo", - prompt="一只在月球上弹吉他的猫,水彩风格", - negative_prompt="模糊, 低质量", # 可选 - size="1024*1024", # 可选,写 "1024x1024" 也可以 - n=1, # 可选 -) -# 返回已完成 Job -for url in job.image_urls: - print(url) -``` - -其余具名参数:`seed` / `style` / `prompt_extend`(智能改写)/ `watermark` / -`function` / `image_urls`(wan2.6-image 参考图列表)/ `extra_input=`(并入线缆 -`input` 段);`generate` 还接受 `poll_interval=` / `max_interval=` 调轮询节奏。 - -**按模型名自动路由协议**:wan2.5 及以下走旧 text2image 端点(仅异步任务); -wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`generate` 同步打 -multimodal-generation 端点,`jobs` 异步任务打 image-generation 端点),出参统一 -归一化为 `Job.results`。调用代码不变,换模型即自动切换。 - -对应 1.x `async_call/fetch/wait` 的分步写法: - -```python -job = client.images.jobs.create( - model="wanx2.1-t2i-turbo", - prompt="一只在月球上弹吉他的猫", - size="1024*1024", # 可选 -) -print(job.task_id, job.status) # PENDING / RUNNING - -snapshot = client.images.jobs.fetch(job.task_id) -print(snapshot.status) - -done = client.images.jobs.wait(job) # 退避轮询到终态,超时抛 DashScopeError -# wait 的 poll_interval / max_interval / timeout 均可选(默认 1s → 8s 退避,总预算 24h) -done = client.images.jobs.wait(job, poll_interval=2.0, timeout=600.0) - -# 任务管理:取消与列举 -client.images.jobs.cancel(job.task_id) -page = client.images.jobs.list(status="SUCCEEDED", page_no=1, page_size=10) -# list 可按 start_time / end_time / model_name / api_key_id / region / status 过滤 -``` - -`generate` 与 `jobs.*` 均返回 `Job`(images / videos / transcription / -embeddings.jobs 共用此结构): - -```json -{ - "task_id": "task-xxx", - "status": "SUCCEEDED", - "request_id": "...", - "submitted_at": "...", - "scheduled_at": "...", - "finished_at": "...", - "output": {"results": [{"url": "https://..."}]}, - "usage": {"image_count": 1}, - "code": null, - "message": null -} -``` - -`status` 取值 `PENDING` / `RUNNING` / `SUCCEEDED` / `FAILED` / `CANCELED` -(另有终态 `UNKNOWN`:任务状态无法确认,`wait` 遇到它直接返回、不抛异常)。 -`FAILED` 时默认(`raise_on_error=True`)抛携带 `code`/`message` 的异常;`wait` -期间任务被 cancel 同样抛错。 - -`Job` 是**唯一保留 `output` 原始字典**的响应类型(各服务的结果字段不同),但配了 -类型化视图,正常取值不必碰 `output`: - -| 取法 | 含义 | -| --- | --- | -| `job.results` | `List[JobResult]`,每项有 `.url` / `.transcription_url`,服务端多给的字段仍可按属性读 | -| `job.image_urls` | `List[str]`,所有结果的 `url` | -| `job.video_url` | `Optional[str]`,视频任务的结果 | -| `job.result_url` | `Optional[str]`,单结果任务的唯一产物(图片 / 视频 / 文件 URL 都收在这一个属性下) | -| `job.job_id` | `task_id` 的别名(`FineTune` 把同一个标识拼作 `job_id`) | -| `job.succeeded` / `job.finished` | 状态判断 | - -时间戳统一为 `submitted_at` / `scheduled_at` / `finished_at`;服务端原始的 -`submit_time` / `scheduled_time` / `end_time` 也能读到同一份值(校验别名), -`status` 同理接受 `task_status`。 - -异步版本(`AioImageSynthesis`)只需 `aclient.images.jobs.create` / `await ...wait(job)`。 - -### 2.3 `client.videos.generate`(1.x:`VideoSynthesis.call`) - -```python -# 文生视频 -job = client.videos.generate( - model="wan2.7-t2v", - prompt="一只小猫在草地上奔跑", - size="1280*720", # 可选;另有 duration / resolution / ratio / - # seed / prompt_extend / watermark 具名参数 -) -print(job.video_url) - -# 图生视频(prompt 与 image_url 可同传,至少传一个) -job = client.videos.generate( - model="wanx2.1-i2v-turbo", - image_url="https://example.com/cat.jpg", -) - -# wan2.7-i2v 起走 media 新协议:类型化素材列表, -# type 为 first_frame / last_frame / driving_audio / first_clip,每种至多一个; -# image_url= 是单个 first_frame 素材的简写(与显式 first_frame 同传会报错) -job = client.videos.generate( - model="wan2.7-i2v", - prompt="镜头缓缓推近", - media=[ - {"type": "first_frame", "url": "https://example.com/start.png"}, - {"type": "last_frame", "url": "https://example.com/end.png"}, - ], -) - -# 分步:jobs.create / fetch / wait / cancel / list,与 images 相同 -job = client.videos.jobs.create(model="wan2.7-t2v", prompt="海浪拍打礁石") -done = client.videos.jobs.wait(job) -``` - -返回值同为 `Job`(结构见 §2.2),视频结果读 `job.video_url`。模型专属的 -`input` 字段经 `extra_input=` 透传,其余服务端参数放 `parameters=`。 - -### 2.4 `client.multimodal.embeddings.create`(1.x:`MultiModalEmbedding.call`) - -```python -resp = client.multimodal.embeddings.create( - model="multimodal-embedding-v1", - input=[ - {"text": "一只猫"}, - {"image": "https://example.com/cat.jpg"}, - # 还支持 {"video": url}、{"audio": url} - ], - # fps / res_level(分辨率档位 0-3)/ max_video_frames 是顶层参数, - # 仅对快照模型生效 -) -for item in resp.embeddings: - print(item.index, item.type, len(item.embedding)) - -# 异步:await aclient.multimodal.embeddings.create(...) -``` - -返回与文本向量相同的 `EmbeddingResponse`(结构见 §3.1),文本 / 图片 / 音频 / -视频的结果落在同一个向量空间里,靠 `item.type` 区分。 - -input 元素也可用类型化的 `EmbeddingItem`(1.x 的 -`MultiModalEmbeddingItemText/Image/Audio` 收敛而来): - -```python -from dashscope.v2.resources.multimodal.embeddings import EmbeddingItem - -resp = client.multimodal.embeddings.create( - model="multimodal-embedding-v1", - input=[ - EmbeddingItem(text="一只猫", factor=1.0), # factor 可选 - EmbeddingItem(image="https://example.com/cat.jpg"), - ], -) -``` - -每个 input 元素至少要设置 `text` / `image` / `audio` / `video` 之一(全空报错); -`EmbeddingItem` 的具名字段就是这四个模态加 `factor`。签名未列出的参数放 -`parameters=`。 - -`oss://` 输入由服务端解析,SDK 自动带上 `X-DashScope-OssResourceResolve: enable` -请求头,**不需要**调用方额外开关(1.x 需要手工传 headers,2.0 已移除该参数)。 - -## 3. 向量 / 排序 / 分词 - -### 3.1 `client.embeddings.create`(1.x:`TextEmbedding.call`) - -```python -resp = client.embeddings.create( - model="text-embedding-v3", - input=["今天天气不错", "明天会下雨吗"], - dimension=1024, # 可选 - text_type="document", # 可选;检索场景 query 侧传 "query" - # 另有 output_type / instruct 具名参数 -) -for emb in resp.embeddings: - print(emb.index, len(emb.embedding)) -print(resp.usage.total_tokens) - -# 异步:await aclient.embeddings.create(...) -``` - -`input` 也接受单个字符串。返回 `EmbeddingResponse`: - -```json -{ - "request_id": "...", - "embeddings": [ - {"index": 0, "type": "text", "embedding": [0.013, ...]}, - {"index": 1, "type": "text", "embedding": [0.021, ...]} - ], - "usage": {"total_tokens": 12} -} -``` - -序号字段统一叫 `index`(1.x 文本向量侧的 `text_index` 是它的校验别名,读到同一 -份值);多模态向量复用同一个 `Embedding` / `EmbeddingResponse`,靠 `type` 区分 -模态。 - -### 3.2 `client.embeddings.jobs.create/fetch/wait`(1.x:`BatchTextEmbedding`) - -```python -job = client.embeddings.jobs.create( - model="text-embedding-async-v2", - file_url="https://example.com/batch_input.txt", # 待向量化文本文件 URL - text_type="document", # 可选 -) -done = client.embeddings.jobs.wait(job) -print(done.result_url) # 结果文件 URL - -# 任务管理:fetch / cancel / list 与 images.jobs 同形 -``` - -返回 `Job`(结构见 §2.2);结果文件 URL 读 `done.result_url`,需要看服务端原始 -字段时再落到 `done.output`。 - -### 3.3 `client.rerank.create`(1.x:`TextReRank.call`) - -```python -resp = client.rerank.create( - model="gte-rerank-v2", - query="什么是量子计算?", - documents=[ - "量子计算是一种利用量子力学原理进行计算的技术。", - "今天超市鸡蛋打折。", - "量子比特可以同时处于 0 和 1 的叠加态。", - ], - top_n=2, # 可选 - return_documents=True, # 可选 - instruct="...", # 可选 -) -for r in resp.results: - print(r.index, r.relevance_score) - -# 异步:await aclient.rerank.create(...) -``` - -返回 `RerankResponse`: - -```json -{ - "request_id": "...", - "results": [ - {"index": 0, "relevance_score": 0.92, "document": {"text": "……"}}, - {"index": 2, "relevance_score": 0.71, "document": {"text": "……"}} - ], - "usage": {"total_tokens": 40} -} -``` - -`document` 仅在 `return_documents=True` 时携带。 - -### 3.4 `client.tokenization.count`(1.x:`Tokenization.call`) - -```python -# model 必填;messages 与 prompt 至少传一个(同给时 prompt 追加为最新 user 消息) -count = client.tokenization.count( - model="qwen3.7-plus", - messages=[ - {"role": "system", "content": "你是助手"}, - {"role": "user", "content": "你好"}, - ], -) -print(count.usage.input_tokens) - -# 或按纯 prompt 计 -count = client.tokenization.count(model="qwen3.7-plus", prompt="你好,世界") -``` - -返回 `TokenCount`:`{"request_id": "...", "usage": {"input_tokens": 12}}`,另带 -`token_ids` / `tokens` 明细;`count.input_tokens` 是 `usage.input_tokens` 的便捷 -属性。`messages` 与 chat 一样接受纯字符串与 parts 列表。 - -只需要本地分词(不发请求)时用 `dashscope.v2.get_tokenizer(model)`; -`dashscope.v2.list_tokenizers()` 列出可用分词器。 - -## 4. 语音 - -完整参数、双向流示例、元数据和生命周期说明见: - -- [ASR Python SDK:实时识别与文件转写](dashscope-2.0-asr-sdk.md) -- [TTS Python SDK:一次性、输出流式与双向流合成](dashscope-2.0-tts-sdk.md) -- [Realtime Python SDK:Omni、Qwen-Audio 与 Livetranslate](dashscope-2.0-realtime-sdk.md) -- [实时语音翻译(Gummy)Python SDK](dashscope-2.0-translation-sdk.md) -- [语音管理面:词表 / 热词 / 声音复刻 / 声音设计](dashscope-2.0-audio-management-sdk.md) - -### 4.1 `client.audio.asr.recognize(...)` 上下文管理器(1.x:`Recognition.start/send_audio_frame/stop`) - -```python -from concurrent.futures import ThreadPoolExecutor - - -def receive_recognition(rec): - for event in rec.events(): - print(event) - - -with client.audio.asr.recognize( - model="paraformer-realtime-v2", - format="pcm", - sample_rate=16000, -) as rec: - with ThreadPoolExecutor(max_workers=1) as pool: - receiver = pool.submit(receive_recognition, rec) - try: - for chunk in iter_pcm_chunks(): # 由应用提供实时 PCM 帧。 - rec.send_audio_frame(chunk) - rec.stop() - receiver.result() - finally: - rec.close() -``` - -识别走 websocket 双工,无 HTTP JSON 响应;`rec.events()` 逐个产出识别事件 -dict(结构同 1.x Recognition 回调里的服务端事件),`rec.results()` 只产出 -识别结果帧。其余可选参数:`vocabulary_id`(定制词表)/ `phrase_id`(热词)/ -`diarization_enabled`(说话人分离)/ `disfluency_removal_enabled`(去语气词)/ -`input=`(透传线缆 input 段)/ `parameters=`。 -会话方法全集:`start()`(进上下文自动调用)/ `send_audio_frame()` / -`update_context()` / `stop()` / `events()` / `results()` / `close()`; -`rec.request_id` / `first_package_delay` / `last_package_delay` 可取。 -`recognize_file(path, ...)` 支持本地文件一次性识别。 - -### 4.2 `client.audio.asr.transcribe`(1.x:`Transcription.async_call/fetch/wait`) - -```python -# 一步到位:提交录音文件转写并等待完成 -# (QwenTranscription 也走这里,换 model 即可,如 model="qwen3-asr-flash-filetrans") -job = client.audio.asr.transcribe( - ["https://example.com/meeting1.wav", "https://example.com/meeting2.wav"], - model="paraformer-v2", # 可选,默认 "paraformer-v2" - # submit_timeout= / poll_interval= / max_interval= 可选,语义同 images.generate -) -for result in job.results: - print(result.transcription_url) - -# 分步控制 -jobs = client.audio.asr.jobs -job = jobs.create(model="paraformer-v2", file_urls=["https://example.com/a.wav"]) -print(job.task_id) -snapshot = jobs.fetch(job.task_id) -done = jobs.wait(job) - -# 异步:await aclient.audio.asr.transcribe(...) -``` - -`transcribe` 返回已完成 `Job`(结构见 §2.2),转写结果读 -`[r.transcription_url for r in job.results]`。 - -### 4.3 `client.audio.tts.synthesize/stream`(1.x:`SpeechSynthesizer.call`) - -```python -# 一次性合成:拿到完整音频字节后写文件 -# 当前走 CosyVoice SpeechSynthesizer WebSocket 协议,不通用适配所有 TTS 协议。 -audio = client.audio.tts.synthesize( - "你好,欢迎收听今天的节目。", # text 必填(位置参数) - model="cosyvoice-v1", # 可选,默认 "cosyvoice-v1" - voice="longxiaochun", # 可选,默认 "longxiaochun" - format="mp3", # 可选,默认 "mp3" - sample_rate=22050, # 可选,默认 22050 - # 其余可选具名参数:volume(音量)/ rate(语速)/ pitch(音调)/ - # language_hints(语种提示)/ enable_ssml -) -with open("out.mp3", "wb") as f: - f.write(audio) - -# 流式合成(对应 1.x tts_v2 SpeechSynthesizer 的流式回调):边收边播 -with open("out.mp3", "wb") as f: - for chunk in client.audio.tts.stream( - "这是一段比较长的文本,会被逐步合成为音频流。", - model="cosyvoice-v1", # 可选,默认 "cosyvoice-v1" - voice="longxiaochun", # 可选,默认 "longxiaochun" - ): - f.write(chunk) # 或直接喂给播放器 -``` - -`stream` 返回 `Iterator[bytes]`(逐块产出的音频字节);`synthesize` 等价于把流 -拼成完整 `bytes` 返回。这两个便捷入口只输出音频,不暴露逐条 JSON 元数据。 - -逐段输入文本时使用 `client.audio.tts.connect(...)` 会话: -`start()` → 多次 `send_text()` / `flush()` → `stop()` → 读完结果 → `close()`。 -进入上下文会自动 `start()`;发送文本与接收结果应并发。 -会话 `events()` 同时返回音频 `bytes` 和原始 JSON,能够读取时间戳、用量等字段; -`audio_chunks()` 只返回音频,但仍更新会话的 `last_response` 和 `usage`。 -完整示例见 [TTS Python SDK](dashscope-2.0-tts-sdk.md)。 - -### 4.3.1 `client.audio.tts.http.synthesize/stream`(1.x:`HttpSpeechSynthesizer.call` / qwen-tts HTTP) - -真 HTTP POST(不经 WebSocket),sambert/cosyvoice 与 qwen-tts 两族共用,按模型名 -前缀路由端点(`qwen*` → multimodal-generation,其余 → audio/tts/SpeechSynthesizer): - -```python -# 一次性(对应 1.x HttpSpeechSynthesizer.call(stream=False)):音频以临时 URL 返回 -result = client.audio.tts.http.synthesize( - "今天天气怎么样?", - model="sambert-zhichu-v1", - voice="zhichu", - format="wav", - sample_rate=16000, -) -print(result.audio_url, result.audio_id, result.expires_at, result.usage) - -# 流式(对应 1.x qwen SpeechSynthesizer.call(stream=True)):SSE 逐片产出 -with open("out.mp3", "wb") as f: - for chunk in client.audio.tts.http.stream( - "那我来给大家推荐一款T恤,这款T恤是纯棉的。", - model="qwen3-tts-flash", - voice="Cherry", - language_type="Chinese", - ): - if chunk.audio: # 本片音频帧(已 base64 解码) - f.write(chunk.audio) - if chunk.is_final: # finish_reason == "stop",带整段音频 URL - print(chunk.audio_url, chunk.usage) -``` - -模型专属的 `input` 字段(如 cosyvoice 的 `volume/rate/pitch`)经 `extra_input=` 透传, -顶层 `parameters=` 原样放入请求体;具名参数与 `extra_input` 同名时具名优先。 -完整参数与返回类型表见 [TTS Python SDK](dashscope-2.0-tts-sdk.md) 的 HTTP 合成一节。 - -### 4.4 `client.audio.realtime.connect(...)`(1.x:`audio.qwen_omni.OmniRealtimeConversation`) - -```python -# connect() 仅 model 必填;ws_url / connector / timeout / headers 均可选 -with client.audio.realtime.connect(model="qwen-omni-turbo-realtime") as rt: - rt.send_event({ - "type": "session.update", - "session": {"modalities": ["text", "audio"], "voice": "Chelsie"}, - }) - rt.send_event({ - "type": "conversation.item.create", - "item": { - "type": "message", - "role": "user", - "content": [{"type": "input_text", "text": "你好"}], - }, - }) - rt.send_event({"type": "response.create"}) - for event in rt.events(): # 连接关闭时迭代结束 - if event["type"] == "response.text.delta": - print(event["delta"], end="") - elif event["type"] == "response.done": - break -``` - -`RealtimeSession` 还提供 `update_session()`、媒体输入、对话条目和响应控制等便捷方法。 -`recv_event()` 取一条事件,正常关闭后返回 `None`;活动响应或结束确认缺失时会抛异常。 -`rt.model` / `rt.url` 可读模型与 websocket 地址。 -`rt.events()` 产出服务端推送的原始事件 dict;`response.done` 只表示本轮响应结束, -不会自动结束会话。Omni/Live 可调用 `end_session()` 后读取至 `session.finished`; -Qwen-Audio 则读完当前响应后 `close()`,不调用 `end_session()`。 -三类模型的字段映射和流程差异见 [Realtime Python SDK](dashscope-2.0-realtime-sdk.md)。 - -### 4.5 `client.audio.translation.recognize(...)`(1.x:`TranslationRecognizerRealtime.start/send_audio_frame/stop`) - -Gummy 实时翻译(`gummy-realtime-v1` / `gummy-chat-v1`)与 `asr.recognize` 同协议、同会话生命周期, -翻译参数具名化,结果走双语类型化视图(对应 1.x 回调里的 -`(transcription_result, translation_result, usage)` 组合): - -```python -with client.audio.translation.recognize( - model="gummy-realtime-v1", - format="pcm", - sample_rate=16000, - source_language="zh", - translation_enabled=True, - translation_target_languages=["en"], -) as session: - session.send_audio_frame(chunk) # 发送线程逐帧喂音频 - session.stop() - for result in session.results(): # 接收线程迭代双语结果 - transcription = result.get_transcription() - for translation in result.get_translations(): - print(translation["lang"], translation["text"]) -``` - -本地文件一次性翻译(1.x `TranslationRecognizerRealtime.call(file)`): -`client.audio.translation.recognize_file(path, model="gummy-realtime-v1", ...)`, -逐项产出 `TranslationResult`。完整示例见 [实时语音翻译(Gummy)Python SDK](dashscope-2.0-translation-sdk.md)。 -qwen3-livetranslate 实时翻译模型走 `realtime.connect`(4.4),不在本资源面。 - -### 4.5.1 `client.tingwu.call` / `client.tingwu.realtime`(1.x:`TingWu.call` / 实时理解) - -听悟(会议 / 音视频理解)两个入口(对应 1.x `dashscope.multimodal.tingwu` 一对): - -```python -# 离线纪要:一次 POST 返回解析结果 -result = client.tingwu.call( - model="tingwu-turbo", - input={"file_url": "https://example.com/meeting.mp4", ...}, - parameters={...}, # 可选 -) -print(result.output, result.usage) - -# 实时理解:websocket 双工上下文管理器,协议同 asr.recognize -with client.tingwu.realtime( - model="tingwu-turbo", - app_id="your-app-id", # 必填 - format="pcm", # 可选 - sample_rate=16000, # 可选 - terminology="...", # 可选,术语库 - max_end_silence=800, # 可选 -) as session: - session.send_audio_frame(chunk) - session.stop() - for event in session.events(): # 产出服务端事件 dict,speech-end 后结束 - ... -``` - -`client.tingwu.realtime` 支持 `connection=` 复用(§4.6.1);异步侧只有 -`aclient.tingwu.call`(ws 实时类保持同步面,与 audio 其余资源一致)。 - -### 4.6 自定义 websocket 连接:`connector=` - -`audio.asr.recognize` / `audio.tts.connect` / `audio.tts.synthesize` / -`audio.tts.stream` / `audio.realtime.connect` / `audio.translation.recognize` -都接受 `connector=`,用于注入自建连接 -(代理、自定义握手或测试)。SDK 会关闭返回的连接对象,不提供连接池归还协议。 -它是一个**可调用对象**: - -```python -# Connector = Callable[[str, Dict[str, str]], Any] -# 入参是 (url, headers),返回一个 websocket 会话对象; -# 缺省时用内置的 websocket-client 实现。 -def my_connector(url, headers): - return my_ws_library.connect(url, header=[f"{k}: {v}" for k, v in headers.items()]) - -rt = client.audio.realtime.connect( - model="qwen-omni-turbo-realtime", - connector=my_connector, # 可选 -) -``` - -只需要换地址时用 `ws_url=`。ASR/TTS 还支持环境变量 -`DASHSCOPE_WEBSOCKET_BASE_URL`;Realtime 使用独立路径,不使用此环境变量。 - -### 4.6.1 连接复用:`client.ws_connection()` + `connection=` - -一条物理连接上跑多个任务(1.x Java 侧常用的复用模式;语音实时对延时 -敏感,省一次握手就是首包延迟): - -```python -conn = client.ws_connection() # 继承 client 的 key/workspace/region/timeout - -# 顺序复用:第二个任务免握手 -for chunk in (pcm_1, pcm_2): - with client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn) as rec: - rec.send_audio_frame(chunk) - rec.stop() - for result in rec.results(): - ... - -# 并发:1 路 ASR(二进制上行) + 1 路 TTS(二进制下行) + 任意多 JSON 任务 -rec = client.audio.asr.recognize(model="paraformer-realtime-v2", connection=conn).start() -tts = client.audio.tts.connect(model="cosyvoice-v1", connection=conn).start() -conn.close() # 由调用方关闭;各会话 close() 只摘离任务、不关共享连接 -``` - -支持 `connection=`:`audio.asr.recognize` / `audio.tts.connect` / -`audio.translation.recognize` / `tingwu.realtime`。协议约束:二进制帧不带 -task_id,单连接二进制上行/下行各单 owner(并发 TTS 需多条连接);realtime -族协议无 task_id、会话结束即关连接,不可复用(压首开延迟用提前 `connect()` -预热)。HTTP 侧所有资源默认共享每 Client 的 keep-alive 池,池参数见 -`settings.http_max_connections` 等(8.5)。详见 -[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 - -### 4.7 管理面:`client.audio.vocabularies` / `phrases` / `voices` / `voice_design`(1.x:`VocabularyService` / `AsrPhraseManager` / `VoiceEnrollmentService` / 声音设计) - -1.x 三个管理类的五动词(`create_*` / `list_*` / `query_*` / `update_*` / `delete_*`) -统一为 `create/list/get/update/delete`(`query_*` → `get`);声音设计在 1.x 无 SDK -类,按官方 REST 建模: - -```python -# 定制词表(1.x VocabularyService):create 返回 vocabulary_id -vocab = client.audio.vocabularies.create( - target_model="paraformer-realtime-v2", - prefix="med", - vocabulary=[{"text": "心肌梗死", "weight": 4}], -) -client.audio.vocabularies.get(vocab.vocabulary_id) -client.audio.vocabularies.list(prefix="med") -client.audio.vocabularies.update(vocab.vocabulary_id, vocabulary=[...]) -client.audio.vocabularies.delete(vocab.vocabulary_id) - -# paraformer 热词(1.x AsrPhraseManager):编译任务,phrase_id 供 asr.recognize 使用 -phrase = client.audio.phrases.create(model="paraformer-v2", phrases={"下一首": 90}) -client.audio.phrases.update(phrase.phrase_id, model="paraformer-v2", phrases={"暂停": 90}) -client.audio.phrases.get(phrase.phrase_id) -client.audio.phrases.list(page_no=1) -client.audio.phrases.delete(phrase.phrase_id) - -# 声音复刻(1.x VoiceEnrollmentService):create 返回 voice_id,可作 TTS voice -voice = client.audio.voices.create( - target_model="cosyvoice-v3-plus", - prefix="demo", - url="https://your-audio-url/sample.wav", -) -client.audio.voices.get(voice.voice_id) -client.audio.voices.list(prefix="demo") -client.audio.voices.update(voice.voice_id, url="https://your-audio-url/new.wav") -client.audio.voices.delete(voice.voice_id) - -# 声音设计(官方 REST,qwen-voice-design):文字描述生成音色,返回 voice + 预览音频 -designed = client.audio.voice_design.create( - target_model="qwen3-tts-vd-realtime-2026-01-15", - voice_prompt="沉稳的中年男性,音色低沉浑厚", - preview_text="各位听众朋友,大家好", -) -client.audio.voice_design.get(designed.voice) -client.audio.voice_design.list() -client.audio.voice_design.delete(designed.voice) -``` - -完整参数与返回类型见 [语音管理面 Python SDK](dashscope-2.0-audio-management-sdk.md)。 - -## 5. 微调 / 部署 / 资源 - -### 5.1 `client.fine_tunes.*`(1.x:`FineTunes.*`) - -```python -# 创建(对应 FineTunes.call) -ft = client.fine_tunes.create( - model="qwen-turbo", - training_file_ids=["file-xxx"], - validation_file_ids=["file-yyy"], # 可选 - mode="sft", # 可选 - hyper_parameters={"n_epochs": 2, "learning_rate": "1e-5"}, # 可选 - finetuned_model="my-ft-name", # 可选 -) -print(ft.job_id, ft.status) - -# 等待终态(对应 FineTunes.wait,退避轮询 + 超时) -done = client.fine_tunes.wait(ft, timeout=7200) # timeout 可选(默认 24h) -print(done.finetuned_model) - -# 事件流(对应 FineTunes.stream_events) -for event in client.fine_tunes.stream_events(ft.job_id): - print(event.output) - -# 训练日志(对应 1.x FineTunes.logs) -logs = client.fine_tunes.logs(ft.job_id, offset=1, lines=200) # offset/lines 可选 -print(logs.content) - -# 其余管理操作 -client.fine_tunes.cancel(ft.job_id) -jobs = client.fine_tunes.list(page_no=1, page_size=10) # page_no/page_size 可选 -one = client.fine_tunes.get(ft.job_id) -client.fine_tunes.delete(ft.job_id) -``` - -`create/get/wait` 返回 `FineTune`(信封已扁平化,字段直接在对象上): - -```json -{ - "request_id": "...", - "job_id": "ft-xxx", - "job_name": "my-ft-name", - "status": "SUCCEEDED", - "model": "qwen-turbo", - "base_model": "qwen-turbo", - "finetuned_model": "ft-20260827-xxxx", - "training_file_ids": ["file-xxx"], - "validation_file_ids": [], - "hyper_parameters": {"n_epochs": 2}, - "created_at": "...", - "finished_at": "..." -} -``` - -`ft.task_id` 是 `job_id` 的别名(与 `Job` 对齐);`ft.succeeded` / `ft.finished` -判断状态。服务端的 `finetuned_output` / `create_time` / `end_time` 是校验别名, -读到同一份值。 - -`list` 返回 `FineTuneList`(`jobs` + `page_no`/`page_size`/`total_count`); -`stream_events` 逐条产出 `FineTuneEvent`(`output` 为事件内容,`is_error` 标记 -错误帧);`logs` 返回 `JobLogs`(`request_id` / `job_id` / `content` 文本); -`cancel` / `delete` 返回 `JobStatus`(`request_id` / `status`)。 - -### 5.2 `client.deployments.*`(1.x:`Deployments.*`) - -```python -# 部署微调产物(对应 1.x Deployments.call) -dep = client.deployments.create( - model="qwen-turbo", - capacity=2, - model_version="ft-20260827-xxxx", # 可选,微调出的模型版本 - suffix="my-suffix", # 可选 - # 其余可选:plan / template_id / deploy_spec / billing_method -) -print(dep.deployed_model, dep.status) - -client.deployments.scale(dep.deployed_model, capacity=4) # 扩缩容 -print(client.deployments.get(dep.deployed_model).status) -for d in client.deployments.list(page_size=20).deployments: # 分页参数可选 - print(d.deployed_model, d.status) # d 为 Deployment,字段直接可读 -for event in client.deployments.stream_events(dep.deployed_model): # 部署事件流 - print(event) -client.deployments.delete(dep.deployed_model) -``` - -`create/get/scale` 返回 `Deployment`(`deployed_model` / `model` / `base_model` / -`status` / `capacity` / `base_capacity` / `ready_capacity` / `workspace_id` / -`charge_type` / `created_at` / `updated_at` + `request_id`);`list` 返回 -`DeploymentList`(`deployments` + `page_no`/`page_size`/`total_count`);`delete` -返回 `JobStatus`(`request_id` / `status`)。 - -`deployed_model` 是后续 `get` / `scale` / `delete` 的定位键,作为位置参数传入。 - -### 5.3 `client.models.list/get`(1.x:`Models.list/get`) - -```python -page = client.models.list(page_no=1, page_size=20, provider="qwen") # 全部可选 -for m in page.models: - print(m.model, m.provider) - -m = client.models.get("qwen3.7-plus") # 不存在抛 NotFoundError -print(m.model) -``` - -`list` 返回 `ModelList`: - -```json -{ - "request_id": "...", - "models": [ - {"model": "qwen3.7-plus", "source": "...", "provider": "qwen", "open_source": false} - ], - "page_no": 1, - "page_size": 20, - "total_count": 100 -} -``` - -`get` 返回单个 `Model`(`list(provider=...)` 的过滤在客户端按 provider/source -匹配)。服务端的 `opensource` 是 `open_source` 的校验别名。 - -### 5.4 `client.files.*`(1.x:`Files.upload/list/get/delete`) - -```python -# purpose="fine-tune" 时本地先做 JSONL 校验 -up = client.files.upload( - file_path="./train.jsonl", - purpose="fine-tune", # 可选,默认 "fine-tune" - description="训练集 v1", # 可选 -) -print(up.file_id) - -for f in client.files.list(page_no=1, page_size=10).files: # 分页参数可选 - print(f.file_id, f.name) - -info = client.files.get(up.file_id) -client.files.delete(up.file_id) -``` - -`upload` / `get` 返回 `File`(`file_id` / `name` / `purpose` / `size` / -`uploaded_at`;信封已扁平化,不再需要 `.data["file_id"]`);`list` 返回 -`FileList`(`files` + `page_no`/`page_size`/`total_count`);`delete` 返回 -`DeletionStatus`(`deleted` 布尔值 + `request_id`)。服务端的 `upload_time` 是 -`uploaded_at` 的校验别名。 - -## 6. Agentic RL - -对应 1.x CLI `dashscope rl` 子命令的编程化使用(`client.rl`; -`client.reinforcement_learning` 是同一实例的长名别名)。资源面分两层: -`client.rl.*` 是本地准备动作,`client.rl.jobs.*` 是训练任务管理。 - -```python -# 注册 rollout / reward 函数(rl register_functions) -registered = client.rl.register_functions( - rollout_classpaths=["my_project.rollout:MyRollout"], # 可选 - reward_classpaths=["my_project.reward:MyReward"], # 可选 - group_reward_classpaths=["my_project.group:MyGroup"], # 可选 - workspace_dir="./", # 可选,默认 "./" - lazy_load=True, # 可选,默认 True -) -print(registered.rollout_instance_ids) - -# 本地测试函数(rl test_functions):三个参数均必填 -client.rl.test_functions( - instance_id=registered.rollout_instance_ids[0], - func_type="rollout", - input_data={"query": "1+1=?"}, -) - -# 上传训练 / 验证数据(rl upload_data) -ids = client.rl.upload_data( - training_files=["./rl_train.jsonl"], - validation_files=["./rl_val.jsonl"], # 可选 -) -print(ids.training_file_ids) - -# 启动完整训练流程(rl run) -job = client.rl.jobs.create(config_path="./rl_config.yaml", job_name="my-rl-job") -# 两参数均可选;config 里的其余字段用 **kwargs 透传 - -# 任务管理(rl get / list / cancel / logs / wait,v2 错误模型) -print(client.rl.jobs.fetch(job.job_id).status) -print(client.rl.jobs.list(page_size=5)) # page_no/page_size 可选 -print(client.rl.jobs.logs(job.job_id, lines=100).content) # offset/lines 可选 -client.rl.jobs.cancel(job.job_id) -client.rl.jobs.delete(job.job_id) -done = client.rl.jobs.wait(job) - -# 函数实例管理(1.x 直通,同样无 timeout=) -client.rl.query_function_instance_logs(instance_id) # 单页日志(原始 dict) -client.rl.query_all_function_instance_logs(instance_id) # 聚合全部分页 -client.rl.delete_function_instance(instance_id) -``` - -返回值:`register_functions` 返回 `RegisteredFunctions`(`rollout_entity_ids` / -`reward_entity_ids` / `group_reward_entity_ids` 及对应的 `*_instance_ids`); -`upload_data` 返回 `UploadedData`(`training_file_ids` / `validation_file_ids`); -`test_functions` 返回 1.x 原对象(业务逻辑复用 1.x);`jobs.create` / -`jobs.fetch` / `jobs.wait` 复用微调资源模型 `FineTune`,`jobs.list` 复用 -`FineTuneList`,`jobs.cancel` 返回 `JobStatus`,`jobs.logs` 返回 `JobLogs` -(结构见 §5.1)。 - -**这些直通方法没有 `timeout=`**:`jobs.create` / `register_functions` / -`test_functions` / `upload_data` / `query_function_instance_logs` / -`query_all_function_instance_logs` / `delete_function_instance` 直通 1.x 业务逻辑 -(涉及本地打包上传与 YAML 解析),超时由 1.x 内部控制;`jobs.fetch` / -`jobs.list` / `jobs.cancel` / `jobs.delete` / `jobs.logs` / `jobs.wait` 走 v2 -传输层,`timeout=` 可用。 - -**但两半的错误行为是统一的**:直通方法的 1.x 异常在挂载层翻成 `SdkError` -(背后没有 HTTP 响应,`status_code is None`,另带目录给出的 `solution`), -任务管理走 v2 传输层抛 `APIStatusError` 子类。两者都是 `DashScopeError` -的子类,所以**一个 `except DashScopeError` 覆盖整个 `client.rl`**(示例见 §9.5)。 - -## 7. AgentStudio(托管 Agent) - -2.0 **完整包含** AgentStudio:`client.agentstudio` 是把 1.x 子 SDK -(`dashscope.agentstudio`)原样懒挂载进来,功能、参数、返回模型与 1.x 完全一致, -不是裁剪版。同步 Client 挂 `agentstudio.Client`,异步 Client 挂 -`agentstudio.AsyncClient`(方法同名同参,加 `await` / `async for` 即可)。 - -典型链路:建 Agent → 开会话 → 发消息 → 流式读回复: - -```python -as_client = client.agentstudio # 首次访问时才构造 - -# 1. 创建托管 Agent(name / model 必填;tools / mcp_servers / skills / -# multiagent / system_prompt / description / metadata 可选) -agent = as_client.agents.create( - name="demo", - model="qwen3.7-plus", - system_prompt="你是助手", # 可选 -) - -# 2. 创建会话(agent 必填,传 id 或 {"id": ..., "version": ...}; -# environment_id / title / resources / vault_ids / -# environment_variables / mcp_configs / metadata 可选) -session = as_client.sessions.create(agent=agent.id, title="第一次对话") - -# 3. 发送事件 + 流式读取回复(user_message 是客户端事件构造辅助函数, -# 同模块还有 user_interrupt / user_tool_result 等) -from dashscope.agentstudio import user_message - -as_client.sessions.events.send(session.id, [user_message("你好")]) -with as_client.sessions.events.stream(session.id) as stream: - for text in stream.text_stream: # 逐条产出完整文本块 - print(text, end="") -# 增量文本:events.stream(session.id, event_deltas=["message"]) 后用 -# stream.text_deltas;直接 for event in stream 则产出类型化 ServerEvent - -# 4. 无需单独关闭:client.close()(异步 aclient.aclose())会连带关闭它 -``` - -资源与方法全集(`*` 为必填;未标注的返回类型即方法名对应模型): - -| 资源 | 方法与关键入参 | 返回 | -| --- | --- | --- | -| `agents` | `create(name*, model*, description/system_prompt/tools/mcp_servers/skills/multiagent/metadata)`、`retrieve(agent_id, version=)`(别名 `get`)、`update(agent_id, version=*, ...)`、`archive(agent_id)`、`list(limit/page/include_archived)`、`list_versions(agent_id)` | `Agent` / `AgentVersion` | -| `sessions` | `create(agent*, environment_id/title/resources/vault_ids/environment_variables/mcp_configs/metadata)`、`retrieve`、`update(title/metadata/vault_ids/...)`、`list(agent_id/statuses/created_at_gt/gte/lt/lte)`、`archive`、`delete` | `Session` / `DeleteResponse` | -| `sessions.events` | `send(session_id, events*)`(空列表抛 `ValueError`)、`list(types/created_at_*/limit/order)`、`stream(session_id, event_deltas=, timeout=)` | `CursorPage[ServerEvent]` / 事件流 | -| `sessions.resources` | `add(session_id, file_id*, resource_type="file", mount_path=)`、`retrieve`、`list`、`delete` | `SessionResource` | -| `sessions.threads` | `list(session_id)`、`retrieve(session_id, thread_id)`、`archive(...)`;`threads.events.list(session_id, thread_id, ...)`(仅 list,无流式) | `SessionThread` | -| `files` | `upload(file*, mime_type=, progress=)`(路径 / 文件对象均可)、`retrieve`、`download(file_id)`(`FileContent` 是 bytes 子类,带 `.write_to_file(path)`)、`list(scope_id=)`、`delete` | `File` | -| `skills` | `create(file_id= 或 file=,二选一)`、`retrieve`、`list(source=)`、`delete`;`skills.versions.create/list/retrieve/download` | `Skill` / `SkillVersion` | -| `vaults` | `create(display_name*, metadata=)`、`retrieve`、`update`、`list(keyword=)`、`archive`、`delete`;`vaults.credentials.create(vault_id, auth*)/retrieve/update/list/archive/delete` | `Vault` / `Credential` | -| `environments` | `create(name*, config*, description/scope/metadata)`(`scope` 默认 `"organization"`)、`retrieve`、`update`、`list`、`archive`、`delete` | `Environment` | -| `deployments` | `create(name*, agent*, initial_events*, description/environment_id/schedule/resources/vault_ids/environment_variables/metadata)`、`retrieve`、`update`、`list(agent_id/keyword/status/created_at_gte/lte)`、`pause` / `unpause` / `archive`、`run(deployment_id)`、`list_runs(deployment_id)` | `Deployment` / `DeploymentRun` | -| `deployment_runs` | `retrieve(deployment_run_id)`、`list` | `DeploymentRun` | -| `webhook_endpoints` | `create(url*, events*, description=)`、`retrieve`、`update`、`delete`、`enable` / `disable`、`test`、`reset_secret`、`list()`、`list_events(webhook_id)` | `WebhookEndpoint` / `WebhookEvent` | -| `security` | `overview()`、`list_agent_logs(current_page/page_size/risk_level/status/risk_name/...)`(页码分页) | `SecurityOverview` / `SecurityAlertList` | - -分页统一为 `CursorPage[T]`(异步 `AsyncCursorPage[T]`):`data` 当前页列表、 -`next_page` 下一页游标、`has_next()` / `get_next()` 手动翻页;直接 `for` 迭代 -会自动翻完所有页。 - -agentstudio 各方法的返回类型是 1.x 子 SDK 自带模型(`Agent` / `Session` / -`ServerEvent` 等),不属于 v2 类型体系,字段以 1.x 为准(响应仍是 `output` -信封,不扁平化)。**异常则已对齐 v2**:挂载层用透明代理把 1.x 异常翻成 -`dashscope.v2.errors` 一族,分两条路—— - -- **HTTP 层错误**(1.x `APIStatusError` 一族):按 `status_code` 进状态映射, - 抛 v2 同名状态类(`BadRequestError` / `AuthenticationError` / - `PermissionDeniedError` / `NotFoundError` / `RateLimitError` / - `InternalServerError` / `ServiceUnavailableError`;409 等目录未收录的状态 - 落通用 `APIStatusError`),`request_id` 保留。1.x 在本地抛的、没带 - `status_code` 的 HTTP 族错误(如缺 `api_key` 的 `AuthenticationError`) - 按其类代表的状态映射,不会退化成 500。 -- **无响应的本地 / 流式错误**(1.x `APIConnectionError` / `APITimeoutError` / - `StreamError` / `StreamClosedError`):翻成 `SdkError`,`code` 为 - `sdk.agentstudio.*`,带目录给出的 `solution` / `allow_retry`(见 §9.5)。 - -所以**一个 `except DashScopeError` 覆盖整个 `client.agentstudio`**: - -```python -from dashscope.v2.errors import DashScopeError, NotFoundError, SdkError - -try: - with client.agentstudio.sessions.events.stream(session.id) as stream: - for text in stream.text_stream: - print(text, end="") -except NotFoundError: - ... # session 不存在(HTTP 404) -except SdkError as e: - ... # 连接 / 超时 / 流式协议错误,e.solution 可直接展示 -except DashScopeError: - ... # 其余全部 -``` - -代理对 `isinstance`(`isinstance(client.agentstudio, AgentStudioClient)` 仍为 -真)、`.transport` 访问和上下文管理协议透明;子资源、流式事件流与分页 -(`CursorPage` / `AsyncCursorPage`)递归包裹,**迭代与翻页中途抛的错误同样被 -翻译**(翻页抓取走的是代理守卫,自动翻页和显式 `get_next()` 都覆盖)。注意 -翻译只发生在 v2 挂载层:直接构造 1.x 的 `dashscope.agentstudio.Client` 仍抛 -原生异常族。 - -懒挂载需要 `workspace`(v2 只向它传 api_key / workspace / region,不透传 -`base_url`):workspace 缺失、且未配置 `DASHSCOPE_AGENTSTUDIO_URL` / -`AGENTSTUDIO_URL` 环境变量时,首次访问 `client.agentstudio` 抛 `ValueError`。 - -## 8. 全局配置 - -### 8.1 Client 参数或 `dashscope.settings`(1.x:`dashscope.api_key` / `save_api_key()`) - -```python -import dashscope - -# 方式一:每个 Client 独立配置(推荐,对应 1.x 配置文件 resolve_config) -client = dashscope.Client( - api_key="sk-xxx", - workspace="ws-xxx", - config_path="~/.dashscope/config.toml", -) - -# 方式二:全局门面,赋值即时生效(对应 1.x 运行时改模块变量) -dashscope.settings.api_key = "sk-xxx" # 未显式赋值时懒读 DASHSCOPE_API_KEY -dashscope.settings.base_url = "https://ws-xxx.cn-beijing.maas.aliyuncs.com/api/v1" -dashscope.settings.workspace = "ws-xxx" -dashscope.settings.region = "cn-beijing" -dashscope.settings.protocol = "native" -dashscope.settings.timeout = 30.0 -dashscope.settings.max_retries = 3 -dashscope.settings.clear() # 清掉显式赋值,回落到环境变量 -``` - -配置优先级:**显式参数 > settings 赋值 > 环境变量 > config.toml > 默认值**。 -`config.toml` 支持 `api_key` / `base_url` / `workspace` / `region` / `protocol` -等键;`api_key` 的最低兜底还有 1.x 的 key 文件 `~/.dashscope/api_key`。 - -`with_options()` 从一个已有 Client 派生出只改若干配置的新实例(共享连接池): - -```python -strict = client.with_options(protocol="openai", timeout=10.0) -``` - -### 8.2 日志级别统一为 `DASHSCOPE_LOG` - -```python -# 环境变量:DASHSCOPE_LOG=DEBUG(取代 1.x 的 DASHSCOPE_LOGGING_LEVEL / LOG_LEVEL) -# 代码内: -dashscope.settings.log_level = "DEBUG" -``` - -### 8.3 websocket 地址 - -```python -# 全局:环境变量 DASHSCOPE_WEBSOCKET_BASE_URL=wss://... -# 或代码内 dashscope.settings.websocket_base_url = "wss://..." -# 单次调用:资源方法的 ws_url= 参数 -audio = client.audio.tts.synthesize( - "你好", - ws_url="wss://ws-xxx.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference", # 单次覆盖 -) -``` - -### 8.4 实例级关闭(1.x:`close_shared_aio_session()`) - -```python -client.close() # 同步 -await aclient.aclose() # 异步 -# 也可用上下文管理: -# with dashscope.Client() as client: ... -# async with dashscope.AsyncClient() as aclient: ... -``` - -### 8.5 HTTP 连接池(keep-alive) - -每个 `Client` 持有一个 httpx 连接池,所有资源的 HTTP 请求默认复用连接 -(1.x 每次调用新建 `requests.Session`);`with_options` 未改连接级选项时 -共享同一个池。池参数走 settings / 环境变量 / config.toml: - -```python -dashscope.settings.http_max_connections = 50 # DASHSCOPE_HTTP_MAX_CONNECTIONS,默认 100 -dashscope.settings.http_max_keepalive_connections = 10 # DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS,默认 20;0 = 关闭 -dashscope.settings.http_keepalive_expiry = 30.0 # DASHSCOPE_HTTP_KEEPALIVE_EXPIRY,默认 5.0 秒 -``` - -池是 per-Client 的:长驻一个 client 才能吃到复用,频繁新建实例会击穿。 - -### 8.6 环境变量一览 - -| 环境变量 | 作用 | 默认 | -| --- | --- | --- | -| `DASHSCOPE_API_KEY` | API Key | — | -| `DASHSCOPE_API_KEY_FILE_PATH` | 从文件读 API Key 的路径 | — | -| `DASHSCOPE_WORKSPACE` | workspace id | —(缺省回退共享网关 + 告警) | -| `DASHSCOPE_REGION` | 地域(六选一) | `cn-beijing` | -| `DASHSCOPE_BASE_URL` | 自定义 HTTP 端点 | — | -| `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | -| `DASHSCOPE_TIMEOUT` | 单次请求超时(秒) | — | -| `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` | -| `DASHSCOPE_CONFIG` | config.toml 路径 | `~/.dashscope/config.toml` | -| `DASHSCOPE_LOG` | 日志级别(debug/info/warning/error) | `warning` | -| `DASHSCOPE_WEBSOCKET_BASE_URL` | ws 端点(不含 realtime 族) | 跟随 workspace/region | -| `DASHSCOPE_HTTP_MAX_CONNECTIONS` | HTTP 连接池上限 | `100` | -| `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | keep-alive 连接数(0=关闭) | `20` | -| `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | keep-alive 空闲回收(秒) | `5.0` | -| `DASHSCOPE_AGENTSTUDIO_URL` / `AGENTSTUDIO_URL` | agentstudio 端点(无 workspace 时的逃生口) | — | - -## 9. 错误处理 - -规范正文见 [`md/dashscope-2.0-design.md`](dashscope-2.0-design.md) §2.7:异常类名与 -HTTP 状态一律从生成的错误目录 `dashscope/v2/_error_registry.py` 派生,不在 SDK 里手写。 - -### 9.1 异常族 - -具体异常类从 `dashscope.v2.errors` import;`dashscope.v2` 只便捷再导出 -`DashScopeError` 与 `ErrorCode`(包根 `dashscope` 都不导出): - -```python -from dashscope.v2 import DashScopeError # 基类,够用时只 import 这一个 -from dashscope.v2.errors import ( - APIStatusError, # 网关声明的失败(9.2 的两层都算) - APIConnectionError, # 连不上,无响应 - APITimeoutError, # 超时(APIConnectionError 子类) - ContentFilterError, # 内容审核拦截(BadRequestError 子类) - SdkError, # SDK 自己报的失败,无响应 - ErrorCode, # 线缆错误码枚举(13 项) -) -``` - -9 个状态类是 `APIStatusError` 的**扁平兄弟**: - -| 类 | `status_code` | 什么时候抛 | -| --- | --- | --- | -| `BadRequestError` | 400 | 参数非法 / 缺参数 / 文件非法 / URL 非法 | -| `AuthenticationError` | 401 | 认证失败 / API Key 无效 | -| `PermissionDeniedError` | 403 | 无权限、未开通、**欠费** | -| `NotFoundError` | 404 | 模型或资源不存在 | -| `RequestTooLargeError` | 413 | 请求体超限 | -| `RateLimitError` | 429 | 限流、并发超限、配额用尽 | -| `InternalServerError` | 500 | 服务端内部错误 | -| `ServiceUnavailableError` | 503 | 服务不可用 | -| `GatewayTimeoutError` | 504 | 网关超时 | - -`ContentFilterError` 是 `BadRequestError` 的**子类**(不是第 10 个兄弟),所以 -`except BadRequestError` 也抓得到审核拦截;要单独处理就把 -`except ContentFilterError` 写在**前面**: - -```python -try: - client.chat.completions.create(model="qwen3.7-plus", messages=[{"role": "user", "content": "..."}]) -except ContentFilterError as e: # 必须排在 BadRequestError 之前 - print("内容被审核拦截", e.request_id) -except BadRequestError as e: # 其余 400 - print("请求参数有问题", e.code, e.message) -``` - -`APIConnectionError` / `APITimeoutError` / `SdkError` 的 `status_code` 是 `None`: -它们背后没有 HTTP 响应。目录未收录的状态码(502 / 507 / 418…)落 -`APIStatusError` 本身,SDK 不猜它是哪一类。 - -### 9.2 两层错误,一个异常族 - -DashScope 网关除了返回非 2xx,也可能返回 **HTTP 200 但 body 带业务错误码** -(`code` / `message`)。两层抛的是**同一族异常**,实例的 `status_code` -说明是哪一层: - -```python -from dashscope.v2.errors import RateLimitError - -try: - resp = client.chat.completions.create(model="qwen3.7-plus", messages=[{"role": "user", "content": "..."}]) -except RateLimitError as e: - # e.status_code == 429 → HTTP 层限流 - # e.status_code == 200 → 业务层限流(线缆码 Throttling / .RateQuota / .AllocationQuota) - # 两种都该退避重试,所以按类 catch 就够,不必区分 - print(e.code, e.status_code, e.request_id) -``` - -线缆码(`InvalidApiKey` / `Throttling` / `DataInspectionFailed`…)与目录 key -是两个命名空间:SDK 内部用 crosswalk 把线缆码对到目录项,再由目录项决定异常类。 -用户只需认类,`e.code` 保留原始线缆码供排障。业务层出现的**未知线缆码** -(crosswalk 未收录)落 `DashScopeError` 基类本身,`status_code` 同样是 200。 - -### 9.3 异常上能拿到什么 - -每个异常都携带同一组字段: - -```python -try: - client.images.generate(model="wan2.2-t2i-flash", prompt="一只猫") -except DashScopeError as e: - e.message # 人类可读消息 - e.code # 线缆错误码,如 "Throttling.RateQuota";SDK 侧错误是目录名 - e.status_code # HTTP 状态;业务层是 200;无响应时是 None - e.request_id # 排障用,提工单必带 - e.body # 原始响应体 - e.headers # 响应头 -``` - -`SdkError` 额外有 `solution` / `allow_retry`(见 9.5)。 - -### 9.4 重试 - -`Client(max_retries=N)` 默认 **2** 次,自动重试 408 / 409 / 429 / 500 / 502 / -503 / 504 与连接错误,优先按服务端的 `Retry-After`(含 `retry-after-ms`)等待, -否则指数退避 + 抖动。重试发生在传输层,按**原始状态码**判定,与最终抛出的 -异常类无关;服务端也可以用 `x-should-retry` 响应头显式要求或禁止某次重试 -(优先于状态码判定)。 - -SDK 已经重试过还抛出来,说明这一轮没救回来。是否再手工重试要看错误类别: - -```python -import time -from dashscope.v2.errors import ( - GatewayTimeoutError, - RateLimitError, - ServiceUnavailableError, -) - -# 只有这几类值得再来一轮;401/403/404/400 重试没有意义,同样的请求还会同样失败 -RETRYABLE = (RateLimitError, ServiceUnavailableError, GatewayTimeoutError) - -for attempt in range(3): - try: - resp = client.chat.completions.create( - model="qwen3.7-plus", - messages=[{"role": "user", "content": "..."}], - ) - break - except RETRYABLE: - if attempt == 2: - raise - time.sleep(2**attempt) -``` - -### 9.5 `SdkError`:SDK 自己报的失败 - -`client.rl` 的 1.x 直通方法(`jobs.create` / `register_functions` / -`test_functions` / `upload_data` / 三个函数实例管理方法)涉及本地打包上传与 -YAML 解析,失败时背后**没有 HTTP 响应**,抛 `SdkError`: - -```python -from dashscope.v2.errors import SdkError - -try: - client.rl.upload_data(training_files=["./rl_train.jsonl"]) -except SdkError as e: - e.code # 目录名,如 "sdk.agentic_rl.OSSUploadError" - e.solution # 目录随附的修复建议,可直接展示给用户 - e.allow_retry # 该错误是否值得重试;SDK 不自动重试这一层 - e.status_code # None - e.__cause__ # 原始 1.x 异常,排障时可看 -``` - -`SdkError` 是 `DashScopeError` 的子类,所以 `client.rl` 的两半——直通 1.x 的 -工作流步骤、走 v2 传输的任务管理——**一个 `except DashScopeError` 全覆盖**: - -```python -try: - registered = client.rl.register_functions(rollout_classpaths=["my_project.rollout:MyRollout"]) - job = client.rl.jobs.create(config_path="./rl_config.yaml") - done = client.rl.jobs.wait(job) -except DashScopeError as e: - # 上传失败 → SdkError;任务查询/取消失败 → APIStatusError 子类 - print(type(e).__name__, e.code, e.message, e.request_id) -``` - -`client.agentstudio` 同样做了这层翻译(§7):挂载层代理把 1.x 异常翻成 v2 -族——HTTP 层错误进状态映射,无响应的本地 / 流式错误翻成 `SdkError` -(`sdk.agentstudio.*` 一族),`except DashScopeError` 同样全覆盖。 - -### 9.6 关掉抛异常:`raise_on_error=False` - -```python -lenient = client.with_options(raise_on_error=False) -resp = lenient.chat.completions.create( - model="qwen3.7-plus", - messages=[{"role": "user", "content": "..."}], -) -if resp.is_error: # ResponseBase 从非空 code 自动推导 - print(resp.code, resp.message, resp.request_id) -``` - -返回值模式下不抛异常,改由每个顶层响应继承的 `ResponseBase` 携带 -`code` / `message` / `is_error`。流式则是产出一个 `is_error=True` 的对象后 -结束迭代(`client.apps` 对齐 1.x `Application` 语义)。**连接失败与超时仍然 -抛异常**:那两种情况根本没有响应对象可返回。 diff --git a/md/dashscope-2.0-interfaces-mapping.md b/md/dashscope-2.0-interfaces-mapping.md deleted file mode 100644 index 933a5fd4..00000000 --- a/md/dashscope-2.0-interfaces-mapping.md +++ /dev/null @@ -1,159 +0,0 @@ -# dashscope 2.0 接口映射核对表(M3 3.9 / 3.10) - -对照 [`md/dashscope-interfaces.md`](dashscope-interfaces.md) 第一部分(SDK 接口), -核对 `dashscope/v2/` 资源树的覆盖情况。 - -状态图例:✅ 已迁移 | 🔀 收敛合并 | ❌ 确定下线 | ⚠️ 缺口(未迁移) - -## 文本 / 对话 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `Generation.call` | `client.chat.completions.create` | ✅ | -| `AioGeneration.call` | `AsyncClient.chat.completions.create` | ✅ | -| `Conversation.call` | `client.chat.completions.create`(history 转 messages) | 🔀 | -| `CodeGeneration.call` | `client.chat.completions.create`(同一端点,model 区分) | 🔀 | -| `Understanding.call` | `client.chat.completions.create`(同一端点,model 区分) | 🔀 | -| `Application.call` | `client.apps.completions.create(app_id=..., prompt=...)` | ✅ | - -## 多模态 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `MultiModalConversation.call` | `client.multimodal.chat.create`(content parts) | ✅ | -| `AioMultiModalConversation.call` | `AsyncClient.multimodal.chat.create` | ✅ | -| `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` + `client.images.jobs.create/fetch/wait` | ✅ | -| `ImageGeneration.call/async_call/fetch/wait`(wan2.6-t2i / wan2.6-image) | `client.images.generate`(同步打 multimodal-generation 端点)+ `client.images.jobs.create/fetch/wait`(异步任务打 image-generation 端点);`client.images` 按模型名自动路由协议 | ✅ | -| `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` + `client.videos.jobs.create/fetch/wait` | ✅ | -| `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | ✅ | -| `AioMultiModalEmbedding.call` | `AsyncClient.multimodal.embeddings.create` | ✅ | - -> `client.chat.completions.create` 会检查 messages,含 image/audio/video part 时 -> 自动改走多模态端点(签名与返回类型不变);`client.multimodal.chat.create` 保留 -> 为显式逃生口——无论有无媒体 part 都打到多模态端点(如给 qwen-vl 发纯文本)。 -> 兼容协议(`openai`/`anthropic`)无独立多模态端点,两者是同一次调用。 -> -> `client.images` 同样按模型名路由:旧 text2image 端点仅支持 wan2.5 及以下模型; -> wan2.6-t2i / wan2.6-image 只应答 multimodal-generation 新协议(`messages` 入参 / -> `choices` 出参),同步结果与任务结果都归一化为统一 `Job`(`results` / `image_urls`)。 - -## 向量 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `TextEmbedding.call` | `client.embeddings.create` | ✅ | -| `BatchTextEmbedding`(call/fetch/wait) | `client.embeddings.jobs.create/fetch/wait`(复用 Job 基建,M3 3.10) | ✅ | - -## 排序 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `TextReRank.call` | `client.rerank.create` | ✅ | -| `AioTextReRank.call` | `AsyncClient.rerank.create` | ✅ | - -## 分词 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `Tokenization.call` | `client.tokenization.count` | ✅ | - -## 语音 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器(`send_audio_frame/stop/events`) | ✅ | -| `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe(file_urls)` + `client.audio.asr.jobs.*` | ✅ | -| `audio.qwen_asr.QwenTranscription` | 同上(`transcribe(model=...)` 按模型区分) | 🔀 | -| `SpeechSynthesizer.call`(sambert/cosyvoice) | `client.audio.tts.synthesize/stream`(ws,`model/voice` 区分) | ✅ | -| `audio.tts_v2.SpeechSynthesizer`(CosyVoice2 流式) | `client.audio.tts.stream` | 🔀 | -| `HttpSpeechSynthesizer.call` | `client.audio.tts.http.synthesize/stream`(M4,HTTP 形态;ws 统一入口亦可) | ✅ | -| `audio.qwen_tts.SpeechSynthesizer` | `client.audio.tts.http.*`(M4 按模型名前缀路由)/ `client.audio.tts.synthesize`(ws) | ✅ | -| `TranslationRecognizerRealtime` / `TranslationRecognizerChat` | `client.audio.translation.recognize(...)`(M3) | ✅ | -| `VocabularyService` / `AsrPhraseManager` | `client.audio.vocabularies` / `client.audio.phrases`(M5) | ✅ | -| `VoiceEnrollmentService`(声音复刻) | `client.audio.voices`(M5;qwen 声音设计另建模为 `client.audio.voice_design`,1.x 无对应类) | ✅ | -| `audio.qwen_omni.OmniRealtimeConversation` | `client.audio.realtime.connect(...)`(M2 起含 omni 类型化助手) | ✅ | - -> audio 资源树的推理入口为 `asr` / `tts` / `realtime` / `translation`,管理面为 -> `vocabularies` / `phrases` / `voices` / `voice_design`;`asr` 下直接是 -> `transcribe` / `recognize` / `jobs.*`,不再有 `transcription` 中间层。 -> 实时语音连接复用(一条 ws 多任务/顺序复用)见 -> [`dashscope-2.0-connection-reuse-sdk.md`](dashscope-2.0-connection-reuse-sdk.md)(M6)。 - -## 微调 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `FineTunes.call` | `client.fine_tunes.create` | ✅ | -| `FineTunes.cancel/list/get/delete` | `client.fine_tunes.cancel/list/get/delete` | ✅ | -| `FineTunes.stream_events` | `client.fine_tunes.stream_events` | ✅ | -| `FineTunes.wait` | `client.fine_tunes.wait`(退避轮询 + 超时) | ✅ | -| `FineTunes.logs` | `client.fine_tunes.logs`(rl 侧同一能力为 `client.rl.jobs.logs`) | ✅ | - -## 部署 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | ✅ | - -## 资源 - -| 1.x 接口 | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `Models.list/get` | `client.models.list/get` | ✅ | -| `Files.upload/list/get/delete` | `client.files.upload/list/get/delete` | ✅ | - -## Agentic RL - -| 1.x 接口(CLI `dashscope rl`) | 2.0 资源 / 方法 | 状态 | -| --- | --- | --- | -| `rl register_functions` | `client.rl.register_functions`(复用 1.x 实现) | ✅ | -| `rl test_functions` | `client.rl.test_functions` | ✅ | -| `rl upload_data` | `client.rl.upload_data` | ✅ | -| `rl run` | `client.rl.jobs.create` | ✅ | -| `rl get/cancel/logs/list` | `client.rl.jobs.fetch/cancel/logs/list`(v2 HTTP/错误模型) | ✅ | -| `rl wait`(CLI 轮询) | `client.rl.jobs.wait`(退避轮询 + 超时) | ✅ | - -> `client.reinforcement_learning` 是 `client.rl` 的长名别名(同一实例)。 -> rl 资源面分两层:`client.rl.*` 是本地准备动作,`client.rl.jobs.*` 是训练任务 -> 管理(与 `images.jobs` / `fine_tunes` 同一套 Job 语义)。 -> **`jobs.create` / `register_functions` / `test_functions` / `upload_data` -> 这四个 1.x 直通方法不接受 `timeout=`**(本地打包上传与 YAML 解析的超时由 1.x -> 内部控制);其余 `jobs.*` 走 v2 传输层,`timeout=` 可用。 - -## Assistants API【确定下线】 - -| 1.x 接口 | 状态 | -| --- | --- | -| `Assistants.create/retrieve/list/update/delete` | ❌ | -| `Threads.create/retrieve/update/delete/create_and_run` | ❌ | -| `Messages.create/retrieve/list/update/delete` | ❌ | -| `Runs.*` | ❌ | -| `Steps.list/retrieve` | ❌ | - -托管 Agent 场景由 `client.agentstudio` 承接(1.x 子 SDK 懒挂载,逻辑不动)。 - -## AgentStudio - -| 接口 | 2.0 挂载 | 状态 | -| --- | --- | --- | -| `agentstudio.Client.agents/sessions/events/files/skills/vaults/environments/deployments` | `client.agentstudio.*`(懒挂载 1.x `Client`/`AsyncClient`) | ✅ | - -## 全局配置 - -| 1.x | 2.0 | 状态 | -| --- | --- | --- | -| `dashscope.api_key` / `save_api_key()` | `Client(api_key=...)` + 配置文件(`resolve_config`) | 🔀 | -| `dashscope.api_key` / `base_*` 模块变量运行时修改 | `dashscope.settings` 全局门面(赋值即时生效,懒读 env,M3 3.11) | ✅ | -| `DASHSCOPE_LOGGING_LEVEL` / `LOG_LEVEL` 两套日志级别 | `DASHSCOPE_LOG` 统一(完整级别)+ `settings.log_level` setter | 🔀 | -| `base_http_api_url` / `base_compatible_api_url` | `Client(base_url=..., protocol=...)` | 🔀 | -| `base_websocket_api_url` | `DASHSCOPE_WEBSOCKET_BASE_URL` 环境变量 + 资源 `ws_url=` 参数 | 🔀 | -| `close_shared_aio_session()` | `client.close()` / `await client.aclose()` | 🔀 | - -## 缺口清单 - -| 缺口 | 1.x 对应 | 说明 | -| --- | --- | --- | -| ~~`client.embeddings` 多模态向量化~~ | `MultiModalEmbedding` / `AioMultiModalEmbedding` | 已补齐(M3 3.10):`multimodal.embeddings.create`,端点 `/services/embeddings/multimodal-embedding/multimodal-embedding` | -| ~~`client.embeddings` 批量任务~~ | `BatchTextEmbedding` | 已补齐(M3 3.10):`embeddings.jobs.create/fetch/wait`,复用 3.2 Job 基建 | - -> M3 3.10 后核对表无遗留缺口(除确定下线的 Assistants API 族)。 diff --git a/md/dashscope-2.0-m1-m3-review.md b/md/dashscope-2.0-m1-m3-review.md deleted file mode 100644 index 66d4f590..00000000 --- a/md/dashscope-2.0-m1-m3-review.md +++ /dev/null @@ -1,182 +0,0 @@ -# dashscope 2.0 M1–M3 深度 Review - -> 评审对象:`V2` 分支 `dashscope/v2/`(HEAD = 0c58a74),对照 [`dashscope-2.0-design.md`](dashscope-2.0-design.md) 的 M1/M2/M3 任务表与 P1–P6 原则。 -> 评审方法:逐文件通读 v2 全部源码(约 6000 行)+ 对照设计文档逐任务核对 + 运行测试套件(**254 passed / 3 skipped**,skipped 为 env-gated 的 live 集成测试)+ git 考古验证零接触承诺。 -> 评审日期:2026-08-28。 -> -> **命名已过时**:本文引用的方法名(`apps.call`、`embeddings.create_multimodal`、`asr.recognition`、`asr.transcription.transcribe` 等)在随后的命名统一中已改名,现行名称与设计约束见 [`dashscope-2.0-naming-review.md`](dashscope-2.0-naming-review.md) 与 [`dashscope-2.0-design.md`](dashscope-2.0-design.md) §2.6。本文结论按当时状态保留,不作回填。 - ---- - -## 0. 总体结论 - -**M1–M3 的功能骨架已按设计落地,资源树 100% 映射(除计划下线的 Assistants),测试覆盖扎实;但设计的最高优先级原则 P2(契约正确性优先)仍有三处未闭合的"静默错误"缺口,另有若干健壮性问题集中在重试、流式聚合与兼容开关路径上。** - -| 维度 | 评价 | -| --- | --- | -| 功能实现完整度 | ★★★★☆ 任务表 21/21 项有形交付;agentstudio 的 errors/HTTP 对齐例外条款未兑现,`with_options()` 缺失,AsyncAudio 面不全 | -| 代码健壮性 | ★★★☆☆ 主干路径稳;3 处 P2 级静默错误 + 重试/聚合/兼容开关路径若干中危问题 | -| 架构与理念一致性 | ★★★★☆ 分层/隔离/资源树严格兑现;P6 codegen 进度偏弱,1 行 1.x 触碰超出白名单 | - ---- - -## 1. 功能实现完整度(按任务表逐项核对) - -### M1 — SDK 2.0 骨架(1.1–1.10) - -| # | 任务 | 状态 | 证据与备注 | -| --- | --- | --- | --- | -| 1.1 | Client/AsyncClient + 配置解析 | ✅ 有偏差 | `client.py`、`v2/_config.py` 齐备。**偏差**:`resolve_config` 中 `protocol/timeout/max_retries` 三个键的 env 槽位硬编码为 `None`(`_config.py:220-259`),只有 `api_key/base_url/workspace` 读环境变量——与 3.11 声明的优先级链「显式 > settings > env > config.toml」不一致(env 环断了三键)。 | -| 1.2 | transport(httpx 双栈 + 重试) | ✅ | `transport/http.py`:Retry-After 解析、指数退避 + 抖动、`x-should-retry` 头均在。健壮性问题见 §2-①。 | -| 1.3 | 异常体系 + ErrorCode + 双层检查 + 兼容开关 | ✅ | `errors.py`:`ErrorCode` 枚举、`_STATUS_MAP`/`_CODE_MAP`、HTTP-200-业务失败双层检查、`raise_on_error` 开关全部落地。 | -| 1.4 | 日志改造 | ✅ | `_logging.py`:request_id/状态码/重试决策入日志,`SensitiveHeadersFilter` 同时挂 handler 与子 logger(22fc5d8 修复),`DASHSCOPE_LOG` 一键开启。 | -| 1.5 | 内部规范消息模型 | ✅ | `types/messages.py`:归一化 + 边界加固(dcb8b33)。 | -| 1.6 | client.chat | ✅ | Pydantic 响应、`@overload` 区分流式/非流式、`output_text`、`text_stream`/`get_final_message`、`reasoning_content` 全部在。聚合缺陷见 §2-③。 | -| 1.7 | embeddings / rerank / tokenization | ✅ | 三资源齐备;tokenization 的 prompt→messages 合并与 rerank instruct 参数经 909110d 修正。 | -| 1.8 | files / models | ✅ | 齐备(69f1711 修正 wire 协议)。`provider` 过滤为客户端实现,见 §2-⑦。 | -| 1.9 | 包根导出 | ✅ | `dashscope/__init__.py:84` 导出 `Client/AsyncClient/chat/settings`;附 705 行手写 `__init__.pyi`。 | -| 1.10 | specs 元数据原型 | ⚠️ 偏弱 | `specs/chat.yaml` 存在,但 M1 之后无新增——与 P6「specs 从 M1 起同步建设」有差距,目前仍是单文件试点。 | - -### M2 — 协议兼容层(2.1–2.6) - -| # | 任务 | 状态 | 证据与备注 | -| --- | --- | --- | --- | -| 2.1 | ChatProtocol ABC + native | ✅ | `protocols/base.py` + `native.py`,接口收敛干净。 | -| 2.2 | openai adapter | ✅ | `openai.py`:strict 开关与透传(e786de8)、`reasoning_content` 回显(9aba4cd)、`stream_options.include_usage`。小问题见 §2-⑪。 | -| 2.3 | anthropic adapter | ✅ | `anthropic.py`(449 行):per-stream 状态隔离(bf6cbab)、system 提取、tool_use/tool_result 合并、默认 max_tokens。8.27 修复后是三 adapter 中最扎实的。 | -| 2.4 | SSE 统一解析 | ✅ | `transport/sse.py`:流内错误抛异常、request_id 透传、畸形帧包装(1a2daef),另有 435 行专项验证套件。 | -| 2.5 | acli providers 收敛 | ✅ | 严格遵守「不改同步副本」:协议翻译只在 `v2/protocols/`,`dashscope/acli/providers/` 零改动。 | -| 2.6 | 兼容端点集成测试 | ⚠️ 形式达标 | `tests/v2/protocols/test_integration_endpoints.py` 存在(百炼 compatible-mode + 通用网关 3 例),但全部 env-gated——CI 默认 skip,「兼容端点可直连」无持续回归保障。建议至少在发版流水线中跑一次真实 key。 | - -### M3 — SDK 全量覆盖(3.1–3.11) - -| # | 任务 | 状态 | 证据与备注 | -| --- | --- | --- | --- | -| 3.1 | multimodal.chat | ✅ | content parts 全链路;与 chat 的复制问题见 §3-P6。 | -| 3.2 | Job 基建 | ✅ | `_jobs.py`:`Job` 统一对象 + 退避轮询(1s→8s 封顶)。`DEFAULT_WAIT_TIMEOUT=86400` 与 1.x 的语义差异见 §2-⑫。 | -| 3.3 | images / videos | ✅ | `generate` 同步封装 + `jobs.create/fetch/wait`,模式统一。 | -| 3.4 | audio.asr | ✅ | 批转写走 Job 基建;实时 Recognition 为 ws 上下文管理器,fake connector 可测。 | -| 3.5 | audio.tts + audio.realtime | ⚠️ 有缺口 | 同步面齐备(synthesize/stream + omni session),fe03bc6 已修 realtime 吞传输错误。**缺口**:`AsyncAudio` 只挂了 `asr.transcription`(`audio/__init__.py:55-57`)——异步用户没有 `tts`、`realtime`、`asr.recognition`。ws 实时接口以同步为主可辩护,但资源树 sync/async 不对称且文档未声明,属未解释的缺口。 | -| 3.6 | fine_tunes / deployments | ✅ | 完整 CRUD + `stream_events` + `wait`(sleep 可注入,测试友好)。 | -| 3.7 | rl 薄挂载 | ✅ | 复用 1.x `agentic_rl`,job 管理(get/list/cancel/logs)走 v2 transport + 错误模型,`client.reinforcement_learning` 别名在。符合「业务逻辑不动」的约束。asyncio.run 风险见 §2-⑥。 | -| 3.8 | apps + agentstudio 挂载 | ⚠️ 例外未兑现 | apps 完整(含流式 + compat 错误事件)。**agentstudio 未兑现设计例外条款**:3.8 明确要求「errors 与底层 HTTP 调用对齐 2.0」,但 `client.py:112-122` 的懒挂载只传了 `api_key/workspace`,1.x AgentStudio 的 HTTP 与错误体系原样保留——挂载层形同 pure pass-through。要么实现对齐,要么修订设计文档豁免此项。 | -| 3.9 | 映射核对表 + 全量冒烟 | ✅ | `dashscope-2.0-interfaces-mapping.md` 声明无遗留缺口(除 Assistants 下线);`tests/v2/smoke/test_full_surface.py`(641 行)逐资源过一遍。 | -| 3.10 | embeddings 缺口补齐 | ✅ | `create_multimodal` + 批量 `jobs.*`(68c0e87)。 | -| 3.11 | settings 全局门面 | ✅ | 懒读 env、setter 即时生效、包根导出。线程安全与 clear() 语义见 §2-⑨。 | - -**任务表外的小缺口**:设计 §2.2 提到「`Client(max_retries=N)` / `with_options()` 可调」——`with_options()`(派生修改配置的客户端副本,OpenAI/Anthropic SDK 均有)未实现。要么补上(很便宜:Config 是 frozen dataclass,`dataclasses.replace` 即可),要么从设计文档删去。 - ---- - -## 2. 代码健壮性(按严重度分级) - -### 高危(P2 级——设计原则点名要消灭的"静默错误") - -**① 传输层错误逃逸 SDK 异常体系** -`transport/http.py:113-122`(同步)与 `:252-261`(异步):重试耗尽后 `raise` 原样抛出裸 `httpx.TransportError`。而 `errors.py:68-72` 定义的 `APIConnectionError`/`APITimeoutError` **全库零引用**——定义了不用。后果:用户按文档 `except DashScopeError` 无法捕获连接错误/超时,异常驱动契约在最常见的失败路径上破防(OpenAI SDK 此处映射为 `APIConnectionError`)。 -**修法**:重试耗尽与 `open_stream` 连接失败时映射进 `APIConnectionError`(超时映射 `APITimeoutError`),保留 `__cause__`。 - -**② chat 非流式在 `raise_on_error=False` 下静默吞错** -`chat/completions.py` 非流式路径:`check_response(response, raise_on_error=False)` 静默返回后,紧接着 `protocol.parse_completion(response.json())` 把错误体解析成一个**空的、看似成功的** ChatCompletion 返回。这正是 P2 点名的最高危类别在兼容开关下复发。对比 apps 资源 compat 模式至少产出 `is_error=True` 事件(f3805d8),chat 连标记都没有。 -**修法**:compat 模式下检查响应体 `code` 字段,将其原样放进返回模型(1.x `GenerationResponse` 语义),或在 ChatCompletion 上补 `is_error/code/message`。 - -**③ `get_final_message()` 对 OpenAI 协议 tool_calls 聚合错误** -`chat/completions.py:144-145`:`_aggregate` 对 `tool_calls` 是「最后一个 chunk 覆盖」。native/anthropic 的流式 tool_calls 是累积式,碰巧正确;OpenAI 协议是非累积片段(按 index 分发、arguments 逐段追加),覆盖式聚合会丢工具、截断参数。 -**修法**:按 `index` 归并、字符串字段追加(OpenAI 语义),或按协议分发聚合策略。 - -### 中危 - -**④ `files.upload` 重试会发空 body** -`files.py:136-143`:`with open(...) as f` 把活句柄塞进 multipart;transport 重试时句柄已消耗,第二次尝试发空文件(httpx 不会重读流式 body)。 -**修法**:上传路径重试前 `f.seek(0)` 不可控——应读入 bytes 或对该请求禁用重试。 - -**⑤ 流内错误处理 chat 与 apps 行为分裂** -设计 §2.2:「流内错误统一抛异常,不 yield 错误对象」。chat/SSE 遵守(且 `raise_on_error=False` 也不影响,流内错误照样 raise);apps 在 compat 模式 yield `is_error=True` 事件。两种语义并存且均无文档说明——用户换资源即踩坑。需要统一,或在设计文档中把 apps 列为显式例外。 - -**⑥ rl 同步方法用 `asyncio.run` 包 1.x 异步实现** -`reinforcement_learning.py:136/153/168/191`:在已有运行中 event loop 的环境(Jupyter、FastAPI、pytest-asyncio)直接 `RuntimeError`。1.x 用户群里有 notebook 场景,这是真实风险。 -**修法**:短期文档标注「同步 rl 方法不可在运行中 loop 内调用」;中期给 1.x agentic_rl 补同步入口(属 M 外工作)。 - -**⑦ `models.list(provider=...)` 客户端过滤误导分页** -`models.py:90-93`:只过滤当前页结果,`total_count` 不调整——过滤后 `total_count=100` 但本页只有 3 条,分页消费者会被误导。服务端不支持 provider 过滤时,应至少在文档/docstring 中声明「total_count 为过滤前值」。 - -**⑧ `resolve_config` 三键不读环境变量** -同 1.1 偏差(`_config.py:220-259`)。`DASHSCOPE_PROTOCOL`/`DASHSCOPE_TIMEOUT` 之类的 env 不生效,运维侧无法用 env 统一切协议。 - -### 低危(记录即可) - -- **⑨** `Settings` 用普通 dict 无锁(`_config.py:52` 起),并发赋值非线程安全;`log_level` setter 有副作用(调 `set_log_level`)但 `clear()` 不恢复 logger 级别——状态泄漏。 -- **⑩** `transport/ws.py:180-183` `recv()` 仍 broad-except(finished 时吞异常返回 None);8.27 修了 `recv_event` 与 realtime,`recv()` 的宽松行为是 TTS 路径在用,可接受但应加注释说明取舍。 -- **⑪** openai adapter `parse_completion` 用 `payload["id"]`(completion id)充当 `request_id`(`openai.py:136/153`)——排障工单对不上,语义错位。 -- **⑫** `DEFAULT_WAIT_TIMEOUT=86400` 与 1.x SDK `wait(timeout=-1)`(无限等待)不对齐;1.x CLI 是 3600。86400 是有意的有限等待折中(防挂死),方向正确,但应在迁移指南中明示。 -- **⑬** `fine_tunes.py` 的 `_check_terminal` 在 sync/async 两个类中逐字重复(`:340-353` 与 `:520-533`)——sync/async 手写复制已开始产生实际重复,M6 codegen 之前的漂移风险现实存在。 -- **⑭** `__init__.pyi` 705 行手写 stub 与实现双维护,codegen 缺位期间必然漂移。 - ---- - -## 3. 架构设计与理念一致性 - -### P1 对齐行业基线 —— ✅ 高 - -Client 实例化 + 资源树导航、httpx 双栈、Pydantic 响应、异常层级、自动重试、`output_text` 便捷属性、`dashscope.chat()` 2 行上手——OpenAI/Anthropic 验证过的基线全部落地,未自创风格。扣分项:`with_options()` 缺失;传输错误未纳入 SDK 异常体系(§2-①)——这两项恰是 Stainless 系 SDK 的标配。 - -### P2 契约正确性优先 —— ⚠️ 部分违反 - -设计的自我定位是「修复静默错误的优先级高于任何新能力」,且开发过程确实在践行:8.25–8.27 的修复提交链(1a2daef 流内错误带 request_id、e786de8 错误信封解析、bf6cbab 流状态隔离、f3805d8 apps compat 错误事件、fe03bc6 realtime 传输错误传播)全部是契约类修复。但 §2-①②③ 三处残留恰好都在 P2 点名的类别里(吞错、错误逃逸、聚合撒谎),**建议作为 M3 收尾项清零后再进 M4**。 - -### P3 渐进迁移 / 零接触隔离 —— ✅ 基本达标(1 处轻微越界) - -git 考古(`git log master..HEAD -- 'dashscope/**' ':!dashscope/v2/**'`)确认 2.0 工作对 1.x 的触碰仅 2 处: - -1. `dashscope/__init__.py` 追加导出(f68a311、e6142da)——设计 §2.4 规则 3 明确要求,合规。 -2. `dashscope/finetune/reinforcement/common/constants.py` 的 `LOGGER_NAME` 从 `"dashscope"` 改为 `"dashscope.finetune.reinforcement"`(0c58a74)——动机正当(避免 1.x rl 的 logger 名与包根 logger 混淆),但规则 1 只允许安全补丁、规则 10 只允许 DeprecationWarning,这一行是 v2 动机驱动的 1.x 行为变更,**超出白名单**。影响极小(日志名变化),建议在设计文档白名单中补记豁免,不宜静默破例。 - -另注:V2 分支同时承载 1.x 常规迭代(v1.26.6–v1.27.2 发布、CLI 打包、Recognition 参数显式化等提交),2.0 与 1.x 发布共用分支——不违反隔离(文件层面无交叉),但发布节奏耦合值得注意。 - -### P4 保留自有优势 —— ✅ 高 - -长任务统一收敛为 `Job` + `jobs.create/fetch/wait`(images/videos/embeddings 批量/asr 转写四处复用同一基建),多模态 content parts 进规范消息模型,RL/微调/部署全流程完整迁入。有限化 wait 超时(86400)是增强而非削弱。无能力下降项。 - -### P5 差异化 CLI 定位 —— 本期范围外(M4 起) - -### P6 codegen 是主线 —— ⚠️ 偏弱 - -`specs/` 目前只有 M1 的 `chat.yaml` 一个试点,M2/M3 期间无新增。与此同时手写复制面在扩大:sync/async × chat/multimodal 的 create/stream/aggregate 脚手架近乎重复(normalize 后 diff 仍有 232 行)、`__init__.pyi` 手写 705 行、`_check_terminal` 已出现逐字重复(§2-⑬)。「specs 从 M1 起同步建设」的执行力度落后于计划,复制债务在 codegen 接管前会持续累积。 - -### §3.3 第三方厂商结论落实 —— ✅ - -reasoning_content 回显(9aba4cd)、`models.list(provider=)` 过滤(客户端)、ErrorCode 覆盖审核/配额类错误——三个 SDK 侧落点都在;CLI 示例属 M4 范围。 - -### 目录布局 —— ✅ - -`v2/` 独立实现、`tests/v2/` 独立测试、包根只导新面,与 §7 目标布局一致。形式偏差:设计写 `resources/images/`、`resources/videos/` 目录,实际是单文件——无关紧要。 - ---- - -## 4. 建议行动项(按优先级) - -| 优先级 | 事项 | 出处 | -| --- | --- | --- | -| P0 | 传输错误映射进 `APIConnectionError`/`APITimeoutError`,删除零引用死类或启用之 | §2-① | -| P0 | chat 非流式 compat 模式不得静默吞错(返回体带 code/is_error) | §2-② | -| P0 | 修 `get_final_message()` 的 OpenAI tool_calls 片段聚合 | §2-③ | -| P1 | `files.upload` 重试空 body(读 bytes 或禁重试) | §2-④ | -| P1 | 统一 chat/apps 流内错误语义,或在设计文档显式豁免 apps | §2-⑤ | -| P1 | 兑现 3.8 agentstudio「errors/HTTP 对齐 2.0」或修订设计文档 | §1-3.8 | -| P2 | 补 `with_options()` 或从设计文档删除 | §1 | -| P2 | AsyncAudio 补齐 tts/realtime 或文档声明 ws 资源仅同步 | §1-3.5 | -| P2 | `resolve_config` 三键 env 补齐或文档化 | §1-1.1 | -| P2 | rl 同步方法 event-loop 冲突写入文档限制 | §2-⑥ | -| P3 | `models.list` 文档声明 total_count 为过滤前值 | §2-⑦ | -| P3 | Settings 线程安全与 `clear()` 恢复 logger | §2-⑨ | -| P3 | 设计文档白名单补记 0c58a74 的 1.x LOGGER_NAME 豁免 | §3-P3 | -| P3 | specs 元数据恢复节奏(P6 纪律);openai request_id 语义修正 | §3-P6、§2-⑪ | -| P3 | 迁移指南写明 `DEFAULT_WAIT_TIMEOUT=86400` 与 1.x 的差异 | §2-⑫ | - ---- - -## 5. 测试证据 - -- `pytest tests/v2`:**254 passed / 3 skipped**(skipped 为 M2 2.6 的 live 集成测试,env-gated)。 -- 测试资产:23 个测试文件,含协议层 947 行(`test_protocols.py`)+ SSE 专项 435 行 + 全资源冒烟 641 行;openai/anthropic 各有 conformance 套件。 -- 冒烟测试覆盖核对表全部资源(chat/multimodal/embeddings/rerank/tokenization/files/models/images/videos/audio asr·tts·realtime/fine_tunes/deployments/rl/apps/agentstudio/async 面)。 diff --git a/md/dashscope-2.0-migration-guide.md b/md/dashscope-2.0-migration-guide.md deleted file mode 100644 index 6e4b10cb..00000000 --- a/md/dashscope-2.0-migration-guide.md +++ /dev/null @@ -1,179 +0,0 @@ -# DashScope 1.x → 2.0 迁移指南 - -> 2.0 是一次有意的重设计,不是 1:1 移植:接口形状向 OpenAI/Anthropic 习惯收敛, -> 管理面与批量任务对齐并增强,推理面差异集中在错误处理与等待语义(§3)。 -> 本文汇总迁移时必须知道的**接口对照、行为差异与不在 2.0 范围的能力**。 -> 逐接口完整核对表见 [`dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md); -> 2.0 各域用法见 [`dashscope-2.0-interfaces-examples.md`](dashscope-2.0-interfaces-examples.md)。 - -## 1. 共存与分层 - -- `dashscope>=2.0` 同一个发布包内含 1.x 与 2.0 两套接口,不做双包分发;1.x 进入 - 冻结维护(只修缺陷、不增功能),存量代码可以先不动,按模块逐步迁移。 -- 2.0 实现直接位于 `dashscope` 根包,`Client` / `AsyncClient` / `chat` / - `settings` / `WsConnection` / `errors` 等均从根包导入;原 `dashscope.v2` - 命名空间已移除。 -- 包根的 `Generation` / `ImageSynthesis` 等 1.x 导出保持不变,两套接口可在 - 同一进程混用(共用 `DASHSCOPE_API_KEY` 等环境变量)。旧 - `dashscope.aigc`、`dashscope.client.base_api`、`dashscope.files` 等 deep import - 通过懒加载别名解析到 `dashscope.legacy_1x`,存量代码无需修改,也不新增警告。 -- AgentStudio 与 FineTune/Agentic RL 的物理实现归入 `dashscope.applications`; - 原 `dashscope.agentstudio`、`dashscope.finetune` 公开路径通过别名保持不变。 - -## 2. 三分钟对照 - -### 2.1 鉴权与全局配置 - -| 1.x | 2.0 | -| --- | --- | -| `dashscope.api_key = "sk-..."` / `save_api_key()` | `Client(api_key=...)`;缺省读 `DASHSCOPE_API_KEY`;支持 `~/.dashscope/config.toml` | -| 运行时改 `dashscope.api_key` 等模块变量 | `dashscope.settings` 全局门面,赋值即时生效 | -| `base_http_api_url` / `base_compatible_api_url` | `Client(base_url=..., protocol=...)` | -| `base_websocket_api_url` | `DASHSCOPE_WEBSOCKET_BASE_URL` 环境变量或资源级 `ws_url=` 参数 | -| `DASHSCOPE_LOGGING_LEVEL` / `LOG_LEVEL` 两套级别 | `DASHSCOPE_LOG` 统一 + `settings.log_level` | -| `close_shared_aio_session()` | `client.close()` / `await aclient.aclose()`(或上下文管理器) | - -### 2.2 各域速查 - -| 域 | 1.x | 2.0 | -| --- | --- | --- | -| 文本生成 | `Generation.call` / `AioGeneration.call` | `client.chat.completions.create`(AsyncClient 同名) | -| 多模态对话 | `MultiModalConversation.call` | 同上(含媒体 part 自动路由)或显式 `client.multimodal.chat.create` | -| 图像 | `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` + `client.images.jobs.create/fetch/wait` | -| 视频 | `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` + `client.videos.jobs.*` | -| 向量 | `TextEmbedding.call` / `BatchTextEmbedding` | `client.embeddings.create` / `client.embeddings.jobs.*` | -| 多模态向量 | `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | -| 排序 | `TextReRank.call` | `client.rerank.create` | -| 分词 | `Tokenization.call` | `client.tokenization.count`(本地分词 `dashscope.tokenizers.get_tokenizer`) | -| 百炼应用 | `Application.call` | `client.apps.completions.create(app_id=...)` | -| 实时识别 | `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器 | -| 批量转写 | `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe(file_urls)` + `client.audio.asr.jobs.*` | -| 语音合成 | `SpeechSynthesizer.call`(sambert/cosyvoice/qwen) | `client.audio.tts.synthesize/stream`(ws);`client.audio.tts.http.*`(HTTP) | -| 实时翻译 | `TranslationRecognizerRealtime` | `client.audio.translation.recognize(...)` | -| Omni 实时 | `OmniRealtimeConversation` | `client.audio.realtime.connect(...)`(omni 类型化助手) | -| 词表/热词/声纹 | `VocabularyService` / `AsrPhraseManager` / `VoiceEnrollmentService` | `client.audio.vocabularies` / `phrases` / `voices`(+ `voice_design`) | -| 微调 | `FineTunes.*` | `client.fine_tunes.*` | -| 部署 | `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | -| 模型/文件 | `Models.list/get`、`Files.*` | `client.models.*`、`client.files.*` | -| Agentic RL | CLI `dashscope rl ...` | `client.rl.*` + `client.rl.jobs.*` | - -```python -# 1.x -from dashscope import Generation -resp = Generation.call(model="qwen3-max", prompt="你好") -if resp.status_code == 200: - print(resp.output.text) - -# 2.0 -import dashscope -client = dashscope.Client() # 缺省读 DASHSCOPE_API_KEY -resp = client.chat.completions.create(model="qwen3-max", messages="你好") -print(resp.choices[0].message.content) # 失败直接抛异常,见 §3.1 -``` - -## 3. 行为差异(迁移前必读) - -### 3.1 错误处理:返回信封 → 抛异常 - -1.x 所有调用返回响应对象,由调用方自查 `status_code` / `code`;2.0 默认 -`raise_on_error=True`,失败直接抛异常族(`DashScopeError` 及其子类,按错误目录 -从线缆码映射): - -```python -# 1.x -resp = Generation.call(...) -if resp.status_code != 200: - print(resp.code, resp.message) - -# 2.0 -from dashscope import DashScopeError -try: - resp = client.chat.completions.create(...) -except DashScopeError as e: - print(e.code, e.message, e.status_code, e.request_id) - -# 兼容模式:恢复信封风格,自查 is_error / code / message -client = dashscope.Client(raise_on_error=False) -``` - -### 3.2 异步任务等待语义 - -| 维度 | 1.x(`wait_timeout=-1`) | 2.0(`Jobs.wait`) | -| --- | --- | --- | -| 等待超时 | 默认无限等待;超时**返回**错误响应 | 默认 `timeout=86400`(24h);超时**抛** `DashScopeError` | -| 任务 FAILED | 返回响应,自查 `output.task_status` | `raise_on_error=True` 时按错误目录抛映射异常;`=False` 时返回终态 `Job` | -| 任务 CANCELED | 返回响应 | `raise_on_error=True` 时抛 `DashScopeError`;`=False` 时返回 `Job` | -| 轮询节奏 | 固定间隔 | 指数退避(`poll_interval` → `max_interval`) | - -### 3.3 视频 `prompt_extend` 默认值 - -1.x `VideoSynthesis.call` 的 deprecated 别名参数 `extend_prompt` **默认 `True`** -(`dashscope/legacy_2x/aigc/video_synthesis.py:63`):不显式传参时默认开启提示词扩展。 -2.0 只保留 `prompt_extend=None`(缺省由服务端决定),不恢复 `extend_prompt` -别名。**依赖 1.x 默认开启的调用,迁移后需显式传 `prompt_extend=True`。** - -### 3.4 流式输出形态 - -1.x `Generation.call(stream=True)` 默认每帧**重放全量**文本(需 -`incremental_output=True` 才给增量);2.0 流式一律**增量**(OpenAI 形态), -自行拼接: - -```python -stream = client.chat.completions.create(model="qwen3-max", messages="你好", stream=True) -for chunk in stream: - delta = chunk.choices[0].delta - if delta.content: - print(delta.content, end="", flush=True) - # 思考模型另有 delta.reasoning_content -``` - -百炼应用(`client.apps.completions`)保留 `incremental_output` 开关,帧形态由它决定。 - -### 3.5 根 `save_api_key` 的实现来源 - -包根 `save_api_key` 现由 `dashscope._config` 提供,签名仍为 -`save_api_key(api_key, api_key_file_path=None)`,默认写入 -`~/.dashscope/api_key`。与旧 1.x writer 相比,新实现显式使用 UTF-8、展开 -`~`,并为自定义路径创建缺失的父目录;`DASHSCOPE_API_KEY_FILE_PATH` 仍只 -影响读取,不改变默认写入位置。需要旧 writer 时可从 -`dashscope.legacy_1x.common.api_key` 显式导入。 - -### 3.6 日志名称与隔离 - -`DASHSCOPE_LOG` 和 `settings.log_level` 的环境变量、级别及脱敏语义不变。 -当前客户端使用独立的 `dashscope._logging` logger 层级,不再以 `v2` 命名, -也不会通过配置共享的 `dashscope` 根 logger 改变 1.x 的 level、handlers 或 -filters。按名称获取当前客户端 logger 时应使用 `dashscope._logging`。 - -## 4. 2.0 新能力(迁移后可选用) - -- 全资源 `AsyncClient` 异步孪生(1.x 仅零散 aio 入口)。 -- pydantic 强类型响应(`extra="allow"` 兜底,字段可点出)。 -- 连接复用:`client.ws_connection()` 一条 ws 承载多任务/顺序复用,全模型 HTTP - keep-alive 连接池——见 [`dashscope-2.0-connection-reuse-sdk.md`](dashscope-2.0-connection-reuse-sdk.md)。 -- `client.with_options(...)` 派生不同配置的客户端,共享连接池。 -- 音频专项:HTTP 合成 `client.audio.tts.http`、实时翻译 `client.audio.translation`、 - 词表/热词/声纹管理面、omni 类型化助手(1.x 均无对应或形态不同)。 - -## 5. 不在 2.0 范围的能力 - -以下能力 **v2 不提供**;均已评审拍板,不再视为缺口。 - -| 能力 | 1.x 位置 | 处置 | 迁移建议 | -| --- | --- | --- | --- | -| Assistants / Threads / Runs | `dashscope/legacy_2x/assistants/`、`dashscope/legacy_2x/threads/` | **随 1.x 下线,2.0 有意不包含** | 勿用于新代码;托管 Agent 场景评估 `client.agentstudio` 或自建编排 | -| CodeGeneration | `legacy_2x/aigc/code_generation.py` | hold,2.0 先不支持 | 继续用 1.x;代码生成场景可直接走 `chat.completions` 同一端点 | -| nlp Understanding | `legacy_2x/nlp/understanding.py` | hold,2.0 先不支持 | 继续用 1.x | -| MultiModalDialog | `legacy_2x/aigc/multimodal_dialog.py` | hold,2.0 先不支持 | 继续用 1.x;或改 `client.multimodal.chat.create` | -| Conversation / History | `dashscope/legacy_2x/aigc/conversation/` | hold,2.0 先不支持 | 继续用 1.x;多轮对话以 messages 数组自行维护(`chat.completions`) | -| `dashscope` CLI(v2 版) | acli(自包含,走 1.x) | v2 无 CLI;计划项,后续再加 | 命令行场景继续用 1.x CLI | - -## 6. 相关文档 - -- [DashScope 2.0 设计规范](dashscope-2.0-design.md) -- [2.0 接口示例总览](dashscope-2.0-interfaces-examples.md) -- [2.0 接口映射核对表](dashscope-2.0-interfaces-mapping.md) -- [2.0 vs 1.x 对齐评审](dashscope-2.0-vs-1.x-alignment-review.md) -- 语音分册:[ASR](dashscope-2.0-asr-sdk.md) / [TTS](dashscope-2.0-tts-sdk.md) / - [实时翻译](dashscope-2.0-translation-sdk.md) / [Realtime](dashscope-2.0-realtime-sdk.md) / - [连接复用](dashscope-2.0-connection-reuse-sdk.md) / [音频管理面](dashscope-2.0-audio-management-sdk.md) diff --git a/md/dashscope-2.0-migration.md b/md/dashscope-2.0-migration.md deleted file mode 100644 index 0ef3480a..00000000 --- a/md/dashscope-2.0-migration.md +++ /dev/null @@ -1,344 +0,0 @@ -# DashScope 2.0 根目录迁移与 `legacy_2x` 归档方案 - -> 状态:设计稿,尚未实施。 -> -> 前置方案:`md/dashscope-2.0-agentstudio-agenticrl-migration.md`。 -> 本文只描述后续目录迁移;不修改 `dashscope-error-registry`,不重复实施 -> AgentStudio / AgenticRL 错误体系统一,也不调整已经完成的 `examples/` 迁移。 - -## 1. 目标 - -1. `dashscope/v2/` 不再作为实现目录,2.0 实现提升到 `dashscope/` 根目录; - 根目录公开 API(`dashscope.Client`、`dashscope.AsyncClient` 等)与实现位置一致。 -2. AgentStudio、AgenticRL / reinforcement 遵循前置方案已经确认的结论: - - 单一实现; - - 原生使用统一错误体系; - - 保留既有 1.x API 行为和需要兼容的导入路径; - - `dashscope.reinforcement` 是 reinforcement 的公开入口, - `dashscope.finetune.reinforcement` 继续可用。 -3. `dashscope/acli/` 保留在根目录,不归档。 -4. 其余属于 2.x 阶段旧公开面的目录和模块迁入 `dashscope/legacy_2x/`, - 明确标记为后续下线,不在本次迁移中直接删除。 -5. `tests/v2/` 提升为 `tests/` 根目录;其余测试套件迁入 - `tests/legacy_1x/`。测试发现、CI、coverage、pre-commit、测试内部导入和 - 路径计算同步修正。 -6. `examples/` 已完成,本次不再搬迁或改写。 - -## 2. 范围界定与不可混淆的目录 - -“除 acli 外其他 2.x 目录”按本文 §4 的显式清单执行,不能解释为“移动除 -acli 外的所有目录”。以下目录有特殊职责,不纳入 `legacy_2x`: - -| 目录 | 处理 | -|---|---| -| `dashscope/acli/` | 当前 CLI,保留根目录 | -| `dashscope/_shared/` | 跨层共享基础设施,保留根目录 | -| `dashscope/legacy_1x/` | 既有 1.x 业务实现,不是本次新建的归档目录 | -| `dashscope/agentstudio/` | 提升为 2.0 实现目录,遵循前置方案 | -| `dashscope/reinforcement/` | 提升为 reinforcement 实现目录和公开入口 | -| `dashscope/finetune/` | 保留 1.x finetune API 与 AgenticRL 兼容面,按 §3.3 合并 | -| `dashscope/tokenizers/` | 合并 2.0 tokenizer 实现与既有兼容导出,不整体归档 | -| `dashscope/v2/` | 内容迁出后删除该实现命名空间 | - -`legacy_2x` 是 2.x 阶段旧 API / 兼容面的过渡归档位置;`legacy_1x` 是其下层 -历史实现的现有位置。本次不把两者合并,也不借目录迁移制造第二份实现。 - -`dashscope.v2` 当前定位为内部实现命名空间,不作为需要长期保留的公开兼容面。 -迁移时所有内部 import、patch target、动态模块字符串和测试统一改为根目录新路径; -最终不保留 `dashscope/v2/` 实现副本。 - -## 3. `dashscope/` 迁移设计 - -### 3.1 目标结构(关键部分) - -```text -dashscope/ -├── __init__.py # 合并导出,不用 v2/__init__.py 覆盖 -├── client.py # 当前 Client / AsyncClient 实现 -├── errors.py # 当前统一错误体系 -├── _error_registry.py # vendored catalog,来源和 pin 不变 -├── _config.py -├── _logging.py -├── _quickstart.py -├── agentic_rl.py # 当前 AgenticRL 实现,见 §3.3 -├── agentstudio/ # 当前 AgentStudio 实现 -├── reinforcement/ # 当前 reinforcement 实现及公开入口 -├── finetune/ -│ ├── _submit.py # 当前 submit transport -│ ├── agentic_rl.py # 既有 1.x AgenticRL 行为和返回类型 -│ └── reinforcement/ # 旧 reinforcement 导入路径的 alias -├── protocols/ -├── resources/ -├── transport/ -├── types/ -├── tokenizers/ # 合并后的实现与资源 -├── acli/ -├── _shared/ -├── legacy_1x/ -└── legacy_2x/ # 过渡归档,后续另行下线 -``` - -### 3.2 常规 v2 文件映射 - -| 当前位置 | 目标位置 | 说明 | -|---|---|---| -| `dashscope/v2/client.py` | `dashscope/client.py` | 现有 `dashscope/client/` 先按 §4 归档,消除文件/包冲突 | -| `dashscope/v2/errors.py` | `dashscope/errors.py` | catalog 映射和错误类不变 | -| `dashscope/v2/_error_registry.py` | `dashscope/_error_registry.py` | 不改 registry 上游或 vendored 内容 | -| `dashscope/v2/_config.py` | `dashscope/_config.py` | 仅调整内部导入 | -| `dashscope/v2/_logging.py` | `dashscope/_logging.py` | 仅调整内部导入 | -| `dashscope/v2/_quickstart.py` | `dashscope/_quickstart.py` | 仅调整内部导入 | -| `dashscope/v2/protocols/` | `dashscope/protocols/` | 整目录提升 | -| `dashscope/v2/resources/` | `dashscope/resources/` | 整目录提升 | -| `dashscope/v2/transport/` | `dashscope/transport/` | 整目录提升 | -| `dashscope/v2/types/` | `dashscope/types/` | 整目录提升 | -| `dashscope/v2/tokenizers/` | `dashscope/tokenizers/` | 合并,不覆盖既有兼容导出 | -| `dashscope/v2/__init__.py` | 合并进 `dashscope/__init__.py` | 根导出仍以现有 `dashscope/__init__.py` 为基线 | -| `dashscope/v2/__init__.pyi`(若后续存在) | 合并进 `dashscope/__init__.pyi` | 类型导出与运行时一致 | - -仅存在 `__pycache__` 的 `devkit/` 不属于 tracked source,不纳入迁移清单; -清理生成物不作为源码迁移提交。 - -### 3.3 AgentStudio / AgenticRL / reinforcement 特殊映射 - -这些路径不能直接按相对目录覆盖,否则会破坏前置方案已经确认的兼容契约。 - -#### AgentStudio - -- `dashscope/v2/agentstudio/*` → `dashscope/agentstudio/*`,由 shim 位置变为唯一实现。 -- 现有 `dashscope.agentstudio` 导入路径不变,公开类、helper、分页和 SSE 行为不变。 -- 异常继续原生使用根目录统一错误体系;旧异常别名继续保留。 -- 原 `tests/v2/test_agentstudio_shim.py` 中“1.x 路径与 v2 路径是同一模块”的断言 - 不能机械替换为自身比较,应改为验证公开导出、deep import、错误别名和可选依赖行为。 -- `dashscope/v2/agentstudio/` 随 v2 目录退出,不保留第二份实现。 - -#### AgenticRL - -- `dashscope/v2/finetune/agentic_rl.py` → `dashscope/agentic_rl.py`,避免覆盖 - 现有 1.x `dashscope/finetune/agentic_rl.py`。 -- `dashscope/v2/finetune/_submit.py` → `dashscope/finetune/_submit.py`,submit - wire format、header 顺序和返回 envelope 不变。 -- 根 `Client(...).rl` 及其 mount 改为从 `dashscope.agentic_rl` 和 - `dashscope.reinforcement` 导入。 -- `dashscope.finetune.agentic_rl.AgenticRL` 继续保留 1.x 子类、五个任务管理动词、 - `FineTunes` 委托和 `FineTune` 返回类型;当前实现继续返回 `SubmitResult`。 -- 不因目录迁移把 1.x 动词合并进当前实现,也不让两个路径返回类型悄悄一致。 - -#### reinforcement - -- `dashscope/v2/finetune/reinforcement/*` → `dashscope/reinforcement/*`, - 由公开 shim 位置变为唯一实现。 -- `dashscope.finetune.reinforcement` 保留为 alias,deep import 仍解析到同一模块对象。 -- `dashscope.reinforcement` 保持公开 FC / uvicorn / `python -m` dotted path: - `dashscope.reinforcement.component.server.server`。 -- 懒加载约束不变:`component.server.*` 和 - `component.observability.genai.*` 不得因包迁移变成 eager import,也不得把 - fastapi / opentelemetry 提升为根包硬依赖。 -- `dashscope.finetune.agentic_rl` 的 re-export 和旧 CLI 继续可用。 -- 数字 `error_code` 兼容、catalog code / solution / allow_retry、root cause 行为不变。 - -### 3.4 根导出与冲突处理 - -1. `dashscope/__init__.py` 和 `.pyi` 采用合并方式,当前 2.0 导出成为规范来源。 -2. 过渡期内仍需保留的根级旧导出可重定向到 `legacy_2x`,其最终删除与 - `legacy_2x` 下线一起评审;不能在目录迁移提交中静默扩大删除范围。 -3. `dashscope/client.py` 与旧 `dashscope/client/` 不能共存为同一导入名。旧包迁入 - `legacy_2x` 后,旧的 `dashscope.client.*` 深层导入属于兼容性变化,必须在迁移 - 指南和破坏性变更清单中单独列出。 -4. tokenizer 的 `qwen.tiktoken` 随实现移动,运行时 loader 与 `MANIFEST.in` - 必须使用同一路径;不能手改生成的 `egg-info` 来伪装迁移完成。 -5. 所有 import 修改同时覆盖字符串形式:monkeypatch、`importlib`、runpy、uvicorn、 - entry point、Sphinx module mapping 和规格文件,不能只改静态 `from/import`。 - -## 4. `dashscope/legacy_2x/` 归档与下线标记 - -### 4.1 本次归档清单 - -以下现有顶层旧公开面包目录迁入 `dashscope/legacy_2x/`,保持目录名: - -```text -aigc/ api_entities/ app/ assistants/ audio/ cli/ client/ common/ -embeddings/ io/ multimodal/ nlp/ protocol/ rerank/ threads/ utils/ -``` - -以下顶层旧模块一并迁入: - -```text -files.py model.py models.py -``` - -`agentstudio/`、`reinforcement/`、`finetune/`、`tokenizers/` 不在该清单中, -按 §3 的特殊规则处理。`acli/`、`_shared/`、`legacy_1x/` 也不归档。 - -### 4.2 归档后的行为 - -- `dashscope/legacy_2x/__init__.py` 和各入口 docstring 标注“过渡兼容,后续下线”。 -- 在包入口使用带正确 `stacklevel` 的 `DeprecationWarning`,并通过测试固定其含义; - 不改为静默删除或立即抛出导入异常。 -- 归档代码继续复用既有实现,不能复制出另一套业务逻辑。 -- `setup.py` 的 console entry point 从 `dashscope.cli:main` 调整到归档后的实际 - 位置,直至该旧 CLI 正式下线;acli 入口不受影响。 -- `setuptools.find_packages()` 会继续打包 `dashscope.legacy_2x`,这是过渡期预期行为; - 正式下线时再单独调整打包与根导出。 -- 本次不确定具体删除版本。下线前另行完成使用面评估、迁移指南、发布说明和删除 PR。 - -## 5. `tests/` 迁移设计 - -### 5.1 目录映射 - -| 当前位置 | 目标位置 | -|---|---| -| `tests/v2/test_*.py` | `tests/test_*.py` | -| `tests/v2/protocols/` | `tests/protocols/` | -| `tests/v2/smoke/` | `tests/smoke/` | -| `tests/unit/` | `tests/legacy_1x/unit/` | -| `tests/integration/` | `tests/legacy_1x/integration/` | -| `tests/legacy/` | `tests/legacy_1x/legacy/` | -| `tests/data/` | 保持在 `tests/data/` | - -`tests/data/` 是共享 fixtures,不是测试套件,不归档,避免无意义地修改全部数据 -路径。`__pycache__`、coverage 和 pytest cache 是生成物,不纳入迁移。 - -`tests/legacy_1x/` 是测试物理归档位置,不等于立即停止执行。过渡期内其中仍包含 -acli、打包、共享层、AgenticRL 等有效架构守卫,必须通过独立 legacy CI job 继续运行; -后续可按测试实际保护对象再拆分,不在目录迁移 PR 中顺手重写全部测试策略。 - -### 5.2 测试代码需要同步的内容 - -1. 所有 `dashscope.v2...` 导入和 patch target 改为 §3 的根实现路径。 -2. `tests.unit...` 改为 `tests.legacy_1x.unit...`;为新增包层级补齐必要的 - `__init__.py`,保证既有跨测试模块导入可解析。 -3. `tests.legacy...` 等动态导入按新包路径调整;`tests/data` 引用保持不变。 -4. 修正依赖 `__file__` 层级的 repository-root 计算: - - 原 `tests/v2/` 文件上移一级; - - 原 unit / integration / legacy 文件下移一级。 -5. 重写依赖物理目录或模块所有权的架构测试,而不是仅替换字符串: - - AgentStudio shim 身份断言; - - reinforcement 三命名空间、模块身份和懒加载断言; - - package root 所有权断言; - - layer decoupling 的 v2 路径、registry 物理路径和中立包清单。 -6. 测试文档中的命令统一改为当前套件、legacy 套件和全量套件三种形式。 - -## 6. 测试流程与配置修正 - -### 6.1 pytest 与 CI - -建议固定以下命令,避免“移动目录后 CI 实际没有运行当前 2.0 测试”: - -```bash -# 当前套件(根 tests,排除过渡归档) -python -m pytest tests --ignore=tests/legacy_1x -q - -# 过渡 legacy 套件(过渡期继续执行) -python -m pytest tests/legacy_1x -q - -# 全量回归 -python -m pytest tests -q -``` - -- `pytest.ini` 的 `testpaths = tests` 可保留,但必须通过 `norecursedirs` 或默认命令 - 明确当前套件是否包含 `legacy_2x`;配置和 CI 的语义必须一致。 -- `.github/workflows/unit_test.yml` 当前只运行 `tests/unit`,需改为当前根测试的 - coverage job,并增加 `tests/legacy_1x` 的独立 job;不能只把字符串改成 - `tests/legacy_1x/unit` 后宣称 2.0 已验收。 -- coverage 继续以实际运行的测试为准;若增加 source / omit 配置,根实现与 - `legacy_2x` 的覆盖口径要分别说明。 -- `requirements-test.txt`、`setup.py`、`.gitignore` 中关于 `tests/unit` 的说明同步更新。 - -### 6.2 pre-commit 与静态检查 - -- `.pre-commit-config.yaml` 中所有 `tests/legacy` exclude 改为 - `tests/legacy_1x/legacy`,逐个核对正则锚点,避免旧排除失效后突然对归档代码启用 - 新规则。 -- 对当前根测试保持现有 Black / lint 规则;归档测试沿用迁移前的排除策略。 -- 路径迁移后运行 import / patch target 搜索,禁止残留指向已删除的 - `dashscope/v2` 或 `tests/v2` 的有效配置。 - -### 6.3 打包、规格和文档 - -- `MANIFEST.in`:`dashscope/v2/tokenizers/qwen.tiktoken` 改为新根路径。 -- `setup.py`:核对 `find_packages()`、package data、console entry point; - 重新生成构建元数据,不手改 `dashscope.egg-info/SOURCES.txt`。 -- `specs/chat.yaml` 的 v2 module mapping 改为根模块。 -- 根 README、迁移指南和仍有效的架构文档更新规范导入路径;历史设计稿可保留当时 - 背景,但若作为操作手册使用则必须注明新路径。 -- `docs/source` 的历史 API 文档如继续发布,导入目标需指向保留的兼容路径或在 - `legacy_2x` 下线时同步下线。 - -## 7. 破坏性变化与兼容边界 - -| 项目 | 本次处理 | -|---|---| -| `dashscope.v2` 内部实现路径 | 退出,内部引用全部迁移;不保留第二实现 | -| 根公开 API(`dashscope.Client` 等) | 名称和行为保持,改为根实现直接提供 | -| `dashscope.agentstudio` | 路径和公开行为保持,shim 位置变为实现 | -| `dashscope.finetune.agentic_rl` | 保留 1.x 子类、动词和 `FineTune` 返回 | -| 当前 AgenticRL 实现 | 新实现位置为 `dashscope.agentic_rl`,仍返回 `SubmitResult` | -| `dashscope.reinforcement` | 公开路径保持,变为实现位置 | -| `dashscope.finetune.reinforcement` | 保留 alias 和模块身份 | -| 旧 `dashscope.client.*` deep import | 随旧包进入 `legacy_2x`,列入迁移指南 | -| 其他归档旧 API | 过渡期从 `legacy_2x` 可用;根级保留项按 §3.4 单独审查 | -| `tests/data` | 不变 | -| `examples` | 已完成,本次不改 | - -## 8. 分阶段实施建议 - -1. **准备与基线**:提交前记录完整测试基线;确认工作区中的 AgentStudio / - reinforcement 修改已纳入基线,避免迁移覆盖未提交工作。 -2. **包迁移**:先处理 `legacy_2x` 和目标冲突,再移动常规 v2 文件,最后处理 - AgentStudio / AgenticRL / reinforcement 特殊映射与根导出。 -3. **引用迁移**:修改静态和动态导入、entry point、package data、specs; - 搜索确认没有有效实现引用旧 v2 路径。 -4. **测试迁移**:提升当前测试、归档 legacy 测试、修复包导入和路径深度, - 重写身份 / 分层架构断言。 -5. **流程迁移**:修正 pytest、CI、coverage、pre-commit 和测试文档。 -6. **验收与发布说明**:执行 §9;单独列出 `legacy_2x` 过渡政策和后续下线事项。 - -每个阶段保持可审查的小提交;目录移动与大规模行为改写不混在同一提交中。 -本次只做位置、引用和必要兼容调整,不顺便重构无关 API。 - -## 9. 验收标准 - -### 9.1 当前实现 - -```bash -python -m pytest tests --ignore=tests/legacy_1x -q -coverage run -m pytest tests --ignore=tests/legacy_1x -coverage report -m -``` - -至少额外固定: - -- 根 `Client` / `AsyncClient`、配置、错误、transport、types、protocols 可导入; -- AgentStudio 公开导入、deep import、SSE 和错误别名行为; -- 两类 AgenticRL 返回类型和 1.x 动词; -- reinforcement 两条公开路径的模块身份、runpy、uvicorn dotted path 和可选依赖懒加载; -- tokenizer 资源能从源码树和 wheel 读取。 - -### 9.2 过渡 legacy 与全量 - -```bash -python -m pytest tests/legacy_1x -q -python -m pytest tests -q -pre-commit run --all-files -git diff --check -``` - -并验证: - -- CI 当前 job 确实执行根测试,legacy job 确实执行 `tests/legacy_1x`; -- 默认 pytest 的包含 / 排除语义与文档一致; -- 构建 sdist / wheel,检查 `legacy_2x` 过渡包、tokenizer 数据和 entry point; -- 在干净环境安装 wheel 后执行当前 API 与保留兼容路径的导入冒烟; -- 搜索无残留的 `dashscope/v2`、`tests/v2` 有效路径引用; -- `examples/` 不再产生迁移 diff,既有示例冒烟保持通过。 - -## 10. 非目标 - -- 不修改 error registry 或其 vendored 错误内容。 -- 不重新设计 AgentStudio / AgenticRL 错误体系。 -- 不在本次删除 `legacy_1x` 或 `legacy_2x`。 -- 不把 acli 移入 `legacy_2x`。 -- 不移动 `tests/data`。 -- 不重复修改已经完成的 `examples/`。 -- 不在目录迁移中增加无关功能、重构或清理生成物。 diff --git a/md/dashscope-2.0-naming-review.md b/md/dashscope-2.0-naming-review.md deleted file mode 100644 index 0d74e39f..00000000 --- a/md/dashscope-2.0-naming-review.md +++ /dev/null @@ -1,661 +0,0 @@ -# DashScope 2.0 SDK 命名评审 - -对每个接口的**调用语句**与**返回结构体类**做全量抽取, -并对照 `dashscope/v2/` 当前实现逐一核对(方法名、返回类型注解、参数名),评估命名的 -一致性与客户友好度。抽取以代码为准(原始依据 `md/dashscope-2.0-sdk-interface.md` 已不在 -仓库中);接口清单见 [`dashscope-2.0-interfaces-mapping.md`](dashscope-2.0-interfaces-mapping.md), -调用示例见 [`dashscope-2.0-interfaces-examples.md`](dashscope-2.0-interfaces-examples.md)。 - -核心结论:**`create` 的双重语义(同步推理 / 创建持久资源)不是问题**——OpenAI SDK 同样如此 -(`chat.completions.create` 阻塞,`fine_tuning.jobs.create` 不阻塞),资源语义本身已消歧。 -真正的问题是: - -1. 除 `create` 之外还有 **11 个**表达"发起一次调用"的动词; -2. 返回类型存在 **4 套**互斥的后缀规范,且有 5 处裸 `dict` / `Any` 逃逸; -3. 同样是"跑一个长任务",**阻塞语义各不相同**。 - ---- - -## 一、全量抽取表 - -| 章节 | 调用语句 | 返回类型 | 语义 | 动词 | -|---|---|---|---|---| -| 0 | `dashscope.chat(model, prompt)` | `ChatCompletion` | 同步推理 | chat | -| 1.1 | `client.chat.completions.create(...)` | `ChatCompletion` / `ChatCompletionChunk` | 同步推理 | **create** | -| 1.4 | `client.apps.call(...)` | `AppCompletion` | 同步推理 | **call** ⚠️ | -| 2.1 | `client.multimodal.chat.create(...)` | `ChatCompletion` | 同步推理 | **create** | -| 2.2 | `client.images.generate(...)` | `Job` | 一步式(阻塞) | **generate** ⚠️ | -| 2.2 | `client.images.jobs.create/fetch/wait` | `Job` | 任务生命周期 | create/fetch/wait | -| 2.3 | `client.videos.generate(...)` | `Job` | 一步式(阻塞) | **generate** ⚠️ | -| 2.3 | `client.videos.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | -| 2.4 | `client.embeddings.create_multimodal(...)` | `MultiModalEmbeddingResponse` | 同步推理 | **create_*** ⚠️ | -| 3.1 | `client.embeddings.create(...)` | `EmbeddingResponse` | 同步推理 | **create** | -| 3.2 | `client.embeddings.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | -| 3.3 | `client.rerank.create(...)` | `RerankResponse` | 同步推理 | **create** | -| 3.4 | `client.tokenization.count(...)` | `TokenCount` | 计算 | **count** | -| 4.1 | `client.audio.asr.recognition(...)` | `Recognition`(ctx manager) | 双工会话 | **名词** ⚠️ | -| 4.1 | `rec.send_audio_frame/stop/events` | `None` / `Iterator[dict]` | 会话内 | — | -| 4.2 | `client.audio.asr.transcription.transcribe(urls)` | `Job` | 一步式(阻塞) | **transcribe** ⚠️ | -| 4.2 | `client.audio.asr.transcription.jobs.create/fetch/wait` | `Job` | 任务生命周期 | — | -| 4.3 | `client.audio.tts.synthesize(text)` | `bytes` | 一步式 | **synthesize** ⚠️ | -| 4.3 | `client.audio.tts.stream(text)` | `Iterator[bytes]` | 流式 | **stream** ⚠️ | -| 4.4 | `client.audio.realtime.session(model=)` | `RealtimeSession`(ctx manager) | 双工会话 | **名词** ⚠️ | -| 4.4 | `rt.send_event/recv_event/events/close` | — | 会话内 | — | -| 5.1 | `client.fine_tunes.create(...)` | `FineTune` | 资源创建(不阻塞) | **create** | -| 5.1 | `client.fine_tunes.get/list/wait` | `FineTune` / `FineTuneList` | CRUD | — | -| 5.1 | `client.fine_tunes.cancel/delete` | `FineTuneCancel` / `FineTuneDelete` | CRUD | — | -| 5.1 | `client.fine_tunes.stream_events(job_id)` | `Iterator[FineTuneEvent]` | 流式 | **stream_*** ⚠️ | -| 5.1 | `client.rl.logs(ft.job_id)` | `RlLogs` | 跨资源 ⚠️ | **logs** | -| 5.2 | `client.deployments.create/get/list/delete` | `Deployment` / `DeploymentList` / `DeploymentDelete` | CRUD | — | -| 5.2 | `client.deployments.scale(model, 4)` | `Deployment` | CRUD | **scale** | -| 5.3 | `client.models.list/get` | `ModelList` / `ModelObject` | CRUD | — | -| 5.4 | `client.files.upload(file_path=)` | `FileUploadResponse` | CRUD | **upload** ⚠️ | -| 5.4 | `client.files.list/get/delete` | `FileList` / `FileObject` / `DeletionStatus` | CRUD | — | -| 6 | `client.rl.register_functions(...)` | `Dict[str, List[str]]` | 裸 dict ⚠️ | 动词_名词 | -| 6 | `client.rl.test_functions(...)` | `Any`(1.x 对象) | 类型逃逸 ⚠️ | 动词_名词 | -| 6 | `client.rl.upload_data(...)` | `Dict[str, List[str]]` | 裸 dict ⚠️ | 动词_名词 | -| 6 | `client.rl.run(...)` | `Any`(1.x 对象),**不阻塞** | 语义撞车 ⚠️ | **run** | -| 6 | `client.rl.get/list/cancel/logs` | `FineTune` / `FineTuneList` / `FineTuneCancel` / `RlLogs` | 复用微调 | — | -| 7 | `client.agentstudio.agents/sessions.create` 等 | 1.x 类型 | 懒挂载子 SDK | create | - ---- - -## 二、诊断:方法名与返回类型(按严重度) - -### P0-1 `Job.output` 是全 SDK 唯一的裸 dict 出口 - -`images` / `videos` / `transcription` / `embeddings.jobs` 四类任务共用 `Job`,但字段是 -`output: Optional[Dict[str, Any]]`、`usage: Optional[Dict[str, Any]]`。客户必须背下每个服务的 -字段名: - -- `job.output["results"][0]["url"]`(images) -- `job.output["video_url"]`(videos) -- `job.output["results"][*]["transcription_url"]`(transcription) -- `done.output["url"]`(batch embeddings) - -接口文档 §3.2 甚至只能写"字段名以服务返回为准,如 `url` 或 `results.url`"——这就是没有类型化 -的直接后果。 - -同类问题: - -| 位置 | 现状 | 对照 | -|---|---|---| -| `AppCompletion.usage` | `Optional[Dict[str, Any]]` | chat / embeddings / rerank / tokenization 的 usage 都是 typed model | -| `FileUploadResponse.data` | `Dict[str, Any]`,取 `up.data["file_id"]` | 同一资源的 `files.list` 返回 typed `FileObject` | -| `rl.register_functions` / `rl.upload_data` | `Dict[str, List[str]]` | — | -| `rl.run` / `rl.test_functions` | `Any`(1.x 原对象) | — | - -**建议**:`Job` 保留 `output` 原始字典(服务端字段各异,不宜强类型化整体),但补 typed 视图与 -便捷属性:`JobResult`(`url` / `transcription_url` / 其余透传)、`job.results`、`job.image_urls`、 -`job.video_url`、`job.result_url`。`FileUploadResponse` 改为直接返回 `FileObject`(`up.file_id`)。 - -### P0-2 `generate` / `transcribe` 阻塞,`run` / `create` 不阻塞 - -`images.generate` = `jobs.create` + `jobs.wait`(阻塞到终态),`audio.asr.transcribe` 同理。 -但 `rl.run` 文档写"启动完整训练流程"却**立即返回**,随后要 `rl.get(job.job_id).output.status` -自行轮询;`fine_tunes.create` 同样不阻塞。三个"跑长任务"的入口阻塞语义各不相同,是最容易踩的坑。 - -**建议**确立一条明规则并写进文档首页与每个方法的 docstring: - -> `.create()` 永不阻塞;`.wait()` 才阻塞;裸领域动词(`generate` / `transcribe`) -> 是 `create + wait` 的语法糖,只在任务必然终结时提供。 - -按此规则:`rl.run` → **`rl.jobs.create`**,并补 `rl.jobs.wait(job)`。落地时把 `rl.get/list/cancel/logs` -一并收进 `rl.jobs`(`get` 随之统一为 `fetch`),`rl` 顶层只留 RL 独有的工作流步骤——否则每个任务 -动词都有 `rl.x` 与 `rl.jobs.x` 两种写法,正是本章要消灭的歧义。 - -### P1-1 `apps.call` 是唯一残留的 1.x 动词 - -`AppCompletion` 这个类名已经与 `ChatCompletion` 平行,方法却叫 `call`。改成 -**`client.apps.completions.create(app_id=..., prompt=...)`** 之后,"同步推理一律 `.create()`" -零例外,且类型名与方法名自洽。这是收益最高的一处改名。 - -### P1-2 `embeddings.create_multimodal` 应挂到 multimodal 命名空间 - -`_multimodal` 方法后缀不可扩展(下次加 `create_batch`?)。`client.multimodal` 命名空间已存在 -(`multimodal.chat`),补 `multimodal.embeddings.create(...)` 即可与之完全对称。 - -另外 `MultiModalEmbedding`(输出元素)与 `MultiModalEmbeddingItem`(输入元素)名字过近;且输出 -元素 `MultiModalEmbedding{index, type, embedding}` 与文本侧 `Embedding{text_index, type, embedding}` -字段几乎相同,**建议合并为一个 `Embedding` 类**、响应合并为 `EmbeddingResponse`。 - -### P1-3 `chat.completions.create` 与 `multimodal.chat.create` 是同一件事的两个入口 - -两者返回同一个 `ChatCompletion`,只是 native 端点不同。客户被迫先判断"我这次算不算多模态"。 -建议 `chat.completions.create` 在检测到 `content` 为 parts 列表时自动路由到多模态端点, -`multimodal.chat.create` 降级为显式逃生口。 - -**落地**:端点常量从资源层移到协议层(`ChatProtocol.MULTIMODAL_PATH`,仅 native 适配器有值, -兼容协议在自己的 chat 路径上翻译 parts),`chat.completions.create` 依据 -`types.messages.has_media_parts()` 自动路由;纯文本 parts 列表仍走文本端点。 -`multimodal.chat.create` 保留为逃生口(强制走多模态端点,覆盖 qwen-vl 纯文本提问这类场景), -两者共用 `chat/completions.py` 里的 `_build_request` / `_send` / `_asend`,签名完全一致。 - -### P1-4 delete / cancel 结果有 4 个类名,实际只有 2 种形状 - -| 类 | 形状 | 用于 | -|---|---|---| -| `DeletionStatus` | `{request_id, deleted: bool}` | `files.delete` | -| `FineTuneDelete` | `{request_id, output: StatusResult}` | `fine_tunes.delete` | -| `FineTuneCancel` | `{request_id, output: StatusResult}` | `fine_tunes.cancel`、`rl.cancel` | -| `DeploymentDelete` | `{request_id, output: StatusResult}` | `deployments.delete` | - -后三个结构完全一样。**建议**删掉 `FineTuneDelete` / `FineTuneCancel` / `DeploymentDelete`, -统一复用 `StatusResult`(改名 `JobStatus` 更达意)。 - -### P1-5 同一个"透传字典"有 6 个名字 - -| 参数名 | 出现位置 | 含义 | -|---|---|---| -| `parameters` | images / videos / `jobs.create` / asr | 服务端 `parameters` 段透传 | -| `extra_parameters` | apps / embeddings / rerank / tokenization | **同上** | -| `extra_input` | images / videos | 服务端 `input` 段透传 | -| `hyper_parameters` | fine_tunes | 服务端字段名 | -| `rag_options` | apps | 服务端字段名 | -| `protocol_options` | `Client` | SDK 自有,协议适配开关 | - -前两个是同一个东西,**必须统一为 `parameters`**。其余四个语义各异,可保留。 - -### P1-6 `file_urls` vs `url` - -`audio.asr.*.create(file_urls=[...])` 与 `embeddings.jobs.create(url=...)` 都是"待处理文件 URL", -一复数一单数。 - -**修正**:核对 1.x 实现(`dashscope/embeddings/batch_text_embedding.py`,请求体为 -`input = {"url": url}`)后确认,批量向量化端点**只接受一个文件**,原建议的 `file_urls: List[str]` -会造出一个服务端无法兑现的复数签名。落地为 **`file_url: str`**,并在 docstring 里点明与 -`asr.transcribe(file_urls=)` 的差异。规则因此改为:单复数必须跟随端点的真实入参个数, -不做表面统一。 - -### P1-7 微调训练日志挂在 `rl` 资源面上 - -接口文档 §5.1 明写"训练日志(对应 1.x `FineTunes.logs`,2.0 移到 rl 资源面)",于是纯微调用户 -要写 `client.rl.logs(ft.job_id)`——完全无法发现。既然 `rl.get/list/cancel` 已复用 `FineTune*` -模型说明后端同源,应改为 `client.fine_tunes.logs(job_id)`;`RlLogs` 随之改名 `JobLogs`。 - -**落地**:`fine_tunes.logs(job_id)` 为唯一实现,`RlLogs` → `JobLogs`(字段 `logs` → `content`)。 -不留 `rl.logs` alias——RL 用户经 P0-2 引入的 `rl.jobs.logs` 转发到同一实现,两条路径都是 -资源树的正常形态,而非新旧并存的过渡写法。 - -### P2-1 会话方法用名词 - -`audio.asr.recognition(...)`、`audio.realtime.session(...)` 都是名词当方法名。 -建议 `audio.asr.recognize(...)`、`audio.realtime.connect(...)`(后者与 OpenAI Realtime -`client.realtime.connect` 一致)。 - -### P2-2 层级深度 3~6 层不齐 - -| 深度 | 例 | -|---|---| -| 3 | `embeddings.create` | -| 4 | `multimodal.chat.create`、`audio.tts.synthesize`、`images.jobs.create` | -| 6 | `audio.asr.transcription.jobs.create` | - -最深那条里 `transcription` 是冗余层(`asr` 已含转写义)。建议拍平为 `audio.asr.transcribe(...)` -+ `audio.asr.jobs.create/fetch/wait` + `audio.asr.recognize(...)`,全部收敛到 4~5 层。 - -### P2-3 `*Object` / `*Output` 两套 payload 后缀,且扁平化程度不一 - -- `ModelObject`、`FileObject`(models / files) -- `AppOutput`、`FineTuneOutput`、`DeploymentOutput`(apps / fine_tunes / deployments)——1.x - `response.output` 信封遗留 - -导致取值深度不一:chat / embeddings / rerank / models 扁平(`resp.choices[0]`、`m.model`), -apps / fine_tunes / deployments 要穿 `output`(`dep.output.deployed_model`)。且便捷属性覆盖不全: -`FineTune.job_id` ✓、`AppCompletion.text` ✓、`Deployment` ✗。 - -**建议**:要么全部扁平化,要么给 `Deployment` 补 `dep.deployed_model` / `dep.status`,与另两个 -对齐。`ModelObject` → `Model`。 - -### P2-4 必填主输入的位置 / 关键字风格不统一 - -允许位置传参:`tts.synthesize("你好", ...)`、`transcribe(urls, ...)`、`dashscope.chat(model, prompt)`、 -`deployments.scale(model, 4)`。 -强制 keyword-only:`images.generate(*, model, prompt)`、`videos.generate(*, ...)` 等。 - -建议除顶层快捷入口 `dashscope.chat()` 外统一 keyword-only(IDE 补全与重构更安全), -`scale(model, capacity=4)`。 - -### P2-5 流式有 4 种表达 - -| 形态 | 位置 | 评价 | -|---|---|---| -| `create(stream=True)` → `Iterator[Chunk]` | chat / multimodal | OpenAI 惯例,保留 | -| `synthesize()` → `bytes` vs `stream()` → `Iterator[bytes]` | audio.tts | 返回类型不同,不宜用 kwarg 合并,保留 | -| `stream_events(job_id)` → `Iterator[FineTuneEvent]` | fine_tunes | 唯一的 `stream_*` 前缀方法 | -| `rec.events()` / `rt.events()` → `Iterator[dict]` | websocket 会话 | 服务端事件 schema,dict 可接受 | - -各有理由,但需在文档同一处集中说明选用规则,否则客户会以为漏了什么。 - ---- - -## 三、诊断:入参(按严重度) - -以下基于对 `dashscope/v2/resources/**` 全部公开方法签名的逐条抽取(共 44 个方法)。 - -### P0-A `timeout` 一词三义,且在相邻两层含义相反 - -| 签名 | `timeout` 的实际含义 | -|---|---| -| `Jobs.fetch(task_id, *, timeout=None)` | 单次 HTTP 请求超时 | -| `Jobs.create(*, ..., timeout=None)` | 单次 HTTP 请求超时 | -| `Jobs.wait(job, *, timeout=86400)` | **等待终态的总预算**(默认 24h) | -| `Images.generate(*, timeout=None, wait_timeout=86400)` | `timeout` = 提交那次 HTTP;`wait_timeout` = 轮询 | - -于是 `client.images.generate(..., timeout=600)`——客户的直觉是"最多等 10 分钟",实际语义是 -"提交请求的 HTTP 超时 10 分钟",而等待仍按 24h 走。同一个方法里 `timeout` 与 `wait_timeout` -并存,且 `wait(timeout=600)` 在下一层又变成了等待预算。**这是会直接导致误用的命名。** - -建议:阻塞式一步法(`generate` / `transcribe`)中 `timeout` 表示整体等待预算,提交超时改名 -`submit_timeout`(或走 `client.with_options(timeout=)`);`Jobs.wait` 保持 `timeout`(上下文 -已明确是等待)。 - -### P0-B `task_id` vs `job_id`:两套任务主键 - -| 资源 | 主键属性 | 传参名 | -|---|---|---| -| images / videos / transcription / batch-embedding | `Job.task_id` | `jobs.fetch(task_id)` | -| fine_tunes / rl | `FineTune.job_id`(→ `output.job_id`) | `get(job_id)` / `cancel(job_id)` / `logs(job_id)` | - -同一个 SDK 里"任务标识"有两个名字,客户从 images 走到 fine_tunes 必须重学一遍。 -建议:`Job` 增 `job_id` 别名属性、`FineTune` 增 `task_id` 别名属性,文档统一推荐 `job_id` -(`task_id` 是服务端字段名,不宜直接改)。 - -### P1-C 分页参数 `page` vs `page_no`,默认值也不齐 - -| 方法 | 页码参数 | `page_size` 默认 | -|---|---|---| -| `files.list` | **`page`** ⚠️ | 10 | -| `deployments.list` | `page_no` | 10 | -| `fine_tunes.list` | `page_no` | 10 | -| `models.list` | `page_no` | **20** ⚠️ | -| `rl.jobs.list` | `page_no` | 10 | - -`files.list(page=)` 是孤例,应统一为 `page_no`。`models.list` 的默认 20 若确为服务端默认, -需在 docstring 标注原因,否则对齐为 10。 - -### P1-D 资源标识符参数名有 7 种 - -`task_id` / `job_id` / `file_id` / `deployed_model` / `model_name` / `app_id` / `instance_id`。 -其中两处需修: - -- `models.get(model_name)` → **`model=`**:与 `ModelObject.model` 字段名、以及全 SDK 其余 40 处 - `model=` 都不一致。 -- `rl.test_functions(instance_id, functype, input_data)`:全 SDK 唯一"三个连续必填位置参数"的 - 方法(其余都是 keyword-only),且 `functype` 拼写不规范 → `func_type`,并改 keyword-only。 - -`file_id` / `app_id` / `deployed_model` 与服务端字段一致,保留。 - -### P1-E `**kwargs` 转发吞掉了下一层的完整签名 - -| 位置 | 现状 | 下一层实际签名 | -|---|---|---| -| `Asr.recognition(**kwargs)` | 无任何可见参数 | `Recognition.__init__(*, model, format, sample_rate, parameters, ws_url, timeout)` | -| `Realtime.session(**kwargs)` | 同上 | `RealtimeSession.__init__(...)` | -| `ImageJobs.fetch(task_id, **kwargs)` | 只可见 `task_id` | `Jobs.fetch(task_id, *, timeout)` | -| `ImageJobs.wait(job, **kwargs)` | 只可见 `job` | `Jobs.wait(job, *, poll_interval, max_interval, timeout)` | -| `VideoJobs` / `EmbeddingJobs` / `TranscriptionJobs` 的 `fetch` / `wait` | 同上 | 同上 | - -功能上是通的(`**kwargs` 会转发),但 IDE 补全、类型检查、`help()`、API 文档自动生成全部失效。 -文档开头承诺"所有资源的每次调用都支持 `timeout=` 关键字参数",而客户在工具层面**看不到**这个 -参数——这正是 `**kwargs` 的代价。建议显式写出签名再转发。 - -### P1-F `rl.*` 4 个方法确实没有 `timeout=` - -`register_functions` / `test_functions` / `upload_data` / `run` 既无 `timeout` 也无 `**kwargs`, -与文档开头的全局承诺直接矛盾(这几个走 1.x 实现)。→ 补上,或修正文档表述为"除 `rl` 的 -1.x 透传方法外"。 - -### P1-G `apps.call` 22 个参数:3 个互斥输入 + 3 个透传字典 - -- 输入入口:`prompt` / `messages` / `history` 三者并存且**无互斥校验**(文档只说"prompt 与 - messages 二选一")。`history` 是 1.x 遗留,与 §1.2"2.0 无状态、自行维护 messages"的设计 - 原则冲突。 -- 透传字典:`biz_params` / `rag_options` / `extra_parameters` 三个(其中 `extra_parameters` - 应并入 `parameters`,见 P1-5)。 - -→ 补互斥校验与清晰报错;`history` 标 deprecated 或移除;参数按"高频在前、低频收进 -`parameters`"重排。 - -**落地**:`history` 直接移除——2.0 接口文档里从未出现它,1.x 的形状是 `{user, bot}` 而非 -`role/content`,且与 §1.2"2.0 无状态、自行维护 messages"冲突。移除后只剩 `prompt` / -`messages`,两者组合语义明确(同时给出时 `prompt` 追加为最新一条 user 消息), -`_validate` 继续保证"二者至少给一个"。`extra_parameters` 已并入 `parameters`(见 P1-5)。 - -`biz_params` **不改名**:它是百炼应用侧 flow/plugin 入参的服务端公开字段,且落在 `input` -而非 `parameters` 段,改叫 `parameters` 会指向另一个位置。改为在 `create` docstring 中 -写明它与 `parameters` 的分工。 - -### P1-H 流式返回类型不对称 - -| 入口 | `stream=True` 返回 | 有无便捷能力 | -|---|---|---| -| `chat.completions.create` / `multimodal.chat.create` | `ChatStream` | `text_stream` / `get_final_message()` / `close()` | -| `apps.call` | 裸 `Iterator[AppCompletion]` | 无 | - -同样是流式,chat 给了包装类,apps 给裸迭代器。建议 apps 返回 `AppStream`(至少带 -`text_stream`)。 - -### P2-I `size="1024*1024"` 不接受 `1024x1024` - -服务端用 `*` 分隔,OpenAI 用 `x`。从 OpenAI 迁移的客户会写 `size="1024x1024"`,且 SDK 不校验, -直接吃一个 400。→ 两种写法都接受并归一化。 - -### P2-J 孤例参数 - -| 参数 | 出现位置 | 问题 | -|---|---|---| -| `extra_headers` | 仅 `embeddings.create_multimodal` | 其余 40+ 方法都没有;要么全线补,要么删掉走 `with_options`。**已移除**:它唯一的真实用途是让服务端解析 `oss://` 私桶地址,现在 SDK 检测到 `oss://` 入参就自动带上 `X-DashScope-OssResourceResolve: enable`(与 1.x 行为一致),客户不必再知道这个头 | -| `connector` | `tts.synthesize/stream`、`Recognition` | 无类型注解,文档未提及。**已补**:`transport.ws.Connector = Callable[[str, Dict[str, str]], Any]` 公开导出,三处签名统一注解 | -| `sleep: Callable[[float], Any]` | `Jobs.wait` / `FineTunes.wait` | 测试注入钩子暴露在公开签名 → 改 `_sleep` 或经 `with_options`。**已改** `_sleep`(全线 7 个资源 + 6 个测试文件),docstring 点明它是替换轮询间隔的测试缝 | - -### P2-K `input` 的接受度不一致 - -- `embeddings.create(input: Union[str, List[str]])` ✓ 接受单个 -- `chat.completions.create(messages: Union[str, List[MessageInput]])` ✓ 接受单个 -- `embeddings.create_multimodal(input: List[...])` ✗ 必须列表 - -→ `create_multimodal` 也应接受单个 item / dict。 - -### P2-L 低频、不透明的服务端参数散在签名上 - -`res_level` / `enable_fusion` / `max_video_frames` / `fps` / `output_type` / `instruct` / -`doc_tag_codes` / `doc_reference_type` / `has_thoughts` / `memory_id` / `image_list` / -`file_list` / `biz_params`。 - -`res_level` 这类缩写没有任何自解释性。签名越长,`model=` / `prompt=` 这些高频参数在 IDE 补全 -列表里越靠后、越难发现。建议:高频参数留在签名上,低频的收进 `parameters=`,并在 docstring -给出常用键清单。 - ---- - -## 四、诊断:输出字段(按严重度) - -以下基于对 `dashscope/v2/**` 全部 pydantic 模型(38 个)字段的逐条抽取。 -**字段名比类名更容易产生歧义**——类名客户只写一次,字段名要写在每一处取值代码里。 - -### P0-C `gmt_create` / `gmt_modified`:阿里内部 DB 命名泄漏到公开 SDK - -`DeploymentOutput` 的时间字段叫 `gmt_create` / `gmt_modified`。这是阿里内部数据库建表规范, -对外部客户完全不可理解(`gmt` 会被读成时区),且与同一 SDK 的其他时间字段全不一致: - -| 模型 | 时间字段 | -|---|---| -| `DeploymentOutput` | **`gmt_create` / `gmt_modified`** ⚠️ | -| `FineTuneOutput` | `create_time` / `end_time` | -| `Job` | `submitted_time` / `scheduled_time` / `end_time` | -| `FileObject` | `upload_time` | - -附带问题:`Job.submitted_time` 是从服务端 `submit_time` **主动改名**而来 -(`from_payload` 里 `submitted_time=output.get("submit_time")`)。客户对着服务端文档 / 抓包 -调试时会发现字段名对不上,这类"SDK 自作主张的改名"要么别做,要么在 docstring 标注。 - -建议全线统一为 `created_at` / `updated_at` / `submitted_at` / `finished_at`(ISO 8601 生态惯例, -OpenAI / Anthropic / Stripe 均如此),或退一步统一为 `create_time` / `modify_time` / `end_time`。 - -### P0-D `finetuned_output`:产出与消费两端叫不同名字 - -微调产出的模型版本 ID,在产出侧叫 `FineTuneOutput.finetuned_output`,在消费侧叫 -`deployments.create(version=...)`: - -```python -done = client.fine_tunes.wait(ft) -version = done.output.finetuned_output # "ft-20260827-xxxx" -dep = client.deployments.create(model="qwen-turbo", version=version) -``` - -`finetuned_output` 这个名字无法让人知道它是一个**模型版本标识**("output" 在本 SDK 里到处是 -响应信封的意思,见 P2-3)。同一个值两个名字,是客户必须靠读文档才能跨过的坎。 - -建议:`finetuned_output` → **`finetuned_model`**,`deployments.create(version=)` → -**`model_version=`**,两端对齐。同时 `DeploymentOutput.model_name` → **`model`**(与全 SDK -其余 40 处 `model` 一致;该模型里已有 `base_model`,`model_name` 反而更像"基座名",歧义大)。 - -### P1-I 同一个 `usage` 字段有 4 个类 + 4 处退化成裸 dict / Any - -| 模型 | `usage` 类型 | 字段 | -|---|---|---| -| `ChatCompletion` / `ChatCompletionChunk` | `Usage` | input_tokens / output_tokens / total_tokens | -| `EmbeddingResponse` | `EmbeddingUsage` | total_tokens | -| `RerankResponse` | `RerankUsage` | total_tokens | -| `TokenCount` | `TokenCountUsage` | input_tokens | -| `AppCompletion` | **`Dict[str, Any]`** ⚠️ | — | -| `MultiModalEmbeddingResponse` | **`Dict[str, Any]`** ⚠️ | — | -| `Job` | **`Dict[str, Any]`** ⚠️ | — | -| `FineTuneOutput` | **`Any`** ⚠️ | — | - -4 个结构几乎相同的 Usage 类,4 处完全无类型。`EmbeddingUsage{total_tokens}` 与 -`RerankUsage{total_tokens}` 是**逐字节相同的两个类**。 - -建议合并为单一 `Usage`(三个 token 字段都给默认值),全 SDK 统一使用;`AppCompletion.usage` -文档里明确是 `{"models": [...]}` 结构,应有独立的 `AppUsage{models: List[ModelUsage]}`。 - -### P1-J "总条数"两个名字:`total` vs `total_count` - -| 模型 | 字段 | -|---|---| -| `FileList` | `total_count` | -| `ModelList` | `total_count` | -| `DeploymentListOutput` | **`total`** ⚠️ | -| `FineTuneListOutput` | **`total`** ⚠️ | - -同为分页列表的总条数,两个名字。统一为 `total_count`。 - -### P1-K "序号"两个名字:`text_index` vs `index` - -`Embedding.text_index`(文本向量)vs `MultiModalEmbedding.index`(多模态向量)——**同一个 -`embeddings` 资源面下**,客户写 `emb.text_index` 和 `item.index`。文档 §3.1 与 §2.4 就分别 -示范了这两种取法。其余 `Choice.index` / `StreamChoice.index` / `RerankResult.index` 都叫 `index`。 - -统一为 `index`(`text_index` 是服务端字段名,用 alias 承接即可)。这也印证 P1-2 的结论: -`Embedding` 与 `MultiModalEmbedding` 本就该是同一个类。 - -### P1-L list 响应两种形状:扁平 vs 套 `output` - -| 扁平(字段在顶层) | 套 `output` 信封 | -|---|---| -| `ChatCompletion.choices` | `AppCompletion.output.text` | -| `EmbeddingResponse.embeddings` | `FineTune.output.job_id` | -| `RerankResponse.results` | `Deployment.output.deployed_model` | -| `ModelList.models` / `.page_no` | `FineTuneList.output.jobs` / `.output.page_no` | -| `FileList.files` / `.page_no` | `DeploymentList.output.deployments` / `.output.page_no` | -| `TokenCount.usage` | `FileUploadResponse.data["file_id"]` | -| `RlLogs.logs` | `Job.output["results"]` | - -**`FileList` 扁平而 `FineTuneList` / `DeploymentList` 套信封**——同样是 `*.list()` 的返回值, -取值深度差一层,这是最刺眼的对比。便捷属性也只补了一半:`FineTune.job_id` ✓、 -`AppCompletion.text` ✓、`TokenCount.input_tokens` ✓、`ChatCompletion.output_text` ✓, -而 `Deployment` ✗、`FineTuneList` ✗、`DeploymentList` ✗、`FileUploadResponse` ✗。 - -建议二选一并全线执行: -- **方案 A(推荐)**:全部扁平化,`Deployment.deployed_model`、`FineTuneList.jobs`、 - `FileUploadResponse` 直接返回 `FileObject`。与服务端信封的映射放在 `_parse` 里。 -- **方案 B**:保留 `output` 但给每个模型补齐便捷属性,做到"穿不穿信封都能取到"。 - -### P1-M 业务错误字段 `code` / `message` / `is_error` 只覆盖 3 / 38 个模型 - -| 有 | 无 | -|---|---| -| `ChatCompletion`(code / message / is_error) | `EmbeddingResponse`、`RerankResponse`、`TokenCount` | -| `AppCompletion`(code / message / is_error) | `ModelList`、`FileList`、`FileObject`、`RlLogs` | -| `Job`(code / message,**缺 is_error**) | `FineTune`、`Deployment`、`FineTuneList`、`DeploymentList` | - -DashScope 的特点是 HTTP 200 也可能带业务错误码。现在客户在 chat / apps 上能看 `is_error`, -在 fine_tunes / deployments 上只能靠 `output.status == "FAILED"` 自己判断,在 embeddings / -rerank 上**完全无从判断**。`Job` 有 `code`/`message` 却没有 `is_error`,与另两个模型不齐。 - -建议抽一个 `ResponseBase`(`request_id` + `code` + `message` + `is_error`)让所有顶层响应模型 -继承,`raise_on_error=False` 时行为全线一致。 - -### P2-M 其余字段级问题 - -| 字段 | 问题 | 建议 | -|---|---|---| -| `ChatCompletion.status_code` | 全 SDK 孤例,其他模型都没有 HTTP 状态码 | 移入 `ResponseBase` 或删除 | -| `ModelObject.opensource` | 非标准拼写,语义模糊(开源模型?开源许可?) | `open_source` | -| `RlLogs.logs: str` | 类名与字段名重复(`rl.logs(id).logs`),且看不出是 str 还是 list | `JobLogs.content` | -| `FineTuneEvent.output: Any` | 完全无类型 | 至少 `Dict[str, Any]`,理想是 typed event | -| `AppOutput.doc_references: List[Dict]` | RAG 引用来源是高频消费字段,却是裸 dict 列表 | typed `DocReference` | -| `AppOutput.thoughts: List[Dict]` | 同上 | typed `Thought` | -| `Job.task_id: str`(REQUIRED) | 类型上必填,但 `from_payload` 用 `or ""` 兜底,语义可空 | 改 `Optional[str]` 或真必填 | -| `Job.output` / `FileUploadResponse.data` | 裸 dict(见 P0-1) | typed | -| `MultiModalEmbeddingItem.factor` | 修饰项排在 4 个模态字段之前,读起来像第 5 个模态 | 移到最后 | -| `ImagePart.url` vs 文档示范的 `{"image": url}` | typed part 与 1.x dict part 两套表示法,文档只示范 dict 形式(而 embeddings 侧反而示范 typed) | 文档统一示范 typed,dict 作为兼容写法 | - ---- - -## 五、建议规范 - -### 5.1 动词规范(收敛到 8 个) - -| 语义 | 动词 | 阻塞 | 适用 | -|---|---|---|---| -| 同步推理 | `create` | 是 | chat / multimodal.chat / multimodal.embeddings / embeddings / rerank / **apps.completions** | -| 长任务提交 | `jobs.create` | 否 | images / videos / audio.asr / embeddings / fine_tunes / rl | -| 长任务查询·等待 | `jobs.fetch` / `jobs.wait` | 否 / 是 | 同上(现状已完全一致 ✓) | -| 一步式语法糖 | `generate` / `transcribe` | 是 | images / videos / audio.asr(= create + wait,需明写) | -| 持久资源 CRUD | `get` / `list` / `delete` / `cancel` / `scale` | 否 | fine_tunes / deployments / models / files(现状已一致 ✓) | -| 双工会话 | `recognize` / `connect` | ctx manager | audio.asr / audio.realtime | -| 纯计算·文件 | `count` / `upload` | 是 | tokenization / files | -| 流式 | `stream=True` 或 `.stream()` / `.events()` | — | 见 P2-5 | - -### 5.2 入参规范 - -| 类别 | 规范 | 说明 | -|---|---|---| -| 模型 | 一律 `model: str` | 不允许 `model_name` 之类变体 | -| 任务主键 | 对外统一 `job_id`,服务端字段 `task_id` 保留为别名 | 见 P0-B | -| HTTP 超时 | `timeout: Optional[float]` | 单次请求;所有公开方法都必须有,且**显式写出**不用 `**kwargs`。唯一例外:`rl` 的 4 个 1.x 透传方法(见 P1-F) | -| 轮询预算 | `wait` 上下文里叫 `timeout`;阻塞式一步法里 `timeout` = 总预算,提交超时叫 `submit_timeout` | 见 P0-A | -| 服务端透传 | `parameters=`(parameters 段)/ `extra_input=`(input 段) | 消灭 `extra_parameters` | -| 分页 | `page_no` / `page_size`,默认值全线一致 | 消灭 `page=` | -| 文件 URL | `file_urls: List[str]`(多文件端点)/ `file_url: str`(单文件端点) | 消灭裸 `url=`;单复数跟随端点真实入参个数,见 P1-6 | -| 必填主输入 | keyword-only(顶层 `dashscope.chat()` 例外) | 见 P2-4 | -| 低频服务端参数 | 收进 `parameters=`,docstring 列常用键 | 见 P2-L | -| 测试注入钩子 | 不进公开签名(`_` 前缀或 `with_options`) | 见 P2-J 的 `sleep` | - -### 5.3 输出字段规范 - -| 类别 | 规范 | 说明 | -|---|---|---| -| 时间戳 | `created_at` / `updated_at` / `submitted_at` / `finished_at` | 消灭 `gmt_*`、`*_time` 混用,见 P0-C | -| 用量 | 单一 `Usage` 类,全 SDK 复用 | 消灭 4 个同构 Usage 类与 4 处裸 dict,见 P1-I | -| 分页 | `page_no` / `page_size` / `total_count` | 消灭 `total`,见 P1-J | -| 序号 | 一律 `index` | 消灭 `text_index`,见 P1-K | -| 模型标识 | 一律 `model`;微调产物 `finetuned_model`;部署产物 `deployed_model` | 消灭 `model_name` / `finetuned_output` / `version`,见 P0-D | -| 任务标识 | 对外 `job_id`,`task_id` 为服务端别名 | 见 P0-B | -| 错误字段 | 所有顶层响应继承 `ResponseBase`(`request_id` / `code` / `message` / `is_error`) | 见 P1-M | -| 响应形状 | 一律扁平,服务端 `output` / `data` 信封在 `_parse` 里拆掉 | 见 P1-L 方案 A | -| 禁止 | 公开模型上不得出现裸 `Dict[str, Any]` / `Any` 字段(服务端原始 `Job.output` 除外,且必须另给 typed 视图) | 见 P0-1 | -| 禁止 | 不得出现阿里内部命名习惯(`gmt_*`、`is_deleted`、`biz_*`) | `biz_params` 见 P1-G | - ---- - -## 六、改名清单 - -**加粗为破坏性变更**(需同步改接口文档与测试)。 - -> 前提:`dashscope.__version__` 当前为 `1.27.1`,2.0 尚未发布(只存在于 `V2` 分支), -> 因此**直接改名,不保留兼容 alias、不加 deprecation 垫片**。清单中少数标注"保留 alias"的 -> 条目,指的是服务端字段名与 SDK 对外名的双向别名属性(如 `Job.job_id` ↔ `Job.task_id`), -> 而非新旧 API 的过渡兼容。 - -### 6.1 方法名 / 资源树 - -| 优先级 | 现状 | 目标 | -|---|---|---| -| P0 | `rl.run` 不阻塞,与 `generate`/`transcribe` 语义撞车 | **`rl.jobs.create`** + 新增 `rl.jobs.wait`;`rl.get/list/cancel/logs` 一并收进 **`rl.jobs`**(`get` → `fetch`) | -| P1 | `apps.call` | **`apps.completions.create`** | -| P1 | `embeddings.create_multimodal` | **`multimodal.embeddings.create`** | -| P1 | `chat.completions.create` 不识别 parts | parts 自动路由到多模态端点 | -| P1 | `rl.logs` 承载微调日志 | **`fine_tunes.logs`**(`rl.jobs.logs` 转发,不留 alias) | -| P2 | `audio.asr.recognition` | `recognize` | -| P2 | `audio.realtime.session` | `connect` | -| P2 | `audio.asr.transcription.*`(6 层) | 拍平到 `audio.asr.*`(4~5 层) | -| P2 | `rl.register_functions` / `test_functions` / `upload_data` | `rl.functions.register` / `.test` / `rl.data.upload` | -| P2 | `fine_tunes.stream_events` 与 agentstudio `sessions.events.stream` 两种形态 | 保留但在文档集中说明(见 P2-5) | - -### 6.2 返回类型 - -| 优先级 | 现状 | 目标 | -|---|---|---| -| P0 | `Job.output` / `Job.usage` 裸 dict | 补 `JobResult` + `job.results` / `job.image_urls` / `job.video_url` / `job.result_url` | -| P0 | `rl.run` → `Any`(1.x 对象) | **`rl.jobs.create` → `FineTune`**:1.x dataclass 经 `_to_job` 适配为 v2 模型 | -| P0 | `rl.test_functions` → `Any` | **不改类型**:返回值就是客户自己 rollout/reward 函数的返回值,任何标注都是假的。以 docstring 明写这一事实 | -| P1 | `rl.register_functions` / `upload_data` → `Dict[str, List[str]]` | typed model | -| P1 | `FileUploadResponse.data` 裸 dict | 改返回 **`FileObject`**(`up.file_id`) | -| P1 | `FineTuneDelete` / `FineTuneCancel` / `DeploymentDelete` | 统一 **`JobStatus`**(原 `StatusResult`) | -| P1 | `MultiModalEmbedding` / `MultiModalEmbeddingResponse` | 合并入 **`Embedding` / `EmbeddingResponse`** | -| P1 | `AppCompletion.usage` 裸 dict | typed `Usage` | -| P1 | `apps.call(stream=True)` → 裸 `Iterator` | **`AppStream`**(带 `text_stream`),与 `ChatStream` 对齐 | -| P1 | `RlLogs` | **`JobLogs`** | -| P2 | `ModelObject` | `Model` | -| P2 | `Deployment` 无便捷属性 | 补 `dep.deployed_model` / `dep.status` | -| P2 | `MultiModal*` 大小写与 `multimodal` 命名空间不一致 | 统一 `Multimodal*` | - -### 6.3 入参 - -| 优先级 | 现状 | 目标 | -|---|---|---| -| P0 | `generate`/`transcribe` 的 `timeout` = HTTP 超时、`wait_timeout` = 轮询 | `timeout` = 总预算,提交超时改 **`submit_timeout`** | -| P0 | `Job.task_id` vs `FineTune.job_id` | 互为别名属性(服务端字段名映射,非兼容垫片),文档统一推荐 `job_id` | -| P1 | `files.list(page=)` | **`page_no=`** | -| P1 | `models.list(page_size=20)` vs 其余 10 | 对齐或 docstring 说明 | -| P1 | `models.get(model_name=)` | **`model=`** | -| P1 | `rl.test_functions(instance_id, functype, input_data)` 位置参数 | keyword-only,`functype` → **`func_type`** | -| P1 | `Asr.recognition(**kwargs)` / `Realtime.session(**kwargs)` / 4 个 `jobs.fetch`/`wait(**kwargs)` | **显式写出签名**再转发 | -| P1 | `rl.register_functions` / `test_functions` / `upload_data` / `jobs.create` 无 `timeout=` | **不补**:1.x `AgenticRL` 本身不接受 timeout,补上就是静默失效的假参数。改为在 `reinforcement_learning` 模块 docstring 中显式声明例外,文档同步修正全局承诺 | -| P1 | `extra_parameters`(apps / embeddings / rerank / tokenization / chat) | **`parameters`** | -| P1 | `embeddings.jobs.create(url=)` | **`file_url=`**(单数,见 P1-6 修正) | -| P1 | `apps.call` 的 `prompt`/`messages`/`history` 无互斥校验 | **移除 `history`**(2.0 文档从未出现、1.x 形状是 `{user, bot}`、与无状态原则冲突);剩下 `prompt`/`messages` 组合语义明确并保留"至少给一个"校验,见 P1-G | -| P2 | `videos.generate(img_url=)` | **已改** `image_url=`(内部仍写 `img_url` 上线) | -| P2 | `images.generate(size="1024*1024")` 不接受 `1024x1024` | **已改**:两种都接受,`_normalize_size` 归一化为服务端写法 | -| P2 | `extra_headers` 仅 `create_multimodal` 有 | **已移除**,改为检测到 `oss://` 入参自动带 `X-DashScope-OssResourceResolve`,见 P2-J | -| P2 | `connector` 无类型注解、未文档化 | **已补** `transport.ws.Connector` 公开别名并统一注解 | -| P2 | `Jobs.wait(sleep=...)` 测试钩子入公开签名 | **已改** `_sleep` | -| P2 | `create_multimodal(input=)` 不接受单个 item | **已改**:单个 dict / `EmbeddingItem` 均可 | -| P2 | `deployments.scale(model, 4)` 位置参数 | **已改** `scale(model, capacity=4)` keyword-only | -| P2 | `res_level` 等低频不透明参数散在签名 | **不收进 `parameters=`**:`res_level` / `max_video_frames` / `fps` 都是 1.x 已文档化的独立入参,且 `res_level` 就是服务端字段名,改名只会制造新的对不上。改为在 `multimodal.embeddings.create` docstring 中说明"仅快照模型可用",其余未列名参数一律走 `parameters=` | - -### 6.4 输出字段 - -| 优先级 | 现状 | 目标 | -|---|---|---| -| P0 | `DeploymentOutput.gmt_create` / `gmt_modified` | **`created_at` / `updated_at`** | -| P0 | `FineTuneOutput.create_time` / `end_time`、`Job.submitted_time` / `scheduled_time` / `end_time`、`FileObject.upload_time` | 统一 **`created_at` / `submitted_at` / `scheduled_at` / `finished_at` / `uploaded_at`** | -| P0 | `FineTuneOutput.finetuned_output` | **`finetuned_model`** | -| P0 | `Job.output` / `Job.usage` / `AppCompletion.usage` / `FineTuneOutput.usage` 裸 dict / Any | typed(见 6.2) | -| P1 | `deployments.create(version=)` | **`model_version=`**(与 `finetuned_model` 对齐) | -| P1 | `DeploymentOutput.model_name` | **`model`** | -| P1 | `Usage` / `EmbeddingUsage` / `RerankUsage` / `TokenCountUsage` 四个同构类 | 合并为单一 **`Usage`** | -| P1 | `DeploymentListOutput.total` / `FineTuneListOutput.total` | **`total_count`** | -| P1 | `Embedding.text_index` | **`index`** | -| P1 | `code` / `message` / `is_error` 仅 3 个模型有 | **已完成**:抽出 `ResponseBase`,全部顶层响应继承。`is_error` 改由 `ResponseBase` 的 after-validator 从非空 `code` **自动推导**(与 `errors.response_error` 同一判据),修掉了"非抛异常模式下除 chat/apps/fine_tunes 外全都漏标 `is_error`"的缺陷——不再有资源能忘记设置它。载荷是列表或 `data` 下单条、`flatten_envelope` 无从合并的地方,新增 `response_envelope(body)` 单独投递信封字段。三个例外(载荷里没有 HTTP 信封):`FineTuneEvent`(SSE 帧,自带 `is_error`)、`RegisteredFunctions` / `UploadedData`(透传 1.x 返回值的记录)。规范落进 `md/dashscope-2.0-design.md` §2.6 | -| P1 | `AppCompletion.output.*` / `FineTune.output.*` / `Deployment.output.*` / `*List.output.*` / `FileUploadResponse.data` 信封 | **已完成全面扁平化**(P1-L 方案 A)。`File` / `Model` / `Deployment` / `FineTune` 四个条目类型同时改为继承 `ResponseBase`;files、models 两处解析器原先从 `data` / `output` 下的单条建模、信封到不了模型上,已用 `response_envelope()` 逐个穿进 `_parse_upload` / `_parse_list` / `_parse_file` / `_parse_delete` 与 `models.get`(含 async) | -| P2 | `ChatCompletion.status_code` 孤例 | **已移入** `ResponseBase`,全部顶层响应共用 | -| P2 | `ModelObject.opensource` | **已改** `Model.open_source`(服务端 `opensource` 保留为 validation alias) | -| P2 | `RlLogs.logs` | **已改** `JobLogs.content` | -| P2 | `FineTuneEvent.output: Any` | **已改** `Union[Dict[str, Any], str, None]`(SSE 帧不保证是 JSON) | -| P2 | `AppOutput.doc_references` / `thoughts` 裸 dict 列表 | **已改**:扁平化后为 `AppCompletion.doc_references: List[DocReference]` / `thoughts: List[Thought]` | -| P2 | `Job.task_id: str` 声明必填但实际可空 | **已对齐**:`task_id: str = ""`,与 `job_id` 属性的 `str` 返回类型一致 | -| P2 | `MultiModalEmbeddingItem.factor` 排在模态字段之前 | **已改**:类名 `EmbeddingItem`,`factor` 移到四个模态字段之后 | -| P2 | `Apps.call(biz_params=)` 内部命名 | **不改名,明确文档化**:它是百炼 flow/plugin 入参的服务端公开字段,且落在 `input` 段而非 `parameters` 段;`create` docstring 已写明两者分工,见 P1-G | - -### 6.5 异常类名 - -**这一类名字不再由 SDK 决定。** 异常类名与 HTTP 状态一律从上游生成的错误目录 `dashscope/v2/_error_registry.py`(`PUBLIC_ERRORS` 16 条)派生:每条目录项的 `error_code` 就是类名、`status_code` 就是它的状态码。所以本节记录的不是"改成了什么名",而是**哪些名从 SDK 手写变成目录派生**,以及派生之后暴露出的缺类。规范正文见 `md/dashscope-2.0-design.md` §2.7,逐条决策与验证数据见 `md/dashscope-2.0-errors-registry-plan.md`。 - -| 优先级 | 现状 | 目标 | -|---|---|---| -| P0 | 状态类的类名与 `status_code` 在 `errors.py` 里手写字面量,`_STATUS_MAP` 只有 5 项(400 / 401 / 403 / 404 / 429) | **已改**:9 个类全部从 `PUBLIC_ERRORS` 派生(`_CATALOG_STATUS` / `_STATUS_MAP`),目录改状态码 SDK 自动跟随,无需改代码 | -| P0 | 503 / 504 无类,与 500 一并落 `InternalServerError`(`ServiceUnavailable` / `GatewayTimeout` 两个线缆码也被压平) | **已补** `ServiceUnavailableError(503)` / `GatewayTimeoutError(504)`;目录里它们本就是两条独立项,压平是 SDK 侧丢信息 | -| P0 | 413 无类,落裸 `APIStatusError` | **已补** `RequestTooLargeError(413)` | -| P0 | 目录未收录的状态码走 `>= 500` 猜测,418 / 502 / 507 一律变成 `InternalServerError` | **已改**:落 `APIStatusError` 本身,**不猜**——猜出来的类会让 `except InternalServerError` 抓到并非 500 的失败,且实例 `status_code`(502)与类声明的(500)矛盾。重试判定不受影响:`transport/_retry.py` 按原始状态码决定,不看异常类 | -| P1 | 13 个 `ErrorCode` 线缆码只有 11 个进 `_CODE_MAP`;漏掉的 `InvalidParameter` 在业务层(HTTP 200)落基类 `DashScopeError`、`Model.NotExist` 完全无映射 | **已补** `_WIRE_TO_PUBLIC` crosswalk 覆盖全部 13 项,业务层与 HTTP 层现在得到同一个类 | -| P1 | `ContentFilterError(DashScopeError)` 与其余状态类不同族,`except BadRequestError` 抓不到审核失败 | **已改** `ContentFilterError(BadRequestError)`:目录把 `content_policy_violation` 命名为 `BadRequestError`,细化只能用子类化,不能引入目录外的状态 | -| P1 | `rl` 直通 1.x,失败抛裸 1.x 异常(`common/errors.py` 共 19 个类),`except DashScopeError` 抓不到 | **已补** `SdkError`:对应目录 `SDK_ERRORS`(32 条)的客户端侧错误,`code` = 目录名、`solution` = 目录随附的修复建议;挂载层 8 个直通方法(sync + async)全部经 `error_from_sdk_exception` 翻译 | -| P2 | `APIStatusError` 读作"非 2xx 响应" | **已改语义**:读作"**网关声明的失败**"——业务层(HTTP 200 带 `code`)造出的实例也是它,实例的 `status_code` 说明是哪一层 | -| P2 | `SdkError` 是否该配一个状态码 | **不给,`status_code is None`**:目录的 `SdkErrorDef` 没有 `external` 字段指回 `PublicError`,标准本身就没给 SDK 侧错误任何公开投影,凭空造一个等于给标准里没有的东西加含义 | -| P2 | `agentstudio` 直通 1.x 异常 | **未改**,仍裸奔:其挂载层直接返回 1.x client 对象,异常在 SDK 边界外抛出,需要透明代理才能拦截,挂在 M3 任务 3.12 | - -**没有引入任何 SDK 自造名。** 新增的 4 个类逐个对应目录里已有的条目:`RequestTooLargeError` / `ServiceUnavailableError` / `GatewayTimeoutError` 是 `PUBLIC_ERRORS` 的 `error_code`,`SdkError` 是 `SDK_ERRORS` 所在的客户端子图。公开异常类 11 → 15,`APIStatusError` 的直接子类 6 → 9 且互为扁平兄弟。唯一带 SDK 判断的是 crosswalk:它把百炼历史线缆码对到目录 key 上,属"选哪条目录项",不属命名——类永远由目录项给出,`test_wire_code_never_leaves_the_catalogued_class` 对每一行钉住这一点。 diff --git a/md/dashscope-2.0-realtime-sdk.md b/md/dashscope-2.0-realtime-sdk.md deleted file mode 100644 index 242dddd5..00000000 --- a/md/dashscope-2.0-realtime-sdk.md +++ /dev/null @@ -1,449 +0,0 @@ -# Realtime 实时交互 Python SDK - -`client.audio.realtime` 使用同一个同步 WebSocket 会话接口,支持 Omni 实时多模态交互、Qwen-Audio 实时音频对话和 Qwen-Livetranslate 实时翻译。本文介绍当前仓库的 DashScope 2.0 实现,不适用于旧版 `OmniRealtimeConversation` / `OmniRealtimeCallback`。 - -SDK 通过 `recv_event()` 或 `events()` 返回原始 JSON 事件,不启动后台回调线程。音频输入可使用原始 `bytes`;音频输出位于 JSON 事件的 Base64 字段中,需要调用方解码。 - -## 前期准备 - -1. 在当前仓库根目录安装源码,确保使用的是包含本文接口的版本。本文不声明线上 PyPI 包的最低版本。 - - ```bash - python -m pip install -e . - ``` - -2. 配置 `DASHSCOPE_API_KEY`;推荐同时配置 `DASHSCOPE_WORKSPACE`,并按模型的服务地域设置 `DASHSCOPE_REGION`。不应把 API Key 写入代码或提交到仓库。 -3. 示例输入 `input.pcm` 为无文件头的 16 kHz、16 bit、单声道 PCM。SDK 不会自动重采样、解码 WAV 文件或采集麦克风;实际音频格式必须与会话配置一致。 - -```python -from dashscope import Client - -with Client(timeout=30.0) as client: - print(client.config.region) -``` - -`Client` 可通过 `api_key`、`workspace`、`region`、`timeout` 显式覆盖配置。默认地域为 `cn-beijing`,默认超时为 300 秒;地域可配置不代表该地域提供所有模型。未配置 workspace 时,SDK 回退至共享网关并发出警告。 - -> Realtime 当前仅支持同步 `Client`,不提供 `AsyncClient.audio.realtime`。需要双向并发时,由应用安排一个发送线程和一个接收线程;同一时刻只能有一个接收者。 - -## 快速开始 - -### Omni / Qwen-Audio:手动提交一轮音频 - -下面的完整示例同时发送和接收。默认使用 Omni;将 `MODEL` 改为 `qwen-audio-3.1-realtime-plus`,即可使用 Qwen-Audio 的配置和结束流程。 - -```python -import base64 -import time -from concurrent.futures import ThreadPoolExecutor - -from dashscope import Client - -MODEL = "qwen-omni-turbo-realtime" -IS_QWEN_AUDIO = MODEL.startswith("qwen-audio-") - - -def receive_response(session): - with open("response.pcm", "wb") as output: - for event in session.events(): - kind = event.get("type") - if kind == "response.audio.delta": - output.write(base64.b64decode(event["delta"])) - elif kind in ( - "response.text.delta", - "response.audio_transcript.delta", - ): - print(event.get("delta", ""), end="", flush=True) - elif kind == "response.done": - print("\nusage:", event.get("response", {}).get("usage")) - return - raise RuntimeError("连接已关闭,未收到本轮 response.done") - - -with Client(timeout=30.0) as client: - with client.audio.realtime.connect(model=MODEL) as session: - settings = { - "input_audio_format": "pcm" if IS_QWEN_AUDIO else "pcm16", - "output_audio_format": "pcm" if IS_QWEN_AUDIO else "pcm16", - "enable_turn_detection": False, - } - if not IS_QWEN_AUDIO: - settings["voice"] = "Chelsie" - session.update_session(["text", "audio"], **settings) - for event in session.events(): - if event.get("type") == "session.updated": - break - else: - raise RuntimeError("连接已关闭,未收到 session.updated") - - with ThreadPoolExecutor(max_workers=1) as pool: - receiver = pool.submit(receive_response, session) - try: - with open("input.pcm", "rb") as source: - while True: - chunk = source.read(3200) - if not chunk: - break - session.append_audio(chunk) - time.sleep(0.1) - session.commit() - session.create_response() - receiver.result() - if not IS_QWEN_AUDIO: - session.end_session() - for event in session.events(): - print(event) - finally: - session.close() -``` - -3200 字节约等于上述输入格式的 100 ms 音频,示例用等待模拟实时采集;麦克风本身按实时节奏产帧时,不需要再等待。`response.pcm` 没有 WAV 文件头,播放时必须使用服务端实际输出的采样率、位宽和声道配置。 - -- **Omni / Qwen-Audio 的 Manual 模式**:`append_audio()` → `commit()` → `create_response()` → 读取至 `response.done`。 -- **Qwen-Audio 关闭连接**:读完当前响应后调用 `close()` 或退出上下文,不调用 `end_session()`。 -- **Omni 结束会话**:调用 `end_session()`,继续接收至 `session.finished`。 -- 多轮对话时,不要每轮退出会话上下文;保持连接,重复输入和响应流程。 - -### Livetranslate:边输入边翻译,并读取最终结果 - -下面以 `qwen3.5-livetranslate-flash-realtime` 为例,使用服务端默认语音检测。文件发送结束后请求结束会话,接收线程继续读取最终转写、译文、用量和 `session.finished`。 - -```python -import base64 -import time -from concurrent.futures import ThreadPoolExecutor - -from dashscope import Client - - -def receive_translation(session): - with open("translation.pcm", "wb") as output: - for event in session.events(): - kind = event.get("type") - if kind == "response.audio.delta": - output.write(base64.b64decode(event["delta"])) - elif kind in ( - "response.text.text", - "response.audio_transcript.text", - ): - print("text:", event.get("text"), "stash:", event.get("stash")) - elif kind in ( - "response.text.delta", - "response.audio_transcript.delta", - ): - print(event.get("delta", ""), end="", flush=True) - elif kind == "response.done": - print("usage:", event.get("response", {}).get("usage")) - else: - print(event) - - -with Client(timeout=30.0) as client: - with client.audio.realtime.connect( - model="qwen3.5-livetranslate-flash-realtime", - ) as session: - session.update_session( - output_modalities=["text", "audio"], - translation_params={"language": "en"}, - ) - for event in session.events(): - if event.get("type") == "session.updated": - break - else: - raise RuntimeError("连接已关闭,未收到 session.updated") - with ThreadPoolExecutor(max_workers=1) as pool: - receiver = pool.submit(receive_translation, session) - try: - with open("input.pcm", "rb") as source: - while True: - chunk = source.read(3200) - if not chunk: - break - session.append_audio(chunk) - time.sleep(0.1) - session.end_session() - receiver.result() - finally: - session.close() -``` - -此示例也可将模型替换为 `qwen3-livetranslate-flash-realtime` 或 `qwen3.8-livetranslate-flash-realtime`;只传输出模态和目标语种时,SDK 会按模型转换字段名,其他参数保留服务端默认值。 - -> Livetranslate 不使用上面的对话触发流程。Manual 模式下 `commit()` 会让服务端自动生成翻译响应,不再发送 `create_response()`;默认语音检测模式则自动提交。不要在每次 `response.done` 时关闭整个会话。 - -## 请求参数 - -### 创建会话:`client.audio.realtime.connect()` - -返回 `RealtimeSession`,此时尚未连接。进入 `with` 上下文或调用返回对象的 `connect()` 后才建立连接。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `model` | `str` | 必填,模型名称;没有默认模型。 | -| `ws_url` | `str` 或 `None` | 默认 `None`,根据 workspace 和 region 选择 Realtime 网关。显式设置时覆盖默认地址。 | -| `timeout` | `float` 或 `None` | 秒;默认继承 `Client` 配置。显式值必须大于 0,作用于连接和 socket 操作,不是会话总时长。 | -| `headers` | `Mapping[str, str]` 或 `None` | 额外握手头;按不区分大小写的名称覆盖默认头,包括鉴权头。不要在日志中输出 API Key。 | -| `connector` | `Callable[[str, dict], Any]` 或 `None` | 自定义连接函数,参数为 URL 和 headers;返回支持 `send`、`recv`、`settimeout`、`close` 的对象。默认使用 `websocket-client`。 | - -如果 `ws_url` 已包含 `model` 查询参数,该参数决定实际服务端模型和 SDK 配置映射;`session.model` 仍保留构造时的 `model` 参数。建议两处保持一致。 - -Realtime 使用独立的 `/api-ws/v1/realtime` 路径,不受 `DASHSCOPE_WEBSOCKET_BASE_URL` 的 ASR/TTS inference 地址覆盖影响;需要覆盖时传 `ws_url`。`Client.base_url` 是 HTTP 地址,也不替代 `ws_url`。 - -自定义 `connector` 不接收第三个超时参数,建立连接阶段的超时由连接函数负责;SDK 在连接返回后设置 socket 超时。其 `close()` 应能解除正在阻塞的接收操作。 - -### 更新会话:`session.update_session()` - -发送 `session.update`,返回 `None`,不等待 `session.updated`。应用应确认配置成功后再发送依赖这些配置的音频。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `output_modalities` | `list[str]` | Omni 必填;Qwen-Audio/Live 可省略。常用 `['text', 'audio']`,取值组合须由模型支持。Live 3.8 映射为 `output_modalities`,其他模型映射为 `modalities`。 | -| `voice` | `str` 或 `None` | 音色。Omni 未传时发送 `None`;新模型未传时不发送,使用服务端默认值。Live 3.8 写入 `audio.output.voice`。 | -| `input_audio_format` | `str` | Omni 默认 `pcm16`;新模型不补默认值。Qwen-Audio 的 PCM 使用 `pcm`,而非 `pcm16`。Live 3.8 映射为 `audio.input.format.type`。 | -| `output_audio_format` | `str` | Omni 默认 `pcm16`;新模型不补默认值。Live 3/3.5 的输出 PCM 使用 `pcm`;Live 3.8 写入 `audio.output.format.type`。 | -| `enable_input_audio_transcription` | `bool` | Omni 默认 `True`。Live 3/3.5 显式 `True` 选择 `qwen3-asr-flash-realtime`,`False` 发送 `{'model': None}`。Live 3.8 ASR 始终开启,显式关闭会抛出 `ValueError`。Qwen-Audio 示例不主动设置此项。 | -| `input_audio_transcription_model` | `str` 或 `None` | 指定转写模型;Omni 默认 `None`。Live 3/3.5 显式 `None` 关闭 ASR,Live 3.8 禁止这一关闭配置。 | -| `enable_turn_detection` | `bool` | Omni 默认 `True`;新模型未传不更改。显式 `False` 发送语音检测字段为 `None`,用于模型支持的 Manual 模式。 | -| `turn_detection_type` | `str` | Omni 默认 `server_vad`。新模型只有显式设置才发送;显式开启检测且未给类型时,Live 3.8 选择 `speaker_detection`,其他模型选择 `server_vad`。 | -| `prefix_padding_ms` | `int` | 检测起点前保留的音频时长,毫秒;Omni 默认 300,新模型未传不发送,支持情况按模型协议。 | -| `turn_detection_threshold` | `float` | 检测阈值;Omni 默认 0.2。新模型未传不发送。Live 3.8 的 `speaker_detection` 阈值由协议固定为 0.5,不应使用 Omni 默认值替代。 | -| `turn_detection_silence_duration_ms` | `int` | 判定结束的静音时长,毫秒;Omni 默认 800,新模型未传不发送。 | -| `turn_detection_param` | `dict` 或 `None` | 额外语音检测参数,字典同名字段覆盖上述字段。Omni 的 `None` 表示不追加;新模型显式 `None` 表示关闭检测,未传则不更改。 | -| `translation_params` | `dict` 或 `None` | 翻译配置,写入 `translation`,如 `{'language': 'en'}`。Live 3/3.5 可按协议设置 `corpus.phrases` 热词映射。新模型空字典保持为空,不补 `language: None`。 | -| `transcription_params` | `dict` 或 `None` | 输入转写配置;`language` 等字段进入 `input_audio_transcription`,`corpus_text` 转为 `corpus.text`,`input_audio_format`/`sample_rate` 提升为音频配置。详见下方规则。 | -| `input_audio_config` | `dict` 或 `None` | 嵌套输入格式,如 `{'type': 'pcm', 'sample_rate': 16000}`,写入 `audio.input.format`。优先用于支持新结构的模型。 | -| `output_audio_config` | `dict` 或 `None` | 嵌套输出格式,如 `{'type': 'pcm', 'sample_rate': 24000}`,写入 `audio.output.format`。 | -| `input_video_representation_compact` | `str` 或 `None` | 写入 `video.input.representation_compact`;是否支持取决于模型,并非所有模型都接收图像。 | -| `**kwargs` | `Any` | 额外 session 字段,如 `instructions`、`tools`、`sample_rate`、`audio`。字段按协议透传,同名原始字段优先;SDK 不保证所有模型支持所有扩展参数。 | - -### 默认值、增量更新和覆盖规则 - -**Omni** 保留完整默认配置行为:每次调用 `update_session()` 都需要输出模态,并重新构建默认音色、格式、ASR、VAD 等字段,不是只更新显式参数。只修改一个原始字段时可使用 `send_event()`: - -```python -session.send_event({ - "type": "session.update", - "session": {"instructions": "请简洁回答。"}, -}) -``` - -**Qwen-Audio、Live 3/3.5/3.8** 只发送显式参数,没有传入的字段不发送,也不会从上一次调用缓存回填: - -```python -session.update_session(instructions="请简洁回答。") -``` - -对应的 session 内容仅为 `{'instructions': '请简洁回答。'}`。此示例适用于支持 `instructions` 的模型;透传能力不代表模型能力。 - -- 新模型区分“未传”和“显式 `None`”;保留显式的 `False`、`0`、空字符串、空字典和空列表。保留这些值不意味着服务端一定接受,字段的合法性仍以模型协议为准。 -- Omni 的 `transcription_params={}` 会补输入 `pcm` / 16000,并过滤该字典的 `None` 值;新模型不补这两个默认值,也不统一过滤 `None`。 -- Live 3/3.5 的显式 ASR 关闭开关优先于转写模型和 `transcription_params`。原始 `input_audio_transcription` kwargs 则可整体覆盖便捷参数生成的字段。 -- Omni 任一不为 `None` 的嵌套格式配置(包括 `{}`)会抑制旧式顶层音频格式;Qwen-Audio/Live 的显式嵌套格式参数也走嵌套序列化,不能据此推断旧模型支持它。 -- Live 3.8 将旧式 `voice`、`input_audio_format`、`output_audio_format`、`sample_rate`、`turn_detection` 转为嵌套字段,不混发对应的旧式顶层字段。嵌套字段覆盖同名便捷字段,未冲突的格式字段会合并;显式 `None` 可整体覆盖子配置。 -- `**kwargs` 的原始 `audio` 会先替换由 `input_audio_config` / `output_audio_config` 生成的 `audio`。不建议同时用两种入口配置同一个子对象;Live 3.8 随后会合并仍存在的顶层便捷字段。 -- Live 3.8 禁止关闭 ASR,并拒绝 `same_language_skip_options`。这是 `update_session()` 的检查;`send_event()` 不执行该检查,原始发送也不代表服务端会接受非法配置。 - -Live 3.8 的配置示例(在已打开的会话中执行): - -```python -session.update_session( - output_modalities=["text", "audio"], - voice="Tina", - input_audio_config={"type": "pcm", "sample_rate": 16000}, - output_audio_config={"type": "pcm", "sample_rate": 24000}, - turn_detection_param={"type": "speaker_detection", "threshold": 0.5}, - translation_params={"language": "en"}, -) -``` - -生成的 session 内容如下;`event_id` 由 SDK 自动补充: - -```json -{ - "output_modalities": ["text", "audio"], - "translation": {"language": "en"}, - "audio": { - "input": { - "format": {"type": "pcm", "sample_rate": 16000}, - "turn_detection": {"type": "speaker_detection", "threshold": 0.5} - }, - "output": { - "format": {"type": "pcm", "sample_rate": 24000}, - "voice": "Tina" - } - } -} -``` - -## 关键接口 - -### 连接与事件收发 - -| 方法 | 参数 | 返回值 | 描述 | -| --- | --- | --- | --- | -| `session.connect()` | 无 | 当前 `RealtimeSession` | 建立连接,不自动读取 `session.created`;重复打开会抛出 `RuntimeError`。上下文入口会自动调用,勿先 connect 再进入同一对象的 with。 | -| `send_event(event)` | `event: dict` | `None` | 发送 JSON 事件;缺少 `event_id` 时生成 `event_` 加 UUID。显式 ID 保留,不修改调用方字典。 | -| `recv_event()` | 无 | `dict` 或 `None` | 阻塞接收一条事件。正常关闭后返回 `None`;活动响应或结束确认未完成时断线会抛异常。 | -| `events()` | 无 | `Iterator[dict]` | 连续读取。`response.done` 不自动终止迭代;收到 `session.finished` 后产出该事件一次并结束。 | -| `close()` | 无 | `None` | 幂等关闭本地连接,不发送 `session.finish`、不等待残留结果。重新 `connect()` 会建立新 socket 并清空会话统计。 | - -### 音频与图像输入 - -| 方法 | 参数 | 返回值 | 描述 | -| --- | --- | --- | --- | -| `append_audio(audio_b64)` | `str` 或 `bytes` | `None` | 发送 `input_audio_buffer.append`;字符串视为已编码的 Base64,bytes 自动 Base64 编码。不是发送文本转语音输入。 | -| `append_video(video_b64)` | `str` 或 `bytes` | `None` | 发送单帧 `input_image_buffer.append`;不是视频文件上传,也不从视频自动抽帧。 | -| `commit()` | 无 | `None` | 发送 `input_audio_buffer.commit`,不等待提交确认,也不隐式调用 `create_response()`。 | -| `clear_appended_audio()` | 无 | `None` | 发送 `input_audio_buffer.clear`,清除服务端未提交的音频缓冲;不是取消已在生成的响应。 | - -适用模型的图像输入可以在已追加音频后执行: - -```python -from pathlib import Path - -session.append_video(Path("frame.jpg").read_bytes()) -``` - -Livetranslate 的图像使用 JPG/JPEG,编码前不超过 500 KB、帧率不超过 2 fps,且必须先至少追加一次音频;模型版本是否支持以协议为准。Qwen-Audio 的所列客户端协议没有图像事件,不要默认套用。SDK 不进行图像转换、大小检查或自动限帧。 - -### 对话和响应控制(Omni / Qwen-Audio) - -| 方法 | 参数 | 返回值 | 描述 | -| --- | --- | --- | --- | -| `create_item(item, *, previous_item_id=None)` | `item: dict`;前置条目 ID 可选 | `None` | 发送 `conversation.item.create`;可创建文本消息或函数调用结果。指定 `previous_item_id` 时传给服务端决定插入位置。 | -| `retrieve_item(item_id)` | `item_id: str` | `None` | 发送 `conversation.item.retrieve`,从后续事件读取结果。 | -| `delete_item(item_id)` | `item_id: str` | `None` | 发送 `conversation.item.delete`。 | -| `create_response(modalities, instructions, **kwargs)` | 两个参数均可省略;类型分别为 `list[str]`、`str`,均可显式 `None` | `None` | 发送 `response.create`。Omni 无参时发送 `{'instructions': None}`,Qwen-Audio 无参时发送空 response 对象;其他字段按 kwargs 透传。 | -| `cancel_response()` | 无 | `None` | 发送 `response.cancel`;继续接收服务端取消结果和 `response.done`,不关闭会话。 | - -在已打开的 Omni 会话中,文本输入示例为: - -```python -session.create_item({ - "type": "message", - "role": "user", - "content": [{"type": "input_text", "text": "请简单介绍一下你自己。"}], -}) -session.create_response(modalities=["text", "audio"]) -``` - -工具结果通过 `create_item({'type': 'function_call_output', 'call_id': ..., 'output': ...})` 回填,再按模型协议创建响应。参数中的 ID 和 output 需使用真实函数调用结果。Livetranslate 的翻译流程不使用上述客户端对话和响应控制事件。 - -### 结束会话:`end_session()` - -| 模型 | 行为 | -| --- | --- | -| Omni | 发送 `session.finish`,立即返回;继续读取至 `session.finished`。 | -| Live 3/3.5/3.8 | 发送 `session.finish`;继续读取剩余输入转写、翻译音频、用量和最终确认。收到 `session.finished` 时 SDK 自动关闭连接。 | -| Qwen-Audio | 抛出 `NotImplementedError`,不发送事件、不关闭连接、不改变会话状态。应读完或取消当前响应,再 `close()`。 | - -请求结束后不能再发送音频、修改配置或创建响应;连接仍打开时重复 `end_session()` 不重复发送。若只收到 `response.done` 就断线,但尚未收到请求的 `session.finished`,SDK 仍将其视为未完整结束。 - -## 模型差异与调用链路 - -| 场景 | Omni | Qwen-Audio | Live 3/3.5 | Live 3.8 | -| --- | --- | --- | --- | --- | -| 配置方式 | helper 每次补 Omni 默认值 | 仅发送显式字段 | 仅发送显式字段 | 仅发送显式字段并映射嵌套音频配置 | -| 输出模态协议字段 | `modalities` | `modalities` | `modalities` | `output_modalities` | -| 输入转写 | 按会话配置 | 不注入 Omni 转写配置 | 默认开启,可显式关闭 | 始终开启,不可关闭 | -| Manual 提交后的响应 | `commit` 后 `create_response` | `commit` 后 `create_response` | `commit` 后自动生成 | 按模型支持的检测模式运行,不照搬 Omni 响应控制 | -| 主要输出文本事件 | `.delta` | `.delta` | `.text`,含 `text` / `stash` | `.delta` | -| 结束整个会话 | finish + finished | 读完当前响应后 close | finish + finished | finish + finished | - -```text -Omni / Qwen-Audio Manual -connect → update_session → session.updated - → append_audio(发送与接收并发)→ commit → create_response - ← response.created / 文本与音频 / response.done - → 下一轮输入,或按模型结束连接 - -Livetranslate -connect → update_session → session.updated - → append_audio(语音检测自动提交,或支持的 Manual 模式 commit) - ← 输入转写 / response.created / 译文与音频 / response.done - → end_session - ← 剩余结果 / session.finished → 自动关闭 -``` - -Qwen-Audio 支持 `server_vad`、`smart_turn` 和 Manual 等模式,不能套用 Omni 的所有检测参数;其音色仅首次更新有效,输入格式和检测设置有首次音频输入之前的限制。SDK 不维护服务端完整的 IDLE/生成/打断状态机,应按模型协议安排配置时机。 - -## 返回事件与会话属性 - -### 事件内容 - -| 事件 | 主要内容 | 处理方式 | -| --- | --- | --- | -| `session.created` / `session.updated` | `session` | 确认会话和配置,SDK 更新 `session_id`。 | -| `input_audio_buffer.speech_started` / `.speech_stopped` | 音频起止毫秒、条目 ID;部分版本提供 `speaker_id` | 原样保留,按 `item_id` 关联,不换算成 SDK 首包延迟。 | -| `conversation.item.input_audio_transcription.*` | 输入转写、语言等模型字段 | 原样读取;不计入译文/回复首文本延迟。 | -| `response.created` | `response.id` | 开始一轮响应,重置该轮首文本/首音频延迟。 | -| `response.text.delta` / `response.audio_transcript.delta` | `delta` | 按响应和条目分组,依次累加文本。 | -| `response.text.text` / `response.audio_transcript.text` | `text`、`stash` | Live 3/3.5 的文本片段及临时文本。`stash` 可能修正,不应作为确定文本永久累加;以最终 done 事件校准。 | -| `response.audio.delta` | Base64 `delta` | 解码后写文件或播放;没有单独的 bytes 迭代器。 | -| `response.text.done` / `response.audio_transcript.done` | 最终 `text` / `transcript` | 用于确认最终文本,具体结构依模型协议。 | -| `response.done` | `response`,可能含 `usage`、状态及输出条目 | 保存每轮用量和完成状态,不能仅凭事件类型认定生成成功。 | -| `session.finished` | 会话最终确认 | 结束迭代并自动关闭。 | -| 其他事件 | 函数参数、环境音转写、自定义扩展等 | 不进行字段白名单过滤,原样返回。 | - -Realtime 不提供独立 `usage` 属性或完整事件历史缓存。每轮用量从 `event['response'].get('usage')` 获取;原始字段可以包含 `input_tokens`、`output_tokens`、`total_tokens` 及细分类别,以该模型实际返回为准。 - -### 属性 - -| 属性 | 类型 | 说明 | -| --- | --- | --- | -| `model` | `str` | 构造时的模型参数。 | -| `url` | `str` | 实际连接 URL,包括模型查询参数。 | -| `session_id` | `str` 或 `None` | 从已读取的 session.created / updated 中更新。 | -| `last_message` | `dict` 或 `None` | 最近读取的 JSON 事件,包括服务端 error;后续事件会覆盖它,不是历史。 | -| `last_response_id` | `str` 或 `None` | 最近一次 response.created 中的响应 ID。 | -| `last_response_create_time` | `float` 或 `None` | 收到 response.created 时的本地单调时钟毫秒值,不是 Unix 时间戳。 | -| `last_first_text_delay` | `float` 或 `None` | 从读取 response.created 到读取该轮首条输出文本事件的毫秒数;支持 `.text` 和 `.delta`。 | -| `last_first_audio_delay` | `float` 或 `None` | 从读取 response.created 到读取该轮首条 response.audio.delta 的毫秒数。 | - -统计在应用消费事件时更新,不是服务端耗时,也不是从发送音频起算。相同响应的重复首包、已结束响应的迟到包和显式不同 response_id 的包不会覆盖该轮首包延迟。只发送不读取时,属性不会由后台自动更新。 - -## 异常处理 - -| 场景 | 表现与处理 | -| --- | --- | -| 服务端 `error` 事件 | 转为 `dashscope.v2.errors` 中的业务异常,如 `BadRequestError`、`RateLimitError`;原始事件在异常 `body` 和 `last_message` 中,不作为普通事件 yield。 | -| `invalid_request_error` 且未请求结束 | SDK 保持连接;修正参数后可继续发送。若通过 `events()` 遇到异常,该迭代器已经终止,需要创建新迭代器或调用 `recv_event()`。 | -| 其他服务端错误,或结束期间的错误 | 关闭连接并抛异常。不要继续向旧会话发送。 | -| 活动响应缺少 `response.done`,或已请求结束但缺少 `session.finished` 时断线 | 抛出 `APIConnectionError`,不把截断输出当作完整结果。 | -| socket 超时、网络重置、发送失败 | 关闭连接并传播底层异常,包括 `websocket.WebSocketTimeoutException`、`OSError` 等;不保证全部转换为 SDK 连接异常。 | -| 非法媒体类型或非法 JSON | 类型/解析异常;媒体只支持字符串或 bytes,不自动读取路径。 | -| 未连接、重复连接、结束后发送 | `RuntimeError`。 | -| Live 3.8 的禁止配置 | `ValueError`,在发送前拒绝,不破坏当前连接。 | -| Qwen-Audio 调用 `end_session()` | `NotImplementedError`;改用读取完成后关闭连接。 | - -WebSocket 不使用 HTTP 自动重试来重放音频或响应请求,`Client.max_retries` 不提供断线续传。应用需要自行决定是否重建会话,以及如何处理已发送但未确认的输入。 - -## 常见问题 - -### 为什么 `response.done` 后 `events()` 还在等待? - -它只代表当前响应结束,会话可以继续多轮交互。单轮读取可以在该事件后退出循环;结束整个会话需按对应模型执行 finish 或 close 流程。`break` 不会自动关闭会话。 - -### 能否同时在两个线程调用 `events()`? - -不应这样做,事件会被两个消费者分流。只保留一个读取者;它可以将事件分发给播放、字幕和统计逻辑。发送与接收可以并行。 - -### 是否有 `start()` / `stop()`? - -Realtime 的生命周期接口为 `connect()` / `end_session()` / `close()`,不是 ASR/TTS 的 `start()` / `stop()`。`cancel_response()` 取消单轮响应,与关闭整个会话不同。 - -### 同一连接可以复用吗? - -Omni/Qwen-Audio 的同一会话可完成多轮对话,Live 也可在会话中连续翻译多段音频。`close()` 后重新 `connect()` 是新连接、新会话,不是恢复旧上下文。这是协议设计而非 SDK 限制:realtime 协议没有 task_id,一条连接就是一个会话,会话结束服务端即关闭连接,因此 `WsConnection` 多任务复用对它不适用(ASR/TTS/翻译/TingWu 等 run-task 族可以,见[连接复用指南](dashscope-2.0-connection-reuse-sdk.md))。要压首开延迟,建议在用户开始说话前提前 `connect()` 预热。仅关闭 `Client` 不替代实时会话的关闭;重连前应等待旧的收发线程退出。 - -## 相关文档 - -- [ASR Python SDK](dashscope-2.0-asr-sdk.md) -- [TTS Python SDK](dashscope-2.0-tts-sdk.md) -- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) -- [2.0 接口示例总览](dashscope-2.0-interfaces-examples.md) -- [Omni Python SDK(本文结构参考,接口为旧版)](https://help.aliyun.com/zh/model-studio/omni-realtime-python-sdk) -- [Qwen-Audio 客户端事件](https://help.aliyun.com/zh/model-studio/fun-audiochat-client-events) -- [Qwen-Audio 服务端事件](https://help.aliyun.com/zh/model-studio/qwen-audio-realtime-server-events) -- [Livetranslate 客户端事件](https://help.aliyun.com/zh/model-studio/live-translator-client-events) -- [Livetranslate 服务端事件](https://help.aliyun.com/zh/model-studio/live-translator-server-events) diff --git a/md/dashscope-2.0-translation-sdk.md b/md/dashscope-2.0-translation-sdk.md deleted file mode 100644 index e70da935..00000000 --- a/md/dashscope-2.0-translation-sdk.md +++ /dev/null @@ -1,160 +0,0 @@ -# 实时语音翻译(Gummy)Python SDK - -通过 `client.audio.translation`,您可以在一条 WebSocket 双工会话上边发送音频边获取识别与翻译结果(对应 1.x `TranslationRecognizerRealtime` / `TranslationRecognizerChat`,模型如 `gummy-realtime-v1`、`gummy-chat-v1`)。本文对应当前仓库的 SDK 实现;模型能力、音频限制及服务端字段以相应模型协议为准。 - -更新的 qwen3-livetranslate 实时翻译模型(如 `qwen3.5-livetranslate-flash-realtime`)走 Realtime 协议,请使用 [`audio.realtime.connect`](dashscope-2.0-realtime-sdk.md) 并配置 `update_session(translation_params=...)`,不在本资源面。 - -| 场景 | 同步客户端 `Client` | 异步客户端 `AsyncClient` | -| --- | --- | --- | -| 实时语音翻译 | `audio.translation.recognize()` | 不支持 | -| 本地文件翻译 | `audio.translation.recognize_file()` | 不支持 | - -## 前期准备 - -1. 准备 API Key,并通过环境变量 `DASHSCOPE_API_KEY` 配置,避免将密钥写入代码。 -2. 建议通过 `DASHSCOPE_WORKSPACE` 配置工作空间,并确保 API Key、工作空间和模型所在地域匹配。地域默认是 `cn-beijing`,可通过 `DASHSCOPE_REGION` 或 `Client(region=...)` 设置。 -3. 在本仓库根目录执行源码安装:`python -m pip install -e .`。 -4. 实时示例需要当前目录下的 `input.pcm`:有限长度、无文件头、16 kHz、16 bit、单声道的原始 PCM 音频。 - -## 快速开始 - -### 实时语音翻译 - -以下代码可保存为 `translation_example.py`,在音频文件所在目录运行 `python translation_example.py`。发送和接收分别运行在两个线程中,逐块读取文件、逐条处理双语结果。 - -```python -import time -from concurrent.futures import ThreadPoolExecutor, as_completed - -from dashscope import Client - - -def send_audio(session, audio_path): - with open(audio_path, "rb") as audio: - while True: - frame = audio.read(3200) - if not frame: - break - session.send_audio_frame(frame) - time.sleep(0.1) - session.stop() - - -def receive_results(session): - for result in session.results(): - transcription = result.get_transcription() - if transcription and transcription.get("text"): - label = "最终" if result.is_sentence_end() else "中间" - print(label, "识别:", transcription["text"], flush=True) - for translation in result.get_translations(): - label = "最终" if translation.get("sentence_end") else "中间" - print(label, f"翻译({translation['lang']}):", translation["text"], flush=True) - - -def main(audio_path): - with Client() as client: - session = client.audio.translation.recognize( - model="gummy-realtime-v1", - format="pcm", - sample_rate=16000, - source_language="zh", - translation_enabled=True, - translation_target_languages=["en"], - timeout=30.0, - ) - with ThreadPoolExecutor(max_workers=2) as executor: - try: - session.start() - futures = [ - executor.submit(receive_results, session), - executor.submit(send_audio, session, audio_path), - ] - for future in as_completed(futures): - future.result() - finally: - session.close() - print("request_id:", session.request_id) - print("usage:", session.usage) - - -if __name__ == "__main__": - main("input.pcm") -``` - -对应 1.x `TranslationRecognizerRealtime.start()` + `send_audio_frame()` + `stop()` 的回调流程:v2 用主动迭代 `session.results()` 替代 `TranslationRecognizerCallback.on_event`,每一项就是 1.x 回调收到的 `(transcription_result, translation_result, usage)` 组合。 - -### 本地文件翻译 - -对应 1.x `TranslationRecognizerRealtime.call(file)`:文件自动按 12800 字节分块、单会话跑到底,逐项产出类型化结果(1.x `TranslationRecognizerResultPack` 的逐项形态)。 - -```python -from dashscope import Client - -with Client() as client: - for result in client.audio.translation.recognize_file( - "input.pcm", - model="gummy-realtime-v1", - format="pcm", - sample_rate=16000, - source_language="zh", - translation_enabled=True, - translation_target_languages=["en"], - ): - transcription = result.get_transcription() - if transcription and result.is_sentence_end(): - print("识别:", transcription["text"]) - for translation in result.get_translations(): - if translation.get("sentence_end"): - print("翻译:", translation["text"]) -``` - -### gummy-chat 单句模型 - -`gummy-chat-v1` 与实时模型共用同一协议与入口,直接 `recognize(model="gummy-chat-v1", ...)` 即可。1.x `TranslationRecognizerChat` 与实时版的差别仅在客户端行为(收到首个句结束后停止发送音频);v2 不单独建模,需要该行为时在 `results()` 里判断 `is_sentence_end()` 后自行停止发送。 - -## 接口说明 - -`Translation` 的推荐调用顺序与 `Recognition` 相同:创建对象 → `start()` → 并发发送音频和读取结果 → `stop()` → 读到 `task-finished` → `close()`。`with client.audio.translation.recognize(...) as session:` 自动执行 `start()` / `close()`。 - -| 接口 | 说明 | -| --- | --- | -| `translation.recognize(...)` | 创建 `TranslationSession`。除下表的翻译参数外,与 `asr.recognize` 相同(`format`/`sample_rate`/`parameters`/`input`/`phrase_id`/`ws_url`/`connector`/`timeout`/`headers`/`api_key`/`workspace`)。 | -| `translation.recognize_file(path, ...)` | 本地文件一次性翻译,产出 `Iterator[TranslationResult]`;路径校验在调用时即生效。 | -| `session.send_audio_frame(data)` | 二进制音频帧,通常 `bytes`。 | -| `session.stop()` | 结束输入,不等待最终结果、不关闭连接。 | -| `session.events()` | 原始事件字典流(含 `task-finished`)。 | -| `session.results()` | 类型化双语结果流,过滤心跳与 `task-finished`。 | -| `session.close()` | 关闭底层连接,可重复调用。 | - -翻译参数(缺省时不发送,由服务端默认值接管:识别开、翻译关、源语言自动): - -| 参数 | 说明 | -| --- | --- | -| `source_language` | 源语言代码(如 `"zh"`、`"en"`),缺省为自动检测。 | -| `transcription_enabled` | 是否输出源语言识别结果。 | -| `translation_enabled` | 是否启用翻译;启用时需给 `translation_target_languages`。 | -| `translation_target_languages` | 目标语言列表(当前仅支持一个目标语言),如 `["en"]`。 | -| `max_end_silence` | VAD 断句静音阈值(毫秒,200–6000)。 | - -`TranslationResult` 是单个结果事件的类型化视图: - -| 接口 | 说明 | -| --- | --- | -| `get_transcription()` | 源语言句子字典(`text`/`begin_time`/`end_time`/`words`/...),该帧无识别结果时为 `None`。 | -| `get_translations()` | 各目标语言句子字典列表(`lang`/`text`/`sentence_end`/...),无翻译结果时为空列表。 | -| `is_sentence_end()` | 识别或任一翻译到达句尾时为 `True`。 | -| `get_usage()` | 该帧携带的 `usage`,无则 `None`。 | -| `request_id` / `usage` / `output` | 原始字段属性;`output` 保留完整服务端输出。 | - -## 连接复用 - -`recognize(..., connection=conn)` 把翻译任务跑在共享的 -`WsConnection`(`client.ws_connection()` 创建)上:顺序任务免再次握手, -纯 JSON 事件并发无限制;二进制上行每条连接单 owner(一路活跃翻译/识别)。 -详见 [连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 - -## 相关文档 - -- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) -- [ASR Python SDK](dashscope-2.0-asr-sdk.md) -- [Realtime Python SDK](dashscope-2.0-realtime-sdk.md)(qwen3-livetranslate 协议族) diff --git a/md/dashscope-2.0-tts-sdk.md b/md/dashscope-2.0-tts-sdk.md deleted file mode 100644 index 19b670af..00000000 --- a/md/dashscope-2.0-tts-sdk.md +++ /dev/null @@ -1,373 +0,0 @@ -# DashScope 2.0 Python SDK:语音合成(TTS) - -通过 `client.audio.tts` 将文本转换为语音,支持一次性合成、流式音频输出及双向流式交互。本文对应当前仓库实现,仅支持同步 `Client`;`AsyncClient.audio.tts` 尚未实现。 - -当前入口使用 CosyVoice 的 `SpeechSynthesizer` WebSocket 协议;不能假设仅更换模型名就能调用采用不同协议的所有 TTS 模型。 - -## 前期准备 - -1. 准备已开通目标模型权限的 API Key,确认模型与音色在所选工作空间及地域可用。 -2. 在本仓库根目录安装源码版本,不以未经验证的已发布版本号作为前提: - - ```bash - python -m pip install -e . - ``` - -3. 在运行环境中安全配置以下环境变量,勿将密钥写入代码或提交到版本库。以下示例均通过 `Client()` 读取配置。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `DASHSCOPE_API_KEY` | `str` | API Key,用于服务鉴权。 | -| `DASHSCOPE_WORKSPACE` | `str` | 工作空间 ID,建议配置,用于工作空间网关路由及请求头。 | - -可通过 `Client(region=...)` 指定与模型服务一致的地域。本文示例调用真实服务并可能产生费用,请在完成配置后按需运行。 - -## 快速开始 - -### 1. 一次性合成 - -`synthesize()` 接收完整文本,等待音频全部返回后将各块拼接为 `bytes`。适合较短文本;结果整体保存在内存中。 - -```python -from dashscope import Client - -with Client() as client: - audio = client.audio.tts.synthesize( - "你好,欢迎使用语音合成服务。", - model="cosyvoice-v1", - voice="longxiaochun", - format="mp3", - sample_rate=22050, - ) - with open("output.mp3", "wb") as output: - output.write(audio) -``` - -### 2. 流式接收音频 - -`stream()` 返回惰性的音频迭代器,首次迭代时才建立连接。内部一次发送全部文本,然后调用 `stop()`;流式的是输出,不是文本输入。 - -```python -from dashscope import Client - -with Client() as client: - chunks = client.audio.tts.stream( - "你好,这段音频将按块写入文件。", - format="mp3", - ) - try: - with open("stream-output.mp3", "wb") as output: - for chunk in chunks: - output.write(chunk) - finally: - chunks.close() -``` - -业务需要提前 `break` 或处理音频时发生异常,也应保留 `finally` 中的 `chunks.close()`,释放内部 WebSocket 会话。提前退出的文件仅包含已消费的音频,不代表完整合成结果。音频块不保证可独立解码,也不保证与句子一一对应。 - -### 3. 双向流式:边发送文本,边接收音频与事件 - -使用 `connect()` 创建会话,在一个线程发送文本、另一个线程读取结果。示例输入为有限元组;接入 LLM 时可替换为逐步产出字符串片段的可迭代对象,无需先收集完整文本。 - -```python -import json -from concurrent.futures import FIRST_EXCEPTION, ThreadPoolExecutor, wait - -from dashscope import Client - - -def send_parts(session, parts): - for text in parts: - session.send_text(text) - session.stop() - - -def receive_results(session): - with open("duplex-output.mp3", "wb") as output: - for frame in session.events(): - if isinstance(frame, bytes): - output.write(frame) - continue - print("JSON 事件:", json.dumps(frame, ensure_ascii=False)) - payload = frame.get("payload") or {} - sentence = (payload.get("output") or {}).get("sentence") - if isinstance(sentence, dict): - for word in sentence.get("words") or (): - print( - "词时间戳:", word.get("text"), - word.get("begin_time"), word.get("end_time"), - ) - if payload.get("usage") is not None: - print("本次 usage:", payload["usage"]) - - -parts = ("你好,", "这是双向流式语音合成。", "文本与音频可以并行传输。") -with Client() as client: - session = client.audio.tts.connect(format="mp3", timeout=30) - with ThreadPoolExecutor(max_workers=2) as executor: - try: - session.start() - print("task_id:", session.session.task_id) - print("启动事件:", session.last_response) - sender = executor.submit(send_parts, session, parts) - receiver = executor.submit(receive_results, session) - done, _ = wait((sender, receiver), return_when=FIRST_EXCEPTION) - for future in done: - future.result() - print("最近 usage:", session.usage) - print("首包延迟(毫秒):", session.first_package_delay) - finally: - session.close() -``` - -`wait(..., FIRST_EXCEPTION)` 在任一任务失败或两者全部成功时返回,`Future.result()` 将线程内异常传播到调用方。`finally` 位于线程池上下文内部,先关闭会话以解除 WebSocket 阻塞,再由线程池等待线程退出。实际 LLM 上游若可能阻塞,还需由业务为上游设置超时或取消机制。 - -发送线程的 `stop()` 只结束输入,接收线程仍持续读取至 `task-finished`。不要在发送结束后立即关闭会话,否则可能丢失尾部音频、时间戳及用量。 - -## 请求参数 - -`connect()`、`synthesize()`、`stream()` 共享以下连接与合成参数;除 `text` 外,均以关键字传入。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `text` | `str` | `synthesize(text, ...)`、`stream(text, ...)` 必填的完整文本。`connect()` 不接收此参数,改用会话的 `send_text(text)` 分段输入。 | -| `model` | `str` | 默认 `"cosyvoice-v1"`。模型名称。 | -| `voice` | `str` | 默认 `"longxiaochun"`。音色须与模型匹配。 | -| `format` | `str` | 默认 `"mp3"`。输出音频编码格式;支持范围由模型服务决定。 | -| `sample_rate` | `int` | 默认 `22050`,单位 Hz。可用采样率及其与格式的组合由服务端校验。 | -| `parameters` | `Optional[dict]` | 默认 `None`。额外模型参数,原样合并进任务参数;同名键覆盖显式参数,详见下文。 | -| `ws_url` | `Optional[str]` | 默认 `None`。自定义 WebSocket 地址;未指定时优先使用全局 WebSocket 地址配置,否则按工作空间及地域生成默认地址。不是 HTTP `base_url`。 | -| `connector` | `Optional[Callable]` | 默认 `None`,使用 `websocket-client`。自定义连接函数签名为 `connector(url, headers)`,返回 WebSocket 兼容对象。 | -| `timeout` | `Optional[float]` | 单位秒,须大于 0。默认 `None` 表示沿用 `client.config.timeout`,并非无限等待。默认连接器将其用于建连,并在连接后设置 socket 超时;不是整个合成任务的总时限。 | -| `headers` | `Optional[Mapping[str, str]]` | 默认 `None`。附加 WebSocket 握手请求头。SDK 根据客户端配置填入鉴权和工作空间请求头;配置存在时会覆盖同名 `Authorization`、`X-DashScope-WorkSpace`。 | - -合成参数先由 `voice`、`format`、`sample_rate` 和固定默认值 `text_type="PlainText"` 组成,再合并 `parameters`。 - -> **覆盖提示**:`parameters` 中的 `voice`、`format`、`sample_rate`、`text_type` 会覆盖前面的值。例如 `format="mp3"` 与 `parameters={"format": "pcm"}` 同时传入,实际请求为 PCM。请同步调整文件后缀和播放器配置。模型特有参数、取值范围及组合约束由服务端校验,SDK 不保证所有模型支持同一组参数。 - -自定义 `connector` 的返回对象需支持 `send()`、`recv()`、`close()`、`settimeout()`;SDK 不向连接函数传递第三个超时参数,因此自定义建连过程的超时由连接函数负责,SDK 在返回后设置 socket 超时。其 `close()` 应能解除阻塞的接收操作。 - -## 关键接口 - -### 入口方法 - -| 接口 | 返回值 | 说明 | -| --- | --- | --- | -| `client.audio.tts.connect(...)` | `SynthesisSession` | 仅创建惰性会话对象,不立即联网。进入上下文或调用 `start()` 才打开连接。 | -| `client.audio.tts.synthesize(text, ...)` | `bytes` | 完整发送文本、结束输入、读取全部音频并拼接;内部自动管理连接。 | -| `client.audio.tts.stream(text, ...)` | `Iterator[bytes]` | 惰性建立会话,一次发送文本并结束输入,逐块输出音频;正常耗尽时自动关闭连接,提前退出需显式关闭迭代器。 | - -### 会话方法 - -除 `send_text(text)` 外,下列会话方法均无参数。 - -| 接口 | 返回值 | 说明 | -| --- | --- | --- | -| `start()` | `SynthesisSession` | 打开 WebSocket,发送 `run-task` 并等待 `task-started`,返回自身。启动事件记录于 `last_response`,不再由结果迭代器输出。 | -| `send_text(text)` | `None` | 发送一个 `str` 文本片段;非字符串抛出 `TypeError`。只能在启动成功、输入尚未结束时调用。 | -| `flush()` | `None` | 请求合成已缓冲文本,发送 `continue-task`,输入为 `{"flush": true}`。不结束文本输入,之后仍可 `send_text()`。 | -| `stop()` | `None` | 发送 `finish-task` 结束输入,不等待合成完成;之后须继续消费结果。结束输入使用此方法,没有 `finish()` 别名。 | -| `cancel()` | `None` | 发送带 `{"directive": "cancel"}` 的 `finish-task`。终态到来前,即使已 `stop()` 也可取消;取消不是暂停,不能恢复输入。 | -| `events()` | `Iterator[Union[dict, bytes]]` | 按到达顺序输出原始 JSON 事件与音频字节,包括 `task-finished` 后结束;遇 `task-failed` 抛异常而非输出失败事件。 | -| `audio_chunks()` | `Iterator[bytes]` | 仅输出音频,但内部仍读取 JSON 并更新 `last_response`、`usage` 等属性,直至任务终态。 | -| `close()` | `None` | 幂等关闭连接,不等待结果,也不替代正常的 `stop()` 加结果消费。关闭后的对象不可重启。 | - -生命周期与并发约束: - -- 启动方式二选一:`with client.audio.tts.connect(...) as session:` 自动启动并关闭;或显式 `start()` 配合 `try/finally: close()`。已 `start()` 的对象不要再次进入 `with session:`,也不要重复 `start()`。 -- `stop()` 或 `cancel()` 后均不可再调用 `send_text()`、`flush()`。连接未关闭时,重复 `stop()` 或 `cancel()` 不会重复发送相同结束动作;终态后也不再发送。关闭后调用这些操作会报错。 -- 同一会话允许一个发送者和一个接收者并行。接收迭代器由锁保护:`events()` 与 `audio_chunks()` 不能同时消费,也不能同时运行两个同类接收迭代器,否则抛出 `RuntimeError`。 -- 若手动持有会话的接收迭代器,提前停止时调用该迭代器的 `close()` 释放读取锁;这不等于关闭会话。若不再继续读取,还需关闭会话。需要音频和元数据时,优先统一使用一个 `events()`。 -- 每次新任务建立独立 WebSocket,无连接池或任务间复用。任务结束或会话关闭后,使用新的 `connect()` 对象发起下一任务。仅关闭 `Client` 不能替代显式会话的资源清理。 - -## 返回事件与属性 - -### 原始事件 - -JSON 事件保留服务端结构,不转换为专用响应模型;音频帧直接返回 `bytes`。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `header.event` | `str` | 事件名称,例如 `task-started`、`result-generated`、`task-finished`、`task-failed`。 | -| `header.task_id` | `str` | 服务端事件中的任务标识。 | -| `header.request_id` | `str`(若返回) | 服务端请求标识,以原始事件为准。 | -| `payload.output` | `dict`(若返回) | 合成结果元数据,具体字段取决于模型及事件。 | -| `payload.output.sentence.words` | `list[dict]`(若返回) | 词级信息;`begin_time`、`end_time` 为毫秒时间戳。并非所有模型或事件均返回。 | -| `payload.usage` | `dict`(若返回) | 服务端原始用量信息。CosyVoice 通常使用 `characters`;仅在服务端实际返回时才有 `tokens` 等字段。 | -| 音频帧 | `bytes` | 按请求格式编码的音频分片,不是 JSON,也不是 Base64 字符串。 | - -以下仅为字段位置示意,不是真实响应,不承诺所有模型均返回这些字段或相同结构: - -```json -{ - "header": {"event": "result-generated", "task_id": "示意任务标识"}, - "payload": { - "output": { - "sentence": { - "words": [{"text": "你好", "begin_time": 0, "end_time": 420}] - } - }, - "usage": {"characters": 2} - } -} -``` - -如需逐句或逐词时间戳,请在消费 `events()` 时处理每个 JSON 事件,不要等任务完成后只查询 `last_response`。 - -### 会话属性 - -| 属性 | 类型 | 说明 | -| --- | --- | --- | -| `last_response` | `Optional[dict]` | 最近一次 JSON 事件,初始为 `None`;启动后是 `task-started`,正常完整消费后通常为 `task-finished`,会覆盖之前的结果事件。字节帧不覆盖它。失败事件也会被保留。 | -| `usage` | `Optional[dict]` | 最近一次非 `null` 的 `payload.usage` 原始值,初始为 `None`。后续事件缺失用量或返回 `null` 时保留上次值;不是 SDK 累加值,不保证包含 token 计数。 | -| `first_package_delay` | `Optional[float]` | 单位毫秒,从首次 `send_text()` 调用时的计时点到接收迭代器首次消费音频字节。尚未消费音频时为 `None`;包括调用方延迟读取造成的等待,不是纯服务端耗时。 | -| `session` | `WsSession` | 底层会话,仅在已启动且未关闭时可访问,否则抛出 `RuntimeError`。通常无需直接调用底层收发方法。 | -| `session.session.task_id` | `str` | 在示例的 `session` 变量上获取底层任务 ID。建议在会话关闭前保存用于排查。 | - -`SynthesisSession` 没有直接的 `request_id` 便捷属性。请求标识可查看 JSON 原始 `header`;异常也提供 `request_id`。属性只随实际接收而更新,不会由后台线程自动刷新;若只调用 `stop()` 而不消费结果,元数据不会自动完整。 - -## 异常处理 - -服务端业务失败与底层连接异常需分别处理;不要假设所有 WebSocket 错误都会被统一转换成 HTTP 风格异常。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `InvalidApiKey` | `AuthenticationError` | 服务端 `task-failed` 的鉴权错误映射,检查 API Key。 | -| `InvalidParameter` | `BadRequestError` | 检查模型、音色、格式、采样率和模型专属参数。 | -| `Throttling` | `RateLimitError` | 检查并发、速率及配额。由业务决定是否新建任务重试。 | -| 其他业务错误码 | `DashScopeError` 或对应子类 | 按 SDK 业务码映射;未知码保留在基础异常中。 | -| 提前 EOF | `APIConnectionError` | 底层接收返回空字符串或 `None`,且尚未收到 `task-finished`,视为连接异常而非成功结束。 | -| socket 超时或断连 | 原生异常 | 如 `websocket.WebSocketTimeoutException`、`websocket.WebSocketConnectionClosedException`、`OSError`,可能原样传播;并不保证变为 `APITimeoutError`。 | -| 非法操作或输入 | `RuntimeError` / `TypeError` / `ValueError` | 包括重复启动、并发读取、结束输入后继续发送、非字符串文本及非正数超时。 | - -收到 `task-failed` 时,SDK 根据 `header.error_code`、`header.error_message` 抛出异常,异常对象的 `body` 保留完整失败事件,`code`、`message`、`request_id` 可用于定位问题。会话的 `last_response` 同样保留失败事件。异常的 `status_code` 不能作为 WebSocket 任务成功的判断依据。 - -启动、收发失败时 SDK 会清理连接;调用方仍应使用上下文管理或 `finally` 释放资源。协议头异常可能抛出 `WsEventError`,非法 JSON 可能抛出原生 JSON 解析异常。 - -WebSocket 路径不提供自动重连、重放或断点续传;客户端 HTTP 重试配置不会自动重放 TTS 任务。确需重试时,应新建会话并由业务确定文本边界,避免重复播放、重复合成或重复计费。 - -## 常见问题 - -**如何衔接 LLM 的流式输出?** - -使用 `connect()`,将 LLM 字符串片段逐次传给 `send_text()`,同时在另一个线程持续消费一个结果迭代器。`stream(text)` 仍要求完整字符串,不接受文本迭代器作为双向输入。 - -**`flush()`、`stop()`、`cancel()` 和 `close()` 有何区别?** - -`flush()` 请求处理缓冲文本但保留输入;`stop()` 正常结束输入并保留输出;`cancel()` 请求取消且不可恢复,`stop()` 后终态前仍可调用;`close()` 直接释放连接。`stop()`、`cancel()` 均不等待任务结果。需要确认终态时继续读取;无需保留输出时可显式关闭。 - -**为什么拿不到时间戳、用量或首包延迟?** - -先确认已消费结果。`audio_chunks()` 不输出 JSON,但会更新会话属性;逐词信息应通过 `events()` 当场读取。模型可能不返回词时间戳或用量。`usage` 不等于固定的 token 统计,首包延迟也仅在首次消费音频后产生。 - -**为什么文件内容与扩展名不匹配?** - -检查 `parameters` 是否覆盖 `format`、`sample_rate`,并按实际编码保存或播放。SDK 只返回音频字节,不负责转码,也不会因文件名后缀改变音频格式。 - -**能否复用会话或使用异步客户端?** - -不能用同一会话启动下一任务,也不能重启已关闭对象。每个任务需新建会话和连接;当前没有 `AsyncClient.audio.tts`,异步应用需自行安排同步调用的线程及生命周期。 - -## HTTP 合成(client.audio.tts.http) - -ws 入口之外,`client.audio.tts.http` 提供纯 HTTP POST 的合成方式(不经过 WebSocket):一次性返回整段音频的临时 URL,或以 SSE 流式逐段返回音频帧。这是 1.x `HttpSpeechSynthesizer`(sambert/cosyvoice)与 qwen-tts `SpeechSynthesizer` 两个模型族的 v2 形态,共用同一入口,SDK 按模型名前缀路由: - -- `qwen*`(如 `qwen-tts`、`qwen3-tts-flash`)→ `services/aigc/multimodal-generation/generation` -- 其他(sambert/cosyvoice 等)→ `services/audio/tts/SpeechSynthesizer` - -两个端点的请求体同为 `{model, input}`、响应同为 `output.audio`,因此调用代码完全一致,只需替换 `model` 与 `voice`。 - -### 一次性合成(sambert 官方示例平移) - -对应 1.x `HttpSpeechSynthesizer.call(stream=False)`;音频以临时 URL 返回,需自行下载。 - -```python -import requests -from dashscope.v2 import Client - -client = Client() -result = client.audio.tts.http.synthesize( - "今天天气怎么样?", - model="sambert-zhichu-v1", - voice="zhichu", - format="wav", - sample_rate=16000, -) -print(result.audio_url, result.expires_at) -with open("output.wav", "wb") as f: - f.write(requests.get(result.audio_url).content) -``` - -### 流式合成(qwen-tts 官方示例平移) - -对应 1.x qwen `SpeechSynthesizer.call(stream=True)`;SSE 由 SDK 自动开启(`X-DashScope-SSE: enable` 请求头),音频帧已按 base64 解码为字节。 - -```python -from dashscope.v2 import Client - -client = Client() -with open("output.mp3", "wb") as f: - for chunk in client.audio.tts.http.stream( - "那我来给大家推荐一款T恤,这款T恤是纯棉的,穿着很舒服。", - model="qwen3-tts-flash", - voice="Cherry", - language_type="Chinese", - ): - if chunk.audio: - f.write(chunk.audio) - if chunk.is_final: - print(chunk.audio_url, chunk.usage) -``` - -cosyvoice 的流式分片额外携带 `chunk.sentence`(`sentence-*` 类型的句级元数据);qwen 族分片只有 `audio` 与末尾的整段 URL 引用。末片 `is_final`(`finish_reason == "stop"`)不保证携带音频字节,整段音频以 `audio_url` 引用为准。 - -### HTTP 请求参数 - -`http.synthesize()` 与 `http.stream()` 参数相同;除 `text` 外均以关键字传入。 - -| 参数 | 类型 | 说明 | -| --- | --- | --- | -| `text` | `str` | 必填。待合成文本。 | -| `model` | `str` | 必填。模型名称,同时决定端点路由(见上)。 | -| `voice` | `str` | 必填。音色须与模型匹配。 | -| `format` | `Optional[str]` | 默认 `None`(服务端默认)。输出音频编码格式。 | -| `sample_rate` | `Optional[int]` | 默认 `None`(服务端默认)。采样率,单位 Hz。 | -| `language_type` | `Optional[str]` | 默认 `None`。语种类型,如 `"Auto"`、`"Chinese"`、`"English"`;主要面向 qwen-tts 族,写入 `input.language_type`。 | -| `parameters` | `Optional[dict]` | 默认 `None`。请求体顶层 `parameters`,原样透传。 | -| `extra_input` | `Optional[dict]` | 默认 `None`。额外 `input` 字段(如 cosyvoice 的 `volume`/`rate`/`pitch`),原样合并;显式参数同名时覆盖它。 | -| `timeout` | `Optional[float]` | 默认 `None`,沿用客户端超时配置。 | -| `headers` | `Optional[Mapping[str, str]]` | 默认 `None`。附加请求头;`stream()` 的 SSE 头已由 SDK 设置,同名传入可覆盖。 | - -### HTTP 返回类型 - -`http.synthesize()` 返回 `HttpSynthesis`;`http.stream()` 逐片返回 `HttpSynthesisChunk`。两者都保留原始 `output` 字典、`usage` 与 `request_id`。 - -| 属性 | 类型 | 说明 | -| --- | --- | --- | -| `audio_url` | `Optional[str]` | 整段音频的临时 URL;`HttpSynthesis` 直接携带,`HttpSynthesisChunk` 仅末片携带。 | -| `audio_id` | `Optional[str]` | 音频 ID。 | -| `expires_at` | `Optional[int]` | `audio_url` 的过期时间戳。 | -| `audio` | `Optional[bytes]` | 仅 `HttpSynthesisChunk`:本片音频帧(已 base64 解码),无音频的分片为 `None`。 | -| `sentence` | `Optional[dict]` | 仅 `HttpSynthesisChunk`:cosyvoice `sentence-*` 分片的句级元数据。 | -| `finish_reason` / `is_final` | `Optional[str]` / `bool` | 仅 `HttpSynthesisChunk`:末片 `finish_reason == "stop"`,`is_final` 为其便捷判断。 | - -HTTP 路径的错误处理与其他 HTTP 资源一致:错误状态码按错误目录映射(如 `InvalidParameter` → `BadRequestError`);SSE 流中的 `event:error` 帧在迭代中途抛出同样映射的异常。ws 路径的连接类注意事项(重连、异步)不适用于此入口。 - -## 连接复用 - -`connect(..., connection=conn)` 把合成任务跑在共享的 -`WsConnection`(`client.ws_connection()` 创建)上,逐句高频合成免去反复 -握手。合成输出是二进制音频(线上不带 task_id),所以每条连接同一时刻 -只能有一路活跃合成——`start()` 会自动占用下行,第二路并发合成会直接 -报错(要并发请再开一条连接);顺序合成则天然复用。详见 -[连接复用指南](dashscope-2.0-connection-reuse-sdk.md)。 - -## 相关文档 - -- [CosyVoice 客户端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/cosyvoice-client-events) -- [CosyVoice 服务端事件](https://docs.bailian.console.aliyun.com/zh/model-studio/cosyvoice-server-events) -- [Sambert Python SDK](https://help.aliyun.com/zh/model-studio/sambert-python-sdk) -- [Qwen-TTS API](https://www.alibabacloud.com/help/zh/model-studio/qwen-tts-api) -- [连接复用指南](dashscope-2.0-connection-reuse-sdk.md) -- [DashScope 2.0 ASR SDK](dashscope-2.0-asr-sdk.md) -- [DashScope 2.0 Realtime SDK](dashscope-2.0-realtime-sdk.md) diff --git a/md/dashscope-2.0-vs-1.x-alignment-review.md b/md/dashscope-2.0-vs-1.x-alignment-review.md deleted file mode 100644 index c5e258b1..00000000 --- a/md/dashscope-2.0-vs-1.x-alignment-review.md +++ /dev/null @@ -1,452 +0,0 @@ -# DashScope SDK 2.0 对 1.x 接口/功能对齐 — 深度 Review - -> 评审范围:`dashscope/v2/`(2.0)对照 `dashscope/`(1.x: aigc / audio / embeddings / rerank / tokenizers / app / assistants / threads / finetune / files / models / utils)。 -> 方法:按 6 个功能域逐文件比对公开方法、参数、流式语义、响应字段。 -> 结论先行:**v2 是一次有意的重设计,不是 1:1 移植**。管理面与批量任务基本对齐甚至更优;推理面(chat / multimodal / image / video / audio)的多处功能回退已按 P0/P1/P2 修复(commit `c9604e2` / `d8ac331` / `b69b585`),TingWu 能力域已补 `client.tingwu`(`82e15d5`)。CodeGeneration、nlp Understanding、MultiModalDialog、Conversation 已决策 **hold,v2 先不支持**;音频侧 HTTP-TTS / omni 类型化助手 / 实时翻译 / 词表声纹管理经 2026-09 产品页核查确认存活,单独立章 **§7 语音模块专项(需立项,专门做)**。所有不同步到 v2 的能力在 **§6 整体标记**。按"能否无损迁移"衡量,**决策范围内的主干场景已可平移**。 -> -> **范围说明**:1.x 的 Assistants / Threads / Runs 子系统正在下线,**2.0 有意不包含**,不计为缺口(详见 §3 P2-0)。`dashscope` CLI 在 2.0 是**后续再加**的计划项,同样不计缺口(详见 §3 P2-7)。 - ---- - -## 0. 对齐计分卡 - -| 领域 | 对齐度 | 主要缺口 | -|---|---|---| -| files / models / fine_tunes / deployments | 🟢 对齐+ | ~~deployments 缺 `stream_events`;finetune/deployment 响应少 `creator/modifier/group`~~ ✅ `b69b585` | -| apps(百炼 Application) | 🟢 对齐+ | ~~`workflow_message`、`usage.models[]` 未建模~~ ✅ `b69b585` | -| 批量转写 `asr.transcribe` | 🟢 对齐 | — | -| chat / multimodal.chat | 🟢 对齐 | ~~本地文件上传、per-call 鉴权、命名参数、累积流、`search_info`~~ ✅ `c9604e2` / `d8ac331` | -| embeddings / rerank | 🟢 对齐 | ~~batch 缺 `cancel/list`;本地 tokenizer 缺失;multimodal 本地文件上传缺失~~ ✅ `c9604e2` / `d8ac331` / `b69b585` | -| tokenization | 🟢 对齐 | ~~`token_ids/tokens` 未类型化;无本地 tiktoken~~ ✅ `d8ac331` / `b69b585`(v2 再导出 1.x `get_tokenizer`) | -| images / videos | 🟢 对齐 | ~~缺 `cancel/list`、本地文件上传、`function` 字段~~ ✅ `c9604e2` / `d8ac331` / `b69b585`;视频 `extend_prompt` 默认值差维持 v2 语义(已标注:迁移指南 §3.3) | -| 实时音频 asr.recognize / tts / realtime | 🟡 部分 | 增量 TTS / `recognize_file` / `RecognitionResult` / callback 适配器 ✅ `b69b585`;omni 类型化助手 ✅(M2)、实时翻译 ✅(M3 `client.audio.translation`)、HTTP-TTS ✅(M4 `client.audio.tts.http`)、管理面 ✅(M5 `client.audio.vocabularies/phrases/voices/voice_design`) | -| reinforcement_learning | 🟢 对齐 | ~~base_url 不一致;缺 function-instance 日志/删除;`jobs.delete` 缺失~~ ✅ `b69b585`(按 v2 默认 endpoint,见 P2-2) | -| 认证(save_api_key 密钥文件) | 🟢 对齐 | ~~v2 不读 `~/.dashscope/api_key`~~ ✅ `b69b585`(key 文件为最低优先级) | -| **TingWu(听悟)** | 🟢 **已补** | ✅ `82e15d5`:`client.tingwu`(离线 call + realtime 会话,wire 形状与 1.x 一致) | -| **nlp Understanding / MultiModalDialog / Conversation** | ⚪ **hold** | 已决策:v2 先不支持(见 §1 P0-4),存量用户继续用 1.x | -| **CodeGeneration** | ⚪ **hold** | 已决策:v2 先不支持(见 §1 P0-3) | -| **Assistants / Threads / Runs** | ⚪ 不在范围 | 1.x 下线中,2.0 有意不包含(非缺口) | -| **`dashscope` CLI** | ⚪ 后续再加 | 2.0 计划项,非缺口(见 §3 P2-7) | - ---- - -## 1. 系统性回退(P0,迁移阻塞级) - -### P0-1 本地文件上传 / OSS 预处理全面移除 - -> **状态**:✅ 已修复(commit `c9604e2`)。v2 复用 1.x `oss_utils.check_and_upload_local`(`v2/resources/_oss.py`),chat(native)/images/videos/embeddings 均先上传本地路径再置 `X-DashScope-OssResourceResolve: enable`。 - -**现象**:1.x 在 multimodal chat、multimodal embeddings、image、video 中会把本地路径自动上传 OSS,并置请求头 `X-DashScope-OssResourceResolve: enable`;v2 只接受已有 `oss://` / HTTP(S) URL。传本地文件的 qwen-vl / 图生图 / 视频用户会直接失败或拿到不可解析的入参。 - -**1.x 参照** -- 工具:`dashscope/utils/oss_utils.py` — `check_and_upload_local:174`、`preprocess_message_element:280`、`upload_file:149`、`OssUtils.upload:34` / `get_upload_certificate:112`。 -- 调用:`dashscope/aigc/multimodal_conversation.py:151` `_preprocess_messages`(:218/:480),命中上传后 `headers["X-DashScope-OssResourceResolve"]="enable"`(:154/:419)。 -- 图像/视频:`dashscope/aigc/image_synthesis.py:287-343`、`video_synthesis.py:293-321`。 - -**v2 现状** -- `dashscope/v2/protocols/native.py:22` `_translate_part` 仅读 `part["url"]`,无上传分支。 -- `dashscope/v2/types/messages.py` 媒体 part 仅 `url`/`frames`。 -- `dashscope/v2/resources/multimodal/embeddings.py:71` `_uses_oss` 只在值已以 `oss://` 开头时置 resolve 头,**不上传**。 -- images/videos 无任何 `check_and_upload_local` 调用。 - -**改法** -1. v2 直接复用 1.x 的 `dashscope/utils/oss_utils.py`(纯工具,不引入循环依赖)。 -2. 在 `native.py:_translate_part` 增加本地路径分支:当 `part["url"]` 是 `file://` / 本地存在路径时,调用 `check_and_upload_local(model, url, api_key)` 换成 `oss://`,并标记"本次有上传"。 - - 因 `_translate_part` 是纯函数且无 api_key,需把上传提到 `build_chat_request`(native.py:59)层:在 `_translate_messages` 前对 messages 跑一遍 `preprocess_message_element`,并把 `has_upload` 透传到 `ChatRequest.headers`,命中时加 `X-DashScope-OssResourceResolve: enable`。 - - api_key 来源:`client.config.api_key`(protocol 目前拿不到 client,需在 `completions._build_request` 处把 config/api_key 传入,或让 protocol 持有一个 uploader 回调)。最小改动:在 `resources/chat/completions.py:_build_request`(completions.py:64)里先对 `messages` 做上传预处理,再交给 protocol。 -3. multimodal embeddings:把 `multimodal/embeddings.py:_contents`(:47)里每个 item 的 `image/audio/video` 值过一遍 `check_and_upload_local`,`_uses_oss` 改为"上传过或本就是 oss://"即置 resolve 头。 -4. images/videos:在 `images.py` 构造 body 前,对 `image_urls` / `extra_input` / `media` 里的本地路径调用 `check_and_upload_local`(`videos.py` 同理对 `image_url`/`media`)。 -5. 上传需要 api_key,统一从 `self._client.config.api_key` 取。 - -**验收**:本地 png/wav/mp4 路径 → 抓包确认请求体已是 `oss://` 且带 `X-DashScope-OssResourceResolve: enable`。 - ---- - -### P0-2 per-call `api_key` / `workspace` / `headers` 全部取消 - -> **状态**:✅ 已修复(commit `c9604e2`,采用方案 A)。transport 四个方法新增 `api_key=`/`workspace=` 形参并覆盖默认头;全部资源方法(含 ws 侧 audio)透传。 - -**现象**:1.x 几乎每个方法都能逐调用覆盖 `api_key=` / `workspace=`(多租户、子账号),chat 还能注入 `plugins` 头;v2 只有 client 级配置。多租户调用方与 per-call header 注入场景无法平移。 - -**1.x 参照**:`generation.py:49` `call(..., workspace, api_key, ...)`;`generation.py:186-192` 注入 `X-DashScope-Plugin`;各资源 `call` 均带 `api_key/workspace`。 - -**v2 现状** -- `dashscope/v2/client.py:53` 仅 client 级;`with_options`(client.py:152)可派生新 client(重量级)。 -- `dashscope/v2/transport/http.py`:`_build_headers:48` 只在 transport `__init__`(:114-126)用 client 级 api_key/workspace;`request:136` / `open_stream` 已支持 per-call `headers=` 与 `base_url=`,但**没有 `api_key=` / `workspace=` 形参**。 - -**改法**(二选一,推荐 A) -- **A. 传输层加 per-call 鉴权形参**:给 `http.py` 的 `request` / `open_stream`(sync+async 共 4 个)增加 `api_key: Optional[str]=None`、`workspace: Optional[str]=None`,在内部用 `_build_headers(api_key or self._api_key, workspace or self._workspace, ...)` 覆盖默认头,再经 `_with_sdk_headers` 合并(注意大小写不敏感,已实现 http.py:71)。资源方法签名透传 `api_key/workspace` 到 transport。ws 侧在 `transport/ws.py:WsSession.__init__` 同样接受覆盖。 -- **B. 仅在资源层透传 headers**:资源方法接受 `extra_headers: Mapping`,直接作为 `headers=` 传给 transport(caller 头已优先且大小写不敏感)。改动小,但把鉴权拼串责任丢给用户。 - -> 注:`plugins` 头属 B 即可覆盖;若要保留 1.x 的 `plugins=` 具名 kwarg,在 chat create 增加 `plugins` 形参并映射为 `X-DashScope-Plugin` 头。 - -**验收**:同一 client 下两次调用传不同 `api_key`,抓包 `Authorization` 不同;传 `workspace` 时 `X-DashScope-WorkSpace` 覆盖 client 默认。 - ---- - -### P0-3 CodeGeneration 缺失(需决策) - -> **状态**:⚪ **已决策:hold,v2 先不支持**(2026-09 评审拍板)。文档核查亦显示官方文档无独立页面,代码生成场景走通用 chat API。存量用户继续用 1.x;后续若后端确认仍在线且有必要,再单独立项。 - -**1.x 参照**:`dashscope/aigc/code_generation.py:52/72`(scene 化代码生成)。 -**v2 现状**:无资源。 -**改法**:确认该 API 是否仍在线。若在线,作为 `chat.completions` 的一个 scene 入口或独立 `client.code_generation` 资源补;若已下线(同 Assistants/Threads/Runs),在迁移文档标注废弃即可,不计缺口。**先与后端确认存废再动手。** - -> **文档核查(2026-09)**:官方文档主入口([DashScope API 参考](https://help.aliyun.com/zh/model-studio/qwen-api-via-dashscope))及站内检索均无 CodeGeneration 独立页面,代码生成场景走通用 chat/文本生成 API。**倾向"不再作为独立 scene 主推",仍以后端确认为准。** - -> Assistants / Threads / Runs 已确认 1.x 下线中、2.0 有意不包含,见 §3 P2-0,不再作为 P0 阻塞项。 - ---- - -### P0-4 整个能力域在 v2 无对应(需决策) - -**现象**:除 CodeGeneration(P0-3)外,以下 1.x 能力域在 v2 整体无对应资源,迁移这些能力的用户没有落点: - -- **nlp Understanding**:`dashscope/nlp/understanding.py`(百炼知识库问答/理解)。 -- **TingWu / TingWuRealtime**:`dashscope/multimodal/tingwu/`(听悟会议转写,含实时)。 -- **MultiModalDialog**:`dashscope/aigc/multimodal_dialog.py`(多模态对话,带历史管理)。 -- **Conversation / History**:`dashscope/conversation/`(对话状态托管)。 - -**改法**:与后端逐一确认**存废/归属**(随 1.x 下线?并入百炼 apps?独立立项?),再决定 v2 是补资源、委派 1.x 还是在迁移文档标注下线。**先确认再动手**;确认前这些域按 🔴 缺失计(见计分卡)。 - -> **状态**:✅ TingWu 已补(commit `82e15d5`)。新增 `client.tingwu`:`TingWu.call`(离线,POST `services/aigc/multimodal-generation/generation`,返回类型化 `TingWuResult`)与 `TingWu.realtime`(duplex ws 会话,`task_group=aigc`/`task=multimodal-generation`/`function=generation`,start/stop wire 形状与 1.x 一致;stop 的 `appId` 修正为真实值——1.x `tingwu_realtime.py` 因 `_Request.app_id` 从未赋值而恒发 `None`)。`WsSession.finish_task` 增加可选 `input` 载荷。**Understanding/MultiModalDialog/Conversation 已决策:hold,v2 先不支持**(2026-09 评审拍板),存量用户继续用 1.x,后续视后端归属结论再立项。 -> -> **文档核查(2026-09)**: -> - **TingWu 确认存活**:官方文档有专页且 2026-09 仍在更新([服务端 Python SDK](https://help.aliyun.com/zh/model-studio/tingwu-sdk-python) / [realtime](https://help.aliyun.com/zh/model-studio/tingwu-sdk-python-realtime)),示例直接用 1.x `dashscope.multimodal.tingwu.tingwu.TingWu`(经 `multimodal-generation/generation` 端点跑 `createTask/getTask` 离线任务)。→ 从"待确认"升级为**"确认保留,v2 缺口坐实"**,建议立项补 `client.tingwu`(可复用 v2 jobs/transport,wire 形状与 1.x 一致)。 -> - **Understanding / MultiModalDialog / Conversation 文档无独立页面**:站内检索无对应 API 文档;多轮对话改以 messages 数组教学([multi-round-conversation](https://help.aliyun.com/zh/model-studio/multi-round-conversation)),多模态交互由 omni 实时([multimodal-interaction-protocol](https://help.aliyun.com/zh/model-studio/multimodal-interaction-protocol))承接。**倾向"不再主推/已并入其他面"**,仍以后端确认为准。 - ---- - -## 2. 功能回退(P1) - -### P1-1 高频命名参数降级为无类型 `parameters=` 透传 - -> **状态**:✅ 已修复(commit `d8ac331`)。chat 9 个具名参数(top_k/tool_choice/n/logprobs/top_logprobs/thinking_budget/enable_search/search_options/parallel_tool_calls)在三个协议落地:anthropic 对无对应项显式拒绝、`top_k` 映射为 Anthropic 原生字段;images/videos/audio 具名参数同步补齐,具名优先于 `parameters=` 兜底。 - -**现象**:`top_k / tool_choice / n / logprobs / top_logprobs / thinking_budget / enable_search / search_options / parallel_tool_calls`(chat),`seed/style/duration/resolution/ratio/prompt_extend/watermark`(image/video),`volume/rate/pitch/vocabulary_id/language_hints`(audio)等在 v2 不再是具名 kwarg,可发现性与类型校验下降;且 **openai 协议 strict 模式会拒绝未知 key**(`dashscope/v2/protocols/openai.py:137-143`)。 - -**v2 现状**:`native.py:59` `build_chat_request` 形参只到 `tools`;其余靠 `extra_parameters`(native.py:95)。chat create 形参见 `completions.py:383`。 - -**改法** -1. 在 `completions.py` 的 `create`(sync :383 / async :477 / 各 overload)与 `native.py:build_chat_request`(:59)、`openai.py` 适配器同步增加具名形参:`top_k / tool_choice / n / logprobs / top_logprobs / thinking_budget / enable_search / search_options / parallel_tool_calls`,在 native 的 `for key,value in (...)`(native.py:81)元组里登记。 -2. openai 协议侧:把这些映射到 OpenAI 字段名;strict 白名单(openai.py:137)相应放行。 -3. image/video:把 `seed/size/style/negative_prompt/duration/resolution` 等提升为 `images.generate` / `videos.generate` 具名 kwarg(当前在 `images.py` 走 `parameters`),保留 `parameters=` 兜底。 -4. audio:`tts.synthesize`(tts.py:45)增加 `volume/rate/pitch/language_hints` 具名;asr.recognize(asr.py:255)增加 `vocabulary_id/phrases/disfluency_removal_enabled/diarization_enabled` 等,内部按 1.x 形状拼 `resources=[{resource_id,resource_type:"asr_phrase"}]`(参照 `recognition.py:341`、`transcription.py:245`)。 - -**验收**:具名传参等价于旧的 `parameters={...}`;openai 协议不再因这些 key 报错。 - ---- - -### P1-2 chat 流式:无累积模式 + text_stream 丢 reasoning/tool_call - -> **状态**:✅ 已修复(commit `d8ac331`)。create 新增 `incremental_output`;native 侧沿用 1.x `ParamUtil.should_modify_incremental_output` 语义——常规模型 wire 保持增量、SDK 端合并为累积快照,tts/omni/qwen-deep-research 透传服务端;openai/anthropic 恒客户端合并。`ChatStream`/`AsyncChatStream` 新增 `reasoning_stream` / `tool_call_stream`。注:1.x 的 merge 实现位于 `dashscope/utils/message_utils.py`(含 n>1 分包/usage 按 index 聚合),v2 在 `completions.py` 以类型化 per-choice slot 重新实现,未逐行移植 n>1 的 finish_reason 隐藏逻辑。 - -**现象**:v2 native 恒置 `incremental_output=True`(native.py:79-80),无累积快照模式;1.x 的 incremental→cumulative 客户端 merge(generation.py:204-243)无对应。`ChatStream.text_stream` 只产出 `delta.content`,**丢弃 `reasoning_content` 与 tool_call 增量**(completions.py:114-119)。 - -**改法** -1. `text_stream`(completions.py:114)保持只出正文;新增 `reasoning_stream` 与 `tool_call_stream`(或在 chunk 上保留 delta 即可,文档说明)。`get_final_message`(completions.py:180 聚合)已含 reasoning/tool_calls,补齐 stream 侧便捷访问。 -2. 累积模式:给 chat create 增加 `incremental_output: Optional[bool]=None`,透传到 native(默认仍 True);若用户传 False,提供一个 `accumulate` 包装器在 SDK 侧把增量合并成累积快照(对齐 1.x merge 行为),避免依赖服务端。 - -**验收**:`for t in stream.text_stream` 与 1.x 一致;`incremental_output=False` 时每个 chunk 是到目前为止的全量文本。 - ---- - -### P1-3 image/video/batch-embedding 缺 `cancel` / `list` - -> **状态**:✅ 已修复(commit `d8ac331`)。`Jobs`/`AsyncJobs` 新增 `cancel`(POST /tasks/{id}/cancel)与 `list`(GET /tasks,`JobList` 分页模型),images/videos/embeddings 的 jobs 门面全部透传。 - -**现象**:1.x 任务类资源都有 cancel/list;v2 `_jobs.py` 只有 create/fetch/wait。 - -**1.x 参照**:`image_synthesis.py:544 cancel` / `:565 list`;`video_synthesis.py:564/585`;`batch_text_embedding.py:167 cancel` / `:188 list`。 - -**v2 现状**:`dashscope/v2/resources/_jobs.py:163 Jobs`(:182 create / :205 fetch / :221 wait),`AsyncJobs:269`。无 cancel/list。 - -**改法** -1. 在 `Jobs` / `AsyncJobs` 增加: - - `cancel(task_id, *, timeout=None)`:1.x 走 `PUT/POST /tasks/{task_id}/cancel`(以 `image_synthesis.cancel` 实际 method/path 为准),复用 `self._client.transport.request(..., sdk_module=self._module)`。 - - `list(*, page_no, page_size, status=None, ...)`:`GET /services/{service}/... ` 或 1.x list 对应 path,返回任务列表类型。 -2. images/videos/embeddings 的包装类(`ImageJobs`/`VideoJobs`/`EmbeddingJobs` 及 async)透传 `cancel/list`。 -3. 新增响应模型 `JobList`(pydantic),对齐 1.x 分页字段。 - -**验收**:`client.images.jobs.cancel(task_id)` / `.list()` 可用;与 1.x 返回字段一致。 - ---- - -### P1-4 chat 响应丢 `output.search_info` / `thoughts` / 原始 output 透传 - -> **状态**:✅ 已修复(commit `d8ac331`)。`ChatCompletion.search_info` 类型化(`SearchInfo.search_results[]`,extra="allow" 兜底未建模字段)。`thoughts` 确认 chat 侧无返回,不建模。 - -**现象**:`parse_completion`(`dashscope/v2/types/completions.py:122`)只保留 `choices/usage/envelope`,`output.search_info`(web 搜索引用)、`output.thoughts`、顶层 `finish_reason`/`text` 被丢弃 → `enable_search` 结果在 v2 不可观测。 - -**改法** -1. `types/completions.py` 的 `ChatCompletion` 增加 `search_info: Optional[SearchInfo]=None`(建模 `search_results[].{title,url,site_name,index}`),`parse_completion` 从 `output.search_info` 提取。 -2. 保留一个 `raw_output: Optional[Dict]=None`(或 `model_extra`)承接未建模字段,避免信息硬丢。 -3. `thoughts` 若 chat 侧确有返回,按 apps 的 `Thought`(apps.py:49)同构建模。 - -**验收**:`enable_search=True` 时 `resp.search_info.search_results` 非空。 - ---- - -### P1-5 本地 tokenizer(get_tokenizer / tiktoken)缺失 + tokenization 丢 token_ids - -> **状态**:✅ 已修复。`TokenCount.token_ids`/`tokens` 于 commit `d8ac331` 补齐;本地 tokenizer 入口于 commit `b69b585` 落地——`dashscope.v2` 直接再导出 1.x `get_tokenizer`/`list_tokenizers`(委派 `dashscope/tokenizers`,不重写 tiktoken),与包根既有导出指向同一对象。 - -**现象**:1.x `get_tokenizer(model)` 返回离线 tiktoken 对象(`encode/decode`);v2 无本地 tokenizer,`tokenization.count` 是服务端往返且只返回计数。 - -**1.x 参照**:`dashscope/tokenizers/tokenizer.py:17 get_tokenizer` / `:43 list_tokenizers`;`qwen_tokenizer.py:103 encode` / `:156 decode`;`tokenization.py:44 Tokenization.call`(返回 `output.token_ids/tokens`)。 - -**v2 现状**:`dashscope/v2/resources/tokenization.py:32 _parse` 只映射 `usage`+envelope,丢 `token_ids/tokens`;`count:70`。 - -**改法** -1. `tokenization.py:_parse`(:32)补 `token_ids`/`tokens` 字段;`TokenCount`(:24)增加 `token_ids: Optional[List[int]]`、`tokens: Optional[List[str]]`。 -2. 本地 tokenizer:决策是否在 v2 暴露 `client.tokenizers.get(model)`。若要,直接复用 1.x `dashscope/tokenizers`(同 P0-3 委派思路),不重写 tiktoken 逻辑。 - -**验收**:`count` 返回 token_ids;`get_tokenizer` 入口在 v2 可达(或文档指明用 1.x)。 - ---- - -### P1-6 `save_api_key` 落盘的 key 文件 v2 不读取 - -> **状态**:✅ 已修复(commit `b69b585`)。`resolve_config` 的 `_pick` 链在 `config.toml` 之后补 key 文件级(`DASHSCOPE_API_KEY_FILE_PATH` → `~/.dashscope/api_key`,文件缺失回退 None 不抛错);`dashscope.v2` 再导出 1.x `save_api_key`,与包根同一对象。 - -**现象**:1.x 支持 `dashscope.save_api_key(...)` 把 key 写入 `~/.dashscope/api_key`(或 `DASHSCOPE_API_KEY_FILE_PATH` 指定路径),之后免配读取(`dashscope/common/api_key.py:45 save_api_key` / `:15 get_default_api_key`)。v2 的 key 解析(`v2/_config.py:542-549 resolve_config`、`Config.api_key` :279)只看 显式参数 → `DASHSCOPE_API_KEY` 环境变量 → `config.toml`,**不读 key 文件** → 走 `save_api_key` 流程的存量用户迁到 v2 直接丢鉴权。 - -**改法** -1. `resolve_config` 的 `_pick` 链在 `config.toml` 之后补两级:`DASHSCOPE_API_KEY_FILE_PATH` 环境变量指定的文件、`~/.dashscope/api_key` 默认文件(读取对齐 1.x `get_default_api_key` 的优先级)。 -2. v2 暴露 `save_api_key(api_key, api_key_file_path=None)`(顶层 `dashscope.v2` 或 `client` 静态方法),实现直接复用/委派 1.x `common/api_key.py:45`,不重写。 - -**验收**:无显式 key、无环境变量、仅有 key 文件时 v2 Client 可初始化;`save_api_key` 入口在 v2 可达。 - ---- - -### P1-7 video `extend_prompt` 默认值与 image/video `function` 字段 - -> **状态**:✅ `function` 已修复(commit `b69b585`)。`images.generate`/`videos.generate` 及各自 jobs(sync+async)新增具名 `function: Optional[str]=None`,写入 `inputs` 且具名优先于 `extra_input`;images 两个 input 构造器(legacy + multimodal)均覆盖。`extend_prompt` 按改法 1 维持 v2 `prompt_extend=None` 语义,默认值差已在迁移指南 §3.3 标注。 - -**现象** -1. 1.x video 的 `extend_prompt: bool = True`(`video_synthesis.py:63` / `:199`,`prompt_extend` 的 deprecated 别名,**默认开**,写入 `inputs["extend_prompt"]` :213);v2 `videos.generate` 的 `prompt_extend` 默认 `None` 且只进 `parameters`。显式行为不同,逐参数对齐时需注意默认值差。 -2. 1.x image/video 有 `function: str = None` 形参(`image_synthesis.py:46` → `inputs["function"]` :312-313;`video_synthesis.py` → `inputs["function"]` :219);v2 `images.generate`/`videos.generate` 无此具名参数(P1-1 只恢复了 parameters 侧字段,`function` 属 inputs 侧)。 - -**改法** -1. video:维持 v2 `prompt_extend=None`(不擅自引入 1.x 的默认 True),但在迁移文档标注默认值差;`extend_prompt` 别名不再恢复(1.x 已 deprecated)。 -2. image/video:`generate`/`jobs.create`(sync+async)增加具名 `function: Optional[str]=None`,写入 `inputs`(非 `parameters`),具名优先于 `extra_input`。 - -**验收**:`images.generate(function="...")` / `videos.generate(function="...")` 与 1.x inputs 形状一致。 - ---- - -## 3. 一致性 / 补全(P2) - -### P2-0 Assistants / Threads / Runs — 有意不在 2.0 范围(非缺口) - -> **状态**:✅ 已在迁移指南 §5 显式声明下线(`md/dashscope-2.0-migration-guide.md`)。 - -1.x 的 Assistant 运行时(`dashscope/assistants/`、`dashscope/threads/`、`threads/runs/`)正在下线,**2.0 有意不实现**。这是产品决策,不计为对齐缺口,也不需要委派/重写。 - -**唯一动作**:在迁移文档显式声明"Assistants/Threads/Runs 不在 2.0 范围,随 1.x 下线",避免用户误以为是遗漏。CodeGeneration(P0-3)若同属下线范畴,一并标注。 - -### P2-1 deployments 缺 `stream_events` / 通用 `put` - -> **状态**:✅ 已修复(commit `b69b585`)。SSE 实现抽出为共享模块 `v2/resources/_sse.py`(fine_tunes 同步重构复用);`Deployments.stream_events`/`AsyncDeployments.stream_events`(GET `/deployments/{id}/stream`)落地,raise/yield 契约与 fine_tunes 一致;新增类型化 `DeploymentEvent`。通用 `put` 未补(1.x `PutMixin` 无实际调用方,`scale` 已覆盖唯一在用场景)。 - -1.x `dashscope/finetune/deployments.py:20` 继承 `StreamEventMixin`/`PutMixin`;v2 `deployments.py:119` 只有固定 `scale`(:188)。 -**改法**:把 `fine_tunes.py` 已有的 `stream_events`(fine_tunes.py:626 SSE 实现)抽出复用到 `Deployments.stream_events(deployed_model)`;path 用 `/deployments/{id}/stream`(对齐 1.x)。 - -### P2-2 RL base_url 不一致 + 缺 function-instance 操作 + 缺 `jobs.delete` - -> **状态**:✅ 已修复(commit `b69b585`)。**决策:RL 走 v2 默认 endpoint**——1.x 把每个 RL 动词强转 `DASHSCOPE_HTTP_BASE_URL` 属 1.x 配置问题,不在 v2 复刻(评审会上拍板,替代原"host 对齐"改法)。已补:`RlJobs.delete`、三个 function-instance 方法(`query_function_instance_logs`/`query_all_function_instance_logs`/`delete_function_instance`,委派 1.x `AgenticRL`)、`test_functions` 的 `pull_logs/log_page_size/start_time/end_time`。 - -1.x `dashscope/finetune/agentic_rl.py:353-438` 的 `cancel/list/get/delete/logs` 强制 `DASHSCOPE_HTTP_BASE_URL`;v2 `reinforcement_learning.py:199 RlJobs`(:239-292)委派 `FineTunes` 走 v2 标准 endpoint → **host 不等价时行为偏**。另缺 `query_function_instance_logs`/`query_all_function_instance_logs`/`delete_function_instance`(agentic_rl.py:513/560/607)与 `jobs.delete`(1.x agentic_rl.py:400)。 -**改法**:确认 RL 管理面正确 host;若确需 `DASHSCOPE_HTTP_BASE_URL`,在 `RlJobs` 的 fetch/cancel/list/logs 调用里传 `base_url=`(transport 已支持 per-request base_url,http.py:92)。补 `RlJobs.delete` 与三个 function-instance 方法(委派 1.x `AgenticRL` 对应方法)。`test_functions`(rl.py:331)补 `pull_logs/log_page_size/start_time/end_time`。 - -### P2-3 任务等待语义翻转(需文档化 + 可选对齐) - -> **状态**:✅ `raise_on_error=False` 分支已修复(commit `b69b585`)。`Jobs.wait`/`AsyncJobs.wait` 在 compat 模式下对 `FAILED/CANCELED` 终态直接返回 Job 而非抛错,且不再空轮询。`DEFAULT_WAIT_TIMEOUT`(86400 vs 1.x 无限)维持 v2 语义,已在迁移指南 §3.2 标注。 - -v2 `Jobs.wait` 默认 `timeout=86400`(1.x 为无限 `-1`),`FAILED/CANCELED` 在 `raise_on_error` 下抛异常而非返回(_jobs.py:73/250/260)。 -**改法**:`DEFAULT_WAIT_TIMEOUT` 是否改回"无限"由产品决策;至少在迁移文档显式说明,并保证 `raise_on_error=False` 时返回终态 Job 而非抛错(确认 `_parse`/`_check_canceled` 在 False 分支返回对象)。 - -### P2-4 响应字段补全 - -> **状态**:✅ 已修复(commit `b69b585`)。`AppCompletion.workflow_message`(嵌套 `WorkflowMessage`)与 `AppUsage.models[]`(`AppModelUsage`)落地;`FineTune` 补 `user_identity/modifier/creator/group`;`Deployment` 补 `modifier/creator`。 - -- apps:`AppCompletion`(apps.py:80)补 `workflow_message`(1.x `application_response.py:120-166`)与 `usage.models[]` 每模型 token 明细类型化(当前仅 `extra="allow"`)。 -- fine_tunes:`FineTune`(fine_tunes.py:86)补 `user_identity/modifier/creator/group`(1.x `customize_types.py:47-50`)。 -- deployments:`Deployment`(deployments.py:45)补 `modifier/creator`(customize_types.py:143-144)。 - -### P2-5 AgentStudio 委派不走 v2 栈(说明性) -`client.py:115 agentstudio` 委派 1.x `agentstudio.Client`,只转发 `api_key/workspace/region`,**不转发 base_url/timeout/http_client**,且不经过 v2 transport/header/error 栈(刚统一的 `x-dashscope-sdk-*` 头对其不生效)。属有意复用,但需文档说明;若要统一标识,可在委派构造时把 v2 的 sdk 头/超时透传给 1.x agentstudio 的 transport。 - -### P2-6 音频实时面补全(较大,单列) - -> **状态**:✅ 前三项已修复(commit `b69b585`),立项项不变。已落地:callback 适配层 `v2/resources/audio/_callbacks.py:run_with_callback`(鸭子类型同时服务 asr/tts 两种 1.x callback 形状);增量 TTS `client.audio.tts.connect`→`SynthesisSession.send_text/stop/audio_chunks` + `enable_ssml`(联动 `text_type` PlainText/SSML),`tts.stream` 重构复用之;本地文件 ASR `asr.recognize_file(path)`(12800 字节分块,对齐 1.x);类型化结果 `RecognitionResult.get_sentence/is_sentence_end/get_usage` + `Recognition.results()`(过滤心跳与非 result 事件)。 - -- **callback 适配层**:1.x push 回调(`RecognitionCallback` recognition.py:140、`ResultCallback` tts_v2:69/90、omni/qwen-tts callback)无适配。建议提供 `events()`→callback 的可选适配器,降低迁移成本。 -- **增量 TTS**:`tts.stream`(tts.py:73)当前一次性 `continue_task({"text":text})`+`finish_task`(tts.py:105),且硬编码 `text_type:"PlainText"`、无 SSML。补 `streaming_call(text)`/`streaming_complete()` 等价能力与 `enable_ssml`(1.x tts_v2:627/657/885)。 -- **本地文件 ASR**:1.x `Recognition.call(file)` 自动分块(recognition.py:444);v2 需手动 `send_audio_frame`。补 `recognize_file(path)` 便捷方法。 -- **缺失能力(需立项)**:实时翻译 `TranslationRecognizerRealtime`(translation_recognizer.py:288)+`TranslationRecognizerChat`(:807)、omni 音视频类型化助手(`update_session/append_audio/append_video/create_response/commit`,omni_realtime.py:399)。当前 v2 仅 `realtime.connect` 裸事件可达。(注:omni 类型化助手 M2、实时翻译 M3、HTTP 合成 M4、词表/热词/声纹管理 M5、连接复用 M6 均已落地,见 §7.4。) -- **类型化结果**:`asr.recognize` 的 `events()` 产裸 dict;补 `RecognitionResult.get_sentence/is_sentence_end/get_usage`(recognition.py:99-125)等价类型。 - -### P2-7 `dashscope` CLI 暂未覆盖 v2(计划项) - -> **状态**:✅ 已在迁移指南 §5 提示"v2 无 CLI,命令行场景继续用 1.x CLI"(`md/dashscope-2.0-migration-guide.md`);v2 版 CLI 仍后续再加。 - -v2 暂不提供 CLI;现有 `dashscope` 命令行(acli,copy 自 agentic CLI,保持自包含、不依赖 dashscope 包)仍走 1.x。**v2 版 CLI 后续再加**,届时再评估入口命名与共用层。当前动作仅:在迁移文档提示"v2 无 CLI,命令行场景继续用 1.x CLI"。 - ---- - -## 4. v2 的净增益(对齐时勿回退) - -- 全资源 **async 孪生**(1.x 多为 sync 或零散 aio)。 -- **pydantic 强类型** + `extra="allow"` 兜底,响应可点出字段。 -- 统一 **`raise_on_error`** 与 `is_error/code/message/status_code` 信封(`types/common.py:24-49`)。 -- files `NotFoundError` 精化(files.py:159)、models `provider` 过滤(models.py:99)、deployments MU 字段 `deploy_spec/billing_method`(deployments.py:111)、`AppStream.text_stream`(apps.py:260)。 -- multimodal embedding 新增 `video` item;`with_options`(client.py:152)派生配置。 -- 统一 SDK 标识头 `x-dashscope-sdk-client`/`session-id`(本次已修)。 - ---- - -## 5. 落地优先级建议 - -| 优先级 | 项 | 估时倾向 | -|---|---|---| -| P0 | 本地文件上传(P0-1)、per-call 鉴权(P0-2) | ✅ `c9604e2` | -| P0 | CodeGeneration(P0-3) | ⚪ 已决策:hold,v2 先不支持 | -| P0 | 能力域(P0-4):TingWu ✅ `82e15d5`;Understanding/MultiModalDialog/Conversation | ⚪ 已决策:hold,v2 先不支持 | -| P1 | 命名参数(P1-1)、流式累积+reasoning(P1-2)、cancel/list(P1-3)、search_info(P1-4)、token_ids(P1-5)、key 文件(P1-6)、extend_prompt/function(P1-7) | ✅ `d8ac331` / `b69b585` | -| P2 | deployments.stream_events、RL endpoint/补全、等待语义、响应字段补全、音频实时面 | ✅ `b69b585`(音频立项项除外) | -| P2 | v2 CLI(P2-7) | 后续再加;✅ 迁移指南 §5 已提示 | -| 专项 | 语音模块专项(§7):ws 协议补齐与 omni 助手(`8eaaf81`)、M3 翻译门面、M4 HTTP 合成、M5 管理面、M6 连接复用(`WsConnection` + 全模型 HTTP keep-alive settings) | ✅ 全部落地;修复计划见 §7.4 | -| — | Assistants/Threads/Runs(P2-0) | 不在 2.0 范围;✅ 迁移指南 §5 已标注下线 | - -> 建议每个 P0/P1 项各开一个 issue,附本文对应小节的 file:line 作为实施清单起点;改动均落在 `dashscope/v2/`,复用 1.x 工具(oss_utils / tokenizers)而非重写,以保持物理隔离同时避免功能回退。 - ---- - -## 6. 不同步到 v2 的能力(整体标记) - -以下能力 **v2 不提供**,存量用户继续用 1.x;均已拍板或落地,不再视为缺口。迁移口径统一见 [`dashscope-2.0-migration-guide.md`](dashscope-2.0-migration-guide.md) §5。 - -| 能力 | 1.x 位置 | 处置 | 产品页现状(2026-09 核查) | -|---|---|---|---| -| Assistants / Threads / Runs | `dashscope/assistants/`、`threads/` | ⚪ 1.x 下线中,2.0 有意不包含(§3 P2-0) | — | -| CodeGeneration | `aigc/code_generation.py` | ⚪ hold,v2 先不支持(§1 P0-3) | 文档无独立页面 | -| nlp Understanding | `nlp/understanding.py` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面 | -| MultiModalDialog | `aigc/multimodal_dialog.py` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面 | -| Conversation / History | `conversation/` | ⚪ hold,v2 先不支持(§1 P0-4) | 文档无独立页面,多轮对话以 messages 数组教学 | -| HTTP-TTS(sambert 等) | `audio/http_tts/http_speech_synthesizer.py:114 HttpSpeechSynthesizer` | ✅ M4 已落地(`client.audio.tts.http`,见 §7.4) | [专页存活](https://help.aliyun.com/zh/model-studio/sambert-python-sdk),但示例已统一走 ws 版 `SpeechSynthesizer`,HTTP 形态不再主推 | -| qwen-tts HTTP | `audio/qwen_tts/speech_synthesizer.py:11` | ✅ M4 已落地(同上,按模型名前缀路由) | 存活且有独立页([模型信息](https://help.aliyun.com/zh/model-studio/model-qwen-tts) / [Qwen-TTS API](https://www.alibabacloud.com/help/zh/model-studio/qwen-tts-api)) | -| 实时翻译(gummy) | `audio/asr/translation_recognizer.py:288/807` | ✅ M3 已落地(`client.audio.translation`,见 §7.4) | [专页存活](https://help.aliyun.com/zh/model-studio/real-time-python-sdk),示例用 `TranslationRecognizerRealtime` | -| 定制词表 / 热词 | `audio/asr/vocabulary.py:31 VocabularyService`、`asr_phrase_manager.py:14 AsrPhraseManager` | ✅ M5 已落地(`client.audio.vocabularies` / `client.audio.phrases`,见 §7.4) | 存活([paraformer 热词](https://help.aliyun.com/zh/model-studio/paraformer-asr-phrase-manager) / [定制热词](https://help.aliyun.com/zh/model-studio/custom-hot-words/)) | -| 声音复刻(声纹注册) | `audio/tts_v2/enrollment.py:31 VoiceEnrollmentService` | ✅ M5 已落地(`client.audio.voices`;同端点的 qwen 声音设计一并建模为 `client.audio.voice_design`,1.x 无对应类) | [专页存活](https://help.aliyun.com/zh/model-studio/voice-cloning-user-guide),但示例以 `tts_v2.SpeechSynthesizer` + 直接 HTTP API 创建音色,未用该类 | -| omni 类型化助手 | `audio/qwen_omni/omni_realtime.py:399`(update_session/append_audio/create_response 等) | ✅ M2 已落地(`client.audio.realtime` omni 类型化助手,`8eaaf81`,见 §7.4) | 存活且主推([Qwen-Omni-Realtime](https://help.aliyun.com/zh/model-studio/realtime) / [Realtime API 概述](https://help.aliyun.com/zh/model-studio/realtime-api-overview)) | -| `dashscope` CLI(v2 版) | acli(自包含,走 1.x) | ⚪ 后续再加(§3 P2-7) | — | - -> 上表各项均已拍板或落地:hold 项存量用户继续用 1.x,CLI 后续再加;语音相关项已全部落地(§7)。 - ---- - -## 7. 语音模块专项(需立项,专门做) - -> 依据语雀专项文档([语音模块专项](https://aliyuque.antfin.com/songsong.sss/tcd79h/vi8tshautdo2k5iy))成稿,正文已由专项负责人提供(2026-09-22),接口对齐清单与严重问题以语雀为准,本章已按 v2 最新代码校正状态。 -> -> 定位:本章各项**不属于对齐修复**,而是 v2 语音面的独立建设,按一个专项(或拆子项)立项推进,不挤占 P0-P2 的修复节奏。 - -### 7.1 专项范围(语雀界定) - -- **语音识别**:qwen-audio(fun-asr/paraformer)系列实时识别、funasr-flash 等离线识别、录音文件识别。 -- **语音合成**:qwen-audio、CosyVoice 等。 -- **声音复刻 / 声音设计 / 热词词表**:均基于 HTTP 协议,可替换通用接口或直接用 http 请求。 -- **Omni**:omni / 翻译 / realtime。 - -### 7.2 严重问题(专项首要,必须解决) - -1. **ASR 缺 `update_context`(必须)** — ✅ 已解决(`8eaaf81` 合入 `dev/v2-audio-streaming-completion`,`d1de92f`):`Recognition.update_context(context)` 走 `continue-task` + `{"input": context}` wire,与 1.x `audio/asr/recognition.py:632` 对齐;非 dict 输入 `TypeError`。 -2. **TTS 流式上传/控制协议缺失(必须)** — ✅ 已解决(`8eaaf81`,`c570d20`/`24bb67b`):`SynthesisSession` 显式生命周期 `start/stop/cancel/close`,`flush()`(continue-task `{"input":{"flush":True}}`,已经真实服务验证,无需 1.x 的重述 model/task 字段)、`cancel()`(finish-task `input.directive=cancel`,ws 层 `_cancel_sent` 幂等);命名按评审 no-alias 前提(naming-review §六:直接改名、不留兼容 alias)收敛为 canonical 名(曾短暂保留的 1.x 别名 `streaming_*` 与 `TtsStream`/`start_stream` 已移除,同一动作只留一个名字)。`async_streaming_complete` 不做(v2 实时面 sync-only 设计)。 -3. **连接复用缺失(必须)** — ✅ 已解决(M6 落地): - - **ws 连接复用**:`WsConnection` + `WsTask`(transport/ws_connection.py)——一条物理连接 + 后台读线程按 `header.task_id` 解复用,支持并发多任务与顺序复用(免再握手);`client.ws_connection()` 工厂继承 client 配置,asr/tts/translation/tingwu 入口均接受 `connection=`。协议约束:二进制帧无 task_id,单连接二进制上行/下行各单 owner(TTS `start()` 自动 claim;并发 TTS 需多条连接),纯 JSON 任务完全并发;realtime 族协议无 task_id、会话结束即关连接,不可复用(非 SDK 限制,loopback 测试固化;压首开延迟走预热)。 - - **HTTP 连接复用(所有模型)**:每 `Client` 一个 httpx keep-alive 池(1.x 每次调用新建 `requests.Session`),所有资源默认复用;池参数开放 settings/env/config.toml(`http_max_connections` / `http_max_keepalive_connections` / `http_keepalive_expiry`);`with_options` 未改连接级选项时共享池。`tests/v2/test_http_pool.py` 本地服务器证明同一 TCP 连接被复用、keepalive=0 端到端生效。背景(2026-09-23 评审):1.x 大量用户以连接复用模式接入(主要 Java 侧),语音实时交互对延时极敏感——连接复用是硬需求而非优化项。 - -### 7.2.1 `dev/v2-audio-streaming-completion` 合入修复清单 - -> 2026-09-23 以 merge `8eaaf81` 合入(tag `v2-audio-m1` / `v2-audio-m2`)。分支经 4 个真实模型线下手工验证(未纳入 CI,模型名以专项验证记录为准):qwen-audio-3.1-asr-flash-streaming、qwen-audio-3.0-tts-flash(voice longanfengyu)、qwen3.5-omni-flash-realtime、qwen-audio-3.0-realtime-flash。合入策略:架构以分支为准,HEAD 独有能力逐项 graft(module 标识头、per-call `api_key`/`workspace`、`RecognitionResult`/`results()`/`recognize_file`、合成具名参数 + `enable_ssml` 联动、jobs `headers` 参数)。 - -| 修复 | 分支 commit | 对应条目 | -|---|---|---| -| `Recognition.update_context(context)`(continue-task + `{"input": context}`) | `d1de92f` | §7.2-1 | -| `SynthesisSession` 显式生命周期 `start/stop/cancel/close` + `flush()` + `cancel()`(ws `_cancel_sent` 幂等) | `c570d20` / `24bb67b` | §7.2-2 | -| ws 严格 framing/生命周期:task-failed 经 header `error_code`/`error_message` 走错误目录、EOF → `APIConnectionError`、finish/cancel/close 幂等 | `2c9a0b2` | §7.2 基础 | -| `RealtimeSession` 具名方法全集 + `session_id`(omni 类型化助手) | `ab3ec1c` | §7.3 Omni 表(M2) | -| livetranslate / qwen-audio 协议族(wire_model 前缀路由;`_incremental_session` / `_nested_translation_session`) | `a147465` / `68f6afe` | §7.4 M3 前置 | -| asr / tts / realtime SDK 使用指南(`dashscope-2.0-{asr,tts,realtime}-sdk.md`) | `cca6dfd` | 文档 | -| loopback ws 集成测试基座(`_LocalWebSocketServer`,aiohttp 真实回环;单测 mock `FakeWebSocket`/`_sent_frames` 早于本分支,由 `52f87e7` 引入、分支内各 commit 复用扩展) | `3adc9f5` | 测试 | - -合入后仍开放:`async_streaming_complete`(不做,v2 实时面 sync-only);§7.2-3 连接复用随后于 M6 落地(见 §7.4)。合入后的命名收敛(移除 `streaming_*` 别名 / `TtsStream` / `start_stream`,`update_context(input)` → `(context)`)见 §7.2-2 与 commit `5f70c02`。M3 gummy 门面(`client.audio.translation`)、M4 HTTP 合成(`client.audio.tts.http`)、M5 管理面(`client.audio.vocabularies/phrases/voices/voice_design`)、M6 连接复用(`WsConnection` + HTTP keep-alive settings)均已于本阶段落地,见 §7.4。 - -### 7.3 接口对齐清单(语雀表格,按 v2 现状校正) - -**Transcription 离线转写** — ✅ 已对齐:`call→asr.transcribe`、`async_call→asr.jobs.create`、`fetch→asr.jobs.fetch`、`wait→asr.jobs.wait`。 - -**Recognition 实时转写** - -| 线上 SDK | v2 入口 | 状态 | -|---|---|---| -| `Recognition.start` | `asr.recognize(...)` + with(`start()`) | ✅ | -| `send_audio_frame` / `stop` / 取结果 | `send_audio_frame` / `stop` / `events()` | ✅ | -| `Recognition.call(file)` | `asr.recognize_file(path)` | ✅ 非必须,已补(`b69b585`) | -| **`Recognition.update_context`** | `Recognition.update_context(context)` | ✅(`8eaaf81`/`d1de92f`,见 7.2-1) | -| `get_first_package_delay` / `get_last_package_delay` | `first_package_delay` / `last_package_delay` 属性 | ✅(`8eaaf81`/`d1de92f`) | -| `get_last_request_id` | `Recognition.request_id` 属性 | ✅(`8eaaf81`/`d1de92f`) | -| `RecognitionCallback` | 主动迭代 `events()`;`_callbacks.run_with_callback` 适配 | 🔀 | -| `RecognitionResult` | `events()` 裸 dict;`results()` 类型化 | ✅(`b69b585`) | -| 热词 | `phrase_id`(run-task 顶层 `resources`,与 1.x 同位) | ✅(`8eaaf81`;原 `phrases` 参数废弃) | - -**语音合成** - -| 线上 SDK | v2 入口 | 状态 | -|---|---|---| -| `tts_v2.SpeechSynthesizer.call` | `audio.tts.synthesize(...)` | ⚠️ 一次性合成近似对应 | -| `streaming_call` | `SynthesisSession.send_text` | ✅(`8eaaf81`) | -| **`streaming_flush`** | `SynthesisSession.flush` | ✅(`8eaaf81`/`c570d20`,见 7.2-2) | -| `streaming_complete` | `stop()` + `audio_chunks()` | ✅(`8eaaf81`) | -| **`async_streaming_complete`** | 无 | ❌ 不做(v2 实时面 sync-only) | -| **`streaming_cancel`** | `SynthesisSession.cancel` | ✅(`8eaaf81`/`c570d20`,见 7.2-2) | -| `close`(长会话管理) | `SynthesisSession` 显式 `start/stop/close` | ✅(`8eaaf81`/`24bb67b`) | -| `HttpSpeechSynthesizer.call(stream=False/True)` | `audio.tts.http.synthesize/stream(...)`(真 HTTP,非 ws) | ✅(M4;`HttpSynthesis`/`HttpSynthesisChunk` 类型化音频 URL/ID/过期时间,见 §7.4) | - -**Omni** - -| 线上 SDK | v2 入口 | 状态 | -|---|---|---| -| `OmniRealtimeConversation.connect` | `audio.realtime.connect(...)` + with | ✅ | -| `send_raw` | `session.send_event(...)`(dict,非 JSON 串) | ⚠️ | -| 接收事件 | `recv_event()` / `events()` | ✅ | -| `update_session`/`create_item`/`append_audio`/`append_video`/`commit`/`clear_appended_audio`/`create_response`/`cancel_response` | `RealtimeSession` 同名具名方法(另含 `retrieve_item`/`delete_item`) | ✅(`8eaaf81`/`ab3ec1c`) | -| `end_session` | `RealtimeSession.end_session()` | ✅(`8eaaf81`/`ab3ec1c`) | -| `get_session_id` / response ID / 延迟指标 | `session_id` 属性;指标见 realtime 事件跟踪 | ✅/❔(`8eaaf81`) | - -**实时翻译** — livetranslate 协议变体(qwen3-livetranslate/qwen3.5/qwen3.8)已内建于 `audio.realtime.connect` 会话层(`8eaaf81`/`a147465`,含嵌套翻译 session 组装);gummy 族(gummy-realtime-v1 / gummy-chat-v1)由 `client.audio.translation` 子资源承载(M3 已落地,见 §7.4):与 `asr.recognize` 同 wire(task_group=audio / task=asr / function=recognition,与 1.x `translation_recognizer.py:451/987` 逐帧一致),翻译参数具名化(`source_language`/`transcription_enabled`/`translation_enabled`/`translation_target_languages`/`max_end_silence`),`results()` 产双语类型 `TranslationResult`(`get_transcription`/`get_translations`/`is_sentence_end`),`recognize_file(path)` 对应 1.x `call(file)`;chat 形态不单独建模(1.x Chat 仅差"句终停发"的客户端行为,wire 相同,以 `is_sentence_end()` 自流控)。 - -**已验证模型**(分支真实跑通,2026-09-23):`qwen-audio-3.1-asr-flash-streaming`、`qwen-audio-3.0-tts-flash`(voice: longanfengyu)、`qwen3.5-omni-flash-realtime`、`qwen-audio-3.0-realtime-flash`。 - -**管理面(HTTP)** — ✅ M5 已落地:定制词表 `client.audio.vocabularies`(1.x `VocabularyService`,POST `services/audio/asr/customization`)、热词 `client.audio.phrases`(1.x `AsrPhraseManager`,编译任务:`create/update` POST `/fine-tunes`,`get/list/delete` 走 `/fine-tunes/outputs`)、声音复刻 `client.audio.voices`(1.x `VoiceEnrollmentService`,POST `services/audio/tts/customization`)、声音设计 `client.audio.voice_design`(官方 REST,与复刻同端点,`model=qwen-voice-design`,action 词 `create/list/query/delete`)。1.x 五动词统一为 `create/list/get/update/delete`(`query_*` → `get`);1.x 服务内 3 次重试改由 transport 重试循环承担;1.x `list_phrases(page=)` 实参被 `**kwargs` 丢弃的缺陷不复刻(v2 发 `page_no`)。 - -### 7.4 修复计划 - -严重问题(7.2)优先于能力补齐,分阶段交付,每阶段独立 commit、独立可发版。 -**进度更新(2026-09-23)**:`dev/v2-audio-streaming-completion` 分支(9 commits,四模型真实验证)经 `8eaaf81` 合入,M1(除连接复用)/M2 已落地,M3 协议层随合入落地、gummy 门面(`client.audio.translation`)本阶段补齐;M4 HTTP 合成(`client.audio.tts.http`)、M5 管理面(`client.audio.vocabularies/phrases/voices/voice_design`)、M6 连接复用(`WsConnection` + 全模型 HTTP keep-alive settings)均已落地,语音专项 §7 全部关闭。 - -| 阶段 | 范围 | 状态 | 交付物 | 验收 | -|---|---|---|---|---| -| M1 ws 协议能力补齐 | 严重问题 1-2 | ✅ `8eaaf81`(tag `v2-audio-m1`) | `Recognition.update_context`;`SynthesisSession` 显式生命周期 + `flush/cancel`;ws 严格 framing/生命周期(`2c9a0b2`) | 真实模型验证(asr-flash-streaming / tts-flash)+ loopback ws 单测(`3adc9f5`) | -| M2 omni 类型化助手 | 7.3 Omni 行 | ✅ `8eaaf81`(tag `v2-audio-m2`) | `RealtimeSession` 具名方法全集(update_session/append_audio/append_video/commit/clear_appended_audio/create_item/retrieve_item/delete_item/create_response/cancel_response/end_session)+ `session_id`(`ab3ec1c`) | omni-flash-realtime / realtime-flash 真实验证 + `test_audio_omni_session.py`(`68f6afe`) | -| M3 实时翻译 | livetranslate / gummy | ✅ | livetranslate 三变体内建 `realtime.connect`(`8eaaf81`/`a147465`);gummy 门面 `client.audio.translation`:`TranslationSession`(复用 asr wire)+ 具名翻译参数 + 双语 `TranslationResult` + `recognize_file`;chat 形态评估结论:不单独建模(wire 相同,句终停发由调用方经 `is_sentence_end()` 表达) | [Gummy 页](https://help.aliyun.com/zh/model-studio/real-time-python-sdk)流程平移至 `md/dashscope-2.0-translation-sdk.md`;`test_audio_translation.py` wire 逐帧对齐 | -| M4 HTTP 合成 | `http_tts:114` + `qwen_tts:11`(合并) | ✅ | `client.audio.tts.http`(真 HTTP POST,非 ws):`synthesize` 一次性 → `HttpSynthesis`(audio_url/audio_id/expires_at/usage/request_id 类型化);`stream` SSE 流式 → `HttpSynthesisChunk`(audio 帧 base64 解码、sentence、is_final,末帧带整段音频 URL 引用);双模型族按模型名前缀路由(`qwen*` → aigc/multimodal-generation/generation,其余 → audio/tts/SpeechSynthesizer),具名参数 `voice/format/sample_rate/language_type` + `parameters=`/`extra_input=` 透传(具名优先) | sambert 与 qwen-tts 两个官方示例平移至 `md/dashscope-2.0-tts-sdk.md`;`test_audio_tts_http.py` MockTransport 6 例(端点路由/参数合并/SSE 分片/错误帧/错误状态) | -| M5 管理面 | 词表/热词/声音复刻/声音设计 | ✅ | `client.audio.vocabularies`(1.x `VocabularyService`,`services/audio/asr/customization`)/ `phrases`(1.x `AsrPhraseManager`,`/fine-tunes` + `/fine-tunes/outputs` 编译任务)/ `voices`(1.x `VoiceEnrollmentService`,`services/audio/tts/customization`)/ `voice_design`(官方 REST,同复刻端点,`qwen-voice-design`);五动词统一 `create/list/get/update/delete`,类型化返回(Vocabulary/Voice/DesignedVoice/Phrase + 各自 List + `JobStatus`) | 1.x 五动词 × 各服务 wire 逐项断言;官方四族示例平移至 `md/dashscope-2.0-audio-management-sdk.md`;`test_audio_management.py` MockTransport 8 例 | -| M6 连接复用(上调,见 7.2-3) | ws 复用 + 全模型 HTTP keep-alive | ✅ | ws:`WsConnection`+`WsTask`(读线程按 `header.task_id` 解复用,并发多任务 + 顺序复用免握手;二进制帧无 task_id → 单连接上行/下行各单 owner,纯 JSON 全并发),`client.ws_connection()` 工厂,asr/tts/translation/tingwu 接受 `connection=`;realtime 族验证结论:协议无 task_id、会话结束即关连接,不可复用(建议预热),loopback 测试固化;HTTP:每 `Client` 一个 keep-alive 池,池参数开放 settings/env/config.toml(`http_max_connections`/`http_max_keepalive_connections`/`http_keepalive_expiry`) | `test_ws_connection.py` 12 例(并发解复用/顺序复用单握手/二进制 owner/错误广播)+ `test_audio_connection.py` 资源集成 7 例;`test_http_pool.py` 本地服务器证明同一 TCP 连接被复用、keepalive=0 端到端生效;指南 `md/dashscope-2.0-connection-reuse-sdk.md` | - -**节奏约定** -- 每阶段 = 1 个 commit(与 P 级修复同样约定),完成后 §6 对应行与 7.3 清单回填 ✅ 与 commit。 -- M6 的 ws 复用经设计评审后落地(评审结论:并发 + 顺序复用全量 demux、realtime 作为验证项、HTTP 池参数开放 settings);HTTP keep-alive 为 transport 默认行为,随 M6 同批落地。 -- 背景(2026-09-23 评审):1.x 大量用户以连接复用模式接入(主要 Java 侧),语音实时交互对延时敏感——连接复用是硬需求而非优化项。 -- 范围与优先级以语雀专项文档为准;本表随专项推进滚动更新。 diff --git a/md/dashscope-2.0.3-python-sdk.md b/md/dashscope-2.0.3-python-sdk.md deleted file mode 100644 index fe6652ce..00000000 --- a/md/dashscope-2.0.3-python-sdk.md +++ /dev/null @@ -1,675 +0,0 @@ -# DashScope Python SDK 2.0.3 接口与迁移指南 - -本文面向 Python SDK 2.0.3,汇总当前包结构、1.x → 2.0 接口映射、核心调用示例与 -迁移注意事项。示例基于当前实现核对;完整参数与分领域说明见文末相关文档。 - -## 1. 安装与最小示例 - -Python 要求:**3.9 及以上**。 - -```bash -pip install -U dashscope -export DASHSCOPE_API_KEY="sk-xxx" -# 推荐配置 workspace;也可在 Client(workspace="ws-xxx") 中显式传入 -export DASHSCOPE_WORKSPACE="ws-xxx" -``` - -```python -import dashscope - -with dashscope.Client() as client: - response = client.chat.completions.create( - model="qwen-plus", - messages="用一句话介绍杭州", - ) - print(response.choices[0].message.content) - print(response.usage.total_tokens, response.request_id) -``` - -异步客户端具有同名资源与方法: - -```python -import dashscope - -async with dashscope.AsyncClient() as client: - response = await client.chat.completions.create( - model="qwen-plus", - messages="你好", - ) - print(response.output_text) -``` - -只需一次文本调用时,可以使用快捷入口: - -```python -response = dashscope.chat("qwen-plus", "给我讲个笑话") -print(response.output_text) -``` - -## 2. 2.0.3 包结构与兼容边界 - -### 2.1 当前规范入口 - -2.0 实现直接位于 `dashscope` 根包,**不再存在 `dashscope.v2` 命名空间**: - -```python -import dashscope -from dashscope import AsyncClient, Client, Config, Settings -from dashscope import DashScopeError, ErrorCode, WsConnection, WsTask -from dashscope import errors, settings -from dashscope.tokenizers import get_tokenizer, list_tokenizers -``` - -资源树挂在 `Client` / `AsyncClient` 上,主要入口如下: - -| 领域 | 当前入口 | -| --- | --- | -| 文本对话 | `client.chat.completions` | -| 百炼应用 | `client.apps.completions` | -| 多模态对话与向量 | `client.multimodal.chat` / `client.multimodal.embeddings` | -| 图像与视频 | `client.images` / `client.videos`,均含 `jobs` | -| 文本向量与批量向量 | `client.embeddings` / `client.embeddings.jobs` | -| 排序与分词 | `client.rerank` / `client.tokenization` | -| 语音 | `client.audio.asr` / `tts` / `realtime` / `translation` | -| 语音管理面 | `client.audio.vocabularies` / `phrases` / `voices` / `voice_design` | -| 听悟 | `client.tingwu` | -| 微调与部署 | `client.fine_tunes` / `client.deployments` | -| 文件与模型 | `client.files` / `client.models` | -| Agentic RL | `client.rl`,长名别名 `client.reinforcement_learning` | -| AgentStudio | `client.agentstudio` | - -### 2.2 1.x 与 2.0 的目录分工 - -- 根包保留 `Generation`、`ImageSynthesis`、`FineTunes`、`Deployments` 等既有 - 1.x 公开导出,存量代码可继续使用。 -- 1.x 深层实现统一位于 `dashscope.legacy_1x`;旧的 `dashscope.aigc.*`、 - `dashscope.files`、`dashscope.models`、`dashscope.client.base_api` 等 deep import - 通过懒加载别名继续可用,存量代码无需修改。 -- AgentStudio 与 FineTune/Agentic RL 的物理实现归入 `dashscope.applications`; - 公开 `dashscope.agentstudio`、`dashscope.finetune` 导入路径通过别名保持不变。 -- 2.0 Agentic RL 公开入口仍为 `dashscope.finetune.agentic_rl`,函数组件与运行框架 - 位于 `dashscope.finetune.reinforcement`。 -- 1.x Agentic RL 管理面位于 `dashscope.legacy_1x.finetune.agentic_rl`。 -- 不提供 `dashscope.reinforcement` 或 `dashscope.legacy_2x` 兼容命名空间。 -- `dashscope` 命令行入口仍由 1.x CLI 提供;2.0 以 `Client` 资源树为编程入口。 - -直接使用 2.0 Agentic RL 类型时: - -```python -from dashscope.finetune.agentic_rl import AgenticRL, SubmitResult -from dashscope.finetune.reinforcement import ( - Dataset, - TrainingDataset, - ValidationDataset, -) -``` - -2.0 `AgenticRL.submit_job()` 返回 `SubmitResult`。1.x wrapper 的同名方法返回 -`FineTune`,并额外提供 `cancel/list/get/delete/logs`;两者不能仅因类名相同就 -混用返回契约。常规业务代码优先使用 `client.rl`,无需直接管理这些实现类。 - -## 3. 1.x → 2.0 接口映射 - -状态说明:**迁移**表示有直接对应入口;**收敛**表示多个旧入口合并为同一资源。 - -### 3.1 文本、应用与多模态 - -| 1.x | 2.0.3 | 说明 | -| --- | --- | --- | -| `Generation.call` | `client.chat.completions.create` | 迁移 | -| `AioGeneration.call` | `await aclient.chat.completions.create` | 迁移 | -| `Conversation.call` | `client.chat.completions.create` | 收敛;调用方维护 messages | -| `CodeGeneration.call` | `client.chat.completions.create` | 收敛;通过 model 区分 | -| `Understanding.call` | `client.chat.completions.create` | 收敛;文本理解走对话端点 | -| `Application.call` | `client.apps.completions.create` | 迁移 | -| `MultiModalConversation.call` | `client.chat.completions.create` 或 `client.multimodal.chat.create` | 媒体 part 自动路由;后者为显式入口 | -| `ImageSynthesis.call/async_call/fetch/wait` | `client.images.generate` / `client.images.jobs.*` | 一步到位或分步控制 | -| `ImageGeneration.call/async_call/fetch/wait` | `client.images.generate` / `client.images.jobs.*` | 按模型名自动路由协议 | -| `VideoSynthesis.call/async_call/fetch/wait` | `client.videos.generate` / `client.videos.jobs.*` | 返回统一 `Job` | -| `MultiModalEmbedding.call` | `client.multimodal.embeddings.create` | 文本、图片、音频、视频向量 | - -### 3.2 向量、排序与分词 - -| 1.x | 2.0.3 | -| --- | --- | -| `TextEmbedding.call` | `client.embeddings.create` | -| `BatchTextEmbedding.call/fetch/wait` | `client.embeddings.jobs.create/fetch/wait` | -| `TextReRank.call` / `AioTextReRank.call` | `client.rerank.create` / `await aclient.rerank.create` | -| `Tokenization.call` | `client.tokenization.count` | -| 本地 tokenizer | `dashscope.tokenizers.get_tokenizer` / `list_tokenizers` | - -### 3.3 语音与听悟 - -| 1.x | 2.0.3 | -| --- | --- | -| `Recognition.start/send_audio_frame/stop` | `client.audio.asr.recognize(...)` 上下文管理器 | -| `Transcription.async_call/fetch/wait` | `client.audio.asr.transcribe` / `client.audio.asr.jobs.*` | -| `QwenTranscription` | 同上,通过 `model=` 区分 | -| `SpeechSynthesizer.call` | `client.audio.tts.synthesize/stream` | -| `tts_v2.SpeechSynthesizer` | `client.audio.tts.stream/connect` | -| `HttpSpeechSynthesizer.call` | `client.audio.tts.http.synthesize/stream` | -| `TranslationRecognizerRealtime` | `client.audio.translation.recognize(...)` | -| `OmniRealtimeConversation` | `client.audio.realtime.connect(...)` | -| `VocabularyService` | `client.audio.vocabularies` | -| `AsrPhraseManager` | `client.audio.phrases` | -| `VoiceEnrollmentService` | `client.audio.voices` | -| `TingWu.call` / 实时理解 | `client.tingwu.call` / `client.tingwu.realtime` | - -2.0 音频 API 当前仍按 preview 管理,首次访问 `client.audio` 会发出 -`FutureWarning`;升级时应核对音频专题文档中的协议与生命周期约束。 - -### 3.4 微调、部署、资源与 Agent - -| 1.x | 2.0.3 | -| --- | --- | -| `FineTunes.call` | `client.fine_tunes.create` | -| `FineTunes.cancel/list/get/delete` | `client.fine_tunes.cancel/list/get/delete` | -| `FineTunes.stream_events/wait/logs` | `client.fine_tunes.stream_events/wait/logs` | -| `Deployments.call/list/get/delete/scale` | `client.deployments.create/list/get/delete/scale` | -| `Models.list/get` | `client.models.list/get` | -| `Files.upload/list/get/delete` | `client.files.upload/list/get/delete` | -| CLI `dashscope rl ...` | `client.rl.*` + `client.rl.jobs.*` | -| AgentStudio 子 SDK | `client.agentstudio.*` 懒挂载 | - -Assistants / Threads / Messages / Runs / Steps 不属于 2.0 资源面。新托管 Agent -场景使用 `client.agentstudio`;不要为了迁移旧对话状态而自行模拟这些资源。 - -## 4. 客户端配置与协议 - -### 4.1 构造参数与配置优先级 - -```python -client = dashscope.Client( - api_key="sk-xxx", - workspace="ws-xxx", - region="cn-beijing", - protocol="native", - timeout=60.0, - max_retries=2, - raise_on_error=True, - config_path="~/.dashscope/config.toml", - protocol_options={"strict": False}, - http_client=None, -) -``` - -配置优先级:**显式参数 > `dashscope.settings` 赋值 > 环境变量 > config.toml > -默认值**。不显式传 `api_key` 时读取 `DASHSCOPE_API_KEY`。建议配置 `workspace` -以使用专属网关;未配置 workspace 且未显式给 `base_url` 时,SDK 回退共享网关并 -发出一次 `UserWarning`。 - -`region` 默认 `cn-beijing`,支持 `cn-beijing`、`ap-southeast-1`、 -`cn-hongkong`、`us-east-1`、`eu-central-1`、`ap-northeast-1`。自建或私有网关 -可显式传 `base_url`。 - -运行时可使用全局配置门面,或派生只改部分选项的客户端: - -```python -dashscope.settings.timeout = 30.0 -dashscope.settings.max_retries = 3 - -strict = client.with_options(protocol="openai", timeout=10.0) -``` - -`with_options()` 在未改变连接级选项时共享原客户端连接池,派生实例不应比原实例 -活得更久。普通请求上的 `timeout=` 是单次 HTTP 请求超时;`images.generate`、 -`videos.generate`、`audio.asr.transcribe` 和各类 `wait` 上的 `timeout=` 是等待 -任务终态的总预算。 - -### 4.2 三种对话协议 - -`protocol` 只改变 `chat.completions` / `multimodal.chat` 的线缆格式,调用方式和 -统一返回类型不变: - -| 取值 | 端点形态 | 说明 | -| --- | --- | --- | -| `native` | DashScope `/services/...` | 默认,支持完整 DashScope 参数 | -| `openai` | `/chat/completions` | 对接百炼 compatible-mode 或 OpenAI 兼容网关 | -| `anthropic` | `/v1/messages` | system 独立、工具 schema 转换 | - -```python -openai_client = dashscope.Client(protocol="openai") -response = openai_client.chat.completions.create( - model="qwen-plus", - messages="你好", -) -``` - -`openai` 协议默认透传非 OpenAI 原生参数;`protocol_options={"strict": True}` -可改为拒绝未知参数。`anthropic` 对无对应字段的参数直接报 400,避免参数被静默 -丢弃;该协议下 `max_tokens` 缺省自动补 `4096`,`stop` 映射为 `stop_sequences`。 - -## 5. 核心调用示例 - -以下示例复用已构造的 `client` / `aclient`。 - -### 5.1 对话、工具与结构化输出 - -```python -response = client.chat.completions.create( - model="qwen-plus", - messages=[ - {"role": "system", "content": "你是一个有帮助的助手。"}, - {"role": "user", "content": "北京今天天气怎么样?"}, - ], - temperature=0.7, - max_tokens=512, - tools=[{ - "type": "function", - "function": { - "name": "get_weather", - "description": "查询城市天气", - "parameters": { - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"], - }, - }, - }], -) -print(response.choices[0].message.tool_calls) - -structured = client.chat.completions.create( - model="qwen-plus", - messages="给我一个用户 JSON", - response_format={"type": "json_object"}, -) -print(structured.output_text) -``` - -签名之外的模型参数通过 `parameters=` 传递。多轮对话由调用方维护 messages 列表, -2.0 chat 本身不保存历史。 - -### 5.2 增量流式输出 - -```python -stream = client.chat.completions.create( - model="qwen-plus", - messages="写一首短诗", - stream=True, -) -for chunk in stream: - delta = chunk.choices[0].delta - if delta.content: - print(delta.content, end="", flush=True) -``` - -`ChatStream.text_stream` 只迭代文本,`get_final_message()` 聚合完整 message(含 -工具调用),`headers` 提供响应头。与 1.x 默认重放全量文本不同,2.0 chat 每帧 -都是增量。异步流式使用 `await aclient.chat.completions.create(..., stream=True)` -和 `async for`。 - -### 5.3 百炼应用 - -```python -response = client.apps.completions.create( - app_id="your-app-id", - prompt="帮我写一份周报大纲", -) -print(response.text) - -for partial in client.apps.completions.create( - app_id="your-app-id", - prompt="讲个故事", - stream=True, - incremental_output=True, -): - print(partial.text, end="") -``` - -`prompt` 与 `messages` 至少传一个;同时给出时,`prompt` 作为最新 user 消息追加。 -`session_id` 用于服务端会话状态,`biz_params` 用于应用流程或插件入参。 - -### 5.4 多模态对话与向量 - -```python -response = client.chat.completions.create( - model="qwen-vl-plus", - messages=[{ - "role": "user", - "content": [ - {"image": "https://example.com/cat.jpg"}, - {"text": "这张图里有什么?"}, - ], - }], -) -print(response.output_text) - -embeddings = client.multimodal.embeddings.create( - model="multimodal-embedding-v1", - input=[ - {"text": "一只猫"}, - {"image": "https://example.com/cat.jpg"}, - ], -) -for item in embeddings.embeddings: - print(item.index, item.type, len(item.embedding)) -``` - -只要 messages 含 image / audio / video part,`chat.completions.create` 就自动走 -多模态端点。`client.multimodal.chat.create` 是始终使用该端点的显式入口。类型化 -向量输入可从 `dashscope.resources.multimodal.embeddings` 导入 `EmbeddingItem`。 - -### 5.5 图像、视频与统一 Job - -```python -image_job = client.images.generate( - model="wanx2.1-t2i-turbo", - prompt="一只在月球上弹吉他的猫,水彩风格", - size="1024*1024", -) -print(image_job.image_urls) - -video_job = client.videos.generate( - model="wan2.7-t2v", - prompt="一只小猫在草地上奔跑", - size="1280*720", -) -print(video_job.video_url) - -# 分步提交与轮询;images / videos / embeddings / asr 的 jobs 语义一致 -job = client.images.jobs.create( - model="wanx2.1-t2i-turbo", - prompt="一只猫", -) -snapshot = client.images.jobs.fetch(job.task_id) -done = client.images.jobs.wait(job, timeout=600.0) -``` - -`generate` 是“提交并等待”,`jobs.create` 只提交,`wait` 使用退避轮询。统一 -`Job` 常用字段为 `task_id` / `status` / `results` / `result_url` / `image_urls` / -`video_url`;只有服务相关的原始载荷保留在 `Job.output`。任务失败或被取消时, -默认抛出 `DashScopeError` 异常族。 - -### 5.6 文本向量、排序与分词 - -```python -embedded = client.embeddings.create( - model="text-embedding-v3", - input=["今天天气不错", "明天会下雨吗"], - dimension=1024, - text_type="document", -) -print([len(item.embedding) for item in embedded.embeddings]) - -ranked = client.rerank.create( - model="gte-rerank-v2", - query="什么是量子计算?", - documents=["量子计算利用量子力学原理。", "今天超市鸡蛋打折。"], - top_n=1, - return_documents=True, -) -print(ranked.results[0].index, ranked.results[0].relevance_score) - -count = client.tokenization.count(model="qwen-plus", prompt="你好,世界") -print(count.input_tokens) -``` - -批量向量使用 `client.embeddings.jobs.create(model=..., file_url=...)`,完成后从 -`job.result_url` 读取结果文件。本地分词不发送请求: - -```python -from dashscope.tokenizers import get_tokenizer - -tokenizer = get_tokenizer("qwen-plus") -token_ids = tokenizer.encode("你好,世界") -print(tokenizer.decode(token_ids)) -``` - -### 5.7 语音与实时会话 - -```python -# 文件转写:提交并等待 -job = client.audio.asr.transcribe( - ["https://example.com/meeting.wav"], - model="paraformer-v2", -) -print([item.transcription_url for item in job.results]) - -# 一次性语音合成 -audio = client.audio.tts.synthesize( - "你好,欢迎收听今天的节目。", - model="cosyvoice-v1", - voice="longxiaochun", - format="mp3", -) -with open("out.mp3", "wb") as file: - file.write(audio) - -# 流式语音合成 -with open("stream.mp3", "wb") as file: - for chunk in client.audio.tts.stream( - "这是一段较长的文本。", - model="cosyvoice-v1", - voice="longxiaochun", - ): - file.write(chunk) -``` - -实时 ASR、双向流 TTS、Realtime 与实时翻译采用 websocket 会话,不是普通 HTTP -流式响应。它们通过上下文管理器管理生命周期,并用 `events()` / `results()` 迭代 -服务端事件。发送与接收通常需要并发,完整示例见文末语音专题文档。 - -支持共享连接的会话可复用 `client.ws_connection()`;共享连接由调用方关闭。实时 -协议是否支持复用取决于任务帧是否携带 `task_id`,不要把 Realtime 会话强行放入 -共享连接。 - -### 5.8 文件、微调与部署 - -```python -uploaded = client.files.upload( - file_path="./train.jsonl", - purpose="fine-tune", -) - -fine_tune = client.fine_tunes.create( - model="qwen-turbo", - training_file_ids=[uploaded.file_id], - hyper_parameters={"n_epochs": 2, "learning_rate": "1e-5"}, -) -finished = client.fine_tunes.wait(fine_tune, timeout=7200) -print(finished.finetuned_model) - -for event in client.fine_tunes.stream_events(fine_tune.job_id): - print(event.output) - -# 部署后使用 deployed_model 作为 get / scale / delete 的定位键 -deployment = client.deployments.create(model="qwen-turbo", capacity=2) -client.deployments.scale(deployment.deployed_model, capacity=4) -print(client.deployments.get(deployment.deployed_model).status) -``` - -2.0 响应信封已扁平化:直接读取 `fine_tune.status`、`uploaded.file_id`,不再经过 -`.output.status` 或 `.data["file_id"]`。顶层响应共享 `request_id` / `code` / -`message` / `is_error` / `status_code` / `headers` 字段。 - -### 5.9 Agentic RL - -```python -registered = client.rl.register_functions( - rollout_classpaths=["my_project.rollout:MyRollout"], - reward_classpaths=["my_project.reward:MyReward"], -) - -client.rl.test_functions( - instance_id=registered.rollout_instance_ids[0], - func_type="rollout", - input_data={"query": "1+1=?"}, -) - -uploaded = client.rl.upload_data( - training_files=["./rl_train.jsonl"], - validation_files=["./rl_val.jsonl"], -) - -job = client.rl.jobs.create( - config_path="./rl_config.yaml", - job_name="my-rl-job", -) -print(client.rl.jobs.fetch(job.job_id).status) -done = client.rl.jobs.wait(job) -``` - -`client.rl.*` 负责函数注册、测试和数据上传,`client.rl.jobs.*` 负责训练任务生命周期。 -`jobs.create/register_functions/test_functions/upload_data` 以及函数实例管理方法 -复用底层工作流,不接受 `timeout=`;其余 `jobs` 方法支持 `timeout=`。整个 -`client.rl` 的错误统一由 `DashScopeError` 覆盖,本地工作流失败通常表现为 -`SdkError`。 - -### 5.10 AgentStudio - -```python -from dashscope.agentstudio import user_message - -studio = client.agentstudio -agent = studio.agents.create( - name="demo", - model="qwen-plus", - system_prompt="你是助手", -) -session = studio.sessions.create(agent=agent.id, title="第一次对话") -studio.sessions.events.send(session.id, [user_message("你好")]) - -with studio.sessions.events.stream(session.id) as stream: - for text in stream.text_stream: - print(text, end="") -``` - -AgentStudio 在首次访问 `client.agentstudio` 时构造,包含 `agents`、`sessions`、 -`events`、`files`、`skills`、`vaults`、`environments`、`deployments` 等完整子 -资源。其返回模型沿用 AgentStudio 自身类型体系,不套用 2.0 的响应扁平化;通过 -`client.agentstudio` 调用时,异常统一翻译为 `dashscope.errors` 异常族。关闭主 -Client 会连带关闭已挂载的 AgentStudio Client。 - -## 6. 错误、重试与流式失败 - -### 6.1 统一异常族 - -```python -from dashscope.errors import ( - APIConnectionError, - APITimeoutError, - BadRequestError, - ContentFilterError, - DashScopeError, - NotFoundError, - RateLimitError, - SdkError, -) - -try: - response = client.chat.completions.create( - model="qwen-plus", - messages="你好", - ) -except ContentFilterError as error: - print("内容审核拦截", error.request_id) -except BadRequestError as error: - print("请求参数错误", error.code, error.message) -except RateLimitError as error: - print("限流", error.status_code, error.request_id) -except DashScopeError as error: - print(type(error).__name__, error.code, error.message, error.request_id) -``` - -九个 HTTP 状态类为 `BadRequestError`(400)、`AuthenticationError`(401)、 -`PermissionDeniedError`(403)、`NotFoundError`(404)、 -`RequestTooLargeError`(413)、`RateLimitError`(429)、 -`InternalServerError`(500)、`ServiceUnavailableError`(503)和 -`GatewayTimeoutError`(504)。`ContentFilterError` 是 `BadRequestError` 子类, -单独捕获时必须放在前面。 - -异常常用字段:`message` / `code` / `status_code` / `request_id` / `body` / -`headers`。连接错误、超时和 SDK 本地错误没有 HTTP 响应,`status_code` 为 -`None`;`SdkError` 另提供 `solution` / `allow_retry`。HTTP 非 2xx 与 HTTP 200 -但 body 带业务错误码的情况使用同一异常族。 - -### 6.2 自动重试与返回值模式 - -默认 `max_retries=2`,传输层按 `Retry-After` 或指数退避加抖动重试连接错误及 -408 / 409 / 429 / 500 / 502 / 503 / 504。认证、权限、资源不存在和参数错误不应 -盲目做应用层重复重试。 - -若需要 1.x 风格的返回值自查: - -```python -lenient = client.with_options(raise_on_error=False) -response = lenient.chat.completions.create(model="qwen-plus", messages="你好") -if response.is_error: - print(response.code, response.message, response.request_id) -``` - -`raise_on_error=False` 不会吞掉连接失败和超时,因为没有响应对象可返回。流式 -业务错误在返回值模式下产出一个 `is_error=True` 的对象后结束迭代。 - -## 7. 全局配置与运维 - -### 7.1 常用环境变量 - -| 环境变量 | 作用 | 默认 | -| --- | --- | --- | -| `DASHSCOPE_API_KEY` | API Key | — | -| `DASHSCOPE_API_KEY_FILE_PATH` | API Key 文件读取路径 | — | -| `DASHSCOPE_WORKSPACE` | workspace id | — | -| `DASHSCOPE_REGION` | 地域 | `cn-beijing` | -| `DASHSCOPE_BASE_URL` | 自定义 HTTP 端点 | — | -| `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | -| `DASHSCOPE_TIMEOUT` | 请求超时(秒) | — | -| `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` | -| `DASHSCOPE_CONFIG` | config.toml 路径 | `~/.dashscope/config.toml` | -| `DASHSCOPE_LOG` | 当前客户端日志级别 | `warning` | -| `DASHSCOPE_WEBSOCKET_BASE_URL` | ASR/TTS 等 websocket 端点覆盖 | 跟随配置 | -| `DASHSCOPE_AGENTSTUDIO_URL` / `AGENTSTUDIO_URL` | AgentStudio 自定义端点 | — | - -HTTP 连接池可通过 `DASHSCOPE_HTTP_MAX_CONNECTIONS`、 -`DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS`、 -`DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` 或对应 settings 字段调整。长驻并复用 Client -才能充分利用 keep-alive。 - -### 7.2 日志与 API Key 文件 - -当前客户端使用独立的 `dashscope._logging` logger 层级,配置它不会改变 1.x 根 -logger 的 level、handlers 或 filters: - -```python -dashscope.settings.log_level = "DEBUG" -``` - -根 `save_api_key(api_key, api_key_file_path=None)` 来自 `dashscope._config`,默认 -写入 `~/.dashscope/api_key`。它使用 UTF-8、展开 `~`,并为自定义路径创建缺失的 -父目录;`DASHSCOPE_API_KEY_FILE_PATH` 只影响读取,不改变默认写入位置。 - -## 8. 升级检查清单 - -1. 将所有新代码的 `dashscope.v2...` 导入改为根 `dashscope...`;不要保留已删除的 - `dashscope.v2` 兼容假设。 -2. 新代码使用 `Client` / `AsyncClient`,不要继续依赖模块级 `api_key` 和 - `.call()` 类方法组织新调用。 -3. 将响应检查从“先判断 `status_code`”调整为异常处理;若暂时保留返回值模式, - 必须检查 `is_error`。 -4. 将 chat 流式消费改为增量 delta;应用流式仍按 `incremental_output` 判断帧语义。 -5. 将图像、视频、批量向量、转写等长任务统一到 `create/fetch/wait`,并为等待 - 操作设置符合业务预算的 `timeout`。 -6. 检查多轮对话是否改为由应用维护 messages;不要依赖 1.x `Conversation` history。 -7. 旧 deep import 可继续通过别名使用;新代码可按公开入口导入。AgentStudio 与 - FineTune/Agentic RL 物理实现位于 `dashscope.applications`,原公开路径不变; - 不存在 `dashscope.reinforcement`。 -8. Agentic RL 不要混淆 2.0 `SubmitResult` 与 1.x `FineTune` 返回契约;优先迁移到 - `client.rl`。 -9. 视频调用若依赖 1.x `extend_prompt=True` 的旧默认行为,迁移后显式传 - `prompt_extend=True`。 -10. 使用 `client.close()` / `await aclient.aclose()` 或上下文管理器释放连接; - 音频 preview、AgentStudio 与 RL 分别按专题文档复核生命周期和参数约束。 - -## 9. 相关文档 - -- [2.0 接口映射核对表](dashscope-2.0-interfaces-mapping.md) -- [2.0 接口调用示例集](dashscope-2.0-interfaces-examples.md) -- [1.x → 2.0 迁移指南](dashscope-2.0-migration-guide.md) -- [2.0 设计规范](dashscope-2.0-design.md) -- [ASR:实时识别与文件转写](dashscope-2.0-asr-sdk.md) -- [TTS:一次性、流式与双向流合成](dashscope-2.0-tts-sdk.md) -- [Realtime:Omni、Qwen-Audio 与 Livetranslate](dashscope-2.0-realtime-sdk.md) -- [实时语音翻译](dashscope-2.0-translation-sdk.md) -- [语音管理面](dashscope-2.0-audio-management-sdk.md) -- [websocket 连接复用](dashscope-2.0-connection-reuse-sdk.md) diff --git a/md/dashscope-interfaces.md b/md/dashscope-interfaces.md deleted file mode 100644 index 626d80ce..00000000 --- a/md/dashscope-interfaces.md +++ /dev/null @@ -1,222 +0,0 @@ -# dashscope 全量接口清单(SDK / 自带 CLI / Agentic) - -每个接口一句话说明,含主要输入。分三部分:dashscope SDK(Python)、dashscope 自带 CLI、Agentic(SDK + acli CLI)。 - ---- - -# 一、dashscope SDK(Python) - -## 文本 / 对话 - -| 接口 | 说明 | -| --- | --- | -| `Generation.call(model, messages, **kw)` | 文本生成/对话,输入模型名+消息列表,支持 stream、tools、temperature | -| `AioGeneration.call(...)` | `Generation` 的 asyncio 异步版 | -| `Conversation.call(model, prompt, history)` | 老式对话接口,history 为 `History`/`HistoryItem` | -| `CodeGeneration.call(model, messages)` | 代码生成,输入同 messages 格式 | -| `Understanding.call(model, prompt)` | 语义理解,输入模型+单句文本 | -| `Application.call(api_key, app_id, prompt, **kw)` | 百炼应用调用,输入应用 ID+用户提示 | - -## 多模态 - -| 接口 | 说明 | -| --- | --- | -| `MultiModalConversation.call(model, messages)` | 多模态对话,messages 元素含 text/image/audio/video URL | -| `AioMultiModalConversation.call(...)` | 异步版 | -| `ImageSynthesis.call/async_call/fetch/wait(model, prompt, n, size)` | 文生图(通义万相),异步任务四件套 | -| `VideoSynthesis.call/async_call/fetch/wait(model, prompt/img_url)` | 文/图生视频 | -| `MultiModalEmbedding.call(model, input=[...])` | 多模态向量化,input 为 ItemText/ItemImage/ItemAudio 列表 | -| `AioMultiModalEmbedding.call(...)` | 异步版 | - -## 向量 / 排序 / 分词 - -| 接口 | 说明 | -| --- | --- | -| `TextEmbedding.call(model, texts)` | 文本批量向量化,输入文本列表 | -| `BatchTextEmbedding`(call/fetch/wait) | 异步批量 embedding 任务 | -| `TextReRank.call(model, query, documents)` | 语义重排,输入查询+文档列表 | -| `AioTextReRank.call(...)` | 异步版 | -| `Tokenization.call(model, prompt)` | 统计 prompt 的 token 数 | - -## 语音 - -| 接口 | 说明 | -| --- | --- | -| `Recognition.start()/send_audio_frame(bytes)/call(file)/stop()` | Paraformer 实时语音识别(websocket 流式) | -| `Transcription.async_call/fetch/wait(model, file_urls)` | 录音文件批量转写,输入音频 URL 列表 | -| `audio.qwen_asr.QwenTranscription` | Qwen 音频转写 | -| `SpeechSynthesizer.call(model, text, sample_rate)` | sambert/cosyvoice 语音合成 | -| `audio.tts_v2.SpeechSynthesizer.call/streaming_call/streaming_complete/streaming_cancel` | CosyVoice2 流式合成,输入文本流 | -| `HttpSpeechSynthesizer.call(model, text)` | HTTP 一次性合成 | -| `audio.qwen_tts.SpeechSynthesizer` | Qwen3-TTS 合成 | -| `audio.qwen_omni.OmniRealtimeConversation` | Omni 实时多模态对话(websocket 回调式) | - -## 微调 / 部署 / 资源 - -| 接口 | 说明 | -| --- | --- | -| `FineTunes.call/cancel/list/get/delete/stream_events/logs/wait` | 微调任务管理,输入训练/验证文件 ID+基座模型 | -| `Deployments.call/list/get/delete/scale` | 微调模型部署与扩缩容,输入 model_name+capacity | -| `Models.list/get(page_no, page_size)` | 查询可用模型列表/详情 | -| `Files.upload(file_path, purpose)/list/get/delete` | 文件上传(微调用)及管理 | - -## Assistants API(百炼智能体)【确定下线:2.0 不再支持】 - -> ⚠️ Assistants/Threads/Messages/Runs/Steps 整个 API 族将下线,dashscope 2.0 不再迁移支持;托管 Agent 场景由 AgentStudio 承接。 - -| 接口 | 说明 | -| --- | --- | -| `Assistants.create/retrieve/list/update/delete` | 助手 CRUD,输入 name/model/instructions/tools | -| `Threads.create/retrieve/update/delete/create_and_run` | 会话线程 CRUD | -| `Messages.create/retrieve/list/update/delete` | 线程内消息 CRUD | -| `Runs.create/create_thread_and_run/retrieve/list/submit_tool_outputs/cancel/wait` | 运行调度与工具结果回填 | -| `Steps.list/retrieve` | 查询运行步骤 | - -## AgentStudio(托管 Agent,`dashscope.agentstudio`) - -独立子 SDK,已是 Client 实例化风格(`Client`/`AsyncClient` + 自有异常体系 + 流式)。 - -| 接口 | 说明 | -| --- | --- | -| `client.agents.create(name, model, ...)` | 创建托管 Agent | -| `client.sessions.create(agent=agent.id)` | 创建会话 | -| `client.sessions.events.send(session.id, [...])` | 发送事件(`user_message` / `user_tool_result` 等) | -| `client.sessions.events.stream(session.id)` | 流式订阅会话事件(上下文管理器,含 `session_status` 等事件类型) | - -## 全局配置 - -| 接口 | 说明 | -| --- | --- | -| `dashscope.api_key` / `save_api_key()` | 密钥设置与持久化 | -| `base_http_api_url` / `base_websocket_api_url` / `base_compatible_api_url` | 端点配置 | -| `close_shared_aio_session()` | 关闭共享异步 HTTP 会话 | - ---- - -# 二、dashscope 自带 CLI(`dashscope` 控制台脚本) - -Typer 实现,全局选项 `-k/--api-key `。 - -## 生成类 - -| 命令 | 说明 | -| --- | --- | -| `dashscope generation create --model --prompt ` | 文本生成,支持流式与采样参数 | -| `dashscope multimodal_conversation create` | 多模态对话,输入含图片/音频/视频 URL 的消息 | -| `dashscope image_synthesis create` | 文生图(万相),输入 prompt+尺寸/数量 | -| `dashscope image_generation create` | 图片生成(另一组模型) | -| `dashscope video_synthesis create` | 文/图生视频,输入 prompt 或首帧图 | -| `dashscope speech_synthesis create` | 语音合成,输入文本+发音人 | -| `dashscope transcription create` | 录音转写,输入音频 URL | -| `dashscope code_generation create` | 代码生成(hidden 别名 call) | -| `dashscope understanding create` | 语义理解(hidden) | -| `dashscope application create` | 百炼应用调用,输入 app_id+prompt | - -## 检索 / 工具类 - -| 命令 | 说明 | -| --- | --- | -| `dashscope embeddings create` | 文本向量化,输入文本列表 | -| `dashscope multimodal_embedding create` | 多模态向量化 | -| `dashscope rerank create` | 重排,输入 query+文档列表 | -| `dashscope tokenization create` | token 计数,输入 prompt | -| `dashscope models list\|get` | 模型列表/详情 | -| `dashscope files upload\|get\|list\|delete` | 文件上传与管理,upload 输入本地路径+purpose | -| `dashscope oss upload` | 上传本地文件到 OSS 换取可访问 URL | - -## 微调 / 部署 / 资源 - -| 命令 | 说明 | -| --- | --- | -| `dashscope ft create` | 创建微调任务,输入训练/验证文件 ID+基座模型 | -| `dashscope ft get\|list\|stream\|cancel\|delete` | 微调任务查询/列表/事件流/取消/删除 | -| `dashscope deployments create` | 部署微调模型,输入 model_name+capacity | -| `dashscope deployments get\|list\|scale\|delete` | 部署查询/列表/扩缩容/删除 | - -(`fine-tunes` 是 `ft` 的隐藏别名) - -## Agentic RL 微调(`dashscope rl`) - -| 命令 | 说明 | -| --- | --- | -| `dashscope rl register_functions --functions ` | 注册 RL 自定义奖励/校验函数 | -| `dashscope rl test_functions` | 测试已注册函数 | -| `dashscope rl upload_data --file ` | 上传 RL 训练数据集 | -| `dashscope rl run --model --dataset ...` | 启动 Agentic RL 训练任务 | -| `dashscope rl get\|cancel\|logs` | 任务查询/取消/日志(输入 job_id) | -| `dashscope rl list` | 列出 RL 训练任务 | - -(`agentic-rl` 是 `rl` 的隐藏别名) - ---- - -# 三、Agentic(dashscope.acli) - -## Agentic SDK(`dashscope.acli.sdk`,包根直接导出) - -| 接口 | 说明 | -| --- | --- | -| `load_config(provider, model, api_key, base_url)` | 加载 acli 配置,入参仅补缺省值(workspace/全局配置与环境变量优先级不变) | -| `create_agent(config, system_prompt, session_id)` | 创建可编程 Agent;session_id 指定时会话持久化到 `.acli/session//history.json` | -| `run_stream(prompt, config, system_prompt, session_id)` | 异步流式跑一轮 agent(含工具调用),yield 文本块 | -| `run_once(prompt, ...)` | 异步单轮执行,返回完整回复文本 | -| `run_once_sync(prompt, ...)` | `run_once` 的同步包装,适合脚本/CI | -| `run_interactive(system_prompt, app_name, default_model, default_provider, api_key, base_url, command, tui, ...)` | 启动交互式 REPL/TUI(acli 的程序化入口) | - -## acli CLI — 启动参数 - -| 参数 | 说明 | -| --- | --- | -| `dashscope [--tui\|--cli\|--dry-run]` | 交互模式选择:TUI / 纯 CLI / 只打印不执行 | -| `-c/--command ` | 一次性执行命令后退出 | -| `--protocol

` | 协议覆盖;`--max-turns N` 限制 agent 轮数 | -| `--version` / `-h` | 版本 / 帮助 | -| `dashscope example ...` | 运行内置示例项目 | - -## acli CLI — 会话与上下文 - -| 命令 | 说明 | -| --- | --- | -| `/session new\|list\|switch\|rename\|fork\|remove\|scene` | 多会话管理;fork 复制会话,scene 管理场景记忆 | -| `/clear` | 清空当前对话历史 | -| `/compress` | LLM 摘要压缩上下文 | -| `/summarize` | 总结当前任务并记录关键步骤 | -| `/history stats\|list\|search\|export\|clear` | 历史统计/全文搜索/导出 | -| `/memory list\|search\|remove\|clear` | 长期记忆条目管理 | -| `/profile list\|search\|add\|remove\|clear` | 用户画像管理 | - -## acli CLI — 模型与能力 - -| 命令 | 说明 | -| --- | --- | -| `/provider` / `/model` | 切换模型服务商/模型 | -| `/capability` | 能力开关目录 | -| `/skill` | 技能管理 | -| `/subagents list\|reload\|enable\|disable\|config` | 子代理管理 | -| `/mcp list\|add\|remove` | MCP 服务连接(stdio 经 config.toml 配置) | -| `/trust` / `/rule` / `/directives` | 权限、规则、指令管理 | - -## acli CLI — 输入输出 - -| 命令 | 说明 | -| --- | --- | -| `/voice on\|off\|model\|silence\|max\|threshold` | 语音输入配置 | -| `/tts on\|off\|speed\|voice\|say\|last` | 语音播报(CosyVoice) | -| `/camera capture\|record` | 拍照/录像输入 | -| `/copy` / `/save [path]` | 复制/保存最近一条回复 | -| `/undo` | 撤销最近一次文件写入/删除(checkpoint) | -| `/theme` | 主题设置;`/json on\|off` JSON 输出 | -| `/log` / `/debug` / `/trace` | 日志与跟踪 | - -## acli CLI — 系统与运维 - -| 命令 | 说明 | -| --- | --- | -| `/cron add\|list\|remove\|pause\|resume` | 定时任务 | -| `/audit recent\|query\|clear` | 审计日志 | -| `/report` | 生成 trace 性能报告 | -| `/setup` | 初始化配置 | -| `/dev` | 模型注册与扩展指南 | -| `/example [restore]` | 示例项目列表/恢复 | -| `/privacy` | 隐私设置 | -| `/info` / `/stats` / `/help` / `/exit` | 运行时信息/统计/帮助/退出 | diff --git a/md/dashscope-sdk-expert-intro.md b/md/dashscope-sdk-expert-intro.md deleted file mode 100644 index 612ac6c6..00000000 --- a/md/dashscope-sdk-expert-intro.md +++ /dev/null @@ -1,92 +0,0 @@ -# DashScope SDK Expert 简介 - -DashScope SDK Expert 是一个面向 **DashScope Python SDK / Java SDK** 与 **阿里云百炼(Model Studio)** 的智能开发助手。它可以提供可运行的代码示例、解释 API 参数与错误码、阅读你的代码定位问题,并直接执行命令验证 SDK 行为——目标是让你**不翻文档、不手写样板代码**,只描述需求就能完成 SDK 开发与问题排查。 - -## 快速开始 - -1. **安装 / 升级 SDK**:DashScope SDK Expert 内置于 DashScope Python SDK,先安装或升级到最新版: - - ```bash - pip install -U dashscope - ``` - -2. **启动 CLI**:在终端执行 `dashscope` 命令,进入交互式会话。 -3. **查看帮助**:在会话中输入 `/help`,即可查看当前可用的全部命令与技能(skill)列表。 -4. **按需提问**:直接用自然语言提问即可,例如: - - “用 qwen-plus 写一个流式对话示例” - - “`InvalidApiKey` 这个错误怎么解决?” - - “帮我看下 `my_app.py` 为什么调用失败” - -> 提示:Expert 会交叉校验本机已安装的 dashscope 版本——版本过旧时先执行 `pip install -U dashscope` 升级,即可获得与最新 SDK 一致的答案。除 `/help` 外,还可通过 `/skill <名称>` 单行触发专项技能(如 `/skill text-generation`、`/skill error-code <错误码>`),具体可用列表以 `/help` 输出为准。 - -## 覆盖能力一览 - -| 领域 | 说明 | -|---|---| -| 文本生成 | Generation、OpenAI 兼容接口 | -| 多模态 | MultiModalConversation、图像合成、视频合成 | -| 语音 | 语音合成(TTS)、录音文件转写(ASR) | -| 检索 | 文本/批量/多模态向量、Rerank、RAG | -| 微调与部署 | SFT/CPT/DPO、独立推理端点、强化学习 | -| Agent | 托管 Agent API、应用调用、Assistants、插件与 MCP | -| CLI | dashscope 命令行全部命令组 | - ---- - -## 最佳实践一:只提需求,不读文档,直接产出可运行代码 - -**痛点**:DashScope SDK 接口多、版本迭代快,开发者经常要翻文档确认参数名、输入格式和模型列表。 - -**做法**:把需求直接用自然语言描述给 DashScope SDK Expert,它会: - -1. 匹配内置的 SDK 知识库(模型列表、参数、输入输出结构、错误码); -2. **交叉校验本机已安装的 SDK 版本**,确保给出的签名与用法与你环境一致(版本不一致时会主动提醒); -3. 直接生成可运行代码,并用 `run_command` 实际执行展示输出。 - -**示例对话**: - -> 用户:写一个 Python 示例,用 qwen-plus 流式输出,统计 token 消耗。 - -助手直接给出完整可运行脚本(messages 格式 + `stream=True` + usage 解析),无需你查任何文档。 - -**小技巧**: - -- 指定语言或框架:“用 Java SDK 实现”“封装成 FastAPI 接口”; -- 指定版本:“我们用的是 dashscope 1.20.x,这个参数支持吗?”——助手会核对安装版本后回答; -- 追问细节:“这个参数的取值范围是什么?”“还有哪些可选模型?” - ---- - -## 最佳实践二:排查问题时,让它自动定位并修复 Bug - -**痛点**:调用报错时,错误码含义不明、报错堆栈指向 SDK 内部,难以判断是代码问题还是参数问题。 - -**做法**:按以下任一路径交给 DashScope SDK Expert: - -1. **贴错误码/报错信息**:如 `Throttling.RateQuota`、`InvalidParameter`,助手会解释含义并给出可执行的修复建议(如调整并发、修改参数格式、申请配额)。 -2. **给出出问题的代码文件路径**:助手会自动读取文件,诊断 SDK 调用问题(常见根因:混用旧版 prompt 格式、messages 结构错误、api_key/workspace 未配置、异步任务未正确轮询等)。 -3. **自动修复闭环**:定位根因后,助手会直接修改文件,并运行测试/脚本验证修复效果,形成 **读取 → 诊断 → 修复 → 验证** 的完整流程。 - -**示例对话**: - -> 用户:运行 `gen.py` 报 `InvalidParameter: url error`,帮我修一下。 - -助手读取 `gen.py` → 发现传入了本地路径而非可访问的 URL → 修改为正确的输入方式 → 重新运行确认通过。 - -**小技巧**: - -- 报错时附上 `request_id`,便于与服务端日志对应; -- 复杂问题可先让助手“解释这段代码逻辑”,确认理解一致后再修复; -- 遇到 SDK 本身的缺陷或未满足的需求,助手会引导你到官方仓库提 Issue: - - Python SDK:https://github.com/dashscope/dashscope-sdk-python/issues - - Java SDK:https://github.com/dashscope/dashscope-sdk-java/issues - ---- - -## 小结 - -| 场景 | 一句话用法 | -|---|---| -| 开始使用 | 执行 `dashscope`,输入 `/help` 查看全部能力 | -| 开发 | 直接描述需求 → 拿到校验过版本的可运行代码 | -| 排障 | 贴报错或给文件路径 → 自动诊断、修复并验证 | diff --git a/md/dashscope-v2-refactor-tasks.md b/md/dashscope-v2-refactor-tasks.md deleted file mode 100644 index 37674fe8..00000000 --- a/md/dashscope-v2-refactor-tasks.md +++ /dev/null @@ -1,195 +0,0 @@ -# DashScope v2 借鉴 Ark 重构任务列表 - -> 目标仓库:`/Users/zhansheng.lzs/ali/pro/ptm/moe-dashscope-sdk-python`(`dashscope/v2/`) -> 依据文档:`ark-vs-dashscope-v2-comparison.md` -> 拆分原则:**一个任务一个 PR**,均可独立合并;除标注的依赖外无隐式耦合。 -> 全局约束:全部改动保持向后兼容(只新增 kwarg / 子类 / 模块,不改既有签名语义);测试放 `tests/v2/`,风格沿用现有 `httpx.MockTransport` 注入。 - -## 任务总览 - -| # | 任务 | 优先级 | 依赖 | 工作量 | 影响面 | -|---|---|---|---|---|---| -| T1 | `RequestOptions` 统一请求定制通道 | ★★★ | 无 | M | transport + 全部 resources | -| T2 | 容错式模型构造 `parse_model()` | ★★★ | T3 | M | types + 全部 resources | -| T3 | `APIResponseValidationError` 异常类 | ★★ | 无 | S | errors.py | -| T4 | 分页抽象 `BasePage` | ★★ | 无 | M | pagination.py + 3 个 list 资源 | -| T5 | 响应封装 `APIResponse.parse(to=)` | ★★ | T1、T2 | L | transport + resources 入口 | -| T6 | 每请求 `x-dashscope-client-request-id` | ★ | 无 | S | _ident.py + http.py | -| T7 | async 孪生漂移治理(unasync) | ★ | 无 | L(分阶段) | 构建/CI | - -推荐执行顺序:**T3 → T6 → T1 → T2 → T4 → T5 → T7**(先小后大,T5 依赖 T1/T2,T7 独立排最后)。 - ---- - -## T1:`RequestOptions` 统一请求定制通道 - -**目标**:借鉴 ark `_request_options.py:44`,给每个资源方法一致的逃生舱 `extra_headers / extra_query / extra_body`,并支持按请求覆盖 `max_retries`。 - -**现状痛点**: -- `SyncTransport.request()` 只接受 `headers/params/timeout/api_key/workspace`(`transport/http.py:156-172`),无 extra 通道;网关要加一个实验性 header 必须改 SDK 发版。 -- `max_retries` 只有 client 级(`http.py:149`),无法按请求关闭(如长任务提交不想重试)。 - -**改动范围**: -1. 新增 `dashscope/v2/_request_options.py`: - - `RequestOptions` dataclass:`extra_headers: Mapping[str,str]`、`extra_query: Mapping[str,Any]`、`extra_body: Mapping[str,Any]`、`timeout`、`max_retries: Optional[int]`。 - - 深合并函数 `_deep_merge(base: dict, extra: dict)`(对齐 ark `_base_client.py:236-243` 语义:dict 递归合并,其余覆盖)。 -2. `transport/http.py`: - - `SyncTransport.request()` / `AsyncTransport.request()` / `open_stream()` 增加 `options: Optional[RequestOptions] = None`。 - - header 合并顺序固定为:`sdk_headers(module)` < auth(api_key/workspace)< 调用方 `headers` < `extra_headers`(后者胜)。 - - `extra_query` 并入 `params`;`extra_body` 与 `json` dict 深合并(`json` 非 dict 时忽略并告警)。 - - 重试循环用 `options.max_retries if options and options.max_retries is not None else self._max_retries`(`http.py:174`、`http.py:223` 两处)。 -3. resources:方法签名统一追加 `extra_headers=None, extra_query=None, extra_body=None`(保留既有 `timeout/api_key/workspace` kwarg 不动);内部组装 `RequestOptions` 传给 transport。从 `chat/completions.py`、`embeddings.py`、`models.py`、`files.py`、`_jobs.py` 做起,其余资源后续跟进(可在 PR 内一次改完,模式相同)。 - -**验收标准**: -- `tests/v2/test_request_options.py`: - - extra_headers 出现在最终请求头且覆盖默认值; - - extra_query 并入 query string;extra_body 深合并(嵌套 dict 合并、标量覆盖); - - `max_retries=0` 时 503 不重试;`max_retries=1` 时重试一次;不设置时走 client 默认; - - 优先级冲突用例:调用方 headers 与 extra_headers 同键时 extra_headers 胜。 - -**风险**:低。纯新增参数;合并顺序需在 docstring 写死,避免后续歧义。 - ---- - -## T2:容错式模型构造 `parse_model()` - -**目标**:借鉴 ark `_models.py` 的 `construct_type` 哲学——服务端新增枚举值、判别字段新值、结构小漂移时不炸用户进程。 - -**现状痛点**:resources 直接 `Model(**data)` 构造(如 `models.py:67-73`),pydantic v2 校验失败会把原始 `ValidationError` 抛给用户;`extra="allow"` 只兜住了"多字段",兜不住"字段值类型漂移"和"Literal/判别新值"(`types/messages.py:30-55` 的 `extra="forbid"` + Literal 判别最脆)。 - -**改动范围**: -1. 新增 `dashscope/v2/types/_parse.py`: - ```python - def parse_model(cls: type[_M], data: Mapping[str, Any], *, strict: bool = False) -> _M - ``` - - 默认路径:`model_validate(data)`;失败时降级为 `cls.model_construct()` + 逐字段 best-effort 赋值(能 validate 的字段 validate,不能的原样塞入),并 `logger.debug` 记录漂移详情(模型名、字段、原始值类型)。 - - `strict=True`:校验失败抛 T3 的 `APIResponseValidationError`(保留 response body 供排障)。 - - 不移植 ark 的 700 行递归强转器,也不做 v1 双栈——pydantic v2 的 `model_construct` 已提供 90% 价值。 -2. 替换 resources 中的裸构造调用(`ModelList(**...)`、`Model(**models[0], ...)` 等,典型位置 `resources/models.py:67-73,157-161`、`resources/files.py`、`resources/_jobs.py`、`resources/chat/completions.py` 的 chunk 解析路径)。 -3. 配置项:`Config` / `Settings` 增加 `strict_validation: bool = False`(env `DASHSCOPE_STRICT_VALIDATION`),让用户可切回严格模式排障。 - -**验收标准**: -- `tests/v2/test_parse_model.py`: - - 未知枚举值、未知 message part `type`、字段类型漂移(int 给了 str)三种场景下默认不抛错、字段可读取、其余字段正常校验; - - `strict=True` 或 `strict_validation` 开启时抛 `APIResponseValidationError`,异常带 body; - - 现有 `tests/v2/` 全部用例不回归。 - -**风险**:中。容错会掩盖真实的服务端回归——必须有 debug 日志 + strict 开关双保险;CHANGELOG 明确行为变化。 - ---- - -## T3:`APIResponseValidationError` 异常类 - -**目标**:把"SDK 解析失败"与"API 调用失败"分成两类,借鉴 ark `_exceptions.py` 的 `ArkAPIResponseValidationError`。 - -**改动范围**: -1. `errors.py` 新增: - ```python - class APIResponseValidationError(DashScopeError): - """Response body failed SDK-side validation (strict mode).""" - ``` - 携带 `response`(httpx.Response 或 None)、`body`、`cause`(原始 `ValidationError`)。 -2. 评估是否在 `_error_registry.py` 的 `SDK_ERRORS` 注册条目(`sdk.response_validation_error`,带 solution 文案)——与错误目录的"SDK 侧错误"约定对齐;若注册则改为继承 `SdkError`。 -3. `errors.py` 顶部 docstring 的"third source"段落补充该类说明。 - -**验收标准**:`tests/v2/test_errors.py` 新增:异常可捕获、`str()` 含 request_id/body 摘要、`isinstance(err, DashScopeError)` 成立、(若注册目录)目录不变量测试通过。 - -**风险**:低。新增类,无既有行为变化。 - ---- - -## T4:分页抽象 `BasePage` - -**目标**:借鉴 ark `pagination.py`,给 list 接口统一的可迭代分页对象。 - -**现状痛点**:`models.list` 返回裸 `ModelList`(`models.py:48-52`,`page_no/page_size/total_count`),翻页要用户手工循环;`files.list`、`jobs.list`(`_jobs.py:182-207`)同构但各写各的。 - -**改动范围**: -1. 新增 `dashscope/v2/pagination.py`: - - `BasePage(Generic[T], ResponseBase)`:字段 `page_no/page_size/total_count`;方法 `has_next_page`(`page_no * page_size < total_count`)、`next_page()`(回调拉下一页,末页返回 None)、`__iter__`(委托 `self.items`)、`iter_pages()`(生成器跨页)。 - - `SyncPage` / `AsyncPage` 两个实现,构造时注入"取页回调"(`Callable[[int, int], BasePage]`),回调由资源方法闭包提供。 -2. 资源接入(保持返回类型向后兼容——让现有 List 模型继承/替换为 Page 子类): - - `models.list` → `ModelList(BasePage[Model])`; - - `files.list` → `FileList` 同理; - - `_jobs.py` 的 `JobList` 同理(注意 `UNKNOWN` 终态语义不受影响)。 -3. `provider` 客户端过滤(`models.py:122-125`)与分页的交互写进 docstring:过滤只作用于当前页,`total_count` 语义不变。 - -**验收标准**: -- `tests/v2/test_pagination.py`:`for m in page` 迭代当前页;`iter_pages()` 跨 3 页聚合且请求参数 page_no 递增;`total_count` 边界(整除、不足一页、空页)正确停止;`next_page()` 末页返回 None。 - -**风险**:低-中。`ModelList` 改继承关系是公开 API 变化但向后兼容(字段全保留);注意 `model_dump()` 序列化结果不要多出内部字段。 - ---- - -## T5:响应封装 `APIResponse.parse(to=)` 与 raw 访问 - -**目标**:借鉴 ark `_response.py`,收敛目前散装的 `check_response` + 手工解析 + `with_headers` 三件套,并提供记忆化的按类型解析。 - -**现状痛点**:每个资源方法重复"request → check_response → response.json() → 构造 → with_headers"五步(`models.py:111-126` 是典型),共十几处;用户拿不到原始 response(无法读非标准响应头、无法自行二次解析)。 - -**改动范围**: -1. 新增 `dashscope/v2/_response.py`: - - `APIResponse`:包装 `httpx.Response` + client 引用;属性 `headers / status_code / request_id / elapsed`;方法 `parse(cast_to=None)`——`cast_to` 为 None 时返回 `response.json()`,否则走 T2 的 `parse_model`,解析结果按 `cast_to` 缓存在 `_parsed_by_type: dict`(对齐 ark `_response.py:305-321` 的记忆化语义)。 - - 同步/异步共用一个类(httpx.Response 在 async 路径同样是已物化的对象,无需孪生)。 -2. transport 新增 `request_api()` 返回 `APIResponse`(内部就是 `request()` + 包装,不重写重试逻辑);`open_stream()` 的 `Stream` 对象补 `headers` 属性(`transport/sse.py` 已有 `Stream.headers`,对齐即可)。 -3. resources 改造分两小步(可同 PR 内两个 commit): - - a) 内部路径切到 `request_api()` + `resp.parse(Model)`,删除重复的 `with_headers` 调用(`parse_model` 内统一挂 headers); - - b) 资源方法增加 `raw: bool = False` kwarg(或 `with_raw_response` 风格),为 True 时返回 `APIResponse` 而非模型。 -4. `with_headers`(`types/common.py:100`)保留为公开工具不删(websocket 来源模型仍在用),docstring 标注 HTTP 路径已内建。 - -**验收标准**: -- `tests/v2/test_response.py`:`parse()` 两次调用返回同一缓存对象;`parse(OtherModel)` 按类型分别缓存;`request_id` 从 `x-request-id` 头提取;`raw=True` 时资源方法返回 `APIResponse` 且不抛 `check_response` 错误。 -- `tests/v2/` 存量用例不回归(重点:headers 字段在异常与成功路径行为不变,`types/common.py:100-116` 文档承诺的语义保持)。 - -**风险**:中。触碰所有资源的公共路径;建议先只切 `models` + `embeddings` 两个资源验证模式,再全量推广(可拆 T5a/T5b 两个 PR)。 - ---- - -## T6:每请求 `x-dashscope-client-request-id` - -**目标**:借鉴 ark `_base_client.py:195` 的 `X-Client-Request-Id`,给每次请求一个客户端生成的 UUID,与服务端 `x-request-id` 互补(网络层失败、请求未到达网关时也有 ID 可查)。 - -**改动范围**: -1. `transport/_ident.py`:新增 `CLIENT_REQUEST_ID_HEADER = "x-dashscope-client-request-id"` 与 `client_request_id() -> str`(`uuid.uuid4().hex`);同样受 `DASHSCOPE_DISABLE_SDK_HEADERS` 开关控制。 -2. `transport/http.py:184` 附近:在每次 `self._client.request()` 调用时生成并注入(**注意放重试循环内每次 attempt 生成一次,或循环外一次**——建议循环外一次:重试属于同一逻辑请求,与服务端 x-request-id 的关联更稳定;决策写进注释)。 -3. debug 日志(`http.py:214`)并排输出 client_request_id 与 x-request-id。 -4. 错误对象:连接层错误(`APIConnectionError`/`APITimeoutError`)无服务端 request_id 时,把 client_request_id 填入 `err.request_id` 便于排障(`http.py:199` 的 `_map_transport_error` 路径)。 - -**验收标准**:`tests/v2/test_ident.py`:header 存在且为 32 位 hex;同客户端两次请求 ID 不同;`DASHSCOPE_DISABLE_SDK_HEADERS` 设置后 header 消失;连接异常时 `err.request_id` 非空。 - -**风险**:低。网关在服务网格中可能剥离未知 header——不影响功能(纯客户端侧排障增强),但先与网关同学确认该 header 名未被占用。 - ---- - -## T7:async 孪生漂移治理(分阶段) - -**目标**:治理 sync/async 人肉复制的漂移风险(`client.py:51 vs 272`、`http.py:128 vs 320`、`_jobs.py`、`resources/*` 全套孪生)。ark 的教训是:机器复制至少保证一致,人肉复制必然漂移。**不上 Stainless**,用轻量方案。 - -**阶段一(本任务内必做):漂移检测** -1. 新增 `tests/v2/test_sync_async_parity.py`:对指定的成对模块(`client` / `transport.http` / `_jobs` / `resources.models` 等),用 `ast` 解析后做归一化比较(剔除 `async/await` 关键字、`Async` 类名前缀、`__aenter__/__aexit__`),方法集合与参数列表必须一致。 -2. CI 挂为必过项——先止血,保证新增方法两端同步。 - -**阶段二(试点):unasync 生成** -1. 评估并引入 [`unasync`](https://github.com/python-trio/unasync)(或等价脚本):以 sync 代码为源,token 替换规则生成 async(`AsyncModels`→`Models` 逆规则、`await `→``、`async def`→`def`)。 -2. 选 1 个简单资源试点(建议 `models.py`:无流式、无 WS,孪生结构最规整,`models.py:91-228`);生成物不手写、不进 review diff(pre-commit/CI 生成并校验一致)。 -3. 试点稳定后推广到 `files.py`、`_jobs.py` 等。 - -**不在范围内**:`transport/ws.py` / `ws_connection.py`(纯同步线程模型,无 async 孪生);`open_stream()`(SSE 的 async 生成规则涉及 `async with`/`__aiter__`,试点后再定规则)。 - -**验收标准**: -- 阶段一:parity 测试能捕获人为制造的漂移(删一个 async 方法,测试红)。 -- 阶段二:试点模块的 async 文件头标注 "GENERATED — DO NOT EDIT",CI 校验生成物与 sync 源一致。 - -**风险**:中-高。触及构建流程;阶段一零风险先行,阶段二试点失败可回退,不影响已合并的阶段一。 - ---- - -## 附:明确不做的事 - -| 事项 | 原因 | -|---|---| -| 引入 Stainless 全量代码生成 | 会冲掉 protocols/Job/WebSocket/OSS 直传等手写差异化能力;ark 自己的生成代码也存在双客户端复制、双序列化约定问题 | -| pydantic v1 兼容层 | v2 只支持 pydantic v2,ark 的 v1 路径已是死代码 | -| `_legacy_response.py` 式新旧双实现长期并存 | ark 的前车之鉴(1000+ 行历史包袱);T5 要求一步切干净 | -| POST 重试幂等区分 | ark 同样没做(`idempotency_key` 是死字段);需网关先支持幂等键,单独立项 | -| mypy/pyright 类型门禁 | 属于两个 SDK 的共同短板,不在"借鉴 ark"范围;如需治理另立任务 | diff --git a/md/error.md b/md/error.md deleted file mode 100644 index b1cd272a..00000000 --- a/md/error.md +++ /dev/null @@ -1,356 +0,0 @@ -# DashScope Python SDK — HTTP API 错误码/消息透传审查 - -## 目标 - -审查 SDK 是否对 HTTP API 输出的错误码(code)和错误消息(message)做了修改,确保原样透传。 - -## 架构概览 - -SDK 有 **两套独立的错误处理管线**: - -1. **核心 SDK 管线** — `dashscope/api_entities/`、`dashscope/common/`、`dashscope/client/`(用于 `dashscope.Generation`、`dashscope.Application` 等) -2. **AgentStudio 管线** — `dashscope/agentstudio/`(AgentStudio 子 SDK,使用 `httpx`) - ---- - -## 正常透传的路径(GOOD) - -以下代码路径都**正确保留**了 API 返回的 `code` 和 `message`: - -### 1. `_handle_error_message()` — 中央 JSON 错误解析器 - -**文件:** `dashscope/common/utils.py`, 行 270–291 - -```python -def _handle_error_message(error, status_code, flattened_output, headers): - code = None - msg = "" - request_id = "" - if flattened_output: - error["status_code"] = status_code - return error - if "message" in error: - msg = error["message"] - if "msg" in error: - msg = error["msg"] - if "code" in error: - code = error["code"] - if "request_id" in error: - request_id = error["request_id"] - return DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - code=code, - message=msg, - headers=headers, - ) -``` - -**结论:** 直接从 API JSON body 提取 `code` 和 `message`,原样透传。也处理了 `"msg"` 作为替代键。 - -### 2. `_handle_http_failed_response()` — sync 非 2xx 处理器 - -**文件:** `dashscope/common/utils.py`, 行 294–336 - -对 JSON 响应完全委托给 `_handle_error_message()`。对非 JSON/非 SSE 响应,`code="Unknown"` 但保留原始 body 作为 `message`。 - -### 3. `_handle_aiohttp_failed_response()` — async 非 2xx 处理器 - -**文件:** `dashscope/common/utils.py`, 行 361–408 - -与 sync 版本相同逻辑,JSON 和 SSE 内容类型委托给 `_handle_error_message()`。 - -### 4. HttpRequest SSE 流错误(sync) - -**文件:** `dashscope/api_entities/http_request.py`, 行 409–418 - -```python -if is_error: - yield DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - output=None, - code=msg["code"] if "code" in msg else None, - message=msg["message"] if "message" in msg else None, - headers=headers, - ) -``` - -直接从 SSE event JSON 提取。 - -### 5. HttpRequest SSE 流错误(async) - -**文件:** `dashscope/api_entities/http_request.py`, 行 299–306 - -```python -if is_error: - yield DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - code=msg["code"], - message=msg["message"], - headers=headers, - ) -``` - -### 6. AioHttpRequest SSE 流错误(async) - -**文件:** `dashscope/api_entities/aiohttp_request.py`, 行 172–178 - -```python -if is_error: - yield DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - code=msg["code"], - message=msg["message"], - ) -``` - -### 7. AioHttpRequest 非 2xx JSON 错误 - -**文件:** `dashscope/api_entities/aiohttp_request.py`, 行 222–246 - -```python -if "application/json" in response.content_type: - error = await response.json() - yield DashScopeAPIResponse( - request_id=request_id, - status_code=response.status, - code=error["code"], - message=message, - ) -``` - -### 8. `_handle_http_stream_response()` — sync stream 错误路径 - -**文件:** `dashscope/common/utils.py`, 行 483–493 - -```python -msg = json.loads(event.data) -yield event.eventType, DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - output=None, - code=msg["code"] if "code" in msg else None, - message=msg["message"] if "message" in msg else None, - headers=headers, -) -``` - -### 9. `DashScopeAPIResponse` — 响应容器 - -**文件:** `dashscope/api_entities/dashscope_response.py`, 行 80–122 - -纯数据容器,不做任何转换。所有 `from_api_response()` 类方法(约 15 个响应类型)都是直接 `code=api_response.code, message=api_response.message` 拷贝。 - -### 10. AgentStudio `from_response()` — 异常构造 - -**文件:** `dashscope/agentstudio/exceptions.py`, 行 177–244 - -当 API 响应 body 包含 `error.code` 和 `error.message` 时,原样保留。fallback(`f"HTTP {status_code}"`、状态码映射)仅在响应 body 缺少结构化错误信息时触发。 - ---- - -## 发现的问题 - -### 问题 1 — StreamEventMixin:错误码和消息被丢弃(HIGH) - -**文件:** `dashscope/client/base_api.py`, 行 1557–1565 - -```python -for is_error, status_code, data in cls._handle_stream(response): - if is_error: - yield DashScopeAPIResponse( - request_id=request_id, - status_code=status_code, - output=None, - code="", # ← 原始 code 丢弃 - message="", # ← 原始 message 丢弃 - ) -``` - -**影响:** 使用 `StreamEventMixin.stream_events()` 的 API(如任务日志流)在流错误时,用户**完全看不到**错误信息。`data` 变量中有完整的 JSON 错误但没有解析。 - -**对比正确代码:** `http_request.py:409-418`(相同模式,但正确解析了 `msg["code"]` 和 `msg["message"]`)。 - -### 问题 2 — WebSocket 握手:错误消息被替换(MODERATE) - -**文件:** `dashscope/api_entities/websocket_request.py`, 行 204–218 - -```python -except aiohttp.WSServerHandshakeError as e: - code = e.status - msg = e.message - if e.status in [HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED]: - msg = "Unauthorized, your api-key is invalid!" # ← 替换了服务端消息 - elif e.status == HTTPStatus.SERVICE_UNAVAILABLE: - msg = SERVICE_503_MESSAGE # ← 替换了服务端消息 - yield DashScopeAPIResponse( - request_id=task_id, - status_code=code, - code=code, # ← HTTP 状态码作为 code,非 API code - message=msg, - ) -``` - -**影响:** WebSocket 鉴权/服务失败时,用户丢失了服务端提供的诊断信息。`code` 被设为 HTTP 状态码整数,而非 API 自身的错误码。 - -### 问题 3 — `iter_over_async` 桥接:包装为自定义格式(LOW) - -**文件:** `dashscope/common/utils.py`, 行 124–131 - -```python -def iter_over_async(ait): - if error is not None: - yield DashScopeAPIResponse( - -1, - "", - "Unknown", - message=f"Error type: {type(error)}, message: {error}", - ) -``` - -**影响:** async→sync 转换异常时,`code="Unknown"`,message 被拼接为自定义格式字符串。这是传输层故障而非 API 错误修改,但会丢失内部异常携带的结构化错误信息。 - -### 问题 4 — 非 JSON 响应:`code="Unknown"`(LOW) - -**文件:** 多处 — `utils.py:215, 322, 333, 388, 405`、`aiohttp_request.py:168, 252`、`http_request.py:294, 404` - -当 API 返回非 JSON/非 SSE 响应(如 HTML 错误页、网关错误)时,`code` 设为 `"Unknown"`,原始 body 保留在 `message` 中。作为 fallback 可接受,但可考虑改为 `code=None` 以明确表示 API 未提供错误码。 - ---- - -## 汇总表 - -| 位置 | Code | Message | 结论 | -|---|---|---|---| -| `_handle_error_message()` (utils.py:270) | 保留 | 保留 | GOOD | -| `_handle_http_failed_response()` JSON (utils.py:300) | 保留 | 保留 | GOOD | -| `_handle_aiohttp_failed_response()` JSON (utils.py:367) | 保留 | 保留 | GOOD | -| HttpRequest SSE sync (http_request.py:409) | 保留 | 保留 | GOOD | -| HttpRequest SSE async (http_request.py:299) | 保留 | 保留 | GOOD | -| AioHttp SSE async (aiohttp_request.py:172) | 保留 | 保留 | GOOD | -| AioHttp 非 2xx JSON (aiohttp_request.py:241) | 保留 | 保留 | GOOD | -| `_handle_http_stream_response()` (utils.py:483) | 保留 | 保留 | GOOD | -| 所有 `from_api_response()` (dashscope_response.py) | 保留 | 保留 | GOOD | -| AgentStudio `from_response()` (exceptions.py:177) | 保留 | 保留 | GOOD | -| **StreamEventMixin** SSE (base_api.py:1559) | **丢弃 → `""`** | **丢弃 → `""`** | **PROBLEM (HIGH)** | -| **WebSocket 握手** (websocket_request.py:207) | HTTP 状态码 | **替换为硬编码字符串** | **PROBLEM (MODERATE)** | -| `iter_over_async` (utils.py:125) | `"Unknown"` | 自定义格式包装 | Minor | -| 非 JSON fallback (多文件) | `"Unknown"` | 保留原始 body | 可接受的 fallback | - ---- - -## 修复建议 - -1. **修复 StreamEventMixin(HIGH)** — `client/base_api.py:1559`:解析 `data` 为 JSON(参考 `http_request.py:409-418` 的做法),而非设置 `code=""` 和 `message=""`。 - -2. **修复 WebSocket 握手(MODERATE)** — `websocket_request.py:208`:保留 `e.message`(服务端消息)而非替换。如需友好提示,应追加而非替换。 - -3. **考虑移除 `code="Unknown"` fallback(LOW)** — 对真正无结构的响应可接受,但可改为 `code=None` 明确表示 API 未提供错误码。 - ---- - -## CLI 层审查(`dashscope/cli/`,分支 dev/sdk-cli-1) - -### 核心错误处理机制 - -CLI 层有 3 个统一的错误处理函数,所有命令模块共用: - -#### 1. `print_failed_message(rsp)` — 通用错误打印 - -**文件:** `dashscope/cli/common.py`, 行 35–41 - -```python -def print_failed_message(rsp): - err_console.print( - f"[red]Failed[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " - f"code: {rsp.code}, message: {rsp.message}", - ) -``` - -**结论:** 直接输出 `rsp.code` 和 `rsp.message`,原样透传。 - -#### 2. `ensure_ok(rsp)` — 统一响应检查与退出 - -**文件:** `dashscope/cli/common.py`, 行 44–91 - -```python -def ensure_ok(rsp, check_business_error=True): - if rsp.status_code != HTTPStatus.OK: - print_failed_message(rsp) # ← 透传 - raise typer.Exit(1) - - if check_business_error: - error_code = output.get("code") - message = output.get("message", "Unknown error") - if error_code and error_code != "": - err_console.print( - f"[red]Business Error[/red] ... code: {error_code}, message: {message}", - ) - raise typer.Exit(1) - return output -``` - -**结论:** HTTP 错误和业务错误都直接透传原始 code/message。`"Unknown error"` 仅作为 message 的 fallback(当 API 未返回 message 时)。 - -#### 3. `handle_sdk_error(action)` — SDK 异常装饰器 - -**文件:** `dashscope/cli/common.py`, 行 110–125 - -```python -def handle_sdk_error(action: str): - def decorator(command_function): - @wraps(command_function) - def wrapper(*args, **kwargs): - try: - return command_function(*args, **kwargs) - except typer.Exit: - raise - except Exception as exception: - error(f"{action}: {exception}") - return wrapper - return decorator -``` - -**结论:** `action` 是操作描述标签(如 "Generation request failed"),`{exception}` 是原始异常消息,不修改 API 错误码/消息。 - -### 各命令模块审查 - -| 模块 | 错误处理方式 | 结论 | -|---|---|---| -| `generation.py` | `print_failed_message(rsp)` | GOOD | -| `fine_tunes.py` | `ensure_ok` + `print_failed_message` | GOOD | -| `files.py` | `ensure_ok` | GOOD | -| `deployments.py` | `ensure_ok` + `print_failed_message` | GOOD | -| `models.py` | `ensure_ok` | GOOD | -| `embeddings.py` | `ensure_ok` | GOOD | -| `application.py` | `ensure_ok` | GOOD | -| `image_synthesis.py` | `ensure_ok` | GOOD | -| `image_generation.py` | `ensure_ok` | GOOD | -| `multimodal_conversation.py` | `ensure_ok` | GOOD | -| `multimodal_embedding.py` | `ensure_ok` | GOOD | -| `rerank.py` | `ensure_ok` | GOOD | -| `tokenization.py` | `ensure_ok` | GOOD | -| `understanding.py` | `ensure_ok` | GOOD | -| `transcription.py` | `ensure_ok` | GOOD | -| `speech_synthesis.py` | `print_failed_message` | GOOD | -| `video_synthesis.py` | `ensure_ok` | GOOD | -| `oss.py` | `error()` 自定义消息(非 API 错误,本地校验) | GOOD | -| `code_generation.py` | `ensure_ok` | GOOD | - -### 轻微注意点 - -**agentic_rl.py** — 用自定义格式拼接错误: - -```python -raise OutputError(f"API returned status {result.status_code}: {result.message}") -``` - -原始 `message` 保留在拼接字符串中未修改,但 `result.code`(API 业务错误码)未单独展示。影响较小,因为 `status_code` 和 `message` 都已包含。 - -### CLI 层结论 - -**CLI 层没有修改 API 的 error code 和 error message,全部原样透传。** 所有命令通过 `print_failed_message` 和 `ensure_ok` 两个统一入口处理错误,直接输出 SDK 返回的 `rsp.code` 和 `rsp.message`。 diff --git a/md/experiences.jsonl b/md/experiences.jsonl deleted file mode 100644 index e9c37fd9..00000000 --- a/md/experiences.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"task": "工具连续失败", "tools": ["read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file"], "outcome": "failure", "lesson": "连续 4 次失败 (read_file),需要换策略"} -{"task": "查看当前git 代码:是否修正了以下问题:\n\n问题 1 — StreamEventMixin:错误码和消息被丢弃(HIGH)\n文件: dashscope/client/base_api.py, 行 1", "tools": ["read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "search_files", "search_files", "list_directory", "search_files", "run_command", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file", "read_file"], "outcome": "failure", "lesson": ""} -{"task": "pr:https://github.com/dashscope/dashscope-sdk-python/pull/162", "tools": ["mcp_connect", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} -{"task": "继续修复下", "tools": ["run_command", "read_file", "read_file", "write_file", "run_command", "run_command", "run_command", "run_command", "run_command", "read_file", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} -{"task": "问题4全部修复?刚才还提示部分修复", "tools": ["read_file"], "outcome": "success", "lesson": ""} -{"task": "CLI部分:CLI 层有 3 个统一的错误处理函数,所有命令模块共用:\n1. print_failed_message(rsp) — 通用错误打印\n文件: dashscope/cli/common.p", "tools": ["read_file", "read_file", "read_file", "search_files"], "outcome": "failure", "lesson": ""} -{"task": "commit & push", "tools": ["run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "success", "lesson": ""} -{"task": "https://github.com/dashscope/dashscope-sdk-python/pull/162 再check下是否都修复了", "tools": ["mcp_connect", "list_directory", "run_command", "read_file", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "read_file", "read_file", "read_file", "read_file", "run_command", "run_command", "read_file", "read_file", "run_command", "run_command", "read_file", "run_command", "read_file", "run_command", "run_command", "run_command", "run_command", "read_file", "read_file", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command", "run_command"], "outcome": "failure", "lesson": ""} diff --git a/md/m0-1x-cli-hotfix.md b/md/m0-1x-cli-hotfix.md deleted file mode 100644 index 81c016fc..00000000 --- a/md/m0-1x-cli-hotfix.md +++ /dev/null @@ -1,24 +0,0 @@ -# M0 — 1.x CLI 快修清单 - -> 从 `md/dashscope-2.0-design.md` 拆出:M0 是对 **1.x** 的修复,在 1.x 维护分支单独实施,**不在 V2 分支**。 -> 背景依据:`md/cli-source-level-comparison-2026-08.md` §5"立即修"清单。 -> 完成后 1.x 进入仅维护期(只修 bug/安全补丁,不再有任何功能性改动;新能力一律进 2.0)。 - ---- - -## 任务清单(每项 ≈ 一次 PR) - -| # | 任务 | 涉及文件 | 验收 | -| --- | --- | --- | --- | -| 0.1 | 修流式分支出错仍 exit 0 的 bug:错误后 `raise typer.Exit(1)` | `cli/generation.py:172-189`(同类流式命令逐一排查) | 流式中途注入错误,退出码=1 | -| 0.2 | 开启 shell 补全:`add_completion=True` + README 安装说明 | `cli/__init__.py:244` | bash/zsh/fish 补全可用 | -| 0.3 | 新增 `dashscope auth whoami/login/logout`,whoami 以退出码表达 key 有效性 | 新 `cli/auth.py` | 有效 key 退出 0,无效退出 2 | -| 0.4 | 流式输出改增量纯文本(TTY);非 TTY 输出 JSON 行 | `cli/generation.py` 等生成类命令 | 管道 `| jq` 可解析 | -| 0.5 | 退出码分级(0 成功 / 1 服务端 / 2 认证 / 3 参数 / 4 限流)落到 `cli/common.py` | `cli/common.py` | CI 用例覆盖各错误类别 | - -## 说明 - -- **为什么先做**:五项均为"小改动、高回报",不依赖 2.0 架构;其中 0.1 是 CI 会误判成功的真实缺陷(对比调研实锤)。 -- **边界**:只做修复与体验补齐,**不引入新 API 能力**(遵循"1.x 仅维护"策略);0.3/0.5 属契约修复而非新功能。 -- **与 2.0 的关系**:2.0 方案(`dashscope-2.0-design.md`)里程碑从 M1 开始;M0 的产出(auth/config 习惯、退出码约定)会被 CLI 2.0(M4)继承。 -- **风险提示**:0.4 改变了默认输出格式(dict repr → 纯文本/JSON),可能影响解析旧输出的存量脚本;建议加 `DASHSCOPE_CLI_LEGACY_OUTPUT=1` 回退开关并提前公告。 diff --git a/md/research-openai-anthropic-references.md b/md/research-openai-anthropic-references.md deleted file mode 100644 index f176c584..00000000 --- a/md/research-openai-anthropic-references.md +++ /dev/null @@ -1,78 +0,0 @@ -# 调研:OpenAI/Athropic SDK/CLI - -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 参考 - -### OpenAI - -- OpenAI SDK:https://developers.openai.com/api/docs/libraries -- OpenAI CLI:https://developers.openai.com/api/docs/libraries/openai-cli -- OpenAI python Git:https://github.com/openai/openai-python -- OpenAI Agents python:https://github.com/openai/openai-agents-python - - https://github.com/openai/openai-agents-python/blob/main/docs/quickstart.md -- OpenAI Skills:https://github.com/openai/ - -### Anthropic - -- Anthropic SDK:https://github.com/anthropics/anthropic-sdk-python -- Docs:https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/python -- CLI:https://platform.claude.com/docs/en/cli-sdks-libraries/cli/using -- Anthropic Claude API:https://platform.claude.com/docs/en/api/overview -- Anthropic Claude-code:https://github.com/anthropics/claude-code -- Docs:https://code.claude.com/docs/zh-CN/overview -- Agent SDK:https://code.claude.com/docs/zh-CN/agent-sdk/overview -- CLI:https://code.claude.com/docs/zh-CN/cli-reference -- Cookbooks:https://github.com/anthropics/claude-cookbooks -- Anthropic Skills:https://github.com/anthropics/skills/tree/main -- Anthropic Prompts:https://github.com/anthropics/prompt-eng-interactive-tutorial - -### Stainless - -- https://www.stainless.com/docs/ - -### oapi-codegen - -- https://github.com/oapi-codegen/oapi-codegen - ---- - -## 现状 - -三者深度对比: - -- https://alidocs.dingtalk.com/i/nodes/YQBnd5ExVEjea40qCgXO1470JyeZqMmz -- https://alidocs.dingtalk.com/i/nodes/N7dx2rn0JbxOaqnACZDa3wRpWMGjLRb3 - -结论: - -- https://alidocs.dingtalk.com/i/nodes/m9bN7RYPWdyrPBREcKpOnzwgVZd1wyK0 -- dashscope vs openai - - https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoxKPoqwCB7dobYDV1waOeDk -- dashscope vs athropic - - https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoxKPoqwCB7dobYDV1waOeDk - ---- - -## 三个对比分析 - -### 三者对比总结(OpenAI / Anthropic / DashScope) - -| 维度 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 上手体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | -| API 简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | -| 流式输出 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | -| Tool Use | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | -| 错误处理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | -| 多模态支持 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 模型多样性 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 工程化程度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | - -**一句话总结**: - -- **OpenAI**:上手最快,API 最简洁(output_text 一行直达) -- **Anthropic**:工程化程度最高(异常处理、自动重试、Tool 装饰器、流式助手) -- **DashScope**:模型多样性最强(文本/图像/视频/语音全覆盖),但需要在异常处理、Client 实例化、Tool Use 简化 三个方向补齐短板 diff --git a/md/sdk-comparison-anthropic-vs-dashscope.md b/md/sdk-comparison-anthropic-vs-dashscope.md deleted file mode 100644 index 2b6e2938..00000000 --- a/md/sdk-comparison-anthropic-vs-dashscope.md +++ /dev/null @@ -1,307 +0,0 @@ -# Anthropic Python SDK vs DashScope SDK 对比分析 - -> 基于 Anthropic Python SDK 文档 和 DashScope SDK 源码的深度对比 -> 生成时间:2026-07-31 -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 1. 初始化方式 - -| 维度 | Anthropic | DashScope | -| --- | --- | --- | -| API Key | `Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))` 或自动读取环境变量 | `dashscope.api_key = "..."` 全局变量,或每次调用传入 | -| Client 实例 | `client = Anthropic()` — 面向对象,创建实例 | `Generation.call(...)` — 全静态类方法,无需实例化 | -| Base URL | `client = Anthropic(base_url="...")` | `dashscope.base_http_api_url = "..."` 全局变量 | -| 异步 Client | `client = AsyncAnthropic()` — 独立的异步类 | `AioGeneration` 等独立的异步类 | - -**评价**:Anthropic 的 Client 实例模式明显更优 — 可以创建多个不同配置的 client(不同 key/不同 region),支持依赖注入自定义 HTTP client(httpx/aiohttp)。DashScope 的全局变量方式在多租户场景下容易出错。 - ---- - -## 2. 最简单的调用(Hello World) - -**Anthropic** - -```python -from anthropic import Anthropic - -client = Anthropic() -message = client.messages.create( - max_tokens=1024, - messages=[ - {"role": "user", "content": "Hello, Claude"} - ], - model="claude-opus-5", -) - -for block in message.content: - if block.type == "text": - print(block.text) -``` - -**DashScope** - -```python -import dashscope - -response = dashscope.Generation.call( - model="qwen-turbo", - messages=[{"role": "user", "content": "Hello"}] -) - -print(response.output.choices[0].message.content) -``` - -**评价**: - -- Anthropic 需要遍历 `message.content` blocks,稍显繁琐,但支持多模态输出(图片、工具调用等) -- DashScope 路径更深:`output.choices[0].message.content` 四层穿透 -- 两者都不如 OpenAI 的 `response.output_text` 一行直达 - ---- - -## 3. 流式输出 - -**Anthropic** - -```python -# 方式一:基础流式 -stream = client.messages.create( - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], - model="claude-opus-5", - stream=True, -) -for event in stream: - print(event.type) - -# 方式二:高级流式助手(推荐) -async with client.messages.stream( - max_tokens=1024, - messages=[{"role": "user", "content": "Say hello"}], - model="claude-opus-5", -) as stream: - async for text in stream.text_stream: - print(text, end="", flush=True) - message = await stream.get_final_message() -``` - -**DashScope** - -```python -responses = Generation.call( - model="qwen-turbo", - messages=[...], - stream=True, - incremental_output=True, # 需要手动设置 -) -for response in responses: - print(response.output.choices[0].message.content, end="") -``` - -**评价**:Anthropic 的流式体验明显更好: - -- 提供 `stream.text_stream` 直接输出文本(无需解析 event type) -- `get_final_message()` 获取完整消息 -- 支持 context manager 管理资源 -- DashScope 需要额外理解 `incremental_output` 参数,且路径深 - ---- - -## 4. Function Calling / Tool Use - -**Anthropic** - -```python -from anthropic import Anthropic, beta_tool - -client = Anthropic() - -@beta_tool -def get_weather(location: str) -> str: - """Get the weather for a given location. - - Args: - location: The city and state, for example, San Francisco, CA - - Returns: - A JSON-encoded string with location, temperature, and weather condition. - """ - return json.dumps({"location": location, "temperature": "68°F", "condition": "Sunny"}) - -# 使用 tool_runner 自动处理工具调用 -runner = client.beta.messages.tool_runner( - max_tokens=1024, - model="claude-opus-5", - tools=[get_weather], - messages=[{"role": "user", "content": "What is the weather in SF?"}], -) - -for message in runner: - print(message) -``` - -**DashScope** - -```python -tools = [{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get the weather for a location", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City and state"} - }, - "required": ["location"] - } - } -}] - -response = Generation.call( - model="qwen-turbo", - messages=[{"role": "user", "content": "SF weather?"}], - tools=tools, -) - -# 需要手动解析 tool_calls 并调用函数 -tool_calls = response.output.choices[0].message.tool_calls -``` - -**评价**:Anthropic 大幅领先: - -- `@beta_tool` 装饰器:从函数签名和 docstring 自动生成 schema -- `tool_runner`:自动处理工具调用循环(调用 → 返回结果 → 再次调用模型) -- DashScope 需要手动定义 JSON schema,手动解析 tool_calls,手动循环调用 - ---- - -## 5. 异步任务(图像生成等) - -**Anthropic**:没有暴露异步任务概念,调用就是同步/流式。 - -**DashScope** - -```python -# 提交任务 -resp = ImageSynthesis.async_call(model="wanx-v1", prompt="...") -# 轮询状态 -while True: - result = ImageSynthesis.fetch(resp.output.task_id) - if result.output.task_status == "SUCCEEDED": - break -``` - -**评价**:DashScope 的异步任务模式增加了使用复杂度,但这是服务端架构决定的(图像生成耗时长),属于合理设计。Anthropic 主要做文本生成,不需要此类功能。 - ---- - -## 6. 错误处理 - -**Anthropic** - -```python -import anthropic - -try: - message = client.messages.create(...) -except anthropic.APIConnectionError as e: - print("The server could not be reached") -except anthropic.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") -except anthropic.APIStatusError as e: - print(f"Status code: {e.status_code}") -``` - -提供完整的异常类型层级: - -- BadRequestError (400) -- AuthenticationError (401) -- PermissionDeniedError (403) -- NotFoundError (404) -- ConflictError (409) -- UnprocessableEntityError (422) -- RateLimitError (429) -- InternalServerError (5xx) -- APIConnectionError (网络错误) - -**DashScope** - -```python -response = Generation.call(...) -if response.status_code != 200: - print(f"Error: {response.code}, {response.message}") - print(f"Request ID: {response.request_id}") -``` - -**评价**:Anthropic 明显更优: - -- 异常驱动的错误处理,符合 Python 惯例 -- 细粒度的异常类型,可以针对不同错误做不同处理 -- DashScope 使用返回值检查(status_code + code + message),容易遗漏错误处理 - ---- - -## 7. 高级功能 - -**Anthropic** - -| 功能 | 说明 | -| --- | --- | -| Token 计数 | `client.messages.count_tokens(...)` 请求前预计算 token 数 | -| Message Batches | 批量处理 API,支持异步大批量请求 | -| File Uploads | 支持 PathLike、bytes tuple、BinaryIO 等多种上传方式 | -| Request ID | `message._request_id` 便于调试和报告 | -| 自动重试 | 408/409/429/5xx 自动重试 2 次,指数退避 | -| 多平台支持 | Bedrock、Vertex、AWS、Microsoft Foundry 多平台 Client | -| HTTP Client 定制 | 支持 httpx/aiohttp 自定义 HTTP client | - -**DashScope** - -| 功能 | 说明 | -| --- | --- | -| 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖 | -| 异步任务 | async_call + fetch 模式对长耗时任务实用 | -| 多模态格式 | `{"text": ..., "image": ...}` 直观简洁 | -| Session 管理 | 支持 WebSocket、SSE 等多种连接方式 | - -**评价**:Anthropic 在工程化程度上领先(token 计数、批量 API、自动重试、多平台),DashScope 在模型多样性上有优势。 - ---- - -## 总结:DashScope 可改进的方向 - -### 🔴 高优先级 - -| # | 改进点 | 现状 | 建议 | -| --- | --- | --- | --- | -| 1 | 异常处理 | 返回值检查 `if response.status_code != 200` | 添加异常驱动错误处理:APIError、RateLimitError、AuthenticationError 等 | -| 2 | Client 实例化 | 全局变量 `dashscope.api_key` | 支持 `DashScope(api_key=..., base_url=...)` 实例,多租户友好 | -| 3 | 流式体验 | 需要 `incremental_output` 参数 + 深层路径 | 提供 `client.messages.stream()` 高级 API,直接输出文本 | - -### 🟡 中优先级 - -| # | 改进点 | 现状 | 建议 | -| --- | --- | --- | --- | -| 4 | Tool Use 简化 | 手动定义 JSON schema + 手动循环调用 | 添加 `@tool` 装饰器 + tool_runner 自动循环 | -| 5 | 自动重试 | 无内置重试机制 | 添加可配置的自动重试(429/5xx 等) | -| 6 | Token 计数 | 无预计算功能 | 添加 `Generation.count_tokens(...)` API | -| 7 | HTTP Client 定制 | 内部封装 httpx | 支持传入自定义 HTTP client(httpx/aiohttp) | - -### 🟢 低优先级(DashScope 做得好的地方) - -| # | 亮点 | 说明 | -| --- | --- | --- | -| 1 | 多模态 content 格式 | `{"text": ..., "image": ...}` 比 Anthropic 的 block 结构更直观 | -| 2 | 内置异步任务 | async_call + fetch 模式对长耗时任务(图像/视频)很实用 | -| 3 | 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖,Anthropic 主要是文本 | - ---- - -## 参考资源 - -- Anthropic Python SDK -- OpenAI Python SDK -- DashScope SDK 源码:dashscope-sdk-python-part/ diff --git a/md/sdk-comparison-openai-vs-dashscope.md b/md/sdk-comparison-openai-vs-dashscope.md deleted file mode 100644 index d31127a7..00000000 --- a/md/sdk-comparison-openai-vs-dashscope.md +++ /dev/null @@ -1,206 +0,0 @@ -# OpenAI SDK vs DashScope SDK 对比分析 - -> 基于 OpenAI Quickstart 文档和 DashScope SDK 源码的深度对比 -> 生成时间:2026-07-30 -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 1. 初始化方式 - -| 维度 | OpenAI | DashScope | -| --- | --- | --- | -| API Key | 环境变量 `OPENAI_API_KEY` 自动读取,或传入 api_key 参数 | 环境变量 `DASHSCOPE_API_KEY` 自动读取,或每次调用传入 | -| Client 实例 | `client = OpenAI()` — 面向对象,创建实例 | `Generation.call(...)` — 全静态类方法,无需实例化 | -| Base URL | `client = OpenAI(base_url="...")` | `dashscope.base_http_api_url = "..."` 全局变量 | - -**评价**:OpenAI 的 Client 实例模式更易用 — 可以创建多个不同配置的 client(不同 key/不同 region),DashScope 的全局变量方式在多租户场景下容易出错。 - ---- - -## 2. 最简单的调用(Hello World) - -**OpenAI(Responses API)** - -```python -from openai import OpenAI -client = OpenAI() -response = client.responses.create( - model="gpt-5.6", - input="Write a one-sentence bedtime story about a unicorn." -) -print(response.output_text) # 一行拿到文本 -``` - -**DashScope** - -```python -import dashscope -response = dashscope.Generation.call( - model="qwen-turbo", - messages=[{"role": "user", "content": "写一个关于独角兽的睡前故事"}] -) -print(response.output.choices[0].message.content) # 路径很深 -``` - -**评价**:OpenAI 明显更简洁。`response.output_text` 一行直达,DashScope 需要 `output.choices[0].message.content` 四层穿透。 - ---- - -## 3. 流式输出 - -**OpenAI** - -```python -stream = client.responses.create( - model="gpt-5.6", - input="Say hello", - stream=True, -) -for event in stream: - print(event) -``` - -**DashScope** - -```python -responses = Generation.call( - model="qwen-turbo", - messages=[...], - stream=True, - incremental_output=True, -) -for response in responses: - print(response.output.choices[0].message.content, end="") -``` - -**评价**:两者流式接口相似,但 DashScope 需要额外理解 `incremental_output` 参数(增量 vs 累积输出),OpenAI 的 Responses API 天然就是增量的,零配置。 - ---- - -## 4. 图片/多模态输入 - -**OpenAI** - -```python -response = client.responses.create( - model="gpt-5.6", - input=[{ - "role": "user", - "content": [ - {"type": "input_text", "text": "What is in this image?"}, - {"type": "input_image", "image_url": "https://..."}, - ], - }], -) -print(response.output_text) -``` - -**DashScope** - -```python -from dashscope import MultiModalConversation -response = MultiModalConversation.call( - model="qwen-vl-max", - messages=[{ - "role": "user", - "content": [ - {"text": "这张图片里有什么?"}, - {"image": "https://..."}, - ], - }], -) -``` - -**评价**:DashScope 的 `{"text": ..., "image": ...}` 格式比 OpenAI 的 `{"type": "input_text", ...}` 更直观简洁,这是 DashScope 做得好的地方。 - ---- - -## 5. Function Calling / Tool Use - -**OpenAI** - -```python -tools = [{"type": "function", "name": "get_weather", ...}] -response = client.responses.create( - model="gpt-5.6", - input=[{"role": "user", "content": "Paris weather?"}], - tools=tools, -) -print(response.output[0]) # tool call -``` - -**DashScope** - -```python -tools = [{"type": "function", "name": "get_weather", ...}] -response = Generation.call( - model="qwen-turbo", - messages=[{"role": "user", "content": "Paris weather?"}], - tools=tools, -) -print(response.output.choices[0].message.tool_calls) -``` - -**评价**:接口几乎一致,OpenAI 的 `response.output[0]` 稍简洁。 - ---- - -## 6. 异步任务(图像生成等) - -**OpenAI**:没有暴露异步任务概念,调用就是同步/流式。 - -**DashScope** - -```python -# 提交任务 -resp = ImageSynthesis.async_call(model="wanx-v1", prompt="...") -# 轮询状态 -while True: - result = ImageSynthesis.fetch(resp.output.task_id) - if result.output.task_status == "SUCCEEDED": - break -``` - -**评价**:DashScope 的异步任务模式增加了使用复杂度,但这是服务端架构决定的(图像生成耗时长),属于合理设计。OpenAI 的 DALL-E 也是同步返回,不需要 task 管理。 - ---- - -## 总结:DashScope 可改进的方向 - -### 🔴 高优先级 - -| # | 改进点 | 现状 | 建议 | -| --- | --- | --- | --- | -| 1 | 响应路径太深 | `resp.output.choices[0].message.content` | 添加便捷属性 `resp.output_text`,像 OpenAI 一样一行取到文本 | -| 2 | 缺少 Client 实例化 | 全局变量 `dashscope.api_key` | 支持 `DashScope(api_key=..., base_url=...)` 实例,多租户友好 | -| 3 | Quickstart 体验 | 无类似 OpenAI 的 5 行代码上手 | 提供 `dashscope.chat(model, prompt)` 极简 API 或 `from dashscope import quick` 快捷入口 | - -### 🟡 中优先级 - -| # | 改进点 | 现状 | 建议 | -| --- | --- | --- | --- | -| 4 | incremental_output 默认值 | 流式需手动设 `incremental_output=True` | 默认改为 True,累积输出几乎没人用 | -| 5 | 错误处理 | status_code + code + message 分散检查 | 添加 `response.raise_for_status()` 或自动抛异常模式 | -| 6 | 类型提示不完整 | `**kwargs` 透传大量参数,IDE 无提示 | 将常用参数(stream、tools 等)都写成显式签名参数(已部分做) | -| 7 | 统一 API 入口 | Generation / MultiModalConversation / ImageSynthesis 分散 | 参考 OpenAI 的 `client.responses.create()` 统一入口,内部路由到不同模型 | - -### 🟢 低优先级(DashScope 做得好的地方) - -| # | 亮点 | 说明 | -| --- | --- | --- | -| 1 | 多模态 content 格式 | `{"text": ..., "image": ...}` 比 OpenAI 的 `{"type": "input_text", ...}` 更直观 | -| 2 | 内置异步任务 | async_call + fetch 模式对长耗时任务(图像/视频)很实用 | -| 3 | 丰富的模型类型 | 文本/图像/视频/语音/Embedding/Rerank 全覆盖,OpenAI 相对较少 | - -**一句话总结** - -OpenAI 的 SDK 在"上手体验"和"API 简洁度"上领先(Client 实例化 + output_text 快捷属性 + 统一 Responses API),DashScope 在多模态格式和异步任务上有自己的优势,但需要补齐 便捷属性、Client 模式 和 Quickstart 体验 这三个短板。 - ---- - -## 参考资源 - -- OpenAI Quickstart -- OpenAI Python SDK -- DashScope SDK 源码:dashscope-sdk-python-part/ diff --git a/md/sdk-deep-comparison-openai-anthropic-dashscope.md b/md/sdk-deep-comparison-openai-anthropic-dashscope.md deleted file mode 100644 index 2d71c613..00000000 --- a/md/sdk-deep-comparison-openai-anthropic-dashscope.md +++ /dev/null @@ -1,1126 +0,0 @@ -# SDK 深度对比:OpenAI vs Anthropic vs DashScope - -> 基于源码级分析 -> 生成时间:2026-07-31 -> OpenAI SDK v1.x | Anthropic SDK v0.39+ | DashScope SDK v1.x -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - -## 目录 - -1. 用户接口友好度 -2. 错误码与异常处理 -3. 功能使用:流式/非流式/同步/异步 -4. 架构设计哲学 -5. DashScope SDK 改进建议 - ---- - -## 1. 用户接口友好度 - -### 1.1 最简调用对比 - -**OpenAI — 极致简洁** - -```python -from openai import OpenAI - -client = OpenAI() # api_key 从环境变量读取 - -response = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], -) - -print(response.choices[0].message.content) -``` - -亮点: - -- 零配置即可使用(环境变量 `OPENAI_API_KEY`) -- `client.chat.completions.create()` — 点号链式调用,IDE 自动补全完美 -- Pydantic 模型,所有字段都有类型提示 -- `response.choices[0].message.content` — 属性访问,不是 dict 索引 - -**Anthropic — 同样简洁** - -```python -from anthropic import Anthropic - -client = Anthropic() # api_key 从环境变量读取 - -message = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], -) - -print(message.content[0].text) -``` - -亮点: - -- 与 OpenAI 几乎相同的模式 -- `message.content[0].text` — 直接返回结构化 content blocks -- Pydantic 模型,完整的类型提示 - -**DashScope — 多层抽象** - -```python -import dashscope -from dashscope import Generation - -dashscope.api_key = "sk-xxx" # 必须手动设置 - -response = Generation.call( - model="qwen-plus", - messages=[{"role": "user", "content": "Hello"}], -) - -# 需要先检查 status_code -if response.status_code == 200: - print(response.output.choices[0].message.content) -else: - print(f"Error: {response.code} - {response.message}") -``` - -问题: - -- 需要手动设置 api_key(虽然支持环境变量,但不如 OpenAI 自然) -- `Generation.call()` — 类方法调用,不支持 IDE 点号链式补全 -- 返回 `DashScopeAPIResponse` 是 dict 子类(DictMixin),不是 Pydantic 模型 -- 成功/失败通过 status_code 判断,不抛异常 — 调用方必须每次检查 - -### 1.2 Client 模式对比 - -| 特性 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| Client 实例化 | `OpenAI(api_key=...)` | `Anthropic(api_key=...)` | 无 Client 模式,使用类方法 | -| API Key 来源 | 环境变量 / 参数 / 文件 | 环境变量 / 参数 | 模块变量 / 环境变量 | -| Base URL 覆盖 | `base_url="..."` | `base_url="..."` | `dashscope.base_http_api_url` | -| 连接池 | httpx.Client 复用 | httpx.Client 复用 | 每次请求新建 requests.Session() | -| 默认超时 | 10 min(connect 5s) | 10 min(connect 5s) | 300s | -| 最大重试 | 2 次(指数退避) | 2 次(指数退避) | 1 次(仅 5xx 重试) | -| IDE 自动补全 | ✅ 完美 | ✅ 完美 | ⚠️ 部分(类方法 + dict 返回) | - -### 1.3 响应对象对比 - -| 特性 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 响应类型 | Pydantic BaseModel | Pydantic BaseModel | DictMixin(dict 子类) | -| 属性访问 | `response.choices[0].message.content` | `message.content[0].text` | `response.output.choices[0].message.content` | -| 字典访问 | `response.model_dump()` | `message.model_dump()` | `response["output"]` | -| 序列化 | `response.model_dump_json()` | `message.model_dump_json()` | `str(response)` → JSON | -| 类型安全 | ✅ 完整 Pydantic 验证 | ✅ 完整 Pydantic 验证 | ⚠️ 无验证(dict 无 schema) | -| 嵌套深度 | `choices[0].message.content` | `content[0].text` | `output.choices[0].message.content`(多一层 output) | -| `__repr__` | 美观的 Pydantic 格式 | 美观的 Pydantic 格式 | `DictMixin({...})` 原始 dict | - -### 1.4 API 设计风格对比 - -| 方面 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| API 风格 | REST 资源嵌套 (`client.chat.completions`) | REST 扁平 (`client.messages`) | 类方法扁平 (`Generation.call()`) | -| 方法命名 | create / list / retrieve / delete | create / list / retrieve / delete | call / list / get / delete / update | -| 参数传递 | 命名参数 + Pydantic 验证 | 命名参数 + Pydantic 验证 | `**kwargs` 大量使用,无验证 | -| 模型名传递 | 字符串 | 字符串 | 字符串 | -| Headers | `default_headers` 属性 | `default_headers` 属性 | `default_headers()` 函数 | - ---- - -## 2. 错误码与异常处理 - -### 2.1 错误处理哲学 - -| SDK | 哲学 | 模式 | -| --- | --- | --- | -| OpenAI | 失败即抛异常 | try/except 捕获,异常类型区分错误类别 | -| Anthropic | 失败即抛异常 | 同 OpenAI,但异常层次更扁平 | -| DashScope | 返回错误对象 | `if response.status_code != 200` 检查,不抛异常 | - -### 2.2 异常类层次 - -**OpenAI** - -``` -openai.OpenAIError (基类) -├── openai.APIError # 通用 API 错误 -├── openai.APIConnectionError # 连接失败 -│ └── openai.APITimeoutError # 超时 -├── openai.RateLimitError # 429 限流 -├── openai.BadRequestError # 400 请求错误 -├── openai.AuthenticationError # 401 认证失败 -├── openai.PermissionDeniedError # 403 权限不足 -├── openai.NotFoundError # 404 资源不存在 -├── openai.ConflictError # 409 冲突 -├── openai.UnprocessableEntityError # 422 参数错误 -├── openai.InternalServerError # 500 服务器错误 -└── openai.LengthFinishReasonError # 生成被截断 -``` - -异常属性: - -```python -except openai.APIError as e: - e.status_code # HTTP 状态码 - e.message # 错误消息 - e.body # 响应体(dict) - e.request_id # 请求 ID - e.code # 错误码 - e.headers # 响应头 -``` - -**Anthropic** - -``` -anthropic.AnthropicError (基类) -├── anthropic.APIError # 通用 API 错误 -│ ├── anthropic.BadRequestError # 400 -│ ├── anthropic.AuthenticationError # 401 -│ ├── anthropic.PermissionDeniedError # 403 -│ ├── anthropic.NotFoundError # 404 -│ ├── anthropic.ConflictError # 409 -│ ├── anthropic.UnprocessableEntityError # 422 -│ ├── anthropic.RateLimitError # 429 -│ └── anthropic.InternalServerError # 500+ -├── anthropic.APIConnectionError # 连接失败 -│ └── anthropic.APITimeoutError # 超时 -└── anthropic.APIStatusError # 带完整响应的错误 - └── 所有上面的子类 -``` - -异常属性: - -```python -except anthropic.APIError as e: - e.status_code # HTTP 状态码 - e.message # 错误消息 - e.body # 响应体 - e.response # 完整 httpx.Response 对象 - e.request_id # 请求 ID(从 headers 提取) - e.error # APIErrorBody 对象(type + message) -``` - -**DashScope** - -无异常类。所有错误通过 `DashScopeAPIResponse` 返回: - -```python -response = Generation.call(model="qwen-plus", messages=[...]) - -# 必须手动检查 -if response.status_code != 200: - print(f"Error: {response.code}") - print(f"Message: {response.message}") - print(f"Request ID: {response.request_id}") -``` - -DashScope 的 common/error.py 仅有 3 个异常: - -```python -class Error(Exception): ... -class InputRequired(Error): ... -class ModelRequired(Error): ... -class InvalidParameter(InputRequired): ... -class InvalidTask(Error): ... -class NotSupport(Error): ... -``` - -这些仅用于客户端参数校验,不用于 API 错误。 - -### 2.3 错误处理代码对比 - -**OpenAI — 类型安全** - -```python -from openai import OpenAI, APIError, RateLimitError, APIConnectionError - -client = OpenAI() - -try: - response = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], - ) - print(response.choices[0].message.content) - -except RateLimitError as e: - print(f"Rate limited: {e.message}, retry after {e.headers.get('retry-after')}") -except APIConnectionError as e: - print(f"Connection failed: {e.message}") -except APIError as e: - print(f"API error {e.status_code}: {e.message}, request_id: {e.request_id}") -``` - -**Anthropic — 类似但更扁平** - -```python -from anthropic import Anthropic, RateLimitError, APIConnectionError - -client = Anthropic() - -try: - message = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], - ) - print(message.content[0].text) - -except RateLimitError as e: - print(f"Rate limited: {e.message}") -except APIConnectionError as e: - print(f"Connection failed: {e.message}") -except Exception as e: - print(f"Unexpected: {e}") -``` - -**DashScope — 手动检查** - -```python -import dashscope -from dashscope import Generation - -response = Generation.call(model="qwen-plus", messages=[...]) - -# ❌ 容易忘记检查,容易遗漏错误 -if response.status_code == 200: - print(response.output.choices[0].message.content) -else: - # 错误码是字符串,如 "InvalidApiKey", "DataInspectionFailed" - print(f"Error {response.code}: {response.message}") - print(f"Request ID: {response.request_id}") -``` - -### 2.4 错误码体系 - -| 特性 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 错误码类型 | Python 异常类 + HTTP status code | Python 异常类 + HTTP status code | 字符串 code("InvalidApiKey" 等) | -| HTTP 状态码映射 | 400/401/403/404/409/422/429/500 | 400/401/403/404/409/422/429/500 | 全部混在 response.status_code | -| 错误分类 | 按 HTTP 状态自动分类 | 按 HTTP 状态自动分类 | 字符串枚举,无自动分类 | -| 重试策略 | 自动重试 429/5xx/连接错误 | 自动重试 429/5xx/连接错误 | 仅 5xx 重试 1 次 | -| 超时处理 | APITimeoutError 异常 | APITimeoutError 异常 | requests.Timeout 异常(未捕获) | -| 限流信息 | Retry-After header | Retry-After header | 无 | -| 请求 ID | e.request_id | e.request_id | response.request_id | - -### 2.5 DashScope 错误码列表(已知) - -| code | 含义 | HTTP Status | -| --- | --- | --- | -| InvalidApiKey | API Key 无效 | 401 | -| AccessDenied | 无权限 | 403 | -| InvalidParameter | 参数错误 | 400 | -| DataInspectionFailed | 内容安全审核失败 | 400 | -| Throttling | 限流 | 429 | -| Throttling.AllocationQuota | 配额耗尽 | 429 | -| ModelNotFound | 模型不存在 | 404 | -| RequestTimeout | 请求超时 | 504 | -| InternalError | 内部错误 | 500 | -| OperationDenied.NoStock | 无库存 | 503 | - -问题:错误码是字符串,没有类型安全,调用方需要写字符串比较: - -```python -if response.code == "Throttling": - time.sleep(5) -elif response.code == "InvalidApiKey": - print("Check your API key") -``` - -对比 OpenAI/Anthropic 的类型安全: - -```python -except RateLimitError: # 类型安全,IDE 可补全 - time.sleep(5) -except AuthenticationError: - print("Check your API key") -``` - ---- - -## 3. 功能使用:流式/非流式/同步/异步 - -### 3.1 四种模式矩阵 - -| 模式 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 同步 + 非流式 | ✅ `client.chat.completions.create()` | ✅ `client.messages.create()` | ✅ `Generation.call()` | -| 同步 + 流式 | ✅ stream=True → Stream[ChatCompletionChunk] | ✅ stream=True → Stream[RawMessageStreamEvent] | ✅ stream=True → Generator[DashScopeAPIResponse] | -| 异步 + 非流式 | ✅ `AsyncOpenAI()` + `await client.chat.completions.create()` | ✅ `AsyncAnthropic()` + `await client.messages.create()` | ✅ `AioGeneration.call()` (需手动实现) | -| 异步 + 流式 | ✅ stream=True → AsyncStream[ChatCompletionChunk] | ✅ stream=True → AsyncStream[RawMessageStreamEvent] | ⚠️ 无原生支持 | - -### 3.2 同步非流式 — 最常用模式 - -**OpenAI** - -```python -from openai import OpenAI - -client = OpenAI() - -response = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], - temperature=0.7, - max_tokens=1024, -) - -# Pydantic 对象,完整类型提示 -content = response.choices[0].message.content -usage = response.usage # CompletionUsage(prompt_tokens=..., completion_tokens=..., total_tokens=...) -``` - -**Anthropic** - -```python -from anthropic import Anthropic - -client = Anthropic() - -message = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], -) - -# Pydantic 对象 -content = message.content[0].text -usage = message.usage # Usage(input_tokens=..., output_tokens=..., cache_creation_input_tokens=...) -``` - -**DashScope** - -```python -from dashscope import Generation - -response = Generation.call( - model="qwen-plus", - messages=[{"role": "user", "content": "Hello"}], - temperature=0.7, - max_tokens=1024, -) - -# DictMixin 对象,无类型提示 -if response.status_code == 200: - content = response.output.choices[0].message.content - usage = response.usage # GenerationUsage(input_tokens=..., output_tokens=...) -``` - -### 3.3 同步流式 - -**OpenAI — 最优雅** - -```python -from openai import OpenAI - -client = OpenAI() - -stream = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], - stream=True, -) - -# Stream 对象实现了 __iter__,每个 chunk 是 ChatCompletionChunk -for chunk in stream: - if chunk.choices[0].delta.content: - print(chunk.choices[0].delta.content, end="", flush=True) -``` - -特点: - -- Stream 类型实现了 `__iter__`,直接 for 循环 -- 每个 chunk 是 ChatCompletionChunk(Pydantic 模型) -- `chunk.choices[0].delta.content` — 增量内容 -- 自动处理 SSE 协议解析 -- `stream.close()` 可提前终止 - -**Anthropic — 事件驱动** - -```python -from anthropic import Anthropic - -client = Anthropic() - -with client.messages.stream( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], -) as stream: - for text in stream.text_stream: - print(text, end="", flush=True) - - # 流结束后获取完整消息 - message = stream.get_final_message() - print(f"\nTotal tokens: {message.usage.input_tokens + message.usage.output_tokens}") -``` - -特点: - -- MessageStreamManager 上下文管理器 -- `stream.text_stream` — 纯文本迭代器(最简洁) -- `stream.__iter__()` — 原始事件迭代器(RawMessageStreamEvent) -- `stream.get_final_message()` — 累积后的完整消息 -- `stream.get_final_text()` — 累积后的纯文本 -- 事件类型丰富:RawMessageStartEvent / RawContentBlockDeltaEvent / RawMessageDeltaEvent 等 - -**DashScope — Generator 模式** - -```python -from dashscope import Generation - -responses = Generation.call( - model="qwen-plus", - messages=[{"role": "user", "content": "Hello"}], - stream=True, - incremental_output=True, # 增量输出 -) - -# 返回 Generator[DashScopeAPIResponse] -for response in responses: - if response.status_code == 200: - text = response.output.choices[0].message.content - print(text, end="", flush=True) - else: - print(f"Error: {response.code}") - break -``` - -特点: - -- 返回 Generator[DashScopeAPIResponse] -- 每个 chunk 仍然是 DashScopeAPIResponse(需要检查 status_code) -- `incremental_output=True` 控制是否增量 -- SSE 解析在 `StreamEventMixin._handle_stream()` 中实现 -- 无 `close()` 方法 - -### 3.4 异步模式 - -**OpenAI — 完整异步支持** - -```python -import asyncio -from openai import AsyncOpenAI - -client = AsyncOpenAI() - -async def main(): - # 异步非流式 - response = await client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], - ) - print(response.choices[0].message.content) - - # 异步流式 - stream = await client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], - stream=True, - ) - async for chunk in stream: - if chunk.choices[0].delta.content: - print(chunk.choices[0].delta.content, end="") - -asyncio.run(main()) -``` - -特点: - -- AsyncOpenAI 使用 httpx.AsyncClient -- AsyncStream 实现了 `__aiter__` -- 连接池复用 -- 完整的异步上下文管理 - -**Anthropic — 完整异步支持** - -```python -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - -async def main(): - # 异步非流式 - message = await client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], - ) - print(message.content[0].text) - - # 异步流式 - async with client.messages.stream( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], - ) as stream: - async for text in stream.text_stream: - print(text, end="") - -asyncio.run(main()) -``` - -**DashScope — 异步支持不完整** - -```python -# DashScope 的异步是通过 BaseAsyncAioApi 实现的 -# 主要用于异步任务(如图像生成),而非 chat 流式 - -from dashscope.client.base_api import BaseAsyncAioApi - -# 异步任务模式(非 chat) -task_response = await SomeModel.async_call(model="...", input={...}) -# 轮询等待 -result = await BaseAsyncAioApi.wait(task_response) - -# ❌ 没有 AsyncGeneration.call() -# ❌ 没有 async for chunk in stream -# ❌ 没有 AsyncStream 类型 -``` - -问题: - -- AioGeneration 存在但功能不完整 -- 异步流式(async for)无原生支持 -- 底层使用 aiohttp,但上层 API 没有暴露完整的异步接口 -- 用户需要在 async 函数中调用同步 API(阻塞事件循环) - -### 3.5 流式辅助功能 - -| 功能 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 累积文本 | 手动拼接 delta | `stream.get_final_text()` | 手动拼接 + incremental_output | -| 累积消息 | 手动拼接 chunks | `stream.get_final_message()` | 手动拼接 | -| Usage 统计 | chunk.usage(最后一个 chunk) | message.usage(最终消息) | 最后一个 chunk 的 usage | -| 提前终止 | `stream.close()` | 退出 with 块 | 无 | -| 事件类型区分 | `chunk.choices[0].finish_reason` | event.type(丰富的枚举) | finish_reason | -| 工具调用流式 | delta.tool_calls | RawContentBlockStartEvent | 无流式工具调用 | - -### 3.6 底层实现 - -| 方面 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| HTTP 客户端 | httpx.Client / httpx.AsyncClient | httpx.Client / httpx.AsyncClient | requests.Session / aiohttp.ClientSession | -| 连接池 | ✅ httpx 内置 | ✅ httpx 内置 | ❌ 每次 with requests.Session() 新建 | -| SSE 解析 | 自研 Stream 类 | 自研 MessageStream | StreamEventMixin._handle_stream() | -| 重试机制 | 内置(指数退避 + jitter) | 内置(指数退避 + jitter) | 仅 5xx 重试 1 次 | -| 代理支持 | httpx 原生 | httpx 原生 | requests 原生 | -| 超时粒度 | connect / read / write / pool | connect / read / write / pool | 仅 timeout 一个值 | - ---- - -## 4. 架构设计哲学 - -### 4.1 设计模式对比 - -| 模式 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 客户端模式 | 实例化 Client → 调用方法 | 实例化 Client → 调用方法 | 类方法直接调用 | -| 响应模式 | Pydantic 模型 | Pydantic 模型 | DictMixin(dict 子类) | -| 错误模式 | 异常驱动 | 异常驱动 | 返回值驱动 | -| 类型系统 | Pydantic v2 + Pydantic Core | Pydantic v1 兼容 | 无类型验证 | -| HTTP 层 | httpx(现代) | httpx(现代) | requests(传统) | -| 代码生成 | 大部分自动生成 | 大部分自动生成 | 手写 | - -### 4.2 代码量对比 - -| 指标 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| SDK 包大小 | ~1MB | ~500KB | ~2MB | -| 核心文件数 | ~50 | ~30 | ~80 | -| 依赖项 | httpx, pydantic, anyio, typing-extensions | httpx, pydantic, anyio, typing-extensions | requests, aiohttp, pyyaml | -| 类型存根 | ✅ 内置 .pyi | ✅ 内置 .pyi | ⚠️ 刚添加(PR #170) | -| 自动重试 | ✅ 内置 | ✅ 内置 | ⚠️ 基础实现 | - -### 4.3 OpenAI 和 Anthropic 为何选择相同架构 - -两者 SDK 架构惊人相似,原因: - -1. Anthropic 早期 API 兼容 OpenAI — 降低用户迁移成本 -2. Pydantic 成为行业标准 — 类型安全 + 自动验证 + 序列化 -3. httpx 替代 requests — 原生异步支持 + 连接池 -4. 异常驱动错误处理 — Python 社区最佳实践(EAFP > LBYL) - -DashScope 选择不同架构的历史原因: - -1. 早期设计 — 基于 requests + dict,追求简单 -2. 多任务支持 — 异步任务(图像生成、语音合成)需要 async_call / wait / fetch 模式 -3. 兼容性考虑 — 保持向后兼容,不敢大改 API - ---- - -## 5. DashScope SDK 改进建议 - -### 5.1 优先级 P0 — 必须修复 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 1 | 添加 Client 模式 | `DashScope()` 实例化 + `client.chat.completions.create()` | OpenAI `OpenAI()` | -| 2 | 添加异常类层次 | 按 HTTP 状态码自动抛出对应异常 | OpenAI 异常体系 | -| 3 | Pydantic 响应模型 | 替换 DictMixin,提供完整类型验证和 IDE 补全 | OpenAI ChatCompletion | -| 4 | 完善异步流式 | `AsyncDashScope()` + `async for chunk in stream` | OpenAI AsyncOpenAI | - -### 5.2 优先级 P1 — 强烈建议 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 5 | httpx 替换 requests | 原生异步 + 连接池复用 + 现代超时控制 | OpenAI / Anthropic | -| 6 | 内置自动重试 | 指数退避 + jitter + Retry-After 支持 | OpenAI `_should_retry()` | -| 7 | 流式辅助 API | `stream.get_final_message()` / `stream.close()` | Anthropic MessageStream | -| 8 | 增量输出默认开启 | `incremental_output=True` 应为默认 | Anthropic 默认增量 | - -### 5.3 优先级 P2 — 锦上添花 - -| # | 改进 | 说明 | 参考 | -| --- | --- | --- | --- | -| 9 | @tool 装饰器 | 从函数签名 + docstring 自动生成 tool schema | OpenAI Agents SDK | -| 10 | Responses API | 兼容 OpenAI Responses API 格式 | OpenAI `client.responses.create()` | -| 11 | Anthropic 兼容层 | 兼容 Anthropic Messages API 格式 | Anthropic `client.messages.create()` | -| 12 | Dashboard 追踪 | 请求追踪和用量统计 Web UI | OpenAI Agents Dashboard | - -### 5.4 渐进式迁移方案 - -DashScope 不能一步到位重构(破坏现有用户),建议分阶段: - -**阶段 1:兼容层(不破坏现有 API)** - -```python -# 现有 API 保持不变 -from dashscope import Generation -response = Generation.call(model="qwen-plus", messages=[...]) - -# 新增 Client 模式(并行可用) -from dashscope import DashScope -client = DashScope() # api_key 从环境变量读取 -response = client.chat.completions.create( - model="qwen-plus", - messages=[{"role": "user", "content": "Hello"}], -) -# response 是 Pydantic 模型 -print(response.choices[0].message.content) -``` - -**阶段 2:异常层(可选启用)** - -```python -# 默认保持返回值模式(向后兼容) -client = DashScope() -response = client.chat.completions.create(...) -if response.status_code != 200: ... - -# 启用异常模式 -client = DashScope(raise_on_error=True) -try: - response = client.chat.completions.create(...) -except RateLimitError as e: - ... -``` - -**阶段 3:完全现代化(下一代)** - -```python -# dashscope v2 -from dashscope import DashScope -from dashscope.errors import RateLimitError, AuthenticationError - -client = DashScope() # httpx + Pydantic + 异常驱动 - -try: - response = client.chat.completions.create( - model="qwen-plus", - messages=[{"role": "user", "content": "Hello"}], - ) - print(response.choices[0].message.content) -except RateLimitError as e: - print(f"Rate limited: {e.message}") -``` - -### 5.5 关键差异总结 - -| 维度 | OpenAI/Anthropic | DashScope 现状 | 差距 | -| --- | --- | --- | --- | -| 初始化 | Client() 实例化 | 类方法 + 模块变量 | 缺少 Client 模式 | -| 调用风格 | client.resource.method() | Class.call() | 缺少资源嵌套 | -| 响应 | Pydantic 模型 | DictMixin (dict) | 缺少类型验证 | -| 错误 | 异常驱动 | 返回值驱动 | 缺少异常体系 | -| 异步 | 完整支持 | 部分支持 | 缺少异步流式 | -| HTTP | httpx(连接池) | requests(无连接池) | 性能差距 | -| 重试 | 自动 + 智能 | 基础 1 次 | 可靠性差距 | -| 类型提示 | 完整 .pyi | 刚添加 | IDE 体验差距 | - ---- - -## 6. 日志与错误可理解性 - -### 6.1 日志系统对比 - -**OpenAI — 最小化 + 敏感信息过滤** - -```python -# 日志配置(_utils/_logs.py) -logger = logging.getLogger("openai") - -def setup_logging(): - env = os.environ.get("OPENAI_LOG") - if env == "debug": - logger.setLevel(logging.DEBUG) - httpx_logger.setLevel(logging.DEBUG) # 同时开启 httpx 日志 -``` - -日志内容示例(DEBUG 模式): - -``` -[2026-07-31 14:12:26 - openai._base_client:1033 - DEBUG] Sending HTTP Request: POST https://api.openai.com/v1/chat/completions -[2026-07-31 14:12:27 - openai._base_client:1074 - DEBUG] HTTP Response: POST https://api.openai.com/v1/chat/completions "200 OK" -[2026-07-31 14:12:27 - openai._base_client:1082 - DEBUG] request_id: req_abc123 -``` - -亮点: - -- SensitiveHeadersFilter — 自动过滤 Authorization 和 api-key 头,防止日志泄露密钥 -- 重试日志非常详细:"Retrying due to status code 429" / "Retrying as header x-should-retry is set to true" -- 环境变量一键开启(`OPENAI_LOG=debug`),无需代码修改 -- 同时输出 httpx 底层日志(连接/超时/重试细节) - -**Anthropic — 与 OpenAI 几乎一致** - -```python -# _base_client.py -log = logging.getLogger(__name__) # anthropic._base_client - -# 重试日志 -log.debug("Retrying due to status code %i", response.status_code) -log.info("Retrying request to %s in %f seconds", options.url, timeout) -log.debug("request_id: %s", response.headers.get("request-id")) -``` - -亮点: - -- 日志格式和粒度与 OpenAI 一致(同一套 Stainless 代码生成器) -- request-id header 追踪 -- 超时错误消息自带文档链接:"Request timed out or interrupted. See https://docs.anthropic.com/en/api/errors#long-requests" - -**DashScope — 基础日志,信息量不足** - -```python -# common/logging.py -logger = logging.getLogger("dashscope") - -# 日志格式 -"%(asctime)s - %(name)s - %(filename)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s" - -# 环境变量: DASHSCOPE_LOGGING_LEVEL=info/debug -``` - -日志内容示例(DEBUG 模式): - -``` -2026-07-31 14:12:26 - dashscope - base_api.py - _get - 599 - DEBUG - Starting request: https://dashscope.aliyuncs.com/api/v1/tasks/task_123 -2026-07-31 14:12:27 - dashscope - base_api.py - _get - 605 - DEBUG - Starting processing response: https://dashscope.aliyuncs.com/api/v1/tasks/task_123 -``` - -问题: - -- ❌ 无敏感信息过滤 — API Key 可能出现在日志中 -- ❌ 无 request_id 日志 — 请求 ID 只在 response 对象中,不在日志里 -- ❌ 无 HTTP 状态码日志 — 只记录 "Starting request" 和 "Starting processing response" -- ❌ 无重试原因日志 — 重试时只记录 "The task xxx is RUNNING",不记录重试次数/等待时间 -- ❌ 日志格式冗长 — 包含 filename + funcName + lineno,但信息密度低 -- ⚠️ 默认关闭 — 不设置 DASHSCOPE_LOGGING_LEVEL 环境变量则无日志 - -### 6.2 日志对比矩阵 - -| 特性 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 开启方式 | OPENAI_LOG=debug | ANTHROPIC_LOG=debug | DASHSCOPE_LOGGING_LEVEL=debug | -| 日志级别 | DEBUG / INFO | DEBUG / INFO | DEBUG / INFO | -| 请求 URL | ✅ POST https://... | ✅ POST https://... | ✅ Starting request: https://... | -| HTTP 状态码 | ✅ "200 OK" | ✅ "200 OK" | ❌ 不记录 | -| Request ID | ✅ request_id: req_xxx | ✅ request_id: xxx | ❌ 不记录 | -| Response Headers | ✅ 完整输出 | ✅ 完整输出 | ❌ 不记录 | -| 敏感信息过滤 | ✅ SensitiveHeadersFilter | ✅ 同源 | ❌ 无 | -| 重试原因 | ✅ "Retrying due to 429" | ✅ "Retrying due to 429" | ❌ 不记录 | -| 重试等待时间 | ✅ "Retrying in 2.5s" | ✅ "Retrying in 2.5s" | ❌ 不记录 | -| 超时异常详情 | ✅ exc_info=True | ✅ exc_info=True | ❌ 无 | -| 连接异常详情 | ✅ exc_info=True | ✅ exc_info=True | ❌ 无 | -| httpx 底层日志 | ✅ 联动开启 | ✅ 联动开启 | N/A(用 requests) | - -### 6.3 错误消息可理解性 - -**OpenAI — 结构化错误消息** - -```python -# 实际错误消息格式 -try: - client.chat.completions.create(model="gpt-4o", messages=[...]) -except openai.RateLimitError as e: - print(e) - # Error code: 429 - {'error': {'message': 'Rate limit reached for gpt-4o - # in organization org-xxx on requests per min (RPM): Limit 60, Used 60, - # Requested 1. Please try again in 1s. Visit ...', - # 'type': 'requests', 'code': 'rate_limit_exceeded'}} - - # 结构化访问 - e.status_code # 429 - e.message # "Rate limit reached..." - e.code # "rate_limit_exceeded" - e.type # "requests" - e.request_id # "req_abc123" - e.body # 完整响应体 dict - e.response # 完整 httpx.Response 对象 - e.headers # 响应头(含 Retry-After) -``` - -亮点: - -- 错误消息包含完整上下文:哪个组织、哪个模型、限制是多少、已用多少、建议等待多久 -- 所有字段都可以通过属性访问,无需解析字符串 -- `e.response.headers.get("Retry-After")` 直接获取重试等待时间 - -**Anthropic — 同样结构化 + 文档链接** - -```python -try: - client.messages.create(model="claude-sonnet-4-20250514", messages=[...]) -except anthropic.RateLimitError as e: - print(e) - # Error code: 429 - {'type': 'error', 'error': {'type': 'rate_limit_error', - # 'message': 'Number of request tokens has exceeded your per-minute rate limit'}} - - e.status_code # 429 - e.message # "Number of request tokens..." - e.request_id # "req_xxx" - e.type # "rate_limit_error"(Anthropic 特有的错误类型枚举) - e.body # 完整响应体 - e.response # 完整 httpx.Response - -# 超时错误自带文档链接 -except anthropic.APITimeoutError as e: - print(e.message) - # "Request timed out or interrupted. This could be due to a network timeout, - # dropped connection, or request cancellation. - # See https://docs.anthropic.com/en/api/errors#long-requests for more details." -``` - -亮点: - -- `e.type` — Anthropic 特有的 ErrorType 枚举(overloaded_error / rate_limit_error / invalid_request_error 等),比 HTTP 状态码更精确 -- 超时错误消息包含排查文档链接 -- 额外异常类型:OverloadedError(529) / DeadlineExceededError(504) / ServiceUnavailableError(503) - -**DashScope — 字符串拼接,信息碎片化** - -```python -response = Generation.call(model="qwen-plus", messages=[...]) - -# 成功时 -print(response) -# {"status_code": 200, "request_id": "req-xxx", "code": "", "message": "", -# "output": {...}, "usage": {...}} - -# 失败时 -if response.status_code != 200: - print(f"code: {response.code}") # "Throttling" - print(f"message: {response.message}") # "Requests rate limit" - print(f"request_id: {response.request_id}") # "req-xxx" - - # ❌ 没有结构化的错误对象 - # ❌ 没有 Retry-After 建议 - # ❌ 没有文档链接 - # ❌ 没有错误分类(限流 vs 认证 vs 参数错误 需要自己判断) -``` - -问题: - -- 错误消息过于简短:"Requests rate limit" — 不告诉用户限制是多少、已用多少、建议等多久 -- 错误码是裸字符串:"Throttling" / "InvalidApiKey" / "DataInspectionFailed" — 无类型安全 -- 同一个 DashScopeAPIResponse 对象既表示成功又表示失败 — 容易忘记检查 -- response.code 可能是 None、空字符串 ""、或错误码字符串 — 三种状态混乱 - -### 6.4 实际排障场景对比 - -**场景 1:API Key 无效** - -OpenAI: - -``` -AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: -sk-xxxx...xxxx. You can find your API key at https://platform.openai.com/account/api-keys.', -'type': 'invalid_request_error', 'code': 'invalid_api_key'}} -``` - -→ 直接告诉用户去哪里找正确的 Key - -DashScope: - -``` -status_code: 401 -code: "InvalidApiKey" -message: "Invalid API-key provided." -``` - -→ 只说无效,不说去哪里获取 - -**场景 2:限流** - -OpenAI: - -``` -RateLimitError: Error code: 429 - {'error': {'message': 'Rate limit reached for gpt-4o -in organization org-xxx on requests per min (RPM): Limit 60, Used 60, Requested 1. -Please try again in 1s. Visit https://platform.openai.com/account/rate-limits for more.', -'type': 'requests', 'code': 'rate_limit_exceeded'}} -``` - -→ 告诉你限制值、已用量、建议等待时间、文档链接 - -DashScope: - -``` -status_code: 429 -code: "Throttling" -message: "Requests rate limit" -``` - -→ 4 个单词,没有任何可操作信息 - -**场景 3:内容安全审核** - -DashScope: - -``` -status_code: 400 -code: "DataInspectionFailed" -message: "Input data may contain inappropriate content." -``` - -→ 不告诉用户哪段内容触发了审核 - -**场景 4:网络超时** - -OpenAI: - -``` -APITimeoutError: Request timed out. -``` - -→ 简洁明确,可通过 `except APITimeoutError` 精确捕获 - -DashScope: - -``` -requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='dashscope.aliyuncs.com', -port=443): Read timed out. (read timeout=300) -``` - -→ 底层 requests 异常直接泄露,用户需要 `except requests.exceptions.ReadTimeout` 捕获 - -### 6.5 异常可追溯性 - -| 特性 | OpenAI | Anthropic | DashScope | -| --- | --- | --- | --- | -| 异常链 | `raise ... from err` 保留原始异常 | 同 | ❌ 不保留(直接返回 response) | -| 原始请求 | e.request (httpx.Request) | e.request (httpx.Request) | ❌ 不保留 | -| 原始响应 | e.response (httpx.Response) | e.response (httpx.Response) | ❌ 不保留 | -| 请求体 | e.request.content | e.request.content | ❌ 不保留 | -| 响应体 | e.body | e.body | response 本身就是 | -| 堆栈追踪 | ✅ 完整 Python traceback | ✅ 完整 | ❌ 无异常,无 traceback | -| 可序列化 | e.body 是 dict | e.body 是 dict | response 是 dict | - -### 6.6 调试体验对比 - -开启 DEBUG 日志后的输出 - -**OpenAI(信息密度高)**: - -``` -[2026-07-31 14:12:26 - openai._base_client:1033 - DEBUG] Sending HTTP Request: POST https://api.openai.com/v1/chat/completions -[2026-07-31 14:12:27 - openai._base_client:1074 - DEBUG] HTTP Response: POST https://api.openai.com/v1/chat/completions "200 OK" Headers({'content-type': 'application/json', 'x-request-id': 'req_abc123', ...}) -[2026-07-31 14:12:27 - openai._base_client:1082 - DEBUG] request_id: req_abc123 -``` - -**DashScope(信息密度低)**: - -``` -2026-07-31 14:12:26 - dashscope - api_request_factory.py - _build_api_request - 128 - DEBUG - encryption enabled -2026-07-31 14:12:26 - dashscope - base_api.py - call - 548 - DEBUG - Starting request: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation/ -2026-07-31 14:12:27 - dashscope - base_api.py - call - 558 - DEBUG - Starting processing response: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation/ -``` - -→ 不知道响应状态码、不知道 request_id、不知道响应内容 - -### 6.7 DashScope 日志改进建议 - -| # | 改进 | 优先级 | 说明 | -| --- | --- | --- | --- | -| 1 | 添加 SensitiveHeadersFilter | P0 | 过滤 Authorization header,防止密钥泄露 | -| 2 | 日志中记录 request_id | P0 | 每个请求/响应都记录 request_id,便于找阿里云工单 | -| 3 | 日志中记录 HTTP 状态码 | P0 | "HTTP Response: POST ... 200 OK" | -| 4 | 重试日志增加原因和等待时间 | P1 | "Retrying due to 429, waiting 2.5s" | -| 5 | 异常链保留 | P1 | `raise DashScopeError(...) from original_error` | -| 6 | 错误消息增加可操作信息 | P1 | 限流时告知限制值和等待时间 | -| 7 | 超时错误消息添加排查文档链接 | P2 | 参考 Anthropic 的做法 | -| 8 | 日志格式简化 | P2 | 去掉 filename + funcName + lineno(用 %(name)s 即可) | - -### 6.8 错误处理最佳实践建议 - -**当前 DashScope 用户被迫写的代码** - -```python -# 每个调用点都要写这套 — 容易遗漏,容易不一致 -response = Generation.call(model="qwen-plus", messages=[...]) -if response.status_code == 200: - result = response.output.choices[0].message.content -elif response.code == "Throttling": - time.sleep(5) - # 重试... -elif response.code == "InvalidApiKey": - print("Check your DASHSCOPE_API_KEY") -elif response.code == "DataInspectionFailed": - print("Content blocked by safety filter") -else: - print(f"Unknown error: {response.status_code} {response.code}: {response.message}") -``` - -**OpenAI/Anthropic 用户写的代码** - -```python -# 一处 try/except,全局生效 — 不可能遗漏 -try: - response = client.chat.completions.create(model="gpt-4o", messages=[...]) - result = response.choices[0].message.content # 无需检查 status_code -except RateLimitError: - time.sleep(int(e.headers.get("Retry-After", 5))) -except AuthenticationError: - print("Check your API key") -``` - -**如果 DashScope 采用异常驱动后的理想代码** - -```python -from dashscope import DashScope -from dashscope.errors import RateLimitError, AuthenticationError, ContentFilterError - -client = DashScope() - -try: - response = client.chat.completions.create(model="qwen-plus", messages=[...]) - result = response.choices[0].message.content -except RateLimitError as e: - print(f"Rate limited. Limit: {e.limit}, Used: {e.used}, Retry after: {e.retry_after}s") - time.sleep(e.retry_after) -except AuthenticationError: - print(f"Invalid API key. Get yours at https://dashscope.console.aliyun.com/apiKey") -except ContentFilterError as e: - print(f"Content blocked: {e.category}") -``` - ---- - -## 参考 - -- OpenAI Python SDK -- Anthropic Python SDK -- DashScope Python SDK -- Pydantic V2 -- httpx diff --git a/md/sdk-source-level-comparison-part2.md b/md/sdk-source-level-comparison-part2.md deleted file mode 100644 index 37637c5b..00000000 --- a/md/sdk-source-level-comparison-part2.md +++ /dev/null @@ -1,397 +0,0 @@ -# SDK 源码级深度对比(二):openai-python / anthropic-sdk-python vs DashScope SDK - -> 基于真实克隆的源码分析,非文档推测 -> 对比维度:1) 用户接口友好度 2) 错误码/异常体系 3) 流式/非流式、同步/异步 -> 来源:钉钉文档(alidocs YMyQA2dXW7gYo6MzcZR0MPdZWzlwrZgb) - ---- - -## 0. 架构血缘(先讲结论) - -| | openai-python | anthropic-sdk-python | dashscope-sdk | -| --- | --- | --- | --- | -| 生成方式 | Stainless 从 OpenAPI spec 生成 | Stainless 从 OpenAPI spec 生成 | 完全手写 | -| HTTP 库 | httpx(_base_client.py 77KB) | httpx(_base_client.py 94KB) | requests(同步)+ aiohttp(异步) | -| 类型系统 | Pydantic 模型(types/ 数千个类) | Pydantic 模型 | dict 子类(DictMixin) | -| 客户端形态 | `OpenAI()` / `AsyncOpenAI()` 实例 | `Anthropic()` / `AsyncAnthropic()` 实例 | `Generation.call()` 类方法 | -| 资源组织 | `client.chat.completions.create()` | `client.messages.create()` | `Generation.call()` 扁平类方法 | - -**关键事实**:OpenAI 和 Anthropic SDK 是同一套 Stainless 框架生成的"孪生架构"(文件名一一对应:_base_client.py、_exceptions.py、_streaming.py、_models.py),两者对比本质是"同一优秀框架 + 各自的 API 设计差异"。DashScope 是完全独立的手写实现,与前两者差距是代际级的。 - ---- - -## 1. 用户接口友好度 - -### 1.1 最简调用:3 行 vs 6 行 - -**OpenAI**(v2.52.0,_client.py:108) - -```python -from openai import OpenAI - -client = OpenAI() # 自动读 OPENAI_API_KEY 环境变量 - -resp = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello"}], -) -print(resp.choices[0].message.content) # 属性访问,IDE 全提示 -``` - -**Anthropic**(v0.120.2,_client.py) - -```python -from anthropic import Anthropic - -client = Anthropic() # 自动读 ANTHROPIC_API_KEY;还支持 OAuth credentials 链 - -msg = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], -) -print(msg.content[0].text) # 结构化 content blocks -``` - -**DashScope**(aigc/generation.py:49) - -```python -import dashscope -from dashscope import Generation - -dashscope.api_key = "sk-xxx" # 或依赖 DASHSCOPE_API_KEY 环境变量 - -resp = Generation.call(model="qwen-plus", messages=[{"role": "user", "content": "Hello"}]) - -if resp.status_code == 200: # ← 用户必须手动检查! - print(resp.output["choices"][0]["message"]["content"]) -else: - print(resp.code, resp.message) -``` - -### 1.2 接口友好度逐项对比 - -| 维度 | openai-python | anthropic-sdk-python | dashscope-sdk | -| --- | --- | --- | --- | -| 实例化客户端 | ✅ OpenAI() | ✅ Anthropic() | ❌ 类方法,无客户端对象 | -| 零配置(环境变量) | ✅ OPENAI_API_KEY | ✅ ANTHROPIC_API_KEY + OAuth 自动发现 | ⚠️ DASHSCOPE_API_KEY 可用但文档主推手动赋值 | -| 链式资源导航 | ✅ client.chat.completions.create | ✅ client.messages.create | ❌ Generation.call 扁平 | -| IDE 自动补全 | ✅ 每个参数有 @overload 签名 | ✅ 同左 | ❌ 30+ 个 `x: Any = None` 参数(generation.py:49-81) | -| 响应字段类型 | ✅ Pydantic:resp.choices[0].message.content | ✅ 同左 | ❌ dict 索引:resp.output["choices"][0]["message"]["content"] | -| 成功与否判断 | ✅ 失败直接抛异常,成功即可用 | ✅ 同左 | ❌ 必须手写 if resp.status_code == 200 | -| 参数校验 | ✅ @required_args 装饰器 + Pydantic 本地校验 | ✅ 同左 | ⚠️ 仅手动检查 prompt/model 非空(generation.py:135-140) | -| client 上下文管理 | ✅ with OpenAI() as client / client.close() | ✅ 同左 | ❌ 无 | -| 同一 client 切配置 | ✅ client.with_options(timeout=..., max_retries=...) | ✅ 同左 | ❌ 每次调用传参 | -| 裸响应访问 | ✅ client.with_raw_response.chat.completions.create(...) 拿 httpx.Response | ✅ 同左 | ❌ 无(headers 混在 response dict 里) | -| 结构化输出 | ✅ beta.chat.completions.parse(response_format=MyPydanticModel),直接 .parse() 出对象 | ✅ messages.parse() + tools 解析 | ❌ 返回 JSON 字符串自己 json.loads | -| token 计数 | ✅ client.chat.completions.estimate_tokens(离线估算) | ✅ client.messages.count_tokens() 服务端计数 | ❌ 需单独调 tokenization API | -| Provider 多云支持 | ✅ providers/ 目录(Azure/Bedrock/Vertex 等) | ✅ AnthropicAWS / AnthropicFoundry 子类 + credentials 链(_client.py:40-53) | ❌ 单 endpoint | - -### 1.3 参数设计的具体差距 - -DashScope Generation.call 签名(generation.py:49-82): - -```python -def call(cls, model: str, prompt: Any = None, history: list = None, - api_key: str = None, messages: List[Message] = None, - plugins: Union[str, Dict] = None, ..., **kwargs) - -> Union[GenerationResponse, Generator[GenerationResponse, None, None]]: -``` - -问题: - -1. `prompt: Any` — 无类型约束; -2. history 已废弃但仍占签名位; -3. 30+ 个显式参数 + `**kwargs` — 显式参数用 `if x is not None: kwargs["x"] = x` 逐个搬运(generation.py:141-169 重复 20 多次),既不是类型安全也不是省代码; -4. 返回值是 Union — 调用方无法静态区分"拿到响应"还是"拿到生成器",只能靠记得自己传没传 stream=True。 - -对比 OpenAI completions.py:4 个 @overload 分别覆盖(流式/非流式)×(有无 parse),每个 overload 返回精确类型(ChatCompletion / Stream[ChatCompletionChunk]),IDE 根据实参自动推断返回类型——流式与否在类型层面就能区分。 - ---- - -## 2. 错误码与异常体系 - -### 2.1 异常层次(源码实证) - -**openai-python _exceptions.py** - -``` -OpenAIError -├── APIError # message + request + body + code/param/type -│ ├── APIResponseValidationError # 服务端返回不符合 schema -│ ├── APIStatusError # 4xx/5xx;含 response/status_code/request_id(x-request-id) -│ │ ├── BadRequestError (400) -│ │ ├── AuthenticationError (401) -│ │ │ └── OAuthError # 解析 error_description + OAuthErrorCode 枚举 -│ │ ├── PermissionDeniedError (403) -│ │ ├── NotFoundError (404) -│ │ ├── ConflictError (409) -│ │ ├── UnprocessableEntityError (422) -│ │ ├── RateLimitError (429) -│ │ └── InternalServerError (5xx) -│ ├── APIConnectionError # 网络层 -│ │ └── APITimeoutError -├── LengthFinishReasonError # finish_reason=length 的解析失败(带 usage) -├── ContentFilterFinishReasonError # 内容过滤 -└── WebSocket*Error # realtime 专用 -``` - -**anthropic-sdk-python _exceptions.py** - -``` -AnthropicError -├── APIError -│ ├── APIResponseValidationError -│ ├── APIWebhookValidationError -│ ├── APIStatusError # 含 request_id(request-id 头) + type(ErrorType 枚举) -│ │ ├── BadRequestError (400) -│ │ ├── AuthenticationError (401) -│ │ ├── PermissionDeniedError (403) -│ │ ├── NotFoundError (404) -│ │ ├── ConflictError (409) -│ │ ├── RequestTooLargeError (413) ← OpenAI 没有 -│ │ ├── UnprocessableEntityError (422) -│ │ ├── RateLimitError (429) -│ │ ├── ServiceUnavailableError(503) ← 独立出来 -│ │ ├── DeadlineExceededError (504) -│ │ ├── OverloadedError (529) ← Anthropic 特有 -│ │ └── InternalServerError -│ ├── APIConnectionError -│ │ └── APITimeoutError # message 里附官方排查文档链接 -└── RetryableError # 中间件可主动抛它触发重试 -``` - -**dashscope-sdk common/error.py** - -``` -DashScopeException -├── AuthenticationError (pass) -├── InvalidParameter (pass) -├── InvalidTask (pass) -├── UnsupportedModel (pass) -├── ... 共 25 个扁平子类,绝大多数只有 pass -├── AssistantError # 解析 message 里的 JSON 拿 request_id/code/message -├── RequestFailure # request_id/message/name/http_code —— 仅 websocket/服务端主动失败用 -└── TimeoutException -``` - -### 2.2 错误处理模型的本质差异 - -| | openai / anthropic | dashscope | -| --- | --- | --- | -| HTTP 4xx/5xx 行为 | 抛异常(异常驱动) | 不抛异常,返回 status_code != 200 的 response 对象 | -| 错误信息载体 | 异常对象:e.status_code / e.code / e.type / e.request_id / e.body / e.response.headers | response dict:resp.code / resp.message / resp.request_id | -| request_id 暴露 | ✅ 自动从响应头提取挂到异常上 | ✅ 在 response 里 | -| 错误可分类捕获 | ✅ except RateLimitError / except AuthenticationError | ❌ 只能 `if resp.code == "Throttling"` 字符串匹配 | -| 流式中错误 | ✅ SSE error 事件 → 抛 APIError(_streaming.py:70-99) | ⚠️ 生成器 yield 出带 code 的 response,用户得逐个检查 | -| 本地校验异常 | TypeError + Pydantic ValidationError | InputRequired / ModelRequired(自定义但扁平) | -| 重试钩子 | _should_retry() 按状态码判定 | ❌ 无 | - -### 2.3 典型错误处理代码对比 - -**openai / anthropic(异常驱动,按语义捕获)**: - -```python -from openai import OpenAI, RateLimitError, AuthenticationError, BadRequestError - -client = OpenAI() -try: - resp = client.chat.completions.create(model="gpt-4o", messages=msgs) -except RateLimitError as e: - # e.status_code=429, e.request_id 自动可用;SDK 已先自动重试过 2 次 - log.warning("限流, request_id=%s", e.request_id) -except AuthenticationError: - fail_fast("key 无效") -except BadRequestError as e: - # e.body 含完整错误 JSON,e.param 指出问题参数 - log.error("参数错误: %s (param=%s)", e.message, e.param) -# 走到这里 resp 一定是可用的 —— 无需 if 检查 -print(resp.choices[0].message.content) -``` - -**dashscope(返回值驱动,字符串匹配)**: - -```python -resp = Generation.call(model="qwen-plus", messages=msgs) -if resp.status_code != 200: - if resp.code == "Throttling": # ← 魔法字符串,无枚举 - ... - elif resp.code == "InvalidApiKey": - ... - log.error("failed: %s %s", resp.code, resp.message) - raise RuntimeError(resp) -print(resp.output["choices"][0]["message"]["content"]) -``` - -差距清单: - -1. DashScope 错误分支必须手写,漏写就是静默错误(resp.output 是 None,下游 KeyError); -2. resp.code 是魔法字符串,没有枚举类、没有文档常量可 import; -3. 无法用 except 语义化捕获,重试/降级逻辑难以收敛; -4. RequestFailure 只在 websocket 路径抛出,HTTP 路径不抛——同一个 SDK 两种错误契约。 - -### 2.4 重试机制(错误处理的延伸) - -| | openai | anthropic | dashscope | -| --- | --- | --- | --- | -| 默认重试 | DEFAULT_MAX_RETRIES=2,自动 | 同左 | ❌ 核心路径无任何重试(全仓 grep 确认,仅 agentstudio/finetune 子模块有) | -| Retry-After 头 | ✅ _parse_retry_after_header(),上限封顶 | ✅ 同左(上限 60s) | ❌ | -| 退避策略 | 指数退避 + 抖动(_base_client.py:803-827) | 同左 | ❌ | -| 可重试判定 | _should_retry():408/409/429/5xx + retry-after 超限不重试 | _should_retry_exception() 还覆盖连接中途断开 | ❌ | -| 用户可调 | OpenAI(max_retries=5) / with_options(max_retries=...) / 单请求 options | 同左 | ❌ | - ---- - -## 3. 流式 / 非流式、同步 / 异步 - -### 3.1 四种组合的完整度 - -| 能力 | openai-python | anthropic-sdk-python | dashscope-sdk | -| --- | --- | --- | --- | -| 同步非流式 | ✅ | ✅ | ✅ Generation.call() | -| 同步流式 | ✅ Stream[Chunk] 迭代器 | ✅ Stream[Event] 迭代器 | ✅ stream=True 返回生成器 | -| 异步非流式 | ✅ AsyncOpenAI | ✅ AsyncAnthropic | ⚠️ AioGeneration.call() | -| 异步流式 | ✅ AsyncStream,async for | ✅ AsyncStream,async for | ⚠️ 有但生态不完整(见 3.4) | -| 连接复用 | ✅ httpx.Client 连接池 | ✅ 同左 | ❌ requests 每调用新建 | - -### 3.2 流式实现细节对比 - -**openai-python(_streaming.py)** - -```python -stream = client.chat.completions.create(model="gpt-4o", messages=msgs, stream=True) -# stream: Stream[ChatCompletionChunk] —— 类型精确到 chunk 类型 -for chunk in stream: - print(chunk.choices[0].delta.content or "", end="") -``` - -- SSE 解析基于 httpx-sse 解码器,`[DONE]` 哨兵(_streaming.py:63) -- SSE 中的 error 事件会抛 APIError(_streaming.py:87-99)——流中出错不会静默 -- 高级助手 `client.chat.completions.stream()`(lib/streaming/chat/): - - 自动累积 chunk → get_final_completion() - - 事件回调:.on("chunk", fn) / .on("content_done", fn) - - tool_calls 增量自动拼接 - -**anthropic-sdk-python(lib/streaming/_messages.py)** - -```python -with client.messages.stream(model="claude-sonnet-4", max_tokens=1024, messages=msgs) as stream: - for text in stream.text_stream: # ← 只要文本,过滤掉所有事件 - print(text, end="") -msg = stream.get_final_message() # ← 退出 with 后拿完整 Message -``` - -这是三者中最强的流式 API: - -- MessageStreamManager 上下文管理器(_messages.py:146) -- `stream.text_stream`:纯文本增量迭代,用户不用处理 content_block 事件细节 -- `stream.get_final_message()`:拿聚合后的完整消息(含 usage) -- `.on("text", handler)` / `.on("content_block_start", ...)` 事件订阅 -- tool use 流式迭代器:stream 可逐工具调用输出 - -**dashscope-sdk(aigc/generation.py)** - -```python -responses = Generation.call(model="qwen-plus", messages=msgs, - stream=True, incremental_output=True) -for resp in responses: # Generator[GenerationResponse] - if resp.status_code == 200: # ← 每个 chunk 都要检查! - print(resp.output["choices"][0]["message"]["content"], end="") -``` - -- 流式返回的是完整 GenerationResponse 快照序列(不是 delta 类型),chunk 级类型不存在 -- `incremental_output=True` 才是增量,False 是累积快照——语义靠参数切换而不是 API 切换,容易拿错 -- 每个 chunk 都要手写 status_code 检查,无统一的流内错误抛出 -- 无 text_stream 等价物,无事件回调,无 final message 聚合——累积逻辑全部用户自理 - -### 3.3 非流式对比(错误契约见 §2,此处看返回值) - -| | openai | anthropic | dashscope | -| --- | --- | --- | --- | -| 返回类型 | ChatCompletion(Pydantic) | Message(Pydantic) | GenerationResponse(dict 子类 DictMixin) | -| 内容访问 | resp.choices[0].message.content | msg.content[0].text | resp.output["choices"][0]["message"]["content"] | -| 序列化 | .model_dump() / .model_dump_json() | 同左 | json.dumps(resp)(__str__ 自定义) | -| 字段缺失 | AttributeError(清晰) | 同左 | KeyError(DictMixin.__getattr__ 把缺 key 转 AttributeError,但 output 内部仍是裸 dict) | - -### 3.4 异步实现对比 - -| | openai | anthropic | dashscope | -| --- | --- | --- | --- | -| 异步客户端 | AsyncOpenAI(与同步逐方法镜像,自动生成) | AsyncAnthropic(同左) | AioGeneration(独立类,generation.py:320) | -| 底层 | httpx.AsyncClient | httpx.AsyncClient | aiohttp(与同步的 requests 是两套代码) | -| 异步流式 | AsyncStream,`async for chunk in ...` | 同左 | ⚠️ 支持但返回类型标注、示例、工具链覆盖远不完整 | -| 代码一致性 | 同步/异步走同一个 _base_client 泛型基类 | 同左 | 同步/异步两套独立维护(BaseApi vs BaseAioApi,base_api.py:429/495),行为漂移风险高 | -| 并发安全 | client 实例可共享,连接池 | 同左 | 全局模块状态(dashscope.api_key)+ 类方法 | - -### 3.5 一个容易被忽视的点:超时与取消 - -| | openai | anthropic | dashscope | -| --- | --- | --- | --- | -| 超时设置 | OpenAI(timeout=httpx.Timeout(600, connect=5)) / 单请求 .with_options(timeout=...) | 同左 | 调用级 timeout 参数(部分 API 有),无连接/读分离 | -| 请求取消 | httpx 原生支持 + AbortController 风格 | 同左 | ❌ | -| 长请求指引 | — | APITimeoutError message 内嵌官方长请求文档链接 | ❌ | - ---- - -## 4. 评分总表 - -| 维度 | openai-python v2.52 | anthropic-sdk v0.120 | dashscope v1.x | -| --- | --- | --- | --- | -| 零配置上手 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | -| 类型提示/IDE 体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | -| 响应结构可发现性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | -| 异常体系完备度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | -| 错误可操作性(request_id/param/type) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | -| 自动重试 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ | -| 流式 API 易用性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐(text_stream 最佳) | ⭐⭐ | -| 异步完整度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | -| 结构化输出 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | - ---- - -## 5. 对 DashScope SDK 的改进建议(按 ROI 排序) - -### P0 — 契约级修复(破坏性,适合 v2.0) - -1. **异常驱动错误处理**:HTTP/业务失败抛 DashScopeAPIError 层次(按 status_code 细分 RateLimitError 等),保留现有返回值行为做兼容开关。这是与两家对齐的最小改动、最大收益。 -2. **错误码枚举**:把 "Throttling" / "InvalidApiKey" 等字符串收敛为可 import 的枚举常量,文档给出完整对照表。 -3. **内置重试**:核心 HTTP 路径加 max_retries(默认 2)+ Retry-After 解析 + 指数退避;DashScope 网关限流场景下这是用户最痛的点。 - -### P1 — 体验补齐(不破坏兼容) - -4. **实例化客户端**:提供 `dashscope.Client(api_key=...)`,`client.generation.create(...)` 链式导航;类方法保留为 deprecated 别名。 -5. **Pydantic 响应模型**:至少为 Generation/MultimodalConversation 提供 typed response,`resp.choices[0].message.content` 可用。 -6. **流式升级**: - - stream=True 时 chunk 级类型标注(Iterator[GenerationChunk]); - - 提供 text_stream 风格助手(Generation.stream_text(...)),内部处理 incremental_output + status 检查 + 聚合; - - 流内错误统一抛异常而非 yield 错误 response。 -7. **with_raw_response 等价物**:暴露原始 headers(排查问题常需要 x-request-id 之外的网关头)。 - -### P2 — 架构现代化(长期) - -8. **httpx 统一同步/异步**:替换 requests + aiohttp 双栈,消除 BaseApi/BaseAioApi 双份维护。 -9. **代码生成**:从百炼 OpenAPI spec 用 Stainless/自研生成器产出资源类,参数 @overload 化,新模型参数自动同步。 -10. **结构化输出**:response_format=PydanticModel 直接返回解析后对象,对齐 OpenAI parse()。 - -**一句话总结** - -OpenAI 和 Anthropic SDK 是同一套现代框架的两个实例:异常驱动、类型完备、四象限(同步/异步 × 流式/非流式)全覆盖。DashScope SDK 是手写时代的产物——功能覆盖不输(甚至更多:异步任务/RL/多模态),但错误契约、类型系统、流式人体工学全面落后一代。优先修 P0 三项(异常驱动 + 错误码枚举 + 自动重试),即可消除 80% 的用户抱怨。 - ---- - -## 附:源码证据索引 - -| 结论 | 文件:行 | -| --- | --- | -| OpenAI 异常层次 | openai-python/src/openai/_exceptions.py:36-163 | -| Anthropic 异常层次(含 529/RetryableError) | anthropic-sdk-python/src/anthropic/_exceptions.py:25-153 | -| OpenAI 重试/Retry-After | openai-python/src/openai/_base_client.py:759-827 | -| Anthropic 重试(含连接中断重试) | anthropic-sdk-python/src/anthropic/_base_client.py:824-896,1130-1162 | -| OpenAI SSE 流内错误抛异常 | openai-python/src/openai/_streaming.py:63-99 | -| Anthropic text_stream/get_final_message | anthropic-sdk-python/src/anthropic/lib/streaming/_messages.py:56-237 | -| DashScope 30+ 无类型参数 | dashscope/aigc/generation.py:49-82 | -| DashScope 手动 kwargs 搬运 | dashscope/aigc/generation.py:141-169 | -| DashScope 扁平异常(pass 体) | dashscope/common/error.py:5-146 | -| DashScope 返回值式响应 | dashscope/api_entities/dashscope_response.py:65-129 | -| DashScope 无核心重试 | 全仓 `grep -rn retry dashscope/client dashscope/protocol` 无结果 | -| DashScope 异步独立栈 | dashscope/client/base_api.py:429(BaseAioApi)/495(BaseApi) | diff --git a/md/self-diagnostic-exception.md b/md/self-diagnostic-exception.md deleted file mode 100644 index e3cf373f..00000000 --- a/md/self-diagnostic-exception.md +++ /dev/null @@ -1,75 +0,0 @@ -# 智能诊断异常(Self-Diagnostic Exception)— dashscope 2.0 设计 - -> 状态:已落地(v2,Tier 0)。对应实现:`dashscope/v2/devkit/diagnosis.py`,挂载点 `dashscope/v2/errors.py`(`DashScopeError.diagnosis`)与 `dashscope/v2/types/common.py`(`ResponseBase.diagnosis`)。 -> 来源:1.x 提案 `self-diagnostic-exception`,针对 2.0 错误体系重写。 - -## 1. 需求与目标 - -传统报错只抛冷冰冰的 HTTP 状态码(400/429),开发者要复制错误去查文档或问 AI。目标:捕获失败响应时,SDK 在本地匹配出结构化、「有人情味」的修复建议,零延迟、零成本、离线可用、无隐私外发。 - -终端体感(新加坡地域未配 Workspace 导致报错): - -```text -================================================================================ -💡 [DashScope 智能诊断] 请求未匹配到有效的百炼工作空间(Workspace) - -🔍 可能的原因与修复方法: - 1. 地域(Region)不匹配:当前走默认北京地域,资源可能在新加坡等其他地域 - 👉 构造客户端时指定 dashscope.Client(region='ap-southeast-1') - 2. 多租户模式下缺少 Workspace 标识 - 👉 设置环境变量 DASHSCOPE_WORKSPACE_ID,或在 Client(workspace=...) / 调用时传入 - -🛠️ 快捷命令:echo $DASHSCOPE_WORKSPACE_ID # 确认工作空间已配置 - -🤖 仍未解决?运行 dashscope 进入交互提问,或一次性提问:dashscope expert "<贴入错误信息>" -================================================================================ -``` - -## 2. 可行性结论 - -**可行,且默认路径不需要额外大模型调用。** DashScope 错误码有限可枚举,一张本地规则表(`(status_code, error code, message pattern)` → 结构化修复建议)即可覆盖绝大多数高频场景。业界先例全是静态规则(Python 3.12 "Did you mean"、Azure CLI 命令建议、Stripe 错误里的文档链接),没有一家在异常路径上同步调大模型。 - -## 3. 接入点:2.0 与 1.x 的差异 - -1.x 挂在 `DashScopeException` 与 `DashScopeAPIResponse` 两处(分别是抛出路径与返回路径)。 - -2.0 是双层错误模型,对应两个挂载点: - -- **抛出路径**(`raise_on_error=True`,默认):所有错误都是 `dashscope.v2.errors.DashScopeError` 子类,携带 `.status_code` / `.code` / `.message`。在基类加 `.diagnosis` 属性,全部子类(含 `SdkError`、`APIStatusError` 家族、网络类 `APIConnectionError`/`APITimeoutError`)自动继承。 -- **返回路径**(`raise_on_error=False`):失败以错误信封返回,即 `ResponseBase`(pydantic)带 `code`/`message`/`status_code`/`is_error`。在 `ResponseBase` 加 `.diagnosis` 属性,仅当 `is_error` 为真时匹配,成功响应返回 `None`、零开销。 - -两处都**懒导入** `diagnosis.diagnose`,成功路径不加载 devkit、不引入循环依赖(`diagnosis.py` 不反向依赖 errors/types)。 - -## 4. 工程约束(必须避开的坑) - -1. **SDK 不直接 print**:库直接打终端会污染 stdout、破坏可组合性。诊断以结构化数据挂在对象上(`err.diagnosis` / `resp.diagnosis`),由 CLI 或 `install_excepthook()` 渲染,且只写 stderr。 -2. **429 成本放大器**:限流在重试循环里高频爆发,每次触发一次付费诊断既烧钱又加剧限流。本地规则表天然无此问题。 -3. **隐私合规**:错误消息常带请求片段;本地匹配不外发,天然规避。 - -## 5. 分层设计 - -- **Tier 0(默认,已落地)**:本地规则库。按 `(status_code, code keyword, message pattern)` 匹配,规则「最specific 优先、首个命中即返回」,输出原因列表 + 修复片段 + 快捷命令 + AI expert 升级提示。已覆盖 Top 高频错误:鉴权、workspace/region、限流、模型不存在、越权、内容审核、参数非法、网络、服务端内部错误。 -- **Tier 1(未做,opt-in 兜底)**:规则未命中且用户显式开启时才调轻量诊断模型;对 429 类高频错误必须禁用或强去重。当前统一引导 `dashscope expert ""`。 - -## 6. 双语 - -规则表中英双语。`DASHSCOPE_DIAGNOSIS_LANG=en` 选英文,默认中文。`render(diagnosis, lang=None)` 输出终端块,结尾附 AI expert 升级提示。 - -## 7. 公开 API - -```python -from dashscope.v2.devkit import diagnose, render, install_excepthook, Diagnosis - -diag = diagnose(status_code=401, code="InvalidApiKey", message="no api key provided") -print(render(diag)) # 终端块(中英由 DASHSCOPE_DIAGNOSIS_LANG 决定) - -install_excepthook() # 未捕获异常 traceback 后追加诊断块(仅 stderr,幂等) - -# 挂载点: -err.diagnosis # DashScopeError 及全部子类 -resp.diagnosis # raise_on_error=False 的错误信封(成功时为 None) -``` - -## 8. 测试 - -`tests/v2/test_devkit_diagnosis.py`:按 status/code/message 三种维度匹配、未命中返回 None、env 切英文、render 含标题与 expert 提示与编号修复、`DashScopeError.diagnosis`、`SdkError` 继承、错误信封 `.diagnosis`(业务错误命中 / 成功为 None)、`install_excepthook` 渲染到 stderr。 diff --git a/md/vendor-sdk-research-compat.md b/md/vendor-sdk-research-compat.md deleted file mode 100644 index b91850fa..00000000 --- a/md/vendor-sdk-research-compat.md +++ /dev/null @@ -1,63 +0,0 @@ -# 国内/第三方大模型厂商 SDK 调研:接入百炼时 dashscope SDK/CLI 的兼容考量 - -> 调研对象:MiniMax、DeepSeek、Baseten、Kimi(Moonshot) -> 调研时间:2026-08-24 -> 问题:这些厂家的模型接入百炼(DashScope 平台)后,dashscope SDK/CLI 需要兼容什么,还是不需要? - ---- - -## 1. 四家 API/SDK 形态速览 - -| 厂商 | 协议形态 | 认证 | 自有 SDK | 特有扩展 | -| --- | --- | --- | --- | --- | -| **DeepSeek** | OpenAI/Anthropic 双兼容(官方原话 "API format compatible with OpenAI/Anthropic"),base_url `https://api.deepseek.com` + `/chat/completions` | Bearer token | 无自有 SDK,直接用 OpenAI SDK 改 base_url | `thinking: {type: enabled}` 思考模式、`reasoning_effort` 推理深度 | -| **Kimi(Moonshot)** | 完全 OpenAI 兼容(chat completions、models、token 估算、余额查询、files),改 base_url 即可 | Bearer token | 官方推荐用 openai 包 + 自定义 base_url,有最低版本要求 | `thinking` 参数走 `extra_body`;partial 标记挂在消息上而非请求级 | -| **MiniMax** | 文本模型支持 **Anthropic SDK 和 OpenAI SDK 双接入**;非文本能力(视频、语音合成/声音克隆、图片、音乐、文档处理)走自有 REST API | API Key(按量)或 Subscription Key(预付)双形态 | 无统一自有 SDK,文本靠第三方 SDK + MCP | 语音合成走 socket 流式;异步媒体任务返回错误码 | -| **Baseten**(推理平台,非国内厂商) | OpenAI 兼容端点 + beta 的 Anthropic Messages 协议 | Bearer token(需覆盖 SDK 默认 key 传法) | 无自有,用 OpenAI/Anthropic 官方 SDK + model slug | 标准 HTTP 错误码 + JSON 错误体 | - -**共同事实:四家在"对话/推理"这条主线上全部收敛到 OpenAI 兼容协议**(MiniMax/Baseten 额外提供 Anthropic 协议)。没有一家要求接入方实现自己的私有 chat 协议。 - ---- - -## 2. 百炼侧现状 - -百炼托管第三方模型(DeepSeek、Kimi 等已在列)提供两条通路: - -1. **OpenAI 兼容模式**:`base_compatible_api_url`(`/compatible-mode/v1`),第三方模型的主推通路; -2. **DashScope native 协议**:部分第三方模型同时以 native 形态暴露(走 Generation 类接口)。 - -即:**厂商协议的差异在百炼网关侧已被翻译成上述两种形态之一**,dashscope SDK 面对的从来不是厂商原始端点,而是百炼网关。 - ---- - -## 3. 结论:需要做专门兼容吗?——基本不需要,但有 6 个具体落点 - -**总判断**:不需要为任何一家厂商做专属 adapter/资源类。厂商协议已行业收敛(OpenAI 为主、Anthropic 为辅),2.0 方案 §3 的 `protocol=openai|anthropic|native` 三适配器就是覆盖这四家(及未来所有厂商)的通用机制。真正要做的是把"厂商特有语义"在通用协议里表达出来: - -| # | 落点 | 说明 | 对应 2.0 方案 | -| --- | --- | --- | --- | -| 1 | **推理/思考参数的透传与一等化** | DeepSeek `thinking`/`reasoning_effort`、Kimi `thinking`(走 extra_body)在百炼兼容模式下同样存在。SDK 需要:`extra_body`/未知参数透传逃生舱(默认);高频参数如 `enable_thinking`/`reasoning_effort` 升级为显式签名参数,思考内容在响应模型中有字段(`reasoning_content`),流式中单独事件化 | §2.2 类型化 + 逃逸舱 | -| 2 | **模型目录可发现性** | 第三方模型在百炼的 model id 与厂商原始 id 不同(带平台前缀/版本)。`client.models.list()` 需支持按 provider/来源过滤,文档给出"厂商名 → 百炼 model id"对照表;CLI `dashscope models list --provider deepseek` | §2.2 models 资源 | -| 3 | **错误语义映射** | 厂商错误码(DeepSeek/Kimi 的结构化 error 对象)经百炼网关后会变成 DashScope 字符串 code。异常层需保证兼容模式下也能拿到可读错误;`ErrorCode` 枚举覆盖第三方模型常见失败(内容审核、配额、模型不存在) | §2.2 错误模型 | -| 4 | **多 API Key 形态**(仅 MiniMax 相关) | MiniMax 有 API Key / Subscription Key 双形态;百炼侧统一为一个 key,**无需兼容**。但若用户用 `protocol=openai` 直连厂商端点(SDK 支持任意兼容端点),key 形态对 SDK 透明(都是 Bearer),也无需处理 | 无需改动 | -| 5 | **非 chat 厂商能力不做兼容** | MiniMax 的视频生成/声音克隆/音乐、Baseten 的自定义模型部署,是厂商私有能力。百炼若要提供,会以 dashscope native 异步任务形态落地(走现有 images/videos/audio 资源树 + jobs 模式),**不是协议兼容问题** | §2.2 长任务统一模式 | -| 6 | **CLI 文档与示例** | `dashscope chat create -m deepseek-...` / `-m kimi-...` 直接可用是卖点,但需示例展示厂商特有参数(`--extra-body '{"thinking":{"type":"enabled"}}'`)和 compatible-mode 与 native 的选择指引 | §4 CLI | - -## 4. 与 2.0 方案的校验 - -- §3 协议层设计(openai/anthropic/native 三 adapter + `strict` 参数翻译策略)**足以覆盖这四家**,无需新增"vendor adapter"维度; -- 唯一需要在 §3 补充的设计细节:**"未知参数透传"必须是默认行为**(strict=False 静默透传而非丢弃)——厂商新参数(如 reasoning 类)上线快于 SDK 发版,透传是兼容性保险; -- acli/expert 侧:providers 已按协议接入,第三方模型在 expert 里天然可用,无额外工作。 - -## 5. 一句话总结 - -**不需要为 MiniMax/DeepSeek/Baseten/Kimi 做专门兼容**——它们的对话协议已全部收敛到 OpenAI/Anthropic 兼容,百炼网关又做了一层收敛。dashscope 2.0 要做的是三件通用事:协议适配层(已规划)、厂商特有参数的透传 + 高频参数一等化(reasoning/thinking)、第三方模型在 models 目录中的可发现性。 - ---- - -## 参考 - -- MiniMax API Overview:https://platform.minimax.io/docs/api-reference/api-overview -- DeepSeek API Docs:https://api-docs.deepseek.com/ -- Baseten Inference APIs:https://docs.baseten.co/inference/model-apis/overview -- Kimi API Overview:https://platform.kimi.ai/docs/api/overview diff --git a/md/zero-cost-local-sandbox.md b/md/zero-cost-local-sandbox.md deleted file mode 100644 index 1e0aee7c..00000000 --- a/md/zero-cost-local-sandbox.md +++ /dev/null @@ -1,76 +0,0 @@ -# 零成本本地沙箱(Zero-Cost Local Sandbox)— dashscope 2.0 设计 - -> 状态:已落地(v2)。对应实现:`dashscope/v2/devkit/mock.py`,挂载点 `dashscope/v2/transport/http.py`。 -> 来源:1.x 提案 `zero-cost-local-sandbox`(方式 A),针对 2.0 资源式架构重写,非简单迁移。 - -## 1. 需求与目标 - -开发者用大模型 Agent 辅助写代码时,Agent 会反复跑单测、调试循环逻辑。每次都打云端 API 带来三个问题:账单激增、触发限流、受外网时延拖累。目标:调试阶段一行开启 mock,零云端 token 消耗,业务代码零改动。 - -```python -import dashscope.v2 as v2 - -v2.enable_dev_mock(mode="static") # 或环境变量 DASHSCOPE_DEV_MOCK=static -client = v2.Client(api_key="sk-xxx") -resp = client.chat.completions.create( - model="qwen-plus", - messages=[{"role": "user", "content": "写一首关于阿里云百炼的诗"}], -) -print(resp.output_text) # "[MOCK] static response for model=qwen-plus" -``` - -## 2. 拦截点:2.0 与 1.x 的关键差异 - -1.x 的唯一 choke point 是 `BaseApi.call`,mock 在此短路并直接返回**已解析的** `DashScopeAPIResponse`。 - -2.0 没有 `BaseApi.call`:每个资源(`chat.completions` / `multimodal.chat` / `embeddings` / …)各自构造请求,统一经 **transport 层** 的四个方法出口: - -``` -SyncTransport.request / open_stream -AsyncTransport.request / open_stream -``` - -transport 返回的是**原始 `httpx.Response`**(wire 字节),协议解析(native / openai / anthropic 适配器 + 类型化模型)在其后的资源层完成。 - -因此 v2 mock 的正确落点是 transport,且**合成的是 wire 响应而非成品对象**——真实解析管线原样跑在 mock 数据上。这比 1.x 更忠实:dev loop 验证的是与生产完全一致的解码路径(协议适配器、SSE 分帧、typed model 构造、流式聚合)。 - -**生产零成本挂载**:transport 每次发送前调 `_dev_mock_requested()`——一次 `DASHSCOPE_DEV_MOCK` 环境变量读取 + 一次 `sys.modules.get("dashscope.v2.devkit")` 探测;只有旗标为真时才 `from dashscope.v2 import devkit`。未开启时不导入 devkit、成功路径无额外依赖。真实发送逻辑抽成 `_*_impl`,mock 分支用一个 `real` 闭包(`functools.partial`)持有它,供 record 模式透传。 - -## 3. 三模式语义 - -- **static**:零网络。按**请求 path 推断协议**(与适配器同源信号),合成该协议能正确解析的 wire body: - - native:`/services/aigc/...generation` → `output.choices[].message` 形态; - - openai:`/chat/completions` → `choices[].message` + `usage`; - - anthropic:`/messages` → `content[]` blocks + `stop_reason`。 - 文本打上抹不掉的 `[MOCK]` 前缀,`request_id="mock-*"`,usage 给确定性估算。 -- **record**:透传真实调用,把 (请求指纹 → 原始 wire 字节) 追加写入 JSONL fixture;**headers/Authorization 一律不落盘**(只存 status_code / content_type / body 文本)。流式用 tee 字节流边转发边捕获,保持直播不缓冲。 -- **replay**:按请求指纹(method + path + body + params 的 sha256,**排除 headers**)查 fixture 回放,重建 `httpx.Response`;**未命中 = 硬报错** `DevMockMiss`(继承 `DashScopeError`),绝不静默走网络。record/replay 与协议无关,覆盖**所有**资源。 - -## 4. 覆盖边界:宁可loud,不可静默错配 - -static 只建模 chat / multimodal 三个协议的 wire 形态。对非 chat 端点(embeddings / images / files / …),static **抛 `DevMockMiss`** 明确提示改用 record/replay,而不是返回一个会被该端点 typed parser 静默误读的 body。这正是 1.x 提案「别让 Agent 在静默错配里空转」原则的 v2 落地。 - -## 5. 流式 - -static 按 `stream=True` 产出增量 SSE 帧序列(native 多帧末片 `finish_reason=stop` 带 usage;openai 追加 `data: [DONE]`;anthropic 走 `message_start → content_block_delta… → message_delta → message_stop`),使 `for chunk in stream` / `async for` 的消费与聚合路径与真实 SSE 完全一致——不接受「只 mock 非流式」。 - -## 6. 明确不做 - -- websocket 通路(realtime 语音 / 多模态对话)的 mock; -- 方式 B(环境变量桥接本地 Ollama)的 native↔OpenAI 翻译 shim——新代码走 OpenAI client 指本地即可,存量 native 翻译单独立项; -- SDK 侧影子流量。 - -## 7. 公开 API - -```python -dashscope.v2.enable_dev_mock(mode="static" | "replay" | "record", fixture_path=None) -dashscope.v2.disable_dev_mock() -dashscope.v2.dev_mock_active() -> bool -# 等价物:dashscope.v2.devkit.{enable,disable,active,intercept,aintercept} -``` - -环境变量:`DASHSCOPE_DEV_MOCK=static|replay|record`、`DASHSCOPE_DEV_MOCK_FIXTURE=`(replay/record 默认 `./dashscope_mock.jsonl`)。代码调用优先于环境变量。 - -## 8. 测试 - -`tests/v2/test_devkit_mock.py`:static 非流式/流式、三协议、无网络证明(指向 dead host)、多模态 path、非 chat 端点 loud miss、env 激活、非法 mode 报错、disable 恢复透传、record 落盘不含凭证、record→replay 往返(同步+异步、流式+非流式)、replay 未命中抛 `DevMockMiss`。 From 6f378c672ec4956e2d04c9804dd4ff9f3e26056b Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 15:12:29 +0800 Subject: [PATCH 18/41] docs(acli): update expert guidance for SDK 2.0 --- .../.acli/skills/agent.md | 193 +++++++++----- .../.acli/skills/api-doc.md | 17 +- .../dashscope-sdk-expert/.acli/skills/cli.md | 24 +- .../.acli/skills/diagnose.md | 12 +- .../.acli/skills/error-code.md | 74 +++++- .../.acli/skills/fine-tuning.md | 199 +++++++++----- .../.acli/skills/multimodal.md | 222 ++++++++++++---- .../.acli/skills/retrieval.md | 250 +++++++++++++----- .../.acli/skills/sdk-example.md | 17 +- .../.acli/skills/speech.md | 215 ++++++++++----- .../.acli/skills/text-generation.md | 250 +++++++++++------- .../.acli/system-prompt.md | 79 +++--- .../examples/dashscope-sdk-expert/README.md | 24 +- 13 files changed, 1086 insertions(+), 490 deletions(-) diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md index 75762b58..858b82ad 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md @@ -4,83 +4,142 @@ description: Quick reference for Agent APIs (agentstudio-hosted Agent/Applicatio --- # Agent +For Python SDK 2.0.3, use `dashscope.Client` / `dashscope.AsyncClient` at the package root; `dashscope.v2` does not exist. Close the client with a context manager or `close()`/`aclose()`. New application calls use `client.apps.completions`; hosted-agent management uses the lazily mounted `client.agentstudio`. Assistants APIs below are exclusively for 1.x maintenance. + ## Use Cases and Selection -- `dashscope.Application`: calls applications published in the Bailian console (AppBuilder/RAG/workflow/plugin apps) directly by `app_id`; no need to manage models or tools — the simplest option. -- `dashscope.agentstudio`: full lifecycle management of AgentStudio-hosted Agents (agents/sessions/environments/files/skills/deployments), with SSE event streaming, client-side tools and MCP; suitable when you need a hosted runtime and multi-turn sessions. -- `dashscope.Assistants/Threads/Runs`: OpenAI-like Assistants API. **Deprecated** (DeprecationWarning in source; migrate to the Responses API); use only for maintaining legacy code. - -## SDK APIs -### agentstudio (`from dashscope.agentstudio import Client, AsyncClient`) -- `Client(api_key=, workspace=, region=None, base_url=None, uid=None, timeout=None, max_retries=2)`; if base_url is not given, `workspace` (or env var `DASHSCOPE_WORKSPACE`) is required; base_url can also be set via `DASHSCOPE_AGENTSTUDIO_URL`/`AGENTSTUDIO_URL`; default region `cn-beijing`, timeout 600s. -- Resource entry points: `client.agents / .sessions / .environments / .files / .skills / .vaults / .deployments / .deployment_runs`. -- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, multiagent=None, metadata=None)` → `Agent`; `model` is a `ModelConfig(id=…, temperature=…, max_tokens=…, enable_thinking=…, thinking_budget=…)` (bare id string = `ModelConfig(id=…)`); `retrieve(agent_id, *, version=None)` (alias `get`); `update(agent_id, *, version, name, model=None, ...)` — version must equal the current version (HTTP 409), `name` is required (`AGENT_005`), and a sent `model` replaces the whole config (keep the rest via `agent.model.copy_with(...)`); `archive(agent_id)`; `list(*, limit, page, include_archived)` → `CursorPage`; `list_versions(agent_id)`. -- `sessions.create(*, agent, environment_id=None, title=None, resources=None, metadata=None)` → `Session`; also `retrieve/update(title, metadata)/list(*, limit, page, agent_id, statuses, created_at_gt/gte/lt/lte)/archive/delete`. -- `environments.create(*, name, config, description=None, scope="organization", metadata=None)`; `files.upload(file, *, mime_type=None, progress=None)` (file accepts a path/binary stream/(name, fileobj)); `skills.create(*, file_id=None, file=None, mime_type=None)`, `skills.versions.create(skill_id, ...)`. - -### agentstudio SSE Streaming Execution (Core) -1. `client.sessions.events.send(session_id, events)`: sends an event list (at least 1 event, otherwise `ValueError`); request body is `{"input": [...]}`. Use the constructors in `types`: `user_message(text_or_blocks, *, session_thread_id=None, metadata=None)`, `user_interrupt()`, `user_tool_confirmation()`, `user_tool_result()`, `user_custom_tool_result()`, `user_define_outcome()`. -2. `with client.sessions.events.stream(session_id, *, timeout=None) as stream:` opens the SSE stream (GET `/sessions/{id}/events/stream`); iterating yields `Message` (`ServerEvent`) objects: `event.type`, `event.content` (block list), `event.stop_reason`, `event.session_status`. -3. `stream.text_stream`: yields only the text of blocks with `type=="text"` from `message` events, and stops automatically when `session_status` becomes `idle/terminated/rescheduling` — the most convenient option. -- Event types (`SSEEventType`): clients may send `message/interrupt/tool_confirmation/function_call_output/tool_call_output/define_outcome`; the server additionally sends `reasoning/tool_call/tool_call_output/mcp_call/mcp_call_output/session_status/error/thread_*/model_request_*/outcome_evaluation`, etc. Session state machine: `idle → running → (rescheduling) → idle | terminated`. - -### Application (Bailian App) -`Application.call(app_id, prompt=None, history=None, workspace=None, api_key=None, messages=None, **kwargs)`; `app_id` is required (raises `InputRequired` if missing); at least one of `prompt` or `messages` is required. kwargs: `stream`, `incremental_output`, `session_id` (for multi-turn continuation, pass the session_id from the previous response), `biz_params` (flow/plugin business parameters), `has_thoughts`, `doc_tag_codes`, `doc_reference_type` (`simple`/`indexed`), `memory_id`, `image_list`, `file_list`, `rag_options`, `temperature/top_p/top_k/seed`. - -### Assistants / Threads / Runs (Deprecated) -- `Assistants.call/create(*, model, name=None, description=None, instructions=None, tools=None, file_ids=[], metadata=None, workspace=None, api_key=None, top_p/top_k/temperature/max_tokens=None)`; `retrieve/list/update/delete`. -- `Threads.create(*, messages=None, metadata=None)`; `Messages.create(thread_id, *, content, role="user", file_ids=[], metadata=None)`. -- `Runs.create(thread_id, *, assistant_id, instructions=None, tools=None, stream=False, ...)`; `Runs.create_thread_and_run(*, assistant_id, thread=None, stream=False, ...)`; `Runs.retrieve(run_id, *, thread_id)`; `Runs.submit_tool_outputs(run_id, *, thread_id, tool_outputs, stream=False)`; `Runs.cancel(...)`. -## Inputs/Outputs -- **Application**: returns `ApplicationResponse` (`status_code/request_id/code/message`); `output` is an `ApplicationOutput`: `text` (answer text), `finish_reason`, `session_id` (returned for multi-turn), `thoughts` (plugin/RAG process when has_thoughts=True), `doc_references`, `workflow_message`; `usage.models[]` contains `model_id/input_tokens/output_tokens`. With `stream=True` a generator is returned; with `stream=True` and `incremental_output=False` the SDK automatically merges deltas into full output. **Failures do not raise exceptions** — you must check `response.status_code == HTTPStatus.OK`, otherwise read `code/message`. -- **agentstudio event stream**: each event is a `Message` (fields `id/type/role/content/metadata/status/session_thread_id/...`); `content` is a block list with block.type ∈ `text/image/audio/data/file/refusal/error`. The block.data of a `session_status` event contains `session_status` and `stop_reason` (e.g. `{"type": "end_turn"}`). -- **Assistants Run state machine**: `queued → in_progress → (requires_action) → completed | failed | cancelled | expired` (plus `cancelling`); on `requires_action`, take tool calls from `run.required_action.submit_tool_outputs`, execute them, send results back via `Runs.submit_tool_outputs`, and poll `retrieve` until `completed`. +- **Bailian applications:** `client.apps.completions.create(...)` invokes an AppBuilder/RAG/workflow/plugin application by `app_id`; application tools and flows remain configured in the console. +- **AgentStudio:** `client.agentstudio` manages hosted agents, sessions, environments, files, skills, deployments, and related resources, with typed models and SSE execution streams. +- **Assistants / Threads / Messages / Runs / Steps:** deprecated 1.x compatibility APIs for repairing existing applications only. There is no 2.0 `Responses` resource, so do not recommend migrating these APIs to Responses. Choose Apps or AgentStudio for new hosted-agent scenarios based on the required service, not a nonexistent resource. + +## Bailian Applications (`client.apps`) + +```text +client.apps.completions.create( + *, app_id, prompt=None, messages=None, stream=False, session_id=None, + biz_params=None, has_thoughts=None, ..., parameters=None, timeout=None, +) +``` -## Plugin and MCP Integration -- agentstudio: pass a list of function/plugin schemas to `tools` of `agents.create` and a list of MCP server configs to `mcp_servers`; the runtime automatically emits `tool_call`/`mcp_call` events; after executing local tools, send results back with `user_tool_result(...)`/`user_tool_confirmation(...)` events. -- Application: plugins are bound to the app in the Bailian console; callers only pass plugin/flow parameters via `biz_params`; set `has_thoughts=True` to see the invocation process. -- Assistants: `tools=[{"type": ...}]`, combined with the Run `requires_action` loop. +Supply `prompt` or `messages`; when both are present, `prompt` becomes the latest user message. Other supported options include document/memory/image/file inputs, `rag_options`, `incremental_output`, model controls, and service-specific `parameters`. For a continued conversation, pass the previous result's `session_id`. -## Minimal Examples ```python -from http import HTTPStatus -from dashscope import Application -resp = Application.call(app_id="YOUR_APP_ID", prompt="Introduce Bailian", has_thoughts=True) -if resp.status_code == HTTPStatus.OK: - print(resp.output.text, resp.output.session_id) -else: - print(resp.request_id, resp.code, resp.message) +import dashscope + +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + result = client.apps.completions.create( + app_id="YOUR_APP_ID", + prompt="Introduce Bailian", + has_thoughts=True, + ) + print(result.text) + print(result.session_id, result.finish_reason) ``` + +`AppCompletion` exposes flat `text`, `finish_reason`, `session_id`, `thoughts`, `doc_references`, `workflow_message`, and `usage`, together with common response metadata. It does not require legacy `.output.text` access. + +With `stream=True`, the return value is `AppStream` (`AsyncAppStream` on `AsyncClient`). Iterate `text_stream` for text and close the stream when finished, especially on early exit: + ```python -from dashscope.agentstudio import Client +import dashscope + +with dashscope.Client() as client: + stream = client.apps.completions.create( + app_id="YOUR_APP_ID", + prompt="Write a short greeting", + stream=True, + incremental_output=True, + ) + try: + for text in stream.text_stream: + print(text, end="") + finally: + stream.close() +``` + +## AgentStudio (`client.agentstudio`) + +Mount AgentStudio from the main client so configuration and cleanup stay connected: + +```python +import dashscope from dashscope.agentstudio.types import user_message -client = Client(api_key="sk-xxx", workspace="my-ws") -agent = client.agents.create(name="demo", model="qwen3.8-flash") -session = client.sessions.create(agent=agent.id) -client.sessions.events.send(session.id, [user_message("Hello!")]) -with client.sessions.events.stream(session.id) as stream: - for text in stream.text_stream: # auto-stops on idle/terminated - print(text, end="") + +with dashscope.Client(workspace="my-ws") as client: + studio = client.agentstudio + agent = studio.agents.create(name="demo", model="qwen3.8-flash") + session = studio.sessions.create(agent=agent.id) + studio.sessions.events.send(session.id, [user_message("Hello!")]) + with studio.sessions.events.stream(session.id) as stream: + for text in stream.text_stream: + print(text, end="") +# Closing the main client also closes its mounted AgentStudio client. ``` -## Common Error Codes -| Error code/exception | HTTP status | Meaning | Handling | -| --- | --- | --- | --- | -| `InputRequired` / `ModelRequired` / `InvalidInput` | raised locally | missing required params such as app_id/prompt/model | add params per the exception message; Application failures do not raise — check status_code | -| `invalid_request_error` / `request_too_large` (InvalidRequestError) | 400/413 | invalid agentstudio request parameters, or payload too large | check parameter names/types, e.g. empty events raises a local ValueError | -| `authentication_error` (AuthenticationError) | 401 | api_key missing or invalid | check api_key or DASHSCOPE_API_KEY | -| `permission_denied_error` / `permission_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership | -| `not_found_error` (NotFoundError) | 404 | agent/session and other resources do not exist | verify the id and region/workspace are correct | -| `conflict_error` (ConflictError) | 409 | version in agents.update does not match the server | retrieve the latest version first, then update | -| `rate_limit_error` / `billing_error` (RateLimitError) | 429 | rate limited or out of quota | retry with backoff (client default max_retries=2) | -| `api_error` / `timeout_error` (InternalServerError), `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry | -| `APITimeoutError` / `APIConnectionError` | no response | connection/read/write timeout | increase timeout, check network, then retry | -| `StreamError` / `StreamClosedError` | mid-stream | SSE protocol error/stream closed | call `events.stream` again to reopen the stream | - -Match on the exception class, not on `.code`: `.code` echoes the server's raw -string, so a 403 may arrive as either `permission_denied_error` or -`permission_error`, and it is normalised to `api_error` when the server omits -it. The class is always the one listed above. +`Client(api_key=..., workspace=..., region=..., ...)` forwards API key, workspace, and region to AgentStudio. A workspace is required unless an AgentStudio base URL is configured. Standalone `dashscope.agentstudio.Client/AsyncClient` remain available, but are unnecessary for normal 2.0 main-client usage. + +Mounted resources are `agents`, `deployments`, `deployment_runs`, `sessions`, `environments`, `files`, `skills`, `vaults`, `memory_stores`, `webhook_endpoints`, and `security`. + +### Agents and Sessions + +- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, multiagent=None, metadata=None)` returns `Agent`. `model` can be a model ID string or `ModelConfig(id=..., temperature=..., max_tokens=..., enable_thinking=..., thinking_budget=...)`. +- `agents.retrieve(agent_id, version=None)` (alias `get`), `list(...)`, and `list_versions(agent_id)` query agent state. `archive(agent_id)` archives it. +- `agents.update(agent_id, *, version, name, model=None, ...)` is optimistic-concurrency checked: `version` must match the current version. The service also requires `name`; sending `model` replaces its model configuration, so retain unspecified settings (for example with the existing model's `copy_with(...)`) rather than accidentally dropping them. +- `sessions.create(*, agent, environment_id=None, title=None, resources=None, vault_ids=None, environment_variables=None, mcp_configs=None, metadata=None)` returns `Session`. Sessions also support `retrieve`/`get`, `update`, `list`, `archive`, and `delete`. +- `environments.create(*, name, config, description=None, scope="organization", metadata=None)` creates an execution environment. `files.upload(file, *, mime_type=None, progress=None)` accepts a path, binary stream, or `(name, fileobj)`. Skills can be created from `file_id` or `file`, with nested version APIs. + +### AgentStudio SSE Execution + +1. `studio.sessions.events.send(session_id, events)` sends a non-empty event list; an empty list raises local `ValueError`. Constructors in `dashscope.agentstudio.types` include `user_message(...)`, `user_interrupt()`, `user_tool_confirmation()`, `user_tool_result()`, `user_custom_tool_result()`, and `user_define_outcome()`. +2. `with studio.sessions.events.stream(session_id, event_deltas=None, timeout=None) as stream:` opens a single-pass SSE stream. Iterating it yields typed server `Message` events with fields such as `type`, `content`, `stop_reason`, and `session_status`. +3. `stream.text_stream` yields text blocks from message events and stops on terminal stream/session states such as `idle`, `terminated`, or `rescheduling`. Opt into `event_deltas` when incremental `text_deltas` are required; do not assume all delta types arrive by default. + +Content blocks can represent text, images, audio, data, files, refusal, or errors. Client event types include message, interrupt, tool confirmation/results, and defined outcomes; server events additionally include reasoning, tool/MCP calls, session status, thread, model-request, and outcome-evaluation events. A typical session state flow is `idle → running → (rescheduling) → idle | terminated`. + +### Plugins, Tools, and MCP + +- AgentStudio: pass tool schemas through `agents.create(tools=[...])` and MCP configurations through `mcp_servers`. The runtime emits tool/MCP call events; execute client-side tools as required and send results or confirmations back through session events. +- Apps: bind plugins and workflows in the Bailian console. Callers pass business parameters with `biz_params`; `has_thoughts=True` requests process information in flat `thoughts`. + +## Errors (Brief) + +New code catches API exception classes from `dashscope.errors`; AgentStudio's stream-specific `StreamError` / `StreamClosedError` remain in `dashscope.agentstudio.exceptions`. Match exception classes rather than inferring the class solely from a raw wire `.code`. + +| HTTP / condition | Current class | Handling | +| --- | --- | --- | +| 400 | `BadRequestError` (`InvalidRequestError` is an alias) | Check parameter names/types; empty event lists are local `ValueError` | +| 401 | `AuthenticationError` | Check `DASHSCOPE_API_KEY` or explicit `api_key` | +| 403 | `PermissionDeniedError` | Check workspace, UID, region, and resource ownership | +| 413 | `RequestTooLargeError` | Reduce payload size and check service limits | +| 404 | `NotFoundError` | Verify agent/session/resource IDs | +| 409 | `APIStatusError` (there is no dedicated conflict catalog class) | Retrieve the latest agent version before retrying update | +| 429 | `RateLimitError` | Retry with backoff and check quota | +| 503 / mapped overloaded error | `ServiceUnavailableError` (`OverloadedError` is an alias) | Retry with backoff | +| 504 | `GatewayTimeoutError` | Retry or troubleshoot with `request_id`; do not label it `InternalServerError` | +| 500 or a code mapped to internal error | `InternalServerError` | Retry where appropriate and retain `request_id` | +| Unmapped HTTP status, including an unknown 5xx | `APIStatusError` | Inspect status/code/body; do not blindly guess `InternalServerError`. An unrecognized wire code may still use the known HTTP status mapping. | +| Connection/read/write timeout | `APIConnectionError` / `APITimeoutError` | Check network and timeout before retrying | +| AgentStudio stream protocol/closed stream | `StreamError` / `StreamClosedError` | Reopen `events.stream` when the operation is retryable | + +See the dedicated errors guide for the complete hierarchy and retry rules. + +## 1.x Compatibility — Maintenance Only + +These APIs retain their old response envelopes and local exception behavior. Do not mix them with 2.0 flat `AppCompletion` models or `client.agentstudio`. + +### Legacy Application + +`dashscope.Application.call(app_id, prompt=None, history=None, workspace=None, api_key=None, messages=None, **kwargs)` invokes a published app. At least one of `prompt` or `messages` is required; missing `app_id` raises `InputRequired`. Legacy kwargs include `stream`, `incremental_output`, `session_id`, `biz_params`, `has_thoughts`, document/memory/image/file inputs, `rag_options`, and model controls. + +`ApplicationResponse.output` contains `text`, `finish_reason`, `session_id`, `thoughts`, `doc_references`, and `workflow_message`. Legacy failures generally do not raise: check `status_code`, then inspect `code/message/request_id`. This is the opposite of the default 2.0 exception flow. + +### Legacy Assistants / Threads / Messages / Runs / Steps + +- `Assistants.call/create(*, model, name=None, description=None, instructions=None, tools=None, file_ids=[], metadata=None, ...)`; `retrieve/list/update/delete`. +- `Threads.create(*, messages=None, metadata=None)`; `Messages.create(thread_id, *, content, role="user", file_ids=[], metadata=None)`. +- `Runs.create(thread_id, *, assistant_id, instructions=None, tools=None, stream=False, ...)` and `Runs.create_thread_and_run(...)`; also `retrieve`, `submit_tool_outputs`, and `cancel`. Steps belong to this same legacy maintenance surface. +- Run states: `queued → in_progress → (requires_action) → completed | failed | cancelled | expired`, with `cancelling` also possible. On `requires_action`, read `run.required_action.submit_tool_outputs`, execute the requested tools, submit outputs, and continue polling. +- Legacy tool schemas and the Run tool-output loop must remain in this compatibility implementation; do not rewrite them as AgentStudio events unless intentionally migrating the application. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md index e46a684b..b37a5661 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/api-doc.md @@ -4,11 +4,16 @@ description: Look up SDK API parameters and usage arguments: [api_name] --- -First use run_command to check the installed SDK version and the actual API signature: -1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -2. `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.{api_name}.call))'")` -3. If the signature is not clear enough, use `run_command("python -c 'import dashscope; help(dashscope.{api_name})'")` to get the full documentation. +First identify whether `{api_name}` refers to a 1.x module/class API or a 2.0 client resource, then inspect the installed SDK: -Based on the actual inspection results, explain all parameters of {api_name}, the return value format, and the supported model list, and provide a typical usage example (Python). +1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +2. Resolve the exact public object and method. Use `inspect.signature` on that bound method; use `help()` on the resource or response model when parameter or return-field documentation is insufficient. Do not mechanically append `.call` to `{api_name}`. +3. For a 2.0 resource, a local `Client(api_key="sk-test")` is enough for inspection and does not make a network request. Close it afterwards. Read the resource implementation or Pydantic response model only when inspection leaves a specific question unresolved. +4. Consult the matching domain skill for the current 2.0.3 mapping and the 1.x compatibility notes. -Note: parameter descriptions must match the inspection results. Do not answer from memory. +Explain the parameters, return type and fields, streaming/task lifecycle where applicable, and supported models from the verified implementation. Include a typical Python example and distinguish these contracts explicitly: + +- 2.0: `Client` / `AsyncClient` resource methods, flattened typed responses, `dashscope.errors` exceptions, and `jobs.create/fetch/wait` for supported long-running tasks. +- 1.x: the existing `.call()` signature and its actual response or task fields; do not rewrite them as 2.0 fields. + +Parameter descriptions and model support must match the inspection results. If the installed version differs from the knowledge base, state the difference instead of guessing. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md index 20aa30ff..25433ea6 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/cli.md @@ -5,11 +5,11 @@ description: Quick reference for all dashscope CLI command groups, subcommands, # dashscope CLI -This file is authoritative for answering `dashscope` CLI usage questions (based on the dashscope/cli/ source code); do not invent options. For details, have the user run `dashscope --help` to verify. +This file is authoritative for answering `dashscope` CLI usage questions (based on `dashscope/legacy_1x/cli/`); do not invent options. **The CLI is still implemented by the 1.x SDK, including its response envelopes and static resource classes. It is not a command facade over 2.0 `dashscope.Client` / `AsyncClient`.** For details, have the user run `dashscope --help` to verify. ## Command overview -| Command group | Subcommands | Purpose | Corresponding SDK class | +| Command group | Subcommands | Purpose | Corresponding 1.x SDK implementation | | --- | --- | --- | --- | | auth | whoami/login/logout | Verify the active API key and persist one to ~/.dashscope/api_key | dashscope.common.api_key (get_default_api_key/save_api_key) | | generation | create | Text generation (including streaming and multi-turn messages) | dashscope.Generation | @@ -22,16 +22,18 @@ This file is authoritative for answering `dashscope` CLI usage questions (based | tokenization | create | Tokenization/token counting | dashscope.Tokenization | | models | list/get | Query available models | dashscope.Models | | application | create | Call a Bailian application | dashscope.Application | -| image-synthesis | create | Text-to-image (async task) | dashscope.ImageSynthesis | -| image-generation | create | Image generation (supports reference images) | dashscope.ImageGeneration | +| image-synthesis | create/fetch/wait/cancel/list | Full text-to-image task lifecycle | dashscope.ImageSynthesis | +| image-generation | create/fetch/wait/cancel/list | Full image-generation task lifecycle, including reference images | `dashscope.legacy_1x.aigc.image_generation.ImageGeneration` | | video-synthesis | create/fetch/wait/cancel/list | Full video synthesis task lifecycle | dashscope.VideoSynthesis | | multimodal-conversation | create | Multimodal conversation | dashscope.MultiModalConversation | | multimodal-embedding | create | Multimodal embeddings (text/image/audio) | dashscope.MultiModalEmbedding | -| transcription | create | Audio file transcription (async task) | dashscope.Transcription | +| transcription | create/fetch/wait/cancel/list | Full audio file transcription task lifecycle | dashscope.Transcription | | speech-synthesis | create | Speech synthesis TTS | dashscope.HttpSpeechSynthesizer | -| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | dashscope.legacy_1x.finetune.agentic_rl.AgenticRL | +| rl (hidden alias agentic-rl) | run/get/cancel/logs/list/register_functions/test_functions/upload_data | Agentic RL fine-tuning | 1.x AgenticRL compatibility wrapper, not `client.rl` | -Note: code-generation and understanding still exist in the source but are deprecated/discontinued and are not registered in the CLI. +Notes: +- `dashscope.ImageGeneration` is **not** a root public export. The CLI imports the internal 1.x class shown above; do not recommend the nonexistent root import. +- code-generation and understanding still exist in the source but are deprecated/discontinued and are not registered in the CLI. ## Common usage @@ -44,7 +46,7 @@ dashscope auth logout # remove the saved key file dashscope generation create -m qwen3.7-plus -p "Hello" -s --temperature 0.7 dashscope ft create -m qwen3.8-32b -t file-xxx -v file-yyy -e 2 -b 16 -l 1e-5 dashscope files upload -f train.jsonl -p fine-tune -d "training set" -dashscope deployments create -m qwen3.8-32b -s demo -c 1 +dashscope deployments create -m qwen3.8-32b -s demo -c 1 --plan ptu dashscope oss upload -m qwen3.7-plus -f ./pic.png dashscope rerank create -m gte-rerank -q "query" -d doc1 -d doc2 -n 3 dashscope embeddings create -m text-embedding-v3 -i "text1" -i "text2" @@ -54,7 +56,9 @@ dashscope application create -a APP_ID -p "question" --has-thoughts dashscope image-synthesis create -m qwen-image-3.0 -p "a cat" -n 2 --size 1024*1024 dashscope image-generation create -m qwen-image-3.0 -t "a cat" --image ref.png dashscope video-synthesis create -m wan2.2-t2v-plus -p "ocean waves" -dashscope video-synthesis wait TASK_ID # fetch/cancel/list take task_id the same way +dashscope video-synthesis wait TASK_ID # fetch/cancel also take TASK_ID; list is a query +# image-synthesis, image-generation, and transcription expose the same +# create/fetch/wait/cancel/list task lifecycle. dashscope multimodal-conversation create -m qwen3-vl-plus -t "describe the image" --image a.jpg dashscope multimodal-embedding create -m multimodal-embedding-v1 --text "cat" --image c.png dashscope transcription create -m paraformer-v2 --file-url https://.../a.wav @@ -69,7 +73,7 @@ Key option notes: - generation create: `-p/--prompt`, `--messages` (JSON string), `-s/--stream`, `--temperature/--top-p/--top-k/--max-tokens/--seed/--stop/--repetition-penalty/--presence-penalty/--enable-search/--n/--result-format` - ft create: `-t/--training-file-ids` (repeatable), `-v/--validation-file-ids`, `--mode`, `-e/--n-epochs`, `-b/--batch-size`, `-l/--learning-rate`, `-p/--prompt-loss`; get/stream/cancel/delete all take job_id as a positional argument; list uses `-p/--page`, `-s/--size` - files: upload `-f/--file`, `-p/--purpose` (default fine-tune), `-d/--description`; all commands support `-u/--base-url` -- deployments: create `-m/-s/--suffix/-c/--capacity/--plan/--template-id`; `scale DEPLOYED_MODEL -c 2`; after create it blocks by default (timeout 3600 seconds) +- deployments: create `-m/-s/--suffix/-c/--capacity/--plan/--template-id`; **`--plan` is required** (for example `--plan ptu`); `scale DEPLOYED_MODEL -c 2`; after create it blocks by default (timeout 3600 seconds) - oss upload: `-f/--file`, `-m/--model`, `-k/--api-key` (reads DASHSCOPE_API_KEY), `-u/--base-url` - rerank create: `-q/--query`, `-d/--document` (repeatable), `--return-documents/--no-return-documents`, `-n/--top-n`, `-i/--instruct` - video-synthesis create: `--img-url/--first-frame-url/--last-frame-url/--duration/--resolution/--ratio/--seed/--prompt-extend/--watermark`; list supports `--start-time/--end-time/--model-name/--status/--region` diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md index 6acac9f9..e84e3401 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/diagnose.md @@ -5,9 +5,11 @@ arguments: [file_path] --- Diagnostic steps: -1. Use `read_file` to read the user's code at {file_path}. -2. Check the installed SDK version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -3. If the code calls a specific API (e.g. `dashscope.Generation.call`), inspect its actual signature: `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'")` -4. If you need to understand SDK internals, locate the source with `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")`, then use `read_file` to read the relevant implementation. -Based on the actual findings above, point out the problems in the user's code and provide the complete fixed code. +1. Use `read_file` to read the user's code at `{file_path}`. Identify the API generation from the call itself: `Generation.call` and similar module/class APIs are 1.x; `Client` / `AsyncClient` resource calls are 2.0. An installed 2.0 package does not by itself make existing 1.x code wrong. +2. Check the installed version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +3. Inspect the exact called object and method with `inspect.signature`; for 2.0, inspect the bound resource method and relevant response model. Do not assume a `.call()` method or a `.output` response envelope. +4. Classify the failure from its actual type, `code`, `status_code`, `message`, and `request_id`. For 2.0, use the `dashscope.errors` hierarchy and distinguish HTTP/API errors from `SdkError` connection, timeout, and local failures. For 1.x, preserve its response-checking and exception conventions. +5. Only if a specific behavior remains unclear, locate the package with `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` and read the relevant implementation. + +Point out the root cause and provide complete fixed code. When repairing existing 1.x code, keep its style and contract unless the user explicitly requests migration. When writing or migrating to 2.0, use current resource methods, flattened responses, client lifecycle management, and `dashscope.errors`; do not mix 1.x and 2.0 return handling in the same example. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md index 1d061c5b..32e6cceb 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/error-code.md @@ -4,8 +4,74 @@ description: Explain a DashScope API error code and how to fix it arguments: [code] --- -First check how this error code is actually handled in the SDK: -1. `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` -2. Use `run_command("grep -rn '{code}' ")` to search the SDK source for where this error code is defined and handled. +First identify whether the failing call uses the 1.x response convention or the 2.0 exception convention. Inspect the installed SDK and search for `{code}` in the public error definitions rather than guessing from the message: -Based on the actual code, explain what DashScope API error code {code} means, list common triggers, and give concrete fix steps with code examples. +1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` +2. `run_command("python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'")` +3. Search the relevant error registry, protocol parser, or 1.x API implementation for the exact wire code. Preserve the original `code`, `message`, and `request_id` in the diagnosis. + +## Python 2.0 errors + +New code imports concrete exceptions from `dashscope.errors`. The package root directly exports `DashScopeError` and `ErrorCode`, not every concrete subclass. + +```python +from dashscope.errors import ( + BadRequestError, + ContentFilterError, + DashScopeError, + RateLimitError, +) + +try: + response = client.chat.completions.create( + model="qwen-plus", + messages="Hello", + ) +except ContentFilterError as error: + print("Content blocked", error.request_id) +except BadRequestError as error: + print("Bad request", error.code, error.message, error.request_id) +except RateLimitError as error: + print("Throttled", error.status_code, error.request_id) +except DashScopeError as error: + print(type(error).__name__, error.code, error.message, error.request_id) +``` + +`ContentFilterError` is a `BadRequestError` subclass, so its `except` clause must come first. + +Both HTTP non-2xx responses and HTTP 200 responses carrying a business error code use the same exception family. Common HTTP status classes are: + +| Status | Exception | +| --- | --- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 413 | `RequestTooLargeError` | +| 429 | `RateLimitError` | +| 500 | `InternalServerError` | +| 503 | `ServiceUnavailableError` | +| 504 | `GatewayTimeoutError` | + +Wire codes refine the meaning: for example, `Arrearage` maps to `PermissionDeniedError`, not 429, and throttling codes map to `RateLimitError`. A gateway timeout is `GatewayTimeoutError`; do not label every unknown 5xx as `InternalServerError` without checking the actual status and code. `ResponseError` concerns response parsing and is not synonymous with every HTTP 500. + +Exceptions expose `message`, `code`, `status_code`, `request_id`, `body`, and `headers` as applicable. Connection failures, timeouts, WebSocket failures, and SDK-local failures belong to the `SdkError` branch; they have no HTTP response, have `status_code is None`, and may also provide `solution` and `allow_retry`. Do not discard these fields by flattening every failure into a status-only message. + +With `raise_on_error=True` (the default), catch `DashScopeError` or a more specific subclass. If the user explicitly selects return-value mode, check `is_error`: + +```python +lenient = client.with_options(raise_on_error=False) +response = lenient.chat.completions.create(model="qwen-plus", messages="Hello") +if response.is_error: + print(response.code, response.message, response.request_id) +``` + +`raise_on_error=False` does not suppress connection failures or timeouts because no response object exists. In a stream, a business error may be represented by an `is_error=True` item before iteration ends. Automatic transport retries do not make authentication, permission, quota, or parameter errors safe to retry blindly. + +## Python 1.x and compatible-mode errors + +For an existing 1.x call such as `Generation.call`, preserve its contract: API failures commonly return a response whose `status_code`, `code`, `message`, and `request_id` must be checked, while local validation errors may still be raised. Do not convert this into a 2.0 `except DashScopeError` example unless the user is migrating. + +OpenAI-compatible code using the official `openai` package keeps that library's own exception hierarchy; it is not the `dashscope.errors` hierarchy. Java errors likewise follow the Java SDK's independent API and version line. + +Based on the verified implementation, explain what `{code}` means, common triggers, whether retry is safe, and concrete fix steps with an example in the user's existing SDK style. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md index dd2310ad..6abedf60 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/fine-tuning.md @@ -5,102 +5,161 @@ description: Quick reference for fine-tuning and deployment (FineTunes/Files/Dep # Fine-tuning and Deployment -> Quick-reference note: all parameter names and return fields on this page follow the SDK source code (`dashscope/applications/finetune/`, `dashscope/legacy_1x/finetune/`, `dashscope/legacy_1x/files.py`, `dashscope/legacy_1x/common/`). Check the signatures on this page before answering; if unsure, verify on the spot with `inspect.signature` instead of relying on memory. +> Python SDK 2.0.3 uses `dashscope.Client` / `dashscope.AsyncClient` at the package root (`dashscope.v2` does not exist). Verify current signatures in `dashscope/resources/`, workflow code in `dashscope/applications/finetune/`, and old implementations in `dashscope/legacy_1x/finetune/`; use `inspect.signature` when unsure. New examples below use flat response fields and close their clients. -## Use Cases and Training Modes (SFT/CPT/DPO/AgenticRL) +## Use Cases and Training Modes -| Mode | Entry Point | Notes | +| Mode | Current entry point | Notes | | --- | --- | --- | -| SFT supervised fine-tuning | `FineTunes.call(..., mode="sft")` | `mode` is written into the request body as `training_type`; the source docstring lists values `sft` / `efficient_sft` | -| CPT continued pre-training / DPO preference optimization | `FineTunes.call` | The SDK does not enumerate CPT/DPO literals; pass them through `mode`/`hyper_parameters` per the server-side docs | -| AgenticRL reinforcement fine-tuning (GSPO) | `dashscope.legacy_1x.finetune.agentic_rl.AgenticRL` | `training_type="reinforcement"`, `hyper_parameters["algorithm"]="gspo"` | +| SFT supervised fine-tuning | `client.fine_tunes.create(..., mode="sft")` | `mode` is sent as `training_type`; source documents `sft` / `efficient_sft` | +| CPT / DPO | `client.fine_tunes.create(...)` | The SDK does not enumerate all server literals; pass supported `mode` and `hyper_parameters` values through | +| Agentic RL (including GSPO configuration) | `client.rl` and `client.rl.jobs` | Current client workflow and job-management facade | +| Direct AgenticRL workflow | `dashscope.finetune.agentic_rl.AgenticRL` | Separate workflow object; do not treat it as `client.rl` or as a FineTunes manager | -## Standard Workflow (upload file → create fine-tune job → poll status → deploy → invoke) +## Standard SFT Workflow -1. Upload file: `Files.upload(file_path, purpose="fine_tune")` → take `output["uploaded_files"][0]["file_id"]` -2. Create job: `FineTunes.call(model, training_file_ids=[file_id], hyper_parameters={...})` → take `output.job_id` (like `ft-xxx`) -3. Poll status: `FineTunes.get(job_id)` for polling, or `FineTunes.wait(job_id)` to block until a terminal state (polls every 30s); logs via `FineTunes.logs(job_id, offset=1, line=1000)`, event stream via `FineTunes.stream_events(job_id)` -4. Deploy: `Deployments.call(model=, capacity=N)` → `output.deployed_model`; use `Deployments.get(deployed_model)` and wait for `output.status` to become `RUNNING` -5. Invoke: the deployment name is the model name; use regular inference APIs such as `Generation.call(model=deployed_model, ...)` -6. Cleanup: `Deployments.scale(deployed_model, capacity)` to scale; `Deployments.delete(deployed_model)` to release; `FineTunes.cancel(job_id)` / `FineTunes.delete(job_id)` to cancel/delete a job +1. Upload JSONL with `client.files.upload(file_path=..., purpose="fine-tune")` and read `File.file_id`. +2. Create a job with `client.fine_tunes.create(...)`; submission returns a `FineTune` snapshot with `job_id` and `status`. +3. Poll with `get(job_id)` or block with `wait(job_or_job_id)`. Read logs with `logs()` or consume SSE with `stream_events()`. +4. After success, deploy `FineTune.finetuned_model` with `client.deployments.create(model=..., capacity=...)`; poll `get()` until the desired deployment status. +5. Invoke the deployment by using `Deployment.deployed_model` as the model name in the appropriate inference API. +6. Scale or release with `client.deployments.scale/delete`; cancel or delete training with `client.fine_tunes.cancel/delete`. -## SDK APIs (source of truth: the source code) +```python +import dashscope -### FineTunes (`dashscope/legacy_1x/finetune/finetunes.py`, SUB_PATH="fine-tunes") +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + uploaded = client.files.upload(file_path="train.jsonl") + + submitted = client.fine_tunes.create( + model="qwen3.8-32b", + training_file_ids=[uploaded.file_id], + hyper_parameters={"n_epochs": 3}, + ) + job = client.fine_tunes.wait(submitted) + print(job.job_id, job.status, job.finetuned_model) + + deployment = client.deployments.create( + model=job.finetuned_model, + capacity=1, + suffix="demo", + ) + print(deployment.deployed_model, deployment.status) +``` -The creation method is named `call` (the SDK has no `create` method): -`FineTunes.call(model, training_file_ids, validation_file_ids=None, mode=None, hyper_parameters={}, api_key=None, workspace=None, **kwargs) -> FineTune` +`AsyncClient` provides awaitable file, fine-tune, deployment, and RL resource methods; use `async with dashscope.AsyncClient()` and close streams with their async cleanup API. -| Parameter | Type | Notes | -| --- | --- | --- | -| model | str | Base model to fine-tune | -| training_file_ids | list/str | Training file id; a str is auto-converted to a single-element list | -| validation_file_ids | list/str/None | Validation file id | -| mode | str/None | Training mode, written into the request body as `training_type` (`sft`/`efficient_sft`) | -| hyper_parameters | dict | Hyperparameters, e.g. `n_epochs`, `batch_size`, `learning_rate` | -| kwargs["finetuned_output"] | str | Custom output model name, passed through as `finetuned_output` in the request body | +## Files (`client.files`) + +| Method | Return / important fields | +| --- | --- | +| `upload(*, file_path, purpose="fine-tune", description=None, ...)` | `File`: `file_id`, `name`, `purpose`, `size`, `uploaded_at` | +| `list(*, page_no=1, page_size=10, ...)` | `FileList`: `files`, `page_no`, `page_size`, `total_count` | +| `get(file_id, ...)` | `File` | +| `delete(file_id, ...)` | `DeletionStatus`, including `deleted` | + +Fine-tune uploads validate JSONL locally, one JSON object per line. Invalid JSONL raises local `ValueError`; filesystem failures remain SDK I/O errors. SFT rows commonly contain `{"messages": [...]}`; RL datasets may include fields such as `rollout_extra` required by the workflow. The 2.0 purpose default is hyphenated `fine-tune`, not the old `fine_tune` example. + +## Fine-Tune Jobs (`client.fine_tunes`) -Other methods: `get(job_id) -> FineTune`; `list(page_no=1, page_size=10) -> FineTuneList`; `cancel(job_id) -> FineTuneCancel`; `delete(job_id) -> FineTuneDelete`; `wait(job_id)` polls until `SUCCEEDED/FAILED/CANCELED`. +```text +create(*, model, training_file_ids, validation_file_ids=None, + mode=None, hyper_parameters=None, finetuned_model=None, ...) -> FineTune +``` + +- `training_file_ids` and `validation_file_ids` accept a string or list; a string is normalized to a one-element list. +- `finetuned_model` is the request-side custom output name and is sent as wire key `model_name`. It is not the 1.x `finetuned_output` request kwarg. +- `get(job_id)` returns a fresh `FineTune`; `list(page_no=1, page_size=10)` returns `FineTuneList.jobs` and `total_count`. +- `cancel(job_id)` and `delete(job_id)` return flat `JobStatus` snapshots. +- `logs(job_id, offset=1, lines=1000)` returns `JobLogs`; read normalized text from `content`. The Python argument is plural `lines` even though the wire query key is singular. +- `stream_events(job_id)` yields `FineTuneEvent` objects. Event `output` is the event payload, not a legacy response envelope. +- `wait(job_or_job_id, poll_interval=..., max_interval=..., timeout=...)` polls until `SUCCEEDED`, `FAILED`, or `CANCELED`; failed/cancelled jobs raise by default. It returns the final `FineTune` snapshot rather than invoking the deployment automatically. + +Flat `FineTune` fields include `job_id`, `job_name`, `status`, `model`, `base_model`, `finetuned_model`, `training_file_ids`, `validation_file_ids`, `hyper_parameters`, `training_type`, `created_at`, `finished_at`, and `usage`, plus common response metadata. `FineTuneList` uses `jobs` and `total_count`. + +## Deployments (`client.deployments`) + +```text +create(*, model, capacity, model_version=None, suffix=None, plan=None, + template_id=None, deploy_spec=None, billing_method=None, ...) -> Deployment +``` -### Deployments (`dashscope/legacy_1x/finetune/deployments.py`, SUB_PATH="deployments") +- `get(deployed_model)` and `scale(deployed_model, *, capacity)` return `Deployment` snapshots. There is no SDK `wait()` on this resource; poll `get()` when readiness is required. +- `list(page_no=1, page_size=10)` returns `DeploymentList.deployments` and `total_count`. +- `delete(deployed_model)` returns `JobStatus`. +- `stream_events(deployed_model_id)` yields `DeploymentEvent`; its `output` is an event payload, not the old response envelope. +- Flat `Deployment` fields include `deployed_model`, `model`, `base_model`, `status`, `capacity`, `ready_capacity`, `charge_type`, `created_at`, and `updated_at`. -- `Deployments.call(model, capacity, version=None, suffix=None, ...) -> Deployment`: request body keys are `model_name/capacity/model_version/suffix`; fine-tuned models do not need `version`; with `suffix` set, the deployment name is `model_suffix` -- `get(deployed_model)`, `list(page_no=1, page_size=10)`, `delete(deployed_model)`, `scale(deployed_model, capacity)` +The Python SDK's `plan` parameter is optional at the signature level; server-side plan/billing requirements still depend on model and account. This differs from the legacy CLI's required `--plan` option. -### Files (`dashscope/files.py`, SUB_PATH="files") +## Agentic RL Through `client.rl` -- `Files.upload(file_path, purpose="fine_tune", description=None, ...) -> DashScopeAPIResponse`: see `FilePurpose` for `purpose` values (`fine_tune`/`assistants`); for `fine_tune`, the JSONL is validated locally first and invalid files raise `InvalidFileFormat` directly -- `list(page=1, page_size=10)`, `get(file_id)`, `delete(file_id)`; all return `DashScopeAPIResponse` +`client.reinforcement_learning` aliases `client.rl`. The resource separates workflow operations from `client.rl.jobs` job management. -### AgenticRL (`dashscope/applications/finetune/agentic_rl.py`, brief) +### Workflow and function instances -- Usage: `client = AgenticRL(api_key=None)` → optionally `client.init(config_path="job.yaml")` (YAML-driven) → `await client.run(...)` completes "register functions → upload datasets → submit job" in one step; or call `register_functions()` / `upload_datasets()` / `submit_job()` step by step -- Key concepts: `RolloutFunctionComponent`/`RewardFunctionComponent` function components; `TrainingDataset`/`ValidationDataset` (`data_source_type`: `file_id`/`download_url`/`oss_mount`); `hyper_parameters` (`algorithm="gspo"`, `n_rollouts`, `kl_loss_coef`, `learning_rate`, etc.); `resources` (e.g. `mtu_spec_code`) -- Job management methods mirror FineTunes: `AgenticRL.get/list/cancel/delete/logs(job_id, ...)` +| Method | Contract | +| --- | --- | +| `register_functions(*, rollout_classpaths=None, reward_classpaths=None, group_reward_classpaths=None, workspace_dir="./", lazy_load=True, api_key=None)` | Returns `RegisteredFunctions`, including registered entity IDs and function instance IDs | +| `test_functions(*, instance_id, func_type, input_data, pull_logs=False, log_page_size=100, start_time=None, end_time=None, api_key=None)` | Returns the user function's own result, not a `FineTune` | +| `upload_data(training_files, validation_files=None, *, api_key=None)` | Returns `UploadedData.training_file_ids` and `validation_file_ids` | +| `query_function_instance_logs(...)` | Returns one raw log-query API dictionary | +| `query_all_function_instance_logs(...)` | Returns aggregated log strings | +| `delete_function_instance(...)` | Returns the raw delete API dictionary | -## Inputs/Outputs (JSONL format and job object fields) +These synchronous workflow methods internally use `asyncio.run`; do not call them from an already running event loop. Use `AsyncClient.rl` there. Their signatures do not accept a generic `timeout=` or per-call `workspace=`; do not copy those options from job methods. -- Training file: JSONL, one valid JSON object per line (the SDK validates line by line with `json.loads`); for SFT each line is usually `{"messages": [...]}`; RL data may carry a `rollout_extra` field -- `FineTune`: `status_code/request_id/code/message/output/usage`; `output` (`FineTuneOutput`) fields: `job_id, job_name, status, model, base_model, finetuned_output, training_file_ids, validation_file_ids, hyper_parameters, training_type, create_time, end_time, usage` -- `output.status` enum (`TaskStatus`): `PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/SUSPENDED/UNKNOWN`; after the job succeeds, use `output.finetuned_output` as the model name for deployment and invocation -- `FineTuneList.output`: `jobs` (List[FineTuneOutput]) + `page_no/page_size/total` -- `Deployment.output` (`DeploymentOutput`): `deployed_model, status, model_name, base_model, capacity, ready_capacity, charge_type, gmt_create, gmt_modified`; `status` (`DeploymentStatus`): `DEPLOYING/RUNNING(SERVING)/PENDING/DELETING/FAILED` +### Jobs (`client.rl.jobs`) -## Minimal Example (create an SFT job) +- `create(*, config_path=None, job_name=None, api_key=None, **kwargs) -> FineTune` initializes and runs the complete AgenticRL workflow, then returns after submission. `kwargs` override open-ended training configuration keys. It does not wait for training completion. +- `fetch(job_id) -> FineTune`; `wait(job_or_job_id) -> FineTune`; `list(...) -> FineTuneList`. +- `cancel(job_id)` and `delete(job_id)` return `JobStatus`; `logs(job_id, offset=1, lines=1000)` returns `JobLogs`. ```python import dashscope -from dashscope import Files, FineTunes - -dashscope.api_key = "sk-xxx" # or use the DASHSCOPE_API_KEY environment variable - -up = Files.upload("train.jsonl", purpose="fine_tune") # validates JSONL locally -file_id = up.output["uploaded_files"][0]["file_id"] - -job = FineTunes.call( - model="qwen3.8-32b", - training_file_ids=[file_id], - hyper_parameters={"n_epochs": 3}, -) -if job.status_code == 200: - rsp = FineTunes.wait(job.output.job_id) # blocks until SUCCEEDED/FAILED/CANCELED - print(rsp.output.status, rsp.output.finetuned_output) -else: - print(job.code, job.message) # on failure read code/message/request_id + +with dashscope.Client() as client: + submitted = client.rl.jobs.create(config_path="rl_config.yaml") + job = client.rl.jobs.wait(submitted) + print(job.job_id, job.status, job.finetuned_model) ``` -## Common Error Codes +Use `register_functions()`, `test_functions()`, and `upload_data()` when those stages must be driven individually; do not assume `jobs.create()` reuses objects returned by a separate workflow unless the configuration explicitly wires them together. + +## Direct `AgenticRL` Workflow — A Separate Contract + +`dashscope.finetune.agentic_rl.AgenticRL` owns the configurable workflow. Typical usage is `AgenticRL(...)`, `init(config_path=..., **kwargs)`, then `await run()`; alternatively drive `register_functions()`, `upload_datasets()`, and synchronous `submit_job()` stage by stage. Components include rollout/reward functions, training/validation datasets, hyperparameters such as `algorithm="gspo"`, and resource settings. + +```python +import asyncio +from dashscope.finetune.agentic_rl import AgenticRL + +async def main(): + workflow = AgenticRL() + workflow.init(config_path="rl_config.yaml") + result = await workflow.run() + print(result.status_code, result.request_id) + +asyncio.run(main()) +``` + +`run()`/`submit_job()` use `SubmitResult`, whose legacy-shaped fields include `status_code`, `request_id`, `code`, `message`, `output`, `usage`, and `headers`. That shape belongs to this workflow API; it is not a `client.rl.jobs` `FineTune`. The public workflow class does **not** provide FineTunes-style `get/list/cancel/delete/logs` methods. For those operations on a submitted RL job, use `client.rl.jobs`. + +In 1.x compatibility code, the AgenticRL wrapper adds `cancel/list/get/delete/logs`, and its `submit_job()` returns a 1.x `FineTune`. Keep that wrapper's return and management contract out of current `client.rl` and public `AgenticRL` examples. + +## Response and Error Handling + +Ordinary 2.0 models expose common `request_id`, `status_code`, `code`, `message`, `is_error`, and `headers` alongside the flat business fields documented above. By default, API/business failures raise exceptions from `dashscope.errors`. If a client is constructed with `raise_on_error=False`, check `response.is_error` before using business fields and retain `request_id` for diagnosis. Intentional raw/event payloads (`Job.output`, `FineTuneEvent.output`, `DeploymentEvent.output`, and direct AgenticRL `SubmitResult.output`) are exceptions, not the standard envelope pattern. + +## 1.x Compatibility — Maintenance Only -| Error code/exception | HTTP status | Meaning | Handling | -| --- | --- | --- | --- | -| `InvalidFileFormat` | none (local exception) | Uploaded file is not valid JSONL | Fix the JSON line by line and re-upload | -| `AuthenticationError` | 401 | API Key missing/invalid | Set `DASHSCOPE_API_KEY` or pass `api_key` explicitly | -| Response `code`/`message` | 403 | No access to the file/job/deployment | Check the `workspace` and resource ownership | -| Response `code`/`message` | 404 | `job_id`/`file_id`/`deployed_model` does not exist | Verify the id with the corresponding `list()` | -| `REPEATABLE_STATUS` | 503/504 | Service temporarily unavailable | Retry, or keep polling with `FineTunes.wait` | -| AgenticRL `error_code` 3001-3008 | - | 3001 key config, 3002 function registration, 3003 dataset upload, 3004 duplicate function name, 3005 job submission, 3006 flow failure, 3007 unsupported function type, 3008 function validation failure | Use the exception's `root_cause` to locate the root cause | +Use this section only to repair existing imports such as `from dashscope import Files, FineTunes, Deployments`; do not mix these objects with 2.0 client resources. -General error-handling pattern: first check `status_code == 200` on the response object; on failure read `code`/`message`/`request_id`; the base class for SDK local exceptions is `dashscope.common.error.DashScopeException`. +- `Files.upload(file_path, purpose="fine_tune", description=None)` returned `DashScopeAPIResponse`; old code reads `output["uploaded_files"][0]["file_id"]`. `Files.list` used `page=...`, and invalid fine-tune JSONL raised `InvalidFileFormat`. +- `FineTunes.call(model, training_file_ids, validation_file_ids=None, mode=None, hyper_parameters={}, ...)` created a job. Old custom naming used `kwargs["finetuned_output"]`; management methods were `get/list/cancel/delete/wait/logs/stream_events`. +- Legacy `FineTune.output` fields included `job_id`, `status`, `finetuned_output`, file IDs, hyperparameters, timestamps, and usage. Successful old jobs therefore deployed `response.output.finetuned_output`. +- `Deployments.call(model, capacity, version=None, suffix=None, ...)` created a deployment. Legacy code reads `Deployment.output.deployed_model`, `output.status`, `output.capacity`, and related fields; `scale` used positional capacity. +- Legacy responses generally did not raise for service failures: check `status_code`, then read `code`, `message`, and `request_id`. Do not apply that rule to 2.0 `Client` calls. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md index eff7bc4b..208b939d 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/multimodal.md @@ -4,88 +4,202 @@ description: Quick reference for multimodal (MultiModalConversation/ImageSynthes --- # Multimodal -Locate before answering: understanding tasks use `MultiModalConversation` (sync/streaming); generation tasks use `ImageSynthesis`/`VideoSynthesis` (async tasks). Parameter names must follow the source code in `dashscope/aigc/`; never answer from memory. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The 2.0 resources live in the root `dashscope` package; **`dashscope.v2` does not exist**. Set `DASHSCOPE_API_KEY` before running the examples. Use the explicitly marked 1.x section only for legacy maintenance, and do not mix 2.0 flattened responses with 1.x `.output` responses. -## Applicable Models and Scenarios +Authoritative sources: `dashscope/resources/multimodal/chat.py`, `images.py`, `videos.py`, and `_jobs.py`. -- Understanding (image/video understanding, OCR, function calling): `qwen3-vl-flash`, `qwen3-vl-plus`, qwen-ocr series, etc. (the `Models` class in source only has the constant `qwen_vl_chat_v1`; pass others as strings). -- Text-to-image / image editing (`ImageSynthesis`, task=`text2image`): `qwen-image-3.0`, `qwen-image-edit-plus` (image editing, automatically switches to image2image task). -- Video generation (`VideoSynthesis`, task=`video-generation`): `wan2.2-t2v-plus` (text-to-video), `wan2.2-i2v-plus` (image-to-video), `wan2.2-kf2v-flash` (first/last frame). +## 2.0.3 Resources and Models -## SDK Interfaces +- Multimodal understanding: `client.multimodal.chat.create`. Models include current Qwen VL/OCR model names; pass the model as a string and verify service-side availability. +- Image generation/editing: `client.images.generate` for submit-and-wait, or `client.images.jobs.create/fetch/wait` for explicit task control. +- Video generation: `client.videos.generate` or `client.videos.jobs.create/fetch/wait`. +- `AsyncClient` mounts the same resources. `await` each operation, and use `async with dashscope.AsyncClient() as client` to close connections. -| Class | Key Methods | Notes | -|---|---|---| -| `MultiModalConversation` | `call(model, messages, ...)` | `stream=True` returns a Generator, otherwise returns a response object; async version `AioMultiModalConversation.call` | -| `ImageSynthesis` | `call / async_call / fetch(task) / wait(task) / cancel(task) / list(...)` | `call` blocks until results are ready; `sync_call` only supports wan2.2-t2i-flash/plus | -| `VideoSynthesis` | Same as above (no `list` difference, parameters below) | Same async task flow | +`client.chat.completions.create` automatically routes messages containing image/audio/video parts to the multimodal endpoint. `client.multimodal.chat.create` always selects that endpoint in the native protocol, including text-only calls to VL models. Compatible protocols translate media on their own chat path. -Async task flow: `async_call(...)` returns a task (`output.task_id`, `task_status=PENDING`) → `fetch(task)` for a single query or `wait(task)` to poll until a terminal state; `cancel(task)` can only cancel `PENDING`. The `task` parameter accepts a task_id string or the response object from async_call. `task_status` values: `PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/UNKNOWN`. +## Multimodal Chat -Key parameters of `MultiModalConversation.call` (all optional, in source signature order): `messages`, `text`, `voice`, `language_type`, `stream`, `temperature`[0,2), `top_p`(0,1], `top_k`, `max_tokens`, `seed`, `stop`, `repetition_penalty`, `presence_penalty`, `result_format`("message"/"text"), `incremental_output`, `enable_search`, `tools`, `tool_choice`, `enable_thinking`, `n`(1-4), `ocr_options`, `logprobs`, `top_logprobs`; plus `api_key`, `workspace`. **Empty `model` raises `ModelRequired`; both messages and text empty raises `ValueError`.** +`create` has the same keyword-only signature and `ChatCompletion` / `ChatStream` return types as `client.chat.completions.create`: -Key parameters of `ImageSynthesis.call/async_call`: `prompt` (required, empty raises `InputRequired`), `negative_prompt`, `size`("width*height" e.g. "1024*1024"), `n`, `seed`, `style` (auto/photography/anime etc.), `prompt_extend`, `watermark`, `ref_img`, `sketch_image_url` (sketch), `base_image_url`+`mask_image_url` (imageedit), `ref_strength`[0,1], `ref_mode`(repaint/refonly). +```python +create(*, model: str, messages: str | list[MessageInput], stream: bool = False, + tools=None, temperature=None, top_p=None, max_tokens=None, stop=None, + seed=None, repetition_penalty=None, presence_penalty=None, + enable_thinking=None, response_format=None, top_k=None, tool_choice=None, + n=None, logprobs=None, top_logprobs=None, thinking_budget=None, + enable_search=None, search_options=None, parallel_tool_calls=None, + incremental_output=None, parameters=None, timeout=None, + api_key=None, workspace=None) +``` + +Use `parameters={...}` for model-specific options not present in the signature. Media content is a list of parts. With the native protocol, local paths and `file://` URIs are uploaded to OSS before the request. + +```python +import dashscope + +messages = [{ + "role": "user", + "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "Describe this image"}, + ], +}] + +with dashscope.Client() as client: + response = client.multimodal.chat.create( + model="qwen-vl-plus", + messages=messages, + ) + print(response.output_text) + + stream = client.multimodal.chat.create( + model="qwen-vl-plus", + messages=messages, + stream=True, + ) + try: + for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) + finally: + stream.close() +``` + +The 2.0 response flattens list-form multimodal text into a string. Read `response.choices[0].message.content` or `response.output_text`, plus flat `usage` / `request_id`; **do not read `.output`**. Streaming chunks use `choices[i].delta`, with the same incremental/cumulative `incremental_output` semantics as text chat. -Key parameters of `VideoSynthesis.call/async_call`: `prompt`, `negative_prompt`, `img_url` (image-to-video), `audio_url`, `first_frame_url`/`last_frame_url` (or `head_frame`/`tail_frame`), `media`, `reference_urls`, `template`, `size`("1280*720"), `duration` (seconds, default 5), `resolution`("720P"/"1080P"), `ratio`("16:9"), `seed`, `prompt_extend`, `watermark`, `shot_type`, `audio_setting`. +## Images and Videos -## Input/Output +### Actual resource signatures -Multimodal messages (content is an array of dicts; local paths or URLs both work, the SDK auto-uploads to OSS and adds header `X-DashScope-OssResourceResolve: enable`): +Both `generate` methods return a finished `Job`. Their `jobs.create` methods accept the same model/input/generation fields but only submit; their `timeout` is the submit HTTP timeout. `generate` additionally has `submit_timeout`, `poll_interval`, and `max_interval`, and its `timeout` is the total wait budget. ```python -messages = [{"role": "user", "content": [ - {"image": "https://.../a.jpg"}, # or {"video": [...]} / {"audio": ...} - {"text": "Describe this image"}, -]}] +client.images.generate( + *, model: str, prompt: str, negative_prompt=None, n=None, size=None, + image_urls=None, extra_input=None, function=None, seed=None, style=None, + prompt_extend=None, watermark=None, parameters=None, + timeout=86400.0, submit_timeout=None, poll_interval=1.0, + max_interval=8.0, api_key=None, workspace=None, +) -> Job + +client.videos.generate( + *, model: str, prompt=None, image_url=None, media=None, extra_input=None, + function=None, seed=None, size=None, duration=None, resolution=None, + ratio=None, prompt_extend=None, watermark=None, parameters=None, + timeout=86400.0, submit_timeout=None, poll_interval=1.0, + max_interval=8.0, api_key=None, workspace=None, +) -> Job + +client.images.jobs.fetch(task_id: str, *, timeout=None, api_key=None, workspace=None) +client.videos.jobs.fetch(task_id: str, *, timeout=None, api_key=None, workspace=None) +client.images.jobs.wait(job_or_task_id, *, poll_interval=1.0, max_interval=8.0, + timeout=86400.0, api_key=None, workspace=None) +client.videos.jobs.wait(job_or_task_id, *, poll_interval=1.0, max_interval=8.0, + timeout=86400.0, api_key=None, workspace=None) ``` -Responses uniformly contain `status_code / request_id / code / message / output / usage` (on error `output` is None; first check `status_code==200` i.e. `HTTPStatus.OK`): +- Image `size` accepts `1024*1024` and normalizes `1024x1024` to the wire form. `image_urls` supplies reference images for supported models; use `extra_input` for model-specific input fields and `parameters` for model-specific generation options. +- Image protocol selection is automatic by model name. Legacy text2image models use asynchronous tasks; `wan2.6-t2i*` / `wan2.6-image*` use the multimodal-generation protocol. A synchronous multimodal `generate` is capped by `submit_timeout`, not polling settings. +- Video `image_url` is the public name for the legacy wire field `img_url`. Older models accept `prompt` and/or `image_url`. `wan2.7-i2v*` requires `media` or `image_url`; media items use asset types `first_frame`, `last_frame`, `driving_audio`, and `first_clip`. `image_url` is shorthand for one `first_frame` item and must not duplicate one already in `media`. +- Unlike 1.x video behavior, explicitly pass `prompt_extend=True` when prompt extension is wanted. -- `MultiModalConversationResponse.output`: `choices[0].message.content` (content array isomorphic to the input), `finish_reason`, `text`; `usage`: `input_tokens`/`output_tokens`/`characters`. -- `ImageSynthesisResponse.output`: `task_id`, `task_status`, `results` (each contains `url`); `usage.image_count`. -- `VideoSynthesisResponse.output`: `task_id`, `task_status`, `video_url`; `usage`: `video_count`/`video_duration`/`video_ratio`. +### Runnable examples -## Minimal Examples +```python +import dashscope + +with dashscope.Client() as client: + image_job = client.images.generate( + model="wanx2.1-t2i-turbo", + prompt="一只在月球上弹吉他的猫,水彩风格", + size="1024*1024", + timeout=600.0, + ) + print(image_job.status, image_job.image_urls) + + video_job = client.videos.generate( + model="wan2.7-t2v", + prompt="一只小猫在草地上奔跑", + size="1280*720", + timeout=900.0, + ) + print(video_job.status, video_job.video_url) +``` -Image-text understanding: +Explicit submission, polling, and waiting: ```python -from http import HTTPStatus -from dashscope import MultiModalConversation - -rsp = MultiModalConversation.call(model="qwen3-vl-flash", messages=messages) -if rsp.status_code == HTTPStatus.OK: - print(rsp.output.choices[0].message.content[0]["text"]) +import dashscope + +with dashscope.Client() as client: + submitted = client.images.jobs.create( + model="wanx2.1-t2i-turbo", + prompt="一只猫", + size="1024*1024", + ) + snapshot = client.images.jobs.fetch(submitted.task_id) + print(snapshot.status) + + done = client.images.jobs.wait(submitted, timeout=600.0) + print(done.image_urls) ``` -Text-to-image async task: +`Job` exposes flat `task_id`, `status`, `results`, `result_url`, `image_urls`, `video_url`, `usage`, and the shared response envelope. Each item in `results` can expose `url` (and service-specific extra fields). Status values are `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`; use `job.succeeded` / `job.finished` for checks. `wait` polls with exponential backoff. With default error handling, `create` / `fetch` raise on `FAILED`, and `wait` raises on `FAILED` or `CANCELED`; `UNKNOWN` is terminal and is returned. + +## Errors (brief) + +- In 2.0, catch `dashscope.errors.DashScopeError` or a more specific subclass. If using `raise_on_error=False`, inspect `response.is_error` / `code` / `message`; for jobs also inspect `status`. +- In 1.x, keep checking `status_code`, `code`, and `message` on the response, including task failure information under the 1.x `output` contract. + +## 1.x Compatibility and Maintenance Guide + +These APIs retain their old response shapes; do not rewrite only the response access while leaving the old entry point in place. + +| 1.x entry point | Maintenance notes | +| --- | --- | +| `MultiModalConversation.call` / `AioMultiModalConversation.call` | `stream=True` returns a generator/async generator; otherwise returns `MultiModalConversationResponse` | +| `ImageSynthesis.call/async_call/fetch/wait/cancel/list` | `call` submits and waits; `async_call` only submits; `sync_call` is limited to supported wan2.2 t2i models | +| `VideoSynthesis.call/async_call/fetch/wait/cancel/list` | Same asynchronous task pattern for video | + +For `MultiModalConversation.call`, key parameters remain `model`, `messages`, `text`, `voice`, `language_type`, `stream`, sampling options, `result_format`, `incremental_output`, `enable_search`, tools, thinking options, `ocr_options`, and logprobs. Empty `model` raises `ModelRequired`; empty `messages` and `text` raises `ValueError`. + +For `ImageSynthesis`, old inputs include `prompt` (required), `negative_prompt`, `size`, `n`, `seed`, `style`, `prompt_extend`, `watermark`, `ref_img`, `sketch_image_url`, `base_image_url`, `mask_image_url`, `ref_strength`, and `ref_mode`. For `VideoSynthesis`, old inputs include `prompt`, `negative_prompt`, `img_url`, `audio_url`, `first_frame_url` / `last_frame_url` (or `head_frame` / `tail_frame`), `media`, `reference_urls`, `template`, `size`, `duration`, `resolution`, `ratio`, `seed`, `prompt_extend`, `watermark`, `shot_type`, and `audio_setting`. Keep model-specific validation when repairing these calls. + +The 1.x task flow is `async_call(...)` → `fetch(task)` for one snapshot or `wait(task)` until terminal; `task` can be a task ID or previous response. `cancel` applies only to `PENDING`. Old response contracts are: + +- `MultiModalConversationResponse.output.choices[0].message.content` (a content-part list), plus `output.text` where supplied. +- `ImageSynthesisResponse.output.task_id/task_status/results`; each result has `url`. +- `VideoSynthesisResponse.output.task_id/task_status/video_url`. +- All expose `status_code`, `request_id`, `code`, `message`, and `usage`; check `status_code == HTTPStatus.OK` before reading `output`. + +Do not use a context manager for these 1.x classmethod calls: ```python -from dashscope import ImageSynthesis +from http import HTTPStatus +from dashscope import ImageSynthesis, MultiModalConversation + +response = MultiModalConversation.call( + model="qwen-vl-plus", + messages=[{"role": "user", "content": [ + {"image": "https://example.com/cat.jpg"}, + {"text": "Describe this image"}, + ]}], +) +if response.status_code == HTTPStatus.OK: + print(response.output.choices[0].message.content) -rsp = ImageSynthesis.async_call( +job = ImageSynthesis.async_call( model="qwen-image-3.0", - prompt="A cat running under the moonlight", n=2, size="1024*1024", + prompt="A cat running under the moonlight", + n=2, + size="1024*1024", ) -rsp = ImageSynthesis.wait(rsp) # poll until SUCCEEDED/FAILED -if rsp.output.task_status == "SUCCEEDED": - for r in rsp.output.results: - print(r.url) +job = ImageSynthesis.wait(job) +if job.status_code == HTTPStatus.OK and job.output.task_status == "SUCCEEDED": + for result in job.output.results: + print(result.url) ``` -## Common Error Codes - -| Error Code | HTTP Status | Meaning | Handling | -|---|---|---|---| -| InvalidApiKey | 401 | API Key missing/invalid | Check `dashscope.api_key` or the `DASHSCOPE_API_KEY` environment variable | -| InvalidParameter | 400 | Invalid parameter (size/format/field name) | Check each item against the parameter tables in this file; locally also raises `InvalidParameter` exception | -| DataInspectionFailed | 400 | prompt/image failed content moderation | Rewrite the prompt or change the input image | -| Throttling | 429 | Rate limit triggered | Retry with exponential backoff, reduce concurrency, confirm quota | -| InternalError | 500 | Server-side error | Retry with `request_id` or file a ticket | -| Task FAILED | 200 | Async task execution failed | Read `output.task_status` and the failure info in `output`, then adjust input and retry | - -Local validation exceptions (no request sent): `ModelRequired` (missing model), `InputRequired` (missing prompt), `ValueError` (messages/text both empty). Network/server exceptions may raise `RequestFailure` (contains `http_code`). - ## Java SDK Java SDK v2.22.23 entry points: diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md index f85137e1..606eced9 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/retrieval.md @@ -5,108 +5,220 @@ description: Quick reference for retrieval (Embedding / Batch Embedding / Multim # Retrieval -> Compiled from the SDK source code (embeddings/text_embedding.py, batch_text_embedding.py, multimodal_embedding.py, rerank/text_rerank.py); parameters and return fields are based on these sources. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The implementation is in the root `dashscope` package; **`dashscope.v2` does not exist**. Set `DASHSCOPE_API_KEY` before running the network examples. Keep the 1.x `.output` contract confined to the compatibility section. + +Authoritative sources: `dashscope/resources/embeddings.py`, `multimodal/embeddings.py`, `rerank.py`, `tokenization.py`, and `_jobs.py`. ## Applicable Models and Scenarios -**TextEmbedding** (task="text-embedding", `TextEmbedding.Models`): -- `text-embedding-v1` / `text-embedding-v2`: basic general-purpose vectors; only text_type is supported; fixed dimension. -- `text-embedding-v3`: supports text_type, dimension (default 1024), output_type (dense/sparse/dense&sparse). -- `text-embedding-v4`: everything v3 offers, plus exclusive dimension options 2048/1536. +- Text vectors: `text-embedding-v1/v2` (fixed dimension), `text-embedding-v3/v4` (configurable dimensions and dense/sparse output where supported). +- Offline batch text vectors: `text-embedding-async-v1/v2`, using a file containing one text per line. +- Multimodal vectors: `multimodal-embedding-one-peace-v1`, `multimodal-embedding-v1`, `qwen3-vl-embedding`, `qwen2.5-vl-embedding`, `tongyi-embedding-vision-plus`, and `tongyi-embedding-vision-flash`. +- Relevance ranking: `gte-rerank`, `gte-rerank-v2`, `qwen3-rerank`, and `qwen3-vl-rerank` where supported by the selected rerank resource. +- Token counting: `client.tokenization.count`; local tokenization is separately available under `dashscope.tokenizers` and sends no request. -**BatchTextEmbedding** (offline batch): `text-embedding-async-v1` / `text-embedding-async-v2`; submits a file url containing line-by-line texts for async batch processing. +Model support and allowed parameter values are service/model-specific; the SDK passes model names as strings rather than enforcing a whitelist. -**MultiModalEmbedding** (task="multimodal-embedding"): mixed text/image/audio/video vectors. Models: `multimodal-embedding-one-peace-v1`, `multimodal-embedding-v1`, `qwen3-vl-embedding`, `qwen2.5-vl-embedding`, `tongyi-embedding-vision-plus`, `tongyi-embedding-vision-flash`; coroutine version `AioMultiModalEmbedding`. +## 2.0.3 Text and Multimodal Embeddings -**TextReRank** (task="text-rerank"): reranks recalled results by relevance. Models: `gte-rerank`, `gte-rerank-v2`, `qwen3-rerank`, `qwen3-vl-rerank`; coroutine version `AioTextReRank`. +### Text embeddings -## SDK Interfaces +```python +client.embeddings.create( + *, model: str, input: str | list[str], text_type=None, dimension=None, + output_type=None, instruct=None, parameters=None, timeout=None, + api_key=None, workspace=None, +) -> EmbeddingResponse +``` -`TextEmbedding.call(model, input, workspace=None, api_key=None, text_type=None, dimension=None, output_type=None, instruct=None, **kwargs) -> DashScopeAPIResponse` +- `text_type` is typically `query` for retrieval queries or `document` for corpus/symmetric inputs. +- v3/v4 support model-dependent `dimension` and `output_type` (`dense`, `sparse`, or `dense&sparse`). Do not pass these to v1/v2. `instruct` supplies a task instruction where supported; additional options belong in `parameters`. +- Empty input raises `ValueError` locally. Streaming is not supported. -| Parameter | Description (based on source docstrings) | -| --- | --- | -| input | str or List[str], internally assembled as `{"texts": [...]}`; streaming not supported | -| text_type | `"query"` (retrieval query) / `"document"` (default; corpus and symmetric tasks) | -| dimension | v3/v4 only: 2048, 1536 (v4 only), 1024 (default), 768, 512, 256, 128, 64 | -| output_type | v3/v4 only: `"dense"` (default) / `"sparse"` / `"dense&sparse"` | -| instruct | Custom task instruction to guide the model in understanding query intent | +### Multimodal embeddings -`MultiModalEmbedding.call(model, input, api_key=None, workspace=None, dimension=None, output_type=None, fps=None, instruct=None, enable_fusion=None, res_level=None, max_video_frames=None, **kwargs)` +The actual 2.0 entry point is **`client.multimodal.embeddings.create`** (there is no method named `create_multimodal`): -- input is a List; elements use `MultiModalEmbeddingItemText(text, factor)` / `...ItemImage(image, factor)` / `...ItemAudio(audio, factor)` (factor: float, required), or equivalent dicts. -- dimension: vector dimension (supported values vary by model); output_type: currently only `"dense"`. -- fps: video frame-sampling ratio in [0,1], default 1.0; instruct: task instruction. -- enable_fusion: qwen3-vl-embedding only; when True, all content is fused into a single vector. -- res_level (0/1/2/3) and max_video_frames (<=64): snapshot-type models only. -- kwargs supports `auto_truncation`: automatically truncates audio >15s / text >70 words; default False (over-length input raises an error). -- Local files are automatically uploaded via OSS (header `X-DashScope-OssResourceResolve: enable`); empty input raises `InputRequired`; empty model raises `ModelRequired`. +```python +client.multimodal.embeddings.create( + *, model: str, input: dict | EmbeddingItem | list, + dimension=None, output_type=None, fps=None, instruct=None, + enable_fusion=None, res_level=None, max_video_frames=None, + parameters=None, timeout=None, api_key=None, workspace=None, +) -> EmbeddingResponse +``` -**BatchTextEmbedding** (BaseAsyncApi): `call(model, url, ...)` submits and waits; `async_call(model, url, ...)` only creates the task; `fetch(task)` checks status; `wait(task)` waits for completion; `cancel(task)` cancels only PENDING tasks; `list(...)` lists tasks. url is the address of a file with line-by-line texts and is required (empty raises InputRequired); kwargs supports text_type (query/document, default document). +Each input item sets exactly one of `text`, `image`, `audio`, or `video`, plus optional `factor`. Use dictionaries or `EmbeddingItem` from `dashscope.resources.multimodal.embeddings`. `fps`, `res_level`, and `max_video_frames` apply only to supported snapshot models; `enable_fusion` is model-specific (including qwen3-vl-embedding). Local paths and `file://` URIs upload to OSS first. Empty input or an item without a modality raises `ValueError`. -`TextReRank.call(model, query, documents, return_documents=None, top_n=None, api_key=None, instruct=None, **kwargs) -> ReRankResponse` +```python +import dashscope + +with dashscope.Client() as client: + embedded = client.embeddings.create( + model="text-embedding-v3", + input=["今天天气不错", "明天会下雨吗"], + text_type="document", + dimension=1024, + ) + for item in embedded.embeddings: + print(item.index, len(item.embedding)) + + multimodal = client.multimodal.embeddings.create( + model="multimodal-embedding-v1", + input=[ + {"text": "一只猫"}, + {"image": "https://example.com/cat.jpg"}, + ], + ) + for item in multimodal.embeddings: + print(item.index, item.type, len(item.embedding)) +``` -- return_documents: default False (results do not include original text); top_n: returns all documents by default. -- instruct: ranking instruction; English recommended. Empty query/documents raises InputRequired; empty model raises ModelRequired. -- AioTextReRank.call is the coroutine version, with an additional workspace parameter. +Both return the same flat `EmbeddingResponse`: `embeddings`, `usage`, and the shared response envelope. Each `Embedding` has `index`, `embedding`, and optional `type`; the 1.x wire name `text_index` is normalized to `index`. The old one-peace singular vector is normalized to item 0. **Do not read `.output["embeddings"]` in 2.0.** -## Input / Output +## 2.0.3 Batch Embedding Jobs -Synchronous interfaces return `DashScopeAPIResponse`: status_code (200 = success) / request_id / code / message / output / usage. -- Embedding: `output["embeddings"]` is `[{"text_index": i, "embedding": [...]}]` (each multimodal item contains `embedding`); `usage` reports token usage (e.g. `input_tokens`). -- Batch: `output` = `{task_id, task_status, url}`; after SUCCEEDED, url points to the result file; `usage.total_tokens`; task_status in PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED. -- ReRank: input `input={"query": str, "documents": List[str]}`; `output.results` is `[{index, relevance_score, document}]`, where index is the position in documents; document is returned only when return_documents=True; `usage.total_tokens`. +```python +client.embeddings.jobs.create( + *, model: str, file_url: str, text_type=None, parameters=None, + timeout=None, api_key=None, workspace=None, +) -> Job +client.embeddings.jobs.fetch(task_id: str, *, timeout=None, + api_key=None, workspace=None) -> Job +client.embeddings.jobs.wait(job_or_task_id, *, poll_interval=1.0, + max_interval=8.0, timeout=86400.0, api_key=None, workspace=None) -> Job +``` -## Minimal Examples +`create` accepts exactly one `file_url`, not a list; an empty URL raises `ValueError`. `create` only submits, `fetch` obtains one snapshot, and `wait` polls to a terminal state. `timeout` on create/fetch is an HTTP timeout; on wait it is the total wait budget. ```python import dashscope -from dashscope import TextEmbedding -resp = TextEmbedding.call( - model=TextEmbedding.Models.text_embedding_v3, - input=["The wind is swift, the sky is high, the gibbons wail", "The islets are clear, the sand is white, the birds wheel"], - text_type="document", dimension=1024, -) -if resp.status_code == 200: # HTTPStatus.OK - for e in resp.output["embeddings"]: - print(e["text_index"], e["embedding"][:3]) +with dashscope.Client() as client: + submitted = client.embeddings.jobs.create( + model="text-embedding-async-v2", + file_url="https://example.com/embedding-input.txt", + text_type="document", + ) + snapshot = client.embeddings.jobs.fetch(submitted.task_id) + print(snapshot.status) + done = client.embeddings.jobs.wait(submitted, timeout=3600.0) + print(done.result_url) # download and parse this result file ``` +Read flat `task_id`, `status`, `result_url`, `usage`, `request_id`, and `job.succeeded` / `job.finished`. Status values are `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. By default, `create` / `fetch` raise on `FAILED`, and `wait` raises on `FAILED` or `CANCELED`; `UNKNOWN` is terminal and returned. + +## 2.0.3 Rerank + ```python -from dashscope import TextReRank +client.rerank.create( + *, model: str, query: str, documents: list[str], top_n=None, + return_documents=None, instruct=None, parameters=None, timeout=None, + api_key=None, workspace=None, +) -> RerankResponse +``` -resp = TextReRank.call( - model=TextReRank.Models.gte_rerank, - query="What is a text embedding?", - documents=["Text embeddings map text into a dense vector space", "The weather is nice today", "Embeddings are often used for semantic retrieval"], - top_n=2, return_documents=True, -) -if resp.status_code == 200: - for r in resp.output.results: - print(r["index"], r["relevance_score"], r["document"]) +`top_n` defaults to service behavior (normally all documents); `return_documents=True` includes source text. `instruct` supplies a ranking instruction where supported. Empty `query` or `documents` raises `ValueError`. + +```python +import dashscope + +with dashscope.Client() as client: + ranked = client.rerank.create( + model="gte-rerank-v2", + query="什么是文本向量?", + documents=["文本向量把文本映射到向量空间。", "今天天气晴朗。"], + top_n=1, + return_documents=True, + ) + for result in ranked.results: + document_text = result.document.text if result.document else None + print(result.index, result.relevance_score, document_text) ``` +`RerankResponse.results` is flat. Each result has `index`, `relevance_score`, and optional `document`; `document` is an object whose text is read as `.text`, not a plain string. `index` refers to the original `documents` position. + +## 2.0.3 Tokenization + +```python +client.tokenization.count( + *, model: str, prompt=None, messages=None, parameters=None, + timeout=None, api_key=None, workspace=None, +) -> TokenCount +``` + +Supply `prompt` or `messages`; if both are supplied, `prompt` is appended as the latest user message. With neither, the SDK raises `ValueError`. + +```python +import dashscope +from dashscope.tokenizers import get_tokenizer + +with dashscope.Client() as client: + count = client.tokenization.count(model="qwen-plus", prompt="你好,世界") + print(count.input_tokens) + print(count.token_ids, count.tokens) # present when returned by the service + +# Local tokenizer: no HTTP request and no Client lifecycle to manage. +tokenizer = get_tokenizer("qwen-plus") +token_ids = tokenizer.encode("你好,世界") +print(tokenizer.decode(token_ids)) +``` + +`TokenCount` exposes `usage`, convenience `input_tokens`, and optional flat `token_ids` / `tokens`. Async clients use `await` for every network operation above and `await client.aclose()` when not using `async with`. + ## RAG Integration Guide -- Self-built pipeline: use TextEmbedding with `text_type="document"` to vectorize the corpus into a vector store; query with `text_type="query"`; after coarse ranking by the vector store, use TextReRank for fine ranking, then feed top_n results to the generation model. -- Managed solution: create a knowledge base in the Bailian console and bind it to an application, then call `dashscope.Application.call(app_id=...)` directly; vectors and retrieval are managed by the platform. See the Application-related documentation. +- Self-built pipeline: call `client.embeddings.create(..., text_type="document")` for the corpus and `text_type="query"` for queries. After vector-store recall, pass candidates to `client.rerank.create`, then send the top results to `client.chat.completions.create`. +- Managed Bailian knowledge-base applications: configure the knowledge base in the console and call `client.apps.completions.create(app_id=..., prompt=...)`; do not manually duplicate platform-managed retrieval. + +## Errors (brief) + +- In 2.0, catch `dashscope.errors.DashScopeError` or a specific subclass. With `raise_on_error=False`, check `response.is_error`, `code`, `message`, and `request_id`; for jobs also check `status`. +- In 1.x, continue checking `status_code`, `code`, and `message`; do not assume HTTP failures raise as they do in the default 2.0 mode. -## Common Error Codes +## 1.x Compatibility and Maintenance Guide -HTTP errors do not raise exceptions: check `resp.status_code != 200`, then read `resp.code` / `resp.message`. +Use these contracts only for existing 1.x calls. They remain available as legacy exports in SDK 2.0.3 but are not the recommended surface for new code. -| Error Code | HTTP Status | Meaning | Handling | -| --- | --- | --- | --- | -| InvalidParameter | 400 | Missing/invalid parameter (e.g. empty documents, unsupported dimension value) | Verify per message; do not pass dimension/output_type for v1/v2 | -| InvalidApiKey | 401 | Invalid API Key | Check api_key or DASHSCOPE_API_KEY | -| AccessDenied | 403 | Model not activated or no permission | Activate the model service; check workspace | -| Model.NotExist / InvalidModel | 404 | Wrong model name | Verify model name (distinguish -v4 from -async-v2) | -| Throttling | 429 | Rate limit hit / quota exhausted | Reduce concurrency, retry with backoff, request quota increase | -| InternalError | 500 | Server internal error | Record request_id and retry, or file a ticket | -| ServiceUnavailable | 503 | Service temporarily unavailable | Retry later (SDK treats 503/504 as retryable) | +| 1.x entry point | Maintenance contract | +| --- | --- | +| `TextEmbedding.call(model, input, workspace=None, api_key=None, text_type=None, dimension=None, output_type=None, instruct=None, **kwargs)` | Returns `DashScopeAPIResponse`; read `output["embeddings"]`, whose items use `text_index` and `embedding` | +| `MultiModalEmbedding.call(...)` / `AioMultiModalEmbedding.call(...)` | Input item classes include text/image/audio variants with `factor`; read `output["embeddings"]` (or old one-peace `output["embedding"]`) | +| `BatchTextEmbedding.call(model, url, ...)` / `async_call` / `fetch` / `wait` / `cancel` / `list` | `call` submits and waits, `async_call` only submits; completed `output.url` points to the result file | +| `TextReRank.call(model, query, documents, return_documents=None, top_n=None, api_key=None, instruct=None, **kwargs)` | Read `output.results`; `document` follows the 1.x response representation | +| `Tokenization.call` | Keep its legacy response access, including `output.token_ids` / `output.tokens`, when maintaining old code | + +For multimodal 1.x calls, `dimension`, `output_type`, `fps`, `instruct`, `enable_fusion`, `res_level`, and `max_video_frames` remain model-dependent. `auto_truncation` is a 1.x extra option for supported models. Empty model/input validation raises 1.x `ModelRequired` / `InputRequired`; batch `url` is required. + +```python +from http import HTTPStatus +from dashscope import TextEmbedding, TextReRank + +response = TextEmbedding.call( + model=TextEmbedding.Models.text_embedding_v3, + input=["hello", "world"], + text_type="document", + dimension=1024, +) +if response.status_code == HTTPStatus.OK: + for item in response.output["embeddings"]: + print(item["text_index"], item["embedding"][:3]) +else: + print(response.request_id, response.code, response.message) + +ranked = TextReRank.call( + model=TextReRank.Models.gte_rerank, + query="What is a text embedding?", + documents=["Text embeddings map text into vectors", "The weather is nice"], + top_n=1, + return_documents=True, +) +if ranked.status_code == HTTPStatus.OK: + for result in ranked.output.results: + print(result["index"], result["relevance_score"], result["document"]) +``` -SDK local pre-validation (raises directly without sending a request): `InputRequired` (empty query/documents for rerank, empty url for batch, empty input for multimodal), `ModelRequired` (empty model). +All synchronous 1.x responses above expose `status_code`, `request_id`, `code`, `message`, `output`, and `usage`. Batch `output` includes `task_id`, `task_status`, and `url`; rerank results include `index`, `relevance_score`, and optionally `document`. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md index de92118c..f48465c1 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/sdk-example.md @@ -4,10 +4,17 @@ description: Generate runnable DashScope SDK code examples (Python/Java) arguments: [api_name] --- -Before generating code, verify the user's installed SDK version and API signature: -1. `run_command("python -c 'import dashscope; print(dashscope.__version__)'")` -2. `run_command("python -c 'import inspect, dashscope; print(inspect.signature(dashscope.{api_name}.call))'")` +Before generating code, identify the API generation and verify the user's installed SDK: -Based on the actual signature, generate a directly runnable code example (Python) for the DashScope SDK's {api_name}, including imports, the call, and result handling. If both streaming and non-streaming modes are available, show both. Make sure to use the latest messages format. +1. Check the version: `run_command("python -c 'import dashscope; print(dashscope.__version__)'")`. +2. Treat resource calls such as `client.chat.completions.create` as 2.0 and module/class calls such as `dashscope.Generation.call` as 1.x. Do not infer a migration request merely because 2.0 is installed. +3. Resolve the actual object named by `{api_name}`, then inspect the real method with `inspect.signature` and, when needed, `help()`. For a 2.0 resource, instantiate `dashscope.Client(api_key="sk-test")`, inspect the bound resource method, and close the client; do not assume every API has a `.call()` method. +4. Cross-check the matching domain skill and cookbook example. Parameter names and types must match the installed implementation, not memory. -Parameter names and types in the generated code must exactly match the inspection results. +Generate a directly runnable Python example for `{api_name}` with imports, invocation, response handling, and error handling. + +- For new 2.0 code, use `Client` or `AsyncClient`, close it directly or with a context manager, and read the flattened typed response rather than `.output`. Show streaming and non-streaming forms only when that resource supports both; chat streams expose incremental `delta` content. +- For a fix to existing 1.x code, preserve its `.call()` style and response contract unless the user explicitly asks to migrate. Keep response `status_code` / `code` / `message` checks where they belong. +- Mark capabilities that exist only in 2.0. Do not invent compatibility aliases, resources, parameters, or return fields. + +Keep Java examples on the Java SDK's own version line; do not substitute Python 2.0 signatures for Java APIs. diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md index 5f0573b2..a39b130f 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/speech.md @@ -4,92 +4,183 @@ description: Quick reference for Speech (SpeechSynthesizer synthesis / Transcrip --- # Speech -Before answering, locate the right path: synthesis goes through `SpeechSynthesizer` (legacy `dashscope.audio.tts` / new `dashscope.audio.tts_v2`); recorded-file transcription goes through `dashscope.audio.asr.Transcription` (async task). Parameter names must be verified against the source in `dashscope/audio/tts/`, `dashscope/audio/tts_v2/`, and `dashscope/audio/asr/transcription.py`; never answer from memory. +For Python SDK 2.0.3, start with `dashscope.Client` or `dashscope.AsyncClient`; the 2.0 API is at the package root (`dashscope.v2` does not exist). Verify parameters against `dashscope/resources/audio/`, `dashscope/resources/tingwu.py`, and `dashscope/types/` rather than answering from memory. Use a context manager or call `close()`/`aclose()` so HTTP and WebSocket resources are released. -## Applicable Models and Scenarios +## Choose the Current API -- Speech synthesis (TTS): - - `dashscope.audio.tts.SpeechSynthesizer`: Sambert series (e.g. `sambert-zhichu-v1`), WebSocket streaming, class method `call` for one-shot synthesis. - - `dashscope.audio.tts_v2.SpeechSynthesizer`: CosyVoice series (e.g. `cosyvoice-v1`, `cosyvoice-v2`), requires a `voice` (e.g. `longxiaochun`, `longxiaochun_v2`), supports streaming text input and voice-clone timbres. -- Recorded-file transcription (ASR): `dashscope.audio.asr.Transcription`, models listed in `Transcription.Models`: `paraformer-v1`, `paraformer-8k-v1` (8kHz telephony), `paraformer-mtl-v1` (multilingual); input is a list of publicly accessible audio URLs, async task. +| Scenario | Current entry point | Contract | +| --- | --- | --- | +| WebSocket TTS, complete audio | `client.audio.tts.synthesize(...)` | Returns audio `bytes` | +| WebSocket TTS, output streaming | `client.audio.tts.stream(...)` | Iterates audio `bytes`; close the iterator if exiting early | +| WebSocket TTS, incremental text | `client.audio.tts.connect(...)` | Synchronous duplex `SynthesisSession` | +| HTTP/SSE TTS | `client.audio.tts.http.synthesize/stream(...)` | URL result or SSE audio chunks, not a WebSocket task | +| Recorded-file transcription | `client.audio.asr.transcribe(...)` or `client.audio.asr.jobs` | Asynchronous server task represented by a flat `Job` | +| Streaming speech recognition | `client.audio.asr.recognize(...)` | Synchronous duplex WebSocket `Recognition` session | +| Omni/Qwen-Audio/Livetranslate | `client.audio.realtime.connect(...)` | Synchronous realtime WebSocket JSON events | +| TingWu offline meeting processing | `client.tingwu.call(...)` | `TingWuResult`; an async variant also exists | +| TingWu streaming recognition | `client.tingwu.realtime(...)` | Synchronous WebSocket session | -## SDK APIs +**AsyncAudio limitation:** `AsyncClient.audio` currently exposes only `audio.asr` for file transcription (`transcribe` and `jobs`). It does not expose TTS, real-time recognition, or `audio.realtime`; use synchronous `Client` for those WebSocket APIs. TingWu real-time sessions are also synchronous. Do not wrap a synchronous session method in `await`. -`tts.SpeechSynthesizer.call(model, text, callback=None, workspace=None, **kwargs) -> SpeechSynthesisResult`, key kwargs: +## WebSocket TTS (`client.audio.tts`) -| Parameter | Description | -|---|---| -| format | `SpeechSynthesizer.AudioFormat`: `format_wav` (default) / `format_pcm` / `format_mp3` | -| sample_rate | sample rate; defaults to the model's sample rate | -| volume | volume 0~100, default 50 | -| rate / pitch | speech rate / pitch, 0.5~2.0, default 1.0 | -| word_timestamp_enabled / phoneme_timestamp_enabled | word-level / phoneme-level timestamps, default False | -| callback | subclass of `ResultCallback`: `on_open/on_event/on_error/on_complete/on_close` | +`synthesize`, `stream`, and `connect` default to `model="cosyvoice-v1"`, `voice="longxiaochun"`, `format="mp3"`, and `sample_rate=22050`. Named options include `volume`, `rate`, `pitch`, `language_hints`, `enable_ssml`, `parameters`, `timeout`, `api_key`, and `workspace`. -`tts_v2.SpeechSynthesizer(model, voice, format=AudioFormat.DEFAULT, volume=50, speech_rate=1.0, pitch_rate=1.0, seed=0, instruction=None, language_hints=None, callback=None, workspace=None, ...)` is constructor-based: -- `call(text, timeout_millis=None)`: without callback, blocks and returns the full audio `bytes`; with callback, audio is delivered in real time via `on_data(data: bytes)`. -- Streaming input: `streaming_call(text)` (may be called multiple times) -> `streaming_complete()` to wait for completion; `streaming_cancel()` to cancel. Empty `model` raises `ModelRequired`; empty apikey raises `InputRequired`. -- `AudioFormat` enum values look like `MP3_22050HZ_MONO_256KBPS`, `WAV_16000HZ_MONO_16BIT`, `PCM_*`, `OGG_OPUS_*`; `DEFAULT` is treated as mp3/22050Hz. -- Helpers: `get_last_request_id()`, `get_first_package_delay()`, `get_response()`. +```python +from pathlib import Path +import dashscope + +with dashscope.Client() as client: # reads DASHSCOPE_API_KEY by default + audio = client.audio.tts.synthesize( + "Hello, Bailian.", + model="cosyvoice-v1", + voice="longxiaochun", + format="mp3", + sample_rate=22050, + ) + Path("out.mp3").write_bytes(audio) +``` + +- `stream(text, ...)` yields encoded audio chunks. Consume it fully, or call its `close()` when abandoning iteration; do not mistake it for an HTTP response. +- `connect(...)` is a context manager. Use `send_text()` for fragments, `flush()` to request buffered synthesis without ending input, and `stop()` to end input. `stop()` does not wait for output: keep one receiver draining `audio_chunks()` or `events()` through completion. Use a sender thread and a single receiver thread for concurrent duplex traffic; `cancel()` requests cancellation. +- A caller-owned `client.ws_connection()` can carry compatible ASR/TTS/TingWu tasks. Close that connection separately; realtime Omni sessions cannot use it. + +### HTTP/SSE TTS (`client.audio.tts.http`) -`Transcription` (async task): +Use this sibling when the desired model/protocol returns an audio URL instead of WebSocket bytes: + +```python +import dashscope + +with dashscope.Client() as client: + result = client.audio.tts.http.synthesize( + "Hello, Bailian.", + model="cosyvoice-v1", + voice="longxiaochun", + ) + print(result.audio_url, result.audio_id, result.expires_at) +``` -| Method | Description | -|---|---| -| `call(model, file_urls, phrase_id=None, api_key=None, workspace=None, **kwargs)` | synchronous wrapper that waits for the result | -| `async_call(...)` (same signature) | submits the task only; returns a response containing `output.task_id` | -| `fetch(task, ...)` | single query; `task` accepts a task_id string or the response object from async_call | -| `wait(task, wait_timeout=-1, ...)` | polls until a terminal state; `wait_timeout=-1` means no time limit | +`http.stream(...)` yields chunks with flat `audio` bytes, `sentence`, `finish_reason`, and `is_final`; the final chunk can carry whole-audio URL fields. Close an SSE stream if not consuming it fully. These fields are not WebSocket `bytes` frames. -Transcription kwargs: `channel_id: List[int]`, `disfluency_removal_enabled` (remove filler words), `diarization_enabled` (speaker diarization), `speaker_count`, `timestamp_alignment_enabled`, `special_word_filter`, `audio_event_detection_enabled`; `phrase_id` (hotwords) is converted to `resources=[{"resource_id": phrase_id, "resource_type": "asr_phrase"}]`. Task flow: `async_call` -> `output.task_id` (`task_status=PENDING`) -> `wait`/`fetch` -> `SUCCEEDED` to get results. +## File Transcription (`client.audio.asr`) -## Input/Output +`transcribe(file_urls, ...)` submits and waits. Its current default model is `qwen-audio-3.1-asr-flash-filetrans`; pass `model=` explicitly when selecting another model. `parameters` carries service-specific options and `phrase_id` selects a hotword resource. Waiting defaults to 86400 seconds and uses bounded exponential polling. -- Synthesis input: `text` (utf-8 text). Output `SpeechSynthesisResult`: `get_audio_data()` for the full audio `bytes`, `get_audio_frame()` for streaming frames, `get_timestamp()/get_timestamps()` for sentence timestamps (`sentence` contains `begin_time`/`end_time`), `get_response()` for the `SpeechSynthesisResponse` (`status_code/code/message`, `usage.characters` billed character count). -- Transcription input: `file_urls: List[str]` (audio file URLs). -- Transcription output `TranscriptionResponse`: `status_code`, `request_id`, `code`, `message`, `output.task_id`, `output.task_status` (`PENDING/RUNNING/SUCCEEDED/FAILED/CANCELED/UNKNOWN`), `usage`. On success, each item in `output.results[]` contains `transcription_url` — the download URL of the recognition-result JSON, which you must GET and parse yourself (contains per-sentence text and timestamps). +For separate submission and polling, use `jobs.create(*, model, file_urls, parameters=None, phrase_id=None, ...)`, `jobs.fetch(task_id)`, and `jobs.wait(job_or_task_id)`. A `Job` exposes `task_id`, `status`, `results`, `finished`, `succeeded`, and common flat metadata. On success, each result's `transcription_url` is a URL to result JSON that the application must download and parse. Terminal states include `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. -## Minimal Examples +```python +import dashscope + +with dashscope.Client() as client: + submitted = client.audio.asr.jobs.create( + model="qwen-audio-3.1-asr-flash-filetrans", + file_urls=["https://example.com/audio.wav"], + ) + job = client.audio.asr.jobs.wait(submitted) + print(job.task_id, job.status) + for result in job.results or []: + print(result.transcription_url) +``` -Synthesis (Sambert): +The same file-transaction surface is asynchronous on `AsyncClient`: ```python -from dashscope.audio.tts import SpeechSynthesizer +import asyncio +import dashscope -result = SpeechSynthesizer.call(model="sambert-zhichu-v1", text="Hello, Bailian.") -with open("out.wav", "wb") as f: - f.write(result.get_audio_data()) +async def main(): + async with dashscope.AsyncClient() as client: + job = await client.audio.asr.transcribe( + file_urls=["https://example.com/audio.wav"], + ) + print(job.status) + +asyncio.run(main()) ``` -Transcription (async task): +`Job.output`, when present, is the retained raw service payload, not the normal 2.0 access pattern; use the flat fields above. + +## Real-Time Recognition (`client.audio.asr.recognize`) + +`recognize(*, model, format="pcm", sample_rate=16000, vocabulary_id=None, disfluency_removal_enabled=None, diarization_enabled=None, parameters=None, ...)` creates a synchronous session. It supports `start()`, `send_audio_frame(bytes)`, `update_context()`, `stop()`, `events()`, `results()`, and `close()`, and can be used as a context manager. ```python -from http import HTTPStatus -from dashscope.audio.asr import Transcription - -task = Transcription.async_call( - model=Transcription.Models.paraformer_v1, - file_urls=["https://example.com/audio.wav"], -) -if task.status_code == HTTPStatus.OK: - rsp = Transcription.wait(task) # or Transcription.wait(task.output.task_id) - if rsp.output.task_status == "SUCCEEDED": - print(rsp.output.results) +import dashscope + +pcm_frames = [b"..."] # replace with actual PCM frames + +with dashscope.Client() as client: + with client.audio.asr.recognize( + model="paraformer-realtime-v2", format="pcm", sample_rate=16000, + ) as session: + for frame in pcm_frames: + session.send_audio_frame(frame) + session.stop() # ends input only + for event in session.events(): # drain through task-finished + print(event) ``` -## Common Error Codes +For incremental sending while results arrive, run the sender and one receiver concurrently. `recognize_file(path, *, model, ...)` drives a local file through the same session and yields event dictionaries. Neither real-time method exists on `AsyncAudio`. + +## Realtime Omni / Qwen-Audio / Livetranslate + +`client.audio.realtime.connect(model=...)` is a synchronous context manager. Important methods include `update_session`, `append_audio`, `append_video`, `commit`, `clear_appended_audio`, `create_item`, `retrieve_item`, `delete_item`, `create_response`, `cancel_response`, `send_event`, `recv_event`, `events`, `end_session`, and `close`. + +```python +import dashscope + +with dashscope.Client() as client: + with client.audio.realtime.connect( + model="qwen-audio-3.0-realtime-plus", + ) as session: + session.update_session(["text"], enable_turn_detection=False) + session.create_item({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Say hello"}], + }) + session.create_response() + for event in session.events(): + print(event.get("type")) + if event.get("type") == "response.done": + break # Qwen-Audio: close after the response; no end_session() +``` + +Events are JSON dictionaries; output audio is Base64 inside events, not an iterator of `bytes`. `events()` normally ends at `session.finished` or normal close, not automatically at every `response.done`, so a single-turn caller may break as above. Protocol capabilities differ by model: Qwen-Audio does not support `end_session()`, while Omni/Livetranslate lifecycle and session options must follow their own protocol. Realtime uses one socket per session and does not support `WsConnection` task reuse. + +## TingWu + +Offline processing uses `client.tingwu.call(*, model, input: dict, parameters=None, ...)`; with `AsyncClient`, await the same method. The input dictionary contains service fields such as `fileUrl` and `appid`. + +```python +import dashscope + +with dashscope.Client() as client: + result = client.tingwu.call( + model="tingwu-turbo", + input={ + "fileUrl": "https://example.com/meeting.mp3", + "appid": "YOUR_APP_ID", + }, + ) + print(result.request_id, result.status_code) +``` + +`TingWuResult` intentionally retains service `output` and `usage` fields; this is a resource-specific result model, not a reason to use legacy `.output` envelopes on other 2.0 resources. + +Real-time TingWu uses synchronous `client.tingwu.realtime(*, model, app_id, format="pcm", sample_rate=16000, terminology=None, max_end_silence=None, parameters=None, ...)`. In its context manager, send PCM with `send_audio_frame()`, call `stop()`, and drain `events()`; iteration ends at `speech-end`. + +## 1.x Compatibility — Maintenance Only + +Keep the following contracts only when repairing existing 1.x code. Do not combine them with `Client` resource calls or flat 2.0 models. -| Error code | HTTP status | Meaning | Handling | -|---|---|---|---| -| InvalidApiKey | 401 | API Key missing/invalid | Check `dashscope.api_key` or the `DASHSCOPE_API_KEY` environment variable | -| InvalidParameter | 400 | Invalid parameters (file_urls unreachable, unsupported format/sample_rate, etc.) | Check each item against the parameter tables in this file | -| InvalidModel | 400 | Wrong model name | Use `Transcription.Models` constants or official model names | -| Throttling | 429 | Rate limited | Retry with exponential backoff, reduce concurrency (`fetch` already retries timeout/connection errors 3 times internally) | -| InternalError | 500 | Server-side error | Retry with `request_id` or file a ticket | -| Task FAILED | 200 | Transcription subtask failed | Read `output.task_status` and failure details in `output`; check audio URL reachability, format, and duration | -| task-failed (WebSocket) | - | Synthesis task failed; tts_v2 without callback raises `Exception("TaskFailed: ...")` | Read the code/message in the event message to locate the cause | +- `dashscope.audio.tts.SpeechSynthesizer`: Sambert synthesis; class method `call(model, text, callback=None, workspace=None, **kwargs)`. Important kwargs include `format`, `sample_rate`, `volume`, `rate`, `pitch`, timestamps, and `ResultCallback`. `SpeechSynthesisResult` uses `get_audio_data()`, `get_audio_frame()`, timestamp getters, and `get_response()`. +- `dashscope.audio.tts_v2.SpeechSynthesizer`: constructor-based CosyVoice synthesis. `call(text)` returns full `bytes` without a callback; callbacks receive `on_data(data)`. Incremental input uses `streaming_call()` / `streaming_complete()` / `streaming_cancel()`. Diagnostics include `get_last_request_id()`, `get_first_package_delay()`, and `get_response()`. +- `dashscope.audio.asr.Transcription`: recorded-file tasks. `call()` waits, `async_call()` submits, and `fetch()` / `wait()` query the task. Its legacy response uses `output.task_id`, `output.task_status`, and `output.results[].transcription_url`; `phrase_id` becomes an `asr_phrase` resource. Preserve this envelope only in old code. +- Legacy local exceptions include `ModelRequired`, `InputRequired`, and `InvalidTask`; legacy WebSocket callbacks can receive `task-failed`. Diagnose task failure from the legacy response/event's code and message. -Local validation exceptions (no request sent, see `dashscope/common/error.py`): `ModelRequired` (missing model), `InputRequired` (missing apikey/format/callback), `InvalidTask` (started twice or submitted before starting). +For new 2.0 code, errors come from `dashscope.errors`; normally `raise_on_error=True` raises them. With `raise_on_error=False`, inspect flat `is_error`, `status_code`, `code`, `message`, and `request_id`. See the dedicated errors guide for the hierarchy and retry policy. ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md index 1785ea22..67585768 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/text-generation.md @@ -5,118 +5,182 @@ description: Quick reference for text generation (Generation / OpenAI-compatible # Text Generation -Before answering, verify the user's SDK version and signature: `python -c "import dashscope,inspect; print(dashscope.__version__); print(inspect.signature(dashscope.Generation.call))"`. Based on SDK source `dashscope/aigc/generation.py`. +For new Python code, target **DashScope SDK 2.0.3** and use `dashscope.Client` / `dashscope.AsyncClient`. The 2.0 implementation is in the root `dashscope` package; **`dashscope.v2` does not exist**. Check the installed version with `python -c "import dashscope; print(dashscope.__version__)"`. Keep 1.x repair advice in the compatibility section below; do not mix its `.output` response contract with 2.0. -## Applicable Models and Scenarios +Authoritative sources: `dashscope/resources/chat/completions.py`, `dashscope/resources/decision.py`, `dashscope/types/completions.py`, and `dashscope/_quickstart.py`. -- Entry point `dashscope.Generation` (sync) / `dashscope.AioGeneration` (async, both top-level exports in `dashscope/__init__.py`), `task = "text-generation"`. -- Plain text chat/continuation: qwen series (e.g. `qwen3.7-plus`, `qwen3.8-flash`). `Generation.Models` only carries a few legacy constants; pass the current model name as a plain string. -- The `model` parameter is just a `str`; the SDK does not validate against a whitelist. For new models like qwen3, pass the model name string directly; refer to the console/docs for exact names. -- `enable_search` is only supported when `model` starts with `"qwen"`; when it starts with `"bailian"` you must pass `customized_model_id`, otherwise `InputRequired` is raised. -- Do not use this API for multimodal; use `MultiModalConversation`. +## 2.0.3 Chat Completions -## SDK Interface +Use `client.chat.completions.create` for text generation, continuation, tool calling, structured output, and chat-style understanding. Pass current model names such as `qwen-plus` as strings; the client does not maintain a model whitelist. The caller owns multi-turn `messages` history. Set `DASHSCOPE_API_KEY`, or pass `api_key=` to the client. -Signature (`Generation.call`, classmethod; `AioGeneration.call` is the async version with the same parameters): +All parameters are keyword-only: ```python -Generation.call(model, prompt=None, history=None, api_key=None, messages=None, - plugins=None, workspace=None, stream=None, temperature=None, top_p=None, - top_k=None, max_tokens=None, seed=None, stop=None, repetition_penalty=None, - presence_penalty=None, result_format=None, incremental_output=None, - enable_search=None, tools=None, tool_choice=None, enable_thinking=None, - thinking_budget=None, n=None, logprobs=None, top_logprobs=None, - search_options=None, parallel_tool_calls=None, response_format=None, - output_format=None, **kwargs) +create(*, model: str, messages: str | list[MessageInput], stream: bool = False, + tools=None, temperature=None, top_p=None, max_tokens=None, stop=None, + seed=None, repetition_penalty=None, presence_penalty=None, + enable_thinking=None, response_format=None, top_k=None, tool_choice=None, + n=None, logprobs=None, top_logprobs=None, thinking_budget=None, + enable_search=None, search_options=None, parallel_tool_calls=None, + incremental_output=None, parameters=None, timeout=None, + api_key=None, workspace=None) ``` -Returns: `Generator[GenerationResponse, None, None]` when `stream=True` (AsyncGenerator for async), otherwise a single `GenerationResponse`. - -| Parameter | Type | Default | Description | -| --- | --- | --- | --- | -| model | str | required | Model name; raises `ModelRequired` if empty | -| prompt | Any | None | Text input; at least one of prompt/messages required, otherwise raises `InputRequired` | -| messages | List[Message] | None | `[{"role":..., "content":...}]`; role constants in `Role`: user/system/assistant (plus bot/attachment) | -| api_key | str | None | Defaults to `dashscope.api_key` (i.e. env var `DASHSCOPE_API_KEY`) | -| stream | bool | None | Whether to stream | -| incremental_output | bool | None | Streaming only: True=each chunk has only new tokens; False=cumulative full text (for some models the SDK requests increments internally and merges them into full text) | -| result_format | str | None | `"message"` (recommended, via output.choices) or `"text"` (via output.text) | -| temperature / top_p / top_k | float/float/int | None | Sampling: [0,2) / (0,1.0] / candidate set size | -| max_tokens / seed / stop | int/int/str\|list | None | Max output tokens / random seed / stop words | -| repetition_penalty / presence_penalty | float | None | Repetition penalty, 1.0 = no penalty / range [-2.0, 2.0] | -| enable_search / search_options | bool/dict | None | Web search (only for qwen*) and its options | -| tools / tool_choice / parallel_tool_calls | list/str\|dict/bool | None | function calling definitions, selection strategy, parallel calls | -| enable_thinking / thinking_budget | bool/int | None | Hybrid thinking models: enable thinking / thinking token budget | -| n / logprobs / top_logprobs | int/bool/int | None | Number of generations 1-4 / return log probabilities and candidates per position | -| response_format | dict | None | e.g. `{"type": "json_object"}` (JSON mode) | -| output_format | str | None | Deep-research models only: "model_detailed_report" (default) / "model_summary_report" | -| workspace / plugins | str/str\|dict | None | Workspace id / plugins (sent via `X-DashScope-Plugin` header) | - -## Input/Output - -- Input is one of two: `messages` (chat) or `prompt` (`history` is deprecated, do not recommend). -- Response `GenerationResponse` (DictMixin, accessible via attributes or subscript): `status_code`, `request_id`, `code`, `message`, `output`, `usage`. -- `output` (GenerationOutput): `text`+`finish_reason` (text format); `choices[i].finish_reason`, `choices[i].message.role/content` (message format). Common `finish_reason` values: `stop`/`length`/`null` (mid-stream). -- `usage` (GenerationUsage): `input_tokens`, `output_tokens` (sum them yourself for the total). -- Streaming: each chunk is also a `GenerationResponse`; with `incremental_output=True`, concatenate each chunk's content for the full text. - -## OpenAI-Compatible API - -- Use the openai library with base_url from `dashscope.base_compatible_api_url`, default `https://dashscope.aliyuncs.com/compatible-mode/v1` (overridable via env var `DASHSCOPE_COMPATIBLE_BASE_URL`). -- Differences: returns native OpenAI objects (`choices[0].message.content`, `usage.prompt_tokens/completion_tokens/total_tokens`); errors are raised as openai exceptions (AuthenticationError/BadRequestError/RateLimitError etc.) instead of a code in the response body. - -## Minimal Example +- `messages` accepts a text string or message dictionaries such as `{"role": "user", "content": "Hello"}`. +- Use named sampling, thinking, search, and tool parameters directly. Put additional model-specific parameters in `parameters={...}` rather than inventing SDK keyword arguments. +- `response_format={"type": "json_object"}` requests JSON mode where supported. Support for each option remains model-dependent. +- `AsyncClient` exposes the same signature with `await`; use `async with dashscope.AsyncClient() as client` and `await client.aclose()` when not using a context manager. + +### Non-streaming and streaming delta ```python -from http import HTTPStatus import dashscope -from dashscope import Generation -dashscope.api_key = "sk-xxx" # or set env var DASHSCOPE_API_KEY +with dashscope.Client() as client: + response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "用一句话介绍杭州"}], + temperature=0.7, + max_tokens=256, + ) + print(response.choices[0].message.content) + # Equivalent convenience view for the first choice: + print(response.output_text) + print(response.usage.total_tokens, response.request_id) + + stream = client.chat.completions.create( + model="qwen-plus", + messages="写一首短诗", + stream=True, + ) + try: + for chunk in stream: + if not chunk.choices: # a trailing usage-only chunk is valid + continue + delta = chunk.choices[0].delta + if delta.content: + print(delta.content, end="", flush=True) + finally: + stream.close() +``` + +A non-streaming `ChatCompletion` exposes flat `choices`, `usage`, `search_info`, `model`, `request_id`, and the shared response envelope. Read `choices[i].message.content`, `finish_reason`, `reasoning_content`, and `tool_calls`; **do not read `.output`**. `usage` has `input_tokens`, `output_tokens`, and `total_tokens`. + +Streaming returns `ChatStream` (`AsyncChatStream` for async). Each `ChatCompletionChunk` contains `choices[i].delta`, not `message`. By default frames are incremental. `incremental_output=False` requests cumulative snapshots, with client-side merging where necessary; in that mode do not concatenate every snapshot. Helpers include `stream.text_stream`, `reasoning_stream`, `tool_call_stream`, and `get_final_message()`; `headers` is on the stream. Async streaming uses `stream = await client.chat.completions.create(..., stream=True)`, `async for`, and `await stream.aclose()`. + +### One-call shortcut + +For a one-off script, `dashscope.chat(model, prompt, **kwargs)` forwards extra keyword arguments to `chat.completions.create`: + +```python +import dashscope + +response = dashscope.chat("qwen-plus", "给我讲个笑话") +print(response.output_text) +``` + +This shortcut uses a lazy process-lifetime singleton client. Prefer an explicitly managed `Client` / `AsyncClient` in applications and tests. + +## 2.0.3 Decision (not text generation) + +`client.decision.create` is the System One typed-decision API, currently using `model="decision-model-preview"`. It answers questions about a business `state`; it **does not generate text and has no streaming API**. It also does not provide function calling or web search. There is no 1.x Decision wrapper. + +```python +create(*, model: str, state: str | dict | list, + questions: dict[str, QuestionInput], timeout=None, + api_key=None, workspace=None) -> DecisionResponse +``` + +`questions` maps caller-chosen question IDs to typed questions or raw wire dictionaries. The typed models are imported from `dashscope.resources.decision`: -# Non-streaming -resp = Generation.call(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Hello"}], - result_format="message") -if resp.status_code == HTTPStatus.OK: - print(resp.output.choices[0].message.content) +| Question | Request fields | Corresponding answer fields | +| --- | --- | --- | +| `ChoiceQuestion` | `type="choice"`, optional `instructions`, required `criteria: Dict[str, str]` with 1–255 option descriptions | `choice` selected option, `probabilities`, `confidence` | +| `NoulQuestion` | `type="noul"`, optional `instructions` and optional `criteria: Dict[str, str]` | `noul` = P(yes), where 0=no and 1=yes; `probabilities` | +| `ScoreQuestion` | `type="score"`, optional `instructions`, required low-to-high `criteria: List[str]` with 2–255 levels | `score` probability-weighted level index, `probabilities`, `confidence`, `legend` | + +Every answer also has optional `type`; the service may add extra fields. `probabilities` covers all options/levels. `DecisionResponse` exposes flat `answers`, `model`, `usage`, and `latency_ms`; answer keys mirror the request question IDs. + +```python +from dashscope import Client +from dashscope.resources.decision import ChoiceQuestion, NoulQuestion, ScoreQuestion + +with Client() as client: + response = client.decision.create( + model="decision-model-preview", + state={"ticket_id": "T-1001", "content": "支付后仍未到账"}, + questions={ + "department": ChoiceQuestion( + instructions="应该由哪个团队处理?", + criteria={"billing": "支付和账单", "technical": "产品故障"}, + ), + "escalate": NoulQuestion(instructions="是否通知值班人员?"), + "severity": ScoreQuestion( + instructions="严重程度如何?", + criteria=["轻微", "中等", "严重"], + ), + }, + ) + +print(response.answers["department"].choice) +print(response.answers["department"].probabilities) +print(response.answers["escalate"].noul) +print(response.answers["severity"].score) +print(response.answers["severity"].probabilities) +``` + +An empty `questions` mapping raises `ValueError`; typed question fields are validated locally by pydantic. Raw dictionaries pass through for forward-compatible fields. Async code uses `await client.decision.create(...)` and returns the same `DecisionResponse` contract. + +## Errors (brief) + +- In 2.0, catch exceptions from `dashscope.errors` (normally `DashScopeError` or a more specific subclass). With `client.with_options(raise_on_error=False)`, check `response.is_error`, then `code` / `message` / `request_id`. +- In 1.x, continue checking `response.status_code`, `response.code`, and `response.message`; local validation can still raise 1.x exceptions. + +## 1.x Compatibility and Repair Guide + +Use this section only to maintain `Generation` / `AioGeneration` applications. These remain 1.x APIs even though they are still exported from the root package in SDK 2.0.3. + +- Entry points: `dashscope.Generation.call` (sync) and `dashscope.AioGeneration.call` (async), `task="text-generation"`. `Generation.Models` contains only legacy constants; pass current model names as strings. +- Signature: `call(model, prompt=None, history=None, api_key=None, messages=None, plugins=None, workspace=None, stream=None, temperature=None, top_p=None, top_k=None, max_tokens=None, seed=None, stop=None, repetition_penalty=None, presence_penalty=None, result_format=None, incremental_output=None, enable_search=None, tools=None, tool_choice=None, enable_thinking=None, thinking_budget=None, n=None, logprobs=None, top_logprobs=None, search_options=None, parallel_tool_calls=None, response_format=None, output_format=None, **kwargs)`. +- Supply `prompt` or `messages`; `history` is deprecated. `result_format="message"` reads `output.choices`; `"text"` reads `output.text`. `enable_search` is supported only for model names beginning with `qwen`; `bailian*` requires `customized_model_id`. +- `stream=True` returns a generator (`AsyncGenerator` for `AioGeneration`). With `incremental_output=True`, concatenate chunk content; otherwise chunks may be cumulative. +- The 1.x `GenerationResponse` contract is `status_code`, `request_id`, `code`, `message`, `output`, and `usage`. In message mode use `output.choices[0].message.content`; usage exposes `input_tokens` and `output_tokens`. + +```python +from http import HTTPStatus +from dashscope import Generation + +response = Generation.call( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + result_format="message", +) +if response.status_code == HTTPStatus.OK: + print(response.output.choices[0].message.content) else: - print(resp.request_id, resp.code, resp.message) - -# Streaming (incremental output) -for r in Generation.call(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Write a short poem"}], - result_format="message", stream=True, - incremental_output=True): - if r.status_code == HTTPStatus.OK: - print(r.output.choices[0].message.content, end="") - else: - print(r.code, r.message); break - -# OpenAI-compatible -from openai import OpenAI -client = OpenAI(api_key=dashscope.api_key, base_url=dashscope.base_compatible_api_url) -print(client.chat.completions.create(model="qwen3.7-plus", - messages=[{"role": "user", "content": "Hello"}]).choices[0].message.content) + print(response.request_id, response.code, response.message) ``` -## Common Error Codes +HTTP failures in this 1.x API normally return an error response rather than raising. Missing model/input can raise `ModelRequired` / `InputRequired`; fix those locally before retrying. -Key point: Generation.call does **not** raise on HTTP failure; it returns a response with `status_code != 200`, with the error in `code`/`message`. Only local parameter validation raises `dashscope.common.error` exceptions. +## OpenAI-Compatible Library (independent API line) -| Error code/exception | HTTP | Meaning | Handling | -| --- | --- | --- | --- | -| InputRequired (exception) | local | Neither prompt nor messages passed | Supply the parameter before calling | -| ModelRequired (exception) | local | model not passed | Pass a model name | -| InvalidApiKey | 401 | API Key missing/invalid | Check DASHSCOPE_API_KEY or dashscope.api_key | -| InvalidParameter | 400 | Invalid parameters (result_format, messages structure, etc.) | Fix per the message | -| Model.NotExist | 400 | Wrong model name or no access | Verify model name and activation status | -| DataInspectionFailed | 400 | Blocked by content safety review | Adjust input/output prompts | -| Throttling / Throttling.RateQuota | 429 | Throttling / QPS exceeded | Lower concurrency, retry with exponential backoff | -| InternalError | 500 | Server-side error | Retry; file a ticket with request_id | -| ServiceUnavailable etc. | 503/504 | Service unavailable / gateway timeout | SDK auto-retries (REPEATABLE_STATUS); back off if it still fails | +This is separate from both the 2.0 resource client and 1.x `Generation`. Use the `openai` package with `base_url=dashscope.base_compatible_api_url` (default `https://dashscope.aliyuncs.com/compatible-mode/v1`, configurable through `DASHSCOPE_COMPATIBLE_BASE_URL`). It returns native OpenAI objects (`choices[0].message.content`, `usage.prompt_tokens/completion_tokens/total_tokens`) and raises `openai` exceptions. -Actual code strings are as returned by the server in `response.code`; in OpenAI-compatible mode they map to openai library exception types. +```python +import os +from openai import OpenAI + +with OpenAI( + api_key=os.environ["DASHSCOPE_API_KEY"], + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", +) as client: + response = client.chat.completions.create( + model="qwen-plus", + messages=[{"role": "user", "content": "Hello"}], + ) + print(response.choices[0].message.content) +``` ## Java SDK diff --git a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md index 34cfa831..3f564bfd 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/.acli/system-prompt.md @@ -19,31 +19,39 @@ Your capabilities: - Execute commands to test SDK behavior - Fetch documentation and examples from URLs -Your expertise covers the domains below. Each has a quick-reference skill under `.acli/skills/` with model lists, SDK/CLI signatures, input/output structures, and error codes — consult the matching skill to answer API questions instead of reading SDK/CLI source code: -- Text generation (Generation, OpenAI-compatible interface) — skill `text-generation` -- Multimodal (MultiModalConversation, ImageSynthesis, VideoSynthesis) — skill `multimodal` -- Speech (SpeechSynthesizer, Transcription) — skill `speech` -- Retrieval (TextEmbedding/BatchTextEmbedding/MultiModalEmbedding, TextReRank, RAG) — skill `retrieval` -- Fine-tuning & deployment (SFT, CPT, DPO; dedicated inference endpoints; AgenticRL) — skill `fine-tuning` -- Agents (Managed Agents API / agentstudio, Application, Assistants, plugins & MCP) — skill `agent` -- CLI commands and options — skill `cli` +The Python knowledge baseline is **2.0.3 for new code**, with 1.x compatibility guidance for existing applications. Each domain has an on-demand skill under `.acli/skills/`; consult the matching skill for API questions instead of scanning SDK source without a specific question: +- Text generation and decision (`client.chat.completions`, `client.decision`, `dashscope.chat`, 1.x Generation and OpenAI-compatible mode) — skill `text-generation` +- Multimodal, images, and videos (`client.multimodal`, `client.images`, `client.videos`) — skill `multimodal` +- Speech and TingWu (`client.audio`, `client.tingwu`, plus 1.x speech APIs) — skill `speech` +- Retrieval (`client.embeddings`, `client.multimodal.embeddings`, `client.rerank`, `client.tokenization`, RAG) — skill `retrieval` +- Files, fine-tuning, deployment, and Agentic RL (`client.files`, `client.fine_tunes`, `client.deployments`, `client.rl`) — skill `fine-tuning` +- Apps and agents (`client.apps`, `client.agentstudio`, 1.x Application and Assistants) — skill `agent` +- CLI commands and options (the `dashscope` CLI remains a 1.x interface) — skill `cli` ## Response Strategy Match your approach to the question type: - **Issue / bug analysis**: Analyze directly from the user's description first. Only inspect SDK source when you need to verify a specific behavior. Go straight to the root cause. -- **API questions** (signature, parameters, usage): Load the matching skill under `.acli/skills/` (covers Python and Java interfaces). If the user mentions a specific SDK version, verify the signatures against the installed package. -- **Example generation**: Cross-check against the installed version, then write runnable code. -- **Questions about you** (what model you are, what you can do, what a skill or slash command means): Answer from this prompt, with **zero tool calls**. Your model name is on the `Current model:` line, your domains and skill list are above, and your capabilities are in this section. Nothing about yourself is discoverable by running a command — probing the environment to answer "what can you do" only makes the user wait for an answer you already had. +- **API questions** (signature, parameters, usage): Load the matching skill. Identify the API generation from the user's call and inspect the exact public object when verification is needed. +- **Example generation**: Cross-check against the installed version, then write runnable code with the correct response and error contract. +- **Questions about you** (what model you are, what you can do, what a skill or slash command means): Answer from this prompt, with **zero tool calls**. Your model name is on the `Current model:` line, your domains and skill list are above, and your capabilities are in this section. Nothing about yourself is discoverable by running a command. + +### Choose 1.x vs 2.0 from the user's context + +- Resource-style `dashscope.Client` / `AsyncClient` calls are 2.0. `Generation.call`, `MultiModalConversation.call`, and similar module/class APIs are 1.x. +- For **new Python code**, recommend 2.0 resource APIs and `dashscope.errors`. Mark features unavailable in 1.x as 2.0-only. +- For **fixes to existing 1.x code**, preserve its public imports, `.call()` style, response fields, and exception behavior unless the user explicitly asks to migrate. Do not rewrite working code merely because the installed package is 2.0. +- For an explicit migration, explain changes to client lifecycle, flattened responses, streaming deltas, long-task `create/fetch/wait`, and error handling. Never combine 1.x `.output` handling with a 2.0 typed response. +- OpenAI-compatible code using the official `openai` package and Java code retain their own API and error conventions. Java follows its independent version line. ### Version Check -If the user asks about a different SDK version than the one described in the skills, or you detect the installed version differs from the knowledge base: +The knowledge base describes Python 2.0.3 and legacy 1.x behavior. If the user asks about another SDK version, or the installed version differs: 1. Warn the user: "⚠️ Detected SDK version differs from the knowledge base (user: X, knowledge base: Y)" 2. Ask if they want to update the knowledge base now -3. If yes, verify against the installed package (`inspect.signature` / `help()`) and update the affected skill files under `.acli/skills/` directly +3. If yes, verify against the installed package (`inspect.signature` / `help()`) and update only the affected skill files under `.acli/skills/` ### Anti-patterns — DO NOT @@ -51,53 +59,54 @@ If the user asks about a different SDK version than the one described in the ski - Read SDK source code when the issue is already clear from the description - Chain multiple tool calls when one targeted call suffices - Repeat what the user already said back to them +- Assume every SDK method is named `.call()` or every response has `.output` +- Refer to `dashscope.v2`; that namespace does not exist in 2.0.3 ### Python SDK inspection (local, zero-cost) When verification is needed, use `run_command` to inspect the installed `dashscope` package: -- **Class signature**: `python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'` -- **Module help**: `python -c 'import dashscope; help(dashscope.Generation)'` -- **List API**: `python -c 'import dashscope; print(dir(dashscope))'` - **Package version**: `python -c 'import dashscope; print(dashscope.__version__)'` -- **Find source**: `python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'` -- Then use `read_file` on the specific source file if needed. +- **1.x method signature**: `python -c 'import inspect, dashscope; print(inspect.signature(dashscope.Generation.call))'` +- **2.0 bound resource signature**: instantiate `dashscope.Client(api_key="sk-test")`, then run `inspect.signature` on the actual method, such as `client.chat.completions.create`; close the client afterwards. Construction and inspection do not call the API. +- **Object or response-model help**: use `help()` on the resolved resource, method, or Pydantic model rather than assuming a fixed class layout. +- **Find source**: `python -c 'import dashscope; import os; print(os.path.dirname(dashscope.__file__))'`, then use `read_file` on the one relevant file if needed. ### Cookbook examples (examples/ directory) The Python SDK repo ships runnable cookbook scripts under `examples/`, organized by API generation. When the user asks you to implement a task, start from the matching script instead of writing from scratch: -- `examples/v1/` — 1.x SDK (`dashscope.Generation`, `dashscope.MultiModalConversation`, ...): text generation (`generation/`, `aio_generation/`), multimodal chat (`multimodal_conversation/`, `aio_multimodal_conversation/`), image/video synthesis, application / agentstudio / assistants, speech (`qwen_tts.py`, `qwen_asr.py`), tingwu, rerank, embeddings. -- `examples/v2/` — 2.0 SDK (resource-style `dashscope.Client()`): `chat.py`, `multimodal_chat.py`, `embeddings.py`, `images.py` / `videos.py` (one-shot generate + manual jobs flow), `rerank.py`, `models.py`, `files.py`, `apps.py`, `tokenization.py`, `audio_tts.py` / `audio_asr.py`, `tingwu.py`, `fine_tunes.py`. -- Java cookbook samples are not in this repo; they live in the Java SDK repo (https://github.com/dashscope/dashscope-sdk-java) and are referenced by class name in the skills. - -### Python repo layout: v1.x vs 2.0 (V2) +- `examples/v1/` — 1.x SDK (`dashscope.Generation`, `dashscope.MultiModalConversation`, ...): text generation, multimodal chat, image/video synthesis, application / agentstudio / assistants, speech, tingwu, rerank, and embeddings. +- `examples/v2/` — 2.0 resource-style `dashscope.Client()` scripts: chat, multimodal chat, embeddings, images/videos and jobs, rerank, models, files, apps, tokenization, audio, tingwu, and fine-tunes. +- Java cookbook samples live in the Java SDK repo (https://github.com/dashscope/dashscope-sdk-java) and are referenced by class name in the skills. -In the Python SDK repo the 2.0 API is isolated by directory. Only these paths are V2: +### Python repo layout: 1.x vs 2.0.3 -- `dashscope/` — the 2.0 client and resources at the package root -- `tests/` — current tests; transitional suites live in `tests/legacy_1x/` -- `examples/v2/` — V2 cookbook +- The 2.0 client and resources are at the package root: `dashscope/client.py`, `dashscope/resources/`, `dashscope/protocols/`, `dashscope/types/`, and `dashscope/errors.py`. +- Legacy implementations live under `dashscope/legacy_1x/`; historical public root exports and deep imports remain available through compatibility aliases. Prefer the user's existing public 1.x import when repairing old code, not a newly rewritten physical path. +- AgentStudio and FineTune implementations are grouped under `dashscope/applications/`, while their established public import paths remain available. +- Current tests live under `tests/`, with legacy suites under `tests/legacy_1x/`. Cookbook generations remain separated under `examples/v1/` and `examples/v2/`. -Everything else (`dashscope/` top-level modules, the rest of `tests/`, `examples/v1/`) is v1.x code. Match the tree to the user's API generation before reading source or quoting examples: resource-style `dashscope.Client` calls → V2 paths; module-level `dashscope.Generation.call(...)` style → v1.x paths. +Match the user's API generation before reading source or quoting examples; root-level 1.x exports and the root-level 2.0 Client coexist. ### Workflow 1. Identify the question type (question about you / issue analysis / API question / example). 2. If it is about you — answer straight from this prompt and stop. No tool call. -3. If issue analysis — answer directly, only verify if something is unclear. -4. If API question — run one targeted `inspect.signature` or `help()` call. -5. Compose a concise, actionable answer. +3. Identify whether the user is maintaining 1.x, writing new 2.0 code, or explicitly migrating. +4. Load the matching skill and run one targeted `inspect.signature` or `help()` call if verification is necessary. +5. Compose a concise, actionable answer with the matching response and error contract. ### SDK Reference -The skills under `.acli/skills/` are the knowledge base: model lists, Python and Java SDK interfaces, CLI commands, input/output structures, and error codes. Load the matching skill via `use_skill` to answer API questions. If a skill lacks a specific detail, fall back to `inspect.signature` or `help()` on the installed package. +The skills under `.acli/skills/` are the searchable knowledge base: model lists, Python and Java interfaces, CLI commands, input/output structures, and error codes. Load the matching skill via `use_skill`. If a skill lacks a specific detail, fall back to `inspect.signature` or `help()` on the installed package; do not duplicate large version-mapping tables into the permanent prompt. Rules: - Provide runnable code examples when relevant -- Reference specific SDK classes and methods -- Explain error codes with actionable fixes -- Use the latest SDK patterns (messages format, not legacy prompt) +- Reference specific SDK classes, resources, and methods +- Explain error codes with actionable fixes and preserve request IDs +- Use the latest 2.0 patterns for new code (messages format, not legacy prompt) +- Preserve the user's established 1.x style for legacy fixes - Answer in the same language as the user's question - When diagnosing errors, use read_file if a path is given - When demonstrating, use run_command to show actual output diff --git a/dashscope/acli/examples/dashscope-sdk-expert/README.md b/dashscope/acli/examples/dashscope-sdk-expert/README.md index c6040deb..114e58f7 100644 --- a/dashscope/acli/examples/dashscope-sdk-expert/README.md +++ b/dashscope/acli/examples/dashscope-sdk-expert/README.md @@ -6,6 +6,8 @@ This example shows how to build a scenario-specific AI expert agent using **Agen **Core idea: configuration-driven, zero Python glue.** The agent's identity, capabilities, skills, and knowledge index are all defined by files under `.acli/`; download the example and run `acli` directly to start. +**SDK baseline:** new Python examples use DashScope **2.0.3** (`dashscope.Client` / `AsyncClient`) and `dashscope.errors`. The skills also retain 1.x guidance for maintaining existing `Generation.call`-style code; installing 2.0 does not require rewriting working 1.x applications. + ## Directory Structure ``` @@ -16,12 +18,12 @@ dashscope-sdk-expert/ ├── hooks.toml # Event hooks ├── system-prompt.md # System prompt (agent persona and behavior rules) └── skills/ # Skill templates (loaded by the model on demand via use_skill) - ├── text-generation.md # Text generation (Generation / OpenAI-compatible, Python+Java) - ├── multimodal.md # Multimodal (MultiModalConversation/ImageSynthesis/VideoSynthesis) - ├── speech.md # Speech (SpeechSynthesizer/Transcription) - ├── retrieval.md # Retrieval (Embedding/TextReRank/RAG) - ├── fine-tuning.md # Fine-tuning & deployment (SFT/CPT/DPO/Deployments) - ├── agent.md # Agent (Application/Assistants/plugins & MCP) + ├── text-generation.md # Chat, decision & 1.x Generation (Python+Java) + ├── multimodal.md # Multimodal chat, images & videos + ├── speech.md # 2.0 audio, TingWu & 1.x speech compatibility + ├── retrieval.md # Embeddings, rerank & tokenization + ├── fine-tuning.md # Files, fine-tunes, deployments & Agentic RL + ├── agent.md # Apps, AgentStudio & 1.x Assistants compatibility ├── cli.md # dashscope CLI command reference ├── sdk-example.md # Generate SDK code examples ├── api-doc.md # View API parameter docs @@ -43,9 +45,11 @@ acli example download dashscope-sdk-expert # Start — no cd, no Python launcher script needed acli acli --tui -acli -c "How do I use Generation.call?" +acli -c "Show a DashScope 2.0 streaming chat example with error handling" ``` +For legacy maintenance, ask for example: `acli -c "Fix this Generation.call code without migrating it to 2.0"`. + ## The Configuration-Driven Approach ### 1. system-prompt.md — Agent Persona @@ -55,13 +59,13 @@ Defines the agent's identity, knowledge scope, and behavior rules. This is the c ```markdown You are DashScope SDK Expert, an intelligent assistant for the DashScope Python SDK... -## Grounded Knowledge First -Before answering, ALWAYS verify against the actual installed SDK... +### Choose 1.x vs 2.0 from the user's context +For new Python code, recommend 2.0 resource APIs and `dashscope.errors`... ``` ### 2. skills/ — Domain Knowledge Base (Loaded on Demand) -The SDK/CLI's public interface knowledge lives directly in domain skills: one file per domain, with model lists, Python and Java SDK signatures, input/output structures, and error codes. When answering API questions, the model loads the matching skill via `use_skill` on demand — **nothing stays permanently in the system prompt** — cutting first-turn input tokens by about 16k characters; details not covered by a skill fall back to `inspect.signature` / `help()` against the installed package. +The SDK/CLI's public interface knowledge lives directly in domain skills: one file per domain, with current 2.0.3 resource signatures, 1.x compatibility notes, Java references, input/output structures, and error handling. When answering API questions, the model loads the matching skill via `use_skill` on demand — **nothing stays permanently in the system prompt** — cutting first-turn input tokens by about 16k characters; details not covered by a skill fall back to `inspect.signature` / `help()` against the installed package. ### 3. skills/ — Task Templates From 49b34b5a2ca4427f79c711103b54aa19ebb37d5c Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 15:38:25 +0800 Subject: [PATCH 19/41] ci: limit unit tests to Python 3.11-3.13 --- .github/workflows/unit_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 63823931..56b691d9 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.12', '3.13', '3.14' ] + python-version: [ '3.11', '3.12', '3.13' ] steps: - name: Checkout repository @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.12', '3.13', '3.14' ] + python-version: [ '3.11', '3.12', '3.13' ] steps: - name: Checkout repository From 38f221a7fea4655259a7e09e2028b6b296de4ce6 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 27 Oct 2026 16:33:55 +0800 Subject: [PATCH 20/41] refactor(errors): unify error handling base classes and adjust Agentic RL exception hierarchy - Removed save_api_key function definition; now imported from dashscope._config - Made content parameter in Message constructor Optional to support None values - RuntimeErrorWithCode and ValueErrorWithCode now inherit from both AgenticRLError and their corresponding built-in exception types - DashScopeError now inherits from DashScopeException to unify the error handling hierarchy - Updated README documenting Agentic RL path restructuring and error handling changes - Added tests verifying inheritance relationships and exception catching behavior of unified error base classes --- README.md | 29 +++++++++++++++++-- dashscope/__init__.pyi | 8 ++--- .../finetune/reinforcement/common/errors.py | 4 +-- dashscope/errors.py | 10 +++++-- tests/test_agentstudio_shim.py | 6 +++- tests/test_finetune_rl_shim.py | 16 ++++++++-- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c7bb464b..612952ca 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The DashScope Python SDK is the official client for [Alibaba Cloud Model Studio (Bailian)](https://www.alibabacloud.com/help/en/model-studio/), covering text generation, multi-modal understanding, embeddings, reranking, image/video generation, speech synthesis & recognition, fine-tuning, agentic RL, applications and more. -**Version 2.0** introduces a resource-style client — `dashscope.Client()` — modeled on the ergonomics of the OpenAI SDK: one client holds a shared connection pool, every capability hangs off it as a typed resource (`client.chat`, `client.images`, `client.audio`, …), responses are typed objects instead of raw dicts, and failures raise exceptions by default. The 1.x module-level API (`dashscope.Generation`, …) is unchanged and still fully supported, so 2.0 is purely additive. +**Version 2.0** introduces a resource-style client — `dashscope.Client()` — modeled on the ergonomics of the OpenAI SDK: one client holds a shared connection pool, every capability hangs off it as a typed resource (`client.chat`, `client.images`, `client.audio`, …), responses are typed objects instead of raw dicts, and failures raise exceptions by default. The core 1.x module-level API (`dashscope.Generation`, …) is unchanged and still fully supported. A few surfaces were *restructured* rather than purely added — most notably the AgentStudio and FineTune/Agentic RL import paths, where `dashscope.finetune.agentic_rl` now resolves to the 2.0 class (`submit_job` returns `SubmitResult`; job lifecycle lives on `client.rl`) and the 1.x class-based surface (`cancel`/`list`/`get`/`delete`/`logs`, `submit_job` → `FineTune`) moved to `dashscope.legacy_1x.finetune.agentic_rl`. See [Package layout and compatibility](#package-layout-and-compatibility) for the full list. ## What's new in 2.0 @@ -456,8 +456,31 @@ implementations are grouped under `dashscope.applications`, while the public through lazy aliases. Existing root 1.x exports remain available. Historical deep modules such as `dashscope.aigc`, `dashscope.client.base_api`, and `dashscope.files` also remain available through lazy aliases to -`dashscope.legacy_1x`. The 1.x Agentic RL job-management surface is under -`dashscope.legacy_1x.finetune`. +`dashscope.legacy_1x`. + +> **Breaking (Agentic RL):** the public `dashscope.finetune.agentic_rl` path now +> resolves to the **2.0** `AgenticRL` — `submit_job` returns `SubmitResult` +> (not the 1.x `FineTune`) and the class no longer carries the job-management +> verbs. On 2.0 the job lifecycle lives on the client +> (`client.rl.jobs.create()`, `.wait()`, `.list()`, `.cancel()`, `.delete()`, +> `.logs()`). Code that relied on the 1.x class-based surface +> (`AgenticRL.cancel(...)`, `.get(...)`, `submit_job` → `FineTune`) should import +> `dashscope.legacy_1x.finetune.agentic_rl` instead, which keeps the five verbs +> (`cancel`/`list`/`get`/`delete`/`logs`) and the `FineTune` return. + +AgentStudio and Agentic RL exceptions are unified into the 2.0 error family: +`AgentStudioError` is now an alias of `dashscope.errors.DashScopeError`, and the +Agentic RL `*WithCode` errors subclass `AgenticRLError`. The former AgentStudio +`raw` response attribute is exposed as `body` (`err.raw` → `err.body`); the +`RuntimeErrorWithCode` / `ValueErrorWithCode` classes remain catchable as the +built-in `RuntimeError` / `ValueError` they wrap. Because both stacks now share +the `dashscope._shared._errors.DashScopeException` base, a single +`except DashScopeException` catches 1.x, 2.0 and AgentStudio errors alike. The +Agentic RL `*WithCode` errors also render differently in `str()` — the 1.x +`[1800] I/O error: …` prefix is replaced by the 2.0 ` … | +code=sdk.…` form (the numeric code is still available via `err.error_code` and +the timestamp via `err.timestamp`), so log parsers keyed on the old format need +updating. `dashscope.save_api_key` now comes from `dashscope._config`. It accepts an optional key-file path, expands `~`, creates missing parent directories, and diff --git a/dashscope/__init__.pyi b/dashscope/__init__.pyi index 3dd4d843..affbf1dd 100644 --- a/dashscope/__init__.pyi +++ b/dashscope/__init__.pyi @@ -36,9 +36,6 @@ base_compatible_api_url: str base_http_api_url: str base_websocket_api_url: str -def save_api_key( - api_key: str, api_key_file_path: Optional[str] = ... -) -> None: ... def close_shared_aio_session() -> None: ... def close_shared_sync_session() -> None: ... def set_region(region: str, workspace_id: Optional[str] = ...) -> None: ... @@ -73,7 +70,10 @@ class Message: role: str content: Union[str, List[Any]] def __init__( - self, role: str, content: Union[str, List[Any]] = None, **kwargs: Any + self, + role: str, + content: Optional[Union[str, List[Any]]] = None, + **kwargs: Any, ) -> None: ... class DashScopeAPIResponse: diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index b0c18fe7..0a3769ec 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -109,11 +109,11 @@ def __init__( self.operation = operation -class RuntimeErrorWithCode(AgenticRLError): +class RuntimeErrorWithCode(AgenticRLError, RuntimeError): _SDK_ERROR_NAME = "sdk.agentic_rl.RuntimeErrorWithCode" -class ValueErrorWithCode(AgenticRLError): +class ValueErrorWithCode(AgenticRLError, ValueError): _SDK_ERROR_NAME = "sdk.agentic_rl.ValueErrorWithCode" diff --git a/dashscope/errors.py b/dashscope/errors.py index 41ed4231..5dc8e875 100644 --- a/dashscope/errors.py +++ b/dashscope/errors.py @@ -39,6 +39,7 @@ from dashscope import _error_registry as _registry from dashscope._error_registry import PublicError, SdkErrorDef +from dashscope._shared._errors import DashScopeException class ErrorCode(str, Enum): @@ -59,8 +60,13 @@ class ErrorCode(str, Enum): ARREARAGE = "Arrearage" -class DashScopeError(Exception): - """Base class for all dashscope 2.0 SDK errors.""" +class DashScopeError(DashScopeException): + """Base class for all dashscope 2.0 SDK errors. + + Roots into the shared :class:`~dashscope._shared._errors.DashScopeException` + so a single ``except DashScopeException`` catches errors from both the 1.x + API classes and the 2.0 client (including the AgentStudio sub-SDK). + """ def __init__( self, diff --git a/tests/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py index 30ba3f06..5c5e4525 100644 --- a/tests/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -138,7 +138,11 @@ def test_deep_import_from_syntax_resolves(): def test_agentstudio_uses_the_unified_error_base(): assert impl_exc.AgentStudioError is sdk_errors.DashScopeError - assert not issubclass( + # The unified 2.0 base roots into the shared DashScopeException, so a + # single ``except DashScopeException`` catches AgentStudio errors too -- + # the invariant documented in ``dashscope/_shared/_errors.py`` and the + # behavior the 1.x ``AgentStudioError(DashScopeException)`` had. + assert issubclass( impl_exc.AgentStudioError, legacy_error.DashScopeException, ) diff --git a/tests/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py index 86ccdda0..e904f6b6 100644 --- a/tests/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -441,6 +441,16 @@ def test_condition_only_catalog_entries_do_not_need_new_classes(): assert "unique name" in error.solution -def test_legacy_builtin_exception_inheritance_is_not_retained(): - assert not issubclass(RuntimeErrorWithCode, RuntimeError) - assert not issubclass(ValueErrorWithCode, ValueError) +def test_legacy_builtin_exception_inheritance_is_retained(): + # 1.x contract: these stayed catchable as the builtins they wrap, so + # ``except RuntimeError`` / ``except ValueError`` keep working, while the + # v2 unification means they are still ``AgenticRLError`` too. + assert issubclass(RuntimeErrorWithCode, RuntimeError) + assert issubclass(ValueErrorWithCode, ValueError) + assert issubclass(RuntimeErrorWithCode, AgenticRLError) + assert issubclass(ValueErrorWithCode, AgenticRLError) + + with pytest.raises(ValueError): + raise ValueErrorWithCode("bad value") + with pytest.raises(RuntimeError): + raise RuntimeErrorWithCode("bad state") From 625069f2583cbe0e3c306c18e97d8de079367d7c Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 16:31:31 +0800 Subject: [PATCH 21/41] feat(acli): sync agenticCLI runtime and tool updates --- dashscope/acli/agent.py | 97 +++++++++++++--------- dashscope/acli/cli/dispatch.py | 5 +- dashscope/acli/config.py | 10 +-- dashscope/acli/providers/__init__.py | 21 +++++ dashscope/acli/runtime.py | 115 +++++++++++++++++++++++++++ dashscope/acli/sdk.py | 56 +++++++++---- dashscope/acli/session.py | 23 ++++-- dashscope/acli/session_events.py | 83 ++++++++++++++++--- dashscope/acli/tools/filesystem.py | 106 ++++++++++++++++-------- dashscope/acli/tools/platform.py | 9 +++ dashscope/acli/tools/registry.py | 19 ++++- dashscope/acli/tools/session.py | 91 ++++++++++----------- dashscope/acli/ui/embedded.py | 27 ++----- dashscope/acli/ui/tts.py | 4 +- dashscope/acli/ui/tui.py | 11 +++ dashscope/acli/ui/voice.py | 5 +- 16 files changed, 497 insertions(+), 185 deletions(-) create mode 100644 dashscope/acli/runtime.py diff --git a/dashscope/acli/agent.py b/dashscope/acli/agent.py index 1ca1e4fa..5bcd8171 100644 --- a/dashscope/acli/agent.py +++ b/dashscope/acli/agent.py @@ -514,18 +514,17 @@ def save_session(self) -> None: if not self.session_path: return try: + from dashscope.acli.utils.paths import atomic_write_text + self.session_path.parent.mkdir(parents=True, exist_ok=True) - tmp = self.session_path.with_suffix( - self.session_path.suffix + ".tmp", - ) - tmp.write_text( + atomic_write_text( + self.session_path, json.dumps( sanitize(self.messages), ensure_ascii=False, indent=2, ), ) - tmp.replace(self.session_path) except OSError: pass # Update session meta message count @@ -934,6 +933,7 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]: tools_schema = self._filter_tools_schema( registry.to_schema_list(user_input_text), ) + registry_version = registry.version # Compress with the full request overhead in view: the system prompt # and tools schema ride on every API call, so they count against the @@ -952,6 +952,30 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]: last_content = "" for _loop_i in range(self.max_turns): + # Connecting MCP services or enabling capabilities can mutate the + # global registry during a tool call. A schema frozen at turn start + # makes the newly registered tools unusable until the user sends a + # second message, so refresh both the schema and the prompt's + # ephemeral capability/MCP sections before the next model call. + if registry.version != registry_version: + system_prompt = self._system_prompt_for_turn(user_input_text) + if memory_context: + system_prompt += memory_context + tools_schema = self._filter_tools_schema( + registry.to_schema_list(user_input_text), + ) + overhead_tokens = estimate_tokens( + system_prompt, + ) + estimate_tokens( + json.dumps(tools_schema, ensure_ascii=False), + ) + await self._ensure_fits_context(overhead_tokens) + messages_with_system = [ + {"role": "system", "content": system_prompt}, + *self.messages, + ] + registry_version = registry.version + # Cheap in-place shrink of stale tool results (no LLM call): # keeps long tool loops from re-sending huge old outputs. if _loop_i > 0: @@ -1465,38 +1489,37 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]: ), ) - # Record the completed turn as events (session-as-event-log - # direction) BEFORE persisting history.json: the event stream is - # the source of truth, so it must never be older than the - # fallback store if we crash between the two writes. Best-effort: - # subagents/SDK callers may run without a session manager, and - # event recording must never break the loop. - try: - from dashscope.acli.session import get_session_manager - - turn_topic = ( - self.session_path.parent.name if self.session_path else None - ) - get_session_manager().record_turn_event( - user_text=text_of(user_input), - assistant_text=last_content, - tools_used=self._current_turn_tools, - outcome=( - _classify_outcome( - self._turn_tool_successes, - self._turn_tool_failures, - ) - if self._current_turn_tools - else "" - ), - topic=turn_topic, - ) - get_session_manager().record_messages_snapshot( - self.messages, - topic=turn_topic, - ) - except Exception: - pass + # Record the completed turn as events only for a persistent session. + # A session_path-less agent is intentionally ephemeral (SDK calls, + # disabled session persistence, isolated subagents); defaulting such a + # run to SessionManager.current_topic silently polluted the interactive + # "default" session. The event stream remains the source of truth for + # sessions that explicitly opted in. + if self.session_path is not None: + try: + from dashscope.acli.session import get_session_manager + + turn_topic = self.session_path.parent.name + get_session_manager().record_turn_event( + user_text=text_of(user_input), + assistant_text=last_content, + tools_used=self._current_turn_tools, + outcome=( + _classify_outcome( + self._turn_tool_successes, + self._turn_tool_failures, + ) + if self._current_turn_tools + else "" + ), + topic=turn_topic, + ) + get_session_manager().record_messages_snapshot( + self.messages, + topic=turn_topic, + ) + except Exception: + pass # Persist session before memory write so a memory exception can't # cost us the conversation. diff --git a/dashscope/acli/cli/dispatch.py b/dashscope/acli/cli/dispatch.py index cadc35cb..02b7f607 100644 --- a/dashscope/acli/cli/dispatch.py +++ b/dashscope/acli/cli/dispatch.py @@ -198,7 +198,10 @@ def _handle_slash_command( f" JSON mode: " f"{'[green]on' if agent.json_mode else '[dim]off'}[/]", ) - console.print(f" Loop mode: [cyan]{config.loop_mode}[/cyan]") + loop_label = config.loop_mode + if loop_label not in ("", "auto"): + loop_label += " [yellow](legacy; inactive)[/yellow]" + console.print(f" Loop mode: [cyan]{loop_label}[/cyan]") console.print(f" Max turns: [cyan]{config.max_turns}[/cyan]") # config.timeout is the LLM request timeout; run_command enforces its # own, so showing one "Timeout" line reported a limit nothing used. diff --git a/dashscope/acli/config.py b/dashscope/acli/config.py index 5c79cabf..9a0ae7cc 100644 --- a/dashscope/acli/config.py +++ b/dashscope/acli/config.py @@ -235,14 +235,12 @@ def from_dict(cls, d: dict) -> DelegationConfig: class Config: provider: str = "tongyi" model: str = "qwen3.8-max" - # Dual-LLM: when thinking_model is set, Plan/Thinking loop phases route to - # the thinking model; Execute uses the execution model (provider/model - # above). - # Empty = single-model mode (default, backward compatible). + # Legacy compatibility fields. The experimental DualLLMRouter and + # structured LoopEngine were removed; non-default values are preserved on + # disk but get_provider_chain emits an explicit warning instead of silently + # pretending they affect execution. thinking_provider: str = "" thinking_model: str = "" - # Loop mode: "auto" = LLM-driven flat loop (default); "structured" = - # 6-phase closed cycle (Target→Plan→Execute→Evaluate→Feedback→Thinking). loop_mode: str = "auto" tongyi_api_key: str = "" anthropic_api_key: str = "" diff --git a/dashscope/acli/providers/__init__.py b/dashscope/acli/providers/__init__.py index 7e7bb34b..8ade4b4c 100644 --- a/dashscope/acli/providers/__init__.py +++ b/dashscope/acli/providers/__init__.py @@ -28,6 +28,9 @@ # Env var hints for the /key command and profile validation. NOT used for # dispatch — protocol routing is driven by profile.protocol (filled from # the user's custom-extensions.toml, see the basic-chat example). +_WARNED_UNSUPPORTED_RUNTIME_OPTIONS: set[tuple[str, str, str]] = set() + + PROVIDER_API_KEY_ENVS = { "anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY", @@ -118,6 +121,24 @@ def get_provider( def get_provider_chain(config) -> ProviderChain: """Build a ProviderChain from acli Config (primary + fallbacks).""" + legacy = ( + getattr(config, "thinking_provider", ""), + getattr(config, "thinking_model", ""), + getattr(config, "loop_mode", "auto"), + ) + if (legacy[0] or legacy[1] or legacy[2] not in ("", "auto")) and ( + legacy not in _WARNED_UNSUPPORTED_RUNTIME_OPTIONS + ): + _WARNED_UNSUPPORTED_RUNTIME_OPTIONS.add(legacy) + from rich.console import Console + + Console().print( + "[yellow]⚠ thinking_provider/thinking_model/structured " + "loop_mode are legacy compatibility settings and are not active " + "in this build; execution uses the primary provider/model and " + "the standard Agent loop.[/yellow]", + ) + profiles = build_profiles_from_config(config) if not profiles: raise RuntimeError("No usable provider configuration") diff --git a/dashscope/acli/runtime.py b/dashscope/acli/runtime.py new file mode 100644 index 00000000..ccaa1622 --- /dev/null +++ b/dashscope/acli/runtime.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""Shared runtime wiring for programmatic and embedded Agent entrypoints. + +The interactive CLI owns presentation and lifecycle, but every entrypoint must +construct the same capability surface. Keeping this wiring here prevents SDK +and embedded one-shot modes from silently losing memory, session tools, vision, +skills, evolution, or delegation. +""" +from __future__ import annotations + +# pylint: disable=unused-import,protected-access + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dashscope.acli.agent import Agent + from dashscope.acli.config import Config + from dashscope.acli.hooks import HookBus + from dashscope.acli.platforms.base import MemoryProvider + + +def import_builtin_tools() -> None: + """Populate the global registry with all built-in static tools.""" + import dashscope.acli.tools.browser # noqa: F401 + import dashscope.acli.tools.camera # noqa: F401 + import dashscope.acli.tools.filesystem # noqa: F401 + import dashscope.acli.tools.shell # noqa: F401 + import dashscope.acli.tools.web_search # noqa: F401 + + +def prepare_extensions(config: Config) -> None: + """Load declarative providers, capabilities, shell tools, and skills.""" + from dashscope.acli.config import PROVIDER_MODELS + from dashscope.acli.extensions import apply_extensions + + del config # kept in the signature for a stable runtime-prep contract + apply_extensions(PROVIDER_MODELS) + + +def create_runtime_memory(config: Config) -> MemoryProvider | None: + """Create and publish the configured memory provider, if enabled.""" + if not config.memory_enabled: + return None + from dashscope.acli.cli.handlers_profile import set_memory_client + from dashscope.acli.platforms import get_memory_provider + + memory = get_memory_provider(config) + if memory is not None: + set_memory_client(memory) + return memory + + +def wire_agent_runtime( + config: Config, + agent: Agent, + hook_bus: HookBus | None = None, +) -> None: + """Attach every dynamic tool/capability family to an existing Agent.""" + import_builtin_tools() + + from dashscope.acli.agents.delegate import ( + set_config as set_delegate_config, + ) + from dashscope.acli.agents.delegate import ( + set_parent_agent as set_delegate_parent, + ) + from dashscope.acli.agents.subagent import ( + set_config as set_subagent_config, + ) + from dashscope.acli.agents.subagent import ( + set_parent_agent as set_subagent_parent, + ) + + set_subagent_parent(agent) + set_subagent_config(config) + set_delegate_parent(agent) + set_delegate_config(config) + + from dashscope.acli.cli.mcp import ( + _connect_mcp, + _disconnect_mcp, + _mcp_clients, + ) + from dashscope.acli.providers import get_provider + from dashscope.acli.skills import get_skill_manager, list_known_services + from dashscope.acli.tools.platform import register_platform_tools + + register_platform_tools(config, connect_mcp_fn=_connect_mcp) + + skill_manager = get_skill_manager() + skill_manager._registry_url = getattr(config, "skill_registry", "") + skill_manager._global = False + skill_manager.load(hook_bus=hook_bus or agent.hook_bus) + + from dashscope.acli.tools.session import register_session_tools + + register_session_tools( + config, + get_agent=lambda: agent, + get_provider_fn=get_provider, + connect_mcp_fn=_connect_mcp, + disconnect_mcp_fn=_disconnect_mcp, + list_mcp_services_fn=list_known_services, + get_mcp_clients_fn=lambda: _mcp_clients, + ) + + from dashscope.acli.tools.vision import register_vision_tools + + register_vision_tools(config, get_provider_fn=get_provider) + + from dashscope.acli.tools.evolution import register_evolution_tools + from dashscope.acli.tools.skills import register_skill_tools + + register_evolution_tools(get_agent=lambda: agent) + register_skill_tools(get_agent=lambda: agent) diff --git a/dashscope/acli/sdk.py b/dashscope/acli/sdk.py index 2af4d6bd..81cf46b3 100644 --- a/dashscope/acli/sdk.py +++ b/dashscope/acli/sdk.py @@ -19,7 +19,6 @@ from __future__ import annotations import asyncio -import uuid from pathlib import Path from typing import AsyncIterator, Optional @@ -30,10 +29,10 @@ def _ensure_tools_imported() -> None: - """Import tool modules so the registry is populated.""" - import dashscope.acli.tools.filesystem # noqa: F401 - import dashscope.acli.tools.shell # noqa: F401 - import dashscope.acli.tools.web_search # noqa: F401 + """Import every static built-in tool module.""" + from dashscope.acli.runtime import import_builtin_tools + + import_builtin_tools() def load_config( @@ -62,40 +61,64 @@ def create_agent( config: Optional[Config] = None, system_prompt: Optional[str] = None, session_id: Optional[str] = None, + *, + oneshot: bool = False, ) -> Agent: """Create an ``Agent`` instance ready for programmatic use. Args: config: acli configuration. If None, loads default config. system_prompt: Optional system prompt override. - session_id: Optional session identifier. If provided, the agent's - in-memory conversation is persisted under - ``.acli/session/{session_id}/history.json``. + session_id: Optional safe topic identifier. If provided, conversation + history is persisted under ``.acli/session//history.json``. + oneshot: Enable autonomous one-shot guards and acceptance behavior. """ _ensure_tools_imported() config = config or load_config() from dashscope.acli.cli import _compose_system_prompt, _load_system_prompt + from dashscope.acli.hooks import create_hook_bus + from dashscope.acli.runtime import ( + create_runtime_memory, + prepare_extensions, + wire_agent_runtime, + ) + from dashscope.acli.session import get_session_manager + from dashscope.acli.tools.platform import disabled_capabilities_hint + prepare_extensions(config) resolved_prompt = _compose_system_prompt( system_prompt or _load_system_prompt(), ) provider = get_provider_chain(config) - executor = Executor() + executor = Executor( + auto_approve=config.auto_approve, + confirm_mode=config.confirm_mode, + ) + hook_bus = create_hook_bus() + memory = create_runtime_memory(config) session_path: Optional[Path] = None if session_id: - session_path = Path(".acli") / "session" / session_id / "history.json" + session_path = get_session_manager().get_history_path(session_id) - return Agent( + agent = Agent( provider=provider, executor=executor, max_turns=config.max_turns, + memory=memory, + user_name=config.user_name, provider_name=config.provider, model_name=config.model, system_prompt=resolved_prompt, session_path=session_path, + disabled_caps_provider=lambda: disabled_capabilities_hint(config), + directives_provider=lambda: config.user_directives, + hook_bus=hook_bus, + oneshot=oneshot, ) + wire_agent_runtime(config, agent, hook_bus) + return agent async def run_stream( @@ -125,13 +148,14 @@ async def run_once( This is the easiest entry point for scripts and CI: it streams the full agent turn, executes any tool calls, and returns the final assistant text. """ - parts: list[str] = [] - async for chunk in run_stream( - prompt, + agent = create_agent( config=config, system_prompt=system_prompt, - session_id=session_id or str(uuid.uuid4())[:8], - ): + session_id=session_id, + oneshot=True, + ) + parts: list[str] = [] + async for chunk in agent.run_stream(prompt): parts.append(chunk) return "".join(parts) diff --git a/dashscope/acli/session.py b/dashscope/acli/session.py index 2ec8a9a3..e982880b 100644 --- a/dashscope/acli/session.py +++ b/dashscope/acli/session.py @@ -179,14 +179,18 @@ def append_scene(self, text: str, topic: str | None = None) -> bool: return self.set_scene(merged, topic) def event_log(self, topic: str | None = None) -> SessionEventLog: - """Return the append-only event log for a topic (default: current). + """Return the append-only event log for a safe topic. - The sidecar is advisory: callers may append lifecycle/turn events - without affecting the history.json read/write path. + Topic validation belongs at this public boundary. Several best-effort + callers accept a topic argument directly; allowing ``../`` here would + write an event stream outside ``session/`` even though create/switch + correctly reject the same name. """ - return SessionEventLog( - self._events_file(topic or self.current_topic), - ) + topic = topic or self.current_topic + topic_dir = self._safe_topic_dir(topic) + if topic_dir is None: + raise ValueError(f"unsafe session topic: {topic!r}") + return SessionEventLog(topic_dir / EVENTS_FILENAME) def record_turn_event( self, @@ -450,9 +454,12 @@ def delete_topic(self, topic: str) -> bool: return False def get_history_path(self, topic: str | None = None) -> Path: - """Get the history.json path for a topic (default: current).""" + """Get the history path for a safe topic (default: current).""" topic = topic or self.current_topic - return self._history_file(topic) + topic_dir = self._safe_topic_dir(topic) + if topic_dir is None: + raise ValueError(f"unsafe session topic: {topic!r}") + return topic_dir / "history.json" def get_input_history_path(self, topic: str | None = None) -> Path: """Get the input-history path for a topic (default: current).""" diff --git a/dashscope/acli/session_events.py b/dashscope/acli/session_events.py index 062f7d6c..baab55dc 100644 --- a/dashscope/acli/session_events.py +++ b/dashscope/acli/session_events.py @@ -24,14 +24,55 @@ import json import os +import threading +from contextlib import contextmanager from datetime import datetime from pathlib import Path from typing import Any +from dashscope.acli.utils.sanitizer import sanitize + # Bump when the on-disk event schema changes in a breaking way. SCHEMA_VERSION = 1 EVENTS_FILENAME = "events.jsonl" +_EVENT_FILE_LOCK = threading.RLock() + + +@contextmanager +def _exclusive_lock(path: Path): + """Cross-process lock on a stable sidecar inode. + + Locking the event file itself is insufficient because snapshot compaction + replaces that inode. A separate ``.lock`` file serializes append and + compaction across processes; the RLock covers platforms without flock and + nested calls in the current process. + """ + lock_path = path.with_name(path.name + ".lock") + lock_path.parent.mkdir(parents=True, exist_ok=True) + with _EVENT_FILE_LOCK, open(lock_path, "a+b") as lock_file: + if os.name == "nt": # pragma: no cover - exercised on Windows CI + import msvcrt + + lock_file.seek(0, os.SEEK_END) + if lock_file.tell() == 0: + lock_file.write(b"0") + lock_file.flush() + lock_file.seek(0) + msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1) + try: + yield + finally: + lock_file.seek(0) + msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1) + else: + import fcntl + + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) def latest_snapshot_messages( @@ -80,15 +121,21 @@ def append(self, event_type: str, data: dict | None = None) -> None: def _append(self, event_type: str, data: dict) -> None: self._file.parent.mkdir(parents=True, exist_ok=True) - entry = { - "v": SCHEMA_VERSION, - "seq": self._next_seq(), - "ts": datetime.now().isoformat(), - "type": event_type, - "data": data, - } - with open(self._file, "a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") + with _exclusive_lock(self._file): + entry = { + "v": SCHEMA_VERSION, + "seq": self._next_seq(), + "ts": datetime.now().isoformat(), + "type": event_type, + # Event snapshots are a persistence boundary just like + # history.json. Redact here so every producer (turn summaries, + # snapshots, lifecycle events, future extensions) gets the + # same protection; replay cannot resurrect plaintext secrets. + "data": sanitize(data), + } + with open(self._file, "a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + f.flush() def _next_seq(self) -> int: """1 + number of non-blank lines currently in the log.""" @@ -166,10 +213,22 @@ def compact_snapshots( the newest ``keep`` snapshots, renumbering ``seq`` to stay line-ordered. The rewrite goes through a tmp file and ``os.replace``, so a crash mid-compaction leaves the original - log intact. + log intact. Append and compaction share a stable sidecar lock. Best-effort: returns True only when the file was rewritten. """ + try: + with _exclusive_lock(self._file): + return self._compact_snapshots_locked(keep, snapshot_type) + except OSError: + return False + + def _compact_snapshots_locked( + self, + keep: int, + snapshot_type: str, + ) -> bool: + """Compaction implementation; caller holds ``_exclusive_lock``.""" try: entries = self.read_raw() except OSError: @@ -181,7 +240,9 @@ def compact_snapshots( return False drop = set(snapshot_idx[: len(snapshot_idx) - keep]) kept = [e for i, e in enumerate(entries) if i not in drop] - tmp = self._file.with_suffix(self._file.suffix + ".tmp") + tmp = self._file.with_name( + f"{self._file.name}.{os.getpid()}.tmp", + ) try: with open(tmp, "w", encoding="utf-8") as f: for seq, entry in enumerate(kept, start=1): diff --git a/dashscope/acli/tools/filesystem.py b/dashscope/acli/tools/filesystem.py index eaa5b282..7b7e58bc 100644 --- a/dashscope/acli/tools/filesystem.py +++ b/dashscope/acli/tools/filesystem.py @@ -49,6 +49,77 @@ } +def _read_text_window( + path: str, + offset: int | None, + limit: int | None, +) -> str: + """Read a numbered text window with bounded memory.""" + + # offset is 1-based so it matches the line numbers printed below; None + # and <=1 both mean "from the top". Keep memory bounded while scanning: + # the previous f.readlines() implementation loaded a multi-gigabyte file + # in full even though the tool only returns a 200-line / 100k-char window. + start = max((offset or 1) - 1, 0) + window = limit if limit and limit > 0 else _READ_DEFAULT_LINES + body_parts: list[str] = [] + body_chars = 0 + included = 0 + total = 0 + collecting = True + + with open(path, "r", encoding="utf-8", errors="replace") as f: + while True: + # Bound a single physical-line allocation as well. If the line is + # longer, consume the remainder in chunks but keep it one logical + # line for numbering/offset purposes. + line = f.readline(_READ_MAX_CHARS + 1) + if not line: + break + logical = line + while line and not line.endswith("\n"): + line = f.readline(_READ_MAX_CHARS + 1) + total += 1 + + if not collecting or total <= start or included >= window: + continue + + rendered = f"{total:>4}\t{logical.rstrip()}" + separator = 1 if body_parts else 0 + remaining = _READ_MAX_CHARS - body_chars - separator + if remaining <= 0: + collecting = False + continue + if len(rendered) > remaining: + # Preserve the old behavior for one extremely long first line: + # return a bounded prefix. For later lines, stop at the last + # complete line so a continuation offset remains truthful. + if not body_parts: + body_parts.append(rendered[:remaining]) + body_chars += min(len(rendered), remaining) + included += 1 + collecting = False + continue + body_parts.append(rendered) + body_chars += separator + len(rendered) + included += 1 + + if start >= total: + return ( + f"[offset {start + 1} is past the end of {path} ({total} lines)]" + ) + + body = "\n".join(body_parts) + end = start + included + omitted = total - end + if omitted > 0: + body += ( + f"\n[omitted {omitted} of {total} lines — " + f"read_file path={path} offset={end + 1} to continue]" + ) + return body + + # --------------------------------------------------------------------------- # Tools # --------------------------------------------------------------------------- @@ -91,40 +162,7 @@ def read_file( "file/identify instead.]" ) - with open(path, "r", encoding="utf-8", errors="replace") as f: - lines = f.readlines() - - total = len(lines) - # offset is 1-based so it matches the line numbers printed below; None - # and <=1 both mean "from the top". - start = max((offset or 1) - 1, 0) - if start >= total: - return ( - f"[offset {start + 1} is past the end of {path} ({total} lines)]" - ) - - window = limit if limit and limit > 0 else _READ_DEFAULT_LINES - end = min(start + window, total) - body = "\n".join( - f"{i:>4}\t{line.rstrip()}" - for i, line in enumerate(lines[start:end], start=start + 1) - ) - - if len(body) > _READ_MAX_CHARS: - cut = body[:_READ_MAX_CHARS] - # Back off to the last complete line so the window never ends on a - # half-line whose number would mislead a follow-up offset. - last_newline = cut.rfind("\n") - body = cut[:last_newline] if last_newline > 0 else cut - end = start + (body.count("\n") + 1 if body else 0) - - omitted = total - end - if omitted > 0: - body += ( - f"\n[omitted {omitted} of {total} lines — " - f"read_file path={path} offset={end + 1} to continue]" - ) - return body + return _read_text_window(path, offset, limit) @tool( diff --git a/dashscope/acli/tools/platform.py b/dashscope/acli/tools/platform.py index 2269065b..f544ad5c 100644 --- a/dashscope/acli/tools/platform.py +++ b/dashscope/acli/tools/platform.py @@ -103,6 +103,15 @@ def register_platform_tools( config: Config, connect_mcp_fn: Callable | None = None, ): + if config.privacy_mode: + # Registration is process-global. A previous non-private Agent may have + # left cloud/MCP tools in the registry, so merely skipping new + # registration is insufficient for SDKs that create multiple Agents. + unregister_cloud_capability_tools() + for name in [t.name for t in registry.list_tools()]: + if name.startswith(("mcp_", "bailian_")): + registry.unregister(name) + caps = config.enabled_capabilities all_enabled = caps is None for cap_key in all_capability_keys(): diff --git a/dashscope/acli/tools/registry.py b/dashscope/acli/tools/registry.py index 9b8de94b..8c8ab7b8 100644 --- a/dashscope/acli/tools/registry.py +++ b/dashscope/acli/tools/registry.py @@ -126,6 +126,9 @@ def _build_parameters_schema(func: Callable) -> dict: "mcp_connect", "mcp_disconnect", "use_skill", + "create_plan", + "complete_step", + "get_plan_status", }, ) @@ -341,21 +344,32 @@ def ever_visible_tool_names() -> frozenset[str]: class ToolRegistry: def __init__(self): self._tools: dict[str, ToolDefinition] = {} + # Monotonic mutation counter. Agent loops use it to detect tools that + # were connected/enabled mid-turn and rebuild the provider schema. + self._version = 0 + + @property + def version(self) -> int: + """Registry mutation version for cheap schema invalidation.""" + return self._version def register(self, tool_def: ToolDefinition): """Register a tool. When used as a decorator with a ToolDefinition, returns a decorator.""" if isinstance(tool_def, ToolDefinition): self._tools[tool_def.name] = tool_def + self._version += 1 def decorator(func: Callable) -> Callable: self._tools[tool_def.name].func = func + self._version += 1 func._tool_definition = tool_def return func return decorator # Called with a callable directly (legacy) self._tools[tool_def.name] = tool_def + self._version += 1 return None def register_mcp_tool( @@ -373,9 +387,12 @@ def register_mcp_tool( parameters=parameters, ) self._tools[name] = tool_def + self._version += 1 def unregister(self, name: str): - self._tools.pop(name, None) + if name in self._tools: + self._tools.pop(name) + self._version += 1 def get(self, name: str) -> ToolDefinition | None: return self._tools.get(name) diff --git a/dashscope/acli/tools/session.py b/dashscope/acli/tools/session.py index 29202e42..601e0bc1 100644 --- a/dashscope/acli/tools/session.py +++ b/dashscope/acli/tools/session.py @@ -284,57 +284,58 @@ async def mcp_disconnect(service: str) -> str: config.save_workspace() return f"Disconnected {service}" - registry.register_mcp_tool( - name="mcp_list_services", - description=( - "List available MCP cloud services " - "(time, code execution, doc parsing, etc.)" - ), - parameters={"type": "object", "properties": {}}, - call_fn=mcp_list_services, - ) - registry.register( - ToolDefinition( - name="mcp_connect", + if not config.privacy_mode: + registry.register_mcp_tool( + name="mcp_list_services", description=( - "Connect an MCP cloud service " - "(its tools become available once connected)" + "List available MCP cloud services " + "(time, code execution, doc parsing, etc.)" ), - permission=PermissionLevel.CONFIRM, - func=mcp_connect, - parameters={ - "type": "object", - "properties": { - "service": { - "type": "string", - "description": ( - "Service name, e.g. time, " - "code-interpreter, doc-analysis" - ), + parameters={"type": "object", "properties": {}}, + call_fn=mcp_list_services, + ) + registry.register( + ToolDefinition( + name="mcp_connect", + description=( + "Connect an MCP cloud service " + "(its tools become available once connected)" + ), + permission=PermissionLevel.CONFIRM, + func=mcp_connect, + parameters={ + "type": "object", + "properties": { + "service": { + "type": "string", + "description": ( + "Service name, e.g. time, " + "code-interpreter, doc-analysis" + ), + }, }, + "required": ["service"], }, - "required": ["service"], - }, - ), - ) - registry.register( - ToolDefinition( - name="mcp_disconnect", - description="Disconnect an MCP cloud service", - permission=PermissionLevel.CONFIRM, - func=mcp_disconnect, - parameters={ - "type": "object", - "properties": { - "service": { - "type": "string", - "description": "Service name", + ), + ) + registry.register( + ToolDefinition( + name="mcp_disconnect", + description="Disconnect an MCP cloud service", + permission=PermissionLevel.CONFIRM, + func=mcp_disconnect, + parameters={ + "type": "object", + "properties": { + "service": { + "type": "string", + "description": "Service name", + }, }, + "required": ["service"], }, - "required": ["service"], - }, - ), - ) + ), + ) # ===== Plan Management ===== diff --git a/dashscope/acli/ui/embedded.py b/dashscope/acli/ui/embedded.py index ea14e0c6..378fa4ae 100644 --- a/dashscope/acli/ui/embedded.py +++ b/dashscope/acli/ui/embedded.py @@ -109,26 +109,13 @@ async def _run_oneshot_embedded( prompt: str, system_prompt: str | None, ): - """One-shot mode using acli's full agent stack.""" - import dashscope.acli.tools.filesystem # noqa: F401 - import dashscope.acli.tools.shell # noqa: F401 - from dashscope.acli.agent import Agent - from dashscope.acli.cli import _compose_system_prompt, _load_system_prompt - from dashscope.acli.executor import Executor - from dashscope.acli.providers import get_provider_chain - - resolved_prompt = _compose_system_prompt( - system_prompt or _load_system_prompt(), - ) - provider = get_provider_chain(config) - executor = Executor() - agent = Agent( - provider=provider, - executor=executor, - max_turns=config.max_turns, - provider_name=config.provider, - model_name=config.model, - system_prompt=resolved_prompt, + """One-shot mode using the same fully wired stack as the SDK.""" + from dashscope.acli.sdk import create_agent + + agent = create_agent( + config=config, + system_prompt=system_prompt, + oneshot=True, ) async for chunk in agent.run_stream(prompt): diff --git a/dashscope/acli/ui/tts.py b/dashscope/acli/ui/tts.py index d5eef739..a02e1f3d 100644 --- a/dashscope/acli/ui/tts.py +++ b/dashscope/acli/ui/tts.py @@ -3,7 +3,7 @@ Supports both streaming playback (low-latency, audio starts before synthesis finishes) and one-shot synthesis. Uses -``dashscope.legacy_1x.audio.tts_v2.SpeechSynthesizer``. +``dashscope.audio.tts_v2.SpeechSynthesizer``. """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements @@ -284,7 +284,7 @@ def _synthesize_and_play(self, text: str) -> Optional[str]: dashscope.api_key = self.api_key try: - from dashscope.legacy_1x.audio.tts_v2 import ( + from dashscope.audio.tts_v2 import ( AudioFormat, ResultCallback, SpeechSynthesizer, diff --git a/dashscope/acli/ui/tui.py b/dashscope/acli/ui/tui.py index aa72783a..0893a52f 100644 --- a/dashscope/acli/ui/tui.py +++ b/dashscope/acli/ui/tui.py @@ -252,6 +252,17 @@ def scroll_end(self, *args, **kwargs): self._follow_output = True return super().scroll_end(*args, **kwargs) + def selection_updated(self, selection: Selection | None) -> None: + """Invalidate Textual's line cache when selection styling changes. + + Textual 8 caches RichLog strips independently of the Screen selection + reactive. ``Widget.refresh()`` alone can leave the old unselected line + in ``StylesCache``, so the selection exists and copies correctly but + has no visible highlight. + """ + self._styles_cache.clear() + super().selection_updated(selection) + def render_line(self, y: int) -> Strip: scroll_x, scroll_y = self.scroll_offset line_y = scroll_y + y diff --git a/dashscope/acli/ui/voice.py b/dashscope/acli/ui/voice.py index 8ba069a7..35af0d03 100644 --- a/dashscope/acli/ui/voice.py +++ b/dashscope/acli/ui/voice.py @@ -58,10 +58,7 @@ class _RealtimeASR: def __init__(self, api_key: str, model: str): os.environ.setdefault("DASHSCOPE_API_KEY", api_key) - from dashscope.legacy_1x.audio.asr import ( - Recognition, - RecognitionCallback, - ) + from dashscope.audio.asr import Recognition, RecognitionCallback self._model = model self._Recognition = Recognition From b27c5f115101c60155f8353fb527e5cd10f56477 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:11:03 +0800 Subject: [PATCH 22/41] test(acli): update audio import contract --- tests/legacy_1x/unit/test_acli_sdk_contract.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/legacy_1x/unit/test_acli_sdk_contract.py b/tests/legacy_1x/unit/test_acli_sdk_contract.py index cfe8f2e4..e6bdc9ac 100644 --- a/tests/legacy_1x/unit/test_acli_sdk_contract.py +++ b/tests/legacy_1x/unit/test_acli_sdk_contract.py @@ -57,8 +57,8 @@ _ALLOWLIST = frozenset( { "dashscope", - "dashscope.legacy_1x.audio.asr", - "dashscope.legacy_1x.audio.tts_v2", + "dashscope.audio.asr", + "dashscope.audio.tts_v2", }, ) @@ -254,21 +254,21 @@ def test_every_name_acli_imports_from_the_sdk_resolves(): def test_tts_v2_exposes_the_names_acli_uses(): - module = importlib.import_module("dashscope.legacy_1x.audio.tts_v2") + module = importlib.import_module("dashscope.audio.tts_v2") for name in ("AudioFormat", "ResultCallback", "SpeechSynthesizer"): assert hasattr( module, name, - ), f"dashscope.legacy_1x.audio.tts_v2.{name} is gone" + ), f"dashscope.audio.tts_v2.{name} is gone" def test_asr_exposes_the_names_acli_uses(): - module = importlib.import_module("dashscope.legacy_1x.audio.asr") + module = importlib.import_module("dashscope.audio.asr") for name in ("Recognition", "RecognitionCallback"): assert hasattr( module, name, - ), f"dashscope.legacy_1x.audio.asr.{name} is gone" + ), f"dashscope.audio.asr.{name} is gone" # --------------------------------------------------------------------------- From 38b5859a90cb2b49cde8b4fa65e4a62c88167648 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 27 Oct 2026 17:57:08 +0800 Subject: [PATCH 23/41] fix(legacy): fix 1.x legacy alias module import issues - Added dashscope.applications.finetune submodule to legacy 1.x alias mapping - Ensured `from dashscope.finetune import ` resolves correctly to 1.x version modules - Added alias support for customize_types, deployments, and finetunes submodules - Fixed import errors caused by module object sharing --- README.md | 16 +++++++++++++ README_zh.md | 36 +++++++++++++++++++++++++++++ dashscope/legacy_1x/_shim.py | 13 +++++++++++ tests/test_legacy_import_aliases.py | 11 +++++++++ 4 files changed, 76 insertions(+) diff --git a/README.md b/README.md index 612952ca..1e159927 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,22 @@ code=sdk.…` form (the numeric code is still available via `err.error_code` and the timestamp via `err.timestamp`), so log parsers keyed on the old format need updating. +> **Breaking (AgentStudio errors):** `ConflictError` is now an alias of +> `APIStatusError` rather than a 409-specific subclass, so `except ConflictError` +> catches *every* status error (400/401/404/429/5xx), not just conflicts. Match on +> `err.status_code == 409` if you need the old behaviour. The Agentic RL +> `error_code` is now a read-only property — assigning `err.error_code = …` +> raises `AttributeError`. + +> **Breaking (`submit_job` / realtime WebSocket):** `submit_job` forwards extra +> keyword arguments straight to the transport, so the 1.x `request_timeout=` is +> renamed `timeout=`, and `workspace=` / `headers=` / `stream=` are no longer +> accepted (passing any of them raises `TypeError`). The realtime WebSocket state +> errors (`WsStateError`, `WsBinaryOwnerConflictError`) now derive from +> `DashScopeError` instead of `RuntimeError`, so a bare `except RuntimeError` no +> longer catches them — catch `errors.DashScopeError` (or the specific class) +> instead. + `dashscope.save_api_key` now comes from `dashscope._config`. It accepts an optional key-file path, expands `~`, creates missing parent directories, and writes UTF-8. Logs from the current client use the independent diff --git a/README_zh.md b/README_zh.md index a1529e69..fac0bb40 100644 --- a/README_zh.md +++ b/README_zh.md @@ -454,6 +454,42 @@ examples/ deep import 已移至 `dashscope.legacy_1x` 下。2.0 Agentic RL 工作流位于 `dashscope.finetune`,1.x 任务管理接口位于 `dashscope.legacy_1x.finetune`。 +> **破坏性变更(Agentic RL):** 公开路径 `dashscope.finetune.agentic_rl` +> 现在解析到 **2.0** 的 `AgenticRL` —— `submit_job` 返回 `SubmitResult` +> (不再是 1.x 的 `FineTune`),且该类不再带任务管理动词。2.0 的任务生命 +> 周期在客户端上(`client.rl.jobs.create()`、`.wait()`、`.list()`、 +> `.cancel()`、`.delete()`、`.logs()`)。依赖 1.x 类接口 +> (`AgenticRL.cancel(...)`、`.get(...)`、`submit_job` → `FineTune`)的代码 +> 应改为导入 `dashscope.legacy_1x.finetune.agentic_rl`,它保留五个动词 +> (`cancel`/`list`/`get`/`delete`/`logs`)与 `FineTune` 返回值。 + +AgentStudio 与 Agentic RL 的异常已统一到 2.0 错误体系:`AgentStudioError` +现在是 `dashscope.errors.DashScopeError` 的别名,Agentic RL 的 `*WithCode` +错误继承自 `AgenticRLError`。原 AgentStudio 的 `raw` 响应属性改以 `body` +暴露(`err.raw` → `err.body`);`RuntimeErrorWithCode` / `ValueErrorWithCode` +仍可作为其包装的内置 `RuntimeError` / `ValueError` 被捕获。由于两套栈现在 +共享 `dashscope._shared._errors.DashScopeException` 基类,一个 +`except DashScopeException` 即可同时捕获 1.x、2.0 与 AgentStudio 错误。 +Agentic RL 的 `*WithCode` 错误 `str()` 渲染也变了 —— 1.x 的 +`[1800] I/O error: …` 前缀被替换为 2.0 的 ` … | code=sdk.…` +形式(数值码仍可通过 `err.error_code` 获取、时间戳通过 `err.timestamp`), +依赖旧格式的日志解析需要更新。 + +> **破坏性变更(AgentStudio 错误):** `ConflictError` 现在是 +> `APIStatusError` 的别名,而不再是仅对应 409 的子类,因此 +> `except ConflictError` 会捕获**所有**状态错误(400/401/404/429/5xx), +> 而不仅是冲突。若需要旧行为,请改用 `err.status_code == 409` 判断。 +> Agentic RL 的 `error_code` 现在是只读 property —— 赋值 +> `err.error_code = …` 会抛 `AttributeError`。 + +> **破坏性变更(`submit_job` / realtime WebSocket):** `submit_job` 会把 +> 额外关键字参数直接转发给传输层,因此 1.x 的 `request_timeout=` 改名为 +> `timeout=`,且不再接受 `workspace=` / `headers=` / `stream=`(传入任一 +> 都会抛 `TypeError`)。realtime WebSocket 状态错误(`WsStateError`、 +> `WsBinaryOwnerConflictError`)现在继承自 `DashScopeError` 而非 +> `RuntimeError`,因此裸 `except RuntimeError` 不再能捕获它们 —— 请改捕获 +> `errors.DashScopeError`(或具体的错误类)。 + `dashscope.save_api_key` 现在来自 `dashscope._config`,仍接受可选的 key 文件路径,并会展开 `~`、创建缺失的父目录、按 UTF-8 写入。当前客户端使用 独立的 `dashscope._logging` logger 层级,不会重新配置共享的 `dashscope` diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index 6ea23dc9..52a7d9fe 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -43,6 +43,19 @@ "dashscope.legacy_1x.finetune.deployments" ), "dashscope.finetune.finetunes": "dashscope.legacy_1x.finetune.finetunes", + # ``dashscope.finetune`` aliases the shared ``dashscope.applications.finetune`` + # module object, so ``from dashscope.finetune import `` resolves the child + # against that canonical ``__name__`` rather than the alias prefix above. Map the + # canonical names too, otherwise those 1.x-only submodules raise ImportError. + "dashscope.applications.finetune.customize_types": ( + "dashscope.legacy_1x.finetune.customize_types" + ), + "dashscope.applications.finetune.deployments": ( + "dashscope.legacy_1x.finetune.deployments" + ), + "dashscope.applications.finetune.finetunes": ( + "dashscope.legacy_1x.finetune.finetunes" + ), } _FINDER_MARKER = "_dashscope_legacy_1x_alias_finder" diff --git a/tests/test_legacy_import_aliases.py b/tests/test_legacy_import_aliases.py index f51eda3a..e8a4e882 100644 --- a/tests/test_legacy_import_aliases.py +++ b/tests/test_legacy_import_aliases.py @@ -60,6 +60,17 @@ def test_all_legacy_module_aliases_in_clean_process(): f'dashscope.legacy_1x.finetune.{name}', ) assert old is canonical, name + +# ``from dashscope.finetune import `` resolves the child against the +# shared canonical ``__name__``; guard the 1.x-only submodules stay reachable. +from dashscope.finetune import customize_types, deployments, finetunes +for name, mod in ( + ('customize_types', customize_types), + ('deployments', deployments), + ('finetunes', finetunes), +): + legacy = importlib.import_module(f'dashscope.legacy_1x.finetune.{name}') + assert mod is legacy, name """, ) From 0cb3454c5d9f7038f3a5f139e86760a3edd52c79 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:55:11 +0800 Subject: [PATCH 24/41] refactor(compat): preserve 1.x application interfaces --- MANIFEST.in | 2 +- README.md | 57 ++-- README_zh.md | 21 +- dashscope/__init__.py | 2 +- dashscope/applications/agentstudio/client.py | 25 +- .../agentstudio/resources/session_events.py | 10 +- .../applications/agentstudio/streaming.py | 23 +- .../applications/agentstudio/transport.py | 46 ++- .../reinforcement/common/_legacy_errors.py | 313 ++++++++++++++++++ .../finetune/reinforcement/common/errors.py | 72 ++-- dashscope/client.py | 31 +- dashscope/legacy_1x/_shim.py | 29 +- dashscope/legacy_1x/agentstudio/__init__.py | 164 +++++++++ dashscope/legacy_1x/agentstudio/client.py | 105 ++++++ dashscope/legacy_1x/agentstudio/exceptions.py | 244 ++++++++++++++ dashscope/legacy_1x/agentstudio/streaming.py | 46 +++ dashscope/legacy_1x/agentstudio/transport.py | 105 ++++++ dashscope/legacy_1x/common/error.py | 63 +--- dashscope/legacy_1x/finetune/agentic_rl.py | 110 +++++- .../finetune/reinforcement/__init__.py | 51 +++ .../finetune/reinforcement/common/__init__.py | 22 ++ .../finetune/reinforcement/common/errors.py | 53 +++ .../{ => legacy_1x}/tokenizers/__init__.py | 0 .../{ => legacy_1x}/tokenizers/qwen.tiktoken | 0 .../tokenizers/qwen_tokenizer.py | 0 .../tokenizers/tokenization.py | 0 .../{ => legacy_1x}/tokenizers/tokenizer.py | 5 +- .../tokenizers/tokenizer_base.py | 0 dashscope/resources/reinforcement_learning.py | 11 +- .../legacy_1x/unit/test_import_deprecation.py | 11 + tests/legacy_1x/unit/test_layer_decoupling.py | 4 - tests/smoke/test_full_surface.py | 2 +- tests/test_agentstudio_errors.py | 10 +- tests/test_agentstudio_shim.py | 74 +++-- tests/test_apps.py | 47 +-- tests/test_finetune_rl_shim.py | 49 ++- tests/test_legacy_import_aliases.py | 17 +- tests/test_package_root.py | 8 +- tests/test_rl.py | 4 +- 39 files changed, 1570 insertions(+), 266 deletions(-) create mode 100644 dashscope/applications/finetune/reinforcement/common/_legacy_errors.py create mode 100644 dashscope/legacy_1x/agentstudio/__init__.py create mode 100644 dashscope/legacy_1x/agentstudio/client.py create mode 100644 dashscope/legacy_1x/agentstudio/exceptions.py create mode 100644 dashscope/legacy_1x/agentstudio/streaming.py create mode 100644 dashscope/legacy_1x/agentstudio/transport.py create mode 100644 dashscope/legacy_1x/finetune/reinforcement/__init__.py create mode 100644 dashscope/legacy_1x/finetune/reinforcement/common/__init__.py create mode 100644 dashscope/legacy_1x/finetune/reinforcement/common/errors.py rename dashscope/{ => legacy_1x}/tokenizers/__init__.py (100%) rename dashscope/{ => legacy_1x}/tokenizers/qwen.tiktoken (100%) rename dashscope/{ => legacy_1x}/tokenizers/qwen_tokenizer.py (100%) rename dashscope/{ => legacy_1x}/tokenizers/tokenization.py (100%) rename dashscope/{ => legacy_1x}/tokenizers/tokenizer.py (87%) rename dashscope/{ => legacy_1x}/tokenizers/tokenizer_base.py (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 8cedf45a..6712512a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include dashscope/tokenizers/qwen.tiktoken +include dashscope/legacy_1x/tokenizers/qwen.tiktoken include dashscope/py.typed include dashscope/__init__.pyi include NOTICE diff --git a/README.md b/README.md index 1e159927..3476aa64 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The DashScope Python SDK is the official client for [Alibaba Cloud Model Studio (Bailian)](https://www.alibabacloud.com/help/en/model-studio/), covering text generation, multi-modal understanding, embeddings, reranking, image/video generation, speech synthesis & recognition, fine-tuning, agentic RL, applications and more. -**Version 2.0** introduces a resource-style client — `dashscope.Client()` — modeled on the ergonomics of the OpenAI SDK: one client holds a shared connection pool, every capability hangs off it as a typed resource (`client.chat`, `client.images`, `client.audio`, …), responses are typed objects instead of raw dicts, and failures raise exceptions by default. The core 1.x module-level API (`dashscope.Generation`, …) is unchanged and still fully supported. A few surfaces were *restructured* rather than purely added — most notably the AgentStudio and FineTune/Agentic RL import paths, where `dashscope.finetune.agentic_rl` now resolves to the 2.0 class (`submit_job` returns `SubmitResult`; job lifecycle lives on `client.rl`) and the 1.x class-based surface (`cancel`/`list`/`get`/`delete`/`logs`, `submit_job` → `FineTune`) moved to `dashscope.legacy_1x.finetune.agentic_rl`. See [Package layout and compatibility](#package-layout-and-compatibility) for the full list. +**Version 2.0** introduces a resource-style client — `dashscope.Client()` — modeled on the ergonomics of the OpenAI SDK: one client holds a shared connection pool, every capability hangs off it as a typed resource (`client.chat`, `client.images`, `client.audio`, …), responses are typed objects instead of raw dicts, and failures raise exceptions by default. The complete 1.x public surface remains available at its historical import paths and is implemented by facades under `dashscope.legacy_1x`; AgentStudio and Agentic RL share their large business cores with 2.0 without sharing public return or error contracts. ## What's new in 2.0 @@ -450,37 +450,30 @@ The 1.x module-level API is unchanged and still supported — you can adopt 2.0 ### Package layout and compatibility The 2.0 implementation is directly under `dashscope`; the former -`dashscope.v2` namespace has been removed. AgentStudio and FineTune/Agentic RL -implementations are grouped under `dashscope.applications`, while the public -`dashscope.agentstudio` and `dashscope.finetune` import paths remain available -through lazy aliases. Existing root 1.x exports remain available. Historical -deep modules such as `dashscope.aigc`, `dashscope.client.base_api`, and -`dashscope.files` also remain available through lazy aliases to -`dashscope.legacy_1x`. - -> **Breaking (Agentic RL):** the public `dashscope.finetune.agentic_rl` path now -> resolves to the **2.0** `AgenticRL` — `submit_job` returns `SubmitResult` -> (not the 1.x `FineTune`) and the class no longer carries the job-management -> verbs. On 2.0 the job lifecycle lives on the client -> (`client.rl.jobs.create()`, `.wait()`, `.list()`, `.cancel()`, `.delete()`, -> `.logs()`). Code that relied on the 1.x class-based surface -> (`AgenticRL.cancel(...)`, `.get(...)`, `submit_job` → `FineTune`) should import -> `dashscope.legacy_1x.finetune.agentic_rl` instead, which keeps the five verbs -> (`cancel`/`list`/`get`/`delete`/`logs`) and the `FineTune` return. - -AgentStudio and Agentic RL exceptions are unified into the 2.0 error family: -`AgentStudioError` is now an alias of `dashscope.errors.DashScopeError`, and the -Agentic RL `*WithCode` errors subclass `AgenticRLError`. The former AgentStudio -`raw` response attribute is exposed as `body` (`err.raw` → `err.body`); the -`RuntimeErrorWithCode` / `ValueErrorWithCode` classes remain catchable as the -built-in `RuntimeError` / `ValueError` they wrap. Because both stacks now share -the `dashscope._shared._errors.DashScopeException` base, a single -`except DashScopeException` catches 1.x, 2.0 and AgentStudio errors alike. The -Agentic RL `*WithCode` errors also render differently in `str()` — the 1.x -`[1800] I/O error: …` prefix is replaced by the 2.0 ` … | -code=sdk.…` form (the numeric code is still available via `err.error_code` and -the timestamp via `err.timestamp`), so log parsers keyed on the old format need -updating. +`dashscope.v2` namespace has been removed. The single AgentStudio and +FineTune/Agentic RL business implementations live under +`dashscope.applications`. Two thin public facades sit on top: + +- historical `dashscope.agentstudio` and `dashscope.finetune.agentic_rl` + imports retain the 1.x clients, methods, return models and exception classes; +- `client.agentstudio` and `client.rl` expose the 2.0 resource APIs, httpx + transport and `dashscope.errors` family. + +Historical deep modules such as `dashscope.aigc`, +`dashscope.client.base_api`, `dashscope.files`, and `dashscope.tokenizers` +remain available through lazy aliases to `dashscope.legacy_1x`. The local +`get_tokenizer` utility therefore keeps its 1.x exception contract; the 2.0 +remote token-counting API is the independent `client.tokenization` resource. +The application cores are internal implementation paths; 2.0 callers should +use the main Client resource tree. + +In particular, the historical Agentic RL class still returns `FineTune` from +`submit_job` and retains `cancel/list/get/delete/logs`. The 2.0 job lifecycle +uses `client.rl.jobs`, and its workflow errors use the new SDK error catalog. +Likewise, standalone `dashscope.agentstudio.Client` keeps the old +`AgentStudioError` hierarchy and `raw` response attribute, while +`client.agentstudio` raises `dashscope.errors` and exposes failed response data +through `body`. > **Breaking (AgentStudio errors):** `ConflictError` is now an alias of > `APIStatusError` rather than a 409-specific subclass, so `except ConflictError` diff --git a/README_zh.md b/README_zh.md index fac0bb40..cd34d887 100644 --- a/README_zh.md +++ b/README_zh.md @@ -450,9 +450,24 @@ examples/ ### 包布局与兼容策略 2.0 实现直接位于 `dashscope` 根包,原 `dashscope.v2` 命名空间已移除。 -包根已有的 1.x 导出继续可用;`dashscope.aigc`、`dashscope.client` 等旧 -deep import 已移至 `dashscope.legacy_1x` 下。2.0 Agentic RL 工作流位于 -`dashscope.finetune`,1.x 任务管理接口位于 `dashscope.legacy_1x.finetune`。 +AgentStudio 与 FineTune/Agentic RL 的唯一业务核心位于 +`dashscope.applications`,其上保留两套薄 Facade: + +- 历史 `dashscope.agentstudio` 与 `dashscope.finetune.agentic_rl` 路径继续 + 提供 1.x 的方法、返回类型和旧异常; +- `client.agentstudio` 与 `client.rl` 提供 2.0 资源接口、httpx 传输和 + `dashscope.errors` 新异常。 + +`dashscope.aigc`、`dashscope.client.base_api`、`dashscope.files`、 +`dashscope.tokenizers` 等其他旧路径继续通过懒加载别名指向 +`dashscope.legacy_1x`。本地 `get_tokenizer` 因此保持 1.x 异常契约;2.0 的远程 +分词计数由独立的 `client.tokenization` 资源提供。应用核心目录属于内部实现, +2.0 业务代码应通过主 Client 资源树调用。 + +历史 Agentic RL 的 `submit_job` 仍返回 `FineTune`,并保留 +`cancel/list/get/delete/logs`;2.0 的任务生命周期位于 `client.rl.jobs`。 +历史 `dashscope.agentstudio.Client` 继续抛旧 `AgentStudioError` 并保留 +`raw`,而 `client.agentstudio` 使用新 errors,失败响应通过 `body` 暴露。 > **破坏性变更(Agentic RL):** 公开路径 `dashscope.finetune.agentic_rl` > 现在解析到 **2.0** 的 `AgenticRL` —— `submit_job` 返回 `SubmitResult` diff --git a/dashscope/__init__.py b/dashscope/__init__.py index c44c4676..de229132 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -74,7 +74,7 @@ from dashscope.legacy_1x.models import Models from dashscope.legacy_1x.nlp.understanding import Understanding from dashscope.legacy_1x.rerank import AioTextReRank, TextReRank -from dashscope.tokenizers import ( +from dashscope.legacy_1x.tokenizers import ( Tokenization, Tokenizer, get_tokenizer, diff --git a/dashscope/applications/agentstudio/client.py b/dashscope/applications/agentstudio/client.py index 55d543f0..368468ca 100644 --- a/dashscope/applications/agentstudio/client.py +++ b/dashscope/applications/agentstudio/client.py @@ -9,6 +9,7 @@ import httpx +from dashscope.applications.agentstudio import exceptions as _default_exceptions from dashscope._shared._validation import ( validate_region, validate_workspace_id, @@ -65,6 +66,10 @@ SyncTransport, AsyncTransport, ) +from dashscope.applications.agentstudio.streaming import ( + AsyncEventStream, + EventStream, +) from dashscope._config import _api_key_from_file, settings @@ -172,6 +177,10 @@ def __init__( ] = None, max_retries: int = AGENTSTUDIO_MAX_RETRIES, http_client: Optional[httpx.Client] = None, + _exceptions_module: Any = _default_exceptions, + _api_key_resolver: Any = _default_api_key, + _transport_cls: Any = SyncTransport, + _event_stream_cls: Any = EventStream, ) -> None: resolved_workspace = workspace or os.environ.get("DASHSCOPE_WORKSPACE") resolved_base = _resolve_base_url( @@ -179,15 +188,17 @@ def __init__( resolved_workspace, region, ) - self.transport = SyncTransport( + self._event_stream_cls = _event_stream_cls + self.transport = _transport_cls( base_url=resolved_base, - api_key=api_key or _default_api_key(), + api_key=api_key or _api_key_resolver(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT, max_retries=max_retries, http_client=http_client, + exceptions_module=_exceptions_module, ) self.agents = Agents(self) self.deployments = Deployments(self) @@ -248,6 +259,10 @@ def __init__( ] = None, max_retries: int = AGENTSTUDIO_MAX_RETRIES, http_client: Optional[httpx.AsyncClient] = None, + _exceptions_module: Any = _default_exceptions, + _api_key_resolver: Any = _default_api_key, + _transport_cls: Any = AsyncTransport, + _event_stream_cls: Any = AsyncEventStream, ) -> None: resolved_workspace = workspace or os.environ.get("DASHSCOPE_WORKSPACE") resolved_base = _resolve_base_url( @@ -255,15 +270,17 @@ def __init__( resolved_workspace, region, ) - self.transport = AsyncTransport( + self._event_stream_cls = _event_stream_cls + self.transport = _transport_cls( base_url=resolved_base, - api_key=api_key or _default_api_key(), + api_key=api_key or _api_key_resolver(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT, max_retries=max_retries, http_client=http_client, + exceptions_module=_exceptions_module, ) self.agents = AsyncAgents(self) self.deployments = AsyncDeployments(self) diff --git a/dashscope/applications/agentstudio/resources/session_events.py b/dashscope/applications/agentstudio/resources/session_events.py index 42499b9c..306377bc 100644 --- a/dashscope/applications/agentstudio/resources/session_events.py +++ b/dashscope/applications/agentstudio/resources/session_events.py @@ -157,7 +157,10 @@ def stream( timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT, ) return _TypedEventStream( - EventStream(response=resp), + self._client._event_stream_cls( + response=resp, + exceptions_module=self._client.transport.exceptions, + ), ) @@ -345,7 +348,10 @@ async def stream( timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT, ) return _AioTypedEventStream( - AsyncEventStream(response=resp), + self._client._event_stream_cls( + response=resp, + exceptions_module=self._client.transport.exceptions, + ), ) diff --git a/dashscope/applications/agentstudio/streaming.py b/dashscope/applications/agentstudio/streaming.py index f6e524d4..89bd5d55 100644 --- a/dashscope/applications/agentstudio/streaming.py +++ b/dashscope/applications/agentstudio/streaming.py @@ -39,7 +39,7 @@ import httpx from httpx_sse import EventSource -from dashscope.applications.agentstudio import exceptions +from dashscope.applications.agentstudio import exceptions as _default_exceptions # A child of the "dashscope" logger, so the level and handler installed by # dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via @@ -50,13 +50,16 @@ _DEFAULT_IDLE_TIMEOUT = 120.0 -def _raise_stream_error(exc: Exception) -> None: - if isinstance(exc, exceptions.AgentStudioError): +def _raise_stream_error( + exc: Exception, + exceptions_module: Any = _default_exceptions, +) -> None: + if isinstance(exc, exceptions_module.AgentStudioError): raise exc if isinstance(exc, httpx.TimeoutException): - raise exceptions.APITimeoutError(str(exc)) from exc + raise exceptions_module.APITimeoutError(str(exc)) from exc if isinstance(exc, (httpx.NetworkError, httpx.RemoteProtocolError)): - raise exceptions.APIConnectionError(str(exc)) from exc + raise exceptions_module.APIConnectionError(str(exc)) from exc raise exc @@ -83,6 +86,7 @@ class EventStream: response: httpx.Response idle_timeout: Optional[float] = _DEFAULT_IDLE_TIMEOUT + exceptions_module: Any = _default_exceptions _event_source: EventSource = field(init=False) _closed: bool = False _producer: Optional[threading.Thread] = field(init=False, default=None) @@ -101,7 +105,7 @@ def __iter__(self) -> Iterator[Dict[str, Any]]: def _iter(self) -> Iterator[Dict[str, Any]]: if self._closed: - raise exceptions.StreamClosedError("stream already closed") + raise self.exceptions_module.StreamClosedError("stream already closed") q: "queue.Queue[Any]" = queue.Queue() def _produce() -> None: @@ -138,7 +142,7 @@ def _produce() -> None: if item is None: return if isinstance(item, Exception): - _raise_stream_error(item) + _raise_stream_error(item, self.exceptions_module) yield item finally: self.close() @@ -164,6 +168,7 @@ class AsyncEventStream: response: httpx.Response idle_timeout: Optional[float] = _DEFAULT_IDLE_TIMEOUT + exceptions_module: Any = _default_exceptions _event_source: EventSource = field(init=False) _closed: bool = False _producer: Optional[asyncio.Task] = field(init=False, default=None) @@ -182,7 +187,7 @@ def __aiter__(self) -> AsyncIterator[Dict[str, Any]]: async def _aiter(self) -> AsyncIterator[Dict[str, Any]]: if self._closed: - raise exceptions.StreamClosedError("stream already closed") + raise self.exceptions_module.StreamClosedError("stream already closed") q: "asyncio.Queue[Any]" = asyncio.Queue() async def _produce() -> None: @@ -221,7 +226,7 @@ async def _produce() -> None: if item is None: return if isinstance(item, Exception): - _raise_stream_error(item) + _raise_stream_error(item, self.exceptions_module) yield item finally: await self.aclose() diff --git a/dashscope/applications/agentstudio/transport.py b/dashscope/applications/agentstudio/transport.py index db6a08c5..4ce40bfd 100644 --- a/dashscope/applications/agentstudio/transport.py +++ b/dashscope/applications/agentstudio/transport.py @@ -28,7 +28,7 @@ import httpx from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.applications.agentstudio import exceptions +from dashscope.applications.agentstudio import exceptions as _default_exceptions from dashscope.applications.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, @@ -185,7 +185,10 @@ def _should_retry(status_code: int, headers: Mapping[str, str]) -> bool: return status_code in (408, 409, 429) or status_code >= 500 -def _should_retry_exception(err: BaseException) -> Tuple[bool, Optional[Any]]: +def _should_retry_exception( + err: BaseException, + exceptions_module: Any = _default_exceptions, +) -> Tuple[bool, Optional[Any]]: """Walk the ``__cause__`` chain to decide if an exception is retryable. Returns ``(should_retry, response_or_none)``. @@ -194,7 +197,7 @@ def _should_retry_exception(err: BaseException) -> Tuple[bool, Optional[Any]]: current: Optional[BaseException] = err while current is not None and id(current) not in seen: seen.add(id(current)) - if isinstance(current, exceptions.AgentStudioError): + if isinstance(current, exceptions_module.AgentStudioError): return False, None if isinstance(current, httpx.TimeoutException): return True, None @@ -216,11 +219,12 @@ def build_headers( user_agent: str, extra: Optional[Mapping[str, str]] = None, json_body: bool = True, + _exceptions_module: Any = _default_exceptions, ) -> Dict[str, str]: """Compose the canonical AgentStudio request headers.""" if not api_key: - raise exceptions.AuthenticationError( + raise _exceptions_module.AuthenticationError( "api_key is required. Pass it via Client(api_key=...) or " "the DASHSCOPE_API_KEY environment variable.", code="authentication_error", @@ -262,6 +266,7 @@ def __init__( timeout: Union[float, httpx.Timeout, Tuple[float, float], None], max_retries: int = AGENTSTUDIO_MAX_RETRIES, http_client: Optional[httpx.Client] = None, + exceptions_module: Any = _default_exceptions, ) -> None: self.base_url = base_url.rstrip("/") self.api_key = api_key @@ -270,6 +275,7 @@ def __init__( self.user_agent = user_agent self.timeout = timeout self.max_retries = max_retries + self.exceptions = exceptions_module self._owns_client = http_client is None if http_client is not None: self._client = http_client @@ -336,6 +342,7 @@ def request( # pylint: disable=too-many-branches files is None and method.upper() not in ("GET", "HEAD", "DELETE") ), + _exceptions_module=self.exceptions, ) if files is not None: headers.pop("Content-Type", None) @@ -363,14 +370,14 @@ def request( # pylint: disable=too-many-branches except httpx.TimeoutException as exc: last_exc = exc if attempt >= self.max_retries: - raise exceptions.APITimeoutError(str(exc)) from exc + raise self.exceptions.APITimeoutError(str(exc)) from exc except Exception as exc: - if isinstance(exc, exceptions.AgentStudioError): + if isinstance(exc, self.exceptions.AgentStudioError): raise - should_retry, _ = _should_retry_exception(exc) + should_retry, _ = _should_retry_exception(exc, self.exceptions) last_exc = exc if attempt >= self.max_retries or not should_retry: - raise exceptions.APIConnectionError(str(exc)) from exc + raise self.exceptions.APIConnectionError(str(exc)) from exc else: if stream: if resp.status_code >= 400: @@ -381,7 +388,7 @@ def request( # pylint: disable=too-many-branches resp.status_code, resp.headers, ): - last_exc = exceptions.APIStatusError( + last_exc = self.exceptions.APIStatusError( f"HTTP {resp.status_code}", status_code=resp.status_code, ) @@ -399,7 +406,7 @@ def request( # pylint: disable=too-many-branches resp.close() time.sleep(_backoff(attempt)) attempt += 1 - raise exceptions.APIConnectionError( + raise self.exceptions.APIConnectionError( str(last_exc) if last_exc else "unknown", ) @@ -428,7 +435,7 @@ def _parse(self, resp: httpx.Response) -> APIResponse: header_rid, str(payload)[:500], ) - raise exceptions.from_response( + raise self.exceptions.from_response( status_code=resp.status_code, body=payload, headers=resp.headers, @@ -488,6 +495,7 @@ def __init__( timeout: Union[float, httpx.Timeout, Tuple[float, float], None], max_retries: int = AGENTSTUDIO_MAX_RETRIES, http_client: Optional[httpx.AsyncClient] = None, + exceptions_module: Any = _default_exceptions, ) -> None: self.base_url = base_url.rstrip("/") self.api_key = api_key @@ -496,6 +504,7 @@ def __init__( self.user_agent = user_agent self.timeout = timeout self.max_retries = max_retries + self.exceptions = exceptions_module self._owns_client = http_client is None if http_client is not None: self._client = http_client @@ -561,6 +570,7 @@ async def request( # pylint: disable=too-many-branches files is None and method.upper() not in ("GET", "HEAD", "DELETE") ), + _exceptions_module=self.exceptions, ) if files is not None: headers.pop("Content-Type", None) @@ -587,14 +597,14 @@ async def request( # pylint: disable=too-many-branches except httpx.TimeoutException as exc: last_exc = exc if attempt >= self.max_retries: - raise exceptions.APITimeoutError(str(exc)) from exc + raise self.exceptions.APITimeoutError(str(exc)) from exc except Exception as exc: - if isinstance(exc, exceptions.AgentStudioError): + if isinstance(exc, self.exceptions.AgentStudioError): raise - should_retry, _ = _should_retry_exception(exc) + should_retry, _ = _should_retry_exception(exc, self.exceptions) last_exc = exc if attempt >= self.max_retries or not should_retry: - raise exceptions.APIConnectionError(str(exc)) from exc + raise self.exceptions.APIConnectionError(str(exc)) from exc else: if stream: if resp.status_code >= 400: @@ -605,7 +615,7 @@ async def request( # pylint: disable=too-many-branches resp.status_code, resp.headers, ): - last_exc = exceptions.APIStatusError( + last_exc = self.exceptions.APIStatusError( f"HTTP {resp.status_code}", status_code=resp.status_code, ) @@ -623,7 +633,7 @@ async def request( # pylint: disable=too-many-branches await resp.aclose() await asyncio.sleep(_backoff(attempt)) attempt += 1 - raise exceptions.APIConnectionError( + raise self.exceptions.APIConnectionError( str(last_exc) if last_exc else "unknown", ) @@ -652,7 +662,7 @@ async def _parse(self, resp: httpx.Response) -> APIResponse: header_rid, str(payload)[:500], ) - raise exceptions.from_response( + raise self.exceptions.from_response( status_code=resp.status_code, body=payload, headers=resp.headers, diff --git a/dashscope/applications/finetune/reinforcement/common/_legacy_errors.py b/dashscope/applications/finetune/reinforcement/common/_legacy_errors.py new file mode 100644 index 00000000..0d9f31da --- /dev/null +++ b/dashscope/applications/finetune/reinforcement/common/_legacy_errors.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +""" +Custom exception hierarchy for the AgenticRL system +""" + +from datetime import datetime +from typing import Optional, Dict + + +class _RootCauseMixin: + """Mixin that provides root-cause traversal and formatting for exceptions + that carry an error code.""" + + @property + def root_cause(self) -> "Exception": + root: "Exception" = self # type: ignore[assignment] + seen = {id(root)} + while root.__cause__ and id(root.__cause__) not in seen: + root = root.__cause__ + seen.add(id(root)) + return root + + def _format_cause(self) -> str: + """Format root cause information if available.""" + if self.__cause__ is not None: + root = self.root_cause + if self is not root: + cause_msg = str(root).split("\n", maxsplit=1)[0][:100].strip() + if cause_msg: + return f" (caused by: {type(root).__name__}: {cause_msg})" + return f" (caused by: {type(root).__name__})" + return "" + + +class AgenticRLError(_RootCauseMixin, Exception): + """Base class for all Agentic RL exceptions.""" + + def __init__(self, message: str, error_code: int = 1000): + super().__init__(message) + self.error_code = error_code + self.timestamp = datetime.now().isoformat() + self.message = message + + def __str__(self): + base = f"[{self.error_code}] {self.message} (at {self.timestamp})" + return f"{base}{self._format_cause()}" + + +class IOErrorWithCode(AgenticRLError): + """Raised for general I/O operation failures""" + + def __init__( + self, + message: str, + error_code: int = 1800, + path: Optional[str] = None, + operation: Optional[str] = None, + ): + super().__init__(f"I/O error: {message}", error_code) + self.path = path + self.operation = operation + + +class RuntimeErrorWithCode(_RootCauseMixin, RuntimeError): + """Enhanced RuntimeError that supports error codes for better error + categorization.""" + + def __init__(self, message: str, error_code: int = 0): + super().__init__(message) + self.error_code = error_code + self.message = message + + def __str__(self): + return f"[{self.error_code}] {self.message}{self._format_cause()}" + + +class ValueErrorWithCode(_RootCauseMixin, ValueError): + """Enhanced ValueError that supports error codes for better error + categorization.""" + + def __init__(self, message: str, error_code: int = 0): + super().__init__(message) + self.error_code = error_code + self.message = message + + def __str__(self): + return f"[{self.error_code}] {self.message}{self._format_cause()}" + + +class InputError(AgenticRLError): + """Raised when invalid input data is detected during validation""" + + def __init__( + self, + message: str, + error_code: int = 1100, + field: Optional[str] = None, + ): + super().__init__(message, error_code) + self.field = field + + +class OutputError(AgenticRLError): + """Raised when service response fails output validation checks""" + + def __init__( + self, + message: str, + error_code: int = 1200, + response: Optional[Dict] = None, + ): + super().__init__(message, error_code) + self.response = response + + +class BaseConnectionError(AgenticRLError): + """Base class for connection-related errors""" + + def __init__( + self, + message: str, + error_code: int = 1300, + endpoint: Optional[str] = None, + ): + super().__init__(message, error_code) + self.endpoint = endpoint + + +class OSSConnectionError(BaseConnectionError): + """Raised when connecting to OSS storage service fails""" + + def __init__( + self, + message: str, + error_code: int = 1310, + endpoint: str = None, + ): + super().__init__( + f"OSS connection failed: {message}", + error_code, + endpoint, + ) + + +class OSSUploadError(BaseConnectionError): + """Raised when file upload operation to OSS fails""" + + def __init__( + self, + message: str, + error_code: int = 1320, + endpoint: str = None, + bucket: Optional[str] = None, + object_key: Optional[str] = None, + file_size: Optional[int] = None, + ): + super().__init__(f"OSS upload failed: {message}", error_code, endpoint) + self.bucket = bucket + self.object_key = object_key + self.file_size = file_size + + +class DeploymentError(AgenticRLError): + """Base class for deployment-related errors""" + + def __init__( + self, + message: str, + error_code: int = 1400, + resource_id: Optional[str] = None, + ): + super().__init__(message, error_code) + self.resource_id = resource_id + + +class RegistrationError(DeploymentError): + """Raised when function registration fails in the deployment system""" + + def __init__( + self, + message: str, + error_code: int = 1410, + resource_id: Optional[str] = None, + ): + super().__init__( + f"Registration failed: {message}", + error_code, + resource_id, + ) + + +class DatasetsError(DeploymentError): + """Raised when update datasets fails in the deployment system""" + + def __init__(self, message: str, error_code: int = 1460): + super().__init__(f"Datasets failed: {message}", error_code) + + +class FunctionLoadError(DeploymentError): + """Raised when loading a registered function into runtime fails""" + + def __init__( + self, + message: str, + error_code: int = 1420, + entity_id: str = None, + error_log: Optional[str] = None, + ): + super().__init__( + f"Function load failed: {message}", + error_code, + entity_id, + ) + self.entity_id = entity_id + self.error_log = error_log + + +class FunctionLayerError(DeploymentError): + """Raised when creating a layer of function fails""" + + def __init__( + self, + message: str, + error_code: int = 1450, + layer_name: str = None, + error_log: Optional[str] = None, + ): + super().__init__( + f"Function layer create failed: {message}", + error_code, + layer_name, + ) + self.layer_name = layer_name + self.error_log = error_log + + +class InstanceWarmupError(DeploymentError): + """Raised when function instance health check fails after deployment""" + + def __init__( + self, + message: str, + error_code: int = 1430, + instance_url: str = None, + timeout: float = 0.0, + retry_after: Optional[float] = None, + ): + super().__init__(f"Instance warmup failed: {message}", error_code) + self.instance_url = instance_url + self.timeout = timeout + self.retry_after = retry_after + + +class InstanceQueryError(DeploymentError): + """Raised when querying function instance status fails""" + + def __init__( + self, + message: str, + error_code: int = 1440, + instance_id: str = None, + query_attempts: int = 1, + ): + super().__init__( + f"Instance query failed: {message}", + error_code, + instance_id, + ) + self.instance_id = instance_id + self.query_attempts = query_attempts + + +class ValidationError(AgenticRLError): + """Base class for data validation failures""" + + def __init__( + self, + message: str, + error_code: int = 1500, + invalid_data: Optional[Dict] = None, + validation_rules: Optional[Dict] = None, + ): + super().__init__(f"Validation failed: {message}", error_code) + self.invalid_data = invalid_data + self.validation_rules = validation_rules + + +class ConfigurationError(ValidationError): + """Raised when invalid system configuration is detected""" + + def __init__( + self, + message: str, + error_code: int = 1510, + config_path: Optional[str] = None, + ): + super().__init__(message, error_code=error_code) + self.config_path = config_path + + +class BasePermissionError(AgenticRLError): + """Raised when an operation lacks required permissions""" + + def __init__( + self, + message: str, + error_code: int = 1700, + operation: str = None, + resource: str = None, + ): + super().__init__(f"Permission denied: {message}", error_code) + self.operation = operation + self.resource = resource diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index 0a3769ec..1223e6bf 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -7,6 +7,28 @@ from dashscope.errors import _CatalogSdkError, sdk_error +from dashscope.applications.finetune.reinforcement.common._legacy_errors import ( + AgenticRLError as _LegacyAgenticRLError, + BaseConnectionError as _LegacyBaseConnectionError, + BasePermissionError as _LegacyBasePermissionError, + ConfigurationError as _LegacyConfigurationError, + DatasetsError as _LegacyDatasetsError, + DeploymentError as _LegacyDeploymentError, + FunctionLayerError as _LegacyFunctionLayerError, + FunctionLoadError as _LegacyFunctionLoadError, + IOErrorWithCode as _LegacyIOErrorWithCode, + InputError as _LegacyInputError, + InstanceQueryError as _LegacyInstanceQueryError, + InstanceWarmupError as _LegacyInstanceWarmupError, + OSSConnectionError as _LegacyOSSConnectionError, + OSSUploadError as _LegacyOSSUploadError, + OutputError as _LegacyOutputError, + RegistrationError as _LegacyRegistrationError, + RuntimeErrorWithCode as _LegacyRuntimeErrorWithCode, + ValidationError as _LegacyValidationError, + ValueErrorWithCode as _LegacyValueErrorWithCode, +) + _LEGACY_ERROR_CODES = { "sdk.agentic_rl.Error": 1000, "sdk.agentic_rl.IOErrorWithCode": 1800, @@ -51,7 +73,11 @@ def _format_cause(self) -> str: return "" -class AgenticRLError(_RootCauseMixin, _CatalogSdkError): +class AgenticRLError( + _RootCauseMixin, + _CatalogSdkError, + _LegacyAgenticRLError, +): """Base class for AgenticRL failures in the v2 error family.""" _SDK_ERROR_NAME = "sdk.agentic_rl.Error" @@ -65,6 +91,10 @@ def __init__( self._legacy_error_code = error_code self.timestamp = datetime.now().isoformat() super().__init__(message, **kwargs) + # Legacy compatibility bases run inside the cooperative MRO and set + # their own defaults. Restore the v2/catalog selection requested by + # this constructor after that initialization completes. + self._legacy_error_code = error_code @classmethod def from_catalog( @@ -89,11 +119,15 @@ def error_code(self) -> int: return self._legacy_error_code return _LEGACY_ERROR_CODES.get(self.code or "", 1000) + @error_code.setter + def error_code(self, value: int) -> None: + self._legacy_error_code = value + def __str__(self) -> str: return f"{super().__str__()}{self._format_cause()}" -class IOErrorWithCode(AgenticRLError): +class IOErrorWithCode(AgenticRLError, _LegacyIOErrorWithCode): _SDK_ERROR_NAME = "sdk.agentic_rl.IOErrorWithCode" def __init__( @@ -109,15 +143,15 @@ def __init__( self.operation = operation -class RuntimeErrorWithCode(AgenticRLError, RuntimeError): +class RuntimeErrorWithCode(AgenticRLError, _LegacyRuntimeErrorWithCode): _SDK_ERROR_NAME = "sdk.agentic_rl.RuntimeErrorWithCode" -class ValueErrorWithCode(AgenticRLError, ValueError): +class ValueErrorWithCode(AgenticRLError, _LegacyValueErrorWithCode): _SDK_ERROR_NAME = "sdk.agentic_rl.ValueErrorWithCode" -class InputError(AgenticRLError): +class InputError(AgenticRLError, _LegacyInputError): _SDK_ERROR_NAME = "sdk.agentic_rl.InputError" def __init__( @@ -131,7 +165,7 @@ def __init__( self.field = field -class OutputError(AgenticRLError): +class OutputError(AgenticRLError, _LegacyOutputError): _SDK_ERROR_NAME = "sdk.agentic_rl.OutputError" def __init__( @@ -145,7 +179,7 @@ def __init__( self.response = response -class BaseConnectionError(AgenticRLError): +class BaseConnectionError(AgenticRLError, _LegacyBaseConnectionError): _SDK_ERROR_NAME = "sdk.agentic_rl.BaseConnectionError" def __init__( @@ -159,7 +193,7 @@ def __init__( self.endpoint = endpoint -class OSSConnectionError(BaseConnectionError): +class OSSConnectionError(BaseConnectionError, _LegacyOSSConnectionError): _SDK_ERROR_NAME = "sdk.agentic_rl.OSSConnectionError" def __init__( @@ -177,7 +211,7 @@ def __init__( ) -class OSSUploadError(BaseConnectionError): +class OSSUploadError(BaseConnectionError, _LegacyOSSUploadError): _SDK_ERROR_NAME = "sdk.agentic_rl.OSSUploadError" def __init__( @@ -201,7 +235,7 @@ def __init__( self.file_size = file_size -class DeploymentError(AgenticRLError): +class DeploymentError(AgenticRLError, _LegacyDeploymentError): _SDK_ERROR_NAME = "sdk.agentic_rl.DeploymentError" def __init__( @@ -215,7 +249,7 @@ def __init__( self.resource_id = resource_id -class RegistrationError(DeploymentError): +class RegistrationError(DeploymentError, _LegacyRegistrationError): _SDK_ERROR_NAME = "sdk.agentic_rl.RegistrationError" def __init__( @@ -233,7 +267,7 @@ def __init__( ) -class DatasetsError(DeploymentError): +class DatasetsError(DeploymentError, _LegacyDatasetsError): _SDK_ERROR_NAME = "sdk.agentic_rl.DatasetsError" def __init__( @@ -245,7 +279,7 @@ def __init__( super().__init__(f"Datasets failed: {message}", error_code, **kwargs) -class FunctionLoadError(DeploymentError): +class FunctionLoadError(DeploymentError, _LegacyFunctionLoadError): _SDK_ERROR_NAME = "sdk.agentic_rl.FunctionLoadError" def __init__( @@ -266,7 +300,7 @@ def __init__( self.error_log = error_log -class FunctionLayerError(DeploymentError): +class FunctionLayerError(DeploymentError, _LegacyFunctionLayerError): _SDK_ERROR_NAME = "sdk.agentic_rl.FunctionLayerError" def __init__( @@ -287,7 +321,7 @@ def __init__( self.error_log = error_log -class InstanceWarmupError(DeploymentError): +class InstanceWarmupError(DeploymentError, _LegacyInstanceWarmupError): _SDK_ERROR_NAME = "sdk.agentic_rl.InstanceWarmupError" def __init__( @@ -309,7 +343,7 @@ def __init__( self.retry_after = retry_after -class InstanceQueryError(DeploymentError): +class InstanceQueryError(DeploymentError, _LegacyInstanceQueryError): _SDK_ERROR_NAME = "sdk.agentic_rl.InstanceQueryError" def __init__( @@ -330,7 +364,7 @@ def __init__( self.query_attempts = query_attempts -class ValidationError(AgenticRLError): +class ValidationError(AgenticRLError, _LegacyValidationError): _SDK_ERROR_NAME = "sdk.agentic_rl.ValidationError" def __init__( @@ -346,7 +380,7 @@ def __init__( self.validation_rules = validation_rules -class ConfigurationError(ValidationError): +class ConfigurationError(ValidationError, _LegacyConfigurationError): _SDK_ERROR_NAME = "sdk.agentic_rl.ConfigurationError" def __init__( @@ -360,7 +394,7 @@ def __init__( self.config_path = config_path -class BasePermissionError(AgenticRLError): +class BasePermissionError(AgenticRLError, _LegacyBasePermissionError): _SDK_ERROR_NAME = "sdk.agentic_rl.BasePermissionError" def __init__( diff --git a/dashscope/client.py b/dashscope/client.py index cdd01d88..cb2888cc 100644 --- a/dashscope/client.py +++ b/dashscope/client.py @@ -53,6 +53,21 @@ def _http_limits(config: Config) -> httpx.Limits: ) +def _agentstudio_base_url(config: Config) -> Optional[str]: + """Derive AgentStudio's service root from an explicit main base URL. + + Default workspace gateways are resolved by the AgentStudio core itself. + A configured main service root ends at ``/api/v1``; AgentStudio is mounted + below it at ``/agentstudio``. + """ + if config.configured_base_url is None: + return None + base = config.configured_base_url.rstrip("/") + if base.endswith("/agentstudio"): + return base + return f"{base}/agentstudio" + + class Client: """Synchronous dashscope 2.0 client. @@ -164,7 +179,7 @@ def agentstudio(self): """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import Client as AgentStudioClient + from dashscope.applications.agentstudio import Client as AgentStudioClient # agentstudio resolves its own base_url from workspace + # region (the v2 base_url does not propagate: the agentstudio @@ -173,6 +188,10 @@ def agentstudio(self): api_key=self._config.api_key, workspace=self._config.workspace, region=self._config.region, + base_url=_agentstudio_base_url(self._config), + timeout=self._config.timeout, + max_retries=self._config.max_retries, + http_client=self._transport.http_client, ) return self._agentstudio @@ -255,6 +274,8 @@ def ws_connection( connector: Optional[Connector] = None, timeout: Optional[float] = None, module: Optional[str] = None, + task_queue_size: int = 1024, + ping_interval: Optional[float] = 30.0, ) -> WsConnection: """Open a shared duplex websocket carrying multiple tasks. @@ -277,6 +298,8 @@ def ws_connection( connector=connector, timeout=config.timeout if timeout is None else timeout, module=module, + task_queue_size=task_queue_size, + ping_interval=ping_interval, ) def __enter__(self) -> "Client": @@ -394,7 +417,7 @@ def agentstudio(self): """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import AsyncClient as AsClient + from dashscope.applications.agentstudio import AsyncClient as AsClient # See Client.agentstudio: agentstudio builds its own base_url # from workspace + region, so forward both. @@ -402,6 +425,10 @@ def agentstudio(self): api_key=self._config.api_key, workspace=self._config.workspace, region=self._config.region, + base_url=_agentstudio_base_url(self._config), + timeout=self._config.timeout, + max_retries=self._config.max_retries, + http_client=self._transport.http_client, ) return self._agentstudio diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index 52a7d9fe..f77295af 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -10,7 +10,7 @@ import sys -_APPLICATION_ROOTS = frozenset({"agentstudio", "finetune"}) +_APPLICATION_ROOTS = frozenset({"finetune"}) _PACKAGE_ROOTS = frozenset( { @@ -28,14 +28,38 @@ "protocol", "rerank", "threads", + "tokenizers", "utils", }, ) _EXACT_MODULES = { + "dashscope.agentstudio": "dashscope.legacy_1x.agentstudio", + "dashscope.agentstudio.client": "dashscope.legacy_1x.agentstudio.client", + "dashscope.agentstudio.exceptions": ( + "dashscope.legacy_1x.agentstudio.exceptions" + ), + "dashscope.agentstudio.streaming": ( + "dashscope.legacy_1x.agentstudio.streaming" + ), + "dashscope.agentstudio.transport": ( + "dashscope.legacy_1x.agentstudio.transport" + ), "dashscope.files": "dashscope.legacy_1x.files", "dashscope.model": "dashscope.legacy_1x.model", "dashscope.models": "dashscope.legacy_1x.models", + "dashscope.finetune.agentic_rl": ( + "dashscope.legacy_1x.finetune.agentic_rl" + ), + "dashscope.finetune.reinforcement": ( + "dashscope.legacy_1x.finetune.reinforcement" + ), + "dashscope.finetune.reinforcement.common": ( + "dashscope.legacy_1x.finetune.reinforcement.common" + ), + "dashscope.finetune.reinforcement.common.errors": ( + "dashscope.legacy_1x.finetune.reinforcement.common.errors" + ), "dashscope.finetune.customize_types": ( "dashscope.legacy_1x.finetune.customize_types" ), @@ -66,6 +90,9 @@ def _canonical_name(name): return _EXACT_MODULES[name] if name.startswith("dashscope.client."): return "dashscope.legacy_1x.client." + name[len("dashscope.client.") :] + if name.startswith("dashscope.agentstudio."): + suffix = name[len("dashscope.agentstudio.") :] + return "dashscope.applications.agentstudio." + suffix parts = name.split(".") if len(parts) > 1 and parts[0] == "dashscope": if parts[1] in _APPLICATION_ROOTS: diff --git a/dashscope/legacy_1x/agentstudio/__init__.py b/dashscope/legacy_1x/agentstudio/__init__.py new file mode 100644 index 00000000..39a23e55 --- /dev/null +++ b/dashscope/legacy_1x/agentstudio/__init__.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The historical AgentStudio facade over the shared application core. + +The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP +API. All resource classes are accessed through a client instance:: + + from dashscope.applications.agentstudio import Client + from dashscope.applications.agentstudio.types import ( + user_message, user_tool_result, + ) + +Example +------- +>>> from dashscope.applications.agentstudio import Client +>>> from dashscope.applications.agentstudio.types import user_message +>>> client = Client(api_key="sk-xxx") +>>> agent = client.agents.create(name="demo", model="qwen-plus") +>>> session = client.sessions.create(agent=agent.id) +>>> client.sessions.events.send(session.id, [user_message("Hello!")]) +>>> with client.sessions.events.stream(session.id) as stream: +... for event in stream: +... print(event.type, event.to_dict()) +... if event.type == "session_status": +... break +""" + +# yapf: disable + +from dashscope.legacy_1x.agentstudio.client import Client, AsyncClient +from dashscope.legacy_1x.agentstudio.transport import APIResponse, RequestOptions +from dashscope.applications.agentstudio import types +from dashscope.legacy_1x.agentstudio.exceptions import ( + AgentStudioError, + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + ConflictError, + InternalServerError, + InvalidRequestError, + NotFoundError, + OverloadedError, + PermissionDeniedError, + RateLimitError, + StreamClosedError, + StreamError, +) +from dashscope.applications.agentstudio.pagination import ( + AsyncCursorPage, + CursorPage, +) +from dashscope.applications.agentstudio.types import ( + Memory, + MemoryStore, + MemoryVersion, + Message, + PermissionPolicy, + SecurityAlert, + SecurityAlertList, + SecurityOverview, + ServerEvent, + SessionResource, + StopReason, + user_define_outcome, + user_interrupt, + user_message, + user_tool_approval_response, + user_custom_tool_result, + user_tool_result, +) +from dashscope.applications.agentstudio.types.params import ( + AgentCreateParams, + AgentUpdateParams, + SessionCreateParams, + SessionUpdateParams, + EnvironmentCreateParams, + EnvironmentUpdateParams, + DeploymentCreateParams, + DeploymentUpdateParams, + SessionEventSendParams, + WebhookEndpointCreateParams, + WebhookEndpointUpdateParams, + WebhookEventListParams, +) +from dashscope.applications.agentstudio.constants import ( + PermissionPolicyType, + SSEEventType, + SessionStatus, + StopReasonType, + WebhookDeliveryStatus, + WebhookDisabledReason, + WebhookEventType, + WebhookStatus, +) + +__all__ = [ + # client + "Client", + "AsyncClient", + # base types + "RequestOptions", + "APIResponse", + # exceptions + "AgentStudioError", + "APIConnectionError", + "APIStatusError", + "APITimeoutError", + "AuthenticationError", + "ConflictError", + "InternalServerError", + "InvalidRequestError", + "NotFoundError", + "OverloadedError", + "PermissionDeniedError", + "RateLimitError", + "StreamError", + "StreamClosedError", + # pagination + "CursorPage", + "AsyncCursorPage", + # unified message type + "Memory", + "MemoryStore", + "MemoryVersion", + "Message", + "PermissionPolicy", + "SecurityAlert", + "SecurityAlertList", + "SecurityOverview", + "ServerEvent", + "SessionResource", + "StopReason", + # client event helpers (re-exported for convenience) + "user_message", + "user_interrupt", + "user_tool_approval_response", + "user_custom_tool_result", + "user_tool_result", + "user_define_outcome", + # typing module + "types", + # params (commonly used request-body types) + "AgentCreateParams", + "AgentUpdateParams", + "SessionCreateParams", + "SessionUpdateParams", + "EnvironmentCreateParams", + "EnvironmentUpdateParams", + "DeploymentCreateParams", + "DeploymentUpdateParams", + "SessionEventSendParams", + "WebhookEndpointCreateParams", + "WebhookEndpointUpdateParams", + "WebhookEventListParams", + "WebhookStatus", + "WebhookDisabledReason", + "WebhookDeliveryStatus", + "WebhookEventType", + "SSEEventType", + "SessionStatus", + "StopReasonType", + "PermissionPolicyType", +] diff --git a/dashscope/legacy_1x/agentstudio/client.py b/dashscope/legacy_1x/agentstudio/client.py new file mode 100644 index 00000000..859af516 --- /dev/null +++ b/dashscope/legacy_1x/agentstudio/client.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The historical AgentStudio Client facade. + +Business resources and HTTP mechanics live in ``dashscope.applications``; +this facade preserves 1.x credential resolution and exception classes. +""" + +from __future__ import annotations + +from typing import Optional, Tuple, Union + +import httpx + +from dashscope.applications.agentstudio.client import ( # noqa: F401 + AsyncClient as _CoreAsyncClient, +) +from dashscope.applications.agentstudio.client import Client as _CoreClient +from dashscope.applications.agentstudio import client as _core_client +from dashscope.applications.agentstudio.constants import ( + AGENTSTUDIO_MAX_RETRIES, +) +from dashscope.legacy_1x.agentstudio import exceptions +from dashscope.legacy_1x.agentstudio.streaming import ( + AsyncEventStream, + EventStream, +) +from dashscope.legacy_1x.agentstudio.transport import ( + AsyncTransport, + SyncTransport, +) +from dashscope.legacy_1x.common.api_key import get_default_api_key + +# Historical module-level helpers remain importable from this facade. +_resolve_base_url = _core_client._resolve_base_url +_user_agent = _core_client._user_agent + + +class Client(_CoreClient): + """1.x AgentStudio client backed by the shared application core.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + region: Optional[str] = None, + base_url: Optional[str] = None, + uid: Optional[str] = None, + timeout: Optional[ + Union[float, httpx.Timeout, Tuple[float, float]] + ] = None, + max_retries: int = AGENTSTUDIO_MAX_RETRIES, + http_client: Optional[httpx.Client] = None, + ) -> None: + super().__init__( + api_key=api_key, + workspace=workspace, + region=region, + base_url=base_url, + uid=uid, + timeout=timeout, + max_retries=max_retries, + http_client=http_client, + _exceptions_module=exceptions, + _api_key_resolver=get_default_api_key, + _transport_cls=SyncTransport, + _event_stream_cls=EventStream, + ) + + +class AsyncClient(_CoreAsyncClient): + """Async 1.x AgentStudio client backed by the shared core.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + region: Optional[str] = None, + base_url: Optional[str] = None, + uid: Optional[str] = None, + timeout: Optional[ + Union[float, httpx.Timeout, Tuple[float, float]] + ] = None, + max_retries: int = AGENTSTUDIO_MAX_RETRIES, + http_client: Optional[httpx.AsyncClient] = None, + ) -> None: + super().__init__( + api_key=api_key, + workspace=workspace, + region=region, + base_url=base_url, + uid=uid, + timeout=timeout, + max_retries=max_retries, + http_client=http_client, + _exceptions_module=exceptions, + _api_key_resolver=get_default_api_key, + _transport_cls=AsyncTransport, + _event_stream_cls=AsyncEventStream, + ) + + +__all__ = ["AsyncClient", "Client"] diff --git a/dashscope/legacy_1x/agentstudio/exceptions.py b/dashscope/legacy_1x/agentstudio/exceptions.py new file mode 100644 index 00000000..124ab5e8 --- /dev/null +++ b/dashscope/legacy_1x/agentstudio/exceptions.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Exception hierarchy for the AgentStudio SDK. + +The AgentStudio service returns errors in the canonical CMA shape:: + + { + "type": "error", + "error": {"code": "invalid_request_error", "message": "..."}, + "request_id": "req_..." + } + +The pre-release backend currently emits ``error_code``/``error_message`` +instead of nested ``error.{code,message}``. We accept both shapes and +normalize to the documented form. The compatibility branch is marked +with ``# TODO(bma-fix)`` so we can remove it once the backend aligns. +""" + +from __future__ import annotations + +from typing import Any, Dict, Mapping, Optional + +from dashscope.common.error import DashScopeException + + +class AgentStudioError(DashScopeException): + """Base exception for all AgentStudio SDK errors. + + Attributes + ---------- + code: str + Machine-readable error code (e.g. ``invalid_request_error``). + message: str + Human-readable error description from the server. + request_id: Optional[str] + Correlation identifier for log lookups (``req_``). + status_code: Optional[int] + HTTP status code if the error originated from a HTTP response. + raw: Optional[Mapping[str, Any]] + Original response payload for debugging. + """ + + code: str = "agentstudio_error" + + def __init__( + self, + message: str, + *, + code: Optional[str] = None, + request_id: Optional[str] = None, + status_code: Optional[int] = None, + raw: Optional[Mapping[str, Any]] = None, + ) -> None: + super().__init__(message) + if code is not None: + self.code = code + self.message = message + self.request_id = request_id + self.status_code = status_code + self.raw = raw + + def __repr__(self) -> str: # pragma: no cover - debug helper + return ( + f"{type(self).__name__}(code={self.code!r}, " + f"message={self.message!r}, request_id={self.request_id!r}, " + f"status_code={self.status_code!r})" + ) + + +# --------------------------------------------------------------------------- +# Connection / transport layer errors (no HTTP response received) +# --------------------------------------------------------------------------- + + +class APIConnectionError(AgentStudioError): + """Raised when the HTTP request fails before a response is read.""" + + code = "api_connection_error" + + +class APITimeoutError(APIConnectionError): + """Raised on connect / read timeouts.""" + + code = "api_timeout_error" + + +# --------------------------------------------------------------------------- +# Server-side errors (HTTP response received) +# --------------------------------------------------------------------------- + + +class APIStatusError(AgentStudioError): + """Raised when the server returns a non-2xx status.""" + + code = "api_status_error" + + +class InvalidRequestError(APIStatusError): + code = "invalid_request_error" + + +class AuthenticationError(APIStatusError): + code = "authentication_error" + + +class PermissionDeniedError(APIStatusError): + code = "permission_denied_error" + + +class NotFoundError(APIStatusError): + code = "not_found_error" + + +class ConflictError(APIStatusError): + code = "conflict_error" + + +class RateLimitError(APIStatusError): + code = "rate_limit_error" + + +class OverloadedError(APIStatusError): + code = "overloaded_error" + + +class InternalServerError(APIStatusError): + code = "api_error" + + +# --------------------------------------------------------------------------- +# Streaming errors +# --------------------------------------------------------------------------- + + +class StreamError(AgentStudioError): + """Raised when an SSE stream encounters a fatal protocol error.""" + + code = "stream_error" + + +class StreamClosedError(StreamError): + """Raised when consumers attempt I/O on an already-closed stream.""" + + code = "stream_closed" + + +# --------------------------------------------------------------------------- +# Dispatcher +# --------------------------------------------------------------------------- + + +_STATUS_TO_DEFAULT: Dict[int, type] = { + 400: InvalidRequestError, + 401: AuthenticationError, + 403: PermissionDeniedError, + 404: NotFoundError, + 409: ConflictError, + 429: RateLimitError, + 500: InternalServerError, + 502: InternalServerError, + 503: OverloadedError, + 504: InternalServerError, +} + +_CODE_TO_CLASS: Dict[str, type] = { + "invalid_request_error": InvalidRequestError, + "authentication_error": AuthenticationError, + "permission_denied_error": PermissionDeniedError, + "not_found_error": NotFoundError, + "conflict_error": ConflictError, + "rate_limit_error": RateLimitError, + "overloaded_error": OverloadedError, + "api_error": InternalServerError, +} + + +def from_response( + *, + status_code: int, + body: Any, + headers: Optional[Mapping[str, str]] = None, +) -> AgentStudioError: + """Build an :class:`AgentStudioError` instance from a HTTP response. + + Accepts both the documented ``{type, error:{code,message}, request_id}`` + shape and the pre-release ``{type, error:{error_code, error_message}}`` + shape. Falls back to a Spring default ``{timestamp,status,error,path}`` + when the body is not JSON-serializable. + + The ``x-request-id`` response header is preferred over the body + ``request_id`` field (server-generated IDs are more reliable for tracing). + """ + + code: Optional[str] = None + message: Optional[str] = None + request_id: Optional[str] = None + + # Prefer server-generated request ID from response header. + if headers: + request_id = headers.get("x-request-id") + + if isinstance(body, Mapping): + # Body request_id as fallback (snake_case canonical). + if request_id is None: + request_id = body.get("request_id") or body.get( + "requestId", + ) # TODO(bma-fix) + err = body.get("error") + if isinstance(err, Mapping): + code = err.get("code") or err.get("error_code") # TODO(bma-fix) + message = err.get("message") or err.get( + "error_message", + ) # TODO(bma-fix) + # Spring default fallback. + if ( + message is None + and "error" in body + and isinstance(body["error"], str) + ): + message = body["error"] + code = body.get("error") or "api_error" + if message is None: + message = body.get("message") + + if message is None: + message = f"HTTP {status_code}" + if code is None: + code = _STATUS_TO_DEFAULT.get( # type: ignore[attr-defined] + status_code, + APIStatusError, + ).code + + cls = ( + _CODE_TO_CLASS.get(code) + or _STATUS_TO_DEFAULT.get(status_code) + or APIStatusError + ) + return cls( + message, + code=code, + request_id=request_id, + status_code=status_code, + raw=body if isinstance(body, Mapping) else {"raw": body}, + ) diff --git a/dashscope/legacy_1x/agentstudio/streaming.py b/dashscope/legacy_1x/agentstudio/streaming.py new file mode 100644 index 00000000..8e2d00a0 --- /dev/null +++ b/dashscope/legacy_1x/agentstudio/streaming.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Historical AgentStudio stream facades over the shared SSE core.""" + +from typing import Optional + +import httpx + +from dashscope.applications.agentstudio.streaming import ( + AsyncEventStream as _CoreAsyncEventStream, +) +from dashscope.applications.agentstudio.streaming import ( + EventStream as _CoreEventStream, +) +from dashscope.legacy_1x.agentstudio import exceptions + + +class EventStream(_CoreEventStream): + def __init__( + self, + response: httpx.Response, + idle_timeout: Optional[float] = 120.0, + exceptions_module=exceptions, + ) -> None: + super().__init__( + response=response, + idle_timeout=idle_timeout, + exceptions_module=exceptions, + ) + + +class AsyncEventStream(_CoreAsyncEventStream): + def __init__( + self, + response: httpx.Response, + idle_timeout: Optional[float] = 120.0, + exceptions_module=exceptions, + ) -> None: + super().__init__( + response=response, + idle_timeout=idle_timeout, + exceptions_module=exceptions, + ) + + +__all__ = ["AsyncEventStream", "EventStream"] diff --git a/dashscope/legacy_1x/agentstudio/transport.py b/dashscope/legacy_1x/agentstudio/transport.py new file mode 100644 index 00000000..ca4f6433 --- /dev/null +++ b/dashscope/legacy_1x/agentstudio/transport.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The 1.x AgentStudio transport facade over the shared implementation.""" + +from __future__ import annotations + +from typing import Any, Mapping, Optional, Tuple, Union + +import httpx + +from dashscope.applications.agentstudio import transport as _core +from dashscope.legacy_1x.agentstudio import exceptions + +APIResponse = _core.APIResponse +RequestOptions = _core.RequestOptions +unwrap = _core.unwrap +is_error_payload = _core.is_error_payload + + +def build_headers( + *, + api_key: Optional[str], + uid: Optional[str], + user_agent: str, + extra: Optional[Mapping[str, str]] = None, + json_body: bool = True, +): + """Build headers while preserving the 1.x AuthenticationError type.""" + return _core.build_headers( + api_key=api_key, + uid=uid, + user_agent=user_agent, + extra=extra, + json_body=json_body, + _exceptions_module=exceptions, + ) + + +class SyncTransport(_core.SyncTransport): + """1.x transport facade; HTTP implementation remains single-sourced.""" + + def __init__( + self, + *, + base_url: str, + api_key: Optional[str], + workspace: Optional[str], + uid: Optional[str], + user_agent: str, + timeout: Union[float, httpx.Timeout, Tuple[float, float], None], + max_retries: int = _core.AGENTSTUDIO_MAX_RETRIES, + http_client: Optional[httpx.Client] = None, + exceptions_module: Any = exceptions, + ) -> None: + super().__init__( + base_url=base_url, + api_key=api_key, + workspace=workspace, + uid=uid, + user_agent=user_agent, + timeout=timeout, + max_retries=max_retries, + http_client=http_client, + exceptions_module=exceptions, + ) + + +class AsyncTransport(_core.AsyncTransport): + """Async 1.x transport facade over the shared core.""" + + def __init__( + self, + *, + base_url: str, + api_key: Optional[str], + workspace: Optional[str], + uid: Optional[str], + user_agent: str, + timeout: Union[float, httpx.Timeout, Tuple[float, float], None], + max_retries: int = _core.AGENTSTUDIO_MAX_RETRIES, + http_client: Optional[httpx.AsyncClient] = None, + exceptions_module: Any = exceptions, + ) -> None: + super().__init__( + base_url=base_url, + api_key=api_key, + workspace=workspace, + uid=uid, + user_agent=user_agent, + timeout=timeout, + max_retries=max_retries, + http_client=http_client, + exceptions_module=exceptions, + ) + + +__all__ = [ + "APIResponse", + "AsyncTransport", + "RequestOptions", + "SyncTransport", + "build_headers", + "is_error_payload", + "unwrap", +] diff --git a/dashscope/legacy_1x/common/error.py b/dashscope/legacy_1x/common/error.py index e65f29e9..aca70b6a 100644 --- a/dashscope/legacy_1x/common/error.py +++ b/dashscope/legacy_1x/common/error.py @@ -1,115 +1,68 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""The 1.x exception family (pending deprecation). - -Every class here belongs to the 1.x API surface and is *going to be* -deprecated along with it: instantiation emits a -:exc:`PendingDeprecationWarning` (ignored by Python's default filters, -so current 1.x users see no noise), and static checkers flag the names -via ``__deprecated__``. New code should use the 2.0 family in -:mod:`dashscope.errors` (``DashScopeError`` and its subclasses). The -1.x classes stay fully supported and importable so existing ``except`` -clauses keep working. - -``DashScopeException`` carries no marker at all: it is re-exported from -the neutral layer (:mod:`dashscope._shared._errors`) that both stacks -inherit from. -""" - -from typing_extensions import deprecated - -# The base class lives in the neutral layer so the AgentStudio sub-SDK -# (owned by the 2.0 package) and the 1.x classes below inherit from one -# and the same object. -from dashscope._shared._errors import DashScopeException - -def _legacy_error(name): - """Pending-deprecation marker for one 1.x error class. - Warns on instantiation (and subclasses), never on import, so the - SDK's own 1.x modules can keep importing these names silently. - """ - return deprecated( - f"dashscope.common.error.{name} is part of the 1.x API, which " - f"is scheduled for deprecation and will be removed in a future " - f"release; use the dashscope.errors (2.0) exception family " - f"instead.", - category=PendingDeprecationWarning, - ) +# One shared base preserves broad ``except DashScopeException`` handlers while +# every concrete 1.x error below remains the unchanged historical class. +from dashscope._shared._errors import DashScopeException -@_legacy_error("AuthenticationError") class AuthenticationError(DashScopeException): pass -@_legacy_error("InvalidParameter") class InvalidParameter(DashScopeException): pass -@_legacy_error("InvalidTask") class InvalidTask(DashScopeException): pass -@_legacy_error("UnsupportedModel") class UnsupportedModel(DashScopeException): pass -@_legacy_error("UnsupportedTask") class UnsupportedTask(DashScopeException): pass -@_legacy_error("ModelRequired") class ModelRequired(DashScopeException): pass -@_legacy_error("InvalidModel") class InvalidModel(DashScopeException): pass -@_legacy_error("InvalidInput") class InvalidInput(DashScopeException): pass -@_legacy_error("InvalidFileFormat") class InvalidFileFormat(DashScopeException): pass -@_legacy_error("UnsupportedApiProtocol") class UnsupportedApiProtocol(DashScopeException): pass -@_legacy_error("NotImplemented") class NotImplemented(DashScopeException): # pylint: disable=redefined-builtin pass -@_legacy_error("MultiInputsWithBinaryNotSupported") class MultiInputsWithBinaryNotSupported(DashScopeException): pass -@_legacy_error("UnexpectedMessageReceived") class UnexpectedMessageReceived(DashScopeException): pass -@_legacy_error("UnsupportedData") class UnsupportedData(DashScopeException): pass -@_legacy_error("AssistantError") class AssistantError(DashScopeException): def __init__(self, **kwargs): self.message = None @@ -135,7 +88,6 @@ def __str__(self): # for server send generation or inference error. -@_legacy_error("RequestFailure") class RequestFailure(DashScopeException): def __init__( self, @@ -159,46 +111,37 @@ def __str__(self): return msg -@_legacy_error("UnknownMessageReceived") class UnknownMessageReceived(DashScopeException): pass -@_legacy_error("InputDataRequired") class InputDataRequired(DashScopeException): pass -@_legacy_error("InputRequired") class InputRequired(DashScopeException): pass -@_legacy_error("UnsupportedDataType") class UnsupportedDataType(DashScopeException): pass -@_legacy_error("ServiceUnavailableError") class ServiceUnavailableError(DashScopeException): pass -@_legacy_error("UnsupportedHTTPMethod") class UnsupportedHTTPMethod(DashScopeException): pass -@_legacy_error("AsyncTaskCreateFailed") class AsyncTaskCreateFailed(DashScopeException): pass -@_legacy_error("UploadFileException") class UploadFileException(DashScopeException): pass -@_legacy_error("TimeoutException") class TimeoutException(DashScopeException): pass diff --git a/dashscope/legacy_1x/finetune/agentic_rl.py b/dashscope/legacy_1x/finetune/agentic_rl.py index 804adb43..70830df8 100644 --- a/dashscope/legacy_1x/finetune/agentic_rl.py +++ b/dashscope/legacy_1x/finetune/agentic_rl.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. """Compatibility shim plus the 1.x surface; the implementation moved to -``dashscope.finetune.agentic_rl``. +``dashscope.applications.finetune.agentic_rl``. The agentic-RL workflow is owned by the 2.0 SDK, while the ``dashscope`` CLI and acli skill docs use this 1.x import path. @@ -13,9 +13,11 @@ from this path and a ``SubmitResult`` from the 2.0 one; the payload and the POST are built once, in the 2.0 class. -Import from ``dashscope.finetune.agentic_rl`` in new code. +New 2.0 code reaches the workflow through ``client.rl``. """ +import functools +import inspect from typing import Dict, List, Optional, Union from dashscope.legacy_1x.finetune.customize_types import ( @@ -25,12 +27,12 @@ FineTuneList, ) from dashscope.legacy_1x.finetune.finetunes import FineTunes -from dashscope.finetune.agentic_rl import AgenticRL as _AgenticRLBase -from dashscope.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.applications.finetune.agentic_rl import AgenticRL as _AgenticRLBase +from dashscope.applications.finetune.agentic_rl import SubmitOutput, SubmitResult # Re-export every name this module historically carried. # pylint: disable-next=unused-import -from dashscope.finetune.reinforcement import ( # noqa: F401 +from dashscope.applications.finetune.reinforcement import ( # noqa: F401 DASHSCOPE_HTTP_BASE_URL, AgenticRLFunctionComponent, AgenticRLTuning, @@ -54,7 +56,14 @@ ) # pylint: disable-next=unused-import -from dashscope.finetune.reinforcement.common.errors import ( # noqa: F401 +from dashscope.applications.finetune.reinforcement.common.errors import ( + AgenticRLError as _CoreAgenticRLError, +) +from dashscope.legacy_1x.finetune.reinforcement.common import ( + errors as _legacy_errors, +) +from dashscope.legacy_1x.finetune.reinforcement.common.errors import ( # noqa: F401 + AgenticRLError, DatasetsError, InstanceQueryError, RegistrationError, @@ -63,6 +72,53 @@ ValueErrorWithCode, ) + +def _to_legacy_error(exc: _CoreAgenticRLError) -> AgenticRLError: + """Rehydrate a core v2 error as the historical 1.x class. + + Bypassing ``__init__`` avoids applying the old message prefixes twice; + the shared core already populated the complete compatibility attributes. + """ + legacy_cls = getattr( + _legacy_errors, + type(exc).__name__, + AgenticRLError, + ) + if not isinstance(legacy_cls, type) or not issubclass( + legacy_cls, + BaseException, + ): + legacy_cls = AgenticRLError + legacy = legacy_cls.__new__(legacy_cls) + Exception.__init__(legacy, *exc.args) + legacy.__dict__.update(exc.__dict__) + legacy.message = getattr(exc, "message", str(exc)) + legacy.error_code = getattr(exc, "error_code", 1000) + return legacy + + +def _translate_sync(method): + @functools.wraps(method) + def wrapped(*args, **kwargs): + try: + return method(*args, **kwargs) + except _CoreAgenticRLError as exc: + raise _to_legacy_error(exc) from exc + + return wrapped + + +def _translate_async(method): + @functools.wraps(method) + async def wrapped(*args, **kwargs): + try: + return await method(*args, **kwargs) + except _CoreAgenticRLError as exc: + raise _to_legacy_error(exc) from exc + + return wrapped + + __all__ = [ "AgenticRL", "DASHSCOPE_HTTP_BASE_URL", @@ -85,6 +141,20 @@ class AgenticRL(_AgenticRLBase): copies of every job verb. """ + def __init__(self, api_key: str = None): + # Keep the historical patch point and old error type. The core class + # uses the same AgenticRLTuning implementation, so no business logic + # is duplicated here. + super(_AgenticRLBase, self).__init__() + try: + set_api_key(api_key) + except Exception as exc: + raise ValueErrorWithCode( + "Invalid API key configuration", + error_code=3001, + ) from exc + self._api_key = api_key + def submit_job( # type: ignore[override] self, model: Optional[str] = None, @@ -220,3 +290,31 @@ def logs( workspace=workspace, **kwargs, ) + + +# Every workflow entry point crosses the facade boundary here. The shared core +# raises the 2.0 family; the historical class translates it back to the exact +# 1.x exception class before it reaches user code. functools.wraps preserves +# the published signatures for inspect/help tooling. +for _method_name in ("init", "register_functions", "upload_datasets", "run"): + _method = getattr(_AgenticRLBase, _method_name) + _wrapper = ( + _translate_async(_method) + if inspect.iscoroutinefunction(_method) + else _translate_sync(_method) + ) + setattr(AgenticRL, _method_name, _wrapper) + +AgenticRL.submit_job = _translate_sync(AgenticRL.submit_job) + +for _method_name in ( + "test_functions", + "query_function_instance_logs", + "query_all_function_instance_logs", + "delete_function_instance", +): + _descriptor = _AgenticRLBase.__dict__[_method_name] + _method = _descriptor.__func__ + setattr(AgenticRL, _method_name, classmethod(_translate_async(_method))) + +del _descriptor, _method, _method_name, _wrapper diff --git a/dashscope/legacy_1x/finetune/reinforcement/__init__.py b/dashscope/legacy_1x/finetune/reinforcement/__init__.py new file mode 100644 index 00000000..85947709 --- /dev/null +++ b/dashscope/legacy_1x/finetune/reinforcement/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The historical Agentic RL facade over the shared application core. + +Models, processors and workflow implementation remain single-sourced under +``dashscope.applications``. The 1.x path re-exports those objects but keeps the +original exception classes. +""" + +from dashscope.applications.finetune import reinforcement as _core +from dashscope.legacy_1x.finetune.reinforcement.common import errors as _errors + +_ERROR_NAMES = { + "AgenticRLError", + "BaseConnectionError", + "BasePermissionError", + "ConfigurationError", + "DatasetsError", + "DeploymentError", + "FunctionLayerError", + "FunctionLoadError", + "IOErrorWithCode", + "InputError", + "InstanceQueryError", + "InstanceWarmupError", + "OSSConnectionError", + "OSSUploadError", + "OutputError", + "RegistrationError", + "RuntimeErrorWithCode", + "ValidationError", + "ValueErrorWithCode", +} + +# Avoid resolving the lazy ``app`` attribute while the AgenticRL facade is +# importing: it imports the legacy CLI, which imports AgenticRL back. +for _name in _core.__all__: + if _name not in _ERROR_NAMES and _name != "app": + globals()[_name] = getattr(_core, _name) +for _name in _ERROR_NAMES: + globals()[_name] = getattr(_errors, _name) + +__all__ = list(_core.__all__) + + +def __getattr__(name): + if name == "app": + from dashscope.legacy_1x.cli.agentic_rl import app + + return app + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/dashscope/legacy_1x/finetune/reinforcement/common/__init__.py b/dashscope/legacy_1x/finetune/reinforcement/common/__init__.py new file mode 100644 index 00000000..4e32329b --- /dev/null +++ b/dashscope/legacy_1x/finetune/reinforcement/common/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Historical common namespace; only errors are version-specific.""" + +import importlib + +from dashscope.legacy_1x.finetune.reinforcement.common import errors + +__all__ = ["errors"] + + +def __getattr__(name): + if name == "errors": + return errors + try: + module = importlib.import_module( + f"dashscope.applications.finetune.reinforcement.common.{name}", + ) + except ModuleNotFoundError as exc: + raise AttributeError(name) from exc + globals()[name] = module + return module diff --git a/dashscope/legacy_1x/finetune/reinforcement/common/errors.py b/dashscope/legacy_1x/finetune/reinforcement/common/errors.py new file mode 100644 index 00000000..fdfdacfe --- /dev/null +++ b/dashscope/legacy_1x/finetune/reinforcement/common/errors.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The unchanged 1.x Agentic RL exception classes.""" + +from dashscope.applications.finetune.reinforcement.common._legacy_errors import ( + AgenticRLError, + BaseConnectionError, + BasePermissionError, + ConfigurationError, + DatasetsError, + DeploymentError, + FunctionLayerError, + FunctionLoadError, + IOErrorWithCode, + InputError, + InstanceQueryError, + InstanceWarmupError, + OSSConnectionError, + OSSUploadError, + OutputError, + RegistrationError, + RuntimeErrorWithCode, + ValidationError, + ValueErrorWithCode, +) + +__all__ = [ + "AgenticRLError", + "BaseConnectionError", + "BasePermissionError", + "ConfigurationError", + "DatasetsError", + "DeploymentError", + "FunctionLayerError", + "FunctionLoadError", + "IOErrorWithCode", + "InputError", + "InstanceQueryError", + "InstanceWarmupError", + "OSSConnectionError", + "OSSUploadError", + "OutputError", + "RegistrationError", + "RuntimeErrorWithCode", + "ValidationError", + "ValueErrorWithCode", +] + +# Preserve the historical module identity for repr/pickle/introspection even +# though the class bodies are single-sourced beside the shared core. +for _name in __all__: + globals()[_name].__module__ = __name__ +del _name diff --git a/dashscope/tokenizers/__init__.py b/dashscope/legacy_1x/tokenizers/__init__.py similarity index 100% rename from dashscope/tokenizers/__init__.py rename to dashscope/legacy_1x/tokenizers/__init__.py diff --git a/dashscope/tokenizers/qwen.tiktoken b/dashscope/legacy_1x/tokenizers/qwen.tiktoken similarity index 100% rename from dashscope/tokenizers/qwen.tiktoken rename to dashscope/legacy_1x/tokenizers/qwen.tiktoken diff --git a/dashscope/tokenizers/qwen_tokenizer.py b/dashscope/legacy_1x/tokenizers/qwen_tokenizer.py similarity index 100% rename from dashscope/tokenizers/qwen_tokenizer.py rename to dashscope/legacy_1x/tokenizers/qwen_tokenizer.py diff --git a/dashscope/tokenizers/tokenization.py b/dashscope/legacy_1x/tokenizers/tokenization.py similarity index 100% rename from dashscope/tokenizers/tokenization.py rename to dashscope/legacy_1x/tokenizers/tokenization.py diff --git a/dashscope/tokenizers/tokenizer.py b/dashscope/legacy_1x/tokenizers/tokenizer.py similarity index 87% rename from dashscope/tokenizers/tokenizer.py rename to dashscope/legacy_1x/tokenizers/tokenizer.py index e8625bd7..eb8d128a 100644 --- a/dashscope/tokenizers/tokenizer.py +++ b/dashscope/legacy_1x/tokenizers/tokenizer.py @@ -4,6 +4,7 @@ import os from typing import List +from dashscope.legacy_1x.common.error import UnsupportedModel from .qwen_tokenizer import QwenTokenizer from .tokenizer_base import Tokenizer @@ -22,14 +23,14 @@ def get_tokenizer(model: str) -> Tokenizer: model (str): The model name. Raises: - ValueError: Not support model + UnsupportedModel: Not support model Returns: Tokenizer: The `Tokenizer` of the model. """ if model.startswith("qwen"): return QwenTokenizer(_VOCAB_PATH) - raise ValueError( + raise UnsupportedModel( f"Not support model: {model}, currently only support qwen models.", ) diff --git a/dashscope/tokenizers/tokenizer_base.py b/dashscope/legacy_1x/tokenizers/tokenizer_base.py similarity index 100% rename from dashscope/tokenizers/tokenizer_base.py rename to dashscope/legacy_1x/tokenizers/tokenizer_base.py diff --git a/dashscope/resources/reinforcement_learning.py b/dashscope/resources/reinforcement_learning.py index fc31fea8..3a56e444 100644 --- a/dashscope/resources/reinforcement_learning.py +++ b/dashscope/resources/reinforcement_learning.py @@ -3,7 +3,7 @@ """client.rl thin mount layer (design doc M3 3.7). The agentic-RL workflow lives in the 2.0 package -(``dashscope.finetune``); this layer only mounts it on the 2.0 Client. +(``dashscope.applications.finetune``); this layer only mounts it on the 2.0 Client. Job management and workflow steps both raise ``dashscope.errors`` natively, so one ``except DashScopeError`` covers both. @@ -73,10 +73,11 @@ ] # Imported below via the module object so tests can monkeypatch -# ``dashscope.finetune.agentic_rl.AgenticRL`` without touching the -# workflow implementation. -from dashscope.finetune import agentic_rl as _agentic_rl # noqa: E402 -from dashscope.finetune import reinforcement as _reinforcement # noqa: E402 +# the shared application core without traversing the historical 1.x alias. +from dashscope.applications.finetune import agentic_rl as _agentic_rl # noqa: E402 +from dashscope.applications.finetune import ( # noqa: E402 + reinforcement as _reinforcement, +) class RegisteredFunctions(BaseModel): diff --git a/tests/legacy_1x/unit/test_import_deprecation.py b/tests/legacy_1x/unit/test_import_deprecation.py index cde7beab..c5210ea8 100644 --- a/tests/legacy_1x/unit/test_import_deprecation.py +++ b/tests/legacy_1x/unit/test_import_deprecation.py @@ -41,6 +41,17 @@ def test_plain_import_emits_no_deprecation_warning(): assert result.returncode == 0, result.stderr +def test_legacy_errors_keep_their_runtime_behavior_under_warning_errors(): + result = _run( + "from dashscope.common.error import InvalidParameter; " + "raise InvalidParameter('bad')", + error_on_deprecation=True, + ) + assert result.returncode != 0 + assert "InvalidParameter: bad" in result.stderr + assert "PendingDeprecationWarning" not in result.stderr + + def test_non_assistants_usage_emits_no_deprecation_warning(): result = _run( "import dashscope; dashscope.Generation; dashscope.TextEmbedding", diff --git a/tests/legacy_1x/unit/test_layer_decoupling.py b/tests/legacy_1x/unit/test_layer_decoupling.py index dcb59e75..c884a1f6 100644 --- a/tests/legacy_1x/unit/test_layer_decoupling.py +++ b/tests/legacy_1x/unit/test_layer_decoupling.py @@ -32,9 +32,6 @@ _DASHSCOPE / "types", _DASHSCOPE / "applications" / "agentstudio", _DASHSCOPE / "applications" / "finetune" / "reinforcement", - _DASHSCOPE / "tokenizers" / "tokenizer.py", - _DASHSCOPE / "tokenizers" / "tokenizer_base.py", - _DASHSCOPE / "tokenizers" / "qwen_tokenizer.py", ) _VERSION_FILE = "dashscope/version.py" @@ -50,7 +47,6 @@ "resources", "transport", "types", - "tokenizers", }, ) diff --git a/tests/smoke/test_full_surface.py b/tests/smoke/test_full_surface.py index 350fa665..b7ab6b24 100644 --- a/tests/smoke/test_full_surface.py +++ b/tests/smoke/test_full_surface.py @@ -600,7 +600,7 @@ def handler(request): assert client.rl.jobs.logs("ft-1").content == "ok" app = client.apps.completions.create(app_id="app-1", prompt="hi") assert app.text == "app says hi" - from dashscope.agentstudio import Client as AgentStudioClient + from dashscope.applications.agentstudio import Client as AgentStudioClient mounted = client.agentstudio assert isinstance(mounted, AgentStudioClient) diff --git a/tests/test_agentstudio_errors.py b/tests/test_agentstudio_errors.py index be555d6b..529ec39b 100644 --- a/tests/test_agentstudio_errors.py +++ b/tests/test_agentstudio_errors.py @@ -17,11 +17,11 @@ import httpx import pytest -from dashscope.agentstudio import AsyncClient as AsAgentStudioClient -from dashscope.agentstudio import Client as AgentStudioClient -from dashscope.agentstudio import exceptions as as_exc -from dashscope.agentstudio.pagination import AsyncCursorPage, CursorPage -from dashscope.agentstudio.types import ServerEvent, Session +from dashscope.applications.agentstudio import AsyncClient as AsAgentStudioClient +from dashscope.applications.agentstudio import Client as AgentStudioClient +from dashscope.applications.agentstudio import exceptions as as_exc +from dashscope.applications.agentstudio.pagination import AsyncCursorPage, CursorPage +from dashscope.applications.agentstudio.types import ServerEvent, Session from dashscope import errors as v2_errors BASE_URL = "https://as.test.local" diff --git a/tests/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py index 5c5e4525..541e5f6e 100644 --- a/tests/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Tests for AgentStudio's root implementation and public contracts. - -The public package owns the only implementation. Deep imports, shared -validation, unified errors and SDK identity must keep their existing behavior. -""" +"""Tests for AgentStudio's shared core and two public facades.""" from __future__ import annotations @@ -14,14 +10,20 @@ import pytest import dashscope.agentstudio as impl +from dashscope.applications import agentstudio as v2_impl from dashscope import errors as sdk_errors from dashscope._shared import _sdk_ident, _validation from dashscope.agentstudio.transport import build_headers +from dashscope.applications.agentstudio.transport import ( + build_headers as build_v2_headers, +) from dashscope.legacy_1x.common import error as legacy_error from dashscope.legacy_1x.common import env as legacy_env from dashscope.legacy_1x.common import utils as legacy_utils -from dashscope.agentstudio import client as as_client +from dashscope.agentstudio import client as legacy_client from dashscope.agentstudio import exceptions as impl_exc +from dashscope.applications.agentstudio import client as as_client +from dashscope.applications.agentstudio import exceptions as v2_exc BASE_URL = "https://as.test.local" @@ -84,27 +86,31 @@ def test_public_names_are_exported(): assert getattr(impl, name) is not None, name -def test_client_classes_have_root_module_ownership(): - assert ( - impl.Client.__module__ == "dashscope.applications.agentstudio.client" - ) - assert impl.AsyncClient.__module__ == ( - "dashscope.applications.agentstudio.client" - ) +def test_client_classes_are_versioned_facades(): + assert impl.Client.__module__ == "dashscope.legacy_1x.agentstudio.client" + assert impl.AsyncClient.__module__ == "dashscope.legacy_1x.agentstudio.client" + assert v2_impl.Client.__module__ == "dashscope.applications.agentstudio.client" + assert v2_impl.AsyncClient.__module__ == "dashscope.applications.agentstudio.client" -def test_exception_aliases_use_root_errors(): - assert impl_exc.AgentStudioError is sdk_errors.DashScopeError - assert impl_exc.AuthenticationError is sdk_errors.AuthenticationError - assert impl_exc.NotFoundError is sdk_errors.NotFoundError +def test_exception_families_are_versioned(): + assert impl_exc.AgentStudioError is not sdk_errors.DashScopeError + assert issubclass(impl_exc.AgentStudioError, legacy_error.DashScopeException) + assert v2_exc.AgentStudioError is sdk_errors.DashScopeError + assert v2_exc.AuthenticationError is sdk_errors.AuthenticationError @pytest.mark.parametrize("name", SUBMODULES) -def test_deep_imports_resolve_to_root_modules(name): +def test_deep_imports_resolve_to_facade_or_shared_core(name): module_name = f"dashscope.agentstudio.{name}" - canonical_name = f"dashscope.applications.agentstudio.{name}" + facade_modules = {"client", "exceptions", "streaming", "transport"} + root = ( + "dashscope.legacy_1x.agentstudio" + if name in facade_modules + else "dashscope.applications.agentstudio" + ) assert importlib.import_module(module_name) is importlib.import_module( - canonical_name, + f"{root}.{name}", ) @@ -136,16 +142,9 @@ def test_deep_import_from_syntax_resolves(): # --------------------------------------------------------------------------- -def test_agentstudio_uses_the_unified_error_base(): - assert impl_exc.AgentStudioError is sdk_errors.DashScopeError - # The unified 2.0 base roots into the shared DashScopeException, so a - # single ``except DashScopeException`` catches AgentStudio errors too -- - # the invariant documented in ``dashscope/_shared/_errors.py`` and the - # behavior the 1.x ``AgentStudioError(DashScopeException)`` had. - assert issubclass( - impl_exc.AgentStudioError, - legacy_error.DashScopeException, - ) +def test_both_agentstudio_error_bases_remain_catchable(): + assert issubclass(impl_exc.AgentStudioError, legacy_error.DashScopeException) + assert issubclass(v2_exc.AgentStudioError, sdk_errors.DashScopeError) def test_validators_are_shared_and_still_reject_host_breakouts(): @@ -229,10 +228,17 @@ def test_session_id_is_shared_with_the_v2_transport(): # --------------------------------------------------------------------------- -# api key resolution no longer reads the 1.x ``dashscope.api_key`` global +# 2.0 API-key resolution is independent of the historical facade # --------------------------------------------------------------------------- +def test_legacy_client_uses_the_historical_key_resolver(monkeypatch): + monkeypatch.setattr(legacy_client, "get_default_api_key", lambda: "sk-old") + client = impl.Client(base_url=BASE_URL) + assert client.transport.api_key == "sk-old" + client.close() + + @pytest.fixture def _no_key_anywhere(monkeypatch): monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) @@ -260,10 +266,10 @@ def test_missing_api_key_resolves_to_none(_no_key_anywhere): def test_missing_api_key_surfaces_at_request_time(_no_key_anywhere): """Construction succeeds; the transport raises the native error.""" - client = impl.Client(base_url=BASE_URL) + client = v2_impl.Client(base_url=BASE_URL) assert client.transport.api_key is None - with pytest.raises(impl_exc.AuthenticationError): - build_headers( + with pytest.raises(v2_exc.AuthenticationError): + build_v2_headers( api_key=client.transport.api_key, uid=None, user_agent="ua", diff --git a/tests/test_apps.py b/tests/test_apps.py index 587e6566..9c29fe94 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -183,7 +183,7 @@ def handler(request): def test_agentstudio_mounted_lazily(): - from dashscope.agentstudio import Client as AgentStudioClient + from dashscope.applications.agentstudio import Client as AgentStudioClient client = _make_client( lambda request: httpx.Response(200, json={}), @@ -194,32 +194,26 @@ def test_agentstudio_mounted_lazily(): assert client.agentstudio is mounted assert hasattr(mounted, "agents") assert hasattr(mounted, "sessions") - # No region configured -> the mount keeps the cn-beijing default. - assert mounted.transport.base_url == ( - "https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio" - ) + # An explicit main base URL is propagated to the AgentStudio service root. + assert mounted.transport.base_url == f"{BASE_URL}/agentstudio" mounted.close() def test_agentstudio_mount_follows_region(): - # agentstudio resolves its own base_url from workspace + region (the - # v2 base_url escape hatch does not propagate), so the v2 region must - # reach the mount instead of silently defaulting to cn-beijing. + # Explicit base_url wins over workspace/region, as on every v2 resource. client = _make_client( lambda request: httpx.Response(200, json={}), workspace="ws-1", region="ap-southeast-1", ) mounted = client.agentstudio - assert mounted.transport.base_url == ( - "https://ws-1.ap-southeast-1.maas.aliyuncs.com/api/v1/agentstudio" - ) + assert mounted.transport.base_url == f"{BASE_URL}/agentstudio" mounted.close() @pytest.mark.asyncio async def test_async_agentstudio_mount_follows_region(): - from dashscope.agentstudio import AsyncClient as AsAgentStudioClient + from dashscope.applications.agentstudio import AsyncClient as AsAgentStudioClient client = _make_async_client( lambda request: httpx.Response(200, json={}), @@ -228,12 +222,25 @@ async def test_async_agentstudio_mount_follows_region(): ) mounted = client.agentstudio assert type(mounted) is AsAgentStudioClient - assert mounted.transport.base_url == ( - "https://ws-1.eu-central-1.maas.aliyuncs.com/api/v1/agentstudio" - ) + assert mounted.transport.base_url == f"{BASE_URL}/agentstudio" await mounted.aclose() +def test_agentstudio_mount_shares_owned_pool_and_client_options(): + client = Client( + api_key="sk-test", + base_url=BASE_URL, + timeout=9.0, + max_retries=4, + ) + mounted = client.agentstudio + assert mounted.transport._client is client.transport.http_client + assert mounted.transport.timeout == 9.0 + assert mounted.transport.max_retries == 4 + client.close() + assert mounted.transport.is_closed is True + + @pytest.mark.asyncio async def test_async_create_non_streaming(): def handler(request): @@ -306,7 +313,7 @@ def handler(request): assert chunks[-1].code == "Throttling" -def test_close_closes_mounted_agentstudio(): +def test_close_preserves_caller_owned_mounted_pool(): client = _make_client( lambda request: httpx.Response(200, json={}), workspace="ws-1", @@ -314,12 +321,13 @@ def test_close_closes_mounted_agentstudio(): mounted = client.agentstudio assert mounted.transport.is_closed is False client.close() - assert mounted.transport.is_closed is True + assert mounted.transport.is_closed is False assert client._agentstudio is None # pylint: disable=protected-access + client.transport.http_client.close() @pytest.mark.asyncio -async def test_aclose_closes_mounted_agentstudio(): +async def test_aclose_preserves_caller_owned_mounted_pool(): client = _make_async_client( lambda request: httpx.Response(200, json={}), workspace="ws-1", @@ -327,8 +335,9 @@ async def test_aclose_closes_mounted_agentstudio(): mounted = client.agentstudio assert mounted.transport.is_closed is False await client.aclose() - assert mounted.transport.is_closed is True + assert mounted.transport.is_closed is False assert client._agentstudio is None # pylint: disable=protected-access + await client.transport.http_client.aclose() @pytest.mark.asyncio diff --git a/tests/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py index e904f6b6..c4560be0 100644 --- a/tests/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -2,9 +2,9 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """The finetune RL implementation and its 1.x compatibility surface. -``dashscope.finetune.reinforcement`` and ``dashscope.finetune.agentic_rl`` -own the 2.0 workflow. ``dashscope.legacy_1x.finetune.agentic_rl`` keeps the -1.x return types and job verbs. The tests also pin Function Compute, +``dashscope.applications.finetune`` owns the single workflow core. +The historical ``dashscope.finetune`` paths expose the 1.x facade, while +``client.rl`` exposes the 2.0 facade. The tests also pin Function Compute, ``uvicorn`` / ``python -m``, lazy optional dependencies and the submit wire format. """ @@ -31,12 +31,12 @@ ) from dashscope.version import __version__ from dashscope.errors import SdkError -from dashscope.finetune import _submit -from dashscope.finetune.agentic_rl import AgenticRL as V2AgenticRL -from dashscope.finetune.agentic_rl import SubmitOutput, SubmitResult -from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.finetune.reinforcement import TrainingType -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.applications.finetune import _submit +from dashscope.applications.finetune.agentic_rl import AgenticRL as V2AgenticRL +from dashscope.applications.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.applications.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.applications.finetune.reinforcement import TrainingType +from dashscope.applications.finetune.reinforcement.common.errors import ( _LEGACY_ERROR_CODES, AgenticRLError, RuntimeErrorWithCode, @@ -45,8 +45,8 @@ from dashscope.resources import reinforcement_learning as _rl_mount PUBLIC = "dashscope.finetune.reinforcement" -V2 = PUBLIC -PATHS = [PUBLIC] +V2 = "dashscope.applications.finetune.reinforcement" +PATHS = [PUBLIC, V2] ENVELOPE = { "status_code": 200, @@ -89,7 +89,7 @@ def check_function_names(self): def _stub(cls, api_key="sk-submit"): # ``set_api_key`` writes DASHSCOPE_API_KEY for real; these tests are # about the submit call, not about credential discovery. - with patch("dashscope.finetune.agentic_rl.set_api_key"): + with patch("dashscope.applications.finetune.agentic_rl.set_api_key"): instance = cls(api_key=api_key) instance.tuning = _StubTuning() return instance @@ -314,7 +314,7 @@ def test_submit_falls_back_to_the_environment(monkeypatch, post_calls): def test_current_submit_returns_submit_result_and_feeds_to_job(): with patch( - "dashscope.finetune._submit.post_submit", + "dashscope.applications.finetune._submit.post_submit", return_value=dict(ENVELOPE), ): result = _stub(V2AgenticRL).submit_job() @@ -338,7 +338,7 @@ def test_legacy_submit_returns_the_finetune_dataclass(): from dashscope.legacy_1x.finetune.customize_types import FineTune with patch( - "dashscope.finetune._submit.post_submit", + "dashscope.applications.finetune._submit.post_submit", return_value=dict(ENVELOPE), ): result = _stub(LegacyAgenticRL).submit_job() @@ -360,7 +360,7 @@ def test_a_failed_submit_is_an_envelope_not_an_exception(): "headers": {}, } with patch( - "dashscope.finetune._submit.post_submit", + "dashscope.applications.finetune._submit.post_submit", return_value=dict(failure), ): result = _stub(V2AgenticRL).submit_job() @@ -373,8 +373,8 @@ def test_a_failed_submit_is_an_envelope_not_an_exception(): # -- single implementation ------------------------------------------------ -def test_public_package_contains_the_unique_implementation(): - public = importlib.import_module(PUBLIC) +def test_application_package_contains_the_unique_implementation(): + public = importlib.import_module(V2) package_dir = pathlib.Path(public.__file__).parent assert package_dir.name == "reinforcement" assert package_dir.parent.name == "finetune" @@ -385,18 +385,17 @@ def test_public_package_contains_the_unique_implementation(): def test_the_v2_mount_points_at_the_v2_workflow_modules(): assert _rl_mount._agentic_rl is importlib.import_module( - "dashscope.finetune.agentic_rl", + "dashscope.applications.finetune.agentic_rl", ) assert _rl_mount._reinforcement is importlib.import_module(V2) -def test_the_rl_error_module_uses_the_public_path(): - public_errors = importlib.import_module(f"{PUBLIC}.common.errors") - canonical_errors = importlib.import_module( - "dashscope.applications.finetune.reinforcement.common.errors", - ) - assert public_errors is canonical_errors - assert public_errors.ValueErrorWithCode is ValueErrorWithCode +def test_the_two_rl_error_modules_are_versioned(): + legacy_errors = importlib.import_module(f"{PUBLIC}.common.errors") + canonical_errors = importlib.import_module(f"{V2}.common.errors") + assert legacy_errors is not canonical_errors + assert legacy_errors.ValueErrorWithCode is not ValueErrorWithCode + assert canonical_errors.ValueErrorWithCode is ValueErrorWithCode @pytest.mark.parametrize( diff --git a/tests/test_legacy_import_aliases.py b/tests/test_legacy_import_aliases.py index e8a4e882..4445e6b1 100644 --- a/tests/test_legacy_import_aliases.py +++ b/tests/test_legacy_import_aliases.py @@ -45,7 +45,7 @@ def test_all_legacy_module_aliases_in_clean_process(): packages = ( 'aigc', 'api_entities', 'app', 'assistants', 'audio', 'cli', 'common', 'embeddings', 'io', 'multimodal', 'nlp', 'protocol', - 'rerank', 'threads', 'utils', + 'rerank', 'threads', 'tokenizers', 'utils', ) modules = ('files', 'model', 'models') for name in packages + modules: @@ -117,31 +117,32 @@ def test_client_and_legacy_base_api_coexist(): @pytest.mark.parametrize( ("old_name", "canonical_name"), [ - ("dashscope.agentstudio", "dashscope.applications.agentstudio"), + ("dashscope.agentstudio", "dashscope.legacy_1x.agentstudio"), ( "dashscope.agentstudio.types.models", "dashscope.applications.agentstudio.types.models", ), ( "dashscope.finetune.agentic_rl", - "dashscope.applications.finetune.agentic_rl", + "dashscope.legacy_1x.finetune.agentic_rl", ), ( "dashscope.finetune.reinforcement.common.errors", - "dashscope.applications.finetune.reinforcement.common.errors", + "dashscope.legacy_1x.finetune.reinforcement.common.errors", ), ], ) -def test_application_public_paths_are_aliases(old_name, canonical_name): +def test_historical_paths_select_the_1x_facades(old_name, canonical_name): assert importlib.import_module(old_name) is importlib.import_module( canonical_name, ) -def test_agentic_rl_current_contract_is_unchanged(): +def test_agentic_rl_historical_contract_is_unchanged(): module = importlib.import_module("dashscope.finetune.agentic_rl") - assert not hasattr(module.AgenticRL, "list") - assert module.SubmitResult is not None + for verb in ("cancel", "list", "get", "delete", "logs"): + assert hasattr(module.AgenticRL, verb) + assert module.AgenticRL.__module__.startswith("dashscope.legacy_1x") def test_plain_import_preserves_cli_and_assistants_laziness(): diff --git a/tests/test_package_root.py b/tests/test_package_root.py index 005ea28c..7bca4b2a 100644 --- a/tests/test_package_root.py +++ b/tests/test_package_root.py @@ -65,10 +65,12 @@ def test_legacy_exports_untouched(): assert hasattr(dashscope, "save_api_key") -def test_tokenizers_use_one_root_implementation(): +def test_tokenizers_keep_the_historical_legacy_implementation(): from dashscope.tokenizers import get_tokenizer, list_tokenizers - assert get_tokenizer.__module__ == "dashscope.tokenizers.tokenizer" + assert get_tokenizer.__module__ == ( + "dashscope.legacy_1x.tokenizers.tokenizer" + ) assert dashscope.get_tokenizer is get_tokenizer assert dashscope.list_tokenizers is list_tokenizers for name in ("get_tokenizer", "list_tokenizers"): @@ -95,7 +97,7 @@ def test_root_get_tokenizer_encodes_locally(): assert ids and all(isinstance(i, int) for i in ids) assert tokenizer.decode(ids) == "hello world" assert "qwen-turbo" in dashscope.list_tokenizers() - with pytest.raises(ValueError): + with pytest.raises(dashscope.common.error.UnsupportedModel): dashscope.get_tokenizer("not-a-qwen-model") diff --git a/tests/test_rl.py b/tests/test_rl.py index 6a86b91d..1943a626 100644 --- a/tests/test_rl.py +++ b/tests/test_rl.py @@ -10,8 +10,8 @@ import httpx import pytest -from dashscope.finetune import agentic_rl as agentic_rl_module -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.applications.finetune import agentic_rl as agentic_rl_module +from dashscope.applications.finetune.reinforcement.common.errors import ( AgenticRLError, InstanceQueryError, OSSUploadError, From 1861e2bd6c9a7ca71e843df1bda41eccd037ef4d Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:56:08 +0800 Subject: [PATCH 25/41] fix(transport): harden HTTP client configuration and retries --- .github/workflows/unit_test.yml | 4 +- dashscope/transport/http.py | 304 +++++++++++++----- requirements.txt | 1 + .../legacy_1x/unit/test_packaging_metadata.py | 5 + tests/test_audio_management.py | 4 +- tests/test_audio_tts_http.py | 4 +- tests/test_transport.py | 115 ++++++- 7 files changed, 344 insertions(+), 93 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 56b691d9..9beeb4e9 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.11', '3.12', '3.13' ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] steps: - name: Checkout repository @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.11', '3.12', '3.13' ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] steps: - name: Checkout repository diff --git a/dashscope/transport/http.py b/dashscope/transport/http.py index 2b1cd269..c0425564 100644 --- a/dashscope/transport/http.py +++ b/dashscope/transport/http.py @@ -113,6 +113,24 @@ def _merge_auth_headers( ) +def _request_headers( + defaults: Mapping[str, str], + sdk: Mapping[str, str], + headers: Optional[Mapping[str, str]], + api_key: Optional[str], + workspace: Optional[str], +) -> dict: + """Merge Client defaults, SDK identity and per-call overrides. + + This is deliberately request-scoped so an injected ``http_client`` still + receives the Client's credentials and user agent. Caller headers and + per-call credentials win case-insensitively. + """ + per_call = _merge_auth_headers(headers, api_key, workspace) + configured = _with_sdk_headers(defaults, per_call) + return _with_sdk_headers(sdk, configured) + + def _abs_url(base_url: Optional[str], path: str) -> str: # Per-request base_url override: an absolute URL replaces the # client-level base_url in httpx's URL merging. Used by the @@ -146,10 +164,17 @@ def __init__( limits: Optional[httpx.Limits] = None, ) -> None: self._owns_client = http_client is None + self._base_url = base_url.rstrip("/") + self._default_headers = _build_headers(api_key, workspace, user_agent) + self._default_timeout = _resolve_timeout(timeout, default_timeout) + self._request_timeout = ( + None if http_client is not None and timeout is None + else self._default_timeout + ) self._client = http_client or httpx.Client( - base_url=base_url, - headers=_build_headers(api_key, workspace, user_agent), - timeout=_resolve_timeout(timeout, default_timeout), + base_url=self._base_url, + headers=self._default_headers, + timeout=self._default_timeout, **({"limits": limits} if limits is not None else {}), ) self._max_retries = max(max_retries, 0) @@ -176,7 +201,7 @@ def request( workspace: Optional[str] = None, sdk_module: Optional[str] = None, ) -> httpx.Response: - url = _abs_url(base_url, path) + url = _abs_url(base_url or self._base_url, path) for attempt in range(self._max_retries + 1): try: response = self._client.request( @@ -187,11 +212,16 @@ def request( data=data, files=files, params=params, - headers=_with_sdk_headers( + headers=_request_headers( + self._default_headers, sdk_headers(sdk_module), - _merge_auth_headers(headers, api_key, workspace), + headers, + api_key, + workspace, + ), + **_timeout_kwarg( + self._request_timeout if timeout is None else timeout, ), - **_timeout_kwarg(timeout), ) except httpx.TransportError as exc: if attempt >= self._max_retries: @@ -257,44 +287,98 @@ def open_stream( workspace: Optional[str] = None, sdk_module: Optional[str] = None, ) -> httpx.Response: - # Streaming responses surface errors inside the event stream; - # only the initial status is checked here, no retries. This - # no-retry contract is pinned by tests/test_transport.py and - # relied on by examples/04_real_world_scenarios/ - # production_ready_rag_harness.py, which layers its own stream - # backoff on top -- update that example if retries are added. - # The caller owns the response and must close it. - try: - response = self._client.send( - self._client.build_request( - method, - _abs_url(base_url, path), - json=json, - params=params, - headers=_with_sdk_headers( - sdk_headers(sdk_module), - _merge_auth_headers(headers, api_key, workspace), + """Open a stream, retrying only before a successful stream exists. + + Initial connection failures and retryable HTTP statuses are safe to + replay because no event has reached the caller yet. Once a successful + response is returned, ownership transfers to the caller and the SDK + never retries or replays stream contents. + """ + url = _abs_url(base_url or self._base_url, path) + for attempt in range(self._max_retries + 1): + try: + response = self._client.send( + self._client.build_request( + method, + url, + json=json, + params=params, + headers=_request_headers( + self._default_headers, + sdk_headers(sdk_module), + headers, + api_key, + workspace, + ), + **_timeout_kwarg( + self._request_timeout + if timeout is None + else timeout, + ), ), - **_timeout_kwarg(timeout), - ), - stream=True, + stream=True, + ) + except httpx.TransportError as exc: + if attempt >= self._max_retries: + raise _map_transport_error(exc) from exc + delay = backoff_delay(attempt, None) + logger.info( + "retrying stream %s %s in %.2fs (attempt %d/%d): %s: %s", + method, + path, + delay, + attempt + 1, + self._max_retries, + type(exc).__name__, + exc, + ) + _sleep(delay) + continue + + if should_retry(response.status_code, response.headers) and ( + attempt < self._max_retries + ): + delay = backoff_delay(attempt, response.headers) + request_id = response.headers.get("x-request-id") + try: + response.read() + except httpx.TransportError: + # The response was never exposed to the caller; a broken + # retryable error body does not make replay unsafe. + pass + finally: + response.close() + logger.info( + "retrying stream %s %s in %.2fs (attempt %d/%d): " + "status %d (request_id=%s)", + method, + path, + delay, + attempt + 1, + self._max_retries, + response.status_code, + request_id, + ) + _sleep(delay) + continue + + if response.status_code >= 400: + try: + response.read() + check_response(response) + except httpx.TransportError as exc: + raise _map_transport_error(exc) from exc + finally: + response.close() + logger.debug( + "stream %s %s -> %d (request_id=%s)", + method, + path, + response.status_code, + response.headers.get("x-request-id"), ) - except httpx.TransportError as exc: - raise _map_transport_error(exc) from exc - if response.status_code >= 400: - try: - response.read() - check_response(response) - finally: - response.close() - logger.debug( - "stream %s %s -> %d (request_id=%s)", - method, - path, - response.status_code, - response.headers.get("x-request-id"), - ) - return response + return response + raise AssertionError("unreachable") def close(self) -> None: if self._owns_client: @@ -316,10 +400,17 @@ def __init__( limits: Optional[httpx.Limits] = None, ) -> None: self._owns_client = http_client is None + self._base_url = base_url.rstrip("/") + self._default_headers = _build_headers(api_key, workspace, user_agent) + self._default_timeout = _resolve_timeout(timeout, default_timeout) + self._request_timeout = ( + None if http_client is not None and timeout is None + else self._default_timeout + ) self._client = http_client or httpx.AsyncClient( - base_url=base_url, - headers=_build_headers(api_key, workspace, user_agent), - timeout=_resolve_timeout(timeout, default_timeout), + base_url=self._base_url, + headers=self._default_headers, + timeout=self._default_timeout, **({"limits": limits} if limits is not None else {}), ) self._max_retries = max(max_retries, 0) @@ -346,7 +437,7 @@ async def request( workspace: Optional[str] = None, sdk_module: Optional[str] = None, ) -> httpx.Response: - url = _abs_url(base_url, path) + url = _abs_url(base_url or self._base_url, path) for attempt in range(self._max_retries + 1): try: response = await self._client.request( @@ -357,11 +448,16 @@ async def request( data=data, files=files, params=params, - headers=_with_sdk_headers( + headers=_request_headers( + self._default_headers, sdk_headers(sdk_module), - _merge_auth_headers(headers, api_key, workspace), + headers, + api_key, + workspace, + ), + **_timeout_kwarg( + self._request_timeout if timeout is None else timeout, ), - **_timeout_kwarg(timeout), ) except httpx.TransportError as exc: if attempt >= self._max_retries: @@ -427,36 +523,86 @@ async def open_stream( workspace: Optional[str] = None, sdk_module: Optional[str] = None, ) -> httpx.Response: - # Same no-retry contract as the sync open_stream above. - request = self._client.build_request( - method, - _abs_url(base_url, path), - json=json, - params=params, - headers=_with_sdk_headers( - sdk_headers(sdk_module), - _merge_auth_headers(headers, api_key, workspace), - ), - **_timeout_kwarg(timeout), - ) - try: - response = await self._client.send(request, stream=True) - except httpx.TransportError as exc: - raise _map_transport_error(exc) from exc - if response.status_code >= 400: + """Async twin of :meth:`SyncTransport.open_stream`.""" + url = _abs_url(base_url or self._base_url, path) + for attempt in range(self._max_retries + 1): + request = self._client.build_request( + method, + url, + json=json, + params=params, + headers=_request_headers( + self._default_headers, + sdk_headers(sdk_module), + headers, + api_key, + workspace, + ), + **_timeout_kwarg( + self._request_timeout if timeout is None else timeout, + ), + ) try: - await response.aread() - check_response(response) - finally: - await response.aclose() - logger.debug( - "stream %s %s -> %d (request_id=%s)", - method, - path, - response.status_code, - response.headers.get("x-request-id"), - ) - return response + response = await self._client.send(request, stream=True) + except httpx.TransportError as exc: + if attempt >= self._max_retries: + raise _map_transport_error(exc) from exc + delay = backoff_delay(attempt, None) + logger.info( + "retrying stream %s %s in %.2fs (attempt %d/%d): %s: %s", + method, + path, + delay, + attempt + 1, + self._max_retries, + type(exc).__name__, + exc, + ) + await _asleep(delay) + continue + + if should_retry(response.status_code, response.headers) and ( + attempt < self._max_retries + ): + delay = backoff_delay(attempt, response.headers) + request_id = response.headers.get("x-request-id") + try: + await response.aread() + except httpx.TransportError: + pass + finally: + await response.aclose() + logger.info( + "retrying stream %s %s in %.2fs (attempt %d/%d): " + "status %d (request_id=%s)", + method, + path, + delay, + attempt + 1, + self._max_retries, + response.status_code, + request_id, + ) + await _asleep(delay) + continue + + if response.status_code >= 400: + try: + await response.aread() + check_response(response) + except httpx.TransportError as exc: + raise _map_transport_error(exc) from exc + finally: + await response.aclose() + logger.debug( + "stream %s %s -> %d (request_id=%s)", + method, + path, + response.status_code, + response.headers.get("x-request-id"), + ) + return response + raise AssertionError("unreachable") async def aclose(self) -> None: if self._owns_client: diff --git a/requirements.txt b/requirements.txt index 62c1e161..39e53a6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ cryptography certifi>=2024.7.4 typer>=0.9.0 rich>=13.0.0 +tomli>=1.1.0; python_version < "3.11" httpx>=0.27.0 httpx-sse>=0.4.0 typing_extensions>=4.16.0 diff --git a/tests/legacy_1x/unit/test_packaging_metadata.py b/tests/legacy_1x/unit/test_packaging_metadata.py index 3dfb76cf..64e8ec00 100644 --- a/tests/legacy_1x/unit/test_packaging_metadata.py +++ b/tests/legacy_1x/unit/test_packaging_metadata.py @@ -294,3 +294,8 @@ def test_repo_classifiers_cover_every_python_carrying_real_traffic(): if shares is None: pytest.skip("pypistats unreachable") assert not repo_audit(shares) + + +def test_python_39_config_reader_is_a_core_dependency(): + requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8") + assert 'tomli>=1.1.0; python_version < "3.11"' in requirements diff --git a/tests/test_audio_management.py b/tests/test_audio_management.py index 54106a93..67921c8c 100644 --- a/tests/test_audio_management.py +++ b/tests/test_audio_management.py @@ -908,10 +908,10 @@ def handler(request): assert requests[0].headers.get_list("authorization") == ["Bearer call-key"] assert requests[0].headers.get_list("x-dashscope-workspace") == ["call-ws"] assert requests[0].extensions["timeout"]["read"] == 0 - assert requests[1].headers["authorization"] == "Bearer pool-key" + assert requests[1].headers["authorization"] == "Bearer sk-test" assert requests[1].headers["x-dashscope-workspace"] == "pool-ws" assert ( - requests[1].extensions["timeout"]["read"] == http_client.timeout.read + requests[1].extensions["timeout"]["read"] == client.config.timeout ) assert all(request.url.host == "api.test.local" for request in requests) assert headers == original diff --git a/tests/test_audio_tts_http.py b/tests/test_audio_tts_http.py index 4628fd2c..e7bc3c86 100644 --- a/tests/test_audio_tts_http.py +++ b/tests/test_audio_tts_http.py @@ -847,10 +847,10 @@ def handler(request): assert requests[0].headers.get_list("x-dashscope-workspace") == ["call-ws"] assert requests[0].headers["x-test"] == "yes" assert requests[0].extensions["timeout"]["read"] == 0 - assert requests[1].headers["authorization"] == "Bearer pool-key" + assert requests[1].headers["authorization"] == "Bearer sk-test" assert requests[1].headers["x-dashscope-workspace"] == "pool-ws" assert ( - requests[1].extensions["timeout"]["read"] == http_client.timeout.read + requests[1].extensions["timeout"]["read"] == client.config.timeout ) assert all(request.url.host == "api.test.local" for request in requests) expected_sse = "enable" if method == "stream" else None diff --git a/tests/test_transport.py b/tests/test_transport.py index 8e1c595b..805a0d1c 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -155,14 +155,14 @@ def test_timeout_maps_to_api_timeout_error(no_sleep): assert rec.calls == 3 -def test_open_stream_transport_error_mapped(no_sleep): +def test_open_stream_transport_error_mapped_after_retries(no_sleep): rec = _Recorder() - t = _make_sync(rec, [httpx.ConnectError("boom")], max_retries=2) + errors = [httpx.ConnectError("boom") for _ in range(3)] + t = _make_sync(rec, errors, max_retries=2) with pytest.raises(APIConnectionError) as exc_info: t.open_stream("POST", "/x") assert exc_info.value.code == "sdk.ConnectionError" - # Streams are never retried. - assert rec.calls == 1 + assert rec.calls == 3 _SSE_ERROR_BODY = ( @@ -247,6 +247,57 @@ def test_default_headers(no_sleep): assert sent["user-agent"] == "ua-test" +def test_injected_http_client_still_receives_client_configuration(no_sleep): + rec = _Recorder() + http_client = httpx.Client( + transport=httpx.MockTransport(rec.handler([])), + base_url="https://wrong.test/root", + ) + transport = SyncTransport( + base_url=BASE_URL, + api_key="sk-configured", + workspace="ws-configured", + user_agent="ua-configured", + timeout=17.0, + http_client=http_client, + ) + transport.request("GET", "/x") + request = rec.requests[0] + assert request.url == f"{BASE_URL}/x" + assert request.headers["authorization"] == "Bearer sk-configured" + assert request.headers["x-dashscope-workspace"] == "ws-configured" + assert request.headers["user-agent"] == "ua-configured" + assert request.extensions["timeout"]["read"] == 17.0 + http_client.close() + + +@pytest.mark.asyncio +async def test_injected_async_client_still_receives_client_configuration( + no_sleep, +): + rec = _Recorder() + http_client = httpx.AsyncClient( + transport=httpx.MockTransport(rec.handler([])), + base_url="https://wrong.test/root", + ) + transport = AsyncTransport( + base_url=BASE_URL, + api_key="sk-configured", + workspace="ws-configured", + user_agent="ua-configured", + timeout=19.0, + http_client=http_client, + ) + await transport.request("GET", "/x") + request = rec.requests[0] + assert request.url == f"{BASE_URL}/x" + assert request.headers["authorization"] == "Bearer sk-configured" + assert request.headers["x-dashscope-workspace"] == "ws-configured" + assert request.headers["user-agent"] == "ua-configured" + assert request.extensions["timeout"]["read"] == 19.0 + await http_client.aclose() + + def test_build_headers_and_timeout(): from dashscope.transport.http import _build_headers, _resolve_timeout @@ -635,6 +686,36 @@ async def test_async_call_auth_wins_case_insensitively(method, lowercase): assert second.headers.get_list("x-dashscope-workspace") == ["pool-ws"] +def test_sync_open_stream_retries_initial_status(no_sleep): + rec = _Recorder() + transport = _make_sync(rec, [(503, {}), (200, {})], max_retries=1) + response = transport.open_stream("GET", "/x") + try: + assert response.status_code == 200 + assert rec.calls == 2 + finally: + response.close() + + +@pytest.mark.asyncio +async def test_async_open_stream_retries_initial_status(no_sleep): + rec = _Recorder() + mock = httpx.MockTransport(rec.handler([(503, {}), (200, {})])) + http_client = httpx.AsyncClient(transport=mock, base_url=BASE_URL) + transport = AsyncTransport( + base_url=BASE_URL, + http_client=http_client, + max_retries=1, + ) + response = await transport.open_stream("GET", "/x") + try: + assert response.status_code == 200 + assert rec.calls == 2 + finally: + await response.aclose() + await http_client.aclose() + + class _TrackedSyncStream(httpx.SyncByteStream): def __init__(self, read_error): self.read_error = read_error @@ -687,9 +768,18 @@ def handler(request): transport=httpx.MockTransport(handler), base_url=BASE_URL, ) as http_client: - transport = SyncTransport(base_url=BASE_URL, http_client=http_client) + transport = SyncTransport( + base_url=BASE_URL, + http_client=http_client, + max_retries=0, + ) if status >= 400: - with pytest.raises(read_error or APIStatusError) as caught: + expected = APIStatusError + if read_error is httpx.ReadError: + expected = APIConnectionError + elif read_error is httpx.ReadTimeout: + expected = APITimeoutError + with pytest.raises(expected) as caught: transport.open_stream("GET", "/x") if read_error is None: assert caught.value.status_code == status @@ -737,9 +827,18 @@ def handler(request): transport=httpx.MockTransport(handler), base_url=BASE_URL, ) as http_client: - transport = AsyncTransport(base_url=BASE_URL, http_client=http_client) + transport = AsyncTransport( + base_url=BASE_URL, + http_client=http_client, + max_retries=0, + ) if status >= 400: - with pytest.raises(read_error or APIStatusError) as caught: + expected = APIStatusError + if read_error is httpx.ReadError: + expected = APIConnectionError + elif read_error is httpx.ReadTimeout: + expected = APITimeoutError + with pytest.raises(expected) as caught: await transport.open_stream("GET", "/x") if read_error is None: assert caught.value.status_code == status From 3b5caaa8f15d3aa040a908f8246449a173a279d5 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:56:43 +0800 Subject: [PATCH 26/41] fix(ws): bound task buffers and normalize timeouts --- dashscope/resources/audio/realtime.py | 15 ++++++- dashscope/transport/ws.py | 22 +++++++-- dashscope/transport/ws_connection.py | 64 +++++++++++++++++++++++++-- tests/test_audio.py | 3 +- tests/test_audio_connection.py | 8 +++- tests/test_audio_omni_session.py | 15 +++++-- tests/test_audio_tts_realtime.py | 3 +- tests/test_audio_ws_integration.py | 5 ++- tests/test_ws_connection.py | 7 +-- 9 files changed, 119 insertions(+), 23 deletions(-) diff --git a/dashscope/resources/audio/realtime.py b/dashscope/resources/audio/realtime.py index 2e34ca36..576a3987 100644 --- a/dashscope/resources/audio/realtime.py +++ b/dashscope/resources/audio/realtime.py @@ -13,7 +13,11 @@ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from dashscope._config import WS_REALTIME_PATH, workspace_ws_url -from dashscope.errors import APIConnectionError, error_from_business_code +from dashscope.errors import ( + APIConnectionError, + APITimeoutError, + error_from_business_code, +) from dashscope.transport import default_user_agent from dashscope.transport._ident import sdk_headers from dashscope.transport.ws import Connector, _close_websocket @@ -180,9 +184,13 @@ def connect(self) -> "RealtimeSession": self._ws = self._connector(self._url, self._headers) if self._timeout is not None: self._ws.settimeout(self._timeout) - except BaseException: + except BaseException as exc: self.close() self._reset_state() + from websocket import WebSocketTimeoutException + + if isinstance(exc, WebSocketTimeoutException): + raise APITimeoutError(detail=str(exc)) from exc raise self._reset_state() return self @@ -659,6 +667,7 @@ def recv_event(self) -> Optional[Dict[str, Any]]: """Receive a raw event; unfinished responses cannot end silently.""" from websocket import ( # pylint: disable=import-outside-toplevel WebSocketConnectionClosedException, + WebSocketTimeoutException, ) # Network reads must not hold the lock needed by send_event() or close(). @@ -676,6 +685,8 @@ def recv_event(self) -> Optional[Dict[str, Any]]: raw = ws.recv() except WebSocketConnectionClosedException: raw = None + except WebSocketTimeoutException as exc: + raise APITimeoutError(detail=str(exc)) from exc with self._lock: if generation != self._generation: return None diff --git a/dashscope/transport/ws.py b/dashscope/transport/ws.py index ed497477..3a2201a7 100644 --- a/dashscope/transport/ws.py +++ b/dashscope/transport/ws.py @@ -100,10 +100,19 @@ def _connect( connector: Optional[Connector], timeout: Optional[float], ) -> Any: - if connector is None: - ws = _default_connector(url, headers, timeout=timeout) - else: - ws = connector(url, headers) + try: + if connector is None: + ws = _default_connector(url, headers, timeout=timeout) + else: + ws = connector(url, headers) + except BaseException as exc: + from websocket import WebSocketTimeoutException + + if isinstance(exc, WebSocketTimeoutException): + from dashscope.errors import APITimeoutError + + raise APITimeoutError(detail=str(exc)) from exc + raise try: if timeout is not None: ws.settimeout(timeout) @@ -124,12 +133,17 @@ def _recv_frame(ws: Any) -> Any: """ from websocket import ( # pylint: disable=import-outside-toplevel WebSocketConnectionClosedException, + WebSocketTimeoutException, ) try: return ws.recv() except WebSocketConnectionClosedException: return None + except WebSocketTimeoutException as exc: + from dashscope.errors import APITimeoutError + + raise APITimeoutError(detail=str(exc)) from exc class _EventProcessor: diff --git a/dashscope/transport/ws_connection.py b/dashscope/transport/ws_connection.py index 41628c89..52afb38e 100644 --- a/dashscope/transport/ws_connection.py +++ b/dashscope/transport/ws_connection.py @@ -42,6 +42,7 @@ ) from dashscope.errors import ( APIConnectionError, + APITimeoutError, WsBinaryOwnerConflictError, WsStateError, ) @@ -78,11 +79,14 @@ def __init__( connection: "WsConnection", task_id: str, timeout: Optional[float], + queue_size: int, ) -> None: self._conn = connection self._task_id = task_id self._timeout = timeout - self._queue: "queue.SimpleQueue[_QueueItem]" = queue.SimpleQueue() + self._queue: "queue.Queue[_QueueItem]" = queue.Queue( + maxsize=queue_size, + ) self._request_id = task_id self._lock = threading.Lock() self._run_sent = False @@ -113,7 +117,21 @@ def _enqueue(self, item: _QueueItem) -> None: if not self._closed and self._error is None: if item[0] == _ERROR: self._error = item[1] - self._queue.put(item) + try: + self._queue.put_nowait(item) + except queue.Full: + if item[0] == _ERROR: + # Preserve terminal visibility without allowing an + # unbounded backlog to retain audio forever. + try: + self._queue.get_nowait() + except queue.Empty: + pass + self._queue.put_nowait(item) + else: + raise WsStateError( + detail="websocket task receive queue is full", + ) def _frame(self, action: str, payload: Dict[str, Any]) -> Dict[str, Any]: return { @@ -230,8 +248,8 @@ def recv(self) -> Union[Dict[str, Any], bytes, None]: try: kind, payload = self._queue.get(timeout=self._timeout) except queue.Empty: - raise TimeoutError( - "timed out waiting for a websocket event", + raise APITimeoutError( + detail="timed out waiting for a websocket event", ) from None if kind == _ERROR: # Requeue the terminal marker so subsequent readers also fail. @@ -265,15 +283,24 @@ def __init__( connector: Optional[Connector] = None, timeout: Optional[float] = None, module: Optional[str] = None, + task_queue_size: int = 1024, + ping_interval: Optional[float] = 30.0, ) -> None: if timeout is not None and timeout <= 0: raise ValueError("timeout must be positive") + if task_queue_size <= 0: + raise ValueError("task_queue_size must be positive") + if ping_interval is not None and ping_interval <= 0: + raise ValueError("ping_interval must be positive") self._url = ( url or settings.websocket_base_url or workspace_ws_url(workspace, WS_INFERENCE_PATH, region) ) self._timeout = timeout + self._task_queue_size = task_queue_size + self._ping_interval = ping_interval + self._heartbeat_stop = threading.Event() self._tasks: Dict[str, WsTask] = {} self._lock = threading.Lock() self._send_lock = threading.Lock() @@ -300,6 +327,14 @@ def __init__( daemon=True, ) self._reader.start() + ping = getattr(self._ws, "ping", None) + if self._ping_interval is not None and callable(ping): + self._heartbeat = threading.Thread( + target=self._heartbeat_main, + name="dashscope-ws-heartbeat", + daemon=True, + ) + self._heartbeat.start() except BaseException: self.close() raise @@ -318,6 +353,7 @@ def open_task(self, *, timeout: Optional[float] = None) -> WsTask: self, uuid.uuid4().hex, self._timeout if timeout is None else timeout, + self._task_queue_size, ) self._tasks[task.task_id] = task return task @@ -385,6 +421,18 @@ def _detach(self, task: WsTask) -> None: self._tasks.pop(task.task_id) self._release_binary(task) + def _heartbeat_main(self) -> None: + assert self._ping_interval is not None + while not self._heartbeat_stop.wait(self._ping_interval): + try: + with self._send_lock: + if self._closed: + return + self._ws.ping() + except BaseException as exc: + self._fail(APIConnectionError(str(exc))) + return + def _reader_main(self) -> None: try: while True: @@ -451,6 +499,7 @@ def _fail(self, exc: BaseException) -> None: # just the shutdown echo. return self._closed = True + self._heartbeat_stop.set() self._binary_uplink_owner = None self._binary_downlink_owner = None _close_websocket(self._ws) @@ -472,3 +521,10 @@ def close(self) -> None: and threading.current_thread() is not reader ): reader.join(timeout=2) + heartbeat = getattr(self, "_heartbeat", None) + if ( + heartbeat is not None + and heartbeat.is_alive() + and threading.current_thread() is not heartbeat + ): + heartbeat.join(timeout=2) diff --git a/tests/test_audio.py b/tests/test_audio.py index 6102b1ae..fecfa1a9 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -14,6 +14,7 @@ from dashscope import AsyncClient, Client from dashscope.errors import ( + APITimeoutError, APIConnectionError, DashScopeError, WsStateError, @@ -526,7 +527,7 @@ def recv(self): model="fun-asr-realtime", connector=lambda url, headers: ws, ) - with pytest.raises(WebSocketTimeoutException): + with pytest.raises(APITimeoutError): with rec: next(rec.events()) assert ws.closed diff --git a/tests/test_audio_connection.py b/tests/test_audio_connection.py index 5f8c7003..d7749ec4 100644 --- a/tests/test_audio_connection.py +++ b/tests/test_audio_connection.py @@ -10,7 +10,11 @@ import pytest from dashscope.client import Client -from dashscope.errors import DashScopeError, WsBinaryOwnerConflictError +from dashscope.errors import ( + APITimeoutError, + DashScopeError, + WsBinaryOwnerConflictError, +) BASE_URL = "https://api.test.local/api/v1" WS_URL = "wss://ws.test.local/api-ws/v1/inference" @@ -432,7 +436,7 @@ def send(self, payload, opcode=1): timeout=0.05, ) try: - expected = TimeoutError if reply == "timeout" else DashScopeError + expected = APITimeoutError if reply == "timeout" else DashScopeError with pytest.raises(expected): rec.start() assert not conn._tasks diff --git a/tests/test_audio_omni_session.py b/tests/test_audio_omni_session.py index a49fdf73..5979be0f 100644 --- a/tests/test_audio_omni_session.py +++ b/tests/test_audio_omni_session.py @@ -17,6 +17,7 @@ from dashscope import Client from dashscope.errors import ( + APITimeoutError, APIConnectionError, BadRequestError, RateLimitError, @@ -555,9 +556,9 @@ def create_connection(url, *, header, timeout): monkeypatch.setattr("websocket.create_connection", create_connection) session = client.audio.realtime.connect(model="omni") - with pytest.raises(WebSocketTimeoutException) as caught: + with pytest.raises(APITimeoutError) as caught: session.__enter__() - assert caught.value is failure + assert caught.value.__cause__ is failure with pytest.raises(RuntimeError, match="not open"): session.recv_event() with session: @@ -757,9 +758,15 @@ def test_recv_transport_failure_closes_and_propagates( session.end_session() ws.inbox.append(failure) ws.close_error = OSError("cleanup failed") - with pytest.raises(type(failure)) as caught: + expected = ( + APITimeoutError + if isinstance(failure, WebSocketTimeoutException) + else type(failure) + ) + with pytest.raises(expected) as caught: session.recv_event() - assert caught.value is failure + if expected is not APITimeoutError: + assert caught.value is failure assert ws.close_calls == 1 diff --git a/tests/test_audio_tts_realtime.py b/tests/test_audio_tts_realtime.py index 3955a55b..f1a5fe76 100644 --- a/tests/test_audio_tts_realtime.py +++ b/tests/test_audio_tts_realtime.py @@ -17,6 +17,7 @@ from dashscope import Client from dashscope.errors import ( + APITimeoutError, BadRequestError, DashScopeError, RateLimitError, @@ -504,5 +505,5 @@ def test_realtime_recv_raises_on_transport_error(): model="qwen-omni-turbo-realtime", connector=lambda url, headers: ws, ) as rt: - with pytest.raises(WebSocketTimeoutException): + with pytest.raises(APITimeoutError): rt.recv_event() diff --git a/tests/test_audio_ws_integration.py b/tests/test_audio_ws_integration.py index 881e1ac8..42f9bf6a 100644 --- a/tests/test_audio_ws_integration.py +++ b/tests/test_audio_ws_integration.py @@ -19,6 +19,7 @@ from dashscope import Client from dashscope.errors import ( + APITimeoutError, APIConnectionError, BadRequestError, RateLimitError, @@ -1277,7 +1278,7 @@ async def scenario(ws, request): "failure, expected_error", [ ("invalid-json", json.JSONDecodeError), - ("receive-timeout", websocket.WebSocketTimeoutException), + ("receive-timeout", APITimeoutError), ("tcp-abort", APIConnectionError), ("close-frame", APIConnectionError), ], @@ -1357,7 +1358,7 @@ async def scenario(ws, request): "failure, expected_error", [ ("invalid-json", json.JSONDecodeError), - ("receive-timeout", websocket.WebSocketTimeoutException), + ("receive-timeout", APITimeoutError), ("tcp-abort", APIConnectionError), ], ) diff --git a/tests/test_ws_connection.py b/tests/test_ws_connection.py index daab3a09..9451aa73 100644 --- a/tests/test_ws_connection.py +++ b/tests/test_ws_connection.py @@ -11,6 +11,7 @@ import pytest from dashscope.errors import ( + APITimeoutError, APIConnectionError, DashScopeError, WsBinaryOwnerConflictError, @@ -307,7 +308,7 @@ def test_finish_task_and_cancel_semantics(): def test_recv_timeout_raises(): conn, _ = _make_connection() task = conn.open_task(timeout=0.05) - with pytest.raises(TimeoutError): + with pytest.raises(APITimeoutError): task.recv() conn.close() @@ -770,7 +771,7 @@ def test_task_handshake_timeout_detaches_without_killing_connection( ): conn = bounded_connection() task = conn.open_task(timeout=0.02) - with pytest.raises(TimeoutError): + with pytest.raises(APITimeoutError): task.run_task( task_group="audio", task="asr", @@ -793,7 +794,7 @@ def test_task_timeout_is_isolated_and_explicit_close_releases_owners( _started(conn, second) first.send_binary(b"first") first.claim_binary_downlink() - with pytest.raises(TimeoutError): + with pytest.raises(APITimeoutError): first.recv() first.close() second.send_binary(b"second") From f8d0f3015c898f2e03983fb5c442697ba43df20a Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:57:21 +0800 Subject: [PATCH 27/41] feat(audio): add async HTTP audio resources --- dashscope/resources/audio/__init__.py | 52 ++++++++++++ dashscope/resources/audio/http_tts.py | 115 ++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/dashscope/resources/audio/__init__.py b/dashscope/resources/audio/__init__.py index 9b62ef94..12c79098 100644 --- a/dashscope/resources/audio/__init__.py +++ b/dashscope/resources/audio/__init__.py @@ -10,6 +10,9 @@ ``voices`` / ``voice_design``. """ +import asyncio +from types import SimpleNamespace + from dashscope.resources.audio.asr import ( Asr, AsyncAsr, @@ -27,6 +30,7 @@ Voices, ) from dashscope.resources.audio.http_tts import ( + AsyncHttpTts, HttpSynthesis, HttpSynthesisChunk, HttpSynthesisStream, @@ -43,6 +47,7 @@ __all__ = [ "Asr", "AsyncAsr", + "AsyncHttpTts", "AsyncAudio", "Audio", "DesignedVoice", @@ -69,6 +74,48 @@ ] +class _ThreadBridgeTransport: + def __init__(self, transport, loop) -> None: + self._transport = transport + self._loop = loop + + def request(self, *args, **kwargs): + future = asyncio.run_coroutine_threadsafe( + self._transport.request(*args, **kwargs), + self._loop, + ) + return future.result() + + +class _AsyncResourceProxy: + """Run sync response parsing in a worker while reusing AsyncTransport.""" + + def __init__(self, client, resource_cls) -> None: + self._client = client + self._resource_cls = resource_cls + + def __getattr__(self, name): + async def call(*args, **kwargs): + loop = asyncio.get_running_loop() + bridge_client = SimpleNamespace( + transport=_ThreadBridgeTransport(self._client.transport, loop), + raise_on_error=self._client.raise_on_error, + ) + resource = self._resource_cls(bridge_client) + return await asyncio.to_thread( + getattr(resource, name), + *args, + **kwargs, + ) + + return call + + +class _AsyncTts: + def __init__(self, client) -> None: + self.http = AsyncHttpTts(client) + + class Audio: """Sync audio surface. @@ -99,3 +146,8 @@ class AsyncAudio: def __init__(self, client) -> None: self.asr = AsyncAsr(client) + self.tts = _AsyncTts(client) + self.vocabularies = _AsyncResourceProxy(client, Vocabularies) + self.phrases = _AsyncResourceProxy(client, Phrases) + self.voices = _AsyncResourceProxy(client, Voices) + self.voice_design = _AsyncResourceProxy(client, VoiceDesign) diff --git a/dashscope/resources/audio/http_tts.py b/dashscope/resources/audio/http_tts.py index 867c012f..d8fb764d 100644 --- a/dashscope/resources/audio/http_tts.py +++ b/dashscope/resources/audio/http_tts.py @@ -20,6 +20,7 @@ import json from typing import ( Any, + AsyncIterator, Dict, Generator, Iterator, @@ -331,6 +332,120 @@ def close(self) -> None: self._response.close() +class AsyncHttpSynthesisStream: + """Async typed iterator over HTTP synthesis SSE parts.""" + + def __init__(self, inner: AsyncIterator[HttpSynthesisChunk], response) -> None: + self._inner = inner + self._response = response + + def __aiter__(self): + return self._inner + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + async def aclose(self) -> None: + await self._response.aclose() + + +class AsyncHttpTts: + """Async HTTP TTS using the AsyncClient's own connection pool.""" + + def __init__(self, client) -> None: + self._client = client + + async def synthesize(self, text: str, **kwargs) -> HttpSynthesis: + model = kwargs.pop("model") + voice = kwargs.pop("voice") + response = await self._client.transport.request( + "POST", + _path_for(model), + json=_build_body(text, model=model, voice=voice, **{ + key: kwargs.pop(key, None) + for key in ( + "format", + "sample_rate", + "language_type", + "parameters", + "extra_input", + ) + }), + headers=kwargs.pop("headers", None), + timeout=kwargs.pop("timeout", None), + sdk_module=MODULE, + api_key=kwargs.pop("api_key", None), + workspace=kwargs.pop("workspace", None), + ) + return with_headers( + _synthesis_from_body( + response_body( + response, + raise_on_error=self._client.raise_on_error, + ), + response.headers.get("x-request-id", ""), + ), + response, + ) + + async def stream(self, text: str, **kwargs) -> AsyncHttpSynthesisStream: + model = kwargs.pop("model") + voice = kwargs.pop("voice") + headers = dict(_SSE_HEADER) + headers.update(kwargs.pop("headers", None) or {}) + response = await self._client.transport.open_stream( + "POST", + _path_for(model), + json=_build_body(text, model=model, voice=voice, **{ + key: kwargs.pop(key, None) + for key in ( + "format", + "sample_rate", + "language_type", + "parameters", + "extra_input", + ) + }), + headers=headers, + timeout=kwargs.pop("timeout", None), + sdk_module=MODULE, + api_key=kwargs.pop("api_key", None), + workspace=kwargs.pop("workspace", None), + ) + + async def events(): + decoder = _SSEDecoder() + header_id = response.headers.get("x-request-id", "") + try: + async for line in response.aiter_lines(): + frame = decoder.feed(line) + if frame is None: + continue + is_error, status, data = frame + if is_error: + raise _audio_stream_error( + status, + data, + response.headers, + ) + yield _chunk_from_data(data, header_id) + frame = decoder.feed("") + if frame is not None: + is_error, status, data = frame + if is_error: + raise _audio_stream_error( + status, + data, + response.headers, + ) + yield _chunk_from_data(data, header_id) + finally: + await response.aclose() + + return AsyncHttpSynthesisStream(events(), response) + + class HttpTts: """``client.audio.tts.http`` -- HTTP synthesis for both families.""" From f4c75699e2445918bb8c11caa3479bd137fa377a Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 17:57:54 +0800 Subject: [PATCH 28/41] fix(protocols): preserve Anthropic thinking blocks --- dashscope/protocols/anthropic.py | 61 ++++++++++++++++++++------------ tests/test_protocols.py | 38 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/dashscope/protocols/anthropic.py b/dashscope/protocols/anthropic.py index 913ddde6..ece8f0bd 100644 --- a/dashscope/protocols/anthropic.py +++ b/dashscope/protocols/anthropic.py @@ -162,9 +162,7 @@ def _translate_message(message: Dict[str, Any]) -> Dict[str, Any]: out: Dict[str, Any] = {"role": role} if role == "assistant": blocks: List[Dict[str, Any]] = [] - thinking = _thinking_block(message) - if thinking is not None: - blocks.append(thinking) + blocks.extend(_thinking_blocks(message)) if message.get("tool_calls") or blocks: if isinstance(content, list): # Parts lists keep their non-text parts; collapsing @@ -182,19 +180,26 @@ def _translate_message(message: Dict[str, Any]) -> Dict[str, Any]: return out -def _thinking_block(message: Dict[str, Any]) -> Optional[Dict[str, Any]]: - # Strict Anthropic-compatible endpoints reject thinking blocks - # without a valid signature, and require signed ones replayed - # verbatim on later turns; unsigned reasoning stays dropped. +def _thinking_blocks(message: Dict[str, Any]) -> List[Dict[str, Any]]: + """Return signed thinking blocks exactly as received when available.""" + preserved = message.get("reasoning_blocks") + if preserved is not None: + if not isinstance(preserved, list) or not all( + isinstance(block, dict) for block in preserved + ): + raise ValueError("reasoning_blocks must be a list of objects") + return [dict(block) for block in preserved] thinking = message.get("reasoning_content") signature = message.get("reasoning_signature") if thinking and signature: - return { - "type": "thinking", - "thinking": thinking, - "signature": signature, - } - return None + return [ + { + "type": "thinking", + "thinking": thinking, + "signature": signature, + }, + ] + return [] def _translate_tools( @@ -275,7 +280,13 @@ def _flush_tool_results() -> None: elif isinstance(part, str): system_blocks.append({"type": "text", "text": part}) else: - system_blocks.append({"type": "text", "text": ""}) + part_type = ( + part.get("type") if isinstance(part, dict) else None + ) + raise ValueError( + "Anthropic system messages support text parts only; " + f"got {part_type!r}", + ) else: system_blocks.append( {"type": "text", "text": _text_of(content)}, @@ -632,7 +643,7 @@ def build_chat_request( def parse_completion(self, payload: Dict[str, Any]) -> ChatCompletion: text_parts: List[str] = [] thinking_parts: List[str] = [] - signature_parts: List[str] = [] + thinking_blocks: List[Dict[str, Any]] = [] tool_calls: List[Dict[str, Any]] = [] for block in payload.get("content") or []: if block.get("type") == "text": @@ -644,8 +655,9 @@ def parse_completion(self, payload: Dict[str, Any]) -> ChatCompletion: # text and stays dropped (verbatim replay of it is # unsupported by the flat canonical model). thinking_parts.append(block.get("thinking", "")) - if block.get("signature"): - signature_parts.append(block["signature"]) + thinking_blocks.append(dict(block)) + elif block.get("type") == "redacted_thinking": + thinking_blocks.append(dict(block)) elif block.get("type") == "tool_use": tool_calls.append( { @@ -667,12 +679,15 @@ def parse_completion(self, payload: Dict[str, Any]) -> ChatCompletion: reasoning_content = "".join(thinking_parts) if reasoning_content: message["reasoning_content"] = reasoning_content - # Flat canonical model: a single thinking block round-trips; - # interleaved-thinking responses (several signed blocks) are - # reduced like their text and cannot be replayed faithfully. - reasoning_signature = "".join(signature_parts) - if reasoning_signature: - message["reasoning_signature"] = reasoning_signature + if thinking_blocks: + message["reasoning_blocks"] = thinking_blocks + signed = [ + block + for block in thinking_blocks + if block.get("type") == "thinking" and block.get("signature") + ] + if len(signed) == 1: + message["reasoning_signature"] = signed[0]["signature"] if tool_calls: message["tool_calls"] = tool_calls stop_reason = payload.get("stop_reason") diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 8aeec92e..bac5d0a3 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -1227,6 +1227,44 @@ def test_anthropic_stream_usage_keeps_cache_counters(): assert ended.usage.cache_read_input_tokens == 64 +def test_anthropic_rejects_non_text_system_parts(): + protocol = AnthropicProtocol() + with pytest.raises(ValueError, match="system messages support text"): + protocol.build_chat_request( + model="qwen3-max", + messages=[ + { + "role": "system", + "content": [{"type": "image", "url": "https://x"}], + }, + {"role": "user", "content": "hi"}, + ], + ) + + +def test_anthropic_preserves_interleaved_thinking_blocks_for_replay(): + protocol = AnthropicProtocol() + payload = { + "id": "msg-1", + "content": [ + {"type": "thinking", "thinking": "a", "signature": "sig-a"}, + {"type": "text", "text": "middle"}, + {"type": "thinking", "thinking": "b", "signature": "sig-b"}, + ], + } + message = protocol.parse_completion(payload).choices[0].message + assert message.reasoning_signature is None + assert [b["signature"] for b in message.reasoning_blocks] == [ + "sig-a", + "sig-b", + ] + request = protocol.build_chat_request( + model="qwen3-max", + messages=[{"role": "user", "content": "hi"}, message.model_dump()], + ) + assert request.body["messages"][1]["content"][:2] == message.reasoning_blocks + + def test_anthropic_parse_completion_thinking(): # Parity with the streaming thinking_delta path: thinking blocks # surface as reasoning_content instead of being silently dropped, From 4f48a3de7706227e5fd0b0d5b97c300eb32f6828 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 27 Oct 2026 18:53:30 +0800 Subject: [PATCH 29/41] fix(ci): align legacy aliases and formatting --- dashscope/applications/agentstudio/client.py | 4 +- .../agentstudio/resources/session_events.py | 3 + .../applications/agentstudio/streaming.py | 12 +++- .../applications/agentstudio/transport.py | 4 +- .../finetune/reinforcement/common/errors.py | 10 +++- dashscope/client.py | 8 ++- dashscope/legacy_1x/_shim.py | 1 + dashscope/legacy_1x/agentstudio/client.py | 1 + dashscope/legacy_1x/finetune/agentic_rl.py | 14 +++-- dashscope/protocols/anthropic.py | 4 +- dashscope/resources/audio/http_tts.py | 56 ++++++++++++------- dashscope/resources/reinforcement_learning.py | 4 +- dashscope/transport/http.py | 6 +- dashscope/transport/ws_connection.py | 4 +- tests/test_agentstudio_errors.py | 9 ++- tests/test_agentstudio_shim.py | 24 ++++++-- tests/test_apps.py | 4 +- tests/test_audio_management.py | 4 +- tests/test_audio_tts_http.py | 4 +- tests/test_finetune_rl_shim.py | 9 ++- tests/test_protocols.py | 4 +- 21 files changed, 132 insertions(+), 57 deletions(-) diff --git a/dashscope/applications/agentstudio/client.py b/dashscope/applications/agentstudio/client.py index 368468ca..b90b7771 100644 --- a/dashscope/applications/agentstudio/client.py +++ b/dashscope/applications/agentstudio/client.py @@ -9,7 +9,9 @@ import httpx -from dashscope.applications.agentstudio import exceptions as _default_exceptions +from dashscope.applications.agentstudio import ( + exceptions as _default_exceptions, +) from dashscope._shared._validation import ( validate_region, validate_workspace_id, diff --git a/dashscope/applications/agentstudio/resources/session_events.py b/dashscope/applications/agentstudio/resources/session_events.py index 306377bc..26970ad9 100644 --- a/dashscope/applications/agentstudio/resources/session_events.py +++ b/dashscope/applications/agentstudio/resources/session_events.py @@ -2,6 +2,9 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """SessionEvents and AsyncSessionEvents resource classes.""" +# Facade and resource objects are one internal AgentStudio implementation. +# pylint: disable=protected-access + from __future__ import annotations from typing import ( diff --git a/dashscope/applications/agentstudio/streaming.py b/dashscope/applications/agentstudio/streaming.py index 89bd5d55..d8677f8e 100644 --- a/dashscope/applications/agentstudio/streaming.py +++ b/dashscope/applications/agentstudio/streaming.py @@ -39,7 +39,9 @@ import httpx from httpx_sse import EventSource -from dashscope.applications.agentstudio import exceptions as _default_exceptions +from dashscope.applications.agentstudio import ( + exceptions as _default_exceptions, +) # A child of the "dashscope" logger, so the level and handler installed by # dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via @@ -105,7 +107,9 @@ def __iter__(self) -> Iterator[Dict[str, Any]]: def _iter(self) -> Iterator[Dict[str, Any]]: if self._closed: - raise self.exceptions_module.StreamClosedError("stream already closed") + raise self.exceptions_module.StreamClosedError( + "stream already closed", + ) q: "queue.Queue[Any]" = queue.Queue() def _produce() -> None: @@ -187,7 +191,9 @@ def __aiter__(self) -> AsyncIterator[Dict[str, Any]]: async def _aiter(self) -> AsyncIterator[Dict[str, Any]]: if self._closed: - raise self.exceptions_module.StreamClosedError("stream already closed") + raise self.exceptions_module.StreamClosedError( + "stream already closed", + ) q: "asyncio.Queue[Any]" = asyncio.Queue() async def _produce() -> None: diff --git a/dashscope/applications/agentstudio/transport.py b/dashscope/applications/agentstudio/transport.py index 4ce40bfd..01323f5a 100644 --- a/dashscope/applications/agentstudio/transport.py +++ b/dashscope/applications/agentstudio/transport.py @@ -28,7 +28,9 @@ import httpx from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.applications.agentstudio import exceptions as _default_exceptions +from dashscope.applications.agentstudio import ( + exceptions as _default_exceptions, +) from dashscope.applications.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index 1223e6bf..7f85a63a 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -143,11 +143,17 @@ def __init__( self.operation = operation -class RuntimeErrorWithCode(AgenticRLError, _LegacyRuntimeErrorWithCode): +class RuntimeErrorWithCode( # type: ignore[misc] + AgenticRLError, + _LegacyRuntimeErrorWithCode, +): _SDK_ERROR_NAME = "sdk.agentic_rl.RuntimeErrorWithCode" -class ValueErrorWithCode(AgenticRLError, _LegacyValueErrorWithCode): +class ValueErrorWithCode( # type: ignore[misc] + AgenticRLError, + _LegacyValueErrorWithCode, +): _SDK_ERROR_NAME = "sdk.agentic_rl.ValueErrorWithCode" diff --git a/dashscope/client.py b/dashscope/client.py index cb2888cc..c4b443ee 100644 --- a/dashscope/client.py +++ b/dashscope/client.py @@ -179,7 +179,9 @@ def agentstudio(self): """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.applications.agentstudio import Client as AgentStudioClient + from dashscope.applications.agentstudio import ( + Client as AgentStudioClient, + ) # agentstudio resolves its own base_url from workspace + # region (the v2 base_url does not propagate: the agentstudio @@ -417,7 +419,9 @@ def agentstudio(self): """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.applications.agentstudio import AsyncClient as AsClient + from dashscope.applications.agentstudio import ( + AsyncClient as AsClient, + ) # See Client.agentstudio: agentstudio builds its own base_url # from workspace + region, so forward both. diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index f77295af..62e29a53 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -34,6 +34,7 @@ ) _EXACT_MODULES = { + "dashscope.finetune": "dashscope.legacy_1x.finetune", "dashscope.agentstudio": "dashscope.legacy_1x.agentstudio", "dashscope.agentstudio.client": "dashscope.legacy_1x.agentstudio.client", "dashscope.agentstudio.exceptions": ( diff --git a/dashscope/legacy_1x/agentstudio/client.py b/dashscope/legacy_1x/agentstudio/client.py index 859af516..1ed4ae51 100644 --- a/dashscope/legacy_1x/agentstudio/client.py +++ b/dashscope/legacy_1x/agentstudio/client.py @@ -32,6 +32,7 @@ from dashscope.legacy_1x.common.api_key import get_default_api_key # Historical module-level helpers remain importable from this facade. +# pylint: disable=protected-access _resolve_base_url = _core_client._resolve_base_url _user_agent = _core_client._user_agent diff --git a/dashscope/legacy_1x/finetune/agentic_rl.py b/dashscope/legacy_1x/finetune/agentic_rl.py index 70830df8..74c0f15b 100644 --- a/dashscope/legacy_1x/finetune/agentic_rl.py +++ b/dashscope/legacy_1x/finetune/agentic_rl.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +# pylint: disable=unused-import """Compatibility shim plus the 1.x surface; the implementation moved to ``dashscope.applications.finetune.agentic_rl``. @@ -18,7 +19,7 @@ import functools import inspect -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, cast from dashscope.legacy_1x.finetune.customize_types import ( FineTune, @@ -27,8 +28,13 @@ FineTuneList, ) from dashscope.legacy_1x.finetune.finetunes import FineTunes -from dashscope.applications.finetune.agentic_rl import AgenticRL as _AgenticRLBase -from dashscope.applications.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.applications.finetune.agentic_rl import ( + AgenticRL as _AgenticRLBase, +) +from dashscope.applications.finetune.agentic_rl import ( + SubmitOutput, + SubmitResult, +) # Re-export every name this module historically carried. # pylint: disable-next=unused-import @@ -94,7 +100,7 @@ def _to_legacy_error(exc: _CoreAgenticRLError) -> AgenticRLError: legacy.__dict__.update(exc.__dict__) legacy.message = getattr(exc, "message", str(exc)) legacy.error_code = getattr(exc, "error_code", 1000) - return legacy + return cast(AgenticRLError, legacy) def _translate_sync(method): diff --git a/dashscope/protocols/anthropic.py b/dashscope/protocols/anthropic.py index ece8f0bd..5d69390f 100644 --- a/dashscope/protocols/anthropic.py +++ b/dashscope/protocols/anthropic.py @@ -281,7 +281,9 @@ def _flush_tool_results() -> None: system_blocks.append({"type": "text", "text": part}) else: part_type = ( - part.get("type") if isinstance(part, dict) else None + part.get("type") + if isinstance(part, dict) + else None ) raise ValueError( "Anthropic system messages support text parts only; " diff --git a/dashscope/resources/audio/http_tts.py b/dashscope/resources/audio/http_tts.py index d8fb764d..5752aad3 100644 --- a/dashscope/resources/audio/http_tts.py +++ b/dashscope/resources/audio/http_tts.py @@ -335,7 +335,11 @@ def close(self) -> None: class AsyncHttpSynthesisStream: """Async typed iterator over HTTP synthesis SSE parts.""" - def __init__(self, inner: AsyncIterator[HttpSynthesisChunk], response) -> None: + def __init__( + self, + inner: AsyncIterator[HttpSynthesisChunk], + response, + ) -> None: self._inner = inner self._response = response @@ -362,16 +366,21 @@ async def synthesize(self, text: str, **kwargs) -> HttpSynthesis: response = await self._client.transport.request( "POST", _path_for(model), - json=_build_body(text, model=model, voice=voice, **{ - key: kwargs.pop(key, None) - for key in ( - "format", - "sample_rate", - "language_type", - "parameters", - "extra_input", - ) - }), + json=_build_body( + text, + model=model, + voice=voice, + **{ + key: kwargs.pop(key, None) + for key in ( + "format", + "sample_rate", + "language_type", + "parameters", + "extra_input", + ) + }, + ), headers=kwargs.pop("headers", None), timeout=kwargs.pop("timeout", None), sdk_module=MODULE, @@ -397,16 +406,21 @@ async def stream(self, text: str, **kwargs) -> AsyncHttpSynthesisStream: response = await self._client.transport.open_stream( "POST", _path_for(model), - json=_build_body(text, model=model, voice=voice, **{ - key: kwargs.pop(key, None) - for key in ( - "format", - "sample_rate", - "language_type", - "parameters", - "extra_input", - ) - }), + json=_build_body( + text, + model=model, + voice=voice, + **{ + key: kwargs.pop(key, None) + for key in ( + "format", + "sample_rate", + "language_type", + "parameters", + "extra_input", + ) + }, + ), headers=headers, timeout=kwargs.pop("timeout", None), sdk_module=MODULE, diff --git a/dashscope/resources/reinforcement_learning.py b/dashscope/resources/reinforcement_learning.py index 3a56e444..1d912d2f 100644 --- a/dashscope/resources/reinforcement_learning.py +++ b/dashscope/resources/reinforcement_learning.py @@ -74,7 +74,9 @@ # Imported below via the module object so tests can monkeypatch # the shared application core without traversing the historical 1.x alias. -from dashscope.applications.finetune import agentic_rl as _agentic_rl # noqa: E402 +from dashscope.applications.finetune import ( + agentic_rl as _agentic_rl, +) # noqa: E402 from dashscope.applications.finetune import ( # noqa: E402 reinforcement as _reinforcement, ) diff --git a/dashscope/transport/http.py b/dashscope/transport/http.py index c0425564..2c9164d2 100644 --- a/dashscope/transport/http.py +++ b/dashscope/transport/http.py @@ -168,7 +168,8 @@ def __init__( self._default_headers = _build_headers(api_key, workspace, user_agent) self._default_timeout = _resolve_timeout(timeout, default_timeout) self._request_timeout = ( - None if http_client is not None and timeout is None + None + if http_client is not None and timeout is None else self._default_timeout ) self._client = http_client or httpx.Client( @@ -404,7 +405,8 @@ def __init__( self._default_headers = _build_headers(api_key, workspace, user_agent) self._default_timeout = _resolve_timeout(timeout, default_timeout) self._request_timeout = ( - None if http_client is not None and timeout is None + None + if http_client is not None and timeout is None else self._default_timeout ) self._client = http_client or httpx.AsyncClient( diff --git a/dashscope/transport/ws_connection.py b/dashscope/transport/ws_connection.py index 52afb38e..3c24f1fa 100644 --- a/dashscope/transport/ws_connection.py +++ b/dashscope/transport/ws_connection.py @@ -119,7 +119,7 @@ def _enqueue(self, item: _QueueItem) -> None: self._error = item[1] try: self._queue.put_nowait(item) - except queue.Full: + except queue.Full as exc: if item[0] == _ERROR: # Preserve terminal visibility without allowing an # unbounded backlog to retain audio forever. @@ -131,7 +131,7 @@ def _enqueue(self, item: _QueueItem) -> None: else: raise WsStateError( detail="websocket task receive queue is full", - ) + ) from exc def _frame(self, action: str, payload: Dict[str, Any]) -> Dict[str, Any]: return { diff --git a/tests/test_agentstudio_errors.py b/tests/test_agentstudio_errors.py index 529ec39b..ee9d09f0 100644 --- a/tests/test_agentstudio_errors.py +++ b/tests/test_agentstudio_errors.py @@ -17,10 +17,15 @@ import httpx import pytest -from dashscope.applications.agentstudio import AsyncClient as AsAgentStudioClient +from dashscope.applications.agentstudio import ( + AsyncClient as AsAgentStudioClient, +) from dashscope.applications.agentstudio import Client as AgentStudioClient from dashscope.applications.agentstudio import exceptions as as_exc -from dashscope.applications.agentstudio.pagination import AsyncCursorPage, CursorPage +from dashscope.applications.agentstudio.pagination import ( + AsyncCursorPage, + CursorPage, +) from dashscope.applications.agentstudio.types import ServerEvent, Session from dashscope import errors as v2_errors diff --git a/tests/test_agentstudio_shim.py b/tests/test_agentstudio_shim.py index 541e5f6e..9a2897a3 100644 --- a/tests/test_agentstudio_shim.py +++ b/tests/test_agentstudio_shim.py @@ -88,14 +88,25 @@ def test_public_names_are_exported(): def test_client_classes_are_versioned_facades(): assert impl.Client.__module__ == "dashscope.legacy_1x.agentstudio.client" - assert impl.AsyncClient.__module__ == "dashscope.legacy_1x.agentstudio.client" - assert v2_impl.Client.__module__ == "dashscope.applications.agentstudio.client" - assert v2_impl.AsyncClient.__module__ == "dashscope.applications.agentstudio.client" + assert ( + impl.AsyncClient.__module__ == "dashscope.legacy_1x.agentstudio.client" + ) + assert ( + v2_impl.Client.__module__ + == "dashscope.applications.agentstudio.client" + ) + assert ( + v2_impl.AsyncClient.__module__ + == "dashscope.applications.agentstudio.client" + ) def test_exception_families_are_versioned(): assert impl_exc.AgentStudioError is not sdk_errors.DashScopeError - assert issubclass(impl_exc.AgentStudioError, legacy_error.DashScopeException) + assert issubclass( + impl_exc.AgentStudioError, + legacy_error.DashScopeException, + ) assert v2_exc.AgentStudioError is sdk_errors.DashScopeError assert v2_exc.AuthenticationError is sdk_errors.AuthenticationError @@ -143,7 +154,10 @@ def test_deep_import_from_syntax_resolves(): def test_both_agentstudio_error_bases_remain_catchable(): - assert issubclass(impl_exc.AgentStudioError, legacy_error.DashScopeException) + assert issubclass( + impl_exc.AgentStudioError, + legacy_error.DashScopeException, + ) assert issubclass(v2_exc.AgentStudioError, sdk_errors.DashScopeError) diff --git a/tests/test_apps.py b/tests/test_apps.py index 9c29fe94..3cc6455b 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -213,7 +213,9 @@ def test_agentstudio_mount_follows_region(): @pytest.mark.asyncio async def test_async_agentstudio_mount_follows_region(): - from dashscope.applications.agentstudio import AsyncClient as AsAgentStudioClient + from dashscope.applications.agentstudio import ( + AsyncClient as AsAgentStudioClient, + ) client = _make_async_client( lambda request: httpx.Response(200, json={}), diff --git a/tests/test_audio_management.py b/tests/test_audio_management.py index 67921c8c..86bb6411 100644 --- a/tests/test_audio_management.py +++ b/tests/test_audio_management.py @@ -910,9 +910,7 @@ def handler(request): assert requests[0].extensions["timeout"]["read"] == 0 assert requests[1].headers["authorization"] == "Bearer sk-test" assert requests[1].headers["x-dashscope-workspace"] == "pool-ws" - assert ( - requests[1].extensions["timeout"]["read"] == client.config.timeout - ) + assert requests[1].extensions["timeout"]["read"] == client.config.timeout assert all(request.url.host == "api.test.local" for request in requests) assert headers == original diff --git a/tests/test_audio_tts_http.py b/tests/test_audio_tts_http.py index e7bc3c86..58c80624 100644 --- a/tests/test_audio_tts_http.py +++ b/tests/test_audio_tts_http.py @@ -849,9 +849,7 @@ def handler(request): assert requests[0].extensions["timeout"]["read"] == 0 assert requests[1].headers["authorization"] == "Bearer sk-test" assert requests[1].headers["x-dashscope-workspace"] == "pool-ws" - assert ( - requests[1].extensions["timeout"]["read"] == client.config.timeout - ) + assert requests[1].extensions["timeout"]["read"] == client.config.timeout assert all(request.url.host == "api.test.local" for request in requests) expected_sse = "enable" if method == "stream" else None assert requests[0].headers.get("x-dashscope-sse") == expected_sse diff --git a/tests/test_finetune_rl_shim.py b/tests/test_finetune_rl_shim.py index c4560be0..82328c3f 100644 --- a/tests/test_finetune_rl_shim.py +++ b/tests/test_finetune_rl_shim.py @@ -33,8 +33,13 @@ from dashscope.errors import SdkError from dashscope.applications.finetune import _submit from dashscope.applications.finetune.agentic_rl import AgenticRL as V2AgenticRL -from dashscope.applications.finetune.agentic_rl import SubmitOutput, SubmitResult -from dashscope.applications.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.applications.finetune.agentic_rl import ( + SubmitOutput, + SubmitResult, +) +from dashscope.applications.finetune.reinforcement import ( + DASHSCOPE_HTTP_BASE_URL, +) from dashscope.applications.finetune.reinforcement import TrainingType from dashscope.applications.finetune.reinforcement.common.errors import ( _LEGACY_ERROR_CODES, diff --git a/tests/test_protocols.py b/tests/test_protocols.py index bac5d0a3..ab326107 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -1262,7 +1262,9 @@ def test_anthropic_preserves_interleaved_thinking_blocks_for_replay(): model="qwen3-max", messages=[{"role": "user", "content": "hi"}, message.model_dump()], ) - assert request.body["messages"][1]["content"][:2] == message.reasoning_blocks + assert ( + request.body["messages"][1]["content"][:2] == message.reasoning_blocks + ) def test_anthropic_parse_completion_thinking(): From 2710cc7fdb133582c1a20ae6669b9adf86268fcf Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Wed, 28 Oct 2026 13:29:00 +0800 Subject: [PATCH 30/41] feat(anthropic): support Anthropic streaming thinking blocks and redacted thinking block parsing - Added _thinking field for storing thinking block state - Implemented parsing support for thinking and redacted_thinking block types - Handled thinking block initialization in content_block_start events - Accumulated thinking content and signatures in content_block_delta events - Added _cumulative_thinking_blocks method for cumulative thinking blocks - Updated streaming aggregation logic to preserve thinking blocks and signature info - Implemented logic to extract a single signature from block list - Added cumulative processing of thinking block content and signatures - Fixed reasoning block handling in message merging - Added JSON response error handling in async audio stream tests - Extended queue-full error handling in WebSocket connections --- README.md | 4 +- README_zh.md | 2 - dashscope/protocols/anthropic.py | 63 +++++++++++- dashscope/resources/audio/http_tts.py | 11 ++ dashscope/resources/chat/completions.py | 76 +++++++++++--- dashscope/transport/ws_connection.py | 38 ++++--- tests/test_audio_tts_http.py | 32 +++++- tests/test_protocols.py | 129 ++++++++++++++++++++++++ tests/test_ws_connection.py | 33 ++++++ 9 files changed, 350 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 3476aa64..843c285d 100644 --- a/README.md +++ b/README.md @@ -478,9 +478,7 @@ through `body`. > **Breaking (AgentStudio errors):** `ConflictError` is now an alias of > `APIStatusError` rather than a 409-specific subclass, so `except ConflictError` > catches *every* status error (400/401/404/429/5xx), not just conflicts. Match on -> `err.status_code == 409` if you need the old behaviour. The Agentic RL -> `error_code` is now a read-only property — assigning `err.error_code = …` -> raises `AttributeError`. +> `err.status_code == 409` if you need the old behaviour. > **Breaking (`submit_job` / realtime WebSocket):** `submit_job` forwards extra > keyword arguments straight to the transport, so the 1.x `request_timeout=` is diff --git a/README_zh.md b/README_zh.md index cd34d887..c032eed8 100644 --- a/README_zh.md +++ b/README_zh.md @@ -494,8 +494,6 @@ Agentic RL 的 `*WithCode` 错误 `str()` 渲染也变了 —— 1.x 的 > `APIStatusError` 的别名,而不再是仅对应 409 的子类,因此 > `except ConflictError` 会捕获**所有**状态错误(400/401/404/429/5xx), > 而不仅是冲突。若需要旧行为,请改用 `err.status_code == 409` 判断。 -> Agentic RL 的 `error_code` 现在是只读 property —— 赋值 -> `err.error_code = …` 会抛 `AttributeError`。 > **破坏性变更(`submit_job` / realtime WebSocket):** `submit_job` 会把 > 额外关键字参数直接转发给传输层,因此 1.x 的 `request_timeout=` 改名为 diff --git a/dashscope/protocols/anthropic.py b/dashscope/protocols/anthropic.py index 5d69390f..a9947dea 100644 --- a/dashscope/protocols/anthropic.py +++ b/dashscope/protocols/anthropic.py @@ -390,6 +390,7 @@ class _StreamParser: def __init__(self) -> None: self._tools: Dict[int, Dict[str, Any]] = {} + self._thinking: Dict[int, Dict[str, Any]] = {} self._usage: Dict[str, int] = {} def parse(self, payload: Dict[str, Any]) -> Optional[ChatCompletionChunk]: @@ -406,6 +407,7 @@ def parse(self, payload: Dict[str, Any]) -> Optional[ChatCompletionChunk]: # resets per-message tool state. if payload.get("type") == "message_stop": self._tools = {} + self._thinking = {} return None parsed = handler(payload) if parsed is None: @@ -426,6 +428,7 @@ def parse(self, payload: Dict[str, Any]) -> Optional[ChatCompletionChunk]: def _message_start(self, payload: Dict[str, Any]): message = payload.get("message") or {} self._tools = {} + self._thinking = {} self._usage = {} usage: Optional[Dict[str, Any]] = None start_usage = message.get("usage") or {} @@ -441,7 +444,37 @@ def _message_start(self, payload: Dict[str, Any]): def _block_start(self, payload: Dict[str, Any]): block = payload.get("content_block") or {} - if block.get("type") != "tool_use": + block_type = block.get("type") + index = payload.get("index", 0) + if block_type == "thinking": + # Signature and text arrive via deltas; start an empty block + # so the cumulative snapshot keeps it in index order. + self._thinking[index] = { + "type": "thinking", + "thinking": "", + "signature": "", + } + return None + if block_type == "redacted_thinking": + # Arrives complete in content_block_start (no deltas follow); + # preserve it verbatim so the turn can be replayed faithfully. + self._thinking[index] = dict(block) + return ( + payload.get("id"), + payload.get("model"), + [ + { + "index": 0, + "delta": { + "reasoning_blocks": ( + self._cumulative_thinking_blocks() + ), + }, + }, + ], + None, + ) + if block_type != "tool_use": # Text/audio block starts carry no content yet; skip. return None choices: List[Dict[str, Any]] = [] @@ -463,13 +496,24 @@ def _block_start(self, payload: Dict[str, Any]): def _block_delta(self, payload: Dict[str, Any]): delta_payload = payload.get("delta") or {} delta_type = delta_payload.get("type") + index = payload.get("index", 0) delta: Dict[str, Any] = {} if delta_type == "text_delta": delta["content"] = delta_payload.get("text", "") elif delta_type == "thinking_delta": + block = self._thinking.get(index) + if block is not None and block.get("type") == "thinking": + block["thinking"] += delta_payload.get("thinking", "") delta["reasoning_content"] = delta_payload.get("thinking", "") + if self._thinking: + delta["reasoning_blocks"] = self._cumulative_thinking_blocks() elif delta_type == "signature_delta": + block = self._thinking.get(index) + if block is not None and block.get("type") == "thinking": + block["signature"] = delta_payload.get("signature", "") delta["reasoning_signature"] = delta_payload.get("signature", "") + if self._thinking: + delta["reasoning_blocks"] = self._cumulative_thinking_blocks() elif delta_type == "input_json_delta": tool = self._tools.get(payload.get("index", 0)) if tool is not None: @@ -481,6 +525,23 @@ def _block_delta(self, payload: Dict[str, Any]): choices = [{"index": 0, "delta": delta}] return payload.get("id"), payload.get("model"), choices, None + def _cumulative_thinking_blocks(self) -> List[Dict[str, Any]]: + """Thinking/redacted_thinking blocks accumulated so far, in order.""" + blocks: List[Dict[str, Any]] = [] + for index in sorted(self._thinking): + block = self._thinking[index] + if block.get("type") == "thinking": + blocks.append( + { + "type": "thinking", + "thinking": block.get("thinking", ""), + "signature": block.get("signature", ""), + }, + ) + else: + blocks.append(dict(block)) + return blocks + def _message_delta(self, payload: Dict[str, Any]): choices: List[Dict[str, Any]] = [] usage: Optional[Dict[str, Any]] = None diff --git a/dashscope/resources/audio/http_tts.py b/dashscope/resources/audio/http_tts.py index 5752aad3..002439fe 100644 --- a/dashscope/resources/audio/http_tts.py +++ b/dashscope/resources/audio/http_tts.py @@ -432,6 +432,17 @@ async def events(): decoder = _SSEDecoder() header_id = response.headers.get("x-request-id", "") try: + media_type = response.headers.get("content-type", "") + media_type = media_type.split(";", 1)[0].strip().lower() + if media_type == "application/json" or media_type.endswith( + "+json", + ): + # A 200 JSON envelope is a service failure, not audio + # SSE; mirror _iter_audio_sse so the error surfaces + # instead of yielding zero chunks silently. + await response.aread() + response_body(response) + return async for line in response.aiter_lines(): frame = decoder.feed(line) if frame is None: diff --git a/dashscope/resources/chat/completions.py b/dashscope/resources/chat/completions.py index 69966704..ea14c923 100644 --- a/dashscope/resources/chat/completions.py +++ b/dashscope/resources/chat/completions.py @@ -384,6 +384,27 @@ def _merge_tool_calls( return merged +def _signature_from_blocks(blocks: Any) -> Optional[str]: + """Replay signature only when exactly one signed thinking block exists. + + Mirrors the non-streaming Anthropic invariant: concatenating the + per-block signatures of an interleaved-thinking turn yields a single + invalid signature the API would reject, so emit none in that case. + """ + if not isinstance(blocks, list): + return None + signed = [ + block + for block in blocks + if isinstance(block, dict) + and block.get("type") == "thinking" + and block.get("signature") + ] + if len(signed) == 1: + return signed[0]["signature"] + return None + + def _cumulative_chunk( acc: Dict[int, Dict[str, Any]], chunk: ChatCompletionChunk, @@ -398,6 +419,7 @@ def _cumulative_chunk( "content": "", "reasoning": "", "signature": "", + "reasoning_blocks": None, "tool_calls": None, "logprobs": None, }, @@ -411,6 +433,10 @@ def _cumulative_chunk( slot["reasoning"] += delta.reasoning_content if delta.reasoning_signature: slot["signature"] += delta.reasoning_signature + blocks = (delta.model_extra or {}).get("reasoning_blocks") + if blocks is not None: + # Anthropic emits a cumulative snapshot; keep the latest. + slot["reasoning_blocks"] = blocks if delta.tool_calls: slot["tool_calls"] = _merge_tool_calls( slot["tool_calls"], @@ -423,20 +449,27 @@ def _cumulative_chunk( slot["logprobs"], (choice.model_extra or {}).get("logprobs"), ) + # When verbatim thinking blocks are present, derive the replay + # signature from them (single-signed-block rule) instead of the + # concatenated per-block fragments, which would be invalid. + signature = slot["signature"] or None + if slot["reasoning_blocks"] is not None: + signature = _signature_from_blocks(slot["reasoning_blocks"]) # model_copy keeps provider extras the rebuild would otherwise # drop (delta-level omni audio); accumulated logprobs override # the current frame's fragment. - update: Dict[str, Any] = { - "delta": choice.delta.model_copy( - update={ - "role": slot["role"], - "content": slot["content"] or None, - "reasoning_content": slot["reasoning"] or None, - "reasoning_signature": slot["signature"] or None, - "tool_calls": slot["tool_calls"], - }, - ), - } + new_delta = choice.delta.model_copy( + update={ + "role": slot["role"], + "content": slot["content"] or None, + "reasoning_content": slot["reasoning"] or None, + "reasoning_signature": signature, + "tool_calls": slot["tool_calls"], + }, + ) + if slot["reasoning_blocks"] is not None: + new_delta.reasoning_blocks = slot["reasoning_blocks"] + update: Dict[str, Any] = {"delta": new_delta} if slot["logprobs"] is not None: update["logprobs"] = slot["logprobs"] choices.append(choice.model_copy(update=update)) @@ -523,6 +556,7 @@ def _aggregate(chunks) -> ChatCompletion: # pylint: disable=too-many-branches "content": [], "reasoning": [], "signature": [], + "reasoning_blocks": None, "message": {}, "logprobs": None, }, @@ -536,6 +570,10 @@ def _aggregate(chunks) -> ChatCompletion: # pylint: disable=too-many-branches slot["reasoning"].append(delta.reasoning_content) if delta.reasoning_signature: slot["signature"].append(delta.reasoning_signature) + blocks = (delta.model_extra or {}).get("reasoning_blocks") + if blocks is not None: + # Anthropic emits a cumulative snapshot; keep the latest. + slot["reasoning_blocks"] = blocks if delta.tool_calls: slot["message"]["tool_calls"] = _merge_tool_calls( slot["message"].get("tool_calls"), @@ -552,22 +590,30 @@ def _aggregate(chunks) -> ChatCompletion: # pylint: disable=too-many-branches choices = [] for index in sorted(acc): slot = acc[index] + signature = "".join(slot["signature"]) or None + message_extras: Dict[str, Any] = {} + if slot["reasoning_blocks"] is not None: + # Verbatim thinking blocks win: derive the replay signature + # from them and carry them so the turn replays faithfully. + message_extras["reasoning_blocks"] = slot["reasoning_blocks"] + signature = _signature_from_blocks(slot["reasoning_blocks"]) message = ChatCompletionMessage( role=slot["message"].get("role", "assistant"), content="".join(slot["content"]) or None, reasoning_content="".join(slot["reasoning"]) or None, - reasoning_signature="".join(slot["signature"]) or None, + reasoning_signature=signature, tool_calls=slot["message"].get("tool_calls"), + **message_extras, ) - extras: Dict[str, Any] = {} + choice_extras: Dict[str, Any] = {} if slot["logprobs"] is not None: - extras["logprobs"] = slot["logprobs"] + choice_extras["logprobs"] = slot["logprobs"] choices.append( Choice( index=index, finish_reason=slot["message"].get("finish_reason"), message=message, - **extras, + **choice_extras, ), ) return ChatCompletion( diff --git a/dashscope/transport/ws_connection.py b/dashscope/transport/ws_connection.py index 3c24f1fa..dd83580d 100644 --- a/dashscope/transport/ws_connection.py +++ b/dashscope/transport/ws_connection.py @@ -114,24 +114,30 @@ def finished(self) -> bool: def _enqueue(self, item: _QueueItem) -> None: """Called with the connection lock held; the first error ends input.""" - if not self._closed and self._error is None: - if item[0] == _ERROR: + if self._closed or self._error is not None: + return + if item[0] == _ERROR: + self._error = item[1] + try: + self._queue.put_nowait(item) + except queue.Full: + if item[0] != _ERROR: + # A slow consumer must not tear down the shared connection: + # fail only this task and let its reader surface the error. + item = ( + _ERROR, + WsStateError( + detail="websocket task receive queue is full", + ), + ) self._error = item[1] + # Preserve terminal visibility without allowing an unbounded + # backlog to retain audio forever: evict the oldest frame. try: - self._queue.put_nowait(item) - except queue.Full as exc: - if item[0] == _ERROR: - # Preserve terminal visibility without allowing an - # unbounded backlog to retain audio forever. - try: - self._queue.get_nowait() - except queue.Empty: - pass - self._queue.put_nowait(item) - else: - raise WsStateError( - detail="websocket task receive queue is full", - ) from exc + self._queue.get_nowait() + except queue.Empty: + pass + self._queue.put_nowait(item) def _frame(self, action: str, payload: Dict[str, Any]) -> Dict[str, Any]: return { diff --git a/tests/test_audio_tts_http.py b/tests/test_audio_tts_http.py index 58c80624..f2353fe0 100644 --- a/tests/test_audio_tts_http.py +++ b/tests/test_audio_tts_http.py @@ -13,7 +13,7 @@ import httpx import pytest -from dashscope import Client +from dashscope import AsyncClient, Client from dashscope.errors import ( APIConnectionError, APIStatusError, @@ -34,6 +34,13 @@ def _make_client(handler, **kwargs): return Client(api_key="sk-test", http_client=http_client, **kwargs) +def _make_async_client(handler, **kwargs): + mock = httpx.MockTransport(handler) + http_client = httpx.AsyncClient(transport=mock, base_url=BASE_URL) + kwargs.setdefault("base_url", BASE_URL) + return AsyncClient(api_key="sk-test", http_client=http_client, **kwargs) + + def _b64(payload: bytes) -> str: return base64.b64encode(payload).decode() @@ -281,6 +288,29 @@ def handler(request): next(stream) +@pytest.mark.asyncio +async def test_async_http_stream_json_envelope_raises(): + # A 200 application/json body is a service failure, not audio SSE. + # The async stream must surface it (parity with the sync path) rather + # than silently yielding zero chunks. + def handler(request): + return httpx.Response( + 200, + headers={"Content-Type": "application/json"}, + json={"code": "InvalidParameter", "message": "bad voice"}, + ) + + client = _make_async_client(handler) + stream = await client.audio.tts.http.stream( + "你好", + model="qwen-audio-3.1-tts-flash", + voice="nobody", + ) + with pytest.raises(BadRequestError, match="bad voice"): + async for _ in stream: + pass + + def test_http_synthesize_error_status_raises(): def handler(request): return httpx.Response( diff --git a/tests/test_protocols.py b/tests/test_protocols.py index ab326107..1554e4d4 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -1267,6 +1267,135 @@ def test_anthropic_preserves_interleaved_thinking_blocks_for_replay(): ) +def test_anthropic_streaming_preserves_interleaved_thinking_blocks(): + # Streaming parity with parse_completion: an interleaved-thinking + # stream must yield verbatim reasoning_blocks and must NOT merge the + # per-block signatures into one invalid reasoning_signature. + from dashscope.resources.chat.completions import _aggregate + + protocol = AnthropicProtocol() + parse = protocol.make_chunk_parser() + events = [ + {"type": "message_start", "message": {"id": "msg-1"}}, + { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "thinking"}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "thinking_delta", "thinking": "a"}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "signature_delta", "signature": "sig-a"}, + }, + { + "type": "content_block_start", + "index": 1, + "content_block": {"type": "text"}, + }, + { + "type": "content_block_delta", + "index": 1, + "delta": {"type": "text_delta", "text": "middle"}, + }, + { + "type": "content_block_start", + "index": 2, + "content_block": {"type": "thinking"}, + }, + { + "type": "content_block_delta", + "index": 2, + "delta": {"type": "thinking_delta", "thinking": "b"}, + }, + { + "type": "content_block_delta", + "index": 2, + "delta": {"type": "signature_delta", "signature": "sig-b"}, + }, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + }, + ] + chunks = [c for c in (parse(e) for e in events) if c is not None] + message = _aggregate(chunks).choices[0].message + assert message.content == "middle" + assert message.reasoning_content == "ab" + # Two signed blocks -> no single replayable signature. + assert message.reasoning_signature is None + assert [b["signature"] for b in message.reasoning_blocks] == [ + "sig-a", + "sig-b", + ] + # The streamed turn replays through the same path as the parsed one. + request = protocol.build_chat_request( + model="qwen3-max", + messages=[{"role": "user", "content": "hi"}, message.model_dump()], + ) + assert ( + request.body["messages"][1]["content"][:2] == message.reasoning_blocks + ) + + +def test_anthropic_streaming_single_signed_block_keeps_signature(): + from dashscope.resources.chat.completions import _aggregate + + protocol = AnthropicProtocol() + parse = protocol.make_chunk_parser() + events = [ + {"type": "message_start", "message": {"id": "msg-2"}}, + { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "thinking"}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "thinking_delta", "thinking": "hmm"}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "signature_delta", "signature": "sig"}, + }, + {"type": "message_delta", "delta": {"stop_reason": "end_turn"}}, + ] + chunks = [c for c in (parse(e) for e in events) if c is not None] + message = _aggregate(chunks).choices[0].message + assert message.reasoning_signature == "sig" + assert message.reasoning_blocks == [ + {"type": "thinking", "thinking": "hmm", "signature": "sig"}, + ] + + +def test_anthropic_streaming_surfaces_redacted_thinking(): + from dashscope.resources.chat.completions import _accumulate_chunks + + protocol = AnthropicProtocol() + parse = protocol.make_chunk_parser() + events = [ + {"type": "message_start", "message": {"id": "msg-3"}}, + { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "redacted_thinking", "data": "enc"}, + }, + {"type": "message_delta", "delta": {"stop_reason": "end_turn"}}, + ] + chunks = [c for c in (parse(e) for e in events) if c is not None] + cumulative = list(_accumulate_chunks(iter(chunks))) + final = cumulative[-1].choices[0].delta + assert (final.model_extra or {}).get("reasoning_blocks") == [ + {"type": "redacted_thinking", "data": "enc"}, + ] + + def test_anthropic_parse_completion_thinking(): # Parity with the streaming thinking_delta path: thinking blocks # surface as reasoning_content instead of being silently dropped, diff --git a/tests/test_ws_connection.py b/tests/test_ws_connection.py index 9451aa73..d8a5efc7 100644 --- a/tests/test_ws_connection.py +++ b/tests/test_ws_connection.py @@ -226,6 +226,39 @@ def test_socket_error_broadcasts_to_all_tasks(): assert conn.closed +def test_slow_consumer_queue_full_fails_only_that_task(): + """One stalled task must not tear down the shared connection. + + Overflowing t1's bounded receive queue fails t1 alone; t2 keeps + routing and the socket stays open for the remaining tasks. + """ + conn, _ = _make_connection(timeout=5, task_queue_size=2) + t1 = conn.open_task() + t2 = conn.open_task() + _started(conn, t1) + _started(conn, t2) + + # Flood t1 past its queue size without draining it, then a t2 frame. + for n in range(5): + conn._ws.inbox.put( + _event("result-generated", t1.task_id, {"n": n}), + ) + conn._ws.inbox.put(_event("result-generated", t2.task_id, {"n": 99})) + + # t2 is unaffected and the connection survives the overflow. + assert t2.recv_event()["payload"] == {"n": 99} + assert not conn.closed + assert not conn._ws.closed + + # t1 sees a terminal WsStateError, isolated to that task. Drain any + # frames queued ahead of the overflow marker, then expect the raise. + with pytest.raises(WsStateError, match="queue is full"): + while True: + t1.recv() + assert not conn.closed + conn.close() + + def test_peer_close_normalizes_to_api_connection_error(): """A library close on the shared reader surfaces as APIConnectionError, not a raw websocket exception -- matching WsSession.recv / realtime.""" From d95b33894462b94660bad349779cfe719a769257 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 29 Oct 2026 11:45:15 +0800 Subject: [PATCH 31/41] refactor(agentic-rl): use relative internal imports --- .../finetune/reinforcement/common/errors.py | 2 +- .../finetune/reinforcement/common/log.py | 2 +- .../finetune/reinforcement/common/model.py | 12 ++++++------ .../finetune/reinforcement/common/utils.py | 8 ++++---- .../reinforcement/component/data/base_data_model.py | 2 +- .../component/data/group_reward_input.py | 2 +- .../reinforcement/component/data/reward_input.py | 2 +- .../reinforcement/component/data/rollout_input.py | 2 +- .../reinforcement/component/demo/__init__.py | 12 +++--------- .../component/demo/group_reward_processor_demo.py | 2 +- .../component/demo/reward_processor_demo.py | 2 +- .../component/demo/rollout_processor_demo.py | 2 +- .../reinforcement/component/func_decorator.py | 4 ++-- .../finetune/reinforcement/component/func_manager.py | 4 ++-- .../component/observability/genai/llm_dashscope.py | 2 +- .../component/observability/genai/llm_openai.py | 2 +- .../component/observability/genai/messages.py | 6 +++--- .../component/observability/genai/tools.py | 2 +- .../component/observability/processor_span.py | 2 +- .../reinforcement/component/observability/tracing.py | 4 ++-- .../component/parser/group_reward_parser.py | 6 +++--- .../reinforcement/component/parser/reward_parser.py | 6 +++--- .../reinforcement/component/parser/rollout_parser.py | 6 +++--- .../processor/abstract_group_reward_processor.py | 6 +++--- .../component/processor/abstract_reward_processor.py | 8 ++++---- .../reinforcement/component/server/server.py | 8 ++++---- 26 files changed, 55 insertions(+), 61 deletions(-) diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index 7f85a63a..026231f8 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -7,7 +7,7 @@ from dashscope.errors import _CatalogSdkError, sdk_error -from dashscope.applications.finetune.reinforcement.common._legacy_errors import ( +from ._legacy_errors import ( AgenticRLError as _LegacyAgenticRLError, BaseConnectionError as _LegacyBaseConnectionError, BasePermissionError as _LegacyBasePermissionError, diff --git a/dashscope/applications/finetune/reinforcement/common/log.py b/dashscope/applications/finetune/reinforcement/common/log.py index e352ae98..a461b46b 100644 --- a/dashscope/applications/finetune/reinforcement/common/log.py +++ b/dashscope/applications/finetune/reinforcement/common/log.py @@ -2,7 +2,7 @@ import logging import sys -from dashscope.applications.finetune.reinforcement.common.constants import ( +from .constants import ( LOGGER_NAME, LOG_LEVEL, ) diff --git a/dashscope/applications/finetune/reinforcement/common/model.py b/dashscope/applications/finetune/reinforcement/common/model.py index 3964bffc..5e631838 100644 --- a/dashscope/applications/finetune/reinforcement/common/model.py +++ b/dashscope/applications/finetune/reinforcement/common/model.py @@ -23,7 +23,7 @@ import yaml # Local Application -from dashscope.applications.finetune.reinforcement.common.constants import ( +from .constants import ( FC_API_KEY, FC_DELETE_API, FC_LOAD_API, @@ -40,7 +40,7 @@ FC_LAYER_QUERY_API, LOG_LEVEL, ) -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from .model_types import ( FileSpec, FunctionType, DatasetsType, @@ -51,7 +51,7 @@ StatusType, TrainingType, ) -from dashscope.applications.finetune.reinforcement.component.data import ( +from ..component.data import ( RewardInput, RewardOutput, RolloutInput, @@ -59,7 +59,7 @@ GroupRewardInput, GroupRewardOutput, ) -from dashscope.applications.finetune.reinforcement.common.utils import ( +from .utils import ( check_file, client_fc, create_deployment_files, @@ -73,8 +73,8 @@ get_weights_from_file, deep_remove_none, ) -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.errors import ( +from .log import logger +from .errors import ( InputError, OutputError, OSSConnectionError, diff --git a/dashscope/applications/finetune/reinforcement/common/utils.py b/dashscope/applications/finetune/reinforcement/common/utils.py index 9b803ce6..032c6b04 100644 --- a/dashscope/applications/finetune/reinforcement/common/utils.py +++ b/dashscope/applications/finetune/reinforcement/common/utils.py @@ -21,8 +21,8 @@ ) from dashscope._shared._sdk_ident import get_sdk_headers -from dashscope.applications.finetune.reinforcement import logger -from dashscope.applications.finetune.reinforcement.common.errors import ( +from .. import logger +from .errors import ( InputError, OutputError, ConfigurationError, @@ -30,7 +30,7 @@ RuntimeErrorWithCode, OSSUploadError, ) -from dashscope.applications.finetune.reinforcement import ( +from .. import ( LOG_LEVEL, DASHSCOPE_API_KEY, BAILIAN_FILE_API, @@ -47,7 +47,7 @@ LOGGER_FILTER_FIELDS, FC_WORKERS_COUNT, ) -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from .model_types import ( FileSpec, FunctionType, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py b/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py index 26d09cb1..2e162f37 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/applications/finetune/reinforcement/component/data/base_data_model.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.model_types import ( FunctionType as FuncType, ) diff --git a/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py index 51808635..65319ad2 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/group_reward_input.py @@ -9,7 +9,7 @@ from typing import Any, List, Optional from pydantic import ConfigDict, Field -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.model_types import ( FunctionType as FuncType, ) from .base_data_model import ( diff --git a/dashscope/applications/finetune/reinforcement/component/data/reward_input.py b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py index 7e1801cf..5b154e6f 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/reward_input.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import ConfigDict, Field -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.model_types import ( FunctionType as FuncType, ) from .base_data_model import ( diff --git a/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py index f5007cd1..53a5865a 100644 --- a/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/applications/finetune/reinforcement/component/data/rollout_input.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional from pydantic import ConfigDict, Field -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.model_types import ( FunctionType as FuncType, ) from .base_data_model import ( diff --git a/dashscope/applications/finetune/reinforcement/component/demo/__init__.py b/dashscope/applications/finetune/reinforcement/component/demo/__init__.py index 44fb0350..ddf0b871 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/__init__.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/__init__.py @@ -1,13 +1,7 @@ # -*- coding: utf-8 -*- -from dashscope.applications.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoGroupRewardProcessor, -) -from dashscope.applications.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRewardProcessor, -) -from dashscope.applications.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRolloutProcessor, -) +from .group_reward_processor_demo import DemoGroupRewardProcessor +from .reward_processor_demo import DemoRewardProcessor +from .rollout_processor_demo import DemoRolloutProcessor __all__ = [ "DemoRewardProcessor", diff --git a/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py index 163c3442..198f5322 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -6,7 +6,7 @@ Demonstrates rule-based scoring for multiple Agent outputs in a group. """ -from dashscope.applications.finetune.reinforcement.common.log import logger +from ...common.log import logger from ..data.base_data_model import ( TaskStatus, ) diff --git a/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py index 350b3090..8b14a8e0 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -6,7 +6,7 @@ Demonstrates rule-based scoring for Agent outputs. """ -from dashscope.applications.finetune.reinforcement.common.log import logger +from ...common.log import logger from ..data.base_data_model import ( TaskStatus, ) diff --git a/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py index b1fbd92a..9a810a5a 100644 --- a/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/applications/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -8,7 +8,7 @@ import time -from dashscope.applications.finetune.reinforcement.common.log import logger +from ...common.log import logger from ..data.base_data_model import ( AgentOutput, TaskStatus, diff --git a/dashscope/applications/finetune/reinforcement/component/func_decorator.py b/dashscope/applications/finetune/reinforcement/component/func_decorator.py index 56d23363..010909f7 100644 --- a/dashscope/applications/finetune/reinforcement/component/func_decorator.py +++ b/dashscope/applications/finetune/reinforcement/component/func_decorator.py @@ -4,8 +4,8 @@ from functools import wraps from typing import Callable, Dict, Optional, Type -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data import ( +from ..common.log import logger +from .data import ( RewardInput, RewardOutput, Reward, diff --git a/dashscope/applications/finetune/reinforcement/component/func_manager.py b/dashscope/applications/finetune/reinforcement/component/func_manager.py index f11aad06..1d7c3f49 100644 --- a/dashscope/applications/finetune/reinforcement/component/func_manager.py +++ b/dashscope/applications/finetune/reinforcement/component/func_manager.py @@ -20,8 +20,8 @@ from functools import partial from typing import Any, Dict, Optional, Type -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ..common.log import logger +from ..common.model_types import ( FunctionType as FuncType, ) from .data.base_data_model import ( diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py index b65443c8..ee5f8c4b 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -7,7 +7,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.applications.finetune.reinforcement.common.log import logger +from ....common.log import logger from ..genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py index c5ac9c45..d0659724 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -16,7 +16,7 @@ from opentelemetry import trace as otel_trace from opentelemetry.trace.status import Status, StatusCode -from dashscope.applications.finetune.reinforcement.common.log import logger +from ....common.log import logger from ..genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py index c53ed3ff..f1f08c9a 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/messages.py @@ -27,11 +27,11 @@ import os from typing import Any, Dict, List, Optional -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.utils import ( +from ....common.log import logger +from ....common.utils import ( deep_mask, ) -from dashscope.applications.finetune.reinforcement.component.observability import ( +from .. import ( genai, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py b/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py index 585abffe..b74cec51 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/genai/tools.py @@ -50,7 +50,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.applications.finetune.reinforcement.component.observability import ( +from .. import ( genai, tracing, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py index bd3ad8f9..a8c69f04 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/processor_span.py @@ -34,7 +34,7 @@ from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar, cast -from dashscope.applications.finetune.reinforcement.common.log import logger +from ...common.log import logger from ..data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/applications/finetune/reinforcement/component/observability/tracing.py b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py index c4488942..36d88784 100644 --- a/dashscope/applications/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/applications/finetune/reinforcement/component/observability/tracing.py @@ -36,8 +36,8 @@ from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.log import logger +from ...common.model_types import ( FunctionType as FuncType, ) from ..data.base_data_model import ( diff --git a/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py index fa7dca9b..f137e372 100644 --- a/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/group_reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data import ( +from ...common.log import logger +from ..data import ( GroupRewardInput, ) -from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( +from .base_parser import ( BaseRequestParser, ) diff --git a/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py index 75503241..c4935d3e 100644 --- a/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from ...common.log import logger +from ..data.reward_input import ( RewardInput, ) -from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( +from .base_parser import ( BaseRequestParser, ) diff --git a/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py b/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py index 1a83e1b7..32682a89 100644 --- a/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py +++ b/dashscope/applications/finetune/reinforcement/component/parser/rollout_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.component.data.rollout_input import ( +from ...common.log import logger +from ..data.rollout_input import ( RolloutInput, ) -from dashscope.applications.finetune.reinforcement.component.parser.base_parser import ( +from .base_parser import ( BaseRequestParser, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py index 9c39dbc5..42b3601e 100644 --- a/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_group_reward_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.applications.finetune.reinforcement.component.data import ( +from ..data import ( GroupRewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data import ( +from ..data import ( GroupRewardOutput, ) -from dashscope.applications.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from .abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py b/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py index 68843806..8cfdc7fd 100644 --- a/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py +++ b/dashscope/applications/finetune/reinforcement/component/processor/abstract_reward_processor.py @@ -14,18 +14,18 @@ from concurrent.futures import ThreadPoolExecutor, Executor from typing import Optional, Dict -from dashscope.applications.finetune.reinforcement.component.data.reward_input import ( +from ..data.reward_input import ( RewardInput, ) -from dashscope.applications.finetune.reinforcement.component.data.reward_output import ( +from ..data.reward_output import ( RewardOutput, ) -from dashscope.applications.finetune.reinforcement.component.func_decorator import ( +from ..func_decorator import ( RewardProcessorMeta, SubRewardFunction, AggregateFunction, ) -from dashscope.applications.finetune.reinforcement.component.processor import ( +from . import ( AbstractProcessor, ) diff --git a/dashscope/applications/finetune/reinforcement/component/server/server.py b/dashscope/applications/finetune/reinforcement/component/server/server.py index 9985fefb..56945cd6 100644 --- a/dashscope/applications/finetune/reinforcement/component/server/server.py +++ b/dashscope/applications/finetune/reinforcement/component/server/server.py @@ -48,15 +48,15 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from dashscope.applications.finetune.reinforcement.common.log import logger -from dashscope.applications.finetune.reinforcement.common.utils import ( +from ...common.log import logger +from ...common.utils import ( get_fc_request_id, get_business_summary, ) -from dashscope.applications.finetune.reinforcement.common.model_types import ( +from ...common.model_types import ( FunctionType as FuncType, ) -from dashscope.applications.finetune.reinforcement.component import ( +from .. import ( BaseDataModel, ) from ..func_manager import ( From 1015c39cefb13b05ff23dd2161db002a5d14c18d Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 29 Oct 2026 11:53:31 +0800 Subject: [PATCH 32/41] fix(errors): classify response failures as internal errors --- dashscope/_error_registry.py | 11 +-------- dashscope/errors.py | 23 ++++--------------- tests/test_error_registry.py | 7 +++--- tests/test_errors.py | 44 +++++++++++++++++++++++------------- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/dashscope/_error_registry.py b/dashscope/_error_registry.py index b6dcea3a..ba581ce6 100644 --- a/dashscope/_error_registry.py +++ b/dashscope/_error_registry.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional __version__ = "1.0.11" -__commit__ = "cd873b6" +__commit__ = "84e5f33" __snapshot__ = False @@ -174,14 +174,6 @@ def format_message( anthropic_error_code="api_error", ) -RESPONSE_ERROR = PublicError( - key="response_error", - status_code=500, - error_code="ResponseError", - error_msg="The server returned a response that could not be parsed.", - anthropic_error_code="api_error", -) - SERVICE_UNAVAILABLE = PublicError( key="service_unavailable", status_code=503, @@ -732,7 +724,6 @@ def format_message( CONCURRENCY_LIMIT_EXCEEDED, INSUFFICIENT_QUOTA, INTERNAL_ERROR, - RESPONSE_ERROR, SERVICE_UNAVAILABLE, REQUEST_TIMEOUT, ] diff --git a/dashscope/errors.py b/dashscope/errors.py index 5dc8e875..d1afa30e 100644 --- a/dashscope/errors.py +++ b/dashscope/errors.py @@ -252,17 +252,6 @@ class InternalServerError(APIStatusError): status_code = _CATALOG_STATUS["InternalServerError"] -class ResponseError(APIStatusError): - """The server returned a response that could not be parsed. - - SDK-side, never gateway-sent: it shares status 500 with - ``internal_error``, so the status maps below must keep resolving a - bare 500 to :class:`InternalServerError`. - """ - - status_code = _CATALOG_STATUS["ResponseError"] - - class ServiceUnavailableError(APIStatusError): status_code = _CATALOG_STATUS["ServiceUnavailableError"] @@ -292,15 +281,13 @@ class ContentFilterError(BadRequestError): RequestTooLargeError, RateLimitError, InternalServerError, - ResponseError, ServiceUnavailableError, GatewayTimeoutError, ) } -# First entry per status wins: response_error shares 500 with -# internal_error, and a bare 500 from the gateway means internal_error -- -# ResponseError is raised by the SDK itself, never picked by status. +# First entry per status wins. Statuses with multiple public entries +# (400, 401, and 429) intentionally resolve to their shared class. _STATUS_MAP: Dict[int, Type[APIStatusError]] = {} for _entry_ in _registry.PUBLIC_ERRORS: _STATUS_MAP.setdefault( @@ -313,9 +300,9 @@ class ContentFilterError(BadRequestError): entry.status_code for entry in _registry.PUBLIC_ERRORS ) -# Only the statuses a single entry claims. 400 / 401 / 429 / 500 carry -# siblings with different ``error_msg`` text, so a status alone cannot -# pick one without stating a reason the gateway never gave. +# Only the statuses a single entry claims. 400 / 401 / 429 carry siblings +# with different ``error_msg`` text, so a status alone cannot pick one +# without stating a reason the gateway never gave. _STATUS_ENTRY: Dict[int, PublicError] = { entry.status_code: entry for entry in _registry.PUBLIC_ERRORS diff --git a/tests/test_error_registry.py b/tests/test_error_registry.py index 6e773d79..5b0f1731 100644 --- a/tests/test_error_registry.py +++ b/tests/test_error_registry.py @@ -46,7 +46,7 @@ "sdk.agentic_rl.FunctionTestTimeout", } -# The ten exception class names the catalog requires. +# The nine exception class names the catalog requires. _REQUIRED_CLASSES = { "AuthenticationError", "BadRequestError", @@ -56,7 +56,6 @@ "PermissionDeniedError", "RateLimitError", "RequestTooLargeError", - "ResponseError", "ServiceUnavailableError", } @@ -83,9 +82,9 @@ def _by_key(key): def test_catalog_size_and_provenance(): assert registry.__version__ == "1.0.11" - assert registry.__commit__ == "cd873b6" + assert registry.__commit__ == "84e5f33" assert registry.__snapshot__ is False - assert len(PUBLIC) == 17 + assert len(PUBLIC) == 16 assert len(SDK) == 41 diff --git a/tests/test_errors.py b/tests/test_errors.py index bbc63223..84ccacfc 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -36,7 +36,6 @@ PermissionDeniedError, RateLimitError, RequestTooLargeError, - ResponseError, SdkError, ServiceUnavailableError, WsBinaryOwnerConflictError, @@ -663,7 +662,7 @@ def test_catalog_classes_are_flat_siblings(): entry.error_code: getattr(errors, entry.error_code) for entry in registry.PUBLIC_ERRORS } - assert len(classes) == 10 + assert len(classes) == 9 for name, cls in classes.items(): assert cls.__bases__ == (APIStatusError,), name for other_name, other in classes.items(): @@ -674,9 +673,8 @@ def test_catalog_classes_are_flat_siblings(): def test_status_map_is_derived_from_the_catalog(): catalogued = {e.status_code for e in registry.PUBLIC_ERRORS} assert set(_STATUS_MAP) == catalogued - # First claimant of a status wins the status-only mapping; later - # siblings on the same status (response_error on 500) are reachable - # only by class, never selected by the status alone. + # First claimant of a status wins the status-only mapping. Entries + # sharing a status also share the same exception class. seen = set() for entry in registry.PUBLIC_ERRORS: if entry.status_code in seen: @@ -686,20 +684,26 @@ def test_status_map_is_derived_from_the_catalog(): assert _STATUS_MAP[entry.status_code] is expected, entry.key -def test_bare_500_still_maps_to_internal_server_error(): - # response_error shares status 500 with internal_error; the gateway - # only ever means internal_error by a bare 500. +def test_bare_500_maps_to_internal_server_error(): assert _STATUS_MAP[500] is InternalServerError with pytest.raises(InternalServerError) as ei: check_response(_resp(500, {"message": "boom"})) assert type(ei.value) is InternalServerError -def test_response_error_is_sdk_side_only(): - assert issubclass(ResponseError, APIStatusError) - assert ResponseError.status_code == 500 - assert ResponseError not in _STATUS_MAP.values() - assert ResponseError not in set(_CODE_MAP.values()) +def test_unparseable_500_response_maps_to_internal_server_error(): + response = httpx.Response(500, text="not-json") + error = errors.response_error(response) + assert type(error) is InternalServerError + assert error.status_code == 500 + assert error.message == "not-json" + + +def test_response_error_is_not_a_public_catalog_entry_or_class(): + assert "response_error" not in { + entry.key for entry in registry.PUBLIC_ERRORS + } + assert not hasattr(errors, "ResponseError") @pytest.mark.parametrize( @@ -827,15 +831,23 @@ def test_placeholders_survive_the_catalog_fallback(): def test_ambiguous_status_gets_no_catalog_text(): - # 400 / 401 / 429 / 500 each carry several entries with different - # text, so a status alone cannot pick one without inventing a reason. - for status in (400, 401, 429, 500): + # 400 / 401 / 429 each carry several entries with different text, so + # a status alone cannot pick one without inventing a reason. + for status in (400, 401, 429): with pytest.raises(APIStatusError) as ei: check_response(_bare(status)) assert ei.value.message == "", status assert ei.value.status_code == status +def test_bodyless_500_gets_internal_error_catalog_text(): + with pytest.raises(InternalServerError) as ei: + check_response(_bare(500)) + assert ei.value.message == ( + "The server encountered an internal error. Please try again later." + ) + + def test_wire_code_identifies_an_entry_on_an_ambiguous_status(): err = errors.error_from_status( 400, From fd5e0106bf7f378024f186e5081c328eda5a22c9 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 29 Oct 2026 13:57:03 +0800 Subject: [PATCH 33/41] refactor(audio): refactor async client bridging in audio module - Removed legacy _ThreadBridgeTransport class, replaced with unified _blocking module - Added bridge_client function for creating bridged clients supporting cross-thread transport calls - Updated _AsyncResourceProxy to use the new bridged client implementation - Moved synchronous response parsing to worker threads to avoid blocking the event loop - Ensured async clients correctly handle file-upload-related blocking operations --- dashscope/_config.py | 30 ++++++-- dashscope/resources/_blocking.py | 70 +++++++++++++++++++ dashscope/resources/_oss_upload.py | 23 ++++++- dashscope/resources/audio/__init__.py | 22 +----- dashscope/resources/chat/completions.py | 11 +-- dashscope/resources/images.py | 20 +++--- dashscope/resources/multimodal/chat.py | 11 +-- dashscope/resources/multimodal/embeddings.py | 11 +-- dashscope/resources/videos.py | 11 +-- tests/test_chat.py | 46 +++++++++++++ tests/test_config.py | 11 +++ tests/test_oss_upload.py | 72 ++++++++++++++++++-- tests/test_settings.py | 2 + 13 files changed, 276 insertions(+), 64 deletions(-) create mode 100644 dashscope/resources/_blocking.py diff --git a/dashscope/_config.py b/dashscope/_config.py index dbec16c6..cfd8580d 100644 --- a/dashscope/_config.py +++ b/dashscope/_config.py @@ -203,6 +203,20 @@ def _validate_region(region: str) -> str: return region +def _validate_base_url(value: Optional[str]) -> Optional[str]: + """Reject a non-string base_url where it is configured. + + Left alone it reaches ``base_url.rstrip("/")`` in the transport and + surfaces as an ``AttributeError`` naming a method the caller never + wrote, far from the bad argument. + """ + if value is not None and not isinstance(value, str): + raise TypeError( + f"base_url must be a string, got {type(value).__name__}", + ) + return value + + def _protocol_default_base_url( protocol: str, workspace: Optional[str], @@ -349,6 +363,7 @@ def base_url(self) -> str: @base_url.setter def base_url(self, value: Optional[str]) -> None: + value = _validate_base_url(value) with self._lock: self._explicit["base_url"] = value @@ -707,12 +722,15 @@ def resolve_config( ), ) - configured_base_url = _pick( - base_url, - settings.get_explicit("base_url"), - os.environ.get(_BASE_URL_ENV) or os.environ.get(_LEGACY_BASE_URL_ENV), - file_cfg.get("base_url"), - None, + configured_base_url = _validate_base_url( + _pick( + base_url, + settings.get_explicit("base_url"), + os.environ.get(_BASE_URL_ENV) + or os.environ.get(_LEGACY_BASE_URL_ENV), + file_cfg.get("base_url"), + None, + ), ) # An explicit base_url is the escape hatch for custom/private gateways. # Otherwise defaults come from the workspace-scoped MaaS gateway, diff --git a/dashscope/resources/_blocking.py b/dashscope/resources/_blocking.py new file mode 100644 index 00000000..12caae1f --- /dev/null +++ b/dashscope/resources/_blocking.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Run the blocking upload path on behalf of an async resource. + +Local-file → OSS upload is written once, synchronously: it fetches the +upload policy over the client transport, then posts the file with a +blocking httpx client. Async resources offload that work with +``asyncio.to_thread``, but a worker thread cannot ``await`` the +``AsyncTransport`` — calling it there returns an un-awaited coroutine +instead of a response. + +So the worker gets a *bridge* client: same client for everything except the +transport, whose ``request`` schedules the coroutine back onto the running +loop with ``run_coroutine_threadsafe`` and blocks on the future. The upload +keeps the async client's connection pool, retries and per-call credentials, +and the event loop stays free to run the coroutine it was handed. +""" + +from __future__ import annotations + +import asyncio +from typing import Any, Callable + +__all__ = ["bridge_client", "run_blocking"] + + +class _BridgeTransport: + def __init__(self, transport, loop: asyncio.AbstractEventLoop) -> None: + self._transport = transport + self._loop = loop + + def request(self, *args, **kwargs): + return asyncio.run_coroutine_threadsafe( + self._transport.request(*args, **kwargs), + self._loop, + ).result() + + +class _BridgeClient: + """Stand-in client whose ``transport`` answers synchronously.""" + + def __init__(self, client, loop: asyncio.AbstractEventLoop) -> None: + self._client = client + self.transport = _BridgeTransport(client.transport, loop) + + def __getattr__(self, name: str) -> Any: + return getattr(self._client, name) + + +def bridge_client(client) -> Any: + """Wrap ``client`` so its transport can be used from a worker thread. + + Must be called from the event loop that owns the client's transport; + the loop is captured here because a worker thread has no way to find it. + """ + return _BridgeClient(client, asyncio.get_running_loop()) + + +async def run_blocking( + client, + fn: Callable[..., Any], + *args, + **kwargs, +) -> Any: + """``asyncio.to_thread(fn, , *args, **kwargs)``. + + ``fn`` takes the client as its first argument, as every blocking helper + on the upload path does. + """ + return await asyncio.to_thread(fn, bridge_client(client), *args, **kwargs) diff --git a/dashscope/resources/_oss_upload.py b/dashscope/resources/_oss_upload.py index caa4713c..c4ebae25 100644 --- a/dashscope/resources/_oss_upload.py +++ b/dashscope/resources/_oss_upload.py @@ -26,7 +26,7 @@ import httpx from dashscope._logging import get_logger -from dashscope.errors import check_response, error_from_status +from dashscope.errors import SdkError, check_response, error_from_status from dashscope.transport.http import default_user_agent __all__ = [ @@ -63,6 +63,20 @@ def _resolve_file_uri_path(file_uri: str) -> str: return os.path.expanduser(file_path) +def _policy_from_body(body: Dict[str, Any]) -> Dict[str, Any]: + """Pull the policy out of the getPolicy envelope. + + ``data`` is the key the gateway answers with -- and the one 1.x read + first. Accepting ``output`` too would let an unrelated body (an + embeddings response, say) through as a "policy", deferring the failure + to a ``KeyError`` on some policy field far from here. + """ + policy = body.get("data") + if not isinstance(policy, dict): + raise SdkError("upload policy response carries no data object") + return policy + + def get_upload_certificate( client, model: str, @@ -73,6 +87,11 @@ def get_upload_certificate( Raises even when the client was built with ``raise_on_error=False``: without a policy there is no URL to upload to and nothing to hand back. + + Blocking, like the rest of this module: an ``AsyncClient`` reaches it + through the bridge in :mod:`dashscope.resources._blocking`, which is + what lets ``client.transport.request`` return a response here instead + of a coroutine. """ response = client.transport.request( "GET", @@ -82,7 +101,7 @@ def get_upload_certificate( workspace=workspace, ) check_response(response) - return response.json()["output"] + return _policy_from_body(response.json()) def _post_to_oss( diff --git a/dashscope/resources/audio/__init__.py b/dashscope/resources/audio/__init__.py index 12c79098..17ac8abc 100644 --- a/dashscope/resources/audio/__init__.py +++ b/dashscope/resources/audio/__init__.py @@ -11,8 +11,8 @@ """ import asyncio -from types import SimpleNamespace +from dashscope.resources._blocking import bridge_client from dashscope.resources.audio.asr import ( Asr, AsyncAsr, @@ -74,19 +74,6 @@ ] -class _ThreadBridgeTransport: - def __init__(self, transport, loop) -> None: - self._transport = transport - self._loop = loop - - def request(self, *args, **kwargs): - future = asyncio.run_coroutine_threadsafe( - self._transport.request(*args, **kwargs), - self._loop, - ) - return future.result() - - class _AsyncResourceProxy: """Run sync response parsing in a worker while reusing AsyncTransport.""" @@ -96,12 +83,7 @@ def __init__(self, client, resource_cls) -> None: def __getattr__(self, name): async def call(*args, **kwargs): - loop = asyncio.get_running_loop() - bridge_client = SimpleNamespace( - transport=_ThreadBridgeTransport(self._client.transport, loop), - raise_on_error=self._client.raise_on_error, - ) - resource = self._resource_cls(bridge_client) + resource = self._resource_cls(bridge_client(self._client)) return await asyncio.to_thread( getattr(resource, name), *args, diff --git a/dashscope/resources/chat/completions.py b/dashscope/resources/chat/completions.py index ea14c923..4a55dfbb 100644 --- a/dashscope/resources/chat/completions.py +++ b/dashscope/resources/chat/completions.py @@ -8,7 +8,6 @@ from __future__ import annotations -import asyncio from dataclasses import replace from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, overload @@ -16,6 +15,7 @@ from dashscope.errors import response_error from dashscope.protocols.base import ChatRequest +from dashscope.resources._blocking import run_blocking from dashscope.resources._oss import ( OSS_RESOLVE_HEADERS, upload_message_media, @@ -997,11 +997,12 @@ async def create( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> Union[ChatCompletion, AsyncChatStream]: - # The upload path is blocking (requests + getPolicy); run the whole - # request build off the event loop so it never stalls other tasks. - built = await asyncio.to_thread( - _build_request, + # The upload path is blocking (file post + getPolicy), so the whole + # request build runs in a worker thread -- over a transport bridged + # back to this loop, which is what lets it serve the policy fetch. + built = await run_blocking( self._client, + _build_request, multimodal=False, model=model, messages=messages, diff --git a/dashscope/resources/images.py b/dashscope/resources/images.py index b9caa6bf..d432d4a9 100644 --- a/dashscope/resources/images.py +++ b/dashscope/resources/images.py @@ -22,10 +22,10 @@ from __future__ import annotations -import asyncio from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union from dashscope.errors import check_response +from dashscope.resources._blocking import run_blocking from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, @@ -572,11 +572,12 @@ async def create( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> Job: - # The upload path is blocking (requests + getPolicy); run it off the - # event loop so it never stalls other tasks. - image_urls, extra_input, has_upload = await asyncio.to_thread( - _upload_inputs, + # The upload path is blocking (file post + getPolicy), so it runs in + # a worker thread -- over a transport bridged back to this loop, + # which is what lets it serve the policy fetch. + image_urls, extra_input, has_upload = await run_blocking( self._client, + _upload_inputs, model, image_urls, extra_input, @@ -741,11 +742,12 @@ async def generate( ) -> Job: """See :meth:`Images.generate`.""" if _uses_multimodal_protocol(model): - # The upload path is blocking (requests + getPolicy); run it off - # the event loop so it never stalls other tasks. - image_urls, extra_input, has_upload = await asyncio.to_thread( - _upload_inputs, + # The upload path is blocking (file post + getPolicy), so it runs + # in a worker thread -- over a transport bridged back to this + # loop, which is what lets it serve the policy fetch. + image_urls, extra_input, has_upload = await run_blocking( self._client, + _upload_inputs, model, image_urls, extra_input, diff --git a/dashscope/resources/multimodal/chat.py b/dashscope/resources/multimodal/chat.py index d086434d..2ca02e3d 100644 --- a/dashscope/resources/multimodal/chat.py +++ b/dashscope/resources/multimodal/chat.py @@ -15,11 +15,11 @@ from __future__ import annotations -import asyncio from typing import Any, Dict, List, Optional, Union, overload from typing_extensions import Literal +from dashscope.resources._blocking import run_blocking from dashscope.resources.chat.completions import ( AsyncChatStream, ChatStream, @@ -286,11 +286,12 @@ async def create( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> Union[ChatCompletion, AsyncChatStream]: - # The upload path is blocking (requests + getPolicy); run the whole - # request build off the event loop so it never stalls other tasks. - built = await asyncio.to_thread( - _build_request, + # The upload path is blocking (file post + getPolicy), so the whole + # request build runs in a worker thread -- over a transport bridged + # back to this loop, which is what lets it serve the policy fetch. + built = await run_blocking( self._client, + _build_request, multimodal=True, model=model, messages=messages, diff --git a/dashscope/resources/multimodal/embeddings.py b/dashscope/resources/multimodal/embeddings.py index fdbfdb00..b6cc1713 100644 --- a/dashscope/resources/multimodal/embeddings.py +++ b/dashscope/resources/multimodal/embeddings.py @@ -7,12 +7,12 @@ from __future__ import annotations -import asyncio from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict from dashscope.errors import check_response +from dashscope.resources._blocking import run_blocking from dashscope.resources._oss import upload_embedding_media from dashscope.resources.embeddings import ( EmbeddingResponse, @@ -205,11 +205,12 @@ async def create( # pylint: disable=too-many-arguments workspace: Optional[str] = None, ) -> EmbeddingResponse: contents = _contents(input) - # The upload path is blocking (requests + getPolicy); run it off the - # event loop so it never stalls other tasks. - await asyncio.to_thread( - upload_embedding_media, + # The upload path is blocking (file post + getPolicy), so it runs in + # a worker thread -- over a transport bridged back to this loop, + # which is what lets it serve the policy fetch. + await run_blocking( self._client, + upload_embedding_media, contents, model, api_key, diff --git a/dashscope/resources/videos.py b/dashscope/resources/videos.py index 466efe1a..a70636b8 100644 --- a/dashscope/resources/videos.py +++ b/dashscope/resources/videos.py @@ -19,9 +19,9 @@ from __future__ import annotations -import asyncio from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union +from dashscope.resources._blocking import run_blocking from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, @@ -411,11 +411,12 @@ async def create( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> Job: - # The upload path is blocking (requests + getPolicy); run it off the - # event loop so it never stalls other tasks. - image_url, media, extra_input, has_upload = await asyncio.to_thread( - _upload_inputs, + # The upload path is blocking (file post + getPolicy), so it runs in + # a worker thread -- over a transport bridged back to this loop, + # which is what lets it serve the policy fetch. + image_url, media, extra_input, has_upload = await run_blocking( self._client, + _upload_inputs, model, image_url, media, diff --git a/tests/test_chat.py b/tests/test_chat.py index dfec3460..0132e7ae 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1571,3 +1571,49 @@ def handler(request): assert captured["body"]["input"]["messages"][0]["content"] == [ {"image": "oss://bucket/cat.png"}, ] + + +@pytest.mark.asyncio +async def test_async_media_upload_reaches_the_gateway(tmp_path, monkeypatch): + # The async call offloads the blocking upload to a worker thread, which + # cannot await the AsyncTransport unless the client is bridged; without + # the bridge the policy fetch returns a coroutine and the upload dies. + monkeypatch.setattr( + "dashscope.resources._oss_upload._post_to_oss", + lambda *args, **kwargs: httpx.Response(200, text=""), + ) + policy = { + "oss_access_key_id": "STS.access-key-id", + "signature": "sig==", + "policy": "pol==", + "upload_dir": "upload/2026/abc", + "x_oss_object_acl": "private", + "x_oss_forbid_overwrite": "true", + "upload_host": "https://dashscope-oss.oss-cn-beijing.aliyuncs.com", + } + captured = {} + + def handler(request): + if request.url.path.endswith("/uploads"): + return httpx.Response( + 200, + json={"request_id": "r", "data": policy}, + ) + captured["oss"] = request.headers.get("X-DashScope-OssResourceResolve") + captured["body"] = json.loads(request.content) + return _native_response() + + image = tmp_path / "cat.png" + image.write_bytes(b"\x89PNG\r\n\x1a\n") + client = _make_async_client(handler) + resp = await client.chat.completions.create( + model="qwen-vl-max", + messages=[{"role": "user", "content": [{"image": str(image)}]}], + ) + + assert captured["oss"] == "enable" + assert captured["body"]["input"]["messages"][0]["content"] == [ + {"image": f"oss://{policy['upload_dir']}/cat.png"}, + ] + assert resp.output_text == "ok" + await client.aclose() diff --git a/tests/test_config.py b/tests/test_config.py index d4a33645..f6177a76 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -508,6 +508,17 @@ def test_invalid_timeout_and_retries(clean_env): resolve_config(max_retries=-1, config_path="/nonexistent/config.toml") +def test_non_string_base_url(clean_env): + # Left unvalidated, a non-string base_url only dies in the transport on + # ``base_url.rstrip("/")`` -- an AttributeError far from the bad argument. + with pytest.raises(TypeError, match="base_url must be a string"): + resolve_config(base_url=123, config_path="/nonexistent/config.toml") + with pytest.raises(TypeError, match="base_url must be a string"): + Client(base_url=123) + with pytest.raises(TypeError, match="base_url must be a string"): + AsyncClient(base_url=123) + + def test_invalid_toml(clean_env, tmp_path): path = _write_config(tmp_path, "not valid [toml") with pytest.raises(ValueError, match="invalid TOML"): diff --git a/tests/test_oss_upload.py b/tests/test_oss_upload.py index 5007ed15..f387693c 100644 --- a/tests/test_oss_upload.py +++ b/tests/test_oss_upload.py @@ -12,9 +12,14 @@ import httpx import pytest -from dashscope import Client -from dashscope.errors import AuthenticationError, PermissionDeniedError +from dashscope import AsyncClient, Client +from dashscope.errors import ( + AuthenticationError, + PermissionDeniedError, + SdkError, +) from dashscope.resources import _oss_upload +from dashscope.resources._blocking import run_blocking BASE_URL = "https://api.test.local/api/v1" OSS_HOST = "https://dashscope-oss.oss-cn-beijing.aliyuncs.com" @@ -37,20 +42,30 @@ def _make_client(handler, **kwargs): return Client(api_key="sk-test", http_client=http_client, **kwargs) -def _policy_client(status=200): - """Client whose transport answers ``GET /uploads`` with a policy.""" - calls = [] +def _make_async_client(handler, **kwargs): + mock = httpx.MockTransport(handler) + http_client = httpx.AsyncClient(transport=mock, base_url=BASE_URL) + kwargs.setdefault("base_url", BASE_URL) + return AsyncClient(api_key="sk-test", http_client=http_client, **kwargs) + +def _policy_handler(calls, status=200): def handler(request): calls.append(request) if status != 200: return httpx.Response(status, json={"message": "denied"}) return httpx.Response( 200, - json={"request_id": "r-1", "output": _CERT}, + json={"request_id": "r-1", "data": _CERT}, ) - return _make_client(handler), calls + return handler + + +def _policy_client(status=200): + """Client whose transport answers ``GET /uploads`` with a policy.""" + calls = [] + return _make_client(_policy_handler(calls, status=status)), calls def _capture_oss(monkeypatch, status=200): @@ -116,6 +131,49 @@ def test_certificate_fetch_failure_raises(): _oss_upload.get_upload_certificate(client, "qwen-vl-max") +@pytest.mark.parametrize( + "body", + [ + {"request_id": "r-1"}, + # "output" is not this endpoint's envelope; accepting it would let + # an unrelated body through as a policy and fail later on a missing + # policy field instead of here. + {"request_id": "r-1", "output": _CERT}, + ], +) +def test_certificate_fetch_rejects_a_body_without_a_policy(body): + client = _make_client(lambda request: httpx.Response(200, json=body)) + + with pytest.raises(SdkError, match="no data object"): + _oss_upload.get_upload_certificate(client, "m") + + +@pytest.mark.asyncio +async def test_async_client_uploads_over_the_bridged_transport( + tmp_path, + monkeypatch, +): + seen = _capture_oss(monkeypatch) + calls = [] + client = _make_async_client(_policy_handler(calls)) + + url, cert = await run_blocking( + client, + _oss_upload.upload_local_file, + "qwen-vl-max", + _png(tmp_path), + ) + + # The policy fetch must come back a response, not the un-awaited + # coroutine an AsyncTransport hands a caller that forgets to await. + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + assert cert == _CERT + assert seen["url"] == OSS_HOST + assert len(calls) == 1 + assert calls[0].headers.get("Authorization") == "Bearer sk-test" + await client.aclose() + + def test_upload_local_file_builds_the_oss_form(tmp_path, monkeypatch): seen = _capture_oss(monkeypatch) client, calls = _policy_client() diff --git a/tests/test_settings.py b/tests/test_settings.py index d0b6ddf7..e150c845 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -156,6 +156,8 @@ def test_setter_validation(clean): settings.timeout = 0 with pytest.raises(ValueError, match="max_retries"): settings.max_retries = -1 + with pytest.raises(TypeError, match="base_url must be a string"): + settings.base_url = 123 def test_log_level_setter_applies_immediately(clean): From 25c259dee5d7f6cdb8491495a39490cef2657356 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 29 Oct 2026 14:21:43 +0800 Subject: [PATCH 34/41] fix(async): harden blocking upload bridge Use an isolated executor for synchronous upload workers so async transports retain access to the event loop default executor. Classify malformed upload-policy responses as catalog-backed internal errors while preserving response metadata. --- dashscope/resources/_blocking.py | 23 ++++++-- dashscope/resources/_oss_upload.py | 38 ++++++++++++-- dashscope/resources/audio/__init__.py | 21 +++++--- tests/test_oss_upload.py | 76 ++++++++++++++++++++++++--- 4 files changed, 136 insertions(+), 22 deletions(-) diff --git a/dashscope/resources/_blocking.py b/dashscope/resources/_blocking.py index 12caae1f..2754d748 100644 --- a/dashscope/resources/_blocking.py +++ b/dashscope/resources/_blocking.py @@ -19,11 +19,23 @@ from __future__ import annotations import asyncio +import contextvars +import functools +from concurrent.futures import ThreadPoolExecutor from typing import Any, Callable __all__ = ["bridge_client", "run_blocking"] +# Keep bridge workers separate from the event loop default executor. Async +# transports may need that default executor themselves (for example DNS +# resolution); occupying it with workers that synchronously wait for those +# transports can otherwise deadlock under concurrent uploads. +_BLOCKING_EXECUTOR = ThreadPoolExecutor( + thread_name_prefix="dashscope-blocking", +) + + class _BridgeTransport: def __init__(self, transport, loop: asyncio.AbstractEventLoop) -> None: self._transport = transport @@ -62,9 +74,14 @@ async def run_blocking( *args, **kwargs, ) -> Any: - """``asyncio.to_thread(fn, , *args, **kwargs)``. + """Run ``fn`` with a bridged client on the isolated worker pool. ``fn`` takes the client as its first argument, as every blocking helper - on the upload path does. + on the upload path does. Context variables are propagated like + :func:`asyncio.to_thread`, without consuming the loop default executor. """ - return await asyncio.to_thread(fn, bridge_client(client), *args, **kwargs) + loop = asyncio.get_running_loop() + bound = functools.partial(fn, bridge_client(client), *args, **kwargs) + context = contextvars.copy_context() + call = functools.partial(context.run, bound) + return await loop.run_in_executor(_BLOCKING_EXECUTOR, call) diff --git a/dashscope/resources/_oss_upload.py b/dashscope/resources/_oss_upload.py index c4ebae25..7a38d99b 100644 --- a/dashscope/resources/_oss_upload.py +++ b/dashscope/resources/_oss_upload.py @@ -25,8 +25,13 @@ import httpx +from dashscope import _error_registry as _registry from dashscope._logging import get_logger -from dashscope.errors import SdkError, check_response, error_from_status +from dashscope.errors import ( + InternalServerError, + check_response, + error_from_status, +) from dashscope.transport.http import default_user_agent __all__ = [ @@ -63,7 +68,26 @@ def _resolve_file_uri_path(file_uri: str) -> str: return os.path.expanduser(file_path) -def _policy_from_body(body: Dict[str, Any]) -> Dict[str, Any]: +def _invalid_policy_response( + response: httpx.Response, + body: Any, +) -> InternalServerError: + """Classify an unusable getPolicy response through ``internal_error``.""" + entry = _registry.INTERNAL_ERROR + data = body if isinstance(body, dict) else {} + return InternalServerError( + entry.error_msg, + code=entry.error_code, + status_code=entry.status_code, + request_id=( + data.get("request_id") or response.headers.get("x-request-id") + ), + body=body, + headers=response.headers, + ) + + +def _policy_from_response(response: httpx.Response) -> Dict[str, Any]: """Pull the policy out of the getPolicy envelope. ``data`` is the key the gateway answers with -- and the one 1.x read @@ -71,9 +95,15 @@ def _policy_from_body(body: Dict[str, Any]) -> Dict[str, Any]: embeddings response, say) through as a "policy", deferring the failure to a ``KeyError`` on some policy field far from here. """ + try: + body = response.json() + except ValueError as exc: + raise _invalid_policy_response(response, response.text) from exc + if not isinstance(body, dict): + raise _invalid_policy_response(response, body) policy = body.get("data") if not isinstance(policy, dict): - raise SdkError("upload policy response carries no data object") + raise _invalid_policy_response(response, body) return policy @@ -101,7 +131,7 @@ def get_upload_certificate( workspace=workspace, ) check_response(response) - return _policy_from_body(response.json()) + return _policy_from_response(response) def _post_to_oss( diff --git a/dashscope/resources/audio/__init__.py b/dashscope/resources/audio/__init__.py index 17ac8abc..6a6ea73c 100644 --- a/dashscope/resources/audio/__init__.py +++ b/dashscope/resources/audio/__init__.py @@ -10,9 +10,7 @@ ``voices`` / ``voice_design``. """ -import asyncio - -from dashscope.resources._blocking import bridge_client +from dashscope.resources._blocking import run_blocking from dashscope.resources.audio.asr import ( Asr, AsyncAsr, @@ -74,6 +72,11 @@ ] +def _call_resource(client, resource_cls, name, args, kwargs): + resource = resource_cls(client) + return getattr(resource, name)(*args, **kwargs) + + class _AsyncResourceProxy: """Run sync response parsing in a worker while reusing AsyncTransport.""" @@ -83,11 +86,13 @@ def __init__(self, client, resource_cls) -> None: def __getattr__(self, name): async def call(*args, **kwargs): - resource = self._resource_cls(bridge_client(self._client)) - return await asyncio.to_thread( - getattr(resource, name), - *args, - **kwargs, + return await run_blocking( + self._client, + _call_resource, + self._resource_cls, + name, + args, + kwargs, ) return call diff --git a/tests/test_oss_upload.py b/tests/test_oss_upload.py index f387693c..49eed1ec 100644 --- a/tests/test_oss_upload.py +++ b/tests/test_oss_upload.py @@ -7,6 +7,7 @@ from __future__ import annotations +import asyncio import os import httpx @@ -15,8 +16,8 @@ from dashscope import AsyncClient, Client from dashscope.errors import ( AuthenticationError, + InternalServerError, PermissionDeniedError, - SdkError, ) from dashscope.resources import _oss_upload from dashscope.resources._blocking import run_blocking @@ -132,20 +133,81 @@ def test_certificate_fetch_failure_raises(): @pytest.mark.parametrize( - "body", + "body,request_id", [ - {"request_id": "r-1"}, + ({"request_id": "r-1"}, "r-1"), # "output" is not this endpoint's envelope; accepting it would let # an unrelated body through as a policy and fail later on a missing # policy field instead of here. - {"request_id": "r-1", "output": _CERT}, + ({"request_id": "r-1", "output": _CERT}, "r-1"), + ([], "header-id"), + ("not-an-object", "header-id"), ], ) -def test_certificate_fetch_rejects_a_body_without_a_policy(body): - client = _make_client(lambda request: httpx.Response(200, json=body)) +def test_certificate_fetch_rejects_an_invalid_policy_body(body, request_id): + client = _make_client( + lambda request: httpx.Response( + 200, + json=body, + headers={"x-request-id": "header-id"}, + ), + ) + + with pytest.raises(InternalServerError) as caught: + _oss_upload.get_upload_certificate(client, "m") + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert caught.value.request_id == request_id + assert caught.value.body == body + assert caught.value.headers["x-request-id"] == "header-id" + - with pytest.raises(SdkError, match="no data object"): +def test_certificate_fetch_rejects_invalid_json_as_internal_error(): + client = _make_client( + lambda request: httpx.Response( + 200, + text="not-json", + headers={"x-request-id": "header-id"}, + ), + ) + + with pytest.raises(InternalServerError) as caught: _oss_upload.get_upload_certificate(client, "m") + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert caught.value.request_id == "header-id" + assert caught.value.body == "not-json" + assert isinstance(caught.value.__cause__, ValueError) + + +@pytest.mark.asyncio +async def test_run_blocking_leaves_the_default_executor_for_async_transport( + monkeypatch, +): + real_to_thread = asyncio.to_thread + default_executor_calls = [] + + async def tracked_to_thread(fn, *args, **kwargs): + default_executor_calls.append(fn) + return await real_to_thread(fn, *args, **kwargs) + + class ExecutorUsingTransport: + async def request(self): + return await asyncio.to_thread(lambda: "ok") + + monkeypatch.setattr(asyncio, "to_thread", tracked_to_thread) + client = type("Client", (), {"transport": ExecutorUsingTransport()})() + + result = await run_blocking( + client, + lambda bridged: bridged.transport.request(), + ) + + assert result == "ok" + # Only the async transport used the loop default executor. The outer + # blocking call ran on the isolated pool, so the two cannot starve each + # other when concurrent uploads fill the bridge pool. + assert len(default_executor_calls) == 1 @pytest.mark.asyncio From 9979f2e3c4fef327f1ff53c3be469b458d6963d0 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 29 Oct 2026 14:59:27 +0800 Subject: [PATCH 35/41] fix(errors): normalize response and websocket failures Map malformed successful JSON responses to the catalog internal error across resources, preserving response metadata and parse causes. Wire bounded WebSocket queue overflow to sdk.ws.QueueCapacityExceeded instead of IllegalState. --- dashscope/errors.py | 50 +++++++++++++++++++ dashscope/resources/_jobs.py | 15 ++++-- dashscope/resources/_oss_upload.py | 32 ++---------- dashscope/resources/apps.py | 5 +- dashscope/resources/chat/completions.py | 6 +-- dashscope/resources/decision.py | 4 +- dashscope/resources/deployments.py | 22 ++++---- dashscope/resources/embeddings.py | 6 +-- dashscope/resources/files.py | 17 ++++--- dashscope/resources/fine_tunes.py | 31 +++++++----- dashscope/resources/images.py | 6 +-- dashscope/resources/models.py | 9 ++-- dashscope/resources/multimodal/embeddings.py | 6 +-- dashscope/resources/reinforcement_learning.py | 27 ++++++---- dashscope/resources/rerank.py | 6 +-- dashscope/resources/tingwu.py | 10 ++-- dashscope/resources/tokenization.py | 6 +-- dashscope/transport/ws_connection.py | 5 +- dashscope/types/common.py | 19 ++++--- tests/test_audio_management.py | 8 ++- tests/test_audio_tts_http.py | 9 +++- tests/test_chat.py | 39 ++++++++++++++- tests/test_error_registry.py | 20 +++++--- tests/test_errors.py | 37 +++++++++++++- tests/test_sse.py | 6 ++- tests/test_ws_connection.py | 6 ++- 26 files changed, 279 insertions(+), 128 deletions(-) diff --git a/dashscope/errors.py b/dashscope/errors.py index d1afa30e..005f53b8 100644 --- a/dashscope/errors.py +++ b/dashscope/errors.py @@ -22,6 +22,7 @@ :class:`APITimeoutError`) are SdkError subclasses backed by the generic ``sdk.ConnectionError`` / ``sdk.Timeout`` entries, the websocket transport errors (:class:`WsEventError` / :class:`WsStateError` / +:class:`WsQueueCapacityExceededError` / :class:`WsBinaryOwnerConflictError`) are backed by the ``sdk.ws.*`` entries. AgentStudio and AgenticRL also raise catalog-backed v2 errors natively; :func:`error_from_sdk_exception` remains available for mounts @@ -194,6 +195,15 @@ class WsStateError(_CatalogSdkError): _SDK_ERROR_NAME = "sdk.ws.IllegalState" +class WsQueueCapacityExceededError(_CatalogSdkError): + """A bounded websocket task receive queue overflowed. + + Backed by the catalog's ``sdk.ws.QueueCapacityExceeded`` entry. + """ + + _SDK_ERROR_NAME = "sdk.ws.QueueCapacityExceeded" + + class WsBinaryOwnerConflictError(_CatalogSdkError): """A shared connection allows one binary owner per direction. @@ -523,6 +533,46 @@ def flush() -> Any: return body if body is not None else fallback +def internal_response_error( + response: httpx.Response, + body: Any, +) -> InternalServerError: + """Build the catalog ``internal_error`` for an unusable response body.""" + entry = _entry("internal_error") + data = body if isinstance(body, dict) else {} + return InternalServerError( + entry.error_msg, + code=entry.error_code, + status_code=entry.status_code, + request_id=( + data.get("request_id") or response.headers.get("x-request-id") + ), + body=body, + headers=response.headers, + ) + + +def response_json( + response: httpx.Response, + *, + require_object: bool = True, +) -> Any: + """Parse a successful JSON response through the v2 error contract. + + A response that cannot be decoded, or whose top-level shape is not the + object every DashScope resource envelope requires, is a server response + contract failure. Report it as the catalog ``internal_error`` rather than + leaking ``JSONDecodeError`` / ``AttributeError`` from the parser. + """ + try: + body = response.json() + except ValueError as exc: + raise internal_response_error(response, response.text) from exc + if require_object and not isinstance(body, dict): + raise internal_response_error(response, body) + return body + + def _parse_body(response: httpx.Response) -> Any: try: return response.json() diff --git a/dashscope/resources/_jobs.py b/dashscope/resources/_jobs.py index 23d22878..be8dd1f9 100644 --- a/dashscope/resources/_jobs.py +++ b/dashscope/resources/_jobs.py @@ -21,6 +21,7 @@ DashScopeError, check_response, error_from_business_code, + response_json, ) from dashscope.types.common import ( ResponseBase, @@ -360,7 +361,10 @@ def list( # pylint: disable=redefined-builtin workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(JobList.from_payload(response.json()), response) + return with_headers( + JobList.from_payload(response_json(response)), + response, + ) def wait( self, @@ -413,7 +417,7 @@ def wait( return self._check_canceled(current) def _parse(self, response: Any) -> Job: - payload = response.json() + payload = response_json(response) job = with_headers(Job.from_payload(payload), response) if job.status == FAILED and self._client.raise_on_error: raise error_from_business_code( @@ -557,7 +561,10 @@ async def list( # pylint: disable=redefined-builtin workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(JobList.from_payload(response.json()), response) + return with_headers( + JobList.from_payload(response_json(response)), + response, + ) async def wait( self, @@ -605,7 +612,7 @@ async def wait( return self._check_canceled(current) def _parse(self, response: Any) -> Job: - payload = response.json() + payload = response_json(response) job = with_headers(Job.from_payload(payload), response) if job.status == FAILED and self._client.raise_on_error: raise error_from_business_code( diff --git a/dashscope/resources/_oss_upload.py b/dashscope/resources/_oss_upload.py index 7a38d99b..e1e92d88 100644 --- a/dashscope/resources/_oss_upload.py +++ b/dashscope/resources/_oss_upload.py @@ -25,12 +25,12 @@ import httpx -from dashscope import _error_registry as _registry from dashscope._logging import get_logger from dashscope.errors import ( - InternalServerError, check_response, error_from_status, + internal_response_error, + response_json, ) from dashscope.transport.http import default_user_agent @@ -68,25 +68,6 @@ def _resolve_file_uri_path(file_uri: str) -> str: return os.path.expanduser(file_path) -def _invalid_policy_response( - response: httpx.Response, - body: Any, -) -> InternalServerError: - """Classify an unusable getPolicy response through ``internal_error``.""" - entry = _registry.INTERNAL_ERROR - data = body if isinstance(body, dict) else {} - return InternalServerError( - entry.error_msg, - code=entry.error_code, - status_code=entry.status_code, - request_id=( - data.get("request_id") or response.headers.get("x-request-id") - ), - body=body, - headers=response.headers, - ) - - def _policy_from_response(response: httpx.Response) -> Dict[str, Any]: """Pull the policy out of the getPolicy envelope. @@ -95,15 +76,10 @@ def _policy_from_response(response: httpx.Response) -> Dict[str, Any]: embeddings response, say) through as a "policy", deferring the failure to a ``KeyError`` on some policy field far from here. """ - try: - body = response.json() - except ValueError as exc: - raise _invalid_policy_response(response, response.text) from exc - if not isinstance(body, dict): - raise _invalid_policy_response(response, body) + body = response_json(response) policy = body.get("data") if not isinstance(policy, dict): - raise _invalid_policy_response(response, body) + raise internal_response_error(response, body) return policy diff --git a/dashscope/resources/apps.py b/dashscope/resources/apps.py index b1eefcad..422ec276 100644 --- a/dashscope/resources/apps.py +++ b/dashscope/resources/apps.py @@ -26,6 +26,7 @@ from dashscope.errors import ( check_response, error_from_business_code, + response_json, ) from dashscope.types.common import ( ResponseBase, @@ -445,7 +446,7 @@ def create( # pylint: disable=too-many-locals workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) def _events(self, response) -> Iterator[AppCompletion]: try: @@ -543,7 +544,7 @@ async def create( # pylint: disable=too-many-locals workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) async def _events(self, response) -> AsyncIterator[AppCompletion]: try: diff --git a/dashscope/resources/chat/completions.py b/dashscope/resources/chat/completions.py index 4a55dfbb..ac195e79 100644 --- a/dashscope/resources/chat/completions.py +++ b/dashscope/resources/chat/completions.py @@ -13,7 +13,7 @@ from typing_extensions import Literal -from dashscope.errors import response_error +from dashscope.errors import response_error, response_json from dashscope.protocols.base import ChatRequest from dashscope.resources._blocking import run_blocking from dashscope.resources._oss import ( @@ -691,7 +691,7 @@ def _send( if error_result is not None: return error_result return with_headers( - client.protocol.parse_completion(response.json()), + client.protocol.parse_completion(response_json(response)), response, ) @@ -742,7 +742,7 @@ async def _asend( if error_result is not None: return error_result return with_headers( - client.protocol.parse_completion(response.json()), + client.protocol.parse_completion(response_json(response)), response, ) diff --git a/dashscope/resources/decision.py b/dashscope/resources/decision.py index c81fc60a..9a444d64 100644 --- a/dashscope/resources/decision.py +++ b/dashscope/resources/decision.py @@ -25,7 +25,7 @@ from pydantic import BaseModel, ConfigDict, Field from dashscope._config import Config, compatible_mode_base_url -from dashscope.errors import response_error +from dashscope.errors import response_error, response_json from dashscope.types.common import ResponseBase, Usage, with_headers _PATH = "/systemone" @@ -193,7 +193,7 @@ def _checked_response(client, response) -> DecisionResponse: """ error = response_error(response) if error is None: - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) if client.raise_on_error: raise error return with_headers( diff --git a/dashscope/resources/deployments.py b/dashscope/resources/deployments.py index 0841c312..34c29241 100644 --- a/dashscope/resources/deployments.py +++ b/dashscope/resources/deployments.py @@ -22,7 +22,7 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.resources._sse import ( _aiter_sse, _iter_sse, @@ -184,7 +184,7 @@ def create( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) def get( self, @@ -203,7 +203,7 @@ def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) def list( self, @@ -224,7 +224,7 @@ def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) def scale( self, @@ -245,7 +245,7 @@ def scale( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) def delete( self, @@ -265,7 +265,7 @@ def delete( ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - JobStatus(**flatten_envelope(response.json())), + JobStatus(**flatten_envelope(response_json(response))), response, ) @@ -339,7 +339,7 @@ async def create( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) async def get( self, @@ -358,7 +358,7 @@ async def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) async def list( self, @@ -379,7 +379,7 @@ async def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) async def scale( self, @@ -400,7 +400,7 @@ async def scale( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) async def delete( self, @@ -420,7 +420,7 @@ async def delete( ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - JobStatus(**flatten_envelope(response.json())), + JobStatus(**flatten_envelope(response_json(response))), response, ) diff --git a/dashscope/resources/embeddings.py b/dashscope/resources/embeddings.py index 0c917a46..b2ae1508 100644 --- a/dashscope/resources/embeddings.py +++ b/dashscope/resources/embeddings.py @@ -14,7 +14,7 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, DEFAULT_POLL_INTERVAL, @@ -380,7 +380,7 @@ def create( ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - parse_embedding_response(response.json()), + parse_embedding_response(response_json(response)), response, ) @@ -424,6 +424,6 @@ async def create( ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - parse_embedding_response(response.json()), + parse_embedding_response(response_json(response)), response, ) diff --git a/dashscope/resources/files.py b/dashscope/resources/files.py index 46b16d57..e064350c 100644 --- a/dashscope/resources/files.py +++ b/dashscope/resources/files.py @@ -17,6 +17,7 @@ check_response, response_error, sdk_error, + response_json, ) from dashscope.types.common import ( ResponseBase, @@ -232,7 +233,7 @@ def upload( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_upload(response.json()), response) + return with_headers(_parse_upload(response_json(response)), response) def list( self, @@ -253,7 +254,7 @@ def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) def get( self, @@ -276,7 +277,7 @@ def get( response, raise_on_error=self._client.raise_on_error, ) - return with_headers(_parse_file(response.json()), response) + return with_headers(_parse_file(response_json(response)), response) def delete( self, @@ -299,7 +300,7 @@ def delete( response, raise_on_error=self._client.raise_on_error, ) - return with_headers(_parse_delete(response.json()), response) + return with_headers(_parse_delete(response_json(response)), response) class AsyncFiles: @@ -329,7 +330,7 @@ async def upload( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_upload(response.json()), response) + return with_headers(_parse_upload(response_json(response)), response) async def list( self, @@ -350,7 +351,7 @@ async def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) async def get( self, @@ -373,7 +374,7 @@ async def get( response, raise_on_error=self._client.raise_on_error, ) - return with_headers(_parse_file(response.json()), response) + return with_headers(_parse_file(response_json(response)), response) async def delete( self, @@ -396,4 +397,4 @@ async def delete( response, raise_on_error=self._client.raise_on_error, ) - return with_headers(_parse_delete(response.json()), response) + return with_headers(_parse_delete(response_json(response)), response) diff --git a/dashscope/resources/fine_tunes.py b/dashscope/resources/fine_tunes.py index bd03d773..6946ff55 100644 --- a/dashscope/resources/fine_tunes.py +++ b/dashscope/resources/fine_tunes.py @@ -39,6 +39,7 @@ DashScopeError, check_response, error_from_business_code, + response_json, ) from dashscope.resources._jobs import ( CANCELED, @@ -271,7 +272,7 @@ def create( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) def get( self, @@ -290,7 +291,7 @@ def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) def list( self, @@ -311,7 +312,7 @@ def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) def cancel( self, @@ -330,7 +331,7 @@ def cancel( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) def delete( self, @@ -349,7 +350,7 @@ def delete( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) def logs( self, @@ -372,7 +373,10 @@ def logs( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_logs(job_id, response.json()), response) + return with_headers( + _parse_logs(job_id, response_json(response)), + response, + ) def stream_events( self, @@ -496,7 +500,7 @@ async def create( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) async def get( self, @@ -515,7 +519,7 @@ async def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) async def list( self, @@ -536,7 +540,7 @@ async def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) async def cancel( self, @@ -555,7 +559,7 @@ async def cancel( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) async def delete( self, @@ -574,7 +578,7 @@ async def delete( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) async def logs( self, @@ -597,7 +601,10 @@ async def logs( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_logs(job_id, response.json()), response) + return with_headers( + _parse_logs(job_id, response_json(response)), + response, + ) async def stream_events( self, diff --git a/dashscope/resources/images.py b/dashscope/resources/images.py index d432d4a9..926cc0a9 100644 --- a/dashscope/resources/images.py +++ b/dashscope/resources/images.py @@ -24,7 +24,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.resources._blocking import run_blocking from dashscope.resources._jobs import ( DEFAULT_MAX_INTERVAL, @@ -513,7 +513,7 @@ def generate( raise_on_error=self._client.raise_on_error, ) return with_headers( - _parse_sync_job(response.json()), + _parse_sync_job(response_json(response)), response, ) job = self.jobs.create( @@ -783,7 +783,7 @@ async def generate( raise_on_error=self._client.raise_on_error, ) return with_headers( - _parse_sync_job(response.json()), + _parse_sync_job(response_json(response)), response, ) job = await self.jobs.create( diff --git a/dashscope/resources/models.py b/dashscope/resources/models.py index 334ebbd6..633229ac 100644 --- a/dashscope/resources/models.py +++ b/dashscope/resources/models.py @@ -12,6 +12,7 @@ ErrorCode, check_response, error_from_business_code, + response_json, ) from dashscope.types.common import ( ResponseBase, @@ -118,7 +119,7 @@ def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - result = _parse_list(response.json()) + result = _parse_list(response_json(response)) if provider: result.models = [ m for m in result.models if _match_provider(m, provider) @@ -144,7 +145,7 @@ def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - body = response.json() + body = response_json(response) models = _parse_get_models(body) if not models: if self._client.raise_on_error: @@ -185,7 +186,7 @@ async def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - result = _parse_list(response.json()) + result = _parse_list(response_json(response)) if provider: result.models = [ m for m in result.models if _match_provider(m, provider) @@ -211,7 +212,7 @@ async def get( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - body = response.json() + body = response_json(response) models = _parse_get_models(body) if not models: if self._client.raise_on_error: diff --git a/dashscope/resources/multimodal/embeddings.py b/dashscope/resources/multimodal/embeddings.py index b6cc1713..aee7379d 100644 --- a/dashscope/resources/multimodal/embeddings.py +++ b/dashscope/resources/multimodal/embeddings.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, ConfigDict -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.resources._blocking import run_blocking from dashscope.resources._oss import upload_embedding_media from dashscope.resources.embeddings import ( @@ -178,7 +178,7 @@ def create( # pylint: disable=too-many-arguments ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - parse_embedding_response(response.json()), + parse_embedding_response(response_json(response)), response, ) @@ -240,6 +240,6 @@ async def create( # pylint: disable=too-many-arguments ) check_response(response, raise_on_error=self._client.raise_on_error) return with_headers( - parse_embedding_response(response.json()), + parse_embedding_response(response_json(response)), response, ) diff --git a/dashscope/resources/reinforcement_learning.py b/dashscope/resources/reinforcement_learning.py index 1d912d2f..35d9f3fd 100644 --- a/dashscope/resources/reinforcement_learning.py +++ b/dashscope/resources/reinforcement_learning.py @@ -41,6 +41,7 @@ DashScopeError, check_response, error_from_business_code, + response_json, ) from dashscope.resources._jobs import ( CANCELED, @@ -264,7 +265,7 @@ def fetch( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) def wait( self, @@ -329,7 +330,7 @@ def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) def cancel( self, @@ -348,7 +349,7 @@ def cancel( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) def delete( self, @@ -367,7 +368,7 @@ def delete( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) def logs( self, @@ -390,7 +391,10 @@ def logs( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_logs(job_id, response.json()), response) + return with_headers( + _parse_logs(job_id, response_json(response)), + response, + ) class Rl: @@ -595,7 +599,7 @@ async def fetch( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_job(response.json()), response) + return with_headers(_parse_job(response_json(response)), response) async def wait( self, @@ -664,7 +668,7 @@ async def list( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_list(response.json()), response) + return with_headers(_parse_list(response_json(response)), response) async def cancel( self, @@ -683,7 +687,7 @@ async def cancel( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) async def delete( self, @@ -702,7 +706,7 @@ async def delete( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_status(response.json()), response) + return with_headers(_parse_status(response_json(response)), response) async def logs( self, @@ -725,7 +729,10 @@ async def logs( workspace=workspace, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse_logs(job_id, response.json()), response) + return with_headers( + _parse_logs(job_id, response_json(response)), + response, + ) class AsyncRl: diff --git a/dashscope/resources/rerank.py b/dashscope/resources/rerank.py index 5edae131..8e4c5936 100644 --- a/dashscope/resources/rerank.py +++ b/dashscope/resources/rerank.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.types.common import ( ResponseBase, Usage, @@ -125,7 +125,7 @@ def create( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) class AsyncRerank: @@ -165,4 +165,4 @@ async def create( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) diff --git a/dashscope/resources/tingwu.py b/dashscope/resources/tingwu.py index 0f8fe615..763354dc 100644 --- a/dashscope/resources/tingwu.py +++ b/dashscope/resources/tingwu.py @@ -17,7 +17,11 @@ from pydantic import ConfigDict, Field -from dashscope.errors import check_response, error_from_business_code +from dashscope.errors import ( + check_response, + error_from_business_code, + response_json, +) from dashscope.transport.ws import Connector, WsSession from dashscope.transport.ws_connection import WsConnection, WsTask from dashscope.types.common import ( @@ -245,7 +249,7 @@ def call( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - envelope = response.json() + envelope = response_json(response) data = response_envelope(envelope) output = envelope.get("output") data["output"] = output if isinstance(output, dict) else {} @@ -331,7 +335,7 @@ async def call( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - envelope = response.json() + envelope = response_json(response) data = response_envelope(envelope) output = envelope.get("output") data["output"] = output if isinstance(output, dict) else {} diff --git a/dashscope/resources/tokenization.py b/dashscope/resources/tokenization.py index c0fcd592..62e609ad 100644 --- a/dashscope/resources/tokenization.py +++ b/dashscope/resources/tokenization.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Union -from dashscope.errors import check_response +from dashscope.errors import check_response, response_json from dashscope.types.common import ( ResponseBase, Usage, @@ -105,7 +105,7 @@ def count( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) class AsyncTokenization: @@ -139,4 +139,4 @@ async def count( sdk_module=MODULE, ) check_response(response, raise_on_error=self._client.raise_on_error) - return with_headers(_parse(response.json()), response) + return with_headers(_parse(response_json(response)), response) diff --git a/dashscope/transport/ws_connection.py b/dashscope/transport/ws_connection.py index dd83580d..43165757 100644 --- a/dashscope/transport/ws_connection.py +++ b/dashscope/transport/ws_connection.py @@ -44,6 +44,7 @@ APIConnectionError, APITimeoutError, WsBinaryOwnerConflictError, + WsQueueCapacityExceededError, WsStateError, ) from dashscope.transport.ws import ( @@ -126,9 +127,7 @@ def _enqueue(self, item: _QueueItem) -> None: # fail only this task and let its reader surface the error. item = ( _ERROR, - WsStateError( - detail="websocket task receive queue is full", - ), + WsQueueCapacityExceededError(), ) self._error = item[1] # Preserve terminal visibility without allowing an unbounded diff --git a/dashscope/types/common.py b/dashscope/types/common.py index ff5595d1..0c08f563 100644 --- a/dashscope/types/common.py +++ b/dashscope/types/common.py @@ -9,7 +9,11 @@ import httpx from pydantic import BaseModel, ConfigDict, Field, model_validator -from dashscope.errors import error_from_business_code, error_from_status +from dashscope.errors import ( + error_from_business_code, + error_from_status, + response_json, +) __all__ = [ "JobStatus", @@ -144,12 +148,13 @@ def response_body( ) -> Dict[str, Any]: """Opt in to strict HTTP envelopes, retaining non-JSON service failures.""" success = 200 <= response.status_code < 300 - try: - raw = response.json() - except ValueError: - if success: - raise - raw = None + if success: + raw = response_json(response) + else: + try: + raw = response.json() + except ValueError: + raw = None if isinstance(raw, dict): body = raw else: diff --git a/tests/test_audio_management.py b/tests/test_audio_management.py index 86bb6411..c9410804 100644 --- a/tests/test_audio_management.py +++ b/tests/test_audio_management.py @@ -685,9 +685,13 @@ def test_management_malformed_json_is_not_suppressed(resource, raise_on_error): lambda request: httpx.Response(200, content=b'{"output":'), raise_on_error=raise_on_error, ) - # Compatibility mode suppresses service errors, not parsing failures. - with pytest.raises(ValueError): + # Compatibility mode suppresses service errors, not response-contract + # failures; malformed JSON uses the catalog internal_error. + with pytest.raises(InternalServerError) as caught: getattr(client.audio, resource).list() + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert isinstance(caught.value.__cause__, ValueError) @pytest.mark.parametrize("resource", _MANAGEMENT_RESOURCES) diff --git a/tests/test_audio_tts_http.py b/tests/test_audio_tts_http.py index f2353fe0..670a7e2d 100644 --- a/tests/test_audio_tts_http.py +++ b/tests/test_audio_tts_http.py @@ -496,8 +496,11 @@ def test_http_malformed_json_is_not_suppressed(raise_on_error): lambda request: httpx.Response(200, content=b'{"output":'), raise_on_error=raise_on_error, ) - with pytest.raises(ValueError): + with pytest.raises(InternalServerError) as caught: client.audio.tts.http.synthesize("你好", **_TTS_ARGS) + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert isinstance(caught.value.__cause__, ValueError) @pytest.mark.parametrize("method", ["synthesize", "stream"]) @@ -658,8 +661,10 @@ def test_http_stream_malformed_json_response_raises_and_closes( headers={"content-type": "application/json"}, raise_on_error=raise_on_error, ) - with pytest.raises(ValueError): + with pytest.raises(InternalServerError) as caught: list(client.audio.tts.http.stream("你好", **_TTS_ARGS)) + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 assert wire.closed and response.is_closed diff --git a/tests/test_chat.py b/tests/test_chat.py index 0132e7ae..359d641a 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -13,7 +13,11 @@ import pytest from dashscope import AsyncClient, Client -from dashscope.errors import ContentFilterError, RateLimitError +from dashscope.errors import ( + ContentFilterError, + InternalServerError, + RateLimitError, +) from dashscope.types.completions import parse_completion BASE_URL = "https://api.test.local/api/v1" @@ -22,6 +26,39 @@ OPENAI_PATH = "/api/v1/chat/completions" +@pytest.mark.asyncio +@pytest.mark.parametrize("asynchronous", [False, True]) +async def test_chat_maps_invalid_success_json_to_internal_error(asynchronous): + response = httpx.Response( + 200, + text="not-json", + headers={"x-request-id": "header-id"}, + ) + client = ( + _make_async_client(lambda request: response, max_retries=0) + if asynchronous + else _make_client(lambda request: response, max_retries=0) + ) + try: + with pytest.raises(InternalServerError) as caught: + call = client.chat.completions.create( + model="qwen-plus", + messages="hi", + ) + if asynchronous: + await call + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert caught.value.request_id == "header-id" + assert caught.value.body == "not-json" + assert isinstance(caught.value.__cause__, ValueError) + finally: + if asynchronous: + await client.aclose() + else: + client.close() + + def _sse(events): out = [] for i, data in enumerate(events, 1): diff --git a/tests/test_error_registry.py b/tests/test_error_registry.py index 5b0f1731..fe858214 100644 --- a/tests/test_error_registry.py +++ b/tests/test_error_registry.py @@ -310,12 +310,12 @@ def test_unfamily_entries_are_the_generic_sdk_ones(): def test_ws_family_entries(): - """The five websocket entries; three wired, two carried for parity. + """The five websocket entries; four wired, one carried for parity. - ProtocolError / IllegalState / BinaryOwnerConflict back the v2 - websocket transport's exception classes. UpstreamQueueFull (sync - send, no send buffer) and QueueCapacityExceeded (unbounded per-task - queues) describe mechanisms the Python transport does not have. + ProtocolError / IllegalState / QueueCapacityExceeded / + BinaryOwnerConflict back the v2 websocket transport's exception classes. + UpstreamQueueFull remains parity-only because sends are synchronous and + the Python transport has no upstream queue. """ entries = _by_name(SDK) expected = { @@ -332,9 +332,17 @@ def test_ws_family_entries(): wired = { "sdk.ws.ProtocolError": "WsEventError", "sdk.ws.IllegalState": "WsStateError", + "sdk.ws.QueueCapacityExceeded": ("WsQueueCapacityExceededError"), "sdk.ws.BinaryOwnerConflict": "WsBinaryOwnerConflictError", } for name, cls_name in wired.items(): cls = getattr(v2_errors, cls_name) assert cls._SDK_ERROR_NAME == name - assert entries[name].vars == ["detail"] + expected_vars = ( + [] + if name.endswith("QueueCapacityExceeded") + else [ + "detail", + ] + ) + assert entries[name].vars == expected_vars diff --git a/tests/test_errors.py b/tests/test_errors.py index 84ccacfc..48bd0d37 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -40,6 +40,7 @@ ServiceUnavailableError, WsBinaryOwnerConflictError, WsEventError, + WsQueueCapacityExceededError, WsStateError, check_response, error_from_business_code, @@ -628,8 +629,36 @@ def test_strict_http_envelope_retains_non_object_failures(raw, raise_on_error): @pytest.mark.parametrize("raw", [None, [], "not an envelope", 0]) @pytest.mark.parametrize("raise_on_error", [False, True]) def test_strict_http_envelope_rejects_non_object_success(raw, raise_on_error): - with pytest.raises(ValueError): - response_body(_resp(200, raw), raise_on_error=raise_on_error) + response = httpx.Response( + 200, + content=json.dumps(raw), + headers={ + "content-type": "application/json", + "x-request-id": "header-id", + }, + ) + with pytest.raises(InternalServerError) as caught: + response_body(response, raise_on_error=raise_on_error) + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert caught.value.request_id == "header-id" + assert caught.value.body == raw + assert caught.value.headers["x-request-id"] == "header-id" + + +def test_strict_http_envelope_rejects_invalid_json_as_internal_error(): + response = httpx.Response( + 200, + text="not-json", + headers={"x-request-id": "header-id"}, + ) + with pytest.raises(InternalServerError) as caught: + response_body(response) + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert caught.value.request_id == "header-id" + assert caught.value.body == "not-json" + assert isinstance(caught.value.__cause__, ValueError) def test_error_code_enum(): @@ -711,6 +740,10 @@ def test_response_error_is_not_a_public_catalog_entry_or_class(): [ (WsEventError, "sdk.ws.ProtocolError"), (WsStateError, "sdk.ws.IllegalState"), + ( + WsQueueCapacityExceededError, + "sdk.ws.QueueCapacityExceeded", + ), (WsBinaryOwnerConflictError, "sdk.ws.BinaryOwnerConflict"), ], ) diff --git a/tests/test_sse.py b/tests/test_sse.py index 7bd9f7b9..eb49c047 100644 --- a/tests/test_sse.py +++ b/tests/test_sse.py @@ -16,6 +16,7 @@ from dashscope.errors import ( ContentFilterError, DashScopeError, + InternalServerError, RateLimitError, ) from dashscope.resources._sse import _aiter_sse, _iter_sse, _stream_error @@ -600,8 +601,11 @@ def test_audio_sse_sync_malformed_json_response(): content=b'{"output":', headers={"content-type": "application/json"}, ) - with pytest.raises(ValueError): + with pytest.raises(InternalServerError) as caught: list(_iter_audio_sse(response)) + assert caught.value.code == "InternalServerError" + assert caught.value.status_code == 500 + assert isinstance(caught.value.__cause__, ValueError) @pytest.mark.asyncio diff --git a/tests/test_ws_connection.py b/tests/test_ws_connection.py index d8a5efc7..f018dd08 100644 --- a/tests/test_ws_connection.py +++ b/tests/test_ws_connection.py @@ -15,6 +15,7 @@ APIConnectionError, DashScopeError, WsBinaryOwnerConflictError, + WsQueueCapacityExceededError, WsStateError, ) from dashscope.transport.ws import WsEventError, WsSession @@ -250,9 +251,10 @@ def test_slow_consumer_queue_full_fails_only_that_task(): assert not conn.closed assert not conn._ws.closed - # t1 sees a terminal WsStateError, isolated to that task. Drain any + # t1 sees the catalog queue-capacity error, isolated to that task. + # Drain any # frames queued ahead of the overflow marker, then expect the raise. - with pytest.raises(WsStateError, match="queue is full"): + with pytest.raises(WsQueueCapacityExceededError, match="capacity"): while True: t1.recv() assert not conn.closed From a38d1a4dbef53dd0c6a97cbdafdcb1daf07c518a Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 29 Oct 2026 15:00:09 +0800 Subject: [PATCH 36/41] fix(compat): preserve lazy modules and refresh quickstart Restore the historical dashscope.tokenizers package attribute and type stub. Re-resolve quickstart configuration before each call, safely retiring stale clients after in-flight requests finish and closing hidden pools at process exit. --- dashscope/__init__.py | 1 + dashscope/__init__.pyi | 1 + dashscope/_quickstart.py | 109 ++++++++++++++++++++++++++++++++----- tests/test_package_root.py | 64 ++++++++++++++++++++++ 4 files changed, 162 insertions(+), 13 deletions(-) diff --git a/dashscope/__init__.py b/dashscope/__init__.py index de229132..450c9eb3 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -179,6 +179,7 @@ def set_region(region: str, workspace_id: str = None): "protocol", "rerank", "threads", + "tokenizers", "utils", }, ) diff --git a/dashscope/__init__.pyi b/dashscope/__init__.pyi index affbf1dd..46a54671 100644 --- a/dashscope/__init__.pyi +++ b/dashscope/__init__.pyi @@ -61,6 +61,7 @@ nlp: Any protocol: Any rerank: Any threads: Any +tokenizers: Any utils: Any # --------------------------------------------------------------------------- diff --git a/dashscope/_quickstart.py b/dashscope/_quickstart.py index 602adcda..bba2a10e 100644 --- a/dashscope/_quickstart.py +++ b/dashscope/_quickstart.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""Module-level quickstart entry points backed by a lazy singleton Client.""" +"""Module-level quickstart entry points backed by a managed Client.""" from __future__ import annotations +import atexit import threading -from typing import Any, Optional +from typing import Any, Dict, Optional, Set +from dashscope._config import Config, resolve_config from dashscope.client import Client from dashscope.types.completions import ChatCompletion @@ -14,25 +16,106 @@ _lock = threading.Lock() _client: Optional[Client] = None +_active_clients: Dict[Client, int] = {} +_retired_clients: Set[Client] = set() + + +def _new_client(config: Config) -> Client: + """Construct the quickstart client from one resolved config snapshot.""" + return Client( + api_key=config.api_key, + base_url=config.configured_base_url, + workspace=config.workspace, + region=config.region, + protocol=config.protocol, + timeout=config.timeout, + max_retries=config.max_retries, + ) + + +def _safe_close(client: Client) -> None: + try: + client.close() + except Exception: + # Cleanup must never hide the result of the user's chat call or make + # interpreter shutdown noisy. + pass + + +def _select_client(*, acquire: bool) -> Client: + """Return a client for the current settings, replacing stale instances.""" + global _client + desired = resolve_config() + close_after_unlock: Optional[Client] = None + with _lock: + current = _client + if current is None or current.config != desired: + replacement = _new_client(desired) + _client = replacement + if current is not None: + if _active_clients.get(current, 0): + _retired_clients.add(current) + else: + close_after_unlock = current + current = replacement + if acquire: + _active_clients[current] = _active_clients.get(current, 0) + 1 + if close_after_unlock is not None: + _safe_close(close_after_unlock) + return current def _get_client() -> Client: + """Return the configuration-aware singleton (private test hook).""" + return _select_client(acquire=False) + + +def _release_client(client: Client) -> None: + close_after_unlock = False + with _lock: + active = _active_clients.get(client, 0) + if active <= 1: + _active_clients.pop(client, None) + if client in _retired_clients: + _retired_clients.remove(client) + close_after_unlock = True + else: + _active_clients[client] = active - 1 + if close_after_unlock: + _safe_close(client) + + +def _close_clients() -> None: + """Best-effort cleanup for the hidden quickstart connection pools.""" global _client - if _client is None: - with _lock: - if _client is None: - _client = Client() - return _client + with _lock: + clients = set(_active_clients) | _retired_clients + if _client is not None: + clients.add(_client) + _client = None + _active_clients.clear() + _retired_clients.clear() + for client in clients: + _safe_close(client) def chat(model: str, prompt: str, **kwargs: Any) -> ChatCompletion: """One-call chat entry: ``dashscope.chat("qwen-plus", "Tell me a joke")``. Extra keyword arguments are forwarded to - ``client.chat.completions.create``. + ``client.chat.completions.create``. Global settings and environment + changes are re-resolved before each call; an in-flight call keeps its + previous client alive until that call finishes. """ - return _get_client().chat.completions.create( - model=model, - messages=[prompt], - **kwargs, - ) + client = _select_client(acquire=True) + try: + return client.chat.completions.create( + model=model, + messages=[prompt], + **kwargs, + ) + finally: + _release_client(client) + + +atexit.register(_close_clients) diff --git a/tests/test_package_root.py b/tests/test_package_root.py index 7bca4b2a..5fdc85c8 100644 --- a/tests/test_package_root.py +++ b/tests/test_package_root.py @@ -8,6 +8,7 @@ from __future__ import annotations import json +from dataclasses import replace import httpx import pytest @@ -65,6 +66,16 @@ def test_legacy_exports_untouched(): assert hasattr(dashscope, "save_api_key") +def test_plain_import_keeps_the_historical_tokenizers_module(): + # Regression guard: the package root exposed this child immediately in + # 1.x. Accessing it must not require an explicit import of the child first. + assert dashscope.tokenizers is __import__( + "dashscope.tokenizers", + fromlist=["tokenizers"], + ) + assert "tokenizers" in dir(dashscope) + + def test_tokenizers_keep_the_historical_legacy_implementation(): from dashscope.tokenizers import get_tokenizer, list_tokenizers @@ -85,6 +96,52 @@ def test_root_uses_current_save_api_key(): assert dashscope.save_api_key.__module__ == "dashscope._config" +def test_quickstart_rebuilds_after_configuration_changes( + monkeypatch, + tmp_path, +): + first_config = _config.resolve_config( + api_key="sk-first", + base_url=BASE_URL, + config_path=tmp_path / "absent.toml", + ) + second_config = replace(first_config, api_key="sk-second") + current = {"config": first_config} + + class FakeClient: + def __init__(self, config): + self.config = config + self.closed = False + + def close(self): + self.closed = True + + monkeypatch.setattr(_quickstart, "_client", None) + monkeypatch.setattr(_quickstart, "_active_clients", {}) + monkeypatch.setattr(_quickstart, "_retired_clients", set()) + monkeypatch.setattr( + _quickstart, + "resolve_config", + lambda: current["config"], + ) + monkeypatch.setattr( + _quickstart, + "_new_client", + lambda config: FakeClient(config), + ) + + first = _quickstart._select_client(acquire=True) + current["config"] = second_config + second = _quickstart._get_client() + + assert second is not first + assert second.config.api_key == "sk-second" + # The old pool stays alive while an earlier call is still using it. + assert not first.closed + _quickstart._release_client(first) + assert first.closed + + def test_root_save_api_key_roundtrip(tmp_path): target = tmp_path / "nested" / "api_key" dashscope.save_api_key("sk-roundtrip", str(target)) @@ -123,6 +180,13 @@ def handler(request): http_client=http_client, ) monkeypatch.setattr(_quickstart, "_client", client) + monkeypatch.setattr(_quickstart, "_active_clients", {}) + monkeypatch.setattr(_quickstart, "_retired_clients", set()) + monkeypatch.setattr( + _quickstart, + "resolve_config", + lambda: client.config, + ) resp = dashscope.chat("qwen-plus", "Tell me a joke", temperature=0.2) From acdfa417e18106e0898f312aff782447d2ccd590 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 29 Oct 2026 15:46:58 +0800 Subject: [PATCH 37/41] feat(common): add user-agent header support - Introduced get_user_agent function in utils module - Added user-agent header to Encryption class API requests - Added user-agent header to fine-tuning related requests - Updated request headers to include SDK identification info --- .../applications/finetune/reinforcement/common/utils.py | 8 +++++--- dashscope/legacy_1x/api_entities/encryption.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dashscope/applications/finetune/reinforcement/common/utils.py b/dashscope/applications/finetune/reinforcement/common/utils.py index 032c6b04..9eba40a6 100644 --- a/dashscope/applications/finetune/reinforcement/common/utils.py +++ b/dashscope/applications/finetune/reinforcement/common/utils.py @@ -20,7 +20,7 @@ retry_if_exception_type, ) -from dashscope._shared._sdk_ident import get_sdk_headers +from dashscope._shared._sdk_ident import get_sdk_headers, get_user_agent from .. import logger from .errors import ( InputError, @@ -175,8 +175,9 @@ async def client_fc( url=url, headers={ "Content-Type": content_type, - "Authorization": "Bearer " + api_key, + "user-agent": get_user_agent(), **get_sdk_headers(module="finetune"), + "Authorization": "Bearer " + api_key, }, data=request_data, timeout=HTTP_REQUEST_TIMEOUT, @@ -615,8 +616,9 @@ async def to_bailian_data(files: List[FileSpec]) -> List[str]: OutputError: If file upload fails """ headers = { - "Authorization": f"Bearer {DASHSCOPE_API_KEY}", + "user-agent": get_user_agent(), **get_sdk_headers(module="finetune"), + "Authorization": f"Bearer {DASHSCOPE_API_KEY}", } form_data = FormData() uploaded_files = [] diff --git a/dashscope/legacy_1x/api_entities/encryption.py b/dashscope/legacy_1x/api_entities/encryption.py index c6dac207..deac4759 100644 --- a/dashscope/legacy_1x/api_entities/encryption.py +++ b/dashscope/legacy_1x/api_entities/encryption.py @@ -21,7 +21,7 @@ from dashscope.legacy_1x.common.logging import logger from dashscope.legacy_1x.common.env import resolve_base_url from dashscope.legacy_1x.common.api_key import get_default_api_key -from dashscope.legacy_1x.common.utils import get_sdk_headers +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class Encryption: @@ -100,8 +100,9 @@ def _get_public_keys(self): api_key = self._api_key or get_default_api_key() url = resolve_base_url(base_url) + "/public-keys/latest" headers = { - "Authorization": f"Bearer {api_key}", + "user-agent": get_user_agent(), **get_sdk_headers(module="utils"), + "Authorization": f"Bearer {api_key}", } response = requests.get( From e8abe3258601f471b38642a8d79a4465bb748078 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 29 Oct 2026 18:20:08 +0800 Subject: [PATCH 38/41] refactor(audio): remove preview warnings and refactor voice customization - Removed all warnings and comments about 2.0 audio API preview status - Removed warnings import and related FutureWarning notices - Refactored VoiceDesign class to use voice_id field uniformly instead of voice - Updated voice_design method parameters and field mappings - Added support for language_hints parameter - Improved DesignedVoice response model data structure - Adjusted documentation in examples and README - Optimized real-time WebSocket task queue capacity error handling --- README.md | 98 +++++++-------- README_zh.md | 90 ++++++++------ dashscope/client.py | 25 +--- dashscope/resources/audio/__init__.py | 9 +- dashscope/resources/audio/customization.py | 99 +++++++++------ examples/README.md | 2 +- examples/README_zh.md | 2 +- tests/test_audio_management.py | 138 ++++++++++++--------- 8 files changed, 247 insertions(+), 216 deletions(-) diff --git a/README.md b/README.md index 843c285d..febbc493 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The DashScope Python SDK is the official client for [Alibaba Cloud Model Studio | Errors | Inspect `resp.code` / `resp.message` by hand | Typed `DashScopeError` exceptions raised by default *(evolving)* | | Streaming | Generator of raw frames | `ChatStream` with `.text_stream`, `.reasoning_stream`, `.get_final_message()` | | Protocols | DashScope native + a separate compatible-mode entry | One client speaks `native`, `openai`, or `anthropic` via `protocol=` | -| Async | Separate `Aio*` classes | `dashscope.AsyncClient()` mirrors the whole sync surface | +| Async | Separate `Aio*` classes | `dashscope.AsyncClient()` mirrors the sync surface (except the duplex audio WebSocket sessions) | | Connections | A fresh session per call | One keep-alive pool per client, shared across resources | | Config | Module globals (`dashscope.api_key`, `set_region`) | Layered: args → `dashscope.settings` → env → `~/.dashscope/config.toml` | @@ -134,6 +134,8 @@ low_latency = client.with_options(timeout=10.0, max_retries=0) Any call also accepts per-request `api_key=`, `workspace=`, and `timeout=` overrides. +Need proxies, custom connection limits or event hooks? Pass your own `http_client=httpx.Client(...)` (`httpx.AsyncClient` for `AsyncClient`). The SDK still applies authentication, retries and absolute endpoint URLs on top of it, and never closes a client it does not own. + ### Protocols One client can speak three wire protocols, selected with `protocol=` (or `DASHSCOPE_PROTOCOL` / `settings.protocol`). The request/response objects stay the same canonical, typed shape regardless of protocol — only the wire format changes: @@ -185,6 +187,8 @@ for text in stream.text_stream: # content deltas By default each frame is incremental. Pass `incremental_output=False` to receive cumulative snapshots instead (the SDK merges deltas for you). +Thinking models surface reasoning on `.reasoning_stream` and, in the merged message, as `message.reasoning_content`. Under `protocol="anthropic"` the SDK also keeps `message.reasoning_signature` plus the raw `thinking` / `redacted_thinking` blocks in `message.reasoning_blocks`, so echoing the assistant message back on the next turn preserves extended thinking. + > **Note:** the SDK does **not** auto-retry streaming requests. Wrap stream consumption in your own retry/backoff for production — see [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py). ### Errors @@ -206,7 +210,12 @@ except errors.DashScopeError as exc: # base class for all SDK errors print("request failed:", exc.code, exc.message) ``` -The hierarchy includes `APIConnectionError` / `APITimeoutError` (network) and, under `APIStatusError`: `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `RequestTooLargeError`, `RateLimitError`, `InternalServerError`, `ServiceUnavailableError`, `GatewayTimeoutError`, and `ContentFilterError`. +Everything derives from `DashScopeError`, in two families: + +- **Server errors** — under `APIStatusError`: `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `RequestTooLargeError`, `RateLimitError`, `InternalServerError`, `ServiceUnavailableError`, `GatewayTimeoutError`, and `ContentFilterError` (a `BadRequestError` subclass). +- **Client-side errors** — under `SdkError`: `APIConnectionError` / `APITimeoutError` for network failures, plus the WebSocket family `WsEventError`, `WsStateError`, `WsQueueCapacityExceededError` and `WsBinaryOwnerConflictError`. Each carries a stable `sdk.*` code. + +A 200 response whose body does not parse into the expected envelope is reported as `InternalServerError` rather than leaking a decoder exception. Prefer to inspect errors inline instead of catching? Build the client with `raise_on_error=False` — failures come back as a normal response object with `is_error=True`, `code`, `message`, and `status_code`: @@ -302,7 +311,7 @@ Notes: ## Capabilities -Every capability is a resource on the client. Sync and async clients expose the same surface (async methods are `await`ed). +Every capability is a resource on the client. Sync and async clients expose the same surface (async methods are `await`ed) — with one exception: the duplex WebSocket audio sessions are sync-only by design, as noted below. | Capability | 2.0 resource | Example | |------------|--------------|---------| @@ -315,7 +324,10 @@ Every capability is a resource on the client. Sync and async clients expose the | Token counting | `client.tokenization.count(...)` | [`text_generation/tokenization.py`](examples/01_atomic_capabilities/text_generation/tokenization.py) | | Image generation | `client.images.generate(...)` (+ `.jobs`) | [`image_video_synthesis/images.py`](examples/01_atomic_capabilities/image_video_synthesis/images.py) | | Video generation | `client.videos.generate(...)` (+ `.jobs`) | [`image_video_synthesis/videos.py`](examples/01_atomic_capabilities/image_video_synthesis/videos.py) | -| Speech: ASR / TTS / realtime *(preview)* | `client.audio.asr` · `client.audio.tts` · `client.audio.realtime` | [`audio/`](examples/01_atomic_capabilities/audio/) | +| Speech recognition (ASR) | Batch `client.audio.asr.transcribe(...)` / `.jobs`; realtime `client.audio.asr.recognize(...)` | [`audio/asr_file.py`](examples/01_atomic_capabilities/audio/asr_file.py) · [`audio/asr_websocket.py`](examples/01_atomic_capabilities/audio/asr_websocket.py) | +| Speech synthesis (TTS) | HTTP `client.audio.tts.http.synthesize(...)` / `.stream(...)`; WebSocket `client.audio.tts.connect(...)` | [`audio/tts_http.py`](examples/01_atomic_capabilities/audio/tts_http.py) · [`audio/tts_websocket.py`](examples/01_atomic_capabilities/audio/tts_websocket.py) | +| Realtime omni dialog | `client.audio.realtime.connect(...)` | [`audio/realtime_omni.py`](examples/01_atomic_capabilities/audio/realtime_omni.py) | +| Voice & vocabulary management | `client.audio.voices` · `.voice_design` · `.vocabularies` · `.phrases` | [`audio/voice_clone.py`](examples/01_atomic_capabilities/audio/voice_clone.py) | | Meeting & audio understanding (TingWu) | `client.tingwu.call(...)` / `.realtime(...)` | [`audio/tingwu.py`](examples/01_atomic_capabilities/audio/tingwu.py) | | Files | `client.files.upload/list/get/delete(...)` | [`files/files.py`](examples/01_atomic_capabilities/files/files.py) | | Models | `client.models.list/get(...)` | [`models/models.py`](examples/01_atomic_capabilities/models/models.py) | @@ -344,12 +356,27 @@ job = client.images.generate(model="wanx2.1-t2i-turbo", prompt="a cozy flower sh if job.succeeded: print(job.image_urls) +# HTTP speech synthesis: one POST, audio comes back as a temporary URL +synth = client.audio.tts.http.synthesize( + "Hello there", model="cosyvoice-v3-flash", voice="longanhuan", format="pcm", +) +print(synth.audio_url) + +# Streaming synthesis yields audio chunks as they are rendered +for chunk in client.audio.tts.http.stream( + "Hello there", model="cosyvoice-v3-flash", voice="longanhuan", format="pcm", +): + if chunk.audio: + ... # chunk.is_final marks the closing part + # A Model Studio application resp = client.apps.completions.create(app_id="YOUR_APP_ID", prompt="How do I make stew?") resp.text ``` -> **Audio is a preview.** The 2.0 `client.audio` surface is not final and may change; accessing it emits a `FutureWarning`. +### Async audio + +`AsyncClient` covers the HTTP-shaped audio resources — `await client.audio.asr.transcribe(...)` for batch transcription, `client.audio.tts.http.synthesize/.stream` for synthesis, and the management resources (`voices`, `voice_design`, `vocabularies`, `phrases`). The duplex WebSocket sessions (`asr.recognize`, `tts.connect`, `realtime.connect`) are sync-only by design; run them in a thread if you need them inside an async app. ### Realtime over one WebSocket @@ -362,6 +389,8 @@ session = client.audio.asr.recognize(model="fun-asr-realtime", connection=connec connection.close() ``` +Each task on the connection reads through a bounded queue (`task_queue_size=`, default 1024), so a slow consumer fails its own task with `errors.WsQueueCapacityExceededError` instead of holding audio in memory indefinitely or tearing down the shared socket. + ## Supported models | Category | Example models | 2.0 resource | @@ -449,51 +478,20 @@ The 1.x module-level API is unchanged and still supported — you can adopt 2.0 ### Package layout and compatibility -The 2.0 implementation is directly under `dashscope`; the former -`dashscope.v2` namespace has been removed. The single AgentStudio and -FineTune/Agentic RL business implementations live under -`dashscope.applications`. Two thin public facades sit on top: - -- historical `dashscope.agentstudio` and `dashscope.finetune.agentic_rl` - imports retain the 1.x clients, methods, return models and exception classes; -- `client.agentstudio` and `client.rl` expose the 2.0 resource APIs, httpx - transport and `dashscope.errors` family. - -Historical deep modules such as `dashscope.aigc`, -`dashscope.client.base_api`, `dashscope.files`, and `dashscope.tokenizers` -remain available through lazy aliases to `dashscope.legacy_1x`. The local -`get_tokenizer` utility therefore keeps its 1.x exception contract; the 2.0 -remote token-counting API is the independent `client.tokenization` resource. -The application cores are internal implementation paths; 2.0 callers should -use the main Client resource tree. - -In particular, the historical Agentic RL class still returns `FineTune` from -`submit_job` and retains `cancel/list/get/delete/logs`. The 2.0 job lifecycle -uses `client.rl.jobs`, and its workflow errors use the new SDK error catalog. -Likewise, standalone `dashscope.agentstudio.Client` keeps the old -`AgentStudioError` hierarchy and `raw` response attribute, while -`client.agentstudio` raises `dashscope.errors` and exposes failed response data -through `body`. - -> **Breaking (AgentStudio errors):** `ConflictError` is now an alias of -> `APIStatusError` rather than a 409-specific subclass, so `except ConflictError` -> catches *every* status error (400/401/404/429/5xx), not just conflicts. Match on -> `err.status_code == 409` if you need the old behaviour. - -> **Breaking (`submit_job` / realtime WebSocket):** `submit_job` forwards extra -> keyword arguments straight to the transport, so the 1.x `request_timeout=` is -> renamed `timeout=`, and `workspace=` / `headers=` / `stream=` are no longer -> accepted (passing any of them raises `TypeError`). The realtime WebSocket state -> errors (`WsStateError`, `WsBinaryOwnerConflictError`) now derive from -> `DashScopeError` instead of `RuntimeError`, so a bare `except RuntimeError` no -> longer catches them — catch `errors.DashScopeError` (or the specific class) -> instead. - -`dashscope.save_api_key` now comes from `dashscope._config`. It accepts an -optional key-file path, expands `~`, creates missing parent directories, and -writes UTF-8. Logs from the current client use the independent -`dashscope._logging` logger hierarchy and do not reconfigure the shared -`dashscope` root logger. +The 2.0 implementation sits directly under `dashscope`; the former `dashscope.v2` namespace is gone. AgentStudio and FineTune/Agentic RL each have a single business core under `dashscope.applications`, with two thin public facades on top: + +- the historical `dashscope.agentstudio` and `dashscope.finetune.agentic_rl` imports keep the 1.x clients, methods, return models and exception classes; +- `client.agentstudio` and `client.rl` expose the 2.0 resource APIs, the httpx transport and the `dashscope.errors` family. + +Historical deep modules such as `dashscope.aigc`, `dashscope.client.base_api`, `dashscope.files` and `dashscope.tokenizers` remain importable through lazy aliases to `dashscope.legacy_1x`. The local `get_tokenizer` utility therefore keeps its 1.x exception contract, while 2.0 remote token counting is the independent `client.tokenization` resource. The application cores are internal implementation paths — 2.0 callers should use the client resource tree. + +Concretely: the historical Agentic RL class still returns `FineTune` from `submit_job` and retains `cancel/list/get/delete/logs`, whereas the 2.0 job lifecycle is `client.rl.jobs` with errors from the SDK catalog. Likewise, standalone `dashscope.agentstudio.Client` keeps the old `AgentStudioError` hierarchy and its `raw` response attribute, while `client.agentstudio` raises `dashscope.errors` and exposes failed response data through `body`. + +> **Breaking (2.0 AgentStudio errors):** on the 2.0 surface — `client.agentstudio`, whose exceptions come from `dashscope.applications.agentstudio` — `ConflictError` is an alias of `APIStatusError` rather than a 409-specific subclass, so `except ConflictError` catches *every* status error (400/401/404/429/5xx), not just conflicts. Match on `err.status_code == 409` if you need conflict-only handling. The historical `dashscope.agentstudio.ConflictError` is untouched and still 409-specific. + +> **Breaking (`submit_job` / realtime WebSocket):** `submit_job` forwards extra keyword arguments straight to the transport, so the 1.x `request_timeout=` is renamed `timeout=`, and `workspace=` / `headers=` / `stream=` are no longer accepted (passing any of them raises `TypeError`). The realtime WebSocket errors (`WsStateError`, `WsQueueCapacityExceededError`, `WsBinaryOwnerConflictError`) now derive from `DashScopeError` instead of `RuntimeError`, so a bare `except RuntimeError` no longer catches them — catch `errors.DashScopeError` (or the specific class) instead. + +`dashscope.save_api_key` now comes from `dashscope._config`. It accepts an optional key-file path, expands `~`, creates missing parent directories, and writes UTF-8. Logs from the current client use the independent `dashscope._logging` logger hierarchy and do not reconfigure the shared `dashscope` root logger. ### Development tests diff --git a/README_zh.md b/README_zh.md index c032eed8..015e76f4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,7 +4,7 @@ DashScope Python SDK 是[阿里云百炼(Model Studio)](https://help.aliyun.com/zh/model-studio/)的官方客户端,覆盖文本生成、多模态理解、向量(Embedding)、重排(Rerank)、图像/视频生成、语音合成与识别、模型调优、Agentic RL、应用调用等能力。 -**2.0 版本**引入了资源风格的客户端 —— `dashscope.Client()`,其易用性对标 OpenAI SDK:一个客户端持有共享连接池,每项能力都以类型化资源的形式挂载其上(`client.chat`、`client.images`、`client.audio`……),响应是类型化对象而非裸字典,失败默认抛出异常。1.x 的模块级 API(`dashscope.Generation` 等)保持不变且仍被完整支持,因此 2.0 是纯增量的。 +**2.0 版本**引入了资源风格的客户端 —— `dashscope.Client()`,其易用性对标 OpenAI SDK:一个客户端持有共享连接池,每项能力都以类型化资源的形式挂载其上(`client.chat`、`client.images`、`client.audio`……),响应是类型化对象而非裸字典,失败默认抛出异常。1.x 的完整公开接口仍保留在历史导入路径上,由 `dashscope.legacy_1x` 下的 Facade 实现;AgentStudio 与 Agentic RL 与 2.0 共享同一套业务核心,但各自的公开返回类型与错误契约保持独立。 ## 2.0 新特性 @@ -16,7 +16,7 @@ DashScope Python SDK 是[阿里云百炼(Model Studio)](https://help.aliyun. | 错误 | 手动检查 `resp.code` / `resp.message` | 默认抛出类型化的 `DashScopeError` 异常 *(完善中)* | | 流式 | 裸帧生成器 | `ChatStream`,提供 `.text_stream`、`.reasoning_stream`、`.get_final_message()` | | 协议 | DashScope 原生 + 单独的 compatible-mode 入口 | 一个客户端通过 `protocol=` 讲 `native`、`openai` 或 `anthropic` | -| 异步 | 独立的 `Aio*` 类 | `dashscope.AsyncClient()` 完整镜像同步接口 | +| 异步 | 独立的 `Aio*` 类 | `dashscope.AsyncClient()` 镜像同步接口(双工语音 WebSocket 会话除外) | | 连接 | 每次调用新建 session | 每个客户端一个 keep-alive 连接池,跨资源共享 | | 配置 | 模块级全局变量(`dashscope.api_key`、`set_region`) | 分层:参数 → `dashscope.settings` → 环境变量 → `~/.dashscope/config.toml` | @@ -134,6 +134,8 @@ low_latency = client.with_options(timeout=10.0, max_retries=0) 每次调用还接受请求级的 `api_key=`、`workspace=`、`timeout=` 覆盖。 +需要代理、自定义连接数上限或 event hook?传入自己的 `http_client=httpx.Client(...)`(`AsyncClient` 对应 `httpx.AsyncClient`)。SDK 仍会在其之上完成鉴权、重试和绝对端点 URL 拼接,并且不会关闭不属于它的客户端。 + ### 协议 一个客户端可以讲三种线路协议,通过 `protocol=`(或 `DASHSCOPE_PROTOCOL` / `settings.protocol`)选择。无论哪种协议,请求/响应对象都保持同一套规范化的类型形态 —— 变化的只是线路格式: @@ -185,6 +187,8 @@ for text in stream.text_stream: # 内容增量 默认每帧是增量的。传入 `incremental_output=False` 可改为接收累积快照(SDK 会替你合并增量)。 +思考模型的推理内容走 `.reasoning_stream`,合并后的消息里对应 `message.reasoning_content`。在 `protocol="anthropic"` 下,SDK 还会保留 `message.reasoning_signature` 以及原始的 `thinking` / `redacted_thinking` 块(位于 `message.reasoning_blocks`),因此把 assistant 消息原样回传即可延续 extended thinking。 + > **注意:** SDK **不会**自动重试流式请求。生产环境请在你自己的重试/退避逻辑中包裹流消费 —— 参见 [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py)。 ### 错误 @@ -206,7 +210,12 @@ except errors.DashScopeError as exc: # 所有 SDK 错误的基类 print("请求失败:", exc.code, exc.message) ``` -异常层级包含 `APIConnectionError` / `APITimeoutError`(网络层),以及在 `APIStatusError` 之下的:`BadRequestError`、`AuthenticationError`、`PermissionDeniedError`、`NotFoundError`、`RequestTooLargeError`、`RateLimitError`、`InternalServerError`、`ServiceUnavailableError`、`GatewayTimeoutError`、`ContentFilterError`。 +所有异常都派生自 `DashScopeError`,分为两个族: + +- **服务端错误** —— `APIStatusError` 之下:`BadRequestError`、`AuthenticationError`、`PermissionDeniedError`、`NotFoundError`、`RequestTooLargeError`、`RateLimitError`、`InternalServerError`、`ServiceUnavailableError`、`GatewayTimeoutError`,以及 `ContentFilterError`(`BadRequestError` 的子类)。 +- **客户端侧错误** —— `SdkError` 之下:网络类的 `APIConnectionError` / `APITimeoutError`,以及 WebSocket 族的 `WsEventError`、`WsStateError`、`WsQueueCapacityExceededError`、`WsBinaryOwnerConflictError`。每个都带有稳定的 `sdk.*` 错误码。 + +HTTP 200 但响应体无法解析成约定信封时,会统一报为 `InternalServerError`,而不是把解析器的异常抛给调用方。 想内联检查错误而不捕获异常?用 `raise_on_error=False` 构建客户端 —— 失败会以正常响应对象返回,带 `is_error=True`、`code`、`message`、`status_code`: @@ -302,7 +311,7 @@ client = dashscope.Client(region="ap-southeast-1", workspace="ws-xxx123") ## 能力一览 -每项能力都是客户端上的一个资源。同步与异步客户端暴露相同的接口(异步方法用 `await` 调用)。 +每项能力都是客户端上的一个资源。同步与异步客户端暴露相同的接口(异步方法用 `await` 调用)—— 只有一个例外:双工 WebSocket 语音会话按设计仅提供同步版本,见下文。 | 能力 | 2.0 资源 | 示例 | |------------|--------------|---------| @@ -315,7 +324,10 @@ client = dashscope.Client(region="ap-southeast-1", workspace="ws-xxx123") | Token 计数 | `client.tokenization.count(...)` | [`text_generation/tokenization.py`](examples/01_atomic_capabilities/text_generation/tokenization.py) | | 图像生成 | `client.images.generate(...)`(+ `.jobs`) | [`image_video_synthesis/images.py`](examples/01_atomic_capabilities/image_video_synthesis/images.py) | | 视频生成 | `client.videos.generate(...)`(+ `.jobs`) | [`image_video_synthesis/videos.py`](examples/01_atomic_capabilities/image_video_synthesis/videos.py) | -| 语音:ASR / TTS / 实时 *(预览)* | `client.audio.asr` · `client.audio.tts` · `client.audio.realtime` | [`audio/`](examples/01_atomic_capabilities/audio/) | +| 语音识别(ASR) | 录音文件批量:`client.audio.asr.transcribe(...)` / `.jobs`;实时:`client.audio.asr.recognize(...)` | [`audio/asr_file.py`](examples/01_atomic_capabilities/audio/asr_file.py) · [`audio/asr_websocket.py`](examples/01_atomic_capabilities/audio/asr_websocket.py) | +| 语音合成(TTS) | HTTP:`client.audio.tts.http.synthesize(...)` / `.stream(...)`;WebSocket:`client.audio.tts.connect(...)` | [`audio/tts_http.py`](examples/01_atomic_capabilities/audio/tts_http.py) · [`audio/tts_websocket.py`](examples/01_atomic_capabilities/audio/tts_websocket.py) | +| 全模态实时对话 | `client.audio.realtime.connect(...)` | [`audio/realtime_omni.py`](examples/01_atomic_capabilities/audio/realtime_omni.py) | +| 音色与词表管理 | `client.audio.voices` · `.voice_design` · `.vocabularies` · `.phrases` | [`audio/voice_clone.py`](examples/01_atomic_capabilities/audio/voice_clone.py) | | 会议与音频理解(通义听悟 TingWu) | `client.tingwu.call(...)` / `.realtime(...)` | [`audio/tingwu.py`](examples/01_atomic_capabilities/audio/tingwu.py) | | 文件 | `client.files.upload/list/get/delete(...)` | [`files/files.py`](examples/01_atomic_capabilities/files/files.py) | | 模型 | `client.models.list/get(...)` | [`models/models.py`](examples/01_atomic_capabilities/models/models.py) | @@ -344,12 +356,27 @@ job = client.images.generate(model="wanx2.1-t2i-turbo", prompt="一间温馨的 if job.succeeded: print(job.image_urls) +# HTTP 语音合成:一次 POST,音频以临时下载地址返回 +synth = client.audio.tts.http.synthesize( + "你好,世界", model="cosyvoice-v3-flash", voice="longanhuan", format="pcm", +) +print(synth.audio_url) + +# 流式合成按渲染进度返回音频分片 +for chunk in client.audio.tts.http.stream( + "你好,世界", model="cosyvoice-v3-flash", voice="longanhuan", format="pcm", +): + if chunk.audio: + ... # chunk.is_final 标记结束分片 + # 百炼应用 resp = client.apps.completions.create(app_id="YOUR_APP_ID", prompt="土豆炖猪脚怎么做?") resp.text ``` -> **语音为预览版。** 2.0 的 `client.audio` 接口尚未定型,可能变化;访问它会发出 `FutureWarning`。 +### 异步语音 + +`AsyncClient` 覆盖 HTTP 形态的语音资源 —— 批量转写 `await client.audio.asr.transcribe(...)`、合成 `client.audio.tts.http.synthesize/.stream`,以及管理类资源(`voices`、`voice_design`、`vocabularies`、`phrases`)。双工 WebSocket 会话(`asr.recognize`、`tts.connect`、`realtime.connect`)按设计只有同步版本;若需在异步应用中使用,请放到线程里运行。 ### 单条 WebSocket 上的实时能力 @@ -362,6 +389,8 @@ session = client.audio.asr.recognize(model="fun-asr-realtime", connection=connec connection.close() ``` +连接上每个任务都通过有界队列读取(`task_queue_size=`,默认 1024):消费过慢时只会让该任务以 `errors.WsQueueCapacityExceededError` 失败,既不会无限占用内存缓存音频,也不会拖垮共享的 socket。 + ## 支持的模型 | 类别 | 示例模型 | 2.0 资源 | @@ -465,43 +494,26 @@ AgentStudio 与 FineTune/Agentic RL 的唯一业务核心位于 2.0 业务代码应通过主 Client 资源树调用。 历史 Agentic RL 的 `submit_job` 仍返回 `FineTune`,并保留 -`cancel/list/get/delete/logs`;2.0 的任务生命周期位于 `client.rl.jobs`。 -历史 `dashscope.agentstudio.Client` 继续抛旧 `AgentStudioError` 并保留 -`raw`,而 `client.agentstudio` 使用新 errors,失败响应通过 `body` 暴露。 - -> **破坏性变更(Agentic RL):** 公开路径 `dashscope.finetune.agentic_rl` -> 现在解析到 **2.0** 的 `AgenticRL` —— `submit_job` 返回 `SubmitResult` -> (不再是 1.x 的 `FineTune`),且该类不再带任务管理动词。2.0 的任务生命 -> 周期在客户端上(`client.rl.jobs.create()`、`.wait()`、`.list()`、 -> `.cancel()`、`.delete()`、`.logs()`)。依赖 1.x 类接口 -> (`AgenticRL.cancel(...)`、`.get(...)`、`submit_job` → `FineTune`)的代码 -> 应改为导入 `dashscope.legacy_1x.finetune.agentic_rl`,它保留五个动词 -> (`cancel`/`list`/`get`/`delete`/`logs`)与 `FineTune` 返回值。 - -AgentStudio 与 Agentic RL 的异常已统一到 2.0 错误体系:`AgentStudioError` -现在是 `dashscope.errors.DashScopeError` 的别名,Agentic RL 的 `*WithCode` -错误继承自 `AgenticRLError`。原 AgentStudio 的 `raw` 响应属性改以 `body` -暴露(`err.raw` → `err.body`);`RuntimeErrorWithCode` / `ValueErrorWithCode` -仍可作为其包装的内置 `RuntimeError` / `ValueError` 被捕获。由于两套栈现在 -共享 `dashscope._shared._errors.DashScopeException` 基类,一个 -`except DashScopeException` 即可同时捕获 1.x、2.0 与 AgentStudio 错误。 -Agentic RL 的 `*WithCode` 错误 `str()` 渲染也变了 —— 1.x 的 -`[1800] I/O error: …` 前缀被替换为 2.0 的 ` … | code=sdk.…` -形式(数值码仍可通过 `err.error_code` 获取、时间戳通过 `err.timestamp`), -依赖旧格式的日志解析需要更新。 - -> **破坏性变更(AgentStudio 错误):** `ConflictError` 现在是 -> `APIStatusError` 的别名,而不再是仅对应 409 的子类,因此 -> `except ConflictError` 会捕获**所有**状态错误(400/401/404/429/5xx), -> 而不仅是冲突。若需要旧行为,请改用 `err.status_code == 409` 判断。 +`cancel/list/get/delete/logs`;2.0 的任务生命周期位于 `client.rl.jobs`, +其工作流错误使用新的 SDK 错误目录。历史 `dashscope.agentstudio.Client` +继续抛旧 `AgentStudioError` 并保留 `raw`,而 `client.agentstudio` 使用新 +errors,失败响应通过 `body` 暴露。 + +> **破坏性变更(2.0 AgentStudio 错误):** 在 2.0 接口上 —— +> `client.agentstudio`,其异常来自 `dashscope.applications.agentstudio` —— +> `ConflictError` 现在是 `APIStatusError` 的别名,而不再是仅对应 409 的 +> 子类,因此 `except ConflictError` 会捕获**所有**状态错误 +> (400/401/404/429/5xx),而不仅是冲突。若只关心冲突,请改用 +> `err.status_code == 409` 判断。历史路径 `dashscope.agentstudio.ConflictError` +> 未受影响,仍只对应 409。 > **破坏性变更(`submit_job` / realtime WebSocket):** `submit_job` 会把 > 额外关键字参数直接转发给传输层,因此 1.x 的 `request_timeout=` 改名为 > `timeout=`,且不再接受 `workspace=` / `headers=` / `stream=`(传入任一 -> 都会抛 `TypeError`)。realtime WebSocket 状态错误(`WsStateError`、 -> `WsBinaryOwnerConflictError`)现在继承自 `DashScopeError` 而非 -> `RuntimeError`,因此裸 `except RuntimeError` 不再能捕获它们 —— 请改捕获 -> `errors.DashScopeError`(或具体的错误类)。 +> 都会抛 `TypeError`)。realtime WebSocket 错误(`WsStateError`、 +> `WsQueueCapacityExceededError`、`WsBinaryOwnerConflictError`)现在继承自 +> `DashScopeError` 而非 `RuntimeError`,因此裸 `except RuntimeError` 不再能 +> 捕获它们 —— 请改捕获 `errors.DashScopeError`(或具体的错误类)。 `dashscope.save_api_key` 现在来自 `dashscope._config`,仍接受可选的 key 文件路径,并会展开 `~`、创建缺失的父目录、按 UTF-8 写入。当前客户端使用 diff --git a/dashscope/client.py b/dashscope/client.py index c4b443ee..d000f3f9 100644 --- a/dashscope/client.py +++ b/dashscope/client.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import Any, Dict, Mapping, Optional, Union -import warnings import httpx @@ -145,18 +144,8 @@ def __init__( @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. - """ + """Audio capabilities (lazily mounted).""" 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 @@ -388,18 +377,8 @@ def __init__( @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. - """ + """Audio capabilities (lazily mounted).""" 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 diff --git a/dashscope/resources/audio/__init__.py b/dashscope/resources/audio/__init__.py index 6a6ea73c..71a972a2 100644 --- a/dashscope/resources/audio/__init__.py +++ b/dashscope/resources/audio/__init__.py @@ -104,11 +104,7 @@ def __init__(self, client) -> None: class Audio: - """Sync audio surface. - - Preview: the 2.0 audio API surface is not final and may change in - a future release. - """ + """Sync audio surface.""" def __init__(self, client) -> None: self.asr = Asr(client) @@ -126,9 +122,6 @@ class AsyncAudio: Only batch transcription is exposed here; the ws realtime resources (``asr.recognize``, ``tts``, ``realtime``) are sync-only by design (design doc M3 3.5). - - Preview: the 2.0 audio API surface is not final and may change in - a future release. """ def __init__(self, client) -> None: diff --git a/dashscope/resources/audio/customization.py b/dashscope/resources/audio/customization.py index 8d38aba2..953363b2 100644 --- a/dashscope/resources/audio/customization.py +++ b/dashscope/resources/audio/customization.py @@ -4,12 +4,17 @@ V2 form of 1.x ``VocabularyService`` (asr hot-word tables, ``audio/asr/vocabulary.py``) and ``VoiceEnrollmentService`` (voice -cloning, ``audio/tts_v2/enrollment.py``), plus the qwen voice-design -family (no 1.x SDK class — modeled from the official REST API). -All three ride the same envelope: ``POST /services/audio/{asr|tts}/ -customization`` with ``{model, input: {action, ...}, parameters?}``; -only the action words and id fields differ per family. 1.x retried -transport failures inside these services; v2 leaves that to the +cloning, ``audio/tts_v2/enrollment.py``), plus voice design (no 1.x SDK +class). All three ride the same envelope: ``POST /services/audio/{asr|tts}/ +customization`` with ``{model, input: {action, ...}, parameters?}``. + +Cloning and design are two doors onto one registry: both post +``model="voice-enrollment"`` to the tts path with the same action words and +the same ``voice_id``, and they share a single per-account voice quota. +They differ only in how a voice is created -- from a sample ``url``, or from +a ``voice_prompt`` describing the wanted sound. + +1.x retried transport failures inside these services; v2 leaves that to the transport's retry loop. """ @@ -77,12 +82,21 @@ class VoiceList(ResponseBase): class DesignedVoice(ResponseBase): - """One designed voice; its id field is ``voice`` on the wire.""" + """One designed voice. + + The same registry entry as a cloned :class:`Voice` -- one endpoint, one + ``voice_id``, one shared quota -- plus the two texts it was designed + from. + """ - voice: Optional[str] = None + voice_id: Optional[str] = None + status: Optional[str] = None preview_audio: Optional[Dict[str, Any]] = None - language: Optional[str] = None target_model: Optional[str] = None + voice_prompt: Optional[str] = None + preview_text: Optional[str] = None + gmt_create: Optional[str] = None + gmt_modified: Optional[str] = None class DesignedVoiceList(ResponseBase): @@ -453,24 +467,25 @@ def delete( class VoiceDesign(_CustomizationBase): - """``client.audio.voice_design`` -- qwen voice design. + """``client.audio.voice_design`` -- voices described in words, not audio. - No 1.x SDK class; modeled from the official REST API, which shares - the voices endpoint with shorter action words and a ``voice`` id - field instead of ``voice_id``. + Shares the voice-enrollment registry with :class:`Voices`: same path, + same ``model``, same action words, same ``voice_id``, one quota. Only + ``create`` differs -- a text prompt where cloning has a sample ``url`` + -- so ``list`` returns cloned voices as well as designed ones. """ PATH = "/services/audio/tts/customization" - DEFAULT_MODEL = "qwen-voice-design" + DEFAULT_MODEL = "voice-enrollment" def create( self, *, target_model: str, + prefix: str, voice_prompt: str, preview_text: str, - preferred_name: Optional[str] = None, - language: Optional[str] = None, + language_hints: Optional[List[str]] = None, extra_input: Optional[Dict[str, Any]] = None, model: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None, @@ -479,21 +494,28 @@ def create( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> DesignedVoice: - """Design a voice from a text prompt; carries ``voice`` + preview.""" + """Design a voice from a text prompt; carries ``voice_id`` + preview. + + ``prefix`` becomes part of the generated id, + ``{target_model}-vd-{prefix}-{unique}``. Only the first element of + ``language_hints`` is honoured, and it must match the language of + ``preview_text``. The voice is unusable until its ``status`` leaves + ``DEPLOYING``. + """ fields: Dict[str, Any] = dict(extra_input or {}) + # Named arguments win over the ``extra_input=`` escape hatch. fields.update( { "target_model": target_model, + "prefix": prefix, "voice_prompt": voice_prompt, "preview_text": preview_text, }, ) - if preferred_name is not None: - fields["preferred_name"] = preferred_name - if language is not None: - fields["language"] = language + if language_hints is not None: + fields["language_hints"] = language_hints response = self._call( - "create", + "create_voice", fields, model=model, parameters=parameters, @@ -510,6 +532,7 @@ def create( def list( self, *, + prefix: Optional[str] = None, page_index: int = 0, page_size: int = 10, model: Optional[str] = None, @@ -518,10 +541,16 @@ def list( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> DesignedVoiceList: - """List designed voices.""" + """List voices, optionally narrowed to one ``prefix``.""" + fields: Dict[str, Any] = { + "page_index": page_index, + "page_size": page_size, + } + if prefix: + fields["prefix"] = prefix response = self._call( - "list", - {"page_index": page_index, "page_size": page_size}, + "list_voice", + fields, model=model, timeout=timeout, headers=headers, @@ -535,7 +564,7 @@ def list( def get( self, - voice: str, + voice_id: str, *, model: Optional[str] = None, timeout: Optional[float] = None, @@ -543,11 +572,11 @@ def get( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> DesignedVoice: - """Fetch one designed voice (wire action ``query``).""" - _require_id(voice, "voice") + """Fetch one voice's details, including its review ``status``.""" + _require_id(voice_id, "voice_id") response = self._call( - "query", - {"voice": voice}, + "query_voice", + {"voice_id": voice_id}, model=model, timeout=timeout, headers=headers, @@ -561,7 +590,7 @@ def get( def delete( self, - voice: str, + voice_id: str, *, model: Optional[str] = None, timeout: Optional[float] = None, @@ -569,11 +598,11 @@ def delete( api_key: Optional[str] = None, workspace: Optional[str] = None, ) -> JobStatus: - """Delete one designed voice.""" - _require_id(voice, "voice") + """Delete one designed voice, freeing its quota.""" + _require_id(voice_id, "voice_id") response = self._call( - "delete", - {"voice": voice}, + "delete_voice", + {"voice_id": voice_id}, model=model, timeout=timeout, headers=headers, diff --git a/examples/README.md b/examples/README.md index e10f90e7..8e1f1141 100644 --- a/examples/README.md +++ b/examples/README.md @@ -93,7 +93,7 @@ Uniform style: `client = dashscope.Client()` (reads the key from the `DASHSCOPE_ | `text_generation/chat.py` | Text generation: one-line `dashscope.chat()`, non-streaming, streaming (text_stream) | | `text_generation/tokenization.py` | Token counting | | `multimodal_vision/multimodal_chat.py` | Multimodal chat (qwen-vl image understanding) | -| `audio/` | Speech samples: ASR/TTS/realtime/voice CLIs (see `audio/README.md`; `direct_call/` holds the no-`with` variants); 2.0 audio API is a preview and may change | +| `audio/` | Speech samples: ASR/TTS/realtime/voice CLIs (see `audio/README.md`; `direct_call/` holds the no-`with` variants) | | `audio/tingwu.py` | TingWu offline tasks and realtime sessions | | `image_video_synthesis/images.py` | Image generation: one-call generate-and-wait + the manual jobs flow | | `image_video_synthesis/videos.py` | Video generation: one-call generate-and-wait + the manual jobs flow | diff --git a/examples/README_zh.md b/examples/README_zh.md index 63d736bc..44ddb9fd 100644 --- a/examples/README_zh.md +++ b/examples/README_zh.md @@ -93,7 +93,7 @@ examples/ | `text_generation/chat.py` | 文本生成:一行 `dashscope.chat()`、非流式、流式(text_stream) | | `text_generation/tokenization.py` | token 计数 | | `multimodal_vision/multimodal_chat.py` | 多模态对话(qwen-vl 图像理解) | -| `audio/` | 语音示例:ASR/TTS/realtime/音色 CLI(见 `audio/README.md`;`direct_call/` 为不使用 `with` 的副本);2.0 audio API 为预览,后续可能变更 | +| `audio/` | 语音示例:ASR/TTS/realtime/音色 CLI(见 `audio/README.md`;`direct_call/` 为不使用 `with` 的副本) | | `audio/tingwu.py` | 听悟离线任务与实时会话 | | `image_video_synthesis/images.py` | 图像生成:一键生成并等待 + 手动 jobs 流程 | | `image_video_synthesis/videos.py` | 视频生成:一键生成并等待 + 手动 jobs 流程 | diff --git a/tests/test_audio_management.py b/tests/test_audio_management.py index c9410804..844ac8c2 100644 --- a/tests/test_audio_management.py +++ b/tests/test_audio_management.py @@ -4,7 +4,8 @@ ``client.audio.vocabularies`` / ``phrases`` / ``voices`` / ``voice_design`` -- 1.x ``VocabularyService`` / ``AsrPhraseManager`` / -``VoiceEnrollmentService`` plus the qwen voice-design REST family. +``VoiceEnrollmentService`` plus prompt-created voice design, which shares +the voice-enrollment registry with cloning. """ # Mock handlers intentionally ignore the request argument. @@ -227,7 +228,19 @@ def handler(request): assert all(call["model"] == "voice-enrollment" for call in calls) -# ── voice design (qwen-voice-design) ───────────────────────── +# ── voice design (voice-enrollment registry, prompt-created) ────────── + + +# Mirrors the "查询音色详情" response in the voice-design API reference. +_DESIGNED_VOICE = { + "voice_id": "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + "gmt_create": "2025-12-10 14:54:09", + "gmt_modified": "2025-12-10 17:47:48", + "status": "OK", + "target_model": "cosyvoice-v3.5-plus", + "voice_prompt": "沉稳的中年男性播音员,音色低沉浑厚", + "preview_text": "各位听众朋友们大家好,欢迎收听本期节目", +} def test_voice_design_crud(): @@ -237,12 +250,14 @@ def handler(request): body = json.loads(request.content.decode()) calls.append(body) action = body["input"]["action"] - if action == "create": + if action == "create_voice": return httpx.Response( 200, json={ + "request_id": "req-design", "output": { - "voice": "announcer", + "voice_id": "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + "target_model": "cosyvoice-v3.5-plus", "preview_audio": { "data": "QUJD", "sample_rate": 24000, @@ -252,72 +267,77 @@ def handler(request): "usage": {"count": 1}, }, ) - if action == "list": - return httpx.Response( - 200, - json={ - "output": { - "voice_list": [ - {"voice": "announcer", "language": "zh"}, - ], - "total_count": 1, - }, - }, - ) - if action == "query": + if action == "list_voice": return httpx.Response( 200, - json={ - "output": {"voice": "announcer", "language": "zh"}, - }, + json={"output": {"voice_list": [_DESIGNED_VOICE]}}, ) + if action == "query_voice": + return httpx.Response(200, json={"output": _DESIGNED_VOICE}) return httpx.Response( 200, - json={"output": {"voice": "announcer"}}, + json={"output": {}, "usage": {"count": 1}}, ) client = _make_client(handler) created = client.audio.voice_design.create( - target_model="qwen3-tts-vd-realtime-2026-01-15", - voice_prompt="沉稳的中年男性,音色低沉浑厚", - preview_text="各位听众朋友,大家好", - preferred_name="announcer", - language="zh", + target_model="cosyvoice-v3.5-plus", + prefix="announcer", + voice_prompt="沉稳的中年男性播音员,音色低沉浑厚", + preview_text="各位听众朋友们大家好,欢迎收听本期节目", + language_hints=["zh"], parameters={"sample_rate": 24000, "response_format": "wav"}, + extra_input={"prefix": "evil", "extra": 1}, ) - assert created.voice == "announcer" + assert created.voice_id == "cosyvoice-v3.5-plus-vd-announcer-xxxxxx" + assert created.request_id == "req-design" # The mock always returns a dict; pylint only sees Optional[Dict]. # pylint: disable-next=unsubscriptable-object assert created.preview_audio["response_format"] == "wav" + + # Named arguments win over the extra_input escape hatch. assert calls[0] == { - "model": "qwen-voice-design", + "model": "voice-enrollment", "input": { - "action": "create", - "target_model": "qwen3-tts-vd-realtime-2026-01-15", - "voice_prompt": "沉稳的中年男性,音色低沉浑厚", - "preview_text": "各位听众朋友,大家好", - "preferred_name": "announcer", - "language": "zh", + "action": "create_voice", + "extra": 1, + "target_model": "cosyvoice-v3.5-plus", + "prefix": "announcer", + "voice_prompt": "沉稳的中年男性播音员,音色低沉浑厚", + "preview_text": "各位听众朋友们大家好,欢迎收听本期节目", + "language_hints": ["zh"], }, "parameters": {"sample_rate": 24000, "response_format": "wav"}, } - listed = client.audio.voice_design.list() - assert listed.voices == [{"voice": "announcer", "language": "zh"}] - assert listed.total_count == 1 + listed = client.audio.voice_design.list(prefix="announcer") + assert listed.voices == [_DESIGNED_VOICE] assert calls[1]["input"] == { - "action": "list", + "action": "list_voice", "page_index": 0, "page_size": 10, + "prefix": "announcer", } - fetched = client.audio.voice_design.get("announcer") - assert fetched.language == "zh" - assert calls[2]["input"] == {"action": "query", "voice": "announcer"} + fetched = client.audio.voice_design.get( + "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + ) + assert fetched.status == "OK" + assert fetched.voice_prompt == "沉稳的中年男性播音员,音色低沉浑厚" + assert fetched.gmt_create == "2025-12-10 14:54:09" + assert calls[2]["input"] == { + "action": "query_voice", + "voice_id": "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + } - client.audio.voice_design.delete("announcer") - assert calls[3]["input"] == {"action": "delete", "voice": "announcer"} - assert all(call["model"] == "qwen-voice-design" for call in calls) + client.audio.voice_design.delete( + "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + ) + assert calls[3]["input"] == { + "action": "delete_voice", + "voice_id": "cosyvoice-v3.5-plus-vd-announcer-xxxxxx", + } + assert all(call["model"] == "voice-enrollment" for call in calls) # ── phrases (1.x AsrPhraseManager, fine-tunes rider) ────────────────── @@ -514,7 +534,8 @@ def test_management_all_actions_keep_real_response_headers(resource, method): "url": "https://cdn.test/audio.wav", }, "voice_design": { - "target_model": "qwen-tts-vd", + "target_model": "cosyvoice-v3.5-plus", + "prefix": "test", "voice_prompt": "warm", "preview_text": "hello", }, @@ -550,7 +571,7 @@ def test_management_all_actions_keep_real_response_headers(resource, method): for key in ("vocabularies", "vocabulary_list") ] + [ - (resource, key, "voices", {"voice": "voice-1", "enabled": False}) + (resource, key, "voices", {"voice_id": "voice-1", "status": "OK"}) for resource in ("voices", "voice_design") for key in ("voices", "voice_list") ] @@ -814,7 +835,7 @@ def handler(request): client = _make_client(handler) voice_extra = {"language_hints": ["en"], "max_prompt_audio_length": 10} - design_extra = {"language": "en", "preferred_name": "original"} + design_extra = {"language_hints": ["en"], "prefix": "original"} original = json.loads(json.dumps([voice_extra, design_extra])) parameters = {} if explicit else None client.audio.voices.create( @@ -827,11 +848,11 @@ def handler(request): parameters=parameters, ) client.audio.voice_design.create( - target_model="qwen3-tts-vd-realtime-2026-01-15", + target_model="cosyvoice-v3.5-plus", + prefix="demo", voice_prompt="低沉", preview_text="你好", - language="" if explicit else None, - preferred_name="" if explicit else None, + language_hints=[] if explicit else None, extra_input=design_extra, parameters=parameters, ) @@ -839,10 +860,8 @@ def handler(request): assert bodies[0]["input"]["max_prompt_audio_length"] == ( 0 if explicit else 10 ) - assert bodies[1]["input"]["language"] == ("" if explicit else "en") - assert bodies[1]["input"]["preferred_name"] == ( - "" if explicit else "original" - ) + assert bodies[1]["input"]["language_hints"] == ([] if explicit else ["en"]) + assert bodies[1]["input"]["prefix"] == "demo" assert [voice_extra, design_extra] == original for body in bodies: if explicit: @@ -867,16 +886,17 @@ def handler(request): url="https://cdn.test/a.wav", extra_input={"action": "delete_voice"}, ) - expected_action = "create_voice" else: client.audio.voice_design.create( - target_model="qwen3-tts-vd-realtime-2026-01-15", + target_model="cosyvoice-v3.5-plus", + prefix="demo", voice_prompt="低沉", preview_text="你好", - extra_input={"action": "delete"}, + extra_input={"action": "delete_voice"}, ) - expected_action = "create" - assert bodies[0]["input"]["action"] == expected_action + # Both families post the same action word, so neither may let the + # caller's escape hatch rewrite it into a delete. + assert bodies[0]["input"]["action"] == "create_voice" @pytest.mark.parametrize("resource", _MANAGEMENT_RESOURCES) From 8758133f42ab437832411f15bd28d8753b3a530b Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Fri, 30 Oct 2026 10:17:43 +0800 Subject: [PATCH 39/41] fix(packaging): clean stale modules before wheel build Setuptools leaves deleted packages in an incremental build/lib tree. Clean the built dashscope package before copying current sources so stale 1.x packages cannot shadow 2.0 modules such as dashscope/client.py. --- setup.py | 17 +++++++- .../legacy_1x/unit/test_packaging_metadata.py | 39 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1cf5da24..d9a94d60 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,14 @@ def readme(): class build_py(_build_py): - """Ship the top-level ``examples/`` tree inside the wheel. + """Build one clean package tree and bundle the top-level examples. + + Setuptools incrementally copies files into ``build/lib`` but does not + delete modules removed from the source tree. That is unsafe for the 2.0 + package migration: a stale ``dashscope/client/__init__.py`` shadows the + new ``dashscope/client.py`` and makes ``from dashscope.client import + Client`` fail in the built wheel. Remove the package build directory + before every build, then copy the current sources and examples only. ``examples/`` lives at the repository root (a sibling of ``dashscope/``) for visibility, but a wheel can only bundle files @@ -50,6 +57,14 @@ class build_py(_build_py): """ def run(self): + source_package = os.path.abspath(os.path.join(package_root, name)) + built_package = os.path.abspath(os.path.join(self.build_lib, name)) + if built_package == source_package: + raise RuntimeError( + "build_lib must not point at the source package directory", + ) + if os.path.isdir(built_package): + shutil.rmtree(built_package) super().run() src = os.path.join(package_root, "examples") dst = os.path.join(self.build_lib, "dashscope", "examples") diff --git a/tests/legacy_1x/unit/test_packaging_metadata.py b/tests/legacy_1x/unit/test_packaging_metadata.py index 64e8ec00..37b0dbc5 100644 --- a/tests/legacy_1x/unit/test_packaging_metadata.py +++ b/tests/legacy_1x/unit/test_packaging_metadata.py @@ -13,6 +13,7 @@ import ast import json import re +import subprocess import sys import urllib.error import urllib.request @@ -299,3 +300,41 @@ def test_repo_classifiers_cover_every_python_carrying_real_traffic(): def test_python_39_config_reader_is_a_core_dependency(): requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8") assert 'tomli>=1.1.0; python_version < "3.11"' in requirements + + +def test_build_py_removes_stale_deleted_packages(tmp_path): + """Incremental wheel builds must not resurrect pre-migration modules. + + A dirty ``build/lib`` can otherwise contain both ``dashscope/client.py`` + and the deleted ``dashscope/client/__init__.py`` package. Python chooses + the stale package and ``from dashscope.client import Client`` then fails. + """ + build_lib = tmp_path / "build-lib" + stale_files = ( + build_lib / "dashscope" / "client" / "__init__.py", + build_lib / "dashscope" / "aigc" / "__init__.py", + build_lib / "dashscope" / "resources" / "qwen.tiktoken", + ) + for stale in stale_files: + stale.parent.mkdir(parents=True, exist_ok=True) + stale.write_text("# stale pre-2.0 build artifact\n", encoding="utf-8") + + result = subprocess.run( + [ + sys.executable, + "setup.py", + "build_py", + "--build-lib", + str(build_lib), + "--force", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert (build_lib / "dashscope" / "client.py").is_file() + for stale in stale_files: + assert not stale.exists(), stale From 7a007c2253b9e6631633d61a7093fd103c3b91d6 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 30 Oct 2026 12:14:14 +0800 Subject: [PATCH 40/41] feat(finetune): optimize function dependency layer creation and status check mechanism - Introduce FC_LAYER_STATUS_TIMEOUT and FC_LAYER_STATUS_INTERVAL environment variables to control layer build timeout and polling interval - Update FunctionLayerError message descriptions to cover build failure, timeout, and status query error scenarios - Refactor get_layer method implementation with build status polling logic and timeout handling - Enhance error handling to distinguish between build failures, status query errors, and timeouts - Fix legacy 1.x version issue where client directory could shadow the client module import - Add comprehensive unit tests covering various layer build states and error scenarios --- dashscope/_error_registry.py | 14 +- .../finetune/reinforcement/__init__.py | 4 + .../reinforcement/common/constants.py | 4 + .../finetune/reinforcement/common/errors.py | 9 +- .../finetune/reinforcement/common/model.py | 144 ++++++++--- dashscope/legacy_1x/_shim.py | 20 ++ .../unit/test_agentic_rl_layer_status.py | 226 ++++++++++++++++++ tests/test_legacy_import_aliases.py | 45 ++++ 8 files changed, 420 insertions(+), 46 deletions(-) create mode 100644 tests/legacy_1x/unit/test_agentic_rl_layer_status.py diff --git a/dashscope/_error_registry.py b/dashscope/_error_registry.py index ba581ce6..36cda129 100644 --- a/dashscope/_error_registry.py +++ b/dashscope/_error_registry.py @@ -486,12 +486,14 @@ def format_message( SDK_AGENTIC_RL_FUNCTION_LAYER_ERROR = SdkErrorDef( name="sdk.agentic_rl.FunctionLayerError", - message="Creating a layer of function failed.", - solution=( - "Failed to create the function dependency layer. Check that " - "the requirements.txt or setup.py is valid and all " - "dependencies are installable. Review the layer build logs " - "for details." + message="The function dependency layer is not usable.", + solution=( + "The dependency layer could not be created, or its build failed, " + "timed out, or its status could not be queried. For a build " + "failure, check that requirements.txt or setup.py is valid and " + "every dependency is installable, then review the layer build " + "logs. For a timeout, raise FC_LAYER_STATUS_TIMEOUT to allow more " + "time. For a status query error, retry once connectivity recovers." ), allow_retry=True, vars=[], diff --git a/dashscope/applications/finetune/reinforcement/__init__.py b/dashscope/applications/finetune/reinforcement/__init__.py index 2f6f74da..88df537a 100644 --- a/dashscope/applications/finetune/reinforcement/__init__.py +++ b/dashscope/applications/finetune/reinforcement/__init__.py @@ -33,6 +33,8 @@ FC_LAYER_QUERY_API, FC_LAYER_USED, FC_LAYER_NAME, + FC_LAYER_STATUS_TIMEOUT, + FC_LAYER_STATUS_INTERVAL, FC_REQUIREMENTS_FILE, FC_ZIP_EXCLUDE_PATTERNS, FC_OSS_FILE_SIZE_WARNING, @@ -164,6 +166,8 @@ "FC_LAYER_QUERY_API", "FC_LAYER_USED", "FC_LAYER_NAME", + "FC_LAYER_STATUS_TIMEOUT", + "FC_LAYER_STATUS_INTERVAL", "FC_REQUIREMENTS_FILE", "FC_ZIP_EXCLUDE_PATTERNS", "FC_OSS_FILE_SIZE_WARNING", diff --git a/dashscope/applications/finetune/reinforcement/common/constants.py b/dashscope/applications/finetune/reinforcement/common/constants.py index 23071fa1..e29a7273 100644 --- a/dashscope/applications/finetune/reinforcement/common/constants.py +++ b/dashscope/applications/finetune/reinforcement/common/constants.py @@ -127,6 +127,10 @@ def get_int_env(env_var: str, default: int) -> int: FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) FC_LAYER_USED = get_bool_env("FC_LAYER_USED", True) FC_LAYER_NAME = "fc_layer" +# Building a layer installs requirements.txt inside Function Compute, so a +# cold build routinely outlasts a single HTTP timeout. +FC_LAYER_STATUS_TIMEOUT = get_int_env("FC_LAYER_STATUS_TIMEOUT", 600) +FC_LAYER_STATUS_INTERVAL = get_int_env("FC_LAYER_STATUS_INTERVAL", 10) FC_REQUIREMENTS_FILE = "./requirements.txt" FC_ZIP_EXCLUDE_PATTERNS = os.environ.get( "FC_ZIP_EXCLUDE_PATTERNS", diff --git a/dashscope/applications/finetune/reinforcement/common/errors.py b/dashscope/applications/finetune/reinforcement/common/errors.py index 026231f8..94f7e2ea 100644 --- a/dashscope/applications/finetune/reinforcement/common/errors.py +++ b/dashscope/applications/finetune/reinforcement/common/errors.py @@ -315,15 +315,20 @@ def __init__( error_code: Optional[int] = None, layer_name: Optional[str] = None, error_log: Optional[str] = None, + layer_code: Optional[str] = None, **kwargs: Any, ) -> None: + # Raised for create, status-query and build-timeout failures alike, so + # the prefix names the subsystem rather than the create stage. The + # archived 1.x base keeps its original wording. super().__init__( - f"Function layer create failed: {message}", + f"Function layer failed: {message}", error_code, - layer_name, + layer_code, **kwargs, ) self.layer_name = layer_name + self.layer_code = layer_code self.error_log = error_log diff --git a/dashscope/applications/finetune/reinforcement/common/model.py b/dashscope/applications/finetune/reinforcement/common/model.py index 5e631838..ffdcff50 100644 --- a/dashscope/applications/finetune/reinforcement/common/model.py +++ b/dashscope/applications/finetune/reinforcement/common/model.py @@ -38,6 +38,8 @@ FC_LAYER_USED, FC_LAYER_CREATE_API, FC_LAYER_QUERY_API, + FC_LAYER_STATUS_TIMEOUT, + FC_LAYER_STATUS_INTERVAL, LOG_LEVEL, ) from .model_types import ( @@ -366,6 +368,7 @@ async def create_layer( ) -> str: """Retrieve OSS signed URL for deployment package.""" layer_code = None + layer_name = None try: # Validate requirements file if provided layer_name = name + "-" + generate_random_id()[:8] @@ -389,7 +392,7 @@ async def create_layer( ) if result.get("status", {}).get("code", 500) != 200: raise FunctionLayerError( - f"Function layer create failed: {result}", + f"create rejected by server | {result}", ) layer_code = result.get("output", {}).get("layer_code", "") @@ -398,10 +401,13 @@ async def create_layer( f"layer_code: {layer_code}", ) + except FunctionLayerError: + raise except Exception as e: raise FunctionLayerError( - "Function layer create failed", + f"create error | {e}", error_code=2013, + layer_name=layer_name, ) from e return layer_code @@ -454,10 +460,20 @@ async def to_oss( return async def get_layer(self, layer_code: str) -> str: - """Get FC layer status.""" - try: - i = 0 - while i < 3: + """Wait for an FC layer build to finish and return its final status. + + Raises FunctionLayerError when the layer fails to build, when the + status endpoint returns an unusable response, or when the build does + not finish within ``FC_LAYER_STATUS_TIMEOUT`` seconds. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + FC_LAYER_STATUS_TIMEOUT + attempt = 0 + status = "" + + while loop.time() < deadline: + attempt += 1 + try: result = await client_fc( FC_API_KEY, FC_LAYER_QUERY_API, @@ -466,36 +482,61 @@ async def get_layer(self, layer_code: str) -> str: }, "GET", ) - status = result.get("output", {}).get("status", "") - if status != "SUCCESS": - i += 1 - logger.debug( - f"Load function layer({i}) | layer_code:" - f" {layer_code} | Status: {status}", - ) - await asyncio.sleep(10) - else: - break - - logger.debug( - f"Load function layer | layer_code: {layer_code} | Status:" - f" {status}", - ) - if status != "SUCCESS": + except Exception as e: raise FunctionLayerError( - f"Function layer create failed: {status}", + f"status query error | layer_code: {layer_code} | {e}", error_code=2014, + layer_code=layer_code, + ) from e + + status = result.get("output", {}).get("status", "") + + if status == "SUCCESS": + logger.debug( + f"Load function layer | layer_code: {layer_code} | " + f"Status: {status}", + ) + return status + + if status == "FAILED": + raise FunctionLayerError( + f"build reported FAILED | layer_code: " + f"{layer_code} | {result}", + error_code=2015, + layer_code=layer_code, ) - return status + if not status: + raise FunctionLayerError( + f"status query returned no status | layer_code: " + f"{layer_code} | {result}", + error_code=2016, + layer_code=layer_code, + ) - except Exception as e: - logger.warning( - f"Load function layer failed | layer_code: {layer_code}, " - f"Error: {str(e)}", + if attempt == 1: + logger.info( + f"Waiting for function layer build | layer_code: " + f"{layer_code} | Status: {status} | Timeout: " + f"{FC_LAYER_STATUS_TIMEOUT}s", + ) + else: + logger.debug( + f"Load function layer({attempt}) | layer_code:" + f" {layer_code} | Status: {status}", + ) + + await asyncio.sleep( + min(FC_LAYER_STATUS_INTERVAL, deadline - loop.time()), ) - return "SUCCESS" + raise FunctionLayerError( + f"build timed out after {FC_LAYER_STATUS_TIMEOUT}s | " + f"layer_code: {layer_code} | Last status: {status} | Raise " + f"FC_LAYER_STATUS_TIMEOUT if the build needs longer", + error_code=2017, + layer_code=layer_code, + ) def clean_temp_files(self, tmp_path: str) -> None: """Cleanup temporary deployment files.""" @@ -770,9 +811,11 @@ async def load( warmup: bool = False, ) -> ResponseFC: """Load and initialize a registered function instance.""" + runtime_obj = runtime or self.runtime + target_entity_id = entity_id or self.entity_id + layer_code = runtime_obj.layer_code if runtime_obj else None + try: - # Resolve target registration ID - target_entity_id = entity_id or self.entity_id if not target_entity_id: raise ValueErrorWithCode( "No valid registration ID provided", @@ -780,17 +823,18 @@ async def load( ) if FC_LAYER_USED: - if self.runtime.layer_code is None: + if layer_code is None: raise ValueErrorWithCode( - "layer_code is required when FC_LAYER_USED is enabled", + "layer_code is required when FC_LAYER_USED is " + "enabled: pass a runtime carrying layer_code, or " + "set FC_LAYER_USED=false", error_code=2022, ) await self.fcmodel.get_layer( - layer_code=self.runtime.layer_code, + layer_code=layer_code, ) # Load function instance - runtime_obj = runtime or self.runtime runtime_dict = ( deep_remove_none({**runtime_obj.model_dump()}) if runtime_obj @@ -826,10 +870,30 @@ async def load( f"{result if LOG_LEVEL == 'DEBUG' else deep_mask(result)}", ) + except FunctionLayerError as e: + logger.warning( + f"Instance initialization blocked by function layer | " + f"EntityID: {target_entity_id}, LayerCode: {layer_code}, " + f"Error: {str(e)}", + ) + return ResponseFC( + status=Status( + task=StatusType.FAILED, + name="FunctionLayerError", + code=526, + message=( + f"Function layer not ready | EntityID: " + f"{target_entity_id}, LayerCode: {layer_code}, " + f"Error: {str(e)}" + ), + ), + output={}, + ) + except Exception as e: - logger.debug( + logger.warning( f"Instance initialization failed | EntityID:" - f" {target_entity_id}, " + f" {target_entity_id}, LayerCode: {layer_code}, " f"Error: {str(e)}", ) return ResponseFC( @@ -837,7 +901,11 @@ async def load( task=StatusType.FAILED, name="FunctionLoadError", code=522, - message=f"Instance initialization failed: {str(e)}", + message=( + f"Instance initialization failed | EntityID: " + f"{target_entity_id}, LayerCode: {layer_code}, " + f"Error: {str(e)}" + ), ), output={}, ) diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py index 62e29a53..d43a1429 100644 --- a/dashscope/legacy_1x/_shim.py +++ b/dashscope/legacy_1x/_shim.py @@ -7,6 +7,7 @@ import importlib import importlib.abc import importlib.util +import os import sys @@ -85,6 +86,23 @@ _FINDER_MARKER = "_dashscope_legacy_1x_alias_finder" +_CLIENT_NAME = "dashscope.client" + + +def _client_module_spec(): + """Bind ``dashscope.client`` to ``client.py`` when a leftover 1.x ``client/`` + directory shadows it, since CPython resolves directories first. + + Returns None on a clean install, where normal resolution already works. + """ + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + module_file = os.path.join(root, "client.py") + if not os.path.isfile(module_file): + return None + if not os.path.isdir(os.path.join(root, "client")): + return None + return importlib.util.spec_from_file_location(_CLIENT_NAME, module_file) + def _canonical_name(name): if name in _EXACT_MODULES: @@ -130,6 +148,8 @@ class _AliasFinder(importlib.abc.MetaPathFinder): _dashscope_legacy_1x_alias_finder = True def find_spec(self, fullname, path=None, target=None): + if fullname == _CLIENT_NAME: + return _client_module_spec() canonical_name = _canonical_name(fullname) if canonical_name is None: return None diff --git a/tests/legacy_1x/unit/test_agentic_rl_layer_status.py b/tests/legacy_1x/unit/test_agentic_rl_layer_status.py new file mode 100644 index 00000000..cd50cfd9 --- /dev/null +++ b/tests/legacy_1x/unit/test_agentic_rl_layer_status.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +"""Layer readiness must gate instance loading instead of being swallowed. + +``get_layer`` used to catch its own ``FunctionLayerError`` and return +``"SUCCESS"``, so an unbuilt layer surfaced later as an unrelated +``Empty instance ID`` load failure. These cover the terminal states, the +stalled build, and the two error paths in ``load``. +""" + +import pytest + +from dashscope.finetune.reinforcement import ( + FunctionComponentModel, + FunctionComponentRuntime, + FunctionLayerError, + RolloutFunctionComponent, +) +from dashscope.finetune.reinforcement.common import model + +LAYER_CODE = "layer_c5669b3b602d4ae9b99bc8f576e4fdce" +ENTITY_ID = "ro-85a1dc67-cd85-49a9-855b-b2bb575d80ed" + +# The payload the FaaS gateway returns when a layer is missing in a region: +# HTTP 200 with the failure described in the body and no ``output.status``. +LAYER_NOT_FOUND_BODY = { + "request_id": "55bd69f2-3e3b-9cc0-989c-07b867aa9f17", + "message": "FaaS layer not found for region cn-wulanchabu", + "code": "BadRequest", + "status": {"code": 200, "message": "OK"}, + "output": {}, +} + +# A layer that is already built, followed by a successful instance load. +READY_LAYER_THEN_INSTANCE = [ + {"output": {"status": "SUCCESS"}}, + { + "output": { + "instanceId": "in-1", + "trigger_url": "https://example.com/invoke", + "trigger_token": "token", + }, + }, +] + + +def _stub_responses(monkeypatch, responses): + """Serve ``responses`` in order, repeating the last one, and count calls.""" + calls = {"count": 0, "payloads": []} + + async def fake_client_fc(*args, **kwargs): + index = calls["count"] + calls["count"] += 1 + calls["payloads"].append(args[2] if len(args) > 2 else kwargs) + response = responses[min(index, len(responses) - 1)] + if isinstance(response, Exception): + raise response + return response + + monkeypatch.setattr(model, "client_fc", fake_client_fc) + return calls + + +def _component(with_runtime=True): + component = RolloutFunctionComponent(name="rollout-1") + component.entity_id = ENTITY_ID + component.fcmodel = FunctionComponentModel() + if with_runtime: + component.runtime = FunctionComponentRuntime(layer_code=LAYER_CODE) + return component + + +async def test_get_layer_returns_ready_status(monkeypatch): + calls = _stub_responses(monkeypatch, [{"output": {"status": "SUCCESS"}}]) + + assert await FunctionComponentModel().get_layer(LAYER_CODE) == "SUCCESS" + assert calls["count"] == 1 + + +async def test_get_layer_waits_out_a_slow_build(monkeypatch): + calls = _stub_responses( + monkeypatch, + [ + {"output": {"status": "RUNNING"}}, + {"output": {"status": "RUNNING"}}, + {"output": {"status": "SUCCESS"}}, + ], + ) + monkeypatch.setattr(model, "FC_LAYER_STATUS_INTERVAL", 0) + + assert await FunctionComponentModel().get_layer(LAYER_CODE) == "SUCCESS" + assert calls["count"] == 3 + + +async def test_get_layer_raises_on_failed_build(monkeypatch): + _stub_responses( + monkeypatch, + [{"output": {"status": "FAILED"}, "message": "pip install exploded"}], + ) + + with pytest.raises(FunctionLayerError) as excinfo: + await FunctionComponentModel().get_layer(LAYER_CODE) + + message = str(excinfo.value) + assert "pip install exploded" in message + assert LAYER_CODE in message + # The class prefix names the subsystem, not the create stage, so a build + # failure must not read as "create failed: ... reported FAILED". + assert "Function layer failed: build reported FAILED" in message + assert excinfo.value.layer_code == LAYER_CODE + assert excinfo.value.layer_name is None + + +async def test_get_layer_raises_on_region_rejection(monkeypatch): + calls = _stub_responses(monkeypatch, [LAYER_NOT_FOUND_BODY]) + + with pytest.raises(FunctionLayerError) as excinfo: + await FunctionComponentModel().get_layer(LAYER_CODE) + + assert "cn-wulanchabu" in str(excinfo.value) + assert calls["count"] == 1 + + +async def test_get_layer_raises_when_build_stalls(monkeypatch): + calls = _stub_responses(monkeypatch, [{"output": {"status": "RUNNING"}}]) + monkeypatch.setattr(model, "FC_LAYER_STATUS_TIMEOUT", 0.2) + monkeypatch.setattr(model, "FC_LAYER_STATUS_INTERVAL", 0.05) + + with pytest.raises(FunctionLayerError) as excinfo: + await FunctionComponentModel().get_layer(LAYER_CODE) + + message = str(excinfo.value) + assert "timed out" in message + assert "RUNNING" in message + assert "FC_LAYER_STATUS_TIMEOUT" in message + assert calls["count"] > 3 + + +async def test_get_layer_raises_on_transport_error(monkeypatch): + _stub_responses(monkeypatch, [RuntimeError("connection reset")]) + + with pytest.raises(FunctionLayerError) as excinfo: + await FunctionComponentModel().get_layer(LAYER_CODE) + + assert "connection reset" in str(excinfo.value) + + +async def test_load_reports_layer_failure_as_such(monkeypatch): + _stub_responses(monkeypatch, [{"output": {"status": "FAILED"}}]) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + result = await _component().load(entity_id=ENTITY_ID) + + assert not result.status.success + assert result.status.name == "FunctionLayerError" + assert result.status.code == 526 + assert LAYER_CODE in result.status.message + assert "Empty instance ID" not in result.status.message + + +async def test_load_without_runtime_explains_missing_layer_code(monkeypatch): + _stub_responses(monkeypatch, [{"output": {"status": "SUCCESS"}}]) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + result = await _component(with_runtime=False).load(entity_id=ENTITY_ID) + + assert not result.status.success + assert "layer_code is required" in result.status.message + assert "NoneType" not in result.status.message + + +async def test_load_accepts_layer_code_from_the_runtime_argument(monkeypatch): + """The 2022 message tells callers to pass a runtime carrying layer_code, + so ``load`` must honour that argument when ``self.runtime`` is absent.""" + _stub_responses(monkeypatch, READY_LAYER_THEN_INSTANCE) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + result = await _component(with_runtime=False).load( + entity_id=ENTITY_ID, + runtime=FunctionComponentRuntime(layer_code=LAYER_CODE), + ) + + assert result.status.success + assert result.output["instance_id"] == "in-1" + + +async def test_load_waits_on_the_runtime_argument_layer(monkeypatch): + """The argument already overrides ``self.runtime`` in the request body, so + it must also decide which layer build is waited on.""" + calls = _stub_responses(monkeypatch, READY_LAYER_THEN_INSTANCE) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + override_code = "layer_0f3d9c1a77b24e08a5c6d4b1e9f07a23" + result = await _component().load( + entity_id=ENTITY_ID, + runtime=FunctionComponentRuntime(layer_code=override_code), + ) + + assert result.status.success + assert calls["payloads"][0] == {"layer_code": override_code} + assert calls["payloads"][1]["layer_code"] == override_code + + +async def test_load_reports_server_rejection_with_context(monkeypatch): + _stub_responses( + monkeypatch, + [{"output": {"status": "SUCCESS"}}, LAYER_NOT_FOUND_BODY], + ) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + result = await _component().load(entity_id=ENTITY_ID) + + assert not result.status.success + assert result.status.name == "FunctionLoadError" + assert ENTITY_ID in result.status.message + assert LAYER_CODE in result.status.message + assert "cn-wulanchabu" in result.status.message + + +async def test_load_succeeds_once_layer_is_ready(monkeypatch): + _stub_responses(monkeypatch, READY_LAYER_THEN_INSTANCE) + monkeypatch.setattr(model, "FC_LAYER_USED", True) + + result = await _component().load(entity_id=ENTITY_ID) + + assert result.status.success + assert result.output["instance_id"] == "in-1" diff --git a/tests/test_legacy_import_aliases.py b/tests/test_legacy_import_aliases.py index 4445e6b1..9b69e605 100644 --- a/tests/test_legacy_import_aliases.py +++ b/tests/test_legacy_import_aliases.py @@ -114,6 +114,51 @@ def test_client_and_legacy_base_api_coexist(): assert Client.__module__ == "dashscope.client" +def test_leftover_1x_client_dir_does_not_shadow_client_module(tmp_path): + """pip leaves 1.x files behind on an in-place upgrade, and a directory + outranks a same-named module, so the empty 1.x ``client/`` would win.""" + site = tmp_path / "site-packages" + installed = site / "dashscope" + installed.mkdir(parents=True) + for entry in (_REPO / "dashscope").iterdir(): + if entry.name == "__pycache__": + continue + (installed / entry.name).symlink_to(entry) + + stale = installed / "client" + stale.mkdir() + (stale / "__init__.py").write_text("", encoding="utf-8") + (stale / "base_api.py").write_text("", encoding="utf-8") + + env = os.environ.copy() + env["PYTHONDONTWRITEBYTECODE"] = "1" + env["PYTHONPATH"] = str(site) + result = subprocess.run( + [ + sys.executable, + "-c", + """ +import dashscope +import dashscope.client as client +from dashscope.client import AsyncClient, Client +from dashscope.client.base_api import BaseApi + +assert client.__file__.endswith('client.py'), client.__file__ +assert Client is dashscope.Client +assert AsyncClient is dashscope.AsyncClient +assert Client.__module__ == 'dashscope.client' +assert BaseApi.__module__ == 'dashscope.legacy_1x.client.base_api' +""", + ], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + @pytest.mark.parametrize( ("old_name", "canonical_name"), [ From 89916e93e6ccb876fc0f4f3c7f78005240b73396 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 30 Oct 2026 14:19:09 +0800 Subject: [PATCH 41/41] docs(readme): update documentation with new feature groups and configuration options - Add acli-openai, acli-anthropic, acli-voice, acli-camera, acli-all feature group descriptions - Add base_url, config_path, protocol_options, raise_on_error, http_client configuration options - Add DASHSCOPE_HTTP_BASE_URL and DASHSCOPE_WEBSOCKET_BASE_URL environment variable support - Extend HTTP connection pool related environment variable configurations - Update model list including qwen-turbo, wan2.6-image, wan2.7-i2v, cosyvoice-v3-flash, etc. - Fix streaming transmission retry mechanism description - Add API key file path environment variable support --- README.md | 41 ++++++++++++++++++++++++++++++----------- README_zh.md | 41 ++++++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index febbc493..3ed6ff90 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,16 @@ The DashScope Python SDK is the official client for [Alibaba Cloud Model Studio pip install -U dashscope ``` -The base install covers SDK API calls, the `dashscope` CLI command, Agentic RL fine-tuning and the local tokenizer. Python 3.9+ is required. One optional feature group remains: +The base install covers SDK API calls, the `dashscope` CLI command, Agentic RL fine-tuning and the local tokenizer. Python 3.9+ is required. Optional feature groups all belong to the bundled AI assistant: | Extra | Provides | Install | |-------|----------|---------| | `acli` | Interactive AI assistant (DashScope SDK Expert) | `pip install "dashscope[acli]"` | +| `acli-openai` | Run the assistant on an OpenAI-compatible model provider | `pip install "dashscope[acli-openai]"` | +| `acli-anthropic` | Run the assistant on an Anthropic model provider | `pip install "dashscope[acli-anthropic]"` | +| `acli-voice` | Voice input and spoken replies in the assistant | `pip install "dashscope[acli-voice]"` | +| `acli-camera` | Camera capture for image input in the assistant | `pip install "dashscope[acli-camera]"` | +| `acli-all` | All of the above (includes `acli`) | `pip install "dashscope[acli-all]"` | To install from a source checkout: @@ -120,9 +125,16 @@ client = dashscope.Client( protocol="native", # native | openai | anthropic timeout=300.0, max_retries=2, + base_url=None, # override the endpoint entirely; default: DASHSCOPE_BASE_URL + config_path=None, # alternate config.toml; default: DASHSCOPE_CONFIG + protocol_options=None, # extra kwargs forwarded to the selected protocol adapter + raise_on_error=True, # False: return error responses instead of raising + http_client=None, # your own httpx.Client (AsyncClient for AsyncClient) ) ``` +All options are keyword-only. + Use it as a context manager to close the pool automatically, or derive a variant with `with_options()` (unmentioned options are inherited; the connection pool is shared when connection-level options are unchanged): ```python @@ -189,7 +201,7 @@ By default each frame is incremental. Pass `incremental_output=False` to receive Thinking models surface reasoning on `.reasoning_stream` and, in the merged message, as `message.reasoning_content`. Under `protocol="anthropic"` the SDK also keeps `message.reasoning_signature` plus the raw `thinking` / `redacted_thinking` blocks in `message.reasoning_blocks`, so echoing the assistant message back on the next turn preserves extended thinking. -> **Note:** the SDK does **not** auto-retry streaming requests. Wrap stream consumption in your own retry/backoff for production — see [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py). +> **Note:** streaming retries stop at the handshake. The SDK retries connection failures and retryable HTTP statuses only *before* the first event reaches you; once a stream is open it is never replayed, so a mid-stream drop surfaces as an exception. Wrap stream consumption in your own retry/backoff for production — see [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py). ### Errors @@ -266,11 +278,19 @@ dashscope.settings.log_level = "INFO" | `DASHSCOPE_WORKSPACE` | Workspace ID (endpoint subdomain) | — | | `DASHSCOPE_REGION` | Region | `cn-beijing` | | `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | -| `DASHSCOPE_BASE_URL` | Override the endpoint entirely (escape hatch) | — | +| `DASHSCOPE_BASE_URL` | Override the HTTP endpoint entirely (escape hatch) | — | +| `DASHSCOPE_HTTP_BASE_URL` | 1.x name for the same override, honored as a fallback | — | +| `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_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` | | `DASHSCOPE_LOG` | Log level | `WARNING` | | `DASHSCOPE_CONFIG` | Path to `config.toml` | `~/.dashscope/config.toml` | +| `DASHSCOPE_API_KEY_FILE_PATH` | Path to the 1.x key file (lowest-priority auth tier) | `~/.dashscope/api_key` | + +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. **Config file** (`~/.dashscope/config.toml`): @@ -395,16 +415,16 @@ Each task on the connection reads through a bounded queue (`task_queue_size=`, d | Category | Example models | 2.0 resource | |----------|----------------|--------------| -| Text generation | qwen-max, qwen-plus, qwen-flash | `client.chat` | +| Text generation | qwen-max, qwen-plus, qwen-turbo | `client.chat` | | Multi-modal understanding | qwen3-vl-plus, qwen-omni | `client.multimodal.chat` | | Text embedding | text-embedding-v4, text-embedding-v3 | `client.embeddings` | | Multi-modal embedding | tongyi-embedding-vision-plus | `client.multimodal.embeddings` | | Rerank | gte-rerank-v2, qwen3-rerank | `client.rerank` | -| Image generation | wanx2.1-t2i-turbo, wan-image | `client.images` | -| Video generation | wanx2.1-t2v, wan-i2v | `client.videos` | -| Speech synthesis (TTS) | cosyvoice-v1, qwen-tts | `client.audio.tts` | +| Image generation | wanx2.1-t2i-turbo, wan2.6-image | `client.images` | +| Video generation | wanx2.1-t2v, wan2.7-i2v | `client.videos` | +| Speech synthesis (TTS) | cosyvoice-v3-flash, cosyvoice-v1, qwen-tts | `client.audio.tts` | | Speech recognition (ASR) | fun-asr-realtime, paraformer-v1 | `client.audio.asr` | -| Omni (realtime) | qwen-omni-realtime | `client.audio.realtime` | +| Omni (realtime) | qwen-omni-turbo-realtime | `client.audio.realtime` | For the latest model list, visit the [Model Studio model plaza](https://bailian.console.aliyun.com/). @@ -414,7 +434,7 @@ The SDK ships with an interactive AI assistant, **DashScope SDK Expert**, built - Run `dashscope` with no arguments to start the assistant. On first run it offers to install the knowledge pack (per-domain quick-reference skills: text, multimodal, speech, retrieval, fine-tuning, agent, cli), so guidance comes from the SDK's public interfaces — parameters, outputs, error codes — without reading the source. - Ask it instead of reading docs — e.g. `dashscope expert "how do I stream chat output"` (asks once and exits). Type `/help` inside the assistant for available commands; `dashscope expert --help` shows all forms. -- Install with the `acli` extra: `pip install "dashscope[acli]"`. +- Install with the `acli` extra: `pip install "dashscope[acli]"`. Add `acli-voice` / `acli-camera` for voice and camera input, or `acli-all` for everything — see the [extras table](#installation). - Full walkthrough: [DashScope SDK Expert guide](https://help.aliyun.com/en/model-studio/dashscope-sdk-expert). ## Shell completion @@ -501,8 +521,7 @@ python -m pytest tests/legacy_1x -q # archived compatibility python -m pytest tests -q # everything ``` -For current coverage, run `coverage run -m pytest tests ---ignore=tests/legacy_1x`, then `coverage report -m`. +For current coverage, run `coverage run -m pytest tests --ignore=tests/legacy_1x`, then `coverage report -m`. ## License diff --git a/README_zh.md b/README_zh.md index 015e76f4..ff02e8b9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -26,11 +26,16 @@ DashScope Python SDK 是[阿里云百炼(Model Studio)](https://help.aliyun. pip install -U dashscope ``` -基础安装包含 SDK API 调用、`dashscope` CLI 命令、Agentic RL 微调和本地 tokenizer,要求 Python 3.9+。仅剩一个可选功能组: +基础安装包含 SDK API 调用、`dashscope` CLI 命令、Agentic RL 微调和本地 tokenizer,要求 Python 3.9+。可选功能组都属于内置的 AI 助手: | Extra | 提供的能力 | 安装命令 | |-------|----------|---------| | `acli` | 交互式 AI 助手(DashScope SDK Expert) | `pip install "dashscope[acli]"` | +| `acli-openai` | 让助手使用 OpenAI 兼容的模型服务方 | `pip install "dashscope[acli-openai]"` | +| `acli-anthropic` | 让助手使用 Anthropic 模型服务方 | `pip install "dashscope[acli-anthropic]"` | +| `acli-voice` | 助手的语音输入与语音播报 | `pip install "dashscope[acli-voice]"` | +| `acli-camera` | 助手的摄像头采集图片输入 | `pip install "dashscope[acli-camera]"` | +| `acli-all` | 以上全部(含 `acli`) | `pip install "dashscope[acli-all]"` | 从源码安装: @@ -120,9 +125,16 @@ client = dashscope.Client( protocol="native", # native | openai | anthropic timeout=300.0, max_retries=2, + base_url=None, # 整体覆盖端点;默认取 DASHSCOPE_BASE_URL + config_path=None, # 指定其他 config.toml;默认取 DASHSCOPE_CONFIG + protocol_options=None, # 透传给所选协议适配器的额外关键字参数 + raise_on_error=True, # False 时返回错误响应对象而不抛异常 + http_client=None, # 自备 httpx.Client(AsyncClient 用 httpx.AsyncClient) ) ``` +所有选项均为关键字参数(keyword-only)。 + 用上下文管理器可自动关闭连接池;也可用 `with_options()` 派生一个变体(未指定的选项会被继承;当连接级选项不变时共享同一连接池): ```python @@ -189,7 +201,7 @@ for text in stream.text_stream: # 内容增量 思考模型的推理内容走 `.reasoning_stream`,合并后的消息里对应 `message.reasoning_content`。在 `protocol="anthropic"` 下,SDK 还会保留 `message.reasoning_signature` 以及原始的 `thinking` / `redacted_thinking` 块(位于 `message.reasoning_blocks`),因此把 assistant 消息原样回传即可延续 extended thinking。 -> **注意:** SDK **不会**自动重试流式请求。生产环境请在你自己的重试/退避逻辑中包裹流消费 —— 参见 [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py)。 +> **注意:** 流式的重试只到握手为止。SDK 仅在第一个事件到达你之前重试连接失败和可重试的 HTTP 状态码;流一旦打开就绝不会重放,因此中途断流会以异常形式抛出。生产环境请在你自己的重试/退避逻辑中包裹流消费 —— 参见 [`examples/04_real_world_scenarios/production_ready_rag_harness.py`](examples/04_real_world_scenarios/production_ready_rag_harness.py)。 ### 错误 @@ -266,11 +278,19 @@ dashscope.settings.log_level = "INFO" | `DASHSCOPE_WORKSPACE` | 业务空间 ID(端点子域名) | —— | | `DASHSCOPE_REGION` | 地域 | `cn-beijing` | | `DASHSCOPE_PROTOCOL` | `native` / `openai` / `anthropic` | `native` | -| `DASHSCOPE_BASE_URL` | 完全覆盖端点(逃生舱) | —— | +| `DASHSCOPE_BASE_URL` | 完全覆盖 HTTP 端点(逃生舱) | —— | +| `DASHSCOPE_HTTP_BASE_URL` | 1.x 的同义覆盖项,作为兜底仍然生效 | —— | +| `DASHSCOPE_WEBSOCKET_BASE_URL` | 覆盖 WebSocket 端点 | 由地域/业务空间推导 | | `DASHSCOPE_TIMEOUT` | 请求超时(秒) | `300` | | `DASHSCOPE_MAX_RETRIES` | 自动重试次数 | `2` | +| `DASHSCOPE_HTTP_MAX_CONNECTIONS` | 连接池大小 | `100` | +| `DASHSCOPE_HTTP_MAX_KEEPALIVE_CONNECTIONS` | 连接池保持的 keep-alive 连接数 | `20` | +| `DASHSCOPE_HTTP_KEEPALIVE_EXPIRY` | keep-alive 过期时间(秒) | `5.0` | | `DASHSCOPE_LOG` | 日志级别 | `WARNING` | | `DASHSCOPE_CONFIG` | `config.toml` 路径 | `~/.dashscope/config.toml` | +| `DASHSCOPE_API_KEY_FILE_PATH` | 1.x key 文件路径(优先级最低的鉴权层) | `~/.dashscope/api_key` | + +三个 `DASHSCOPE_HTTP_*` 连接池变量同样可以在 `settings` 和 `config.toml` 中以 `http_max_connections`、`http_max_keepalive_connections`、`http_keepalive_expiry` 设置。传入自备的 `http_client=` 时,以它为准,上述配置全部被忽略。 **配置文件**(`~/.dashscope/config.toml`): @@ -395,16 +415,16 @@ connection.close() | 类别 | 示例模型 | 2.0 资源 | |----------|----------------|--------------| -| 文本生成 | qwen-max、qwen-plus、qwen-flash | `client.chat` | +| 文本生成 | qwen-max、qwen-plus、qwen-turbo | `client.chat` | | 多模态理解 | qwen3-vl-plus、qwen-omni | `client.multimodal.chat` | | 文本向量 | text-embedding-v4、text-embedding-v3 | `client.embeddings` | | 多模态向量 | tongyi-embedding-vision-plus | `client.multimodal.embeddings` | | 重排 | gte-rerank-v2、qwen3-rerank | `client.rerank` | -| 图像生成 | wanx2.1-t2i-turbo、wan-image | `client.images` | -| 视频生成 | wanx2.1-t2v、wan-i2v | `client.videos` | -| 语音合成(TTS) | cosyvoice-v1、qwen-tts | `client.audio.tts` | +| 图像生成 | wanx2.1-t2i-turbo、wan2.6-image | `client.images` | +| 视频生成 | wanx2.1-t2v、wan2.7-i2v | `client.videos` | +| 语音合成(TTS) | cosyvoice-v3-flash、cosyvoice-v1、qwen-tts | `client.audio.tts` | | 语音识别(ASR) | fun-asr-realtime、paraformer-v1 | `client.audio.asr` | -| 全模态(实时) | qwen-omni-realtime | `client.audio.realtime` | +| 全模态(实时) | qwen-omni-turbo-realtime | `client.audio.realtime` | 最新模型列表请访问[百炼模型广场](https://bailian.console.aliyun.com/)。 @@ -414,7 +434,7 @@ SDK 内置了交互式 AI 助手 **DashScope SDK Expert**,基于随包提供 - 直接运行 `dashscope`(不带参数)即可启动助手。首次运行时会提示安装知识包(按领域划分的速查技能:文本、多模态、语音、检索、微调、Agent、CLI),使助手的指导来自 SDK 的公开接口 —— 参数、输出、错误码 —— 而无需阅读源码。 - 直接提问代替翻文档 —— 如 `dashscope expert "如何流式输出对话结果"`(一次性提问后退出)。在助手内输入 `/help` 查看可用命令;`dashscope expert --help` 查看全部形式。 -- 通过 `acli` extra 安装:`pip install "dashscope[acli]"`。 +- 通过 `acli` extra 安装:`pip install "dashscope[acli]"`。需要语音/摄像头输入可加装 `acli-voice` / `acli-camera`,或直接用 `acli-all` 装齐 —— 详见[安装](#安装)一节的 extra 表格。 - 完整使用指南:[DashScope SDK Expert 文档](https://help.aliyun.com/zh/model-studio/dashscope-sdk-expert)。 ## Shell 命令补全 @@ -528,8 +548,7 @@ python -m pytest tests/legacy_1x -q # 归档兼容套件 python -m pytest tests -q # 全量 ``` -当前实现的 coverage 使用 `coverage run -m pytest tests ---ignore=tests/legacy_1x`,随后执行 `coverage report -m`。 +当前实现的 coverage 使用 `coverage run -m pytest tests --ignore=tests/legacy_1x`,随后执行 `coverage report -m`。 ## 许可证