From 5cea230ff7911cbc234fb93ed67dea18ef340719 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Wed, 21 Oct 2026 17:31:08 +0800 Subject: [PATCH 1/7] 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 c5fdea8..148e269 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 bb9229b..28de30c 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 7217e39..c50a48f 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 ad8bc0b..48b57dd 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 17c2b1c..cadaff6 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 7d856e0..7064ed6 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 7a3042f..49cf937 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 da5a64c..9e8e7cd 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 f363ed6..af2e4c6 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 faf964f..82e0487 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 2/7] 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 0000000..6250b55 --- /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 0000000..c5fdea8 --- /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 0000000..134a0e6 --- /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 0000000..9f85ce6 --- /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 3/7] 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 99375a1..4b174aa 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 e69de29..3d4c9c3 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 7064ed6..5e1aa9d 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 49cf937..af497df 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 4/7] 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 5ddd1f4..cbf2d6b 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 6250b55..2b1e598 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 0000000..d291c80 --- /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 0000000..45cf03b --- /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 c5fdea8..044c945 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 0000000..0190e5f --- /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 a02e1f3..d5eef73 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 35af0d0..8ba069a 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 edd5818..5152e1f 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 6f1e32c..eba54d2 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 e0f1ba7..5b018c1 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 2cc9f71..292369d 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 bdf170a..f4bd5f1 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 244d227..cd5ca98 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 e69de29..105fba9 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 998f1a1..e421da0 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 b62d3da..55ede46 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 6eb707a..1220656 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 1fa101a..53fa4de 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 4bfb916..0a06ed4 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 8f06115..8374578 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 bd4d16c..ed6d74f 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 6100812..0000000 --- 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 6f5dedd..0000000 --- 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 9246fa5..0000000 --- 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 e69de29..75c5b90 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 0000000..64709f7 --- /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 0000000..2ce6ec9 --- /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 0000000..6f1e32c --- /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 2ec2c51..99957e6 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 4dbf422..281c72b 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 65a2f85..1f937f3 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 0c6f034..0f80f85 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 b830495..8d896ba 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 eb25276..fd56c71 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 c3d900b..67cf0e7 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 0c46257..12c3438 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 2de1c46..7edd7d7 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 611e72e..e278462 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 c6ea54a..4b83910 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 6511c29..1ff1fc8 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 1a9a664..eb3b984 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 5d8ac2f..d2ace56 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 a40dc2b..c6dac20 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 96ee9ea..4f28362 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 2358b4e..7e285aa 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 0000000..e0f1ba7 --- /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 6afb72c..90d70fa 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 19af166..976d715 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 0000000..9c63d15 --- /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 624c4c8..7169175 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 5342f78..2036050 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 6e11800..6cc21c8 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 0000000..bdf170a --- /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 c1dd1a5..d493a41 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 b9bf978..0967f54 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 88631ac..609f6c1 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 d24d277..bd32fea 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 9e8510b..9a3111b 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 9d304d3..d0e451c 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 b2e3e84..ca83195 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 f731ff9..f32b5f7 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 8f5251a..ddef003 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 f15f7c8..423b359 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 31eb038..48c6785 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 0381abe..c332868 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 239606a..073f708 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 eea32e3..a96b132 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 0000000..b72c1dd --- /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 9f75714..f4406ab 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 f850f71..f6b8fbd 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 a2ef3e7..3c83461 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 89aff97..f2144f8 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 414941f..5366e45 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 68f0eea..b05d050 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 6fffe1e..c3bb664 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 ae61cca..392614d 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 cb25a71..8b5f25a 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 d531752..2694b65 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 2305dae..9b7e667 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 a2efb62..7ae850d 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 63e393c..37754ca 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 dc0dc74..f4bc96b 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 a113399..1031db2 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 ea4db7b..c48299e 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 80bb815..e6e52bf 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 07ea030..f61f481 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 6978c39..0fba7bd 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 a869d01..6e212fa 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 af25d27..098098a 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 30fdfca..f430e86 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 8c46492..39455a5 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 8ae1817..deb1ba8 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 d083e84..74bc785 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 330baca..df8afa0 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 2b49cc7..73efc01 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 a5ca4ab..f345e05 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 9b546b9..1fc3461 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 28de30c..732e097 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 0000000..998f1a1 --- /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 c2c9b9c..d2ef183 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 405ee10..762653d 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 d68c13f..846a31b 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 b388bad..9835e19 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 0000000..c3e908c --- /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 ea6caf2..688a591 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 0000000..2e5a2bd --- /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 0000000..300a493 --- /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 0000000..540bb57 --- /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 63a04f8..1b563c5 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 fbceeb7..bf62aad 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 899c09e..577a865 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 0000000..e69de29 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 e6be294..b42db4a 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 0000000..e69de29 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 0000000..a271a68 --- /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 960a835..b399056 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 0000000..0249d6c --- /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 0000000..e69de29 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 81e5c00..54488db 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 10953af..8f2f2f0 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 0000000..e69de29 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 756d1ed..904836a 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 e7e0c86..2dcc80d 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 b47ff07..aa55241 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 e4effe3..788a4e5 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 0000000..e69de29 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 95187df..6541024 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 6aea734..b979ec1 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 fee38a4..9f97c04 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 540bb57..61f1c31 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 e69de29..48fa823 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 e69de29..4f4774b 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 2f0829c..dc61248 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 5ae531f..f0f9f93 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 e43a8af..bf30f99 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 9a538f9..2b2baa5 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 9a29de5..ba40b69 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 05ba3b0..b321876 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 2e385bf..89520f9 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 e69de29..5f7f9be 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 c50a48f..d6a0381 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 48b57dd..a727fae 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 0000000..edd5818 --- /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 be840af..87573d6 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 124ab5e..055e314 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 76c6512..aeeacbc 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 f47d6d9..0978b1a 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 91bf155..738eb2d 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 149b64f..4e93b23 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 8aed2ee..26a4ab3 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 e05c7be..a2c9936 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 573084a..1308a20 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 a6ebc72..8e5af66 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 617acaf..6fcccc6 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 1691a65..fc2e4b3 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 e2f2031..6c8164c 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 4bb68f1..e5be717 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 c9d9833..3ad70cf 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 c68e492..7440ec6 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 fc1a448..c20ee2c 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 bdbdc68..b338462 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 9083ba4..0a4e1e1 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 9cd3747..7c20eea 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 f56ef69..4339dab 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 d85faa6..a4c9268 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 f2c91dc..97dacbc 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 0000000..ed3aa25 --- /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 0000000..a698173 --- /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 0000000..8fa398c --- /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 0000000..391e24f --- /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 0000000..e69de29 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 e87de62..dcab4d9 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 3db44a5..0dd13fb 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 d0eb5a2..8baf5eb 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 a3d58a5..64a583d 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 93d768b..bc2a733 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 6277aa7..5172728 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 c1970c5..1b59e38 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 acd897f..dc03f39 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 07208c8..a1fdd8b 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 f21bd71..ef7435a 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 93c3061..1264d78 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 ea7bfa1..485da9e 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 1b92355..a251812 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 0000000..f43bc02 --- /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 a43688d..dd3476d 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 d99ad89..2ac8bc0 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 8099cc1..51a5f74 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 e10d946..26a99c2 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 a1ee558..d135ff4 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 96a00c3..5701ec1 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 2beb244..21670a5 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 2676e13..b72c7d4 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 3814c38..0a0dd20 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 62e435c..645a39d 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 44abe0f..e182465 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 c0d48b8..c6c260e 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 65fc382..3014ab1 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 1da284b..447109e 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 0000000..bb44299 --- /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 fac2769..0d7d3b6 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 aceb78d..e03c850 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 0dff819..c8d36b5 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 0aaed03..738b610 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 0000000..b579f7a --- /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 5a8f60a..df61885 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 8f13afd..f4fd4f5 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 0b64262..c9b4aec 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 9c6dace..bace5d5 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 0000000..e69de29 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 ce2d400..854e269 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 0000000..e69de29 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 0000000..e69de29 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 14400ac..b5454ee 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 c4b971c..7311f4a 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 a58700b..fc963ae 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 2235e4a..4299980 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 0000000..fb1eebe --- /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 8967547..f96de9f 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 cadaff6..258f902 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 ce80824..10b868a 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 c776c97..198f041 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 0000000..4e340f3 --- /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 0000000..9a538f9 --- /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 0000000..e8625bd --- /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 0000000..2e385bf --- /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 3d7c704..d0fea9f 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 55bb909..97be0ef 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 6e4406d..ff1ad09 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 e58c610..51794a9 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 940457b..cd647fb 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 e442c10..8ce3ad5 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 19e03d2..c7534d3 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 c1992ab..a76e844 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 1f096a4..b60cf76 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 92a6063..931c04d 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 47bf5d5..6f0d770 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 f5e425c..cc17d71 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 9b427d4..1922431 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 45b45d5..c3a7402 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 0ec959f..97cba0e 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 c5a84f4..f0ed25b 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 9a52320..ef84ae9 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 e546841..b2e886e 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 e8b5359..4729f22 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 5b19961..b1ffcd6 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 947297b..ff39b3b 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 3227a16..90d89c2 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 1e4d059..28e51fb 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 b196d52..293cc61 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 8b19746..678e209 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 00a6a97..4015481 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 2211ec5..4e240b0 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 46dede1..c61cdfa 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 7806808..fdcb6ef 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 b8e893e..47f4c05 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 0713088..cd2e578 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 0000000..baa56ec --- /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 e217842..59620f3 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 9fc79ff..0da6ed3 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 b82061a..e236fd9 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 2e2c4c0..3d95cf3 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 480c071..ead3b65 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 942b55b..29d2f75 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 1ea0811..b6c2236 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 10d3105..4d8f175 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 46375f5..e520033 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 957f830..71458a5 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 f3997b4..84e9572 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 c421e5e..8d2435e 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 f28f737..ca14905 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 9fd69d3..88bbf08 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 6a19f29..66788fc 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 142d6ca..b03835f 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 aac880a..f013171 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 60917fc..7f6b0b3 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 245efbc..ebbf7b6 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 0058b0e..d52621e 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 6380b39..ff09ac6 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 efb7fd1..5a56170 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 9da078b..69b6ec4 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 20cfc42..28ce93f 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 c15ea14..b0ca73d 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 c1af60c..16930ec 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 831ce9c..f378ad8 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 8dedcc0..c1dc326 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 ed7a135..599d6c5 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 d59410d..bd72d15 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 6ecce74..86fe722 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 0000000..a7ef235 --- /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 0e1d340..081ebc6 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 633ccb8..8cbf021 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 ceaedb5..710c08a 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 95748bf..ac600e5 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 505d384..2b4eb02 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 dcb4016..7a9c613 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 b742794..fcccd57 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 798db20..52dee3d 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 2d45475..c9e60b2 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 682c6a1..4742d56 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 5f9782f..84fc7bb 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 8be53bb..63f50a5 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 32dcaf2..c91f5cf 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 80743d1..f54f07f 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 00d1b1e..78221c2 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 0000000..9e3e442 --- /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 f51386b..3d4700b 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 f9ce1c9..0bc51ed 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 9e8e7cd..b1df754 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 0000000..e1af41c --- /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 c8fdb49..a7dc46e 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 0000000..b75c190 --- /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 af2e4c6..7cd6557 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 29e189b..dd982c9 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 5/7] 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 baa56ec..d8a3293 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 6/7] 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 9c2675f..54333af 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 c243553..018eb31 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 3d1b94e..23654c6 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 ffc58c4..7bb805e 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 3ba08e9..d605a04 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 f22b3ca..b58767f 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 ff76efb..ee76334 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 82e0487..f482001 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 7/7] 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 54333af..b6dcea3 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 018eb31..77c83fa 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 71b2c9f..e5f073f 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 450873d..2716598 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 e918f51..3a9ea72 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 d8d962d..29d8194 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 cafdebd..34b0875 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 d605a04..89f2815 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 b58767f..4568e6a 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 5716cb4..f723586 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 "{",