diff --git a/MANIFEST.in b/MANIFEST.in index 5ddd1f49..cbf2d6b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include dashscope/resources/qwen.tiktoken +include dashscope/v2/tokenizers/qwen.tiktoken include dashscope/py.typed include dashscope/__init__.pyi include NOTICE diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 99375a16..4b174aa5 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -16,25 +16,33 @@ ) from dashscope.version import __version__ -from dashscope.aigc.code_generation import CodeGeneration -from dashscope.aigc.conversation import Conversation, History, HistoryItem -from dashscope.aigc.generation import AioGeneration, Generation -from dashscope.aigc.image_synthesis import ImageSynthesis -from dashscope.aigc.multimodal_conversation import ( +from dashscope.legacy_1x.aigc.code_generation import CodeGeneration +from dashscope.legacy_1x.aigc.conversation import ( + Conversation, + History, + HistoryItem, +) +from dashscope.legacy_1x.aigc.generation import AioGeneration, Generation +from dashscope.legacy_1x.aigc.image_synthesis import ImageSynthesis +from dashscope.legacy_1x.aigc.multimodal_conversation import ( MultiModalConversation, AioMultiModalConversation, ) -from dashscope.aigc.video_synthesis import VideoSynthesis -from dashscope.app.application import Application -from dashscope.audio.asr.transcription import Transcription -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.aigc.video_synthesis import VideoSynthesis +from dashscope.legacy_1x.app.application import Application +from dashscope.legacy_1x.audio.asr.transcription import Transcription +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, ) -from dashscope.audio.tts.speech_synthesizer import SpeechSynthesizer -from dashscope.api_entities.aio_session import close_shared_aio_session -from dashscope.api_entities.http_request import close_shared_sync_session -from dashscope.common.api_key import save_api_key -from dashscope.common.env import ( +from dashscope.legacy_1x.audio.tts.speech_synthesizer import SpeechSynthesizer +from dashscope.legacy_1x.api_entities.aio_session import ( + close_shared_aio_session, +) +from dashscope.legacy_1x.api_entities.http_request import ( + close_shared_sync_session, +) +from dashscope.legacy_1x.common.api_key import save_api_key +from dashscope.legacy_1x.common.env import ( MAAS_REGIONS, api_key, api_key_file_path, @@ -45,22 +53,24 @@ ) from dashscope.finetune.deployments import Deployments from dashscope.finetune.finetunes import FineTunes -from dashscope.embeddings.batch_text_embedding import BatchTextEmbedding -from dashscope.embeddings.batch_text_embedding_response import ( +from dashscope.legacy_1x.embeddings.batch_text_embedding import ( + BatchTextEmbedding, +) +from dashscope.legacy_1x.embeddings.batch_text_embedding_response import ( BatchTextEmbeddingResponse, ) -from dashscope.embeddings.multimodal_embedding import ( +from dashscope.legacy_1x.embeddings.multimodal_embedding import ( MultiModalEmbedding, MultiModalEmbeddingItemAudio, MultiModalEmbeddingItemImage, MultiModalEmbeddingItemText, AioMultiModalEmbedding, ) -from dashscope.embeddings.text_embedding import TextEmbedding -from dashscope.files import Files -from dashscope.models import Models -from dashscope.nlp.understanding import Understanding -from dashscope.rerank import AioTextReRank, TextReRank +from dashscope.legacy_1x.embeddings.text_embedding import TextEmbedding +from dashscope.legacy_1x.files import Files +from dashscope.legacy_1x.models import Models +from dashscope.legacy_1x.nlp.understanding import Understanding +from dashscope.legacy_1x.rerank import AioTextReRank, TextReRank from dashscope.tokenizers import ( Tokenization, Tokenizer, @@ -114,28 +124,29 @@ def set_region(region: str, workspace_id: str = None): base_websocket_api_url = f"wss://{host}/api-ws/v1/inference" -# The Assistants API (dashscope.assistants / dashscope.threads) is deprecated. +# The Assistants API (dashscope.legacy_1x.assistants / +# dashscope.legacy_1x.threads) is deprecated. # Resolve its names lazily (PEP 562) so a plain `import dashscope` does not emit # the DeprecationWarning for code that never touches them; accessing any of these -# names imports dashscope.assistants, which warns once. +# names imports dashscope.legacy_1x.assistants, which warns once. _DEPRECATED_LAZY_IMPORTS = { - "Assistant": "dashscope.assistants", - "AssistantList": "dashscope.assistants", - "Assistants": "dashscope.assistants", - "AssistantFile": "dashscope.assistants", - "DeleteResponse": "dashscope.assistants", - "MessageFile": "dashscope.threads", - "Messages": "dashscope.threads", - "Run": "dashscope.threads", - "RunList": "dashscope.threads", - "Runs": "dashscope.threads", - "RunStep": "dashscope.threads", - "RunStepList": "dashscope.threads", - "Steps": "dashscope.threads", - "Thread": "dashscope.threads", - "ThreadMessage": "dashscope.threads", - "ThreadMessageList": "dashscope.threads", - "Threads": "dashscope.threads", + "Assistant": "dashscope.legacy_1x.assistants", + "AssistantList": "dashscope.legacy_1x.assistants", + "Assistants": "dashscope.legacy_1x.assistants", + "AssistantFile": "dashscope.legacy_1x.assistants", + "DeleteResponse": "dashscope.legacy_1x.assistants", + "MessageFile": "dashscope.legacy_1x.threads", + "Messages": "dashscope.legacy_1x.threads", + "Run": "dashscope.legacy_1x.threads", + "RunList": "dashscope.legacy_1x.threads", + "Runs": "dashscope.legacy_1x.threads", + "RunStep": "dashscope.legacy_1x.threads", + "RunStepList": "dashscope.legacy_1x.threads", + "Steps": "dashscope.legacy_1x.threads", + "Thread": "dashscope.legacy_1x.threads", + "ThreadMessage": "dashscope.legacy_1x.threads", + "ThreadMessageList": "dashscope.legacy_1x.threads", + "Threads": "dashscope.legacy_1x.threads", } diff --git a/dashscope/_shared/__init__.py b/dashscope/_shared/__init__.py new file mode 100644 index 00000000..2b1e5986 --- /dev/null +++ b/dashscope/_shared/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Neutral leaf layer shared by the 1.x API and the 2.0 implementation. + +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. + +The only thing this package reaches outside itself is +``dashscope.version``, read defensively by ``_sdk_ident``. That module +is a bare constant with no imports of its own, and it deliberately stays +where it is: ``setup.py`` and ``docs/source/conf.py`` ``exec()`` the +file to read the version *without* importing the package, and turning +it into a re-export of something here would drag the whole SDK and its +runtime dependencies into any build environment that only wanted a +string. + +What this package must never import is the 1.x business surface +(``client``, ``common``, ``finetune``, ``audio`` and the rest). +``common/utils`` reaches ``_sdk_ident`` while ``dashscope/__init__`` is +still executing, so an import back into that surface would close a cycle +through a partially initialized root. ``dashscope.version`` is safe +precisely because it has no imports of its own; the business modules +all have plenty. +""" diff --git a/dashscope/_shared/_errors.py b/dashscope/_shared/_errors.py new file mode 100644 index 00000000..d291c803 --- /dev/null +++ b/dashscope/_shared/_errors.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The one exception base class both stacks inherit from. + +``except dashscope.legacy_1x.common.error.DashScopeException`` has to catch errors +raised by the 1.x API classes *and* by the AgentStudio sub-SDK, which the +2.0 client mounts directly. That only works if every subclass in both +stacks derives from the same class object, so the base lives here rather +than in either stack: copying it would silently split the hierarchy and +let errors escape a caller's ``except`` clause. + +``dashscope.legacy_1x.common.error`` re-exports this name, so the 1.x import path +keeps working and keeps the identical class. +""" + + +class DashScopeException(Exception): + """Base class for every error the dashscope SDK raises.""" + + +__all__ = ["DashScopeException"] diff --git a/dashscope/_shared/_sdk_ident.py b/dashscope/_shared/_sdk_ident.py new file mode 100644 index 00000000..45cf03b9 --- /dev/null +++ b/dashscope/_shared/_sdk_ident.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""SDK identification headers shared by the 1.x API and the 2.0 package. + +``x-dashscope-sdk-client`` tells the backend which client and which +functional area issued a request, and ``x-dashscope-sdk-session-id`` +groups every request one interpreter process makes. + +The client segment is process-wide *mutable* state: entry points that +embed the SDK overwrite it (the ``dashscope`` CLI marks itself as +``python-cli`` via ``set_sdk_client``), and every caller in both stacks +must see the same value. Duplicating it would silently drop the override +for whichever stack held the copy, so it lives here -- one source of +truth, same reason as the session id in ``_shared._sdk_session``. + +The 2.0 transport builds its own header in +``dashscope.v2.transport._ident`` because it appends a trailing +generation marker (``aigc`` -> ``aigc2``) that this unmarked 1.x shape +must not carry. Sub-SDKs that predate the marker -- AgentStudio, whose +``agentstudio`` tag the backend already reports on -- keep using this +module. + +``get_user_agent`` belongs here for the same reason as the two headers: +it is the other half of the request identity, and it must agree with +``get_sdk_headers`` on the version segment. It is the *1.x shape* +(``dashscope/; python/...; platform/...; processor/...``) and is +deliberately not shared with the 2.0 transport's own +``dashscope.v2.transport.http.default_user_agent()`` +(``dashscope-v2-python/``). Those are two distinct backend +contracts, not two spellings of one value, so they must not be merged. + +Only the stdlib is imported at module level; ``dashscope.version`` is +pulled in lazily and defensively so this leaf stays below both stacks. +""" + +import os +import platform +from typing import Dict + +from dashscope._shared._sdk_session import sdk_session_id + +_SDK_CLIENT = "python-sdk" + +# Shared with the 2.0 transport so one process reports one +# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session; +# acli deliberately keeps its own, it is a one-way sync copy). +_SDK_SESSION_ID = sdk_session_id() + +__all__ = ["get_sdk_headers", "get_user_agent", "set_sdk_client"] + + +def _sdk_version() -> str: + try: + from dashscope.version import __version__ + except Exception: # pragma: no cover - defensive import guard + return "0.0.0" + return __version__ + + +# Read once at import so the value is stable for the process lifetime. +_VERSION = _sdk_version() + + +def set_sdk_client(client: str) -> None: + """Override the client identifier in the x-dashscope-sdk-client header. + + Process-wide, used by entry points that embed the SDK (e.g. the + ``dashscope`` CLI marks itself as ``python-cli``). + """ + global _SDK_CLIENT # pylint: disable=global-statement + if client: + _SDK_CLIENT = client + + +def get_sdk_headers(module: str = "") -> Dict[str, str]: + """Structured SDK identification headers for backend statistics. + + Set DASHSCOPE_DISABLE_SDK_HEADERS=1 to omit them. + + Args: + module: Optional module name (e.g. agentstudio, rl, app). + """ + if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): + return {} + parts = [_SDK_CLIENT, _VERSION] + if module: + parts.append(module) + return { + "x-dashscope-sdk-client": "/".join(parts), + "x-dashscope-sdk-session-id": _SDK_SESSION_ID, + } + + +def get_user_agent() -> str: + """The 1.x-shaped ``user-agent`` value. + + Kept byte-identical to the pre-move implementation: the version + segment is ``_VERSION`` here rather than a second read of + ``dashscope.version``, so it can never drift from the one in + ``get_sdk_headers``. + """ + try: + platform_ = ( + platform.platform().replace("\n", "").replace("\r", "").strip() + ) + except Exception: + platform_ = "unknown" + + try: + processor_ = ( + platform.processor().replace("\n", "").replace("\r", "").strip() + ) + except Exception: + processor_ = "unknown" + + ua = ( + f"dashscope/{_VERSION}; python/{platform.python_version()}; " + f"platform/{platform_}; " + f"processor/{processor_}" + ) + return ua diff --git a/dashscope/common/_sdk_session.py b/dashscope/_shared/_sdk_session.py similarity index 74% rename from dashscope/common/_sdk_session.py rename to dashscope/_shared/_sdk_session.py index c5fdea8e..044c9455 100644 --- a/dashscope/common/_sdk_session.py +++ b/dashscope/_shared/_sdk_session.py @@ -5,18 +5,18 @@ The ``x-dashscope-sdk-session-id`` header lets server-side access logs group every request one SDK session makes. A process can emit requests through both stacks at once -- the 1.x API classes and the 2.0 transport --- and the 2.0 client even delegates some calls (rl, agentstudio) -straight into 1.x. If each stack minted its own id, one logical session -would split across several ids and the grouping breaks. +-- and the sub-SDKs owned by 2.0 (AgentStudio, agentic RL) build their +request headers from ``_shared._sdk_ident`` rather than from the v2 +transport. If each stack minted its own id, one logical session would +split across several ids and the grouping breaks. This leaf module is the one source of truth both import. It depends only on the stdlib so it can be pulled in from anywhere without dragging ``dashscope/__init__`` (and its v2 surface) into a cycle. The embedded ``acli`` deliberately keeps its own id: it is a one-way -sync copy of the agenticCLI repo (design doc ยง3.2), so wiring it to this -module would be silently reverted by the next sync and cannot be -upstreamed as written. +sync copy of the agenticCLI repo, so wiring it to this module would be +silently reverted by the next sync and cannot be upstreamed as written. """ from __future__ import annotations diff --git a/dashscope/_shared/_validation.py b/dashscope/_shared/_validation.py new file mode 100644 index 00000000..0190e5fa --- /dev/null +++ b/dashscope/_shared/_validation.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Host-label validation shared by the 1.x API and the 2.0 implementation. + +``workspace_id`` and ``region`` are interpolated into the host of the MaaS +endpoint, so both stacks must reject the same malformed values with the +same rule. Keeping the check in one leaf module means a value that is +safe for one stack is safe for the other, and neither stack has to import +the other to get it. + +Stdlib only, like the rest of ``dashscope._shared``. +""" + +import re + +# workspace_id and region become DNS labels of the MaaS endpoint host, so +# they must not contain characters (/, ?, #, @, :, whitespace, ...) that +# could break out of the host and redirect requests elsewhere. +# \A...\Z rather than ^...$: $ also matches before a trailing newline. +_WORKSPACE_ID_PATTERN = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\Z") +# RFC 1123 hostname label: 1-63 chars, no leading/trailing hyphen. Upper +# case is allowed because DNS is case-insensitive and httpx lowercases the +# host, so rejecting it would break working callers for no security gain. +_REGION_PATTERN = re.compile( + r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z", +) + +__all__ = ["validate_region", "validate_workspace_id"] + + +def validate_workspace_id(workspace_id): + """Validate that workspace_id is safe to use as a URL subdomain. + + Raises: + ValueError: If workspace_id contains characters outside + [A-Za-z0-9_-], is empty, or exceeds 64 characters. + """ + if not workspace_id or not _WORKSPACE_ID_PATTERN.fullmatch( + workspace_id, + ): + raise ValueError( + f"Invalid workspace_id {workspace_id!r}: must match " + "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$", + ) + return workspace_id + + +def validate_region(region): + """Validate that region is safe to use as a URL subdomain. + + Deliberately a hostname-label check and not a MAAS_REGIONS whitelist: + rejecting an unknown-but-well-formed region would break callers as soon + as a subpackage supports a region this list has not caught up with. + + Raises: + ValueError: If region is not a valid hostname label. + """ + if not region or not _REGION_PATTERN.fullmatch(region): + raise ValueError( + f"Invalid region {region!r}: must be a hostname label " + "(1-63 chars of [A-Za-z0-9-], not starting or ending " + "with '-')", + ) + return region diff --git a/dashscope/acli/ui/tts.py b/dashscope/acli/ui/tts.py index a02e1f3d..d5eef739 100644 --- a/dashscope/acli/ui/tts.py +++ b/dashscope/acli/ui/tts.py @@ -3,7 +3,7 @@ Supports both streaming playback (low-latency, audio starts before synthesis finishes) and one-shot synthesis. Uses -``dashscope.audio.tts_v2.SpeechSynthesizer``. +``dashscope.legacy_1x.audio.tts_v2.SpeechSynthesizer``. """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements @@ -284,7 +284,7 @@ def _synthesize_and_play(self, text: str) -> Optional[str]: dashscope.api_key = self.api_key try: - from dashscope.audio.tts_v2 import ( + from dashscope.legacy_1x.audio.tts_v2 import ( AudioFormat, ResultCallback, SpeechSynthesizer, diff --git a/dashscope/acli/ui/voice.py b/dashscope/acli/ui/voice.py index 35af0d03..8ba069a7 100644 --- a/dashscope/acli/ui/voice.py +++ b/dashscope/acli/ui/voice.py @@ -58,7 +58,10 @@ class _RealtimeASR: def __init__(self, api_key: str, model: str): os.environ.setdefault("DASHSCOPE_API_KEY", api_key) - from dashscope.audio.asr import Recognition, RecognitionCallback + from dashscope.legacy_1x.audio.asr import ( + Recognition, + RecognitionCallback, + ) self._model = model self._Recognition = Recognition diff --git a/dashscope/agentstudio/__init__.py b/dashscope/agentstudio/__init__.py index edd58186..5152e1fc 100644 --- a/dashscope/agentstudio/__init__.py +++ b/dashscope/agentstudio/__init__.py @@ -1,164 +1,58 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -"""AgentStudio sub-SDK for the dashscope Python client. +"""Compatibility shim; the implementation moved to ``dashscope.v2.agentstudio``. -The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP -API. All resource classes are accessed through a client instance:: +The AgentStudio sub-SDK is owned by the 2.0 package. This module keeps the +historical import path working: every public name is re-exported, and every +submodule is aliased in ``sys.modules`` so deep imports such as +``from dashscope.agentstudio.resources.agents import Agents`` resolve to the +very same module object -- and therefore the very same classes -- as the +``dashscope.v2.agentstudio`` path. - from dashscope.agentstudio import Client - from dashscope.agentstudio.types import ( - user_message, user_tool_result, - ) - -Example -------- ->>> from dashscope.agentstudio import Client ->>> from dashscope.agentstudio.types import user_message ->>> client = Client(api_key="sk-xxx") ->>> agent = client.agents.create(name="demo", model="qwen-plus") ->>> session = client.sessions.create(agent=agent.id) ->>> client.sessions.events.send(session.id, [user_message("Hello!")]) ->>> with client.sessions.events.stream(session.id) as stream: -... for event in stream: -... print(event.type, event.to_dict()) -... if event.type == "session_status": -... break +Import from ``dashscope.v2.agentstudio`` in new code. """ -# yapf: disable +import pkgutil +import sys -from .client import Client, AsyncClient -from .transport import APIResponse, RequestOptions -from . import types -from .exceptions import ( - AgentStudioError, - APIConnectionError, - APIStatusError, - APITimeoutError, - AuthenticationError, - ConflictError, - InternalServerError, - InvalidRequestError, - NotFoundError, - OverloadedError, - PermissionDeniedError, - RateLimitError, - StreamClosedError, - StreamError, -) -from .pagination import ( - AsyncCursorPage, - CursorPage, -) -from .types import ( - Memory, - MemoryStore, - MemoryVersion, - Message, - PermissionPolicy, - SecurityAlert, - SecurityAlertList, - SecurityOverview, - ServerEvent, - SessionResource, - StopReason, - user_define_outcome, - user_interrupt, - user_message, - user_tool_approval_response, - user_custom_tool_result, - user_tool_result, -) -from .types.params import ( - AgentCreateParams, - AgentUpdateParams, - SessionCreateParams, - SessionUpdateParams, - EnvironmentCreateParams, - EnvironmentUpdateParams, - DeploymentCreateParams, - DeploymentUpdateParams, - SessionEventSendParams, - WebhookEndpointCreateParams, - WebhookEndpointUpdateParams, - WebhookEventListParams, -) -from .constants import ( - PermissionPolicyType, - SSEEventType, - SessionStatus, - StopReasonType, - WebhookDeliveryStatus, - WebhookDisabledReason, - WebhookEventType, - WebhookStatus, +# Importing the package runs its own eager imports, so every submodule is +# already in sys.modules by the time the walk below starts. +from dashscope.v2 import agentstudio as _impl + +# pylint: disable-next=wildcard-import,unused-wildcard-import +from dashscope.v2.agentstudio import * # noqa: F401,F403 + +# The wildcard only carries the names in the implementation's __all__, which +# lists classes and helpers but not its submodules. Naming them here keeps +# ``from dashscope.agentstudio import exceptions`` resolvable statically; at +# runtime they are already imported by the package's own eager imports. +# pylint: disable-next=unused-import +from dashscope.v2.agentstudio import ( # noqa: F401 + client, + constants, + exceptions, + pagination, + resources, + streaming, + transport, + types, ) -__all__ = [ - # client - "Client", - "AsyncClient", - # base types - "RequestOptions", - "APIResponse", - # exceptions - "AgentStudioError", - "APIConnectionError", - "APIStatusError", - "APITimeoutError", - "AuthenticationError", - "ConflictError", - "InternalServerError", - "InvalidRequestError", - "NotFoundError", - "OverloadedError", - "PermissionDeniedError", - "RateLimitError", - "StreamError", - "StreamClosedError", - # pagination - "CursorPage", - "AsyncCursorPage", - # unified message type - "Memory", - "MemoryStore", - "MemoryVersion", - "Message", - "PermissionPolicy", - "SecurityAlert", - "SecurityAlertList", - "SecurityOverview", - "ServerEvent", - "SessionResource", - "StopReason", - # client event helpers (re-exported for convenience) - "user_message", - "user_interrupt", - "user_tool_approval_response", - "user_custom_tool_result", - "user_tool_result", - "user_define_outcome", - # typing module - "types", - # params (commonly used request-body types) - "AgentCreateParams", - "AgentUpdateParams", - "SessionCreateParams", - "SessionUpdateParams", - "EnvironmentCreateParams", - "EnvironmentUpdateParams", - "DeploymentCreateParams", - "DeploymentUpdateParams", - "SessionEventSendParams", - "WebhookEndpointCreateParams", - "WebhookEndpointUpdateParams", - "WebhookEventListParams", - "WebhookStatus", - "WebhookDisabledReason", - "WebhookDeliveryStatus", - "WebhookEventType", - "SSEEventType", - "SessionStatus", - "StopReasonType", - "PermissionPolicyType", -] +_V2_PACKAGE = _impl.__name__ + +__all__ = list(_impl.__all__) + +# Alias each submodule under the old dotted path. Registering in +# sys.modules alone is not enough: the import system skips a module it +# finds there, so the attribute on the parent would never be set and +# ``dashscope.agentstudio.resources`` would raise AttributeError. +for _info in pkgutil.walk_packages(_impl.__path__, prefix=_V2_PACKAGE + "."): + _module = sys.modules.get(_info.name) + if _module is None: # pragma: no cover - walk_packages imports as it goes + continue + _alias = __name__ + _info.name[len(_V2_PACKAGE) :] + sys.modules.setdefault(_alias, _module) + _parent_name, _, _leaf = _alias.rpartition(".") + _parent = sys.modules.get(_parent_name) + if _parent is not None: + setattr(_parent, _leaf, _module) diff --git a/dashscope/aigc/__init__.py b/dashscope/aigc/__init__.py index 6f1e32c8..eba54d24 100644 --- a/dashscope/aigc/__init__.py +++ b/dashscope/aigc/__init__.py @@ -1,24 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from .conversation import Conversation, History, HistoryItem -from .generation import Generation, AioGeneration -from .image_synthesis import ImageSynthesis, AioImageSynthesis -from .multimodal_conversation import ( - MultiModalConversation, - AioMultiModalConversation, -) -from .video_synthesis import VideoSynthesis, AioVideoSynthesis +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.aigc``. -__all__ = [ - "Generation", - "AioGeneration", - "Conversation", - "HistoryItem", - "History", - "ImageSynthesis", - "AioImageSynthesis", - "MultiModalConversation", - "AioMultiModalConversation", - "VideoSynthesis", - "AioVideoSynthesis", -] +Import from ``dashscope.legacy_1x.aigc`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.aigc") diff --git a/dashscope/api_entities/__init__.py b/dashscope/api_entities/__init__.py index e69de29b..3d4c9c3d 100644 --- a/dashscope/api_entities/__init__.py +++ b/dashscope/api_entities/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to +``dashscope.legacy_1x.api_entities``. + +Import from ``dashscope.legacy_1x.api_entities`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.api_entities") diff --git a/dashscope/app/__init__.py b/dashscope/app/__init__.py index e0f1ba76..5b018c18 100644 --- a/dashscope/app/__init__.py +++ b/dashscope/app/__init__.py @@ -1,6 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.app``. -from .application import Application +Import from ``dashscope.legacy_1x.app`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = ["Application"] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.app") diff --git a/dashscope/assistants/__init__.py b/dashscope/assistants/__init__.py index 2cc9f714..292369d2 100644 --- a/dashscope/assistants/__init__.py +++ b/dashscope/assistants/__init__.py @@ -1,31 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.assistants``. -import warnings +Import from ``dashscope.legacy_1x.assistants`` (or the public ``dashscope`` +root re-exports) in new code. +""" -# yapf: disable +from dashscope.legacy_1x._shim import install as _install -from dashscope.assistants.assistant_types import ( - Assistant, AssistantFile, - AssistantList, - DeleteResponse, -) -from dashscope.assistants.assistants import Assistants - -__all__ = [ - 'Assistant', - 'Assistants', - 'AssistantList', - 'AssistantFile', - 'DeleteResponse', -] - -# Deprecation warning -_DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " - "removed in a future release. Please migrate to the Responses API. " - "See https://help.aliyun.com/zh/model-studio/" - "synchronous-call-api-reference for migration details." -) - -warnings.warn(_DEPRECATION_MSG, category=DeprecationWarning, stacklevel=2) +_install(__name__, "dashscope.legacy_1x.assistants") diff --git a/dashscope/audio/__init__.py b/dashscope/audio/__init__.py index bdf170a7..f4bd5f18 100644 --- a/dashscope/audio/__init__.py +++ b/dashscope/audio/__init__.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.audio``. -from . import asr, tts, tts_v2, qwen_tts, qwen_tts_realtime, qwen_omni +Import from ``dashscope.legacy_1x.audio`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = [ # type: ignore[misc] - asr, - tts, - tts_v2, - qwen_tts, - qwen_tts_realtime, - qwen_omni, -] # noqa: E501 +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.audio") diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index 244d227b..cd5ca981 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -1,591 +1,11 @@ # -*- coding: utf-8 -*- -# pylint: disable=wrong-import-position -"""DashScope command-line entry point. +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.cli``. -This package is intentionally thin โ€” all command-specific logic lives in -sub-modules (generation, fine_tunes, files, etc.). +Import from ``dashscope.legacy_1x.cli`` (or the public ``dashscope`` +root re-exports) in new code. """ -import sys -import warnings -from pathlib import Path -from typing import Optional -# Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL -warnings.filterwarnings( - "ignore", - message=".*urllib3.*only supports OpenSSL.*", - category=Warning, -) +from dashscope.legacy_1x._shim import install as _install -import typer # noqa: E402 -from rich.markup import escape # noqa: E402 - -import dashscope # noqa: E402 -from dashscope.cli.common import console, err_console # noqa: E402 -from dashscope.common.error import AuthenticationError # noqa: E402 -from dashscope.common.utils import set_sdk_client # noqa: E402 -from dashscope.cli import ( # noqa: E402 - application, - auth, - deployments, - embeddings, - files, - fine_tunes, - generation, - image_generation, - image_synthesis, - models, - multimodal_conversation, - multimodal_embedding, - oss, - rerank, - speech_synthesis, - tokenization, - transcription, - video_synthesis, -) - -set_sdk_client("python-cli") - - -# --------------------------------------------------------------------------- -# Legacy command compatibility layer -# --------------------------------------------------------------------------- - -# Command name mapping: old -> new -_COMMAND_MAP = { - "fine_tunes.call": "fine-tunes create", - "fine_tunes.get": "fine-tunes get", - "fine_tunes.list": "fine-tunes list", - "fine_tunes.stream": "fine-tunes stream", - "fine_tunes.cancel": "fine-tunes cancel", - "fine_tunes.delete": "fine-tunes delete", - "generation.call": "generation create", - "files.upload": "files upload", - "files.get": "files get", - "files.list": "files list", - "files.delete": "files delete", - "deployments.call": "deployments create", - "deployments.get": "deployments get", - "deployments.list": "deployments list", - "deployments.scale": "deployments scale", - "deployments.delete": "deployments delete", - "oss.upload": "oss upload", -} - -# Parameter name mapping: old -> new (underscore to dash) -_PARAM_MAP = { - "--training_file_ids": "--training-file-ids", - "--validation_file_ids": "--validation-file-ids", - "--n_epochs": "--n-epochs", - "--batch_size": "--batch-size", - "--learning_rate": "--learning-rate", - "--prompt_loss": "--prompt-loss", - "--hyper_parameters": "--hyper-parameters", - "--file_id": "--file-id", - "--deployed_model": "--deployed-model", - "--base_url": "--base-url", - "--api_key": "--api-key", - "--start_page": "--start-page", - "--page_size": "--page-size", -} - -_TOP_LEVEL_COMMANDS = { - "auth", - "generation", - "ft", - "fine-tunes", - "files", - "deployments", - "oss", - "rerank", - "embeddings", - "tokenization", - "models", - "application", - "image-synthesis", - "video-synthesis", - "image-generation", - "multimodal-conversation", - "multimodal-embedding", - "transcription", - "speech-synthesis", - "rl", - "agentic-rl", -} - - -_COMMANDS_WITH_LOCAL_API_KEY = {"oss", "rl", "agentic-rl"} - -# Routing keyword for the bundled agent, handled in main() before typer sees -# argv. Deliberately NOT a member of _TOP_LEVEL_COMMANDS: that set mirrors the -# typer-registered command groups, and `expert` is not one of them. -_EXPERT_COMMAND = "expert" - -_EXPERT_USAGE = """[bold]DashScope SDK Expert[/bold] โ€” the built-in AI assistant. - -[bold]Usage:[/bold] - dashscope interactive assistant (TUI by default) - dashscope expert the same, spelled explicitly - dashscope expert --cli interactive, plain REPL instead of the TUI - dashscope expert --tui interactive, force the TUI - dashscope expert "question" ask once and exit - -With no argument, piped stdin is read as a one-shot prompt. -SDK subcommands (generation, ft, files, models, ...) dispatch as usual; an -unrecognized command is reported as an error rather than treated as a -question, so ask one with [bold]dashscope expert "question"[/bold]. - -Requires [bold]pip install dashscope[acli][/bold].""" - -_LEGACY_COMMANDS_WITH_SIZE_OPTION = { - "files.list", - "fine_tunes.list", - "deployments.list", -} - - -def _translate_param_name(arg): - """Translate legacy underscore option names to Typer hyphen names.""" - if arg in _PARAM_MAP: - return _PARAM_MAP[arg] - if arg.startswith("--") and "=" in arg: - option_name, option_value = arg.split("=", 1) - if option_name in _PARAM_MAP: - return f"{_PARAM_MAP[option_name]}={option_value}" - return arg - - -def _translate_legacy_args(argv): - """Translate legacy argparse command format to Typer format. - - Legacy format: dashscope fine_tunes.call --training_file_ids ... - New format: dashscope fine-tunes create --training-file-ids ... - - Returns modified argv list. - """ - if len(argv) < 2: - return argv - - is_legacy_command = argv[1] in _COMMAND_MAP - if not is_legacy_command: - return [argv[0]] + [_translate_param_name(arg) for arg in argv[1:]] - - command_args = _COMMAND_MAP[argv[1]].split() - top_level_args = [] - translated_args = [] - i = 2 - while i < len(argv): - arg = argv[i] - translated_arg = _translate_param_name(arg) - if argv[1] in _LEGACY_COMMANDS_WITH_SIZE_OPTION: - if translated_arg == "--page-size": - translated_arg = "--size" - elif translated_arg.startswith("--page-size="): - translated_arg = translated_arg.replace( - "--page-size=", - "--size=", - 1, - ) - - if translated_arg == "--api-key": - top_level_args.append("--api-key") - if i + 1 < len(argv): - top_level_args.append(argv[i + 1]) - i += 2 - else: - i += 1 - continue - - if translated_arg.startswith("--api-key="): - top_level_args.append(translated_arg) - i += 1 - continue - - translated_args.append(translated_arg) - i += 1 - - return [argv[0]] + top_level_args + command_args + translated_args - - -def _exit_missing_api_key_value(option_name): - err_console.print( - f"[red]Error:[/red] Option '{option_name}' requires an argument.", - ) - sys.exit(2) - - -def _extract_global_api_key(argv): - """Extract global -k/--api-key from argv and set dashscope.api_key. - - For backward compatibility, global api-key can appear before or after most - command names. Commands that define their own local api-key option are left - untouched once their command name has been reached. - """ - new_argv = [] - i = 0 - current_command = None - while i < len(argv): - arg = argv[i] - - if i > 0 and current_command is None and arg in _TOP_LEVEL_COMMANDS: - current_command = arg - - should_parse_global_api_key = ( - current_command not in _COMMANDS_WITH_LOCAL_API_KEY - ) - - if arg in ("-k", "--api-key"): - if i + 1 >= len(argv): - _exit_missing_api_key_value(arg) - next_arg = argv[i + 1] - if not next_arg or next_arg.startswith("-"): - _exit_missing_api_key_value(arg) - if should_parse_global_api_key: - if next_arg in _TOP_LEVEL_COMMANDS: - _exit_missing_api_key_value(arg) - dashscope.api_key = next_arg - i += 2 - continue - - if arg.startswith("--api-key="): - api_key = arg.split("=", 1)[1] - if not api_key: - _exit_missing_api_key_value("--api-key") - if should_parse_global_api_key: - dashscope.api_key = api_key - i += 1 - continue - - new_argv.append(arg) - i += 1 - - return new_argv - - -def _translate_help_shortcut(argv): - """Translate -h to --help for convenience.""" - return [arg if arg != "-h" else "--help" for arg in argv] - - -_MD_MARKER = "" -_PY_MARKER = "# managed-by: dashscope-expert" -_EXPERT_EXAMPLE = "dashscope-sdk-expert" - - -def _cleanup_legacy_expert_sync(): - """Remove files the deleted expert bundling synced into ~/.acli. - - Only files carrying the managed marker are deleted โ€” without it the file - is user-owned and left untouched. Needed so stale global copies don't - shadow the workspace ./.acli/ configs installed via example download. - """ - try: - from dashscope.acli.config import CONFIG_DIR - except ImportError: - return - md_targets = [] - for subdir in ("skills", "references"): - target_dir = CONFIG_DIR / subdir - if target_dir.is_dir(): - md_targets.extend(target_dir.glob("*.md")) - for path in md_targets: - try: - if _MD_MARKER in path.read_text(encoding="utf-8"): - path.unlink() - except OSError: - pass - build = CONFIG_DIR / "build_sdk_index.py" - try: - if build.is_file() and _PY_MARKER in build.read_text(encoding="utf-8"): - build.unlink() - except OSError: - pass - - -def _maybe_offer_example_download(): - """First interactive run without ./.acli: offer the SDK-expert example. - - Accepting merges the example into ./.acli/ via the same code path as - `example download` (copied from the packaged install dir when present); - declining writes a marker into ./.acli/ so we never ask again here. - """ - if not sys.stdin.isatty() or (Path.cwd() / ".acli").is_dir(): - return - try: - from dashscope.acli.cli.examples import _handle_example_command - from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale - except ImportError: - return - err_console.print( - "[cyan]First run:[/cyan] download the dashscope-sdk-expert " - "example config into ./.acli/\n" - "(SDK Q&A expert persona + skill templates + SDK knowledge " - "index; pure config, editable anytime).", - ) - err_console.print( - f"[dim]Guide: {_GUIDE_DOC.format(_doc_locale())}[/dim]", - ) - try: - answer = input("Download the example config? [Y/n] ").strip().lower() - except (EOFError, KeyboardInterrupt): - err_console.print() - return - if answer in ("n", "no"): - try: - marker = Path.cwd() / ".acli" / ".dashscope-example-declined" - marker.parent.mkdir(parents=True, exist_ok=True) - marker.write_text("declined\n", encoding="utf-8") - except OSError: - pass - err_console.print( - "[dim]Skipped. To customize later, run: " - "python -m dashscope.acli example download " - "dashscope-sdk-expert[/dim]", - ) - return - _handle_example_command(["download", _EXPERT_EXAMPLE]) - - -def _route_to_expert(command, tui=None): - """Run the vendored acli agent (bare ``dashscope`` or ``dashscope expert``). - - ``command`` is a one-shot prompt, or None for the interactive REPL. - """ - try: - from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale - from dashscope.acli.ui.embedded import run - except ImportError as exception: - err_console.print( - "[yellow]Tip:[/yellow] Install dashscope[acli] to enable " - "the agent " - f"({exception}).", - ) - return - - if command is None and not sys.stdin.isatty(): - # The interactive REPL needs a real terminal; prompt_toolkit crashes - # on non-tty stdin (e.g. kqueue rejects /dev/null with EINVAL). - # Treat piped text as a one-shot prompt instead. - try: - piped = sys.stdin.read().strip() - except OSError: - piped = "" - if not piped: - err_console.print( - "[red]Error:[/red] Interactive mode requires a terminal. " - 'Pass a prompt instead, e.g. dashscope expert "your question".', - ) - sys.exit(2) - command = piped - - _cleanup_legacy_expert_sync() - if command is None: - _maybe_offer_example_download() - - try: - run( - app_name="DashScope SDK Expert", - prompt_symbol="dashscope> ", - api_key=dashscope.api_key or None, - command=command, - tui=tui, - module="expert", - guide_url=_GUIDE_DOC.format(_doc_locale()), - ) - except SystemExit: - pass - except Exception as exception: # pylint: disable=broad-except - err_console.print( - f"[yellow]Tip:[/yellow] Agent failed to start ({exception}).", - ) - - -# --------------------------------------------------------------------------- -# Typer app -# --------------------------------------------------------------------------- - -app = typer.Typer( - name="dashscope", - help="DashScope command line tools.", - add_completion=True, - no_args_is_help=True, - rich_markup_mode="rich", -) - - -@app.callback() -def callback( - api_key: Optional[str] = typer.Option( - None, - "-k", - "--api-key", - help="DashScope API Key.", - ), -): - """Configure global CLI options.""" - if api_key: - dashscope.api_key = api_key - - -# Register sub-command groups -app.add_typer(auth.app) -app.add_typer(generation.app) -app.add_typer(fine_tunes.app, name="ft") -app.add_typer(fine_tunes.app, name="fine-tunes", hidden=True) -app.add_typer(files.app) -app.add_typer(deployments.app) -app.add_typer(oss.app) -app.add_typer(rerank.app) -app.add_typer(embeddings.app) -app.add_typer(tokenization.app) -app.add_typer(models.app) -app.add_typer(application.app) -app.add_typer(image_synthesis.app) -app.add_typer(video_synthesis.app) -app.add_typer(image_generation.app) -app.add_typer(multimodal_conversation.app) -app.add_typer(multimodal_embedding.app) -app.add_typer(transcription.app) -app.add_typer(speech_synthesis.app) - - -# Records why the Agentic-RL Typer app could not be registered, so the reason -# can be reported when `rl` is actually invoked instead of on every command. -_RL_IMPORT_ERROR: Optional[Exception] = None - -_RL_COMMAND_NAMES = ("rl", "agentic-rl") - - -def _register_rl_app(): - """Lazily import and register the Agentic-RL Typer app. - - Wrapped in a function so that a missing optional dependency - won't crash the entire CLI at import time. - - The failure is recorded rather than reported: `rl` is an optional - extra, so a user running any other command has no reason to be told - about it. ``main()`` surfaces it when the rl command is invoked. - """ - global _RL_IMPORT_ERROR # pylint: disable=global-statement - - try: - from dashscope.cli.agentic_rl import app as rl_app - - app.add_typer( - rl_app, - name="rl", - help="๐Ÿš€ Agentic RL fine-tuning commands", - ) - app.add_typer( - rl_app, - name="agentic-rl", - help="๐Ÿš€ Agentic RL fine-tuning commands", - hidden=True, - ) - except Exception as exception: # pylint: disable=broad-except - _RL_IMPORT_ERROR = exception - - -def _rl_unavailable_detail(exception: Exception) -> str: - """Render a deferred rl import failure as an actionable message.""" - # Unescaped, rich reads a bracketed run in the exception text as a style - # tag and drops it from the printed output. - detail = escape(str(exception)) - if "pip install" not in detail: - detail += ( - ". Reinstall to pull in the missing dependencies: " - "[bold]pip install -U dashscope[/bold]" - ) - return detail - - -_register_rl_app() - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - - -def _first_non_option(argv): - """Return the first non-option argument in *argv* (skip index 0), - or None if all arguments are options.""" - for arg in argv[1:]: - if not arg.startswith("-"): - return arg - return None - - -def main(): - """Entry point for the ``dashscope`` console script.""" - # Translate legacy command format first so legacy --api_key can be treated - # as the global --api-key option. - argv = _translate_legacy_args(sys.argv) - - # Extract global api-key parameter after legacy argument normalization. - argv = _extract_global_api_key(argv) - - # Extract agent mode flags (--cli / --tui); they belong to the vendored - # acli and must not leak into typer or the one-shot prompt text. - # Bare `dashscope` defaults to the TUI; --cli opts into the plain CLI REPL. - forced_tui = True - if "--cli" in argv: - forced_tui = False - argv = [a for a in argv if a != "--cli"] - if "--tui" in argv: - forced_tui = True - argv = [a for a in argv if a != "--tui"] - - first_cmd = _first_non_option(argv) - - # `rl` is an optional extra: report a missing dependency only when the - # user asks for that command, never on unrelated invocations. Without - # this gate the dispatch below reaches typer, which only knows that `rl` - # is in _TOP_LEVEL_COMMANDS but was never registered โ€” "No such command" - # does not tell the user how to get it. - if _RL_IMPORT_ERROR is not None and first_cmd in _RL_COMMAND_NAMES: - err_console.print( - f"[red]Error:[/red] {_rl_unavailable_detail(_RL_IMPORT_ERROR)}", - ) - sys.exit(1) - - # No arguments at all โ†’ the agent, interactively. -k/--api-key and - # --cli/--tui were stripped above, so `dashscope -k sk-x` lands here too. - if len(argv) == 1: - _route_to_expert(None, tui=forced_tui) - return - - # `dashscope expert ...` โ†’ the agent, explicitly. Handled before the - # --help gate so `dashscope expert --help` describes the agent instead of - # reaching typer, which has no such command. - if first_cmd == _EXPERT_COMMAND: - idx = argv.index(_EXPERT_COMMAND) - expert_args = argv[idx + 1 :] - if "--help" in expert_args or "-h" in expert_args: - console.print(_EXPERT_USAGE) - return - prompt = " ".join(expert_args).strip() - _route_to_expert(prompt or None, tui=forced_tui) - return - - # Top-level --help / -h โ†’ show help and exit normally - if "--help" in argv or "-h" in argv: - argv = _translate_help_shortcut(argv) - sys.argv = argv - app() - return - - # Anything that is not bare `dashscope` and not `dashscope expert ...` - # belongs to typer, so a typo or a stray option gets a real error instead - # of silently opening a chat. Forwarding an unrecognized token as a - # question used to swallow whole command lines -- `dashscope auth whoami` - # opened a session whose first message was the command itself. - argv = _translate_help_shortcut(argv) - sys.argv = argv - - try: - app() - except AuthenticationError as exception: - err_console.print(f"[red]Error:[/red] {exception}") - sys.exit(1) +_install(__name__, "dashscope.legacy_1x.cli") diff --git a/dashscope/common/__init__.py b/dashscope/common/__init__.py index e69de29b..105fba9c 100644 --- a/dashscope/common/__init__.py +++ b/dashscope/common/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.common``. + +Import from ``dashscope.legacy_1x.common`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.common") diff --git a/dashscope/embeddings/__init__.py b/dashscope/embeddings/__init__.py index 998f1a1f..e421da08 100644 --- a/dashscope/embeddings/__init__.py +++ b/dashscope/embeddings/__init__.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.embeddings``. -from .batch_text_embedding import BatchTextEmbedding -from .batch_text_embedding_response import BatchTextEmbeddingResponse -from .text_embedding import TextEmbedding +Import from ``dashscope.legacy_1x.embeddings`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = [ - "TextEmbedding", - "BatchTextEmbedding", - "BatchTextEmbeddingResponse", -] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.embeddings") diff --git a/dashscope/files.py b/dashscope/files.py index b62d3da8..55ede462 100644 --- a/dashscope/files.py +++ b/dashscope/files.py @@ -1,136 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.files``. -import os -from typing import Optional +Import from ``dashscope.legacy_1x.files`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import ( - DeleteMixin, - FileUploadMixin, - GetMixin, - ListMixin, -) -from dashscope.common.constants import FilePurpose -from dashscope.common.error import InvalidFileFormat -from dashscope.common.utils import is_validate_fine_tune_file +from dashscope.legacy_1x._shim import install as _install - -class Files(FileUploadMixin, ListMixin, DeleteMixin, GetMixin): - SUB_PATH = "files" - - @classmethod - def upload( # pylint: disable=arguments-renamed - cls, - file_path: str, # type: ignore[override] - purpose: str = FilePurpose.fine_tune, # type: ignore[override] - description: Optional[str] = None, # type: ignore[override] - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Upload file for model fine-tune or other tasks. - - Args: - file_path (str): The local file name to upload. - purpose (str): The purpose of the file[fine-tune|inference] - description (str, optional): The file description message. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The upload information - """ - if purpose == FilePurpose.fine_tune: - if not is_validate_fine_tune_file(file_path): - raise InvalidFileFormat( - f"The file {file_path} is not in valid jsonl format", - ) - with open(file_path, "rb") as f: - return super().upload( # type: ignore[return-value] - files=[ - ( - "files", - ( - os.path.basename(f.name), - f, - None, - ), - ), - ], - descriptions=[description] if description is not None else [], - api_key=api_key, - workspace=workspace, - **kwargs, - ) - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List uploaded files. - - Args: - api_key (str, optional): - The api api_key, can be None, - if None, will get by default rule(TODO: api key doc). - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The fine-tune jobs in the result. - """ - return super().list( # type: ignore[return-value] - page, - page_size, - api_key, - workspace=workspace, - **kwargs, - ) - - @classmethod - def get( # type: ignore[override] - cls, - file_id: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the file info. - - Args: - file_id (str): The file id. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The job info - """ - # type: ignore - return super().get(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 - - @classmethod - def delete( # type: ignore[override] - cls, - file_id: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Delete uploaded file. - - Args: - file_id (str): The file id want to delete. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: Delete result. - """ # type: ignore - return super().delete(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 +_install(__name__, "dashscope.legacy_1x.files") diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 6eb707a2..1220656b 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -1,13 +1,25 @@ # -*- coding: utf-8 -*- -from __future__ import annotations - # Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim plus the 1.x surface; the implementation moved to +``dashscope.v2.finetune.agentic_rl``. + +The agentic-RL workflow is owned by the 2.0 SDK, but this import path has +to keep resolving: the ``dashscope`` CLI, the samples under +``reinforcement/examples/workspace`` and the acli skill docs all name +``dashscope.finetune.agentic_rl.AgenticRL``. + +What stays here is the half of the surface that belongs to 1.x -- the +five job-management verbs, which delegate to ``dashscope.FineTunes`` (a +1.x root public API, alongside ``Deployments``), and the ``FineTune`` +dataclasses they return. ``submit_job`` therefore returns a ``FineTune`` +from this path and a ``SubmitResult`` from the 2.0 one; the payload and +the POST are built once, in the 2.0 class. -import time -from typing import Union, List, Optional, ClassVar, Dict, Any -from typing_extensions import Self +Import from ``dashscope.v2.finetune.agentic_rl`` in new code. +""" + +from typing import Dict, List, Optional, Union -from dashscope.client.base_api import CreateMixin from dashscope.finetune.customize_types import ( FineTune, FineTuneCancel, @@ -15,143 +27,70 @@ FineTuneList, ) from dashscope.finetune.finetunes import FineTunes -from dashscope.finetune.reinforcement import ( +from dashscope.v2.finetune.agentic_rl import AgenticRL as _AgenticRLBase +from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult + +# Re-exported so every name this module historically carried stays +# importable from here. Both spellings resolve to the same objects: the +# reverse-reference shim under ``dashscope/finetune/reinforcement`` +# aliases each submodule to the 2.0 one. +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement import ( # noqa: F401 + DASHSCOPE_HTTP_BASE_URL, AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, + AgenticRLTuning, Dataset, - TrainingDataset, - ValidationDataset, -) -from dashscope.finetune.reinforcement import AgenticRLTuning, TuningModel -from dashscope.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL -from dashscope.finetune.reinforcement import ( - FunctionType, DatasetsType, - TrainingType, -) -from dashscope.finetune.reinforcement import ( + FunctionType, + GroupRewardInput, + RewardFunctionComponent, RewardInput, + RolloutFunctionComponent, RolloutInput, - GroupRewardInput, -) -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement import ( - set_api_key, + TrainingDataset, + TrainingType, + TuningModel, + ValidationDataset, + deep_remove_none, generate_random_id, get_func_type_id, - deep_remove_none, + logger, + set_api_key, ) -from dashscope.finetune.reinforcement.common.errors import ( - RegistrationError, - ValidationError, + +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement.common.errors import ( # noqa: F401 + DatasetsError, InstanceQueryError, + RegistrationError, RuntimeErrorWithCode, + ValidationError, ValueErrorWithCode, - DatasetsError, ) - -class AgenticRL(AgenticRLTuning, CreateMixin): - SUB_PATH: ClassVar[str] = "fine-tunes" - - def __init__(self, api_key: str = None): - super().__init__() - - try: - set_api_key(api_key) - except Exception as e: - raise ValueErrorWithCode( - "Invalid API key configuration", - error_code=3001, - ) from e - - def init(self, config_path: Optional[str] = None, **kwargs) -> Self: - """ - Initialize an AgenticRL instance from a YAML configuration file. - """ - self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) - - return self - - async def register_functions( - self, - functions: Optional[ - Union[ - List[Union[RolloutFunctionComponent, RewardFunctionComponent]], - RolloutFunctionComponent, - RewardFunctionComponent, - ] - ] = None, - lazy_load: Optional[bool] = True, - ) -> tuple[ - List[str], - List[str], - List[str], - List[str], - List[str], - List[str], - ]: - """Register function components and return entity/instance IDs.""" - if functions: - self.tuning.functions = functions - - try: - ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) = await self.tuning.register_functions( - lazy_load=lazy_load, - ) - logger.info("Function components registered") - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RegistrationError( - "Function registration failed", - error_code=3002, - ) from e - - return ( - rollout_entity_ids, - reward_entity_ids, - group_reward_entity_ids, - rollout_instance_ids, - reward_instance_ids, - group_reward_instance_ids, - ) - - async def upload_datasets( - self, - datasets: Optional[List[Dataset]] = None, - training_files: Optional[Union[List[str], str]] = None, - validation_files: Optional[Union[List[str], str]] = None, - ) -> tuple[List[str], List[str]]: - if datasets: - self.tuning.datasets = datasets - - try: - ( - uploaded_training_ids, - uploaded_validation_ids, - ) = await self.tuning.upload_datasets( - training_files=training_files or [], - validation_files=validation_files or [], - ) - logger.info("Datasets uploaded") - except Exception as e: - raise DatasetsError( - "Datasets upload failed", - error_code=3003, - ) from e - - return uploaded_training_ids, uploaded_validation_ids - - def submit_job( +__all__ = [ + "AgenticRL", + "DASHSCOPE_HTTP_BASE_URL", + "FineTune", + "FineTuneCancel", + "FineTuneDelete", + "FineTuneList", + "FineTunes", + "SubmitOutput", + "SubmitResult", +] + + +class AgenticRL(_AgenticRLBase): + """The 1.x surface: ``FineTune`` returns plus the ``FineTunes`` verbs. + + The five verbs below are 1.x-only. ``Client(...).rl.jobs`` already + implements the same lifecycle natively on the 2.0 transport, so + duplicating them into the 2.0 class would have given the SDK two + copies of every job verb. + """ + + def submit_job( # type: ignore[override] self, model: Optional[str] = None, datasets: Optional[List[Dataset]] = None, @@ -178,168 +117,18 @@ def submit_job( """ Submit RL tuning job to the platform. """ - teacher_model = teacher_model or self.tuning.teacher_model - - # Resolve job name (fallback to class default) - if job_name: - self.tuning.name = job_name - job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" - - # Model name - if model: - self.tuning.model.name = model - - # rollouts/rewards - if functions: - self.tuning.functions = functions - rollouts = self.tuning.combine_ids_runtimes( - functype=FunctionType.ROLLOUT, - ) - rewards = self.tuning.combine_ids_runtimes( - functype=FunctionType.REWARD, - ) - rewards.extend( - self.tuning.combine_ids_runtimes( - functype=FunctionType.GROUP_REWARD, - id_str=get_func_type_id(FunctionType.REWARD), - ), - ) - # names of functions - if not self.tuning.check_function_names(): - raise ValueErrorWithCode( - "Duplicate function names detected. All function names must " - "be unique.", - error_code=3004, - ) - - # datasets - if datasets: - self.tuning.datasets = datasets - training_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.TRAINING - ] - validation_datasets = [ - ds - for ds in self.tuning.datasets - if ds.type == DatasetsType.VALIDATION - ] - - # hyper_parameters - if hyper_parameters: - self.tuning.training.hyper_parameters = hyper_parameters - - # resources - if resources: - self.tuning.training.resources = resources - - training_type = self.tuning.training.type - if teacher_model: - training_type = TrainingType.PG_OPD - elif training_type == TrainingType.PG_OPD: - raise ValueErrorWithCode( - f"teacher_model is required when training.type is {training_type}", - error_code=3006, - ) - - request = { - "model": self.tuning.model.name, - "training_datasets": [ds.model_dump() for ds in training_datasets], - "validation_datasets": [ - ds.model_dump() for ds in validation_datasets - ], - "rollout": rollouts[0] if rollouts else None, - "rewards": rewards, - "hyper_parameters": self.tuning.training.hyper_parameters, - "resource_config": self.tuning.training.resources, - "training_type": str(training_type), - "teacher_model": teacher_model, - "job_name": job_name_with_suffix, - } - request = deep_remove_none(request) - logger.info(f"agentic_rl submit_job request: {request}") - - kwargs["base_address"] = DASHSCOPE_HTTP_BASE_URL - try: - resp = super().call( - request, - **kwargs, - ) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode( - "Job submission failed", - error_code=3005, - ) from e - - return FineTune(**resp) - - async def run( - self, - model: Optional[str] = None, - # Datasets parameters - training_datasets: Optional[List[TrainingDataset]] = None, - validation_datasets: Optional[List[ValidationDataset]] = None, - # Path-driven parameters (auto-register & upload) - functions: Optional[ - Union[ - List[ - Union[ - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ], - RolloutFunctionComponent, - RewardFunctionComponent, - AgenticRLFunctionComponent, - ] - ] = None, - # Common parameters - hyper_parameters: Optional[Dict[str, str]] = None, - resources: Optional[Dict[str, str]] = None, - job_name: Optional[str] = None, - teacher_model: Optional[str] = None, - **kwargs, - ) -> FineTune: - """ - Execute RL tuning workflow. - """ - try: - logger.info( - "Path-Driven mode: Registering functions & uploading " - "datasets...", - ) - await self.register_functions( - functions=functions, - lazy_load=True, - ) - - datasets = list(training_datasets or []) + list( - validation_datasets or [], - ) - await self.upload_datasets( - datasets=datasets, - ) - - return self.submit_job( + return FineTune( + **self._submit_envelope( model=model, datasets=datasets, + functions=functions, hyper_parameters=hyper_parameters, resources=resources, job_name=job_name, teacher_model=teacher_model, **kwargs, - ) - except Exception as e: - if hasattr(e, "error_code"): - raise - raise RuntimeErrorWithCode( - "RL tuning workflow failed", - error_code=3006, - ) from e + ), + ) @classmethod def cancel( @@ -436,250 +225,3 @@ def logs( workspace=workspace, **kwargs, ) - - @classmethod - async def test_functions( - cls, - instance_id: str, - functype: FunctionType, - input_data: Dict[str, Any], - api_key: str = None, - pull_logs: bool = False, - log_page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ): - """Test a deployed function instance with custom input data. - - Args: - instance_id: Target function instance ID. - functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). - input_data: Test input payload. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - pull_logs: If True, pull all logs of the function instance - (with pagination) after verification and print them - between separator markers. - log_page_size: Page size used when pulling logs. - start_time: Optional start time filter (in seconds) for log - pulling. Defaults to 24 hours before ``end_time``. - end_time: Optional end time filter (in seconds) for log - pulling. Defaults to the current time (now). - """ - try: - set_api_key(api_key) - - if functype == FunctionType.ROLLOUT: - value = RolloutInput.model_validate(input_data) - elif functype == FunctionType.REWARD: - value = RewardInput.model_validate(input_data) - elif functype == FunctionType.GROUP_REWARD: - value = GroupRewardInput.model_validate(input_data) - else: - raise ValueErrorWithCode( - f"Unsupported function type: {functype}", - error_code=3007, - ) - - logger.info( - f"Starting {str(functype)} verification", - extra={ - "instance_id": instance_id, - "input_params": value.model_dump(exclude={"api_key"}), - }, - ) - - result = await AgenticRLFunctionComponent.verify_function( - value, - instance_id, - ) - - if pull_logs: - await cls._pull_function_instance_logs( - function_instance_id=instance_id, - page_size=log_page_size, - start_time=start_time, - end_time=end_time, - ) - - return result - - except Exception as e: - raise ValidationError( - "Function test failed", - error_code=3008, - ) from e - - @classmethod - async def query_function_instance_logs( - cls, - function_instance_id: str, - page_number: int = 1, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> Dict[str, Any]: - """Query one page of logs for a function (faas) instance. - - Args: - function_instance_id: Target function instance ID. - page_number: Page number, starting from 1. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the log query API. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_function_instance_logs( - function_instance_id=function_instance_id, - page_number=page_number, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3009, - ) from e - - @classmethod - async def query_all_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - keywords: Optional[List[str]] = None, - api_key: str = None, - ) -> List[str]: - """Fetch all logs of a function (faas) instance. - - Pages are fetched one by one via the paginated log query until - all entries are collected. - - Args: - function_instance_id: Target function instance ID. - page_size: Number of log entries per page. - start_time: Optional start time filter (in seconds). - end_time: Optional end time filter (in seconds). - keywords: Optional keyword filters for log messages. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Aggregated list of log messages across all pages. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - keywords=keywords, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance log query failed", - error_code=3010, - ) from e - - @classmethod - async def delete_function_instance( - cls, - function_instance_id: str, - api_key: str = None, - ) -> Dict[str, Any]: - """Delete a function (faas) runtime instance. - - Args: - function_instance_id: Target function instance ID. - api_key: DashScope API key (uses DASHSCOPE_API_KEY env var - if omitted). - - Returns: - Raw response dict of the delete API, containing - ``data.status`` (e.g. "deleted") and - ``data.sandbox_code``. - - Raises: - InstanceQueryError: If the deletion fails. - """ - try: - set_api_key(api_key) - - fc_component = AgenticRLFunctionComponent - return await fc_component.delete_function_instance( - function_instance_id=function_instance_id, - ) - - except Exception as e: - if hasattr(e, "error_code"): - raise - raise InstanceQueryError( - "Function instance deletion failed", - error_code=3011, - ) from e - - @classmethod - async def _pull_function_instance_logs( - cls, - function_instance_id: str, - page_size: int = 100, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - ) -> None: - """Pull all logs of a function instance and print them between - separator markers (best effort, never raises). - - When ``start_time``/``end_time`` are not provided, defaults to - pulling logs from 24 hours ago until now. - """ - if end_time is None: - end_time = int(time.time()) - if start_time is None: - start_time = end_time - 24 * 60 * 60 - logger.info( - "************start query log " - f"(function_instance_id={function_instance_id})*******", - ) - try: - logs = await cls.query_all_function_instance_logs( - function_instance_id=function_instance_id, - page_size=page_size, - start_time=start_time, - end_time=end_time, - ) - for entry in logs: - logger.info(f"[function instance log] {entry}") - logger.info( - f"Pulled {len(logs)} log entries for function instance " - f"{function_instance_id}", - ) - except Exception as e: - logger.warning( - f"Failed to pull logs for function instance " - f"{function_instance_id}: {e}", - ) - finally: - logger.info("************end query log *******") diff --git a/dashscope/finetune/customize_types.py b/dashscope/finetune/customize_types.py index 1fa101a8..53fa4de7 100644 --- a/dashscope/finetune/customize_types.py +++ b/dashscope/finetune/customize_types.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import Dict, List, Optional -from dashscope.common.base_type import BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseObjectMixin __all__ = ["Deployment", "FineTune", "DeploymentList", "FineTuneList"] diff --git a/dashscope/finetune/deployments.py b/dashscope/finetune/deployments.py index 4bfb916b..0a06ed46 100644 --- a/dashscope/finetune/deployments.py +++ b/dashscope/finetune/deployments.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from typing import Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetMixin, diff --git a/dashscope/finetune/finetunes.py b/dashscope/finetune/finetunes.py index 8f061153..83745783 100644 --- a/dashscope/finetune/finetunes.py +++ b/dashscope/finetune/finetunes.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import Iterator, Optional, Union -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, DeleteMixin, @@ -14,7 +14,7 @@ LogMixin, StreamEventMixin, ) -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from dashscope.finetune.customize_types import ( FineTune, FineTuneCancel, diff --git a/dashscope/finetune/reinforcement/__init__.py b/dashscope/finetune/reinforcement/__init__.py index bd4d16c5..ed6d74fe 100644 --- a/dashscope/finetune/reinforcement/__init__.py +++ b/dashscope/finetune/reinforcement/__init__.py @@ -1,267 +1,164 @@ # -*- coding: utf-8 -*- -try: - import pydantic # noqa: F401 - import tenacity # noqa: F401 - import yaml # noqa: F401 -except ImportError as _rl_dep_err: - raise ImportError( - "Agentic RL fine-tuning dependencies are missing from this " - "install. Reinstall with: pip install -U dashscope", - ) from _rl_dep_err +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility shim; the implementation moved to +``dashscope.v2.finetune.reinforcement``. -from dashscope.finetune.reinforcement.common.constants import ( - BAILIAN_FILE_API, - BAILIAN_FILE_TIMEOUT, - DASHSCOPE_API_KEY, - DASHSCOPE_HTTP_BASE_URL, - LOG_LEVEL, - FC_API_KEY, - FC_DELETE_API, - FC_FILES_START, - FC_LOAD_API, - FC_PYPI_LIB, - FC_PYPI_REPO, - FC_QUERY_API, - FC_QUERY_LOG_API, - FC_REGISTER_REWARD_API, - FC_REGISTER_ROLLOUT_API, - FC_REGISTER_GROUP_REWARD_API, - FC_SERVER_CLASSPATH, - FC_UPLOAD_OSS_API, - FC_WORKERS_COUNT, - FC_LAYER_CREATE_API, - FC_LAYER_QUERY_API, - FC_LAYER_USED, - FC_LAYER_NAME, - FC_REQUIREMENTS_FILE, - FC_ZIP_EXCLUDE_PATTERNS, - FC_OSS_FILE_SIZE_WARNING, - DATASETS_FILE_SIZE_WARNING, - HTTP_REQUEST_TIMEOUT, - LOGGER_FILTER_FIELDS, -) -from dashscope.finetune.reinforcement.common.errors import ( - AgenticRLError, - InputError, - OutputError, - BaseConnectionError, - OSSConnectionError, - OSSUploadError, - DeploymentError, - RegistrationError, - FunctionLoadError, - InstanceWarmupError, - InstanceQueryError, - FunctionLayerError, - ValidationError, - ConfigurationError, - BasePermissionError, - IOErrorWithCode, - RuntimeErrorWithCode, - ValueErrorWithCode, -) -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model import ( - FunctionComponentModel, - TuningModel, - FoundationModel, - FunctionComponentRuntime, - AgenticRLFunctionComponent, - RolloutFunctionComponent, - RewardFunctionComponent, - GroupRewardFunctionComponent, - Datasets, - Dataset, - TrainingDataset, - ValidationDataset, - MountStorage, - Training, - Observability, - AgenticRLTuning, - Models, -) -from dashscope.finetune.reinforcement.common.model_types import ( - FileSpec, - FunctionType, - DatasetsType, - DataSourceType, - RequestFC, - ResponseFC, - Status, - StatusType, - TrainingType, -) -from dashscope.finetune.reinforcement.common.utils import ( - check_file, - client_fc, - create_deployment_files, - deep_mask, - generate_random_id, - get_filepath_classname, - upload_zip_to_oss_and_by_signed_url, - set_api_key, - to_bailian_data, - zip_dir, - get_func_type_id, - deep_remove_none, - extract_reward_weights, - get_weights_from_file, - serialize_for_output, -) -from dashscope.finetune.reinforcement.component import ( - reward_func, - sub_reward_func, - aggregate_func, -) -from dashscope.finetune.reinforcement.component.data import ( - AgentOutput, - Reward, - RewardInput, - RewardOutput, - RolloutInput, - RolloutOutput, - GroupReward, - GroupRewardInput, - GroupRewardOutput, - TaskStatus, -) -from dashscope.finetune.reinforcement.component.func_decorator import ( - RewardProcessorMeta, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractRolloutProcessor, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor import ( - AbstractGroupRewardProcessor, +The agentic-RL workflow package is owned by the 2.0 SDK. This path has to +keep resolving indefinitely, because two of its consumers resolve the +module *by name* in a process this repo does not control: + +- ``FC_SERVER_CLASSPATH`` is written into the ``start.sh`` that gets + deployed to a Function Compute sandbox, where the installed dashscope + may be any published version. +- ``examples/workspace/scripts/start_local_*.sh`` launches the reward / + rollout server with ``python3 -m + dashscope.finetune.reinforcement.component.server.server``, and + ``server.py`` hands the same dotted string to ``uvicorn.run``, which + imports it inside its worker process. + +Unlike the AgentStudio shim, aliasing here cannot be eager. Nine of the +package's modules -- ``component.server.*`` and +``component.observability.genai.*`` -- are imported lazily and need +fastapi / opentelemetry, both optional. Walking the tree up front would +either miss them or promote two optional dependencies to import-time +hard ones, so a ``sys.meta_path`` finder aliases each submodule on +demand and hands back the *same module object* the v2 path uses. + +Import from ``dashscope.v2.finetune.reinforcement`` in new code. +""" + +import importlib +import importlib.util +import sys + +# Importing the package runs its own dependency guard, so a missing +# pydantic / tenacity / pyyaml still raises from this path. +from dashscope.v2.finetune import reinforcement as _impl + +# pylint: disable-next=wildcard-import,unused-wildcard-import +from dashscope.v2.finetune.reinforcement import * # noqa: F401,F403 + +# The wildcard only carries the names in the implementation's __all__, +# which lists classes and helpers but not its subpackages. Binding them +# here keeps the deep imports the samples use statically resolvable for +# pylint and IDEs, to whom the meta_path finder below is invisible. At +# runtime both are already imported by the package's own eager imports. +# pylint: disable-next=unused-import +from dashscope.v2.finetune.reinforcement import ( # noqa: F401 + common, + component, ) +_OLD = __name__ +_NEW = _impl.__name__ + +# ``app`` is the 1.x CLI's typer app and is reachable from this path +# only; the 2.0 package must not depend on the CLI. It stays lazy +# because resolving it pulls in typer and the rest of the CLI. +# pylint: disable-next=undefined-all-variable +__all__ = list(_impl.__all__) + ["app"] # noqa: F405 + def __getattr__(name): if name == "app": - from dashscope.cli.agentic_rl import app + from dashscope.legacy_1x.cli.agentic_rl import app return app - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + raise AttributeError(f"module {_OLD!r} has no attribute {name!r}") + + +def _restore_identity(module, v2_spec): + """Put the v2 identity back on a module object both paths share. + + ``importlib._bootstrap._init_module_attrs`` stamps the spec it was + handed onto whatever ``create_module`` returned, and that spec names + the legacy path. The object is the v2 module, so it must keep + reporting the v2 name: pickling, ``importlib.reload`` and anything + comparing ``module.__spec__.name`` against the ``sys.modules`` key + all read it. + """ + module.__name__ = v2_spec.name + module.__spec__ = v2_spec + module.__loader__ = v2_spec.loader + module.__package__ = v2_spec.parent + module.__file__ = v2_spec.origin + if v2_spec.submodule_search_locations is not None: + module.__path__ = v2_spec.submodule_search_locations + + +class _AliasLoader: + """Loader that hands back the already-imported v2 module. + + Not an ``importlib.abc.Loader`` subclass on purpose: the import + system resolves loaders by duck typing, and the ABC still declares + ``module_repr`` abstract on this interpreter even though 3.12 + removed it. + """ + + def __init__(self, v2_spec): + self._v2_spec = v2_spec + + def create_module(self, spec): + module = sys.modules.get(self._v2_spec.name) + if module is None: + module = importlib.import_module(self._v2_spec.name) + # Binding the attribute on the parent is what makes + # ``dashscope.finetune.reinforcement.common.utils`` work as an + # expression and not just as an import target: the import system + # skips a module it already finds in sys.modules and never sets + # the attribute itself. + parent_name, _, leaf = spec.name.rpartition(".") + parent = sys.modules.get(parent_name) + if parent is not None: + setattr(parent, leaf, module) + return module + + def exec_module(self, module): + # Deliberately does not re-run the module body. It already ran + # under the v2 name, and a second pass would build a duplicate + # FastAPI app, OpenTelemetry tracer and log handler. + _restore_identity(module, self._v2_spec) + + def get_code(self, fullname): # pylint: disable=unused-argument + # ``runpy`` -- and therefore ``python -m`` -- refuses a loader + # that yields no code object, and uvicorn resolves its import + # string through the same path. + return self._v2_spec.loader.get_code(self._v2_spec.name) + + +class _Finder: + """Resolve ``dashscope.finetune.reinforcement.*`` to the v2 modules. + + Duck-typed for the same reason as ``_AliasLoader``: ``sys.meta_path`` + entries only need a ``find_spec``. + """ + + def find_spec( # pylint: disable=unused-argument + self, + fullname, + path=None, + target=None, + ): + if not fullname.startswith(_OLD + "."): + return None + v2_name = _NEW + fullname[len(_OLD) :] + # find_spec resolves without importing the leaf, so a module that + # needs fastapi or opentelemetry stays unimported until someone + # actually asks for it. + v2_spec = importlib.util.find_spec(v2_name) + if v2_spec is None or v2_spec.loader is None: + return None + return importlib.util.spec_from_file_location( + fullname, + v2_spec.origin, + loader=_AliasLoader(v2_spec), + submodule_search_locations=v2_spec.submodule_search_locations, + ) -# pylint: disable=undefined-all-variable -__all__ = [ - # Constants - "BAILIAN_FILE_API", - "BAILIAN_FILE_TIMEOUT", - "DASHSCOPE_API_KEY", - "DASHSCOPE_HTTP_BASE_URL", - "LOG_LEVEL", - "FC_API_KEY", - "FC_DELETE_API", - "FC_FILES_START", - "FC_LOAD_API", - "FC_PYPI_LIB", - "FC_PYPI_REPO", - "FC_QUERY_API", - "FC_QUERY_LOG_API", - "FC_REGISTER_REWARD_API", - "FC_REGISTER_ROLLOUT_API", - "FC_REGISTER_GROUP_REWARD_API", - "FC_SERVER_CLASSPATH", - "FC_UPLOAD_OSS_API", - "FC_WORKERS_COUNT", - "FC_LAYER_CREATE_API", - "FC_LAYER_QUERY_API", - "FC_LAYER_USED", - "FC_LAYER_NAME", - "FC_REQUIREMENTS_FILE", - "FC_ZIP_EXCLUDE_PATTERNS", - "FC_OSS_FILE_SIZE_WARNING", - "DATASETS_FILE_SIZE_WARNING", - "HTTP_REQUEST_TIMEOUT", - "LOGGER_FILTER_FIELDS", - # Errors - "AgenticRLError", - "InputError", - "OutputError", - "BaseConnectionError", - "OSSConnectionError", - "OSSUploadError", - "DeploymentError", - "RegistrationError", - "FunctionLoadError", - "FunctionLayerError", - "InstanceWarmupError", - "InstanceQueryError", - "ValidationError", - "ConfigurationError", - "BasePermissionError", - "IOErrorWithCode", - "RuntimeErrorWithCode", - "ValueErrorWithCode", - # Logging - "logger", - # Utilities - "check_file", - "client_fc", - "create_deployment_files", - "deep_mask", - "generate_random_id", - "get_filepath_classname", - "set_api_key", - "to_bailian_data", - "upload_zip_to_oss_and_by_signed_url", - "zip_dir", - "get_func_type_id", - "deep_remove_none", - "extract_reward_weights", - "get_weights_from_file", - "serialize_for_output", - # Model Types - "FileSpec", - "FunctionType", - "DatasetsType", - "DataSourceType", - "RequestFC", - "ResponseFC", - "Status", - "StatusType", - "TrainingType", - # Data models - "RolloutInput", - "RolloutOutput", - "RewardInput", - "RewardOutput", - "GroupRewardInput", - "GroupRewardOutput", - "GroupReward", - "Reward", - "AgentOutput", - "TaskStatus", - # Func Manager - "reward_func", - "sub_reward_func", - "aggregate_func", - "RewardProcessorMeta", - "AbstractRolloutProcessor", - "AbstractRewardProcessor", - "AbstractGroupRewardProcessor", - # Core Models - "FunctionComponentModel", - "TuningModel", - "FoundationModel", - "FunctionComponentRuntime", - "AgenticRLFunctionComponent", - "RolloutFunctionComponent", - "RewardFunctionComponent", - "GroupRewardFunctionComponent", - "Datasets", - "Dataset", - "TrainingDataset", - "ValidationDataset", - "MountStorage", - "Training", - "Observability", - "AgenticRLTuning", - "Models", - "app", -] +# Inserted ahead of PathFinder rather than appended: the alias inherits +# the v2 package's __path__, so PathFinder would find the real file +# there and execute it a *second* time under the legacy name, yielding +# two distinct class objects for the same source. +if not any(isinstance(entry, _Finder) for entry in sys.meta_path): + sys.meta_path.insert(0, _Finder()) diff --git a/dashscope/finetune/reinforcement/component/demo/__init__.py b/dashscope/finetune/reinforcement/component/demo/__init__.py deleted file mode 100644 index 6100812f..00000000 --- a/dashscope/finetune/reinforcement/component/demo/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRewardProcessor, -) -from dashscope.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long - DemoRolloutProcessor, -) - -__all__ = [ - "DemoRewardProcessor", - # Inherits AbstractRewardProcessor, returns RewardOutput - "DemoRolloutProcessor", - # Inherits AbstractRolloutProcessor, returns RolloutOutput - "DemoGroupRewardProcessor", - # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput -] diff --git a/dashscope/finetune/reinforcement/component/parser/__init__.py b/dashscope/finetune/reinforcement/component/parser/__init__.py deleted file mode 100644 index 6f5dedd0..00000000 --- a/dashscope/finetune/reinforcement/component/parser/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.parser.base_parser import ( - BaseRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 - GroupRewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( - RewardRequestParser, -) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( - RolloutRequestParser, -) - -__all__ = [ - "BaseRequestParser", - "RewardRequestParser", - "RolloutRequestParser", - "GroupRewardRequestParser", -] diff --git a/dashscope/finetune/reinforcement/component/processor/__init__.py b/dashscope/finetune/reinforcement/component/processor/__init__.py deleted file mode 100644 index 9246fa56..00000000 --- a/dashscope/finetune/reinforcement/component/processor/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractGroupRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRewardProcessor, -) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long - AbstractRolloutProcessor, -) - -__all__ = [ - "AbstractProcessor", - "AbstractRewardProcessor", - "AbstractRolloutProcessor", - "AbstractGroupRewardProcessor", -] diff --git a/dashscope/io/__init__.py b/dashscope/io/__init__.py index e69de29b..75c5b903 100644 --- a/dashscope/io/__init__.py +++ b/dashscope/io/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.io``. + +Import from ``dashscope.legacy_1x.io`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.io") diff --git a/dashscope/legacy_1x/__init__.py b/dashscope/legacy_1x/__init__.py new file mode 100644 index 00000000..64709f71 --- /dev/null +++ b/dashscope/legacy_1x/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The dashscope 1.x SDK. + +Everything under this package is the legacy 1.x business surface. It is +kept importable both at its canonical ``dashscope.legacy_1x.*`` path and, +for backward compatibility, at the historical top-level paths (for +example ``dashscope.aigc`` -> ``dashscope.legacy_1x.aigc``) via thin +alias shims installed at the package root. + +New code should target the 2.0 SDK, which lives at the top level of +``dashscope`` (``dashscope.Client``, ``dashscope.chat``, ...). +""" diff --git a/dashscope/legacy_1x/_shim.py b/dashscope/legacy_1x/_shim.py new file mode 100644 index 00000000..2ce6ec93 --- /dev/null +++ b/dashscope/legacy_1x/_shim.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat aliasing for the historical top-level 1.x import paths. + +The 1.x business surface now lives under ``dashscope.legacy_1x``. The thin +shim packages left at the old top-level names (``dashscope/aigc/__init__.py`` +and friends) call :func:`install` so that ``import dashscope.aigc`` and +``from dashscope.aigc.generation import Generation`` keep resolving. + +The shim re-exports the implementation's public names and points its +``__path__`` at the implementation package, so a submodule import loads the +real file. Submodules reached through the old spelling are therefore distinct +module objects from their ``dashscope.legacy_1x.*`` twins; nothing inside this +repository relies on that (all internal code, tests and examples use the +canonical ``dashscope.legacy_1x`` path, and ``import dashscope`` re-exports the +public names directly), so the aliasing is best-effort back-compat for external +callers only. It is deliberately lazy -- it never imports submodules itself -- +so importing a shim cannot trigger import-time side effects in the 1.x tree +(the ``dashscope.legacy_1x.cli`` package has some). +""" + +from __future__ import annotations + +import importlib +import sys + + +def install(shim_name: str, impl_name: str): + """Alias the package/module ``shim_name`` onto ``impl_name``. + + Returns the implementation module so a shim can also do + ``_impl = install(__name__, "dashscope.legacy_1x.aigc")`` when it wants + to reference it. + """ + impl = importlib.import_module(impl_name) + shim = sys.modules[shim_name] + + # Some 1.x packages declare a malformed ``__all__`` (e.g. ``audio`` + # lists module objects rather than names), so union the string entries + # of ``__all__`` with every public attribute instead of trusting it. + declared = getattr(impl, "__all__", None) or [] + names = {name for name in declared if isinstance(name, str)} + names.update(name for name in dir(impl) if not name.startswith("_")) + for name in names: + try: + setattr(shim, name, getattr(impl, name)) + except AttributeError: # pragma: no cover - name vanished mid-walk + continue + + if hasattr(impl, "__path__"): + # Let ``import dashscope..`` find the implementation's + # files without eagerly importing them here. + shim.__path__ = impl.__path__ + + return impl diff --git a/dashscope/legacy_1x/aigc/__init__.py b/dashscope/legacy_1x/aigc/__init__.py new file mode 100644 index 00000000..6f1e32c8 --- /dev/null +++ b/dashscope/legacy_1x/aigc/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +from .conversation import Conversation, History, HistoryItem +from .generation import Generation, AioGeneration +from .image_synthesis import ImageSynthesis, AioImageSynthesis +from .multimodal_conversation import ( + MultiModalConversation, + AioMultiModalConversation, +) +from .video_synthesis import VideoSynthesis, AioVideoSynthesis + +__all__ = [ + "Generation", + "AioGeneration", + "Conversation", + "HistoryItem", + "History", + "ImageSynthesis", + "AioImageSynthesis", + "MultiModalConversation", + "AioMultiModalConversation", + "VideoSynthesis", + "AioVideoSynthesis", +] diff --git a/dashscope/aigc/chat_completion.py b/dashscope/legacy_1x/aigc/chat_completion.py similarity index 97% rename from dashscope/aigc/chat_completion.py rename to dashscope/legacy_1x/aigc/chat_completion.py index 2ec2c512..99957e65 100644 --- a/dashscope/aigc/chat_completion.py +++ b/dashscope/legacy_1x/aigc/chat_completion.py @@ -5,18 +5,18 @@ from typing import Any, Dict, Generator, List, Union import dashscope -from dashscope.aigc.generation import Generation -from dashscope.api_entities.chat_completion_types import ( +from dashscope.legacy_1x.aigc.generation import Generation +from dashscope.legacy_1x.api_entities.chat_completion_types import ( ChatCompletion, ChatCompletionChunk, ) -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( GenerationResponse, Message, ) -from dashscope.client.base_api import BaseAioApi, CreateMixin -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseAioApi, CreateMixin +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Completions(CreateMixin): diff --git a/dashscope/aigc/code_generation.py b/dashscope/legacy_1x/aigc/code_generation.py similarity index 97% rename from dashscope/aigc/code_generation.py rename to dashscope/legacy_1x/aigc/code_generation.py index 4dbf422e..281c72b6 100644 --- a/dashscope/aigc/code_generation.py +++ b/dashscope/legacy_1x/aigc/code_generation.py @@ -3,15 +3,15 @@ from typing import Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import MESSAGE, SCENE -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import MESSAGE, SCENE +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class MessageParam(DictMixin): diff --git a/dashscope/aigc/conversation.py b/dashscope/legacy_1x/aigc/conversation.py similarity index 97% rename from dashscope/aigc/conversation.py rename to dashscope/legacy_1x/aigc/conversation.py index 65a2f856..1f937f3f 100644 --- a/dashscope/aigc/conversation.py +++ b/dashscope/legacy_1x/aigc/conversation.py @@ -6,16 +6,24 @@ from http import HTTPStatus from typing import Any, Dict, Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( ConversationResponse, Message, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import DEPRECATED_MESSAGE, HISTORY, PROMPT -from dashscope.common.error import InputRequired, InvalidInput, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ( + DEPRECATED_MESSAGE, + HISTORY, + PROMPT, +) +from dashscope.legacy_1x.common.error import ( + InputRequired, + InvalidInput, + ModelRequired, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class HistoryItem(dict): diff --git a/dashscope/aigc/generation.py b/dashscope/legacy_1x/aigc/generation.py similarity index 97% rename from dashscope/aigc/generation.py rename to dashscope/legacy_1x/aigc/generation.py index 0c6f0341..0f80f85c 100644 --- a/dashscope/aigc/generation.py +++ b/dashscope/legacy_1x/aigc/generation.py @@ -5,24 +5,24 @@ import json from typing import Any, Dict, Generator, List, Union, AsyncGenerator -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( GenerationResponse, Message, Role, ) -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ( CUSTOMIZED_MODEL_ID, DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import merge_single_response class Generation(BaseApi): diff --git a/dashscope/aigc/image_generation.py b/dashscope/legacy_1x/aigc/image_generation.py similarity index 98% rename from dashscope/aigc/image_generation.py rename to dashscope/legacy_1x/aigc/image_generation.py index b8304957..8d896bae 100644 --- a/dashscope/aigc/image_generation.py +++ b/dashscope/legacy_1x/aigc/image_generation.py @@ -3,22 +3,22 @@ from typing import Generator, List, Union, AsyncGenerator -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( Message, DashScopeAPIResponse, ImageGenerationResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( BaseAioApi, BaseApi, BaseAsyncApi, BaseAsyncAioApi, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import merge_single_response class ImageGeneration(BaseApi, BaseAsyncApi): diff --git a/dashscope/aigc/image_synthesis.py b/dashscope/legacy_1x/aigc/image_synthesis.py similarity index 99% rename from dashscope/aigc/image_synthesis.py rename to dashscope/legacy_1x/aigc/image_synthesis.py index eb25276a..fd56c717 100644 --- a/dashscope/aigc/image_synthesis.py +++ b/dashscope/legacy_1x/aigc/image_synthesis.py @@ -3,20 +3,24 @@ from typing import Any, Dict, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, ImageSynthesisResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( BaseAsyncApi, BaseApi, BaseAsyncAioApi, BaseAioApi, ) -from dashscope.common.constants import IMAGES, NEGATIVE_PROMPT, PROMPT -from dashscope.common.error import InputRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.common.constants import ( + IMAGES, + NEGATIVE_PROMPT, + PROMPT, +) +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local class ImageSynthesis(BaseAsyncApi): diff --git a/dashscope/aigc/multimodal_conversation.py b/dashscope/legacy_1x/aigc/multimodal_conversation.py similarity index 97% rename from dashscope/aigc/multimodal_conversation.py rename to dashscope/legacy_1x/aigc/multimodal_conversation.py index c3d900b7..67cf0e7c 100644 --- a/dashscope/aigc/multimodal_conversation.py +++ b/dashscope/legacy_1x/aigc/multimodal_conversation.py @@ -4,15 +4,17 @@ import copy from typing import Any, AsyncGenerator, Dict, Generator, List, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( MultiModalConversationResponse, ) -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.error import ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element -from dashscope.utils.param_utils import ParamUtil -from dashscope.utils.message_utils import merge_multimodal_single_response +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.utils.param_utils import ParamUtil +from dashscope.legacy_1x.utils.message_utils import ( + merge_multimodal_single_response, +) class MultiModalConversation(BaseApi): diff --git a/dashscope/aigc/video_synthesis.py b/dashscope/legacy_1x/aigc/video_synthesis.py similarity index 99% rename from dashscope/aigc/video_synthesis.py rename to dashscope/legacy_1x/aigc/video_synthesis.py index 0c46257e..12c34384 100644 --- a/dashscope/aigc/video_synthesis.py +++ b/dashscope/legacy_1x/aigc/video_synthesis.py @@ -5,19 +5,19 @@ from concurrent.futures import ThreadPoolExecutor, as_completed -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, VideoSynthesisResponse, ) -from dashscope.client.base_api import BaseAsyncApi, BaseAsyncAioApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseAsyncApi, BaseAsyncAioApi +from dashscope.legacy_1x.common.constants import ( PROMPT, REFERENCE_VIDEO_URLS, REFERENCE_URLS, MEDIA_URLS, ) -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local class VideoSynthesis(BaseAsyncApi): diff --git a/dashscope/finetune/reinforcement/common/__init__.py b/dashscope/legacy_1x/api_entities/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/common/__init__.py rename to dashscope/legacy_1x/api_entities/__init__.py diff --git a/dashscope/api_entities/aio_session.py b/dashscope/legacy_1x/api_entities/aio_session.py similarity index 98% rename from dashscope/api_entities/aio_session.py rename to dashscope/legacy_1x/api_entities/aio_session.py index 2de1c46e..7edd7d74 100644 --- a/dashscope/api_entities/aio_session.py +++ b/dashscope/legacy_1x/api_entities/aio_session.py @@ -16,7 +16,7 @@ import aiohttp import certifi -from dashscope.common.logging import logger +from dashscope.legacy_1x.common.logging import logger _shared_ssl_context: Optional[ssl.SSLContext] = None _aio_sessions: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary() diff --git a/dashscope/api_entities/aiohttp_request.py b/dashscope/legacy_1x/api_entities/aiohttp_request.py similarity index 96% rename from dashscope/api_entities/aiohttp_request.py rename to dashscope/legacy_1x/api_entities/aiohttp_request.py index 611e72ec..e2784627 100644 --- a/dashscope/api_entities/aiohttp_request.py +++ b/dashscope/legacy_1x/api_entities/aiohttp_request.py @@ -7,20 +7,22 @@ import aiohttp -from dashscope.api_entities.aio_session import ( +from dashscope.legacy_1x.api_entities.aio_session import ( get_shared_aio_session, send_with_retry_async, ) -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SSE_CONTENT_TYPE, HTTPMethod, ) -from dashscope.common.error import UnsupportedHTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import async_to_sync +from dashscope.legacy_1x.common.error import UnsupportedHTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import async_to_sync class AioHttpRequest(AioBaseRequest): diff --git a/dashscope/api_entities/api_request_data.py b/dashscope/legacy_1x/api_entities/api_request_data.py similarity index 97% rename from dashscope/api_entities/api_request_data.py rename to dashscope/legacy_1x/api_entities/api_request_data.py index c6ea54ab..4b839103 100644 --- a/dashscope/api_entities/api_request_data.py +++ b/dashscope/legacy_1x/api_entities/api_request_data.py @@ -7,8 +7,8 @@ import aiohttp -from dashscope.common.constants import ApiProtocol -from dashscope.io.input_output import InputResolver +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.io.input_output import InputResolver class ApiRequestData: diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/legacy_1x/api_entities/api_request_factory.py similarity index 92% rename from dashscope/api_entities/api_request_factory.py rename to dashscope/legacy_1x/api_entities/api_request_factory.py index 6511c29c..1ff1fc83 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/legacy_1x/api_entities/api_request_factory.py @@ -7,20 +7,23 @@ import requests import dashscope -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.api_entities.encryption import Encryption -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.websocket_request import WebSocketRequest -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.websocket_request import WebSocketRequest +from dashscope.legacy_1x.common.constants import ( SERVICE_API_PATH, ApiProtocol, HTTPMethod, ) -from dashscope.common.error import InputDataRequired, UnsupportedApiProtocol -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers -from dashscope.common.env import resolve_base_url -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.error import ( + InputDataRequired, + UnsupportedApiProtocol, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode def _build_api_request( # pylint: disable=too-many-branches diff --git a/dashscope/api_entities/base_request.py b/dashscope/legacy_1x/api_entities/base_request.py similarity index 83% rename from dashscope/api_entities/base_request.py rename to dashscope/legacy_1x/api_entities/base_request.py index 1a9a664f..eb3b9847 100644 --- a/dashscope/api_entities/base_request.py +++ b/dashscope/legacy_1x/api_entities/base_request.py @@ -4,8 +4,10 @@ import os from abc import ABC, abstractmethod -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.constants import ( + DASHSCOPE_DISABLE_DATA_INSPECTION_ENV, +) +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class BaseRequest(ABC): diff --git a/dashscope/api_entities/chat_completion_types.py b/dashscope/legacy_1x/api_entities/chat_completion_types.py similarity index 99% rename from dashscope/api_entities/chat_completion_types.py rename to dashscope/legacy_1x/api_entities/chat_completion_types.py index 5d8ac2f9..d2ace56a 100644 --- a/dashscope/api_entities/chat_completion_types.py +++ b/dashscope/legacy_1x/api_entities/chat_completion_types.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import List, Literal, Optional -from dashscope.common.base_type import BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseObjectMixin @dataclass(init=False) diff --git a/dashscope/api_entities/dashscope_response.py b/dashscope/legacy_1x/api_entities/dashscope_response.py similarity index 100% rename from dashscope/api_entities/dashscope_response.py rename to dashscope/legacy_1x/api_entities/dashscope_response.py diff --git a/dashscope/api_entities/encryption.py b/dashscope/legacy_1x/api_entities/encryption.py similarity index 95% rename from dashscope/api_entities/encryption.py rename to dashscope/legacy_1x/api_entities/encryption.py index a40dc2bf..c6dac207 100644 --- a/dashscope/api_entities/encryption.py +++ b/dashscope/legacy_1x/api_entities/encryption.py @@ -13,15 +13,15 @@ from cryptography.hazmat.backends import default_backend import dashscope -from dashscope.common.constants import ( +from dashscope.legacy_1x.common.constants import ( ENCRYPTION_AES_SECRET_KEY_BYTES, ENCRYPTION_AES_IV_LENGTH, DEFAULT_REQUEST_TIMEOUT_SECONDS, ) -from dashscope.common.logging import logger -from dashscope.common.env import resolve_base_url -from dashscope.common.api_key import get_default_api_key -from dashscope.common.utils import get_sdk_headers +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.utils import get_sdk_headers class Encryption: diff --git a/dashscope/api_entities/http_request.py b/dashscope/legacy_1x/api_entities/http_request.py similarity index 97% rename from dashscope/api_entities/http_request.py rename to dashscope/legacy_1x/api_entities/http_request.py index 96ee9ea8..4f283625 100644 --- a/dashscope/api_entities/http_request.py +++ b/dashscope/legacy_1x/api_entities/http_request.py @@ -12,26 +12,28 @@ import requests from requests.adapters import HTTPAdapter -from dashscope.api_entities.aio_session import ( +from dashscope.legacy_1x.api_entities.aio_session import ( get_shared_aio_session, send_with_retry_async, ) -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SSE_CONTENT_TYPE, HTTPMethod, ) -from dashscope.common.error import UnsupportedHTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import ( +from dashscope.legacy_1x.common.error import UnsupportedHTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import ( _handle_aio_stream, _handle_aiohttp_failed_response, _handle_http_failed_response, _handle_stream, ) -from dashscope.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.encryption import Encryption # Shared synchronous session for connection pooling _shared_sync_session: Optional[requests.Session] = None diff --git a/dashscope/api_entities/websocket_request.py b/dashscope/legacy_1x/api_entities/websocket_request.py similarity index 97% rename from dashscope/api_entities/websocket_request.py rename to dashscope/legacy_1x/api_entities/websocket_request.py index 2358b4ec..7e285aa8 100644 --- a/dashscope/api_entities/websocket_request.py +++ b/dashscope/legacy_1x/api_entities/websocket_request.py @@ -9,21 +9,23 @@ import aiohttp -from dashscope.api_entities.base_request import AioBaseRequest -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.base_request import AioBaseRequest +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, SERVICE_503_MESSAGE, WEBSOCKET_ERROR_CODE, ) -from dashscope.common.error import ( +from dashscope.legacy_1x.common.error import ( RequestFailure, UnexpectedMessageReceived, UnknownMessageReceived, ) -from dashscope.common.logging import logger -from dashscope.common.utils import async_to_sync -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import async_to_sync +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, ERROR_MESSAGE, ERROR_NAME, diff --git a/dashscope/legacy_1x/app/__init__.py b/dashscope/legacy_1x/app/__init__.py new file mode 100644 index 00000000..e0f1ba76 --- /dev/null +++ b/dashscope/legacy_1x/app/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .application import Application + +__all__ = ["Application"] diff --git a/dashscope/app/application.py b/dashscope/legacy_1x/app/application.py similarity index 95% rename from dashscope/app/application.py rename to dashscope/legacy_1x/app/application.py index 6afb72cf..90d70fa3 100644 --- a/dashscope/app/application.py +++ b/dashscope/legacy_1x/app/application.py @@ -8,20 +8,22 @@ import copy from typing import Any, Dict, Generator, List, Optional, Union -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import Message, Role -from dashscope.app.application_response import ApplicationResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.api_key import get_default_api_key -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.app.application_response import ApplicationResponse +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import ( DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, InvalidInput -from dashscope.common.logging import logger -from dashscope.utils.message_utils import merge_single_response +from dashscope.legacy_1x.common.error import InputRequired, InvalidInput +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.utils.message_utils import merge_single_response class Application(BaseApi): diff --git a/dashscope/app/application_response.py b/dashscope/legacy_1x/app/application_response.py similarity index 99% rename from dashscope/app/application_response.py rename to dashscope/legacy_1x/app/application_response.py index 19af1668..976d7150 100644 --- a/dashscope/app/application_response.py +++ b/dashscope/legacy_1x/app/application_response.py @@ -9,7 +9,7 @@ from http import HTTPStatus from typing import Dict, List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) diff --git a/dashscope/legacy_1x/assistants/__init__.py b/dashscope/legacy_1x/assistants/__init__.py new file mode 100644 index 00000000..9c63d153 --- /dev/null +++ b/dashscope/legacy_1x/assistants/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import warnings + +# yapf: disable + +from dashscope.legacy_1x.assistants.assistant_types import ( + Assistant, AssistantFile, + AssistantList, + DeleteResponse, +) +from dashscope.legacy_1x.assistants.assistants import Assistants + +__all__ = [ + 'Assistant', + 'Assistants', + 'AssistantList', + 'AssistantFile', + 'DeleteResponse', +] + +# Deprecation warning +_DEPRECATION_MSG = ( + "The Assistants API (dashscope.assistants) is deprecated and will be " + "removed in a future release. Please migrate to the Responses API. " + "See https://help.aliyun.com/zh/model-studio/" + "synchronous-call-api-reference for migration details." +) + +warnings.warn(_DEPRECATION_MSG, category=DeprecationWarning, stacklevel=2) diff --git a/dashscope/assistants/assistant_types.py b/dashscope/legacy_1x/assistants/assistant_types.py similarity index 98% rename from dashscope/assistants/assistant_types.py rename to dashscope/legacy_1x/assistants/assistant_types.py index 624c4c8e..71691752 100644 --- a/dashscope/assistants/assistant_types.py +++ b/dashscope/legacy_1x/assistants/assistant_types.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Dict, List, Optional, Union -from dashscope.common.base_type import BaseList, BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseList, BaseObjectMixin __all__ = [ "Assistant", diff --git a/dashscope/assistants/assistants.py b/dashscope/legacy_1x/assistants/assistants.py similarity index 98% rename from dashscope/assistants/assistants.py rename to dashscope/legacy_1x/assistants/assistants.py index 5342f785..2036050e 100644 --- a/dashscope/assistants/assistants.py +++ b/dashscope/legacy_1x/assistants/assistants.py @@ -4,12 +4,12 @@ import warnings from typing import Dict, List, Optional -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( Assistant, AssistantList, DeleteResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, DeleteMixin, @@ -17,13 +17,13 @@ ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import ModelRequired +from dashscope.legacy_1x.common.error import ModelRequired __all__ = ["Assistants"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.assistants) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/assistants/files.py b/dashscope/legacy_1x/assistants/files.py similarity index 96% rename from dashscope/assistants/files.py rename to dashscope/legacy_1x/assistants/files.py index 6e118001..6cc21c88 100644 --- a/dashscope/assistants/files.py +++ b/dashscope/legacy_1x/assistants/files.py @@ -3,24 +3,24 @@ import warnings from typing import Optional -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( AssistantFile, AssistantFileList, DeleteResponse, ) -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetStatusMixin, ListObjectMixin, ) -from dashscope.common.error import InputRequired +from dashscope.legacy_1x.common.error import InputRequired __all__ = ["Files"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.assistants) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.assistants) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/audio/__init__.py b/dashscope/legacy_1x/audio/__init__.py new file mode 100644 index 00000000..bdf170a7 --- /dev/null +++ b/dashscope/legacy_1x/audio/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from . import asr, tts, tts_v2, qwen_tts, qwen_tts_realtime, qwen_omni + +__all__ = [ # type: ignore[misc] + asr, + tts, + tts_v2, + qwen_tts, + qwen_tts_realtime, + qwen_omni, +] # noqa: E501 diff --git a/dashscope/audio/asr/__init__.py b/dashscope/legacy_1x/audio/asr/__init__.py similarity index 100% rename from dashscope/audio/asr/__init__.py rename to dashscope/legacy_1x/audio/asr/__init__.py diff --git a/dashscope/audio/asr/asr_phrase_manager.py b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py similarity index 96% rename from dashscope/audio/asr/asr_phrase_manager.py rename to dashscope/legacy_1x/audio/asr/asr_phrase_manager.py index c1dd1a5f..d493a41e 100644 --- a/dashscope/audio/asr/asr_phrase_manager.py +++ b/dashscope/legacy_1x/audio/asr/asr_phrase_manager.py @@ -4,10 +4,12 @@ from http import HTTPStatus from typing import Any, Dict -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.error import InvalidParameter -from dashscope.common.logging import logger +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.error import InvalidParameter +from dashscope.legacy_1x.common.logging import logger from dashscope.finetune.finetunes import FineTunes diff --git a/dashscope/audio/asr/recognition.py b/dashscope/legacy_1x/audio/asr/recognition.py similarity index 98% rename from dashscope/audio/asr/recognition.py rename to dashscope/legacy_1x/audio/asr/recognition.py index b9bf978e..0967f549 100644 --- a/dashscope/audio/asr/recognition.py +++ b/dashscope/legacy_1x/audio/asr/recognition.py @@ -12,19 +12,21 @@ from threading import Timer from typing import Any, Dict, List, Optional, Union -from dashscope.api_entities.dashscope_response import RecognitionResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.error import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + RecognitionResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.error import ( InputDataRequired, InputRequired, InvalidParameter, InvalidTask, ModelRequired, ) -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode def _merge_recognition_params( diff --git a/dashscope/audio/asr/transcription.py b/dashscope/legacy_1x/audio/asr/transcription.py similarity index 96% rename from dashscope/audio/asr/transcription.py rename to dashscope/legacy_1x/audio/asr/transcription.py index 88631aca..609f6c11 100644 --- a/dashscope/audio/asr/transcription.py +++ b/dashscope/legacy_1x/audio/asr/transcription.py @@ -6,14 +6,14 @@ import requests -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, TranscriptionResponse, ) -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Transcription(BaseAsyncApi): diff --git a/dashscope/audio/asr/translation_recognizer.py b/dashscope/legacy_1x/audio/asr/translation_recognizer.py similarity index 99% rename from dashscope/audio/asr/translation_recognizer.py rename to dashscope/legacy_1x/audio/asr/translation_recognizer.py index d24d277f..bd32fea9 100644 --- a/dashscope/audio/asr/translation_recognizer.py +++ b/dashscope/legacy_1x/audio/asr/translation_recognizer.py @@ -11,19 +11,19 @@ from threading import Timer from typing import Any, Dict, List, Optional -from dashscope.audio.asr.recognition import _merge_recognition_params -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.error import ( +from dashscope.legacy_1x.audio.asr.recognition import _merge_recognition_params +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.error import ( InputDataRequired, InputRequired, InvalidParameter, InvalidTask, ModelRequired, ) -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode DASHSCOPE_TRANSLATION_KEY = "translations" DASHSCOPE_TRANSCRIPTION_KEY = "transcription" diff --git a/dashscope/audio/asr/vocabulary.py b/dashscope/legacy_1x/audio/asr/vocabulary.py similarity index 97% rename from dashscope/audio/asr/vocabulary.py rename to dashscope/legacy_1x/audio/asr/vocabulary.py index 9e8510b4..9a3111ba 100644 --- a/dashscope/audio/asr/vocabulary.py +++ b/dashscope/legacy_1x/audio/asr/vocabulary.py @@ -6,9 +6,9 @@ import requests -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class VocabularyServiceException(Exception): diff --git a/dashscope/audio/http_tts/__init__.py b/dashscope/legacy_1x/audio/http_tts/__init__.py similarity index 74% rename from dashscope/audio/http_tts/__init__.py rename to dashscope/legacy_1x/audio/http_tts/__init__.py index 9d304d32..d0e451c2 100644 --- a/dashscope/audio/http_tts/__init__.py +++ b/dashscope/legacy_1x/audio/http_tts/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesisResult, HttpSpeechSynthesizer, ) diff --git a/dashscope/audio/http_tts/http_speech_synthesizer.py b/dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py similarity index 95% rename from dashscope/audio/http_tts/http_speech_synthesizer.py rename to dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py index b2e3e84e..ca831955 100644 --- a/dashscope/audio/http_tts/http_speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/http_tts/http_speech_synthesizer.py @@ -5,12 +5,12 @@ from http import HTTPStatus from typing import Dict, Iterator, List, Optional, Union -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, SpeechSynthesisResponse, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import HTTPMethod +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import HTTPMethod class HttpSpeechSynthesisResult: @@ -199,12 +199,12 @@ def _http_call( url: Optional[str] = None, ): """Make HTTP API call using BaseApi infrastructure.""" - from dashscope.api_entities.http_request import HttpRequest + from dashscope.legacy_1x.api_entities.http_request import HttpRequest # Get base URL import dashscope - from dashscope.common.env import resolve_base_url - from dashscope.common.utils import get_sdk_headers, join_url + from dashscope.legacy_1x.common.env import resolve_base_url + from dashscope.legacy_1x.common.utils import get_sdk_headers, join_url if not url: url = dashscope.base_http_api_url @@ -215,7 +215,7 @@ def _http_call( ) # Get API key - from dashscope.common.api_key import get_default_api_key + from dashscope.legacy_1x.common.api_key import get_default_api_key if api_key is None: api_key = get_default_api_key() diff --git a/dashscope/audio/qwen_asr/__init__.py b/dashscope/legacy_1x/audio/qwen_asr/__init__.py similarity index 100% rename from dashscope/audio/qwen_asr/__init__.py rename to dashscope/legacy_1x/audio/qwen_asr/__init__.py diff --git a/dashscope/audio/qwen_asr/qwen_transcription.py b/dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py similarity index 96% rename from dashscope/audio/qwen_asr/qwen_transcription.py rename to dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py index f731ff92..f32b5f78 100644 --- a/dashscope/audio/qwen_asr/qwen_transcription.py +++ b/dashscope/legacy_1x/audio/qwen_asr/qwen_transcription.py @@ -6,13 +6,13 @@ import requests -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, TranscriptionResponse, ) -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class QwenTranscription(BaseAsyncApi): diff --git a/dashscope/audio/qwen_omni/__init__.py b/dashscope/legacy_1x/audio/qwen_omni/__init__.py similarity index 100% rename from dashscope/audio/qwen_omni/__init__.py rename to dashscope/legacy_1x/audio/qwen_omni/__init__.py diff --git a/dashscope/audio/qwen_omni/omni_realtime.py b/dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py similarity index 99% rename from dashscope/audio/qwen_omni/omni_realtime.py rename to dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py index 8f5251a2..ddef0037 100644 --- a/dashscope/audio/qwen_omni/omni_realtime.py +++ b/dashscope/legacy_1x/audio/qwen_omni/omni_realtime.py @@ -12,9 +12,9 @@ import websocket # pylint: disable=wrong-import-order import dashscope -from dashscope.common.error import ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class OmniRealtimeCallback: diff --git a/dashscope/audio/qwen_tts/__init__.py b/dashscope/legacy_1x/audio/qwen_tts/__init__.py similarity index 100% rename from dashscope/audio/qwen_tts/__init__.py rename to dashscope/legacy_1x/audio/qwen_tts/__init__.py diff --git a/dashscope/audio/qwen_tts/speech_synthesizer.py b/dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py similarity index 92% rename from dashscope/audio/qwen_tts/speech_synthesizer.py rename to dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py index f15f7c89..423b3598 100644 --- a/dashscope/audio/qwen_tts/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/qwen_tts/speech_synthesizer.py @@ -3,9 +3,11 @@ from typing import Generator, Union -from dashscope.api_entities.dashscope_response import TextToSpeechResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.api_entities.dashscope_response import ( + TextToSpeechResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired class SpeechSynthesizer(BaseApi): diff --git a/dashscope/audio/qwen_tts_realtime/__init__.py b/dashscope/legacy_1x/audio/qwen_tts_realtime/__init__.py similarity index 100% rename from dashscope/audio/qwen_tts_realtime/__init__.py rename to dashscope/legacy_1x/audio/qwen_tts_realtime/__init__.py diff --git a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py b/dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py similarity index 98% rename from dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py rename to dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py index 31eb0380..48c67856 100644 --- a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py +++ b/dashscope/legacy_1x/audio/qwen_tts_realtime/qwen_tts_realtime.py @@ -10,9 +10,9 @@ import websocket # pylint: disable=wrong-import-order import dashscope -from dashscope.common.error import ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.common.error import ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent class QwenTtsRealtimeCallback: diff --git a/dashscope/audio/tts/__init__.py b/dashscope/legacy_1x/audio/tts/__init__.py similarity index 100% rename from dashscope/audio/tts/__init__.py rename to dashscope/legacy_1x/audio/tts/__init__.py diff --git a/dashscope/audio/tts/speech_synthesizer.py b/dashscope/legacy_1x/audio/tts/speech_synthesizer.py similarity index 96% rename from dashscope/audio/tts/speech_synthesizer.py rename to dashscope/legacy_1x/audio/tts/speech_synthesizer.py index 0381abe8..c3328681 100644 --- a/dashscope/audio/tts/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/tts/speech_synthesizer.py @@ -4,10 +4,12 @@ from http import HTTPStatus from typing import Dict, List -from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + SpeechSynthesisResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class SpeechSynthesisResult: diff --git a/dashscope/audio/tts_v2/__init__.py b/dashscope/legacy_1x/audio/tts_v2/__init__.py similarity index 100% rename from dashscope/audio/tts_v2/__init__.py rename to dashscope/legacy_1x/audio/tts_v2/__init__.py diff --git a/dashscope/audio/tts_v2/enrollment.py b/dashscope/legacy_1x/audio/tts_v2/enrollment.py similarity index 97% rename from dashscope/audio/tts_v2/enrollment.py rename to dashscope/legacy_1x/audio/tts_v2/enrollment.py index 239606a1..073f7086 100644 --- a/dashscope/audio/tts_v2/enrollment.py +++ b/dashscope/legacy_1x/audio/tts_v2/enrollment.py @@ -6,9 +6,9 @@ import requests -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod -from dashscope.common.logging import logger +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.common.logging import logger class VoiceEnrollmentException(Exception): diff --git a/dashscope/audio/tts_v2/speech_synthesizer.py b/dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py similarity index 99% rename from dashscope/audio/tts_v2/speech_synthesizer.py rename to dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py index eea32e3f..a96b132f 100644 --- a/dashscope/audio/tts_v2/speech_synthesizer.py +++ b/dashscope/legacy_1x/audio/tts_v2/speech_synthesizer.py @@ -14,17 +14,17 @@ import websocket import dashscope -from dashscope.common.constants import WEBSOCKET_ERROR_CODE -from dashscope.common.env import resolve_base_url -from dashscope.common.error import ( +from dashscope.legacy_1x.common.constants import WEBSOCKET_ERROR_CODE +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.error import ( InputRequired, InvalidTask, ModelRequired, RequestFailure, ) -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, ERROR_MESSAGE, ERROR_NAME, diff --git a/dashscope/legacy_1x/cli/__init__.py b/dashscope/legacy_1x/cli/__init__.py new file mode 100644 index 00000000..b72c1dde --- /dev/null +++ b/dashscope/legacy_1x/cli/__init__.py @@ -0,0 +1,591 @@ +# -*- coding: utf-8 -*- +# pylint: disable=wrong-import-position +"""DashScope command-line entry point. + +This package is intentionally thin โ€” all command-specific logic lives in +sub-modules (generation, fine_tunes, files, etc.). +""" +import sys +import warnings +from pathlib import Path +from typing import Optional + +# Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL +warnings.filterwarnings( + "ignore", + message=".*urllib3.*only supports OpenSSL.*", + category=Warning, +) + +import typer # noqa: E402 +from rich.markup import escape # noqa: E402 + +import dashscope # noqa: E402 +from dashscope.legacy_1x.cli.common import console, err_console # noqa: E402 +from dashscope.legacy_1x.common.error import AuthenticationError # noqa: E402 +from dashscope.legacy_1x.common.utils import set_sdk_client # noqa: E402 +from dashscope.legacy_1x.cli import ( # noqa: E402 + application, + auth, + deployments, + embeddings, + files, + fine_tunes, + generation, + image_generation, + image_synthesis, + models, + multimodal_conversation, + multimodal_embedding, + oss, + rerank, + speech_synthesis, + tokenization, + transcription, + video_synthesis, +) + +set_sdk_client("python-cli") + + +# --------------------------------------------------------------------------- +# Legacy command compatibility layer +# --------------------------------------------------------------------------- + +# Command name mapping: old -> new +_COMMAND_MAP = { + "fine_tunes.call": "fine-tunes create", + "fine_tunes.get": "fine-tunes get", + "fine_tunes.list": "fine-tunes list", + "fine_tunes.stream": "fine-tunes stream", + "fine_tunes.cancel": "fine-tunes cancel", + "fine_tunes.delete": "fine-tunes delete", + "generation.call": "generation create", + "files.upload": "files upload", + "files.get": "files get", + "files.list": "files list", + "files.delete": "files delete", + "deployments.call": "deployments create", + "deployments.get": "deployments get", + "deployments.list": "deployments list", + "deployments.scale": "deployments scale", + "deployments.delete": "deployments delete", + "oss.upload": "oss upload", +} + +# Parameter name mapping: old -> new (underscore to dash) +_PARAM_MAP = { + "--training_file_ids": "--training-file-ids", + "--validation_file_ids": "--validation-file-ids", + "--n_epochs": "--n-epochs", + "--batch_size": "--batch-size", + "--learning_rate": "--learning-rate", + "--prompt_loss": "--prompt-loss", + "--hyper_parameters": "--hyper-parameters", + "--file_id": "--file-id", + "--deployed_model": "--deployed-model", + "--base_url": "--base-url", + "--api_key": "--api-key", + "--start_page": "--start-page", + "--page_size": "--page-size", +} + +_TOP_LEVEL_COMMANDS = { + "auth", + "generation", + "ft", + "fine-tunes", + "files", + "deployments", + "oss", + "rerank", + "embeddings", + "tokenization", + "models", + "application", + "image-synthesis", + "video-synthesis", + "image-generation", + "multimodal-conversation", + "multimodal-embedding", + "transcription", + "speech-synthesis", + "rl", + "agentic-rl", +} + + +_COMMANDS_WITH_LOCAL_API_KEY = {"oss", "rl", "agentic-rl"} + +# Routing keyword for the bundled agent, handled in main() before typer sees +# argv. Deliberately NOT a member of _TOP_LEVEL_COMMANDS: that set mirrors the +# typer-registered command groups, and `expert` is not one of them. +_EXPERT_COMMAND = "expert" + +_EXPERT_USAGE = """[bold]DashScope SDK Expert[/bold] โ€” the built-in AI assistant. + +[bold]Usage:[/bold] + dashscope interactive assistant (TUI by default) + dashscope expert the same, spelled explicitly + dashscope expert --cli interactive, plain REPL instead of the TUI + dashscope expert --tui interactive, force the TUI + dashscope expert "question" ask once and exit + +With no argument, piped stdin is read as a one-shot prompt. +SDK subcommands (generation, ft, files, models, ...) dispatch as usual; an +unrecognized command is reported as an error rather than treated as a +question, so ask one with [bold]dashscope expert "question"[/bold]. + +Requires [bold]pip install dashscope[acli][/bold].""" + +_LEGACY_COMMANDS_WITH_SIZE_OPTION = { + "files.list", + "fine_tunes.list", + "deployments.list", +} + + +def _translate_param_name(arg): + """Translate legacy underscore option names to Typer hyphen names.""" + if arg in _PARAM_MAP: + return _PARAM_MAP[arg] + if arg.startswith("--") and "=" in arg: + option_name, option_value = arg.split("=", 1) + if option_name in _PARAM_MAP: + return f"{_PARAM_MAP[option_name]}={option_value}" + return arg + + +def _translate_legacy_args(argv): + """Translate legacy argparse command format to Typer format. + + Legacy format: dashscope fine_tunes.call --training_file_ids ... + New format: dashscope fine-tunes create --training-file-ids ... + + Returns modified argv list. + """ + if len(argv) < 2: + return argv + + is_legacy_command = argv[1] in _COMMAND_MAP + if not is_legacy_command: + return [argv[0]] + [_translate_param_name(arg) for arg in argv[1:]] + + command_args = _COMMAND_MAP[argv[1]].split() + top_level_args = [] + translated_args = [] + i = 2 + while i < len(argv): + arg = argv[i] + translated_arg = _translate_param_name(arg) + if argv[1] in _LEGACY_COMMANDS_WITH_SIZE_OPTION: + if translated_arg == "--page-size": + translated_arg = "--size" + elif translated_arg.startswith("--page-size="): + translated_arg = translated_arg.replace( + "--page-size=", + "--size=", + 1, + ) + + if translated_arg == "--api-key": + top_level_args.append("--api-key") + if i + 1 < len(argv): + top_level_args.append(argv[i + 1]) + i += 2 + else: + i += 1 + continue + + if translated_arg.startswith("--api-key="): + top_level_args.append(translated_arg) + i += 1 + continue + + translated_args.append(translated_arg) + i += 1 + + return [argv[0]] + top_level_args + command_args + translated_args + + +def _exit_missing_api_key_value(option_name): + err_console.print( + f"[red]Error:[/red] Option '{option_name}' requires an argument.", + ) + sys.exit(2) + + +def _extract_global_api_key(argv): + """Extract global -k/--api-key from argv and set dashscope.api_key. + + For backward compatibility, global api-key can appear before or after most + command names. Commands that define their own local api-key option are left + untouched once their command name has been reached. + """ + new_argv = [] + i = 0 + current_command = None + while i < len(argv): + arg = argv[i] + + if i > 0 and current_command is None and arg in _TOP_LEVEL_COMMANDS: + current_command = arg + + should_parse_global_api_key = ( + current_command not in _COMMANDS_WITH_LOCAL_API_KEY + ) + + if arg in ("-k", "--api-key"): + if i + 1 >= len(argv): + _exit_missing_api_key_value(arg) + next_arg = argv[i + 1] + if not next_arg or next_arg.startswith("-"): + _exit_missing_api_key_value(arg) + if should_parse_global_api_key: + if next_arg in _TOP_LEVEL_COMMANDS: + _exit_missing_api_key_value(arg) + dashscope.api_key = next_arg + i += 2 + continue + + if arg.startswith("--api-key="): + api_key = arg.split("=", 1)[1] + if not api_key: + _exit_missing_api_key_value("--api-key") + if should_parse_global_api_key: + dashscope.api_key = api_key + i += 1 + continue + + new_argv.append(arg) + i += 1 + + return new_argv + + +def _translate_help_shortcut(argv): + """Translate -h to --help for convenience.""" + return [arg if arg != "-h" else "--help" for arg in argv] + + +_MD_MARKER = "" +_PY_MARKER = "# managed-by: dashscope-expert" +_EXPERT_EXAMPLE = "dashscope-sdk-expert" + + +def _cleanup_legacy_expert_sync(): + """Remove files the deleted expert bundling synced into ~/.acli. + + Only files carrying the managed marker are deleted โ€” without it the file + is user-owned and left untouched. Needed so stale global copies don't + shadow the workspace ./.acli/ configs installed via example download. + """ + try: + from dashscope.acli.config import CONFIG_DIR + except ImportError: + return + md_targets = [] + for subdir in ("skills", "references"): + target_dir = CONFIG_DIR / subdir + if target_dir.is_dir(): + md_targets.extend(target_dir.glob("*.md")) + for path in md_targets: + try: + if _MD_MARKER in path.read_text(encoding="utf-8"): + path.unlink() + except OSError: + pass + build = CONFIG_DIR / "build_sdk_index.py" + try: + if build.is_file() and _PY_MARKER in build.read_text(encoding="utf-8"): + build.unlink() + except OSError: + pass + + +def _maybe_offer_example_download(): + """First interactive run without ./.acli: offer the SDK-expert example. + + Accepting merges the example into ./.acli/ via the same code path as + `example download` (copied from the packaged install dir when present); + declining writes a marker into ./.acli/ so we never ask again here. + """ + if not sys.stdin.isatty() or (Path.cwd() / ".acli").is_dir(): + return + try: + from dashscope.acli.cli.examples import _handle_example_command + from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale + except ImportError: + return + err_console.print( + "[cyan]First run:[/cyan] download the dashscope-sdk-expert " + "example config into ./.acli/\n" + "(SDK Q&A expert persona + skill templates + SDK knowledge " + "index; pure config, editable anytime).", + ) + err_console.print( + f"[dim]Guide: {_GUIDE_DOC.format(_doc_locale())}[/dim]", + ) + try: + answer = input("Download the example config? [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + err_console.print() + return + if answer in ("n", "no"): + try: + marker = Path.cwd() / ".acli" / ".dashscope-example-declined" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text("declined\n", encoding="utf-8") + except OSError: + pass + err_console.print( + "[dim]Skipped. To customize later, run: " + "python -m dashscope.acli example download " + "dashscope-sdk-expert[/dim]", + ) + return + _handle_example_command(["download", _EXPERT_EXAMPLE]) + + +def _route_to_expert(command, tui=None): + """Run the vendored acli agent (bare ``dashscope`` or ``dashscope expert``). + + ``command`` is a one-shot prompt, or None for the interactive REPL. + """ + try: + from dashscope.acli.cli.handlers_key import _GUIDE_DOC, _doc_locale + from dashscope.acli.ui.embedded import run + except ImportError as exception: + err_console.print( + "[yellow]Tip:[/yellow] Install dashscope[acli] to enable " + "the agent " + f"({exception}).", + ) + return + + if command is None and not sys.stdin.isatty(): + # The interactive REPL needs a real terminal; prompt_toolkit crashes + # on non-tty stdin (e.g. kqueue rejects /dev/null with EINVAL). + # Treat piped text as a one-shot prompt instead. + try: + piped = sys.stdin.read().strip() + except OSError: + piped = "" + if not piped: + err_console.print( + "[red]Error:[/red] Interactive mode requires a terminal. " + 'Pass a prompt instead, e.g. dashscope expert "your question".', + ) + sys.exit(2) + command = piped + + _cleanup_legacy_expert_sync() + if command is None: + _maybe_offer_example_download() + + try: + run( + app_name="DashScope SDK Expert", + prompt_symbol="dashscope> ", + api_key=dashscope.api_key or None, + command=command, + tui=tui, + module="expert", + guide_url=_GUIDE_DOC.format(_doc_locale()), + ) + except SystemExit: + pass + except Exception as exception: # pylint: disable=broad-except + err_console.print( + f"[yellow]Tip:[/yellow] Agent failed to start ({exception}).", + ) + + +# --------------------------------------------------------------------------- +# Typer app +# --------------------------------------------------------------------------- + +app = typer.Typer( + name="dashscope", + help="DashScope command line tools.", + add_completion=True, + no_args_is_help=True, + rich_markup_mode="rich", +) + + +@app.callback() +def callback( + api_key: Optional[str] = typer.Option( + None, + "-k", + "--api-key", + help="DashScope API Key.", + ), +): + """Configure global CLI options.""" + if api_key: + dashscope.api_key = api_key + + +# Register sub-command groups +app.add_typer(auth.app) +app.add_typer(generation.app) +app.add_typer(fine_tunes.app, name="ft") +app.add_typer(fine_tunes.app, name="fine-tunes", hidden=True) +app.add_typer(files.app) +app.add_typer(deployments.app) +app.add_typer(oss.app) +app.add_typer(rerank.app) +app.add_typer(embeddings.app) +app.add_typer(tokenization.app) +app.add_typer(models.app) +app.add_typer(application.app) +app.add_typer(image_synthesis.app) +app.add_typer(video_synthesis.app) +app.add_typer(image_generation.app) +app.add_typer(multimodal_conversation.app) +app.add_typer(multimodal_embedding.app) +app.add_typer(transcription.app) +app.add_typer(speech_synthesis.app) + + +# Records why the Agentic-RL Typer app could not be registered, so the reason +# can be reported when `rl` is actually invoked instead of on every command. +_RL_IMPORT_ERROR: Optional[Exception] = None + +_RL_COMMAND_NAMES = ("rl", "agentic-rl") + + +def _register_rl_app(): + """Lazily import and register the Agentic-RL Typer app. + + Wrapped in a function so that a missing optional dependency + won't crash the entire CLI at import time. + + The failure is recorded rather than reported: `rl` is an optional + extra, so a user running any other command has no reason to be told + about it. ``main()`` surfaces it when the rl command is invoked. + """ + global _RL_IMPORT_ERROR # pylint: disable=global-statement + + try: + from dashscope.legacy_1x.cli.agentic_rl import app as rl_app + + app.add_typer( + rl_app, + name="rl", + help="๐Ÿš€ Agentic RL fine-tuning commands", + ) + app.add_typer( + rl_app, + name="agentic-rl", + help="๐Ÿš€ Agentic RL fine-tuning commands", + hidden=True, + ) + except Exception as exception: # pylint: disable=broad-except + _RL_IMPORT_ERROR = exception + + +def _rl_unavailable_detail(exception: Exception) -> str: + """Render a deferred rl import failure as an actionable message.""" + # Unescaped, rich reads a bracketed run in the exception text as a style + # tag and drops it from the printed output. + detail = escape(str(exception)) + if "pip install" not in detail: + detail += ( + ". Reinstall to pull in the missing dependencies: " + "[bold]pip install -U dashscope[/bold]" + ) + return detail + + +_register_rl_app() + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def _first_non_option(argv): + """Return the first non-option argument in *argv* (skip index 0), + or None if all arguments are options.""" + for arg in argv[1:]: + if not arg.startswith("-"): + return arg + return None + + +def main(): + """Entry point for the ``dashscope`` console script.""" + # Translate legacy command format first so legacy --api_key can be treated + # as the global --api-key option. + argv = _translate_legacy_args(sys.argv) + + # Extract global api-key parameter after legacy argument normalization. + argv = _extract_global_api_key(argv) + + # Extract agent mode flags (--cli / --tui); they belong to the vendored + # acli and must not leak into typer or the one-shot prompt text. + # Bare `dashscope` defaults to the TUI; --cli opts into the plain CLI REPL. + forced_tui = True + if "--cli" in argv: + forced_tui = False + argv = [a for a in argv if a != "--cli"] + if "--tui" in argv: + forced_tui = True + argv = [a for a in argv if a != "--tui"] + + first_cmd = _first_non_option(argv) + + # `rl` is an optional extra: report a missing dependency only when the + # user asks for that command, never on unrelated invocations. Without + # this gate the dispatch below reaches typer, which only knows that `rl` + # is in _TOP_LEVEL_COMMANDS but was never registered โ€” "No such command" + # does not tell the user how to get it. + if _RL_IMPORT_ERROR is not None and first_cmd in _RL_COMMAND_NAMES: + err_console.print( + f"[red]Error:[/red] {_rl_unavailable_detail(_RL_IMPORT_ERROR)}", + ) + sys.exit(1) + + # No arguments at all โ†’ the agent, interactively. -k/--api-key and + # --cli/--tui were stripped above, so `dashscope -k sk-x` lands here too. + if len(argv) == 1: + _route_to_expert(None, tui=forced_tui) + return + + # `dashscope expert ...` โ†’ the agent, explicitly. Handled before the + # --help gate so `dashscope expert --help` describes the agent instead of + # reaching typer, which has no such command. + if first_cmd == _EXPERT_COMMAND: + idx = argv.index(_EXPERT_COMMAND) + expert_args = argv[idx + 1 :] + if "--help" in expert_args or "-h" in expert_args: + console.print(_EXPERT_USAGE) + return + prompt = " ".join(expert_args).strip() + _route_to_expert(prompt or None, tui=forced_tui) + return + + # Top-level --help / -h โ†’ show help and exit normally + if "--help" in argv or "-h" in argv: + argv = _translate_help_shortcut(argv) + sys.argv = argv + app() + return + + # Anything that is not bare `dashscope` and not `dashscope expert ...` + # belongs to typer, so a typo or a stray option gets a real error instead + # of silently opening a chat. Forwarding an unrecognized token as a + # question used to swallow whole command lines -- `dashscope auth whoami` + # opened a session whose first message was the command itself. + argv = _translate_help_shortcut(argv) + sys.argv = argv + + try: + app() + except AuthenticationError as exception: + err_console.print(f"[red]Error:[/red] {exception}") + sys.exit(1) diff --git a/dashscope/cli/__main__.py b/dashscope/legacy_1x/cli/__main__.py similarity index 70% rename from dashscope/cli/__main__.py rename to dashscope/legacy_1x/cli/__main__.py index 9f75714a..f4406ab1 100644 --- a/dashscope/cli/__main__.py +++ b/dashscope/legacy_1x/cli/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable=wrong-import-position -"""Allow ``python -m dashscope.cli`` invocation.""" +"""Allow ``python -m dashscope.legacy_1x.cli`` invocation.""" import warnings # Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL @@ -11,7 +11,7 @@ ) # fmt: off -from dashscope.cli import main # noqa: E402 +from dashscope.legacy_1x.cli import main # noqa: E402 # fmt: on main() diff --git a/dashscope/cli/agentic_rl.py b/dashscope/legacy_1x/cli/agentic_rl.py similarity index 99% rename from dashscope/cli/agentic_rl.py rename to dashscope/legacy_1x/cli/agentic_rl.py index f850f718..f6b8fbdd 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/legacy_1x/cli/agentic_rl.py @@ -17,14 +17,13 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table -from dashscope.finetune.agentic_rl import AgenticRL +from dashscope.finetune.agentic_rl import AgenticRL, SubmitResult from dashscope.finetune.reinforcement import ( serialize_for_output, FunctionType, ) from dashscope.finetune.reinforcement.common.errors import OutputError -from dashscope.finetune.customize_types import FineTune -from dashscope.cli.common import error, normalize_local_path_or_url +from dashscope.legacy_1x.cli.common import error, normalize_local_path_or_url app = typer.Typer( @@ -427,7 +426,7 @@ async def _run_workflow_async( config_path: Optional[str], api_key: Optional[str], run_kwargs: Dict[str, Any], -) -> FineTune: +) -> SubmitResult: """ Execute the RL tuning workflow asynchronously. diff --git a/dashscope/cli/application.py b/dashscope/legacy_1x/cli/application.py similarity index 96% rename from dashscope/cli/application.py rename to dashscope/legacy_1x/cli/application.py index a2ef3e72..3c834619 100644 --- a/dashscope/cli/application.py +++ b/dashscope/legacy_1x/cli/application.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="application", diff --git a/dashscope/cli/auth.py b/dashscope/legacy_1x/cli/auth.py similarity index 91% rename from dashscope/cli/auth.py rename to dashscope/legacy_1x/cli/auth.py index 89aff97c..f2144f8f 100644 --- a/dashscope/cli/auth.py +++ b/dashscope/legacy_1x/cli/auth.py @@ -6,10 +6,15 @@ import typer import dashscope -from dashscope.cli.common import console, err_console -from dashscope.common.api_key import get_default_api_key, save_api_key -from dashscope.common.constants import DEFAULT_DASHSCOPE_API_KEY_FILE_PATH -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.cli.common import console, err_console +from dashscope.legacy_1x.common.api_key import ( + get_default_api_key, + save_api_key, +) +from dashscope.legacy_1x.common.constants import ( + DEFAULT_DASHSCOPE_API_KEY_FILE_PATH, +) +from dashscope.legacy_1x.common.error import AuthenticationError app = typer.Typer( name="auth", diff --git a/dashscope/cli/code_generation.py b/dashscope/legacy_1x/cli/code_generation.py similarity index 93% rename from dashscope/cli/code_generation.py rename to dashscope/legacy_1x/cli/code_generation.py index 414941fd..5366e451 100644 --- a/dashscope/cli/code_generation.py +++ b/dashscope/legacy_1x/cli/code_generation.py @@ -6,11 +6,16 @@ import typer import dashscope -from dashscope.aigc.code_generation import ( +from dashscope.legacy_1x.aigc.code_generation import ( AttachmentRoleMessageParam, UserRoleMessageParam, ) -from dashscope.cli.common import console, ensure_ok, error, handle_sdk_error +from dashscope.legacy_1x.cli.common import ( + console, + ensure_ok, + error, + handle_sdk_error, +) app = typer.Typer( name="code-generation", diff --git a/dashscope/cli/common.py b/dashscope/legacy_1x/cli/common.py similarity index 99% rename from dashscope/cli/common.py rename to dashscope/legacy_1x/cli/common.py index 68f0eea4..b05d0504 100644 --- a/dashscope/cli/common.py +++ b/dashscope/legacy_1x/cli/common.py @@ -10,7 +10,7 @@ import typer from rich.console import Console -logger = logging.getLogger("dashscope.cli") +logger = logging.getLogger("dashscope.legacy_1x.cli") CommandFunction = TypeVar("CommandFunction", bound=Callable) # --------------------------------------------------------------------------- diff --git a/dashscope/cli/deployments.py b/dashscope/legacy_1x/cli/deployments.py similarity index 98% rename from dashscope/cli/deployments.py rename to dashscope/legacy_1x/cli/deployments.py index 6fffe1ec..c3bb664e 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/legacy_1x/cli/deployments.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import DeploymentStatus -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import DeploymentStatus +from dashscope.legacy_1x.cli.common import ( POLL_INTERVAL, console, err_console, diff --git a/dashscope/cli/embeddings.py b/dashscope/legacy_1x/cli/embeddings.py similarity index 96% rename from dashscope/cli/embeddings.py rename to dashscope/legacy_1x/cli/embeddings.py index ae61ccab..392614dc 100644 --- a/dashscope/cli/embeddings.py +++ b/dashscope/legacy_1x/cli/embeddings.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="embeddings", diff --git a/dashscope/cli/files.py b/dashscope/legacy_1x/cli/files.py similarity index 97% rename from dashscope/cli/files.py rename to dashscope/legacy_1x/cli/files.py index cb25a710..8b5f25aa 100644 --- a/dashscope/cli/files.py +++ b/dashscope/legacy_1x/cli/files.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import FilePurpose -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import FilePurpose +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, error, diff --git a/dashscope/cli/fine_tunes.py b/dashscope/legacy_1x/cli/fine_tunes.py similarity index 99% rename from dashscope/cli/fine_tunes.py rename to dashscope/legacy_1x/cli/fine_tunes.py index d5317527..2694b654 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/legacy_1x/cli/fine_tunes.py @@ -7,8 +7,8 @@ import typer import dashscope -from dashscope.common.constants import TaskStatus -from dashscope.cli.common import ( +from dashscope.legacy_1x.common.constants import TaskStatus +from dashscope.legacy_1x.cli.common import ( LOG_PAGE_SIZE, POLL_INTERVAL, console, diff --git a/dashscope/cli/generation.py b/dashscope/legacy_1x/cli/generation.py similarity index 98% rename from dashscope/cli/generation.py rename to dashscope/legacy_1x/cli/generation.py index 2305dae7..9b7e6677 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/legacy_1x/cli/generation.py @@ -7,8 +7,8 @@ import typer -from dashscope.aigc import Generation -from dashscope.cli.common import ( +from dashscope.legacy_1x.aigc import Generation +from dashscope.legacy_1x.cli.common import ( error, extract_text, handle_sdk_error, diff --git a/dashscope/cli/image_generation.py b/dashscope/legacy_1x/cli/image_generation.py similarity index 96% rename from dashscope/cli/image_generation.py rename to dashscope/legacy_1x/cli/image_generation.py index a2efb627..7ae850dc 100644 --- a/dashscope/cli/image_generation.py +++ b/dashscope/legacy_1x/cli/image_generation.py @@ -5,9 +5,9 @@ import typer -from dashscope.aigc.image_generation import ImageGeneration -from dashscope.api_entities.dashscope_response import Message, Role -from dashscope.cli.common import ( +from dashscope.legacy_1x.aigc.image_generation import ImageGeneration +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/image_synthesis.py b/dashscope/legacy_1x/cli/image_synthesis.py similarity index 99% rename from dashscope/cli/image_synthesis.py rename to dashscope/legacy_1x/cli/image_synthesis.py index 63e393cb..37754ca1 100644 --- a/dashscope/cli/image_synthesis.py +++ b/dashscope/legacy_1x/cli/image_synthesis.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/models.py b/dashscope/legacy_1x/cli/models.py similarity index 94% rename from dashscope/cli/models.py rename to dashscope/legacy_1x/cli/models.py index dc0dc747..f4bc96b8 100644 --- a/dashscope/cli/models.py +++ b/dashscope/legacy_1x/cli/models.py @@ -5,7 +5,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="models", diff --git a/dashscope/cli/multimodal_conversation.py b/dashscope/legacy_1x/cli/multimodal_conversation.py similarity index 98% rename from dashscope/cli/multimodal_conversation.py rename to dashscope/legacy_1x/cli/multimodal_conversation.py index a1133993..1031db24 100644 --- a/dashscope/cli/multimodal_conversation.py +++ b/dashscope/legacy_1x/cli/multimodal_conversation.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/multimodal_embedding.py b/dashscope/legacy_1x/cli/multimodal_embedding.py similarity index 98% rename from dashscope/cli/multimodal_embedding.py rename to dashscope/legacy_1x/cli/multimodal_embedding.py index ea4db7b8..c48299e5 100644 --- a/dashscope/cli/multimodal_embedding.py +++ b/dashscope/legacy_1x/cli/multimodal_embedding.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, error, diff --git a/dashscope/cli/oss.py b/dashscope/legacy_1x/cli/oss.py similarity index 92% rename from dashscope/cli/oss.py rename to dashscope/legacy_1x/cli/oss.py index 80bb815b..e6e52bf2 100644 --- a/dashscope/cli/oss.py +++ b/dashscope/legacy_1x/cli/oss.py @@ -6,8 +6,13 @@ import typer import dashscope -from dashscope.utils.oss_utils import OssUtils -from dashscope.cli.common import console, error, handle_sdk_error, success +from dashscope.legacy_1x.utils.oss_utils import OssUtils +from dashscope.legacy_1x.cli.common import ( + console, + error, + handle_sdk_error, + success, +) app = typer.Typer( name="oss", diff --git a/dashscope/cli/rerank.py b/dashscope/legacy_1x/cli/rerank.py similarity index 95% rename from dashscope/cli/rerank.py rename to dashscope/legacy_1x/cli/rerank.py index 07ea0307..f61f4814 100644 --- a/dashscope/cli/rerank.py +++ b/dashscope/legacy_1x/cli/rerank.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="rerank", diff --git a/dashscope/cli/speech_synthesis.py b/dashscope/legacy_1x/cli/speech_synthesis.py similarity index 98% rename from dashscope/cli/speech_synthesis.py rename to dashscope/legacy_1x/cli/speech_synthesis.py index 6978c394..0fba7bd2 100644 --- a/dashscope/cli/speech_synthesis.py +++ b/dashscope/legacy_1x/cli/speech_synthesis.py @@ -7,7 +7,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, error, handle_sdk_error, diff --git a/dashscope/cli/tokenization.py b/dashscope/legacy_1x/cli/tokenization.py similarity index 94% rename from dashscope/cli/tokenization.py rename to dashscope/legacy_1x/cli/tokenization.py index a869d01a..6e212fa7 100644 --- a/dashscope/cli/tokenization.py +++ b/dashscope/legacy_1x/cli/tokenization.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="tokenization", diff --git a/dashscope/cli/transcription.py b/dashscope/legacy_1x/cli/transcription.py similarity index 99% rename from dashscope/cli/transcription.py rename to dashscope/legacy_1x/cli/transcription.py index af25d27e..098098a0 100644 --- a/dashscope/cli/transcription.py +++ b/dashscope/legacy_1x/cli/transcription.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import ( +from dashscope.legacy_1x.cli.common import ( console, ensure_ok, handle_sdk_error, diff --git a/dashscope/cli/understanding.py b/dashscope/legacy_1x/cli/understanding.py similarity index 95% rename from dashscope/cli/understanding.py rename to dashscope/legacy_1x/cli/understanding.py index 30fdfca2..f430e866 100644 --- a/dashscope/cli/understanding.py +++ b/dashscope/legacy_1x/cli/understanding.py @@ -6,7 +6,7 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.legacy_1x.cli.common import console, ensure_ok, handle_sdk_error app = typer.Typer( name="understanding", diff --git a/dashscope/cli/video_synthesis.py b/dashscope/legacy_1x/cli/video_synthesis.py similarity index 98% rename from dashscope/cli/video_synthesis.py rename to dashscope/legacy_1x/cli/video_synthesis.py index 8c46492f..39455a50 100644 --- a/dashscope/cli/video_synthesis.py +++ b/dashscope/legacy_1x/cli/video_synthesis.py @@ -6,7 +6,12 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error, success +from dashscope.legacy_1x.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + success, +) app = typer.Typer( name="video-synthesis", diff --git a/dashscope/finetune/reinforcement/component/server/__init__.py b/dashscope/legacy_1x/client/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/component/server/__init__.py rename to dashscope/legacy_1x/client/__init__.py diff --git a/dashscope/client/base_api.py b/dashscope/legacy_1x/client/base_api.py similarity index 98% rename from dashscope/client/base_api.py rename to dashscope/legacy_1x/client/base_api.py index 8ae18173..deb1ba81 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/legacy_1x/client/base_api.py @@ -10,10 +10,14 @@ import requests import dashscope -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.common.api_key import get_default_api_key -from dashscope.common.constants import ( +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import ( DEFAULT_REQUEST_TIMEOUT_SECONDS, REPEATABLE_STATUS, REQUEST_TIMEOUT_KEYWORD, @@ -21,10 +25,14 @@ TaskStatus, HTTPMethod, ) -from dashscope.common.error import InvalidParameter, InvalidTask, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.env import resolve_base_url -from dashscope.common.utils import ( +from dashscope.legacy_1x.common.error import ( + InvalidParameter, + InvalidTask, + ModelRequired, +) +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.utils import ( _handle_http_failed_response, _handle_http_response, _handle_http_stream_response, @@ -411,7 +419,7 @@ async def list( message="", ) else: - from dashscope.common.utils import ( # pylint: disable=import-outside-toplevel # noqa: E501 + from dashscope.legacy_1x.common.utils import ( # pylint: disable=import-outside-toplevel # noqa: E501 _handle_aiohttp_failed_response, ) diff --git a/dashscope/finetune/reinforcement/examples/observability/__init__.py b/dashscope/legacy_1x/common/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/observability/__init__.py rename to dashscope/legacy_1x/common/__init__.py diff --git a/dashscope/legacy_1x/common/_sdk_session.py b/dashscope/legacy_1x/common/_sdk_session.py new file mode 100644 index 00000000..148e269d --- /dev/null +++ b/dashscope/legacy_1x/common/_sdk_session.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility re-export; the implementation moved to ``dashscope._shared``. + +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 dashscope._shared._sdk_session import sdk_session_id + +__all__ = ["sdk_session_id"] diff --git a/dashscope/common/api_key.py b/dashscope/legacy_1x/common/api_key.py similarity index 95% rename from dashscope/common/api_key.py rename to dashscope/legacy_1x/common/api_key.py index d083e849..74bc785f 100644 --- a/dashscope/common/api_key.py +++ b/dashscope/legacy_1x/common/api_key.py @@ -5,11 +5,11 @@ from typing import Optional import dashscope -from dashscope.common.constants import ( +from dashscope.legacy_1x.common.constants import ( DEFAULT_DASHSCOPE_API_KEY_FILE_PATH, DEFAULT_DASHSCOPE_CACHE_PATH, ) -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.common.error import AuthenticationError def get_default_api_key(): diff --git a/dashscope/common/base_type.py b/dashscope/legacy_1x/common/base_type.py similarity index 100% rename from dashscope/common/base_type.py rename to dashscope/legacy_1x/common/base_type.py diff --git a/dashscope/common/constants.py b/dashscope/legacy_1x/common/constants.py similarity index 100% rename from dashscope/common/constants.py rename to dashscope/legacy_1x/common/constants.py diff --git a/dashscope/common/env.py b/dashscope/legacy_1x/common/env.py similarity index 62% rename from dashscope/common/env.py rename to dashscope/legacy_1x/common/env.py index 330baca9..df8afa0c 100644 --- a/dashscope/common/env.py +++ b/dashscope/legacy_1x/common/env.py @@ -2,64 +2,21 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import os -import re -from dashscope.common.constants import ( +# Re-exported for callers that import them from here; ``validate_region`` +# is not used in this module itself. +# pylint: disable-next=unused-import +from dashscope._shared._validation import ( # noqa: F401 + validate_region, + validate_workspace_id, +) +from dashscope.legacy_1x.common.constants import ( DASHSCOPE_API_KEY_ENV, DASHSCOPE_API_KEY_FILE_PATH_ENV, DASHSCOPE_API_REGION_ENV, DASHSCOPE_API_VERSION_ENV, ) -from dashscope.common.error import InputRequired - -# workspace_id and region become DNS labels of the MaaS endpoint host, so -# they must not contain characters (/, ?, #, @, :, whitespace, ...) that -# could break out of the host and redirect requests elsewhere. -# \A...\Z rather than ^...$: $ also matches before a trailing newline. -_WORKSPACE_ID_PATTERN = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\Z") -# RFC 1123 hostname label: 1-63 chars, no leading/trailing hyphen. Upper -# case is allowed because DNS is case-insensitive and httpx lowercases the -# host, so rejecting it would break working callers for no security gain. -_REGION_PATTERN = re.compile( - r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z", -) - - -def validate_workspace_id(workspace_id): - """Validate that workspace_id is safe to use as a URL subdomain. - - Raises: - ValueError: If workspace_id contains characters outside - [A-Za-z0-9_-], is empty, or exceeds 64 characters. - """ - if not workspace_id or not _WORKSPACE_ID_PATTERN.fullmatch( - workspace_id, - ): - raise ValueError( - f"Invalid workspace_id {workspace_id!r}: must match " - "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$", - ) - return workspace_id - - -def validate_region(region): - """Validate that region is safe to use as a URL subdomain. - - Deliberately a hostname-label check and not a MAAS_REGIONS whitelist: - rejecting an unknown-but-well-formed region would break callers as soon - as a subpackage supports a region this list has not caught up with. - - Raises: - ValueError: If region is not a valid hostname label. - """ - if not region or not _REGION_PATTERN.fullmatch(region): - raise ValueError( - f"Invalid region {region!r}: must be a hostname label " - "(1-63 chars of [A-Za-z0-9-], not starting or ending " - "with '-')", - ) - return region - +from dashscope.legacy_1x.common.error import InputRequired # MaaS regions: region -> URL subdomain identifier # cn-beijing is deliberately excluded: it is the default diff --git a/dashscope/common/error.py b/dashscope/legacy_1x/common/error.py similarity index 92% rename from dashscope/common/error.py rename to dashscope/legacy_1x/common/error.py index 2b49cc71..73efc01e 100644 --- a/dashscope/common/error.py +++ b/dashscope/legacy_1x/common/error.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. - -class DashScopeException(Exception): - pass +# The base class lives in the neutral layer so the AgentStudio sub-SDK +# (owned by the 2.0 package) and the 1.x classes below inherit from one +# and the same object. +from dashscope._shared._errors import DashScopeException class AuthenticationError(DashScopeException): diff --git a/dashscope/common/logging.py b/dashscope/legacy_1x/common/logging.py similarity index 93% rename from dashscope/common/logging.py rename to dashscope/legacy_1x/common/logging.py index a5ca4abb..f345e053 100644 --- a/dashscope/common/logging.py +++ b/dashscope/legacy_1x/common/logging.py @@ -4,7 +4,7 @@ import logging import os -from dashscope.common.constants import DASHSCOPE_LOGGING_LEVEL_ENV +from dashscope.legacy_1x.common.constants import DASHSCOPE_LOGGING_LEVEL_ENV logger = logging.getLogger("dashscope") diff --git a/dashscope/common/message_manager.py b/dashscope/legacy_1x/common/message_manager.py similarity index 95% rename from dashscope/common/message_manager.py rename to dashscope/legacy_1x/common/message_manager.py index 9b546b9e..1fc34617 100644 --- a/dashscope/common/message_manager.py +++ b/dashscope/legacy_1x/common/message_manager.py @@ -4,7 +4,7 @@ from collections import deque from typing import List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( ConversationResponse, GenerationResponse, Message, diff --git a/dashscope/common/utils.py b/dashscope/legacy_1x/common/utils.py similarity index 88% rename from dashscope/common/utils.py rename to dashscope/legacy_1x/common/utils.py index bb9229ba..732e0971 100644 --- a/dashscope/common/utils.py +++ b/dashscope/legacy_1x/common/utils.py @@ -4,7 +4,6 @@ import asyncio import json import os -import platform import queue import threading from dataclasses import dataclass @@ -15,17 +14,23 @@ import aiohttp import requests -from dashscope.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; -# acli deliberately keeps its own, it is a one-way sync copy). -_SDK_SESSION_ID = sdk_session_id() +# The x-dashscope-sdk-client state (including the CLI's ``python-cli`` +# override), the request-identity builders and the session id live in the +# neutral layer so both stacks share one copy; re-exported here for +# callers that import them from this module. +# pylint: disable-next=unused-import +from dashscope._shared._sdk_ident import ( # noqa: F401 + _SDK_SESSION_ID, + get_sdk_headers, + get_user_agent, + set_sdk_client, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.common.api_key import get_default_api_key +from dashscope.legacy_1x.common.constants import SSE_CONTENT_TYPE +from dashscope.legacy_1x.common.logging import logger def is_validate_fine_tune_file(file_path): @@ -143,31 +148,6 @@ def async_to_sync(async_generator): yield message -def get_user_agent(): - try: - platform_ = ( - platform.platform().replace("\n", "").replace("\r", "").strip() - ) - except Exception: - platform_ = "unknown" - - try: - processor_ = ( - platform.processor().replace("\n", "").replace("\r", "").strip() - ) - except Exception: - processor_ = "unknown" - - ua = ( - f"dashscope/{__version__}; python/{platform.python_version()}; " - f"platform/{platform_}; " - f"processor/{processor_}" - ) - return ua - - -_SDK_CLIENT = "python-sdk" - # Package name -> sdk module tag, for packages whose directory name does # not match the module segment (threads/messages/runs/steps belong to the # assistants module). @@ -179,46 +159,21 @@ def get_user_agent(): def get_api_module(module_name: str) -> str: """Derive the sdk module tag from an API class's __module__. - e.g. "dashscope.aigc.generation" -> "aigc", - "dashscope.threads.runs.runs" -> "assistants". + e.g. "dashscope.legacy_1x.aigc.generation" -> "aigc", + "dashscope.legacy_1x.threads.runs.runs" -> "assistants". """ parts = module_name.split(".") if len(parts) < 2 or parts[0] != "dashscope": return "" - pkg = parts[1] + # The 1.x tree now lives under ``dashscope.legacy_1x``; the logical API + # package is the segment after that container, not ``legacy_1x`` itself. + idx = 2 if parts[1] == "legacy_1x" else 1 + if len(parts) <= idx: + return "" + pkg = parts[idx] return _SDK_MODULE_PACKAGE_OVERRIDES.get(pkg, pkg) -def set_sdk_client(client: str) -> None: - """Override the client identifier in the x-dashscope-sdk-client header. - - Process-wide, used by entry points that embed the SDK (e.g. the - ``dashscope`` CLI marks itself as ``python-cli``). - """ - global _SDK_CLIENT # pylint: disable=global-statement - if client: - _SDK_CLIENT = client - - -def get_sdk_headers(module: str = "") -> Dict[str, str]: - """Structured SDK identification headers for backend statistics. - - Set DASHSCOPE_DISABLE_SDK_HEADERS=1 to omit them. - - Args: - module: Optional module name (e.g. agentstudio, rl, app). - """ - if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): - return {} - parts = [_SDK_CLIENT, __version__] - if module: - parts.append(module) - return { - "x-dashscope-sdk-client": "/".join(parts), - "x-dashscope-sdk-session-id": _SDK_SESSION_ID, - } - - def default_headers(api_key: str = None, module: str = "") -> Dict[str, str]: ua = get_user_agent() headers = {"user-agent": ua} diff --git a/dashscope/legacy_1x/embeddings/__init__.py b/dashscope/legacy_1x/embeddings/__init__.py new file mode 100644 index 00000000..998f1a1f --- /dev/null +++ b/dashscope/legacy_1x/embeddings/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .batch_text_embedding import BatchTextEmbedding +from .batch_text_embedding_response import BatchTextEmbeddingResponse +from .text_embedding import TextEmbedding + +__all__ = [ + "TextEmbedding", + "BatchTextEmbedding", + "BatchTextEmbeddingResponse", +] diff --git a/dashscope/embeddings/batch_text_embedding.py b/dashscope/legacy_1x/embeddings/batch_text_embedding.py similarity index 95% rename from dashscope/embeddings/batch_text_embedding.py rename to dashscope/legacy_1x/embeddings/batch_text_embedding.py index c2c9b9cb..d2ef1832 100644 --- a/dashscope/embeddings/batch_text_embedding.py +++ b/dashscope/legacy_1x/embeddings/batch_text_embedding.py @@ -3,11 +3,13 @@ from typing import Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.error import InputRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.embeddings.batch_text_embedding_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.embeddings.batch_text_embedding_response import ( BatchTextEmbeddingResponse, ) diff --git a/dashscope/embeddings/batch_text_embedding_response.py b/dashscope/legacy_1x/embeddings/batch_text_embedding_response.py similarity index 96% rename from dashscope/embeddings/batch_text_embedding_response.py rename to dashscope/legacy_1x/embeddings/batch_text_embedding_response.py index 405ee102..762653d9 100644 --- a/dashscope/embeddings/batch_text_embedding_response.py +++ b/dashscope/legacy_1x/embeddings/batch_text_embedding_response.py @@ -5,7 +5,7 @@ from attr import dataclass -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) diff --git a/dashscope/embeddings/multimodal_embedding.py b/dashscope/legacy_1x/embeddings/multimodal_embedding.py similarity index 96% rename from dashscope/embeddings/multimodal_embedding.py rename to dashscope/legacy_1x/embeddings/multimodal_embedding.py index d68c13f5..846a31bd 100644 --- a/dashscope/embeddings/multimodal_embedding.py +++ b/dashscope/legacy_1x/embeddings/multimodal_embedding.py @@ -4,14 +4,14 @@ from dataclasses import dataclass from typing import List -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, DictMixin, ) -from dashscope.client.base_api import BaseApi, BaseAioApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task -from dashscope.utils.oss_utils import preprocess_message_element +from dashscope.legacy_1x.client.base_api import BaseApi, BaseAioApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.utils.oss_utils import preprocess_message_element @dataclass(init=False) diff --git a/dashscope/embeddings/text_embedding.py b/dashscope/legacy_1x/embeddings/text_embedding.py similarity index 90% rename from dashscope/embeddings/text_embedding.py rename to dashscope/legacy_1x/embeddings/text_embedding.py index b388bad2..9835e194 100644 --- a/dashscope/embeddings/text_embedding.py +++ b/dashscope/legacy_1x/embeddings/text_embedding.py @@ -3,10 +3,12 @@ from typing import List, Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import TEXT_EMBEDDING_INPUT_KEY -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import TEXT_EMBEDDING_INPUT_KEY +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class TextEmbedding(BaseApi): diff --git a/dashscope/legacy_1x/files.py b/dashscope/legacy_1x/files.py new file mode 100644 index 00000000..c3e908c8 --- /dev/null +++ b/dashscope/legacy_1x/files.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import os +from typing import Optional + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import ( + DeleteMixin, + FileUploadMixin, + GetMixin, + ListMixin, +) +from dashscope.legacy_1x.common.constants import FilePurpose +from dashscope.legacy_1x.common.error import InvalidFileFormat +from dashscope.legacy_1x.common.utils import is_validate_fine_tune_file + + +class Files(FileUploadMixin, ListMixin, DeleteMixin, GetMixin): + SUB_PATH = "files" + + @classmethod + def upload( # pylint: disable=arguments-renamed + cls, + file_path: str, # type: ignore[override] + purpose: str = FilePurpose.fine_tune, # type: ignore[override] + description: Optional[str] = None, # type: ignore[override] + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Upload file for model fine-tune or other tasks. + + Args: + file_path (str): The local file name to upload. + purpose (str): The purpose of the file[fine-tune|inference] + description (str, optional): The file description message. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The upload information + """ + if purpose == FilePurpose.fine_tune: + if not is_validate_fine_tune_file(file_path): + raise InvalidFileFormat( + f"The file {file_path} is not in valid jsonl format", + ) + with open(file_path, "rb") as f: + return super().upload( # type: ignore[return-value] + files=[ + ( + "files", + ( + os.path.basename(f.name), + f, + None, + ), + ), + ], + descriptions=[description] if description is not None else [], + api_key=api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List uploaded files. + + Args: + api_key (str, optional): + The api api_key, can be None, + if None, will get by default rule(TODO: api key doc). + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The fine-tune jobs in the result. + """ + return super().list( # type: ignore[return-value] + page, + page_size, + api_key, + workspace=workspace, + **kwargs, + ) + + @classmethod + def get( # type: ignore[override] + cls, + file_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the file info. + + Args: + file_id (str): The file id. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The job info + """ + # type: ignore + return super().get(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 + + @classmethod + def delete( # type: ignore[override] + cls, + file_id: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Delete uploaded file. + + Args: + file_id (str): The file id want to delete. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: Delete result. + """ # type: ignore + return super().delete(file_id, api_key, workspace=workspace, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/legacy_1x/io/__init__.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/__init__.py rename to dashscope/legacy_1x/io/__init__.py diff --git a/dashscope/io/input_output.py b/dashscope/legacy_1x/io/input_output.py similarity index 98% rename from dashscope/io/input_output.py rename to dashscope/legacy_1x/io/input_output.py index ea6caf27..688a5916 100644 --- a/dashscope/io/input_output.py +++ b/dashscope/legacy_1x/io/input_output.py @@ -5,7 +5,7 @@ import io from typing import Generator -from dashscope.common.error import UnsupportedDataType +from dashscope.legacy_1x.common.error import UnsupportedDataType class InputResolver: diff --git a/dashscope/legacy_1x/model.py b/dashscope/legacy_1x/model.py new file mode 100644 index 00000000..2e5a2bdf --- /dev/null +++ b/dashscope/legacy_1x/model.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin, ListMixin + + +class Model(ListMixin, GetMixin): + SUB_PATH = "models" + + @classmethod + def get( # type: ignore[override] + cls, + name: str, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the model information. + + Args: + name (str): The model name. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The model information. + """ + # type: ignore + return super().get( # type: ignore[return-value] + name, + api_key, + workspace=workspace, + **kwargs, + ) # noqa: E501 + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + workspace: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List models. + + Args: + api_key (str, optional): The api key + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + + Returns: + DashScopeAPIResponse: The models. + """ + return super().list( # type: ignore[return-value] + api_key, + page, + page_size, + workspace=workspace, + **kwargs, + ) diff --git a/dashscope/legacy_1x/models.py b/dashscope/legacy_1x/models.py new file mode 100644 index 00000000..300a4931 --- /dev/null +++ b/dashscope/legacy_1x/models.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin, ListMixin, _get +from dashscope.legacy_1x.common.utils import get_api_module, join_url +import dashscope + + +class Models(ListMixin, GetMixin): + SUB_PATH = "models" + + @classmethod + def get( # type: ignore[override] + cls, + name: str, + api_key: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """Get the model information. + + Args: + name (str): The model name. + api_key (str, optional): The api key. Defaults to None. + workspace (str): The dashscope workspace id. + + Returns: + DashScopeAPIResponse: The model information. + """ + from http import HTTPStatus + + # Use query parameter to filter by model name on server side + # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 + url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) + params = {"model": name, "page_no": 1, "page_size": 1} + + response = _get( + url, + params=params, + api_key=api_key, + module=get_api_module(cls.__module__), + **kwargs, + ) + + if response.status_code != HTTPStatus.OK: + return response # type: ignore[return-value] + + output = response.output + if not output or "models" not in output or not output["models"]: + response.status_code = 404 + response.message = f"Model '{name}' not found" + response.output = None + return response # type: ignore[return-value] + + # Return the first (and only) model from the filtered list + response.output = output["models"][0] + return response # type: ignore[return-value] + + @classmethod + def list( # type: ignore[override] + cls, + page=1, + page_size=10, + api_key: str = None, + **kwargs, + ) -> DashScopeAPIResponse: + """List models. + + Args: + api_key (str, optional): The api key + page (int, optional): Page number. Defaults to 1. + page_size (int, optional): Items per page. Defaults to 10. + + Returns: + DashScopeAPIResponse: The models. + """ + # type: ignore + return super().list(page, page_size, api_key=api_key, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 diff --git a/dashscope/legacy_1x/multimodal/__init__.py b/dashscope/legacy_1x/multimodal/__init__.py new file mode 100644 index 00000000..540bb57a --- /dev/null +++ b/dashscope/legacy_1x/multimodal/__init__.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from .tingwu import tingwu +from .tingwu.tingwu import TingWu +from .tingwu.tingwu_realtime import TingWuRealtime, TingWuRealtimeCallback + +from .multimodal_dialog import MultiModalDialog, MultiModalCallback +from .dialog_state import DialogState +from .multimodal_constants import ( + RequestToRespondType, + RESPONSE_NAME_TASK_STARTED, + RESPONSE_NAME_RESULT_GENERATED, + RESPONSE_NAME_TASK_FINISHED, + RESPONSE_NAME_TASK_FAILED, + RESPONSE_NAME_STARTED, + RESPONSE_NAME_STOPPED, + RESPONSE_NAME_STATE_CHANGED, + RESPONSE_NAME_REQUEST_ACCEPTED, + RESPONSE_NAME_SPEECH_STARTED, + RESPONSE_NAME_SPEECH_ENDED, + RESPONSE_NAME_RESPONDING_STARTED, + RESPONSE_NAME_RESPONDING_ENDED, + RESPONSE_NAME_SPEECH_CONTENT, + RESPONSE_NAME_RESPONDING_CONTENT, + RESPONSE_NAME_ERROR, + RESPONSE_NAME_HEART_BEAT, +) +from .multimodal_request_params import ( + DashHeader, + DashPayloadParameters, + DashPayloadInput, + DashPayload, + RequestBodyInput, + AsrPostProcessing, + ReplaceWord, + Upstream, + Downstream, + DialogAttributes, + Locations, + Network, + Device, + ClientInfo, + BizParams, + RequestParameters, + RequestToRespondParameters, + RequestToRespondBodyInput, + get_random_uuid, +) + +__all__ = [ + "tingwu", + "TingWu", + "TingWuRealtime", + "TingWuRealtimeCallback", + "MultiModalDialog", + "MultiModalCallback", + "DialogState", + "RequestToRespondType", + "RESPONSE_NAME_TASK_STARTED", + "RESPONSE_NAME_RESULT_GENERATED", + "RESPONSE_NAME_TASK_FINISHED", + "RESPONSE_NAME_TASK_FAILED", + "RESPONSE_NAME_STARTED", + "RESPONSE_NAME_STOPPED", + "RESPONSE_NAME_STATE_CHANGED", + "RESPONSE_NAME_REQUEST_ACCEPTED", + "RESPONSE_NAME_SPEECH_STARTED", + "RESPONSE_NAME_SPEECH_ENDED", + "RESPONSE_NAME_RESPONDING_STARTED", + "RESPONSE_NAME_RESPONDING_ENDED", + "RESPONSE_NAME_SPEECH_CONTENT", + "RESPONSE_NAME_RESPONDING_CONTENT", + "RESPONSE_NAME_ERROR", + "RESPONSE_NAME_HEART_BEAT", + "DashHeader", + "DashPayloadParameters", + "DashPayloadInput", + "DashPayload", + "RequestBodyInput", + "AsrPostProcessing", + "ReplaceWord", + "Upstream", + "Downstream", + "DialogAttributes", + "Locations", + "Network", + "Device", + "ClientInfo", + "BizParams", + "RequestParameters", + "RequestToRespondParameters", + "RequestToRespondBodyInput", + "get_random_uuid", +] diff --git a/dashscope/multimodal/dialog_state.py b/dashscope/legacy_1x/multimodal/dialog_state.py similarity index 100% rename from dashscope/multimodal/dialog_state.py rename to dashscope/legacy_1x/multimodal/dialog_state.py diff --git a/dashscope/multimodal/multimodal_constants.py b/dashscope/legacy_1x/multimodal/multimodal_constants.py similarity index 100% rename from dashscope/multimodal/multimodal_constants.py rename to dashscope/legacy_1x/multimodal/multimodal_constants.py diff --git a/dashscope/multimodal/multimodal_dialog.py b/dashscope/legacy_1x/multimodal/multimodal_dialog.py similarity index 98% rename from dashscope/multimodal/multimodal_dialog.py rename to dashscope/legacy_1x/multimodal/multimodal_dialog.py index 63a04f84..1b563c52 100644 --- a/dashscope/multimodal/multimodal_dialog.py +++ b/dashscope/legacy_1x/multimodal/multimodal_dialog.py @@ -7,12 +7,12 @@ import websocket import dashscope -from dashscope.common.env import resolve_base_url -from dashscope.common.logging import logger -from dashscope.common.error import InputRequired -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.multimodal import dialog_state -from dashscope.multimodal.multimodal_constants import ( +from dashscope.legacy_1x.common.env import resolve_base_url +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.multimodal import dialog_state +from dashscope.legacy_1x.multimodal.multimodal_constants import ( RESPONSE_NAME_STARTED, RESPONSE_NAME_STOPPED, RESPONSE_NAME_STATE_CHANGED, @@ -26,7 +26,7 @@ RESPONSE_NAME_ERROR, RESPONSE_NAME_HEART_BEAT, ) -from dashscope.multimodal.multimodal_request_params import ( +from dashscope.legacy_1x.multimodal.multimodal_request_params import ( RequestParameters, get_random_uuid, DashHeader, @@ -35,7 +35,7 @@ RequestToRespondParameters, RequestToRespondBodyInput, ) -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.protocol.websocket import ActionType class MultiModalCallback: diff --git a/dashscope/multimodal/multimodal_request_params.py b/dashscope/legacy_1x/multimodal/multimodal_request_params.py similarity index 100% rename from dashscope/multimodal/multimodal_request_params.py rename to dashscope/legacy_1x/multimodal/multimodal_request_params.py diff --git a/dashscope/multimodal/tingwu/__init__.py b/dashscope/legacy_1x/multimodal/tingwu/__init__.py similarity index 100% rename from dashscope/multimodal/tingwu/__init__.py rename to dashscope/legacy_1x/multimodal/tingwu/__init__.py diff --git a/dashscope/multimodal/tingwu/tingwu.py b/dashscope/legacy_1x/multimodal/tingwu/tingwu.py similarity index 88% rename from dashscope/multimodal/tingwu/tingwu.py rename to dashscope/legacy_1x/multimodal/tingwu/tingwu.py index fbceeb77..bf62aada 100644 --- a/dashscope/multimodal/tingwu/tingwu.py +++ b/dashscope/legacy_1x/multimodal/tingwu/tingwu.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- from typing import Dict, Any -from dashscope.api_entities.api_request_factory import _build_api_request -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import ModelRequired +from dashscope.legacy_1x.api_entities.api_request_factory import ( + _build_api_request, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import ModelRequired class TingWu(BaseApi): diff --git a/dashscope/multimodal/tingwu/tingwu_realtime.py b/dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py similarity index 98% rename from dashscope/multimodal/tingwu/tingwu_realtime.py rename to dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py index 899c09e9..577a8658 100644 --- a/dashscope/multimodal/tingwu/tingwu_realtime.py +++ b/dashscope/legacy_1x/multimodal/tingwu/tingwu_realtime.py @@ -8,15 +8,15 @@ from dataclasses import dataclass, field from queue import Queue import dashscope -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InvalidParameter, ModelRequired -from dashscope.common.env import resolve_base_url +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InvalidParameter, ModelRequired +from dashscope.legacy_1x.common.env import resolve_base_url import websocket # pylint: disable=wrong-import-order # pylint: disable=ungrouped-imports -from dashscope.common.logging import logger -from dashscope.common.utils import get_sdk_headers, get_user_agent -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_sdk_headers, get_user_agent +from dashscope.legacy_1x.protocol.websocket import ActionType class TingWuRealtimeCallback: diff --git a/dashscope/legacy_1x/nlp/__init__.py b/dashscope/legacy_1x/nlp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/nlp/understanding.py b/dashscope/legacy_1x/nlp/understanding.py similarity index 89% rename from dashscope/nlp/understanding.py rename to dashscope/legacy_1x/nlp/understanding.py index e6be2944..b42db4a3 100644 --- a/dashscope/nlp/understanding.py +++ b/dashscope/legacy_1x/nlp/understanding.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import _get_task_group_and_task class Understanding(BaseApi): diff --git a/dashscope/legacy_1x/protocol/__init__.py b/dashscope/legacy_1x/protocol/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/protocol/websocket.py b/dashscope/legacy_1x/protocol/websocket.py similarity index 100% rename from dashscope/protocol/websocket.py rename to dashscope/legacy_1x/protocol/websocket.py diff --git a/dashscope/legacy_1x/rerank/__init__.py b/dashscope/legacy_1x/rerank/__init__.py new file mode 100644 index 00000000..a271a68e --- /dev/null +++ b/dashscope/legacy_1x/rerank/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.legacy_1x.rerank.text_rerank import AioTextReRank, TextReRank + +__all__ = ["AioTextReRank", "TextReRank"] diff --git a/dashscope/rerank/text_rerank.py b/dashscope/legacy_1x/rerank/text_rerank.py similarity index 94% rename from dashscope/rerank/text_rerank.py rename to dashscope/legacy_1x/rerank/text_rerank.py index 960a8357..b3990562 100644 --- a/dashscope/rerank/text_rerank.py +++ b/dashscope/legacy_1x/rerank/text_rerank.py @@ -3,10 +3,10 @@ from typing import Any, Dict, List, Tuple -from dashscope.api_entities.dashscope_response import ReRankResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.utils import _get_task_group_and_task +from dashscope.legacy_1x.api_entities.dashscope_response import ReRankResponse +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.utils import _get_task_group_and_task __all__ = ["TextReRank", "AioTextReRank"] diff --git a/dashscope/legacy_1x/threads/__init__.py b/dashscope/legacy_1x/threads/__init__.py new file mode 100644 index 00000000..0249d6c8 --- /dev/null +++ b/dashscope/legacy_1x/threads/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +# yapf: disable + +from dashscope.legacy_1x.threads.messages.messages import Messages +from dashscope.legacy_1x.threads.runs.runs import Runs +from dashscope.legacy_1x.threads.runs.steps import Steps +from dashscope.legacy_1x.threads.thread_types import ( + MessageFile, Run, RunList, RunStep, + RunStepList, Thread, ThreadMessage, + ThreadMessageList, +) +from dashscope.legacy_1x.threads.threads import Threads + +__all__ = [ + 'MessageFile', + 'Messages', + 'Run', + 'Runs', + 'RunList', + 'Steps', + 'RunStep', + 'RunStepList', + 'Threads', + 'Thread', + 'ThreadMessage', + 'ThreadMessageList', +] diff --git a/dashscope/legacy_1x/threads/messages/__init__.py b/dashscope/legacy_1x/threads/messages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/threads/messages/files.py b/dashscope/legacy_1x/threads/messages/files.py similarity index 94% rename from dashscope/threads/messages/files.py rename to dashscope/legacy_1x/threads/messages/files.py index 81e5c00c..54488dbb 100644 --- a/dashscope/threads/messages/files.py +++ b/dashscope/legacy_1x/threads/messages/files.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.client.base_api import GetStatusMixin, ListObjectMixin -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import MessageFile, MessageFileList +from dashscope.legacy_1x.client.base_api import GetStatusMixin, ListObjectMixin +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import ( + MessageFile, + MessageFileList, +) __all__ = ["Files"] diff --git a/dashscope/threads/messages/messages.py b/dashscope/legacy_1x/threads/messages/messages.py similarity index 96% rename from dashscope/threads/messages/messages.py rename to dashscope/legacy_1x/threads/messages/messages.py index 10953af4..8f2f2f0a 100644 --- a/dashscope/threads/messages/messages.py +++ b/dashscope/legacy_1x/threads/messages/messages.py @@ -4,20 +4,23 @@ import warnings from typing import Dict, List, Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CreateMixin, GetStatusMixin, ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import ThreadMessage, ThreadMessageList +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import ( + ThreadMessage, + ThreadMessageList, +) __all__ = ["Messages"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/threads/runs/__init__.py b/dashscope/legacy_1x/threads/runs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/threads/runs/runs.py b/dashscope/legacy_1x/threads/runs/runs.py similarity index 98% rename from dashscope/threads/runs/runs.py rename to dashscope/legacy_1x/threads/runs/runs.py index 756d1ed9..904836ad 100644 --- a/dashscope/threads/runs/runs.py +++ b/dashscope/legacy_1x/threads/runs/runs.py @@ -6,20 +6,20 @@ from http import HTTPStatus from typing import Dict, List, Optional -from dashscope.client.base_api import ( +from dashscope.legacy_1x.client.base_api import ( CancelMixin, CreateMixin, GetStatusMixin, ListObjectMixin, UpdateMixin, ) -from dashscope.common.error import ( +from dashscope.legacy_1x.common.error import ( AssistantError, InputRequired, TimeoutException, ) -from dashscope.common.logging import logger -from dashscope.threads.thread_types import ( +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.threads.thread_types import ( Run, RunList, RunStep, @@ -33,7 +33,7 @@ # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/threads/runs/steps.py b/dashscope/legacy_1x/threads/runs/steps.py similarity index 93% rename from dashscope/threads/runs/steps.py rename to dashscope/legacy_1x/threads/runs/steps.py index e7e0c86a..2dcc80d9 100644 --- a/dashscope/threads/runs/steps.py +++ b/dashscope/legacy_1x/threads/runs/steps.py @@ -3,15 +3,15 @@ import warnings -from dashscope.client.base_api import GetStatusMixin, ListObjectMixin -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import RunStep, RunStepList +from dashscope.legacy_1x.client.base_api import GetStatusMixin, ListObjectMixin +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import RunStep, RunStepList __all__ = ["Steps"] # Deprecation warning message _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/threads/thread_types.py b/dashscope/legacy_1x/threads/thread_types.py similarity index 99% rename from dashscope/threads/thread_types.py rename to dashscope/legacy_1x/threads/thread_types.py index b47ff070..aa552416 100644 --- a/dashscope/threads/thread_types.py +++ b/dashscope/legacy_1x/threads/thread_types.py @@ -6,10 +6,10 @@ from dataclasses import dataclass from typing import Dict, List, Literal, Optional, Union -from dashscope.assistants.assistant_types import ( +from dashscope.legacy_1x.assistants.assistant_types import ( Tool, convert_tools_dict_to_objects, ) -from dashscope.common.base_type import BaseList, BaseObjectMixin +from dashscope.legacy_1x.common.base_type import BaseList, BaseObjectMixin __all__ = [ 'MessageFile', 'MessageFileList', 'Usage', 'ImageFile', diff --git a/dashscope/threads/threads.py b/dashscope/legacy_1x/threads/threads.py similarity index 96% rename from dashscope/threads/threads.py rename to dashscope/legacy_1x/threads/threads.py index e4effe33..788a4e57 100644 --- a/dashscope/threads/threads.py +++ b/dashscope/legacy_1x/threads/threads.py @@ -4,20 +4,20 @@ import warnings from typing import Dict, List, Optional -from dashscope.assistants.assistant_types import DeleteResponse -from dashscope.client.base_api import ( +from dashscope.legacy_1x.assistants.assistant_types import DeleteResponse +from dashscope.legacy_1x.client.base_api import ( CreateMixin, DeleteMixin, GetStatusMixin, UpdateMixin, ) -from dashscope.common.error import InputRequired -from dashscope.threads.thread_types import Run, Thread +from dashscope.legacy_1x.common.error import InputRequired +from dashscope.legacy_1x.threads.thread_types import Run, Thread __all__ = ["Threads"] _DEPRECATION_MSG = ( - "The Assistants API (dashscope.threads) is deprecated and will be " + "The Assistants API (dashscope.legacy_1x.threads) is deprecated and will be " "removed in a future release. Please migrate to the Responses API. " "See https://help.aliyun.com/zh/model-studio/" "synchronous-call-api-reference for migration details." diff --git a/dashscope/legacy_1x/utils/__init__.py b/dashscope/legacy_1x/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/utils/message_utils.py b/dashscope/legacy_1x/utils/message_utils.py similarity index 100% rename from dashscope/utils/message_utils.py rename to dashscope/legacy_1x/utils/message_utils.py diff --git a/dashscope/utils/oss_utils.py b/dashscope/legacy_1x/utils/oss_utils.py similarity index 96% rename from dashscope/utils/oss_utils.py rename to dashscope/legacy_1x/utils/oss_utils.py index 95187dff..65410244 100644 --- a/dashscope/utils/oss_utils.py +++ b/dashscope/legacy_1x/utils/oss_utils.py @@ -11,12 +11,14 @@ import requests -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin -from dashscope.common.constants import FILE_PATH_SCHEMA -from dashscope.common.error import InvalidInput, UploadFileException -from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import GetMixin +from dashscope.legacy_1x.common.constants import FILE_PATH_SCHEMA +from dashscope.legacy_1x.common.error import InvalidInput, UploadFileException +from dashscope.legacy_1x.common.logging import logger +from dashscope.legacy_1x.common.utils import get_user_agent class OssUtils(GetMixin): diff --git a/dashscope/utils/param_utils.py b/dashscope/legacy_1x/utils/param_utils.py similarity index 100% rename from dashscope/utils/param_utils.py rename to dashscope/legacy_1x/utils/param_utils.py diff --git a/dashscope/model.py b/dashscope/model.py index 6aea734d..b979ec12 100644 --- a/dashscope/model.py +++ b/dashscope/model.py @@ -1,62 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.model``. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin, ListMixin +Import from ``dashscope.legacy_1x.model`` (or the public ``dashscope`` +root re-exports) in new code. +""" +from dashscope.legacy_1x._shim import install as _install -class Model(ListMixin, GetMixin): - SUB_PATH = "models" - - @classmethod - def get( # type: ignore[override] - cls, - name: str, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the model information. - - Args: - name (str): The model name. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The model information. - """ - # type: ignore - return super().get( # type: ignore[return-value] - name, - api_key, - workspace=workspace, - **kwargs, - ) # noqa: E501 - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - workspace: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List models. - - Args: - api_key (str, optional): The api key - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - - Returns: - DashScopeAPIResponse: The models. - """ - return super().list( # type: ignore[return-value] - api_key, - page, - page_size, - workspace=workspace, - **kwargs, - ) +_install(__name__, "dashscope.legacy_1x.model") diff --git a/dashscope/models.py b/dashscope/models.py index fee38a4d..9f97c04f 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -1,78 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.models``. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin, ListMixin, _get -from dashscope.common.utils import get_api_module, join_url -import dashscope +Import from ``dashscope.legacy_1x.models`` (or the public ``dashscope`` +root re-exports) in new code. +""" +from dashscope.legacy_1x._shim import install as _install -class Models(ListMixin, GetMixin): - SUB_PATH = "models" - - @classmethod - def get( # type: ignore[override] - cls, - name: str, - api_key: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """Get the model information. - - Args: - name (str): The model name. - api_key (str, optional): The api key. Defaults to None. - workspace (str): The dashscope workspace id. - - Returns: - DashScopeAPIResponse: The model information. - """ - from http import HTTPStatus - - # Use query parameter to filter by model name on server side - # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 - url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) - params = {"model": name, "page_no": 1, "page_size": 1} - - response = _get( - url, - params=params, - api_key=api_key, - module=get_api_module(cls.__module__), - **kwargs, - ) - - if response.status_code != HTTPStatus.OK: - return response # type: ignore[return-value] - - output = response.output - if not output or "models" not in output or not output["models"]: - response.status_code = 404 - response.message = f"Model '{name}' not found" - response.output = None - return response # type: ignore[return-value] - - # Return the first (and only) model from the filtered list - response.output = output["models"][0] - return response # type: ignore[return-value] - - @classmethod - def list( # type: ignore[override] - cls, - page=1, - page_size=10, - api_key: str = None, - **kwargs, - ) -> DashScopeAPIResponse: - """List models. - - Args: - api_key (str, optional): The api key - page (int, optional): Page number. Defaults to 1. - page_size (int, optional): Items per page. Defaults to 10. - - Returns: - DashScopeAPIResponse: The models. - """ - # type: ignore - return super().list(page, page_size, api_key=api_key, **kwargs) # type: ignore[return-value] # pylint: disable=line-too-long # noqa: E501 +_install(__name__, "dashscope.legacy_1x.models") diff --git a/dashscope/multimodal/__init__.py b/dashscope/multimodal/__init__.py index 540bb57a..61f1c31b 100644 --- a/dashscope/multimodal/__init__.py +++ b/dashscope/multimodal/__init__.py @@ -1,95 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.multimodal``. -from .tingwu import tingwu -from .tingwu.tingwu import TingWu -from .tingwu.tingwu_realtime import TingWuRealtime, TingWuRealtimeCallback +Import from ``dashscope.legacy_1x.multimodal`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from .multimodal_dialog import MultiModalDialog, MultiModalCallback -from .dialog_state import DialogState -from .multimodal_constants import ( - RequestToRespondType, - RESPONSE_NAME_TASK_STARTED, - RESPONSE_NAME_RESULT_GENERATED, - RESPONSE_NAME_TASK_FINISHED, - RESPONSE_NAME_TASK_FAILED, - RESPONSE_NAME_STARTED, - RESPONSE_NAME_STOPPED, - RESPONSE_NAME_STATE_CHANGED, - RESPONSE_NAME_REQUEST_ACCEPTED, - RESPONSE_NAME_SPEECH_STARTED, - RESPONSE_NAME_SPEECH_ENDED, - RESPONSE_NAME_RESPONDING_STARTED, - RESPONSE_NAME_RESPONDING_ENDED, - RESPONSE_NAME_SPEECH_CONTENT, - RESPONSE_NAME_RESPONDING_CONTENT, - RESPONSE_NAME_ERROR, - RESPONSE_NAME_HEART_BEAT, -) -from .multimodal_request_params import ( - DashHeader, - DashPayloadParameters, - DashPayloadInput, - DashPayload, - RequestBodyInput, - AsrPostProcessing, - ReplaceWord, - Upstream, - Downstream, - DialogAttributes, - Locations, - Network, - Device, - ClientInfo, - BizParams, - RequestParameters, - RequestToRespondParameters, - RequestToRespondBodyInput, - get_random_uuid, -) +from dashscope.legacy_1x._shim import install as _install -__all__ = [ - "tingwu", - "TingWu", - "TingWuRealtime", - "TingWuRealtimeCallback", - "MultiModalDialog", - "MultiModalCallback", - "DialogState", - "RequestToRespondType", - "RESPONSE_NAME_TASK_STARTED", - "RESPONSE_NAME_RESULT_GENERATED", - "RESPONSE_NAME_TASK_FINISHED", - "RESPONSE_NAME_TASK_FAILED", - "RESPONSE_NAME_STARTED", - "RESPONSE_NAME_STOPPED", - "RESPONSE_NAME_STATE_CHANGED", - "RESPONSE_NAME_REQUEST_ACCEPTED", - "RESPONSE_NAME_SPEECH_STARTED", - "RESPONSE_NAME_SPEECH_ENDED", - "RESPONSE_NAME_RESPONDING_STARTED", - "RESPONSE_NAME_RESPONDING_ENDED", - "RESPONSE_NAME_SPEECH_CONTENT", - "RESPONSE_NAME_RESPONDING_CONTENT", - "RESPONSE_NAME_ERROR", - "RESPONSE_NAME_HEART_BEAT", - "DashHeader", - "DashPayloadParameters", - "DashPayloadInput", - "DashPayload", - "RequestBodyInput", - "AsrPostProcessing", - "ReplaceWord", - "Upstream", - "Downstream", - "DialogAttributes", - "Locations", - "Network", - "Device", - "ClientInfo", - "BizParams", - "RequestParameters", - "RequestToRespondParameters", - "RequestToRespondBodyInput", - "get_random_uuid", -] +_install(__name__, "dashscope.legacy_1x.multimodal") diff --git a/dashscope/nlp/__init__.py b/dashscope/nlp/__init__.py index e69de29b..48fa8237 100644 --- a/dashscope/nlp/__init__.py +++ b/dashscope/nlp/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.nlp``. + +Import from ``dashscope.legacy_1x.nlp`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.nlp") diff --git a/dashscope/protocol/__init__.py b/dashscope/protocol/__init__.py index e69de29b..4f4774b0 100644 --- a/dashscope/protocol/__init__.py +++ b/dashscope/protocol/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.protocol``. + +Import from ``dashscope.legacy_1x.protocol`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.protocol") diff --git a/dashscope/rerank/__init__.py b/dashscope/rerank/__init__.py index 2f0829c3..dc612489 100644 --- a/dashscope/rerank/__init__.py +++ b/dashscope/rerank/__init__.py @@ -1,6 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.rerank``. -from dashscope.rerank.text_rerank import AioTextReRank, TextReRank +Import from ``dashscope.legacy_1x.rerank`` (or the public ``dashscope`` +root re-exports) in new code. +""" -__all__ = ["AioTextReRank", "TextReRank"] +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.rerank") diff --git a/dashscope/threads/__init__.py b/dashscope/threads/__init__.py index 5ae531f2..f0f9f935 100644 --- a/dashscope/threads/__init__.py +++ b/dashscope/threads/__init__.py @@ -1,29 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.threads``. -# yapf: disable +Import from ``dashscope.legacy_1x.threads`` (or the public ``dashscope`` +root re-exports) in new code. +""" -from dashscope.threads.messages.messages import Messages -from dashscope.threads.runs.runs import Runs -from dashscope.threads.runs.steps import Steps -from dashscope.threads.thread_types import ( - MessageFile, Run, RunList, RunStep, - RunStepList, Thread, ThreadMessage, - ThreadMessageList, -) -from dashscope.threads.threads import Threads +from dashscope.legacy_1x._shim import install as _install -__all__ = [ - 'MessageFile', - 'Messages', - 'Run', - 'Runs', - 'RunList', - 'Steps', - 'RunStep', - 'RunStepList', - 'Threads', - 'Thread', - 'ThreadMessage', - 'ThreadMessageList', -] +_install(__name__, "dashscope.legacy_1x.threads") diff --git a/dashscope/tokenizers/__init__.py b/dashscope/tokenizers/__init__.py index e43a8af3..bf30f999 100644 --- a/dashscope/tokenizers/__init__.py +++ b/dashscope/tokenizers/__init__.py @@ -2,8 +2,16 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from .tokenization import Tokenization -from .tokenizer import get_tokenizer, list_tokenizers -from .tokenizer_base import Tokenizer + +# The offline tokenizer implementation lives in the 2.0 package; both stacks +# share it so there is one copy of the code and of the bundled vocab. It is +# imported second on purpose: the 1.x chain above must finish loading before +# the v2 chain starts pulling modules in. +from dashscope.v2.tokenizers import ( # pylint: disable=wrong-import-order + Tokenizer, + get_tokenizer, + list_tokenizers, +) __all__ = [ "Tokenization", diff --git a/dashscope/tokenizers/qwen_tokenizer.py b/dashscope/tokenizers/qwen_tokenizer.py index 9a538f95..2b2baa57 100644 --- a/dashscope/tokenizers/qwen_tokenizer.py +++ b/dashscope/tokenizers/qwen_tokenizer.py @@ -1,167 +1,39 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -# mypy: disable-error-code="annotation-unchecked" - -import base64 -import unicodedata -from typing import Collection, Dict, List, Set, Union - -from .tokenizer_base import Tokenizer - -PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long -ENDOFTEXT = "<|endoftext|>" -IMSTART = "<|im_start|>" -IMEND = "<|im_end|>" -# as the default behavior is changed to allow special tokens in -# regular texts, the surface forms of special tokens need to be -# as different as possible to minimize the impact -EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) -# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 -SPECIAL_START_ID = 151643 -SPECIAL_TOKENS = tuple( - enumerate( - ( - ( - ENDOFTEXT, - IMSTART, - IMEND, - ) - + EXTRAS - ), - start=SPECIAL_START_ID, - ), +"""Compatibility re-export; the implementation moved to the 2.0 package. + +The Qwen tiktoken tokenizer and its special-token tables live only in +``dashscope.v2.tokenizers.qwen_tokenizer``, next to the bundled vocab, so +both stacks share one copy. Import from there in new code. + +``Tokenizer`` is re-exported because the old module imported it into its +own namespace, making ``from dashscope.tokenizers.qwen_tokenizer import +Tokenizer`` a working (if incidental) path. +""" + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 + ENDOFTEXT, + EXTRAS, + IMEND, + IMSTART, + PAT_STR, + SPECIAL_START_ID, + SPECIAL_TOKENS, + SPECIAL_TOKENS_SET, + QwenTokenizer, + Tokenizer, ) -SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) - -# tiktoken's BPE merges tokens recursively in Rust, which can overflow the -# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). -# Split text into chunks below this threshold before encoding. -_CHUNK_SIZE = 100_000 - - -class QwenTokenizer(Tokenizer): - @staticmethod - def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: - with open(tiktoken_bpe_file, "rb") as f: - contents = f.read() - return { - base64.b64decode(token): int(rank) - for token, rank in ( - line.split() for line in contents.splitlines() if line - ) - } - - def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): - self._errors = errors - self._vocab_file = vocab_file - self._extra_vocab_file = extra_vocab_file - - self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( - vocab_file, - ) # type: Dict[bytes, int] - self._special_tokens = { - token: index for index, token in SPECIAL_TOKENS - } - - # try load extra vocab from file - if extra_vocab_file is not None: - used_ids = set(self._mergeable_ranks.values()) | set( - self._special_tokens.values(), - ) - extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) - for token, index in extra_mergeable_ranks.items(): - if token in self._mergeable_ranks: - continue - if index in used_ids: - continue - self._mergeable_ranks[token] = index - # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long - import tiktoken - - enc = tiktoken.Encoding( - "Qwen", - pat_str=PAT_STR, - mergeable_ranks=self._mergeable_ranks, - special_tokens=self._special_tokens, - ) - assert ( - len(self._mergeable_ranks) + len(self._special_tokens) - == enc.n_vocab - ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long - - self.decoder = { - v: k for k, v in self._mergeable_ranks.items() - } # type: dict[int, bytes|str] - self.decoder.update({v: k for k, v in self._special_tokens.items()}) - - self._tokenizer = enc # type: tiktoken.Encoding - - self.eod_id = self._tokenizer.eot_token - self.im_start_id = self._special_tokens[IMSTART] - self.im_end_id = self._special_tokens[IMEND] - - def encode( # type: ignore[override] - self, - text: str, - allowed_special: Union[Set, str] = "all", - disallowed_special: Union[Collection, str] = (), - ) -> Union[List[List], List]: - text = unicodedata.normalize("NFC", text) - if len(text) <= _CHUNK_SIZE: - return self._tokenizer.encode( - text, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ) - - result = [] - for chunk in self._split_text(text): - result.extend( - self._tokenizer.encode( - chunk, - allowed_special=allowed_special, - disallowed_special=disallowed_special, - ), - ) - return result - - @staticmethod - def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: - """Split text into chunks at safe tokenization boundaries.""" - parts: List[str] = [] - for i, line in enumerate(text.split("\n")): - piece = line if i == 0 else "\n" + line - if len(piece) <= chunk_size: - parts.append(piece) - else: - for j in range(0, len(piece), chunk_size): - parts.append(piece[j : j + chunk_size]) - - chunks: List[str] = [] - current_chunk: List[str] = [] - current_len = 0 - for part in parts: - if current_len + len(part) <= chunk_size: - current_chunk.append(part) - current_len += len(part) - else: - if current_chunk: - chunks.append("".join(current_chunk)) - current_chunk = [part] - current_len = len(part) - if current_chunk: - chunks.append("".join(current_chunk)) - return chunks - def decode( - self, - token_ids: Union[int, List[int]], - skip_special_tokens: bool = False, - errors: str = None, - **kwargs, - ) -> str: - if isinstance(token_ids, int): - token_ids = [token_ids] - if skip_special_tokens: - token_ids = [i for i in token_ids if i < self.eod_id] - return self._tokenizer.decode(token_ids, errors=errors or self._errors) +__all__ = [ + "ENDOFTEXT", + "EXTRAS", + "IMEND", + "IMSTART", + "PAT_STR", + "SPECIAL_START_ID", + "SPECIAL_TOKENS", + "SPECIAL_TOKENS_SET", + "QwenTokenizer", + "Tokenizer", +] diff --git a/dashscope/tokenizers/tokenization.py b/dashscope/tokenizers/tokenization.py index 9a29de53..ba40b69f 100644 --- a/dashscope/tokenizers/tokenization.py +++ b/dashscope/tokenizers/tokenization.py @@ -4,21 +4,21 @@ import copy from typing import Any, List, Optional -from dashscope.api_entities.dashscope_response import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( DashScopeAPIResponse, Message, Role, ) -from dashscope.client.base_api import BaseApi -from dashscope.common.constants import ( +from dashscope.legacy_1x.client.base_api import BaseApi +from dashscope.legacy_1x.common.constants import ( CUSTOMIZED_MODEL_ID, DEPRECATED_MESSAGE, HISTORY, MESSAGES, PROMPT, ) -from dashscope.common.error import InputRequired, ModelRequired -from dashscope.common.logging import logger +from dashscope.legacy_1x.common.error import InputRequired, ModelRequired +from dashscope.legacy_1x.common.logging import logger class Tokenization(BaseApi): diff --git a/dashscope/tokenizers/tokenizer.py b/dashscope/tokenizers/tokenizer.py index 05ba3b0f..b3218769 100644 --- a/dashscope/tokenizers/tokenizer.py +++ b/dashscope/tokenizers/tokenizer.py @@ -1,49 +1,41 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. - -import os -from typing import List - -from dashscope.common.error import UnsupportedModel -from dashscope.tokenizers.qwen_tokenizer import QwenTokenizer - -from .tokenizer_base import Tokenizer - -QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] -current_path = os.path.dirname(os.path.abspath(__file__)) -root_path = os.path.dirname(current_path) - - -def get_tokenizer(model: str) -> Tokenizer: - """Get a tokenizer based on model name. - - Args: - model (str): The model name. - - Raises: - UnsupportedModel: Not support model - - Returns: - Tokenizer: The `Tokenizer` of the model. - """ - if model in QWEN_SERIALS: - return QwenTokenizer( - os.path.join(root_path, "resources", "qwen.tiktoken"), - ) - elif model.startswith("qwen"): - return QwenTokenizer( - os.path.join(root_path, "resources", "qwen.tiktoken"), - ) - else: - raise UnsupportedModel( - f"Not support model: {model}, currently only support qwen models.", - ) - - -def list_tokenizers() -> List[str]: - """List support models - - Returns: - List[str]: The model list. - """ - return QWEN_SERIALS +"""Compatibility re-export; the implementation moved to the 2.0 package. + +The offline tokenizer factory lives only in +``dashscope.v2.tokenizers.tokenizer``, next to the bundled vocab, so both +stacks share one copy of the code and of the vocab. Import from there in +new code. + +Note that :func:`get_tokenizer` raises ``ValueError`` for an unknown model; +it used to raise ``dashscope.legacy_1x.common.error.UnsupportedModel``, which the 2.0 +leaf must not depend on. + +``Tokenizer`` and ``QwenTokenizer`` are re-exported because the old module +imported them into its own namespace. The old module-level ``current_path`` +and ``root_path`` are deliberately not restored: they only located the +vocab, which no longer ships next to this file. +""" + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.tokenizer import ( # noqa: F401 + QWEN_SERIALS, + get_tokenizer, + list_tokenizers, +) + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.qwen_tokenizer import ( # noqa: F401 + QwenTokenizer, +) + +# pylint: disable-next=unused-import +from dashscope.v2.tokenizers.tokenizer_base import Tokenizer # noqa: F401 + +__all__ = [ + "QWEN_SERIALS", + "QwenTokenizer", + "Tokenizer", + "get_tokenizer", + "list_tokenizers", +] diff --git a/dashscope/tokenizers/tokenizer_base.py b/dashscope/tokenizers/tokenizer_base.py index 2e385bf8..89520f92 100644 --- a/dashscope/tokenizers/tokenizer_base.py +++ b/dashscope/tokenizers/tokenizer_base.py @@ -1,31 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +"""Compatibility re-export; the implementation moved to the 2.0 package. -from typing import List +The offline tokenizer base class lives only in +``dashscope.v2.tokenizers.tokenizer_base`` so both stacks share one copy. +Import from there in new code. +""" +from dashscope.v2.tokenizers.tokenizer_base import Tokenizer -class Tokenizer: - """Base tokenizer interface for local tokenizers.""" - - def __init__(self): - pass - - def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 - """Encode input text string to token ids. - - Args: - text (str): The string to be encoded. - - Returns: - List[int]: The token ids. - """ - - def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 - """Decode token ids to string. - - Args: - token_ids (List[int]): The input token ids. - - Returns: - str: The string of the token ids. - """ +__all__ = ["Tokenizer"] diff --git a/dashscope/utils/__init__.py b/dashscope/utils/__init__.py index e69de29b..5f7f9be0 100644 --- a/dashscope/utils/__init__.py +++ b/dashscope/utils/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Backward-compat shim; the implementation moved to ``dashscope.legacy_1x.utils``. + +Import from ``dashscope.legacy_1x.utils`` (or the public ``dashscope`` +root re-exports) in new code. +""" + +from dashscope.legacy_1x._shim import install as _install + +_install(__name__, "dashscope.legacy_1x.utils") diff --git a/dashscope/v2/__init__.py b/dashscope/v2/__init__.py index 7217e39b..d6a03811 100644 --- a/dashscope/v2/__init__.py +++ b/dashscope/v2/__init__.py @@ -7,21 +7,13 @@ """ 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.tokenizers import get_tokenizer, list_tokenizers 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 -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 ad8bc0be..a727faeb 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" @@ -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 @@ -542,6 +542,27 @@ 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/_error_registry.py b/dashscope/v2/_error_registry.py index 9c2675fa..b6dcea3a 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__ = "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, @@ -208,6 +216,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}.", @@ -594,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 = [ @@ -611,11 +732,16 @@ def format_message( CONCURRENCY_LIMIT_EXCEEDED, INSUFFICIENT_QUOTA, INTERNAL_ERROR, + RESPONSE_ERROR, SERVICE_UNAVAILABLE, REQUEST_TIMEOUT, ] 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, @@ -647,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/agentstudio/__init__.py b/dashscope/v2/agentstudio/__init__.py new file mode 100644 index 00000000..edd58186 --- /dev/null +++ b/dashscope/v2/agentstudio/__init__.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""AgentStudio sub-SDK for the dashscope Python client. + +The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP +API. All resource classes are accessed through a client instance:: + + from dashscope.agentstudio import Client + from dashscope.agentstudio.types import ( + user_message, user_tool_result, + ) + +Example +------- +>>> from dashscope.agentstudio import Client +>>> from dashscope.agentstudio.types import user_message +>>> client = Client(api_key="sk-xxx") +>>> agent = client.agents.create(name="demo", model="qwen-plus") +>>> session = client.sessions.create(agent=agent.id) +>>> client.sessions.events.send(session.id, [user_message("Hello!")]) +>>> with client.sessions.events.stream(session.id) as stream: +... for event in stream: +... print(event.type, event.to_dict()) +... if event.type == "session_status": +... break +""" + +# yapf: disable + +from .client import Client, AsyncClient +from .transport import APIResponse, RequestOptions +from . import types +from .exceptions import ( + AgentStudioError, + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + ConflictError, + InternalServerError, + InvalidRequestError, + NotFoundError, + OverloadedError, + PermissionDeniedError, + RateLimitError, + StreamClosedError, + StreamError, +) +from .pagination import ( + AsyncCursorPage, + CursorPage, +) +from .types import ( + Memory, + MemoryStore, + MemoryVersion, + Message, + PermissionPolicy, + SecurityAlert, + SecurityAlertList, + SecurityOverview, + ServerEvent, + SessionResource, + StopReason, + user_define_outcome, + user_interrupt, + user_message, + user_tool_approval_response, + user_custom_tool_result, + user_tool_result, +) +from .types.params import ( + AgentCreateParams, + AgentUpdateParams, + SessionCreateParams, + SessionUpdateParams, + EnvironmentCreateParams, + EnvironmentUpdateParams, + DeploymentCreateParams, + DeploymentUpdateParams, + SessionEventSendParams, + WebhookEndpointCreateParams, + WebhookEndpointUpdateParams, + WebhookEventListParams, +) +from .constants import ( + PermissionPolicyType, + SSEEventType, + SessionStatus, + StopReasonType, + WebhookDeliveryStatus, + WebhookDisabledReason, + WebhookEventType, + WebhookStatus, +) + +__all__ = [ + # client + "Client", + "AsyncClient", + # base types + "RequestOptions", + "APIResponse", + # exceptions + "AgentStudioError", + "APIConnectionError", + "APIStatusError", + "APITimeoutError", + "AuthenticationError", + "ConflictError", + "InternalServerError", + "InvalidRequestError", + "NotFoundError", + "OverloadedError", + "PermissionDeniedError", + "RateLimitError", + "StreamError", + "StreamClosedError", + # pagination + "CursorPage", + "AsyncCursorPage", + # unified message type + "Memory", + "MemoryStore", + "MemoryVersion", + "Message", + "PermissionPolicy", + "SecurityAlert", + "SecurityAlertList", + "SecurityOverview", + "ServerEvent", + "SessionResource", + "StopReason", + # client event helpers (re-exported for convenience) + "user_message", + "user_interrupt", + "user_tool_approval_response", + "user_custom_tool_result", + "user_tool_result", + "user_define_outcome", + # typing module + "types", + # params (commonly used request-body types) + "AgentCreateParams", + "AgentUpdateParams", + "SessionCreateParams", + "SessionUpdateParams", + "EnvironmentCreateParams", + "EnvironmentUpdateParams", + "DeploymentCreateParams", + "DeploymentUpdateParams", + "SessionEventSendParams", + "WebhookEndpointCreateParams", + "WebhookEndpointUpdateParams", + "WebhookEventListParams", + "WebhookStatus", + "WebhookDisabledReason", + "WebhookDeliveryStatus", + "WebhookEventType", + "SSEEventType", + "SessionStatus", + "StopReasonType", + "PermissionPolicyType", +] diff --git a/dashscope/agentstudio/client.py b/dashscope/v2/agentstudio/client.py similarity index 79% rename from dashscope/agentstudio/client.py rename to dashscope/v2/agentstudio/client.py index be840af8..87573d6e 100644 --- a/dashscope/agentstudio/client.py +++ b/dashscope/v2/agentstudio/client.py @@ -9,39 +9,59 @@ import httpx -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope._shared._validation import ( + validate_region, + validate_workspace_id, +) +from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.v2.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.v2.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles +from dashscope.v2.agentstudio.resources.memory_stores import ( MemoryStores, AsyncMemoryStores, ) -from dashscope.agentstudio.resources.security import Security, AsyncSecurity -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import Skills, AsyncSkills -from dashscope.agentstudio.resources.vaults import Vaults, AsyncVaults -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.v2.agentstudio.resources.security import Security, AsyncSecurity +from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.v2.agentstudio.resources.skills import Skills, AsyncSkills +from dashscope.v2.agentstudio.resources.vaults import Vaults, AsyncVaults +from dashscope.v2.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_BASE_URL_TEMPLATE, AGENTSTUDIO_DEFAULT_REGION, AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.agentstudio.transport import SyncTransport, AsyncTransport -from dashscope.common.api_key import get_default_api_key -from dashscope.common.env import validate_region, validate_workspace_id +from dashscope.v2.agentstudio.transport import SyncTransport, AsyncTransport +from dashscope.v2._config import _api_key_from_file, settings + + +def _default_api_key() -> Optional[str]: + """Resolve the api key when ``Client(api_key=...)`` was not given. + + Same tiers as the 2.0 client: ``settings.api_key`` (explicit value or + the ``DASHSCOPE_API_KEY`` env var), then the key file written by + ``save_api_key``. Returns ``None`` rather than raising -- the + transport turns a missing key into this SDK's own + ``AuthenticationError`` on the first request, with a message that + names the AgentStudio entry points. + + This deliberately does not read the 1.x ``dashscope.api_key`` module + global; set ``dashscope.v2.settings.api_key`` or the environment + variable instead. + """ + return settings.api_key or _api_key_from_file() def _resolve_base_url( @@ -132,7 +152,7 @@ def __init__( ) self.transport = SyncTransport( base_url=resolved_base, - api_key=api_key or get_default_api_key(), + api_key=api_key or _default_api_key(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), @@ -207,7 +227,7 @@ def __init__( ) self.transport = AsyncTransport( base_url=resolved_base, - api_key=api_key or get_default_api_key(), + api_key=api_key or _default_api_key(), workspace=resolved_workspace, uid=uid or os.environ.get("DASHSCOPE_UID"), user_agent=_user_agent(resolved_base), diff --git a/dashscope/agentstudio/constants.py b/dashscope/v2/agentstudio/constants.py similarity index 100% rename from dashscope/agentstudio/constants.py rename to dashscope/v2/agentstudio/constants.py diff --git a/dashscope/agentstudio/exceptions.py b/dashscope/v2/agentstudio/exceptions.py similarity index 99% rename from dashscope/agentstudio/exceptions.py rename to dashscope/v2/agentstudio/exceptions.py index 124ab5e8..055e3143 100644 --- a/dashscope/agentstudio/exceptions.py +++ b/dashscope/v2/agentstudio/exceptions.py @@ -20,7 +20,7 @@ from typing import Any, Dict, Mapping, Optional -from dashscope.common.error import DashScopeException +from dashscope._shared._errors import DashScopeException class AgentStudioError(DashScopeException): diff --git a/dashscope/agentstudio/pagination.py b/dashscope/v2/agentstudio/pagination.py similarity index 100% rename from dashscope/agentstudio/pagination.py rename to dashscope/v2/agentstudio/pagination.py diff --git a/dashscope/agentstudio/resources/__init__.py b/dashscope/v2/agentstudio/resources/__init__.py similarity index 72% rename from dashscope/agentstudio/resources/__init__.py rename to dashscope/v2/agentstudio/resources/__init__.py index 76c65123..aeeacbc8 100644 --- a/dashscope/agentstudio/resources/__init__.py +++ b/dashscope/v2/agentstudio/resources/__init__.py @@ -15,19 +15,19 @@ client.sessions.events.send(session.id, [user_message("hello")]) """ -from dashscope.agentstudio.resources.agents import Agents, AsyncAgents -from dashscope.agentstudio.resources.deployments import ( +from dashscope.v2.agentstudio.resources.agents import Agents, AsyncAgents +from dashscope.v2.agentstudio.resources.deployments import ( AsyncDeploymentRuns, AsyncDeployments, DeploymentRuns, Deployments, ) -from dashscope.agentstudio.resources.environments import ( +from dashscope.v2.agentstudio.resources.environments import ( Environments, AsyncEnvironments, ) -from dashscope.agentstudio.resources.files import Files, AsyncFiles -from dashscope.agentstudio.resources.memory_stores import ( +from dashscope.v2.agentstudio.resources.files import Files, AsyncFiles +from dashscope.v2.agentstudio.resources.memory_stores import ( AsyncMemories, AsyncMemoryStores, AsyncMemoryVersions, @@ -35,26 +35,26 @@ MemoryStores, MemoryVersions, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.v2.agentstudio.resources.session_events import ( AsyncSessionEvents, SessionEvents, _AioTypedEventStream, _TypedEventStream, ) -from dashscope.agentstudio.resources.sessions import Sessions, AsyncSessions -from dashscope.agentstudio.resources.skills import ( +from dashscope.v2.agentstudio.resources.sessions import Sessions, AsyncSessions +from dashscope.v2.agentstudio.resources.skills import ( AsyncSkillVersions, AsyncSkills, SkillVersions, Skills, ) -from dashscope.agentstudio.resources.vaults import ( +from dashscope.v2.agentstudio.resources.vaults import ( AsyncCredentials, AsyncVaults, Credentials, Vaults, ) -from dashscope.agentstudio.resources.webhook_endpoints import ( +from dashscope.v2.agentstudio.resources.webhook_endpoints import ( AsyncWebhookEndpoints, WebhookEndpoints, ) diff --git a/dashscope/agentstudio/resources/_helpers.py b/dashscope/v2/agentstudio/resources/_helpers.py similarity index 99% rename from dashscope/agentstudio/resources/_helpers.py rename to dashscope/v2/agentstudio/resources/_helpers.py index f47d6d94..0978b1ab 100644 --- a/dashscope/agentstudio/resources/_helpers.py +++ b/dashscope/v2/agentstudio/resources/_helpers.py @@ -6,7 +6,7 @@ from typing import Any, Mapping -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( Agent, AgentVersion, Credential, diff --git a/dashscope/agentstudio/resources/agents.py b/dashscope/v2/agentstudio/resources/agents.py similarity index 97% rename from dashscope/agentstudio/resources/agents.py rename to dashscope/v2/agentstudio/resources/agents.py index 91bf1553..738eb2d3 100644 --- a/dashscope/agentstudio/resources/agents.py +++ b/dashscope/v2/agentstudio/resources/agents.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_agent, _coerce_agent_version, ) -from dashscope.agentstudio.types import Agent, AgentVersion -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import Agent, AgentVersion +from dashscope.v2.agentstudio.types.params import ( AgentCreateParams, AgentListParams, AgentUpdateParams, diff --git a/dashscope/agentstudio/resources/deployments.py b/dashscope/v2/agentstudio/resources/deployments.py similarity index 98% rename from dashscope/agentstudio/resources/deployments.py rename to dashscope/v2/agentstudio/resources/deployments.py index 149b64f2..4e93b234 100644 --- a/dashscope/agentstudio/resources/deployments.py +++ b/dashscope/v2/agentstudio/resources/deployments.py @@ -6,17 +6,17 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_deployment, _coerce_deployment_run, ) -from dashscope.agentstudio.types import Deployment, DeploymentRun -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import Deployment, DeploymentRun +from dashscope.v2.agentstudio.types.params import ( _NOT_GIVEN, DeploymentCreateParams, DeploymentListParams, diff --git a/dashscope/agentstudio/resources/environments.py b/dashscope/v2/agentstudio/resources/environments.py similarity index 96% rename from dashscope/agentstudio/resources/environments.py rename to dashscope/v2/agentstudio/resources/environments.py index 8aed2eeb..26a4ab38 100644 --- a/dashscope/agentstudio/resources/environments.py +++ b/dashscope/v2/agentstudio/resources/environments.py @@ -6,16 +6,16 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_env, ) -from dashscope.agentstudio.types import DeleteResponse, Environment -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Environment +from dashscope.v2.agentstudio.types.params import ( EnvironmentCreateParams, EnvironmentListParams, EnvironmentUpdateParams, diff --git a/dashscope/agentstudio/resources/files.py b/dashscope/v2/agentstudio/resources/files.py similarity index 97% rename from dashscope/agentstudio/resources/files.py rename to dashscope/v2/agentstudio/resources/files.py index e05c7be8..a2c9936e 100644 --- a/dashscope/agentstudio/resources/files.py +++ b/dashscope/v2/agentstudio/resources/files.py @@ -18,16 +18,16 @@ Union, ) -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_file, ) -from dashscope.agentstudio.types import DeleteResponse, File -from dashscope.agentstudio.types.params import FileListParams +from dashscope.v2.agentstudio.types import DeleteResponse, File +from dashscope.v2.agentstudio.types.params import FileListParams _PATH_FILES = "/files" diff --git a/dashscope/agentstudio/resources/memory_stores.py b/dashscope/v2/agentstudio/resources/memory_stores.py similarity index 99% rename from dashscope/agentstudio/resources/memory_stores.py rename to dashscope/v2/agentstudio/resources/memory_stores.py index 573084a0..1308a20e 100644 --- a/dashscope/agentstudio/resources/memory_stores.py +++ b/dashscope/v2/agentstudio/resources/memory_stores.py @@ -41,12 +41,12 @@ from typing import Any, List, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_memory, _coerce_memory_store, _coerce_memory_version, @@ -59,13 +59,13 @@ _memory_version_redact_path, _memory_versions_path, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( DeleteResponse, Memory, MemoryStore, MemoryVersion, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( MemoryCreateParams, MemoryListParams, MemoryStoreCreateParams, diff --git a/dashscope/agentstudio/resources/security.py b/dashscope/v2/agentstudio/resources/security.py similarity index 96% rename from dashscope/agentstudio/resources/security.py rename to dashscope/v2/agentstudio/resources/security.py index a6ebc723..8e5af669 100644 --- a/dashscope/agentstudio/resources/security.py +++ b/dashscope/v2/agentstudio/resources/security.py @@ -12,8 +12,8 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.types import SecurityAlertList, SecurityOverview -from dashscope.agentstudio.types.params import SecurityListAgentLogsParams +from dashscope.v2.agentstudio.types import SecurityAlertList, SecurityOverview +from dashscope.v2.agentstudio.types.params import SecurityListAgentLogsParams _PATH_SECURITY_OVERVIEW = "/security/overview" _PATH_SECURITY_AGENT_LOGS = "/security/agent_logs" diff --git a/dashscope/agentstudio/resources/session_events.py b/dashscope/v2/agentstudio/resources/session_events.py similarity index 97% rename from dashscope/agentstudio/resources/session_events.py rename to dashscope/v2/agentstudio/resources/session_events.py index 617acaf9..6fcccc61 100644 --- a/dashscope/agentstudio/resources/session_events.py +++ b/dashscope/v2/agentstudio/resources/session_events.py @@ -14,23 +14,23 @@ Sequence, ) -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_event, _events_path, _stream_path, ) -from dashscope.agentstudio.streaming import AsyncEventStream, EventStream -from dashscope.agentstudio.types import ServerEvent -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.streaming import AsyncEventStream, EventStream +from dashscope.v2.agentstudio.types import ServerEvent +from dashscope.v2.agentstudio.types.params import ( SessionEventListParams, SessionEventSendParams, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, BlockType, SessionStatus, diff --git a/dashscope/agentstudio/resources/session_resources.py b/dashscope/v2/agentstudio/resources/session_resources.py similarity index 95% rename from dashscope/agentstudio/resources/session_resources.py rename to dashscope/v2/agentstudio/resources/session_resources.py index 1691a654..fc2e4b37 100644 --- a/dashscope/agentstudio/resources/session_resources.py +++ b/dashscope/v2/agentstudio/resources/session_resources.py @@ -10,18 +10,18 @@ from typing import Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_session_resource, _session_resource_item_path, _session_resources_path, ) -from dashscope.agentstudio.types import DeleteResponse, SessionResource -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, SessionResource +from dashscope.v2.agentstudio.types.params import ( SessionResourceAddParams, SessionResourceListParams, ) diff --git a/dashscope/agentstudio/resources/session_threads.py b/dashscope/v2/agentstudio/resources/session_threads.py similarity index 97% rename from dashscope/agentstudio/resources/session_threads.py rename to dashscope/v2/agentstudio/resources/session_threads.py index e2f20313..6c8164c1 100644 --- a/dashscope/agentstudio/resources/session_threads.py +++ b/dashscope/v2/agentstudio/resources/session_threads.py @@ -10,12 +10,12 @@ from typing import Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_event, _coerce_session_thread, _session_thread_archive_path, @@ -23,8 +23,8 @@ _session_thread_item_path, _session_threads_path, ) -from dashscope.agentstudio.types import ServerEvent, SessionThread -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import ServerEvent, SessionThread +from dashscope.v2.agentstudio.types.params import ( SessionEventListParams, SessionThreadListParams, ) diff --git a/dashscope/agentstudio/resources/sessions.py b/dashscope/v2/agentstudio/resources/sessions.py similarity index 95% rename from dashscope/agentstudio/resources/sessions.py rename to dashscope/v2/agentstudio/resources/sessions.py index 4bb68f15..e5be7178 100644 --- a/dashscope/agentstudio/resources/sessions.py +++ b/dashscope/v2/agentstudio/resources/sessions.py @@ -6,28 +6,28 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_session, ) -from dashscope.agentstudio.resources.session_events import ( +from dashscope.v2.agentstudio.resources.session_events import ( SessionEvents, AsyncSessionEvents, ) -from dashscope.agentstudio.resources.session_resources import ( +from dashscope.v2.agentstudio.resources.session_resources import ( SessionResources, AsyncSessionResources, ) -from dashscope.agentstudio.resources.session_threads import ( +from dashscope.v2.agentstudio.resources.session_threads import ( SessionThreads, AsyncSessionThreads, ) -from dashscope.agentstudio.types import DeleteResponse, Session -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Session +from dashscope.v2.agentstudio.types.params import ( SessionCreateParams, SessionListParams, SessionUpdateParams, diff --git a/dashscope/agentstudio/resources/skills.py b/dashscope/v2/agentstudio/resources/skills.py similarity index 97% rename from dashscope/agentstudio/resources/skills.py rename to dashscope/v2/agentstudio/resources/skills.py index c9d9833e..3ad70cf8 100644 --- a/dashscope/agentstudio/resources/skills.py +++ b/dashscope/v2/agentstudio/resources/skills.py @@ -7,17 +7,17 @@ import os # pylint: disable=unused-import # noqa: F401 from typing import Any, BinaryIO, Dict, Optional, Tuple, Union -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_skill, _coerce_skill_version, ) -from dashscope.agentstudio.types import DeleteResponse, Skill, SkillVersion -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types import DeleteResponse, Skill, SkillVersion +from dashscope.v2.agentstudio.types.params import ( SkillCreateParams, SkillListParams, SkillVersionCreateParams, diff --git a/dashscope/agentstudio/resources/vaults.py b/dashscope/v2/agentstudio/resources/vaults.py similarity index 98% rename from dashscope/agentstudio/resources/vaults.py rename to dashscope/v2/agentstudio/resources/vaults.py index c68e4922..7440ec69 100644 --- a/dashscope/agentstudio/resources/vaults.py +++ b/dashscope/v2/agentstudio/resources/vaults.py @@ -6,21 +6,21 @@ from typing import Any, Mapping, Optional -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_credential, _coerce_vault, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( Credential, DeleteResponse, Vault, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( CredentialCreateParams, CredentialListParams, CredentialUpdateParams, diff --git a/dashscope/agentstudio/resources/webhook_endpoints.py b/dashscope/v2/agentstudio/resources/webhook_endpoints.py similarity index 97% rename from dashscope/agentstudio/resources/webhook_endpoints.py rename to dashscope/v2/agentstudio/resources/webhook_endpoints.py index fc1a4485..c20ee2c4 100644 --- a/dashscope/agentstudio/resources/webhook_endpoints.py +++ b/dashscope/v2/agentstudio/resources/webhook_endpoints.py @@ -6,23 +6,23 @@ from typing import Any, Dict, Optional, Sequence -from dashscope.agentstudio.pagination import ( +from dashscope.v2.agentstudio.pagination import ( AsyncCursorPage, CursorPage, build_page, ) -from dashscope.agentstudio.resources._helpers import ( +from dashscope.v2.agentstudio.resources._helpers import ( _coerce_webhook_endpoint, _coerce_webhook_event, _coerce_webhook_secret_reset, ) -from dashscope.agentstudio.types import ( +from dashscope.v2.agentstudio.types import ( WebhookEndpoint, WebhookEndpointList, WebhookEvent, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( +from dashscope.v2.agentstudio.types.params import ( WebhookEndpointCreateParams, WebhookEndpointUpdateParams, WebhookEventListParams, diff --git a/dashscope/agentstudio/streaming.py b/dashscope/v2/agentstudio/streaming.py similarity index 95% rename from dashscope/agentstudio/streaming.py rename to dashscope/v2/agentstudio/streaming.py index bdbdc687..b338462d 100644 --- a/dashscope/agentstudio/streaming.py +++ b/dashscope/v2/agentstudio/streaming.py @@ -30,6 +30,7 @@ import asyncio import json +import logging import queue import threading from dataclasses import dataclass, field @@ -38,8 +39,13 @@ import httpx from httpx_sse import EventSource -from dashscope.agentstudio import exceptions -from dashscope.common.logging import logger +from dashscope.v2.agentstudio import exceptions + +# A child of the "dashscope" logger, so the level and handler installed by +# dashscope.legacy_1x.common.logging.enable_logging() still reach these warnings via +# propagation -- without importing that module (and its import-time side +# effects) just to name a logger. +logger = logging.getLogger("dashscope.agentstudio.streaming") _DEFAULT_IDLE_TIMEOUT = 120.0 diff --git a/dashscope/agentstudio/transport.py b/dashscope/v2/agentstudio/transport.py similarity index 99% rename from dashscope/agentstudio/transport.py rename to dashscope/v2/agentstudio/transport.py index 9083ba49..0a4e1e19 100644 --- a/dashscope/agentstudio/transport.py +++ b/dashscope/v2/agentstudio/transport.py @@ -27,12 +27,12 @@ import httpx -from dashscope.agentstudio import exceptions -from dashscope.agentstudio.constants import ( +from dashscope._shared._sdk_ident import get_sdk_headers +from dashscope.v2.agentstudio import exceptions +from dashscope.v2.agentstudio.constants import ( AGENTSTUDIO_DEFAULT_TIMEOUT, AGENTSTUDIO_MAX_RETRIES, ) -from dashscope.common.utils import get_sdk_headers logger = logging.getLogger("dashscope.agentstudio") diff --git a/dashscope/agentstudio/types/__init__.py b/dashscope/v2/agentstudio/types/__init__.py similarity index 93% rename from dashscope/agentstudio/types/__init__.py rename to dashscope/v2/agentstudio/types/__init__.py index 9cd37476..7c20eeab 100644 --- a/dashscope/agentstudio/types/__init__.py +++ b/dashscope/v2/agentstudio/types/__init__.py @@ -4,8 +4,8 @@ Re-exports all public symbols from sub-modules for backward compatibility: from dashscope.agentstudio.types import Agent, Message, user_message """ -from dashscope.agentstudio.types.models import * # noqa: F401,F403 -from dashscope.agentstudio.types.models import ( # noqa: F401 +from dashscope.v2.agentstudio.types.models import * # noqa: F401,F403 +from dashscope.v2.agentstudio.types.models import ( # noqa: F401 BaseModel, Metadata, Scope, @@ -93,7 +93,7 @@ WebhookEventData, WebhookSecretReset, ) -from dashscope.agentstudio.types.params import ( # noqa: F401 +from dashscope.v2.agentstudio.types.params import ( # noqa: F401 AgentCreateParams, AgentUpdateParams, AgentListParams, diff --git a/dashscope/agentstudio/types/models.py b/dashscope/v2/agentstudio/types/models.py similarity index 99% rename from dashscope/agentstudio/types/models.py rename to dashscope/v2/agentstudio/types/models.py index f56ef695..4339dab3 100644 --- a/dashscope/agentstudio/types/models.py +++ b/dashscope/v2/agentstudio/types/models.py @@ -27,7 +27,7 @@ Union, ) -from dashscope.agentstudio.constants import ( +from dashscope.v2.agentstudio.constants import ( SSEEventType, MessageRole, BlockType, diff --git a/dashscope/agentstudio/types/params.py b/dashscope/v2/agentstudio/types/params.py similarity index 99% rename from dashscope/agentstudio/types/params.py rename to dashscope/v2/agentstudio/types/params.py index d85faa63..a4c9268e 100644 --- a/dashscope/agentstudio/types/params.py +++ b/dashscope/v2/agentstudio/types/params.py @@ -18,7 +18,7 @@ from typing import Any, Mapping, Optional, Sequence -from dashscope.agentstudio.types.models import BaseModel +from dashscope.v2.agentstudio.types.models import BaseModel _NOT_GIVEN = object() diff --git a/dashscope/v2/client.py b/dashscope/v2/client.py index f2c91dc6..97dacbcf 100644 --- a/dashscope/v2/client.py +++ b/dashscope/v2/client.py @@ -151,22 +151,24 @@ def reinforcement_learning(self) -> "Rl": @property def agentstudio(self): - """Lazily mounted 1.x AgentStudio sub-SDK (design doc M3 3.8). + """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). The client is wrapped in a transparent proxy (design doc M3 3.12) - that translates 1.x AgentStudio errors into the ``dashscope.v2`` + that translates AgentStudio errors into the ``dashscope.v2`` exception family, so ``except dashscope.v2.errors.DashScopeError`` covers them. The proxy spoofs ``__class__`` and forwards attributes, so ``isinstance(client.agentstudio, AgentStudioClient)`` - and ``.transport`` access keep working; no 1.x code is touched. + and ``.transport`` access keep working. - AgentStudio rides the shared 1.x transport, so its requests keep - the unmarked 1.x module tag (``agentstudio``), not the v2 ``2`` - marker -- intentional, since this is the 1.x code path. + AgentStudio ships its own transport rather than riding this + client's, so its requests keep the unmarked module tag + (``agentstudio``) instead of the v2 ``2`` marker -- the backend + already reports on that tag, and changing it would split the + server-side statistics. """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import Client as AgentStudioClient + from dashscope.v2.agentstudio import Client as AgentStudioClient from dashscope.v2.resources._agentstudio import wrap_agentstudio # agentstudio resolves its own base_url from workspace + @@ -392,15 +394,15 @@ def reinforcement_learning(self) -> "AsyncRl": @property def agentstudio(self): - """Lazily mounted 1.x AgentStudio sub-SDK (design doc M3 3.8). + """Lazily mounted AgentStudio sub-SDK (design doc M3 3.8). See :attr:`Client.agentstudio`: the returned client is wrapped in - a transparent proxy that aligns 1.x AgentStudio errors with the + a transparent proxy that aligns AgentStudio errors with the ``dashscope.v2`` exception family (design doc M3 3.12). """ if self._agentstudio is None: # pylint: disable=import-outside-toplevel - from dashscope.agentstudio import AsyncClient as AsClient + from dashscope.v2.agentstudio import AsyncClient as AsClient from dashscope.v2.resources._agentstudio import wrap_agentstudio # See Client.agentstudio: agentstudio builds its own base_url diff --git a/dashscope/v2/errors.py b/dashscope/v2/errors.py index c2435535..77c83fac 100644 --- a/dashscope/v2/errors.py +++ b/dashscope/v2/errors.py @@ -18,8 +18,14 @@ 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, 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 @@ -85,12 +91,110 @@ 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 _CatalogSdkError(SdkError): + """SdkError whose message defaults to the entry's ``{detail}`` template.""" + + _SDK_ERROR_NAME = "" + + 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 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.""" + """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): @@ -142,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"] @@ -161,38 +276,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 ( @@ -203,23 +286,29 @@ def __init__( 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 @@ -263,10 +352,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 +432,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/finetune/__init__.py b/dashscope/v2/finetune/__init__.py new file mode 100644 index 00000000..ed3aa25f --- /dev/null +++ b/dashscope/v2/finetune/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Agentic fine-tuning, owned by the 2.0 package. + +Holds the agentic-RL workflow package (``reinforcement``) and the +``AgenticRL`` driver that ``Client(...).rl`` is built on. + +The 1.x job-management surface -- ``dashscope.FineTunes`` and +``dashscope.Deployments`` -- stays at ``dashscope.finetune``; only the +modules listed in the decoupling plan moved here, and each keeps a +reverse-reference shim at its old import path. + +Deliberately imports nothing at module level: ``reinforcement`` requires +pydantic / tenacity / pyyaml, and pulling those into ``import +dashscope.v2`` would make the 2.0 client depend on them. +""" diff --git a/dashscope/v2/finetune/_submit.py b/dashscope/v2/finetune/_submit.py new file mode 100644 index 00000000..a698173c --- /dev/null +++ b/dashscope/v2/finetune/_submit.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The job-submit POST for ``AgenticRL``, self-contained inside 2.0. + +``AgenticRL.submit_job`` used to inherit ``CreateMixin`` from +``dashscope.legacy_1x.client.base_api`` for one non-streaming POST. Keeping that +would have left the last import of the 1.x client inside ``dashscope.v2``, +so the call is spelled out here instead: a few dozen lines, against +pulling the 1.x request/response stack (``default_headers``, ``join_url``, +``_handle_http_stream_response``) into the 2.0 package. + +The wire format is deliberately byte-identical to what ``CreateMixin`` +sent, header order included, because the backend keys its statistics off +``x-dashscope-sdk-client`` and ``user-agent``: + +- The module tag stays the *unmarked* ``finetune``. Deriving it from this + module's dotted name would now yield ``v2``, and the v2 transport's + marked form (``finetune2``) belongs to ``client.rl.jobs`` rather than + to the submit path. Both spellings already exist today; each keeps its + own. +- The identity headers come from ``dashscope._shared._sdk_ident``, so the + CLI's ``set_sdk_client("python-cli")`` override and the process-wide + session id still apply, and ``DASHSCOPE_DISABLE_SDK_HEADERS`` still + suppresses them. + +Only the non-SSE branches of the 1.x response parser are mirrored. +``submit_job`` posts without streaming to ``/fine-tunes``, which answers +``application/json``; a ``text/event-stream`` reply could not be consumed +meaningfully on this path, so there is nothing to reproduce. +""" + +from __future__ import annotations + +import json +from typing import Any, Dict + +import requests + +from dashscope._shared._sdk_ident import get_sdk_headers, get_user_agent + +__all__ = ["SUBMIT_TIMEOUT", "post_submit"] + +# Mirrors 1.x's DEFAULT_REQUEST_TIMEOUT_SECONDS. +SUBMIT_TIMEOUT = 300 + +_JSON = "application/json; charset=utf-8" + +# Hard-coded rather than derived from __module__: see the module +# docstring. The backend already reports on the unmarked spelling. +_SDK_MODULE = "finetune" + + +def _headers(api_key: str) -> Dict[str, str]: + """Build the request headers in 1.x's insertion order.""" + headers = {"Content-Type": _JSON, "user-agent": get_user_agent()} + headers.update(get_sdk_headers(module=_SDK_MODULE)) + headers["Authorization"] = f"Bearer {api_key}" + headers["Accept"] = _JSON + return headers + + +def _success(response: requests.Response) -> Dict[str, Any]: + body = response.json() + output: Any = body.get("data") + if "output" in body: + output = body["output"] + request_id = body.pop("request_id", "") + if "data" not in body and "output" not in body: + output = body + return { + "request_id": request_id, + "status_code": response.status_code, + "code": body.get("code"), + "message": body.get("message", ""), + "output": output, + "usage": body.get("usage"), + "headers": dict(response.headers), + } + + +def _failure(response: requests.Response) -> Dict[str, Any]: + code: Any = "Unknown" + message: Any = response.content.decode("utf-8") + request_id = "" + if "application/json" in response.headers.get("content-type", ""): + error = response.json() + code = error.get("code") + message = error.get("message", "") + if "msg" in error: + message = error["msg"] + request_id = error.get("request_id", "") + return { + "request_id": request_id, + "status_code": response.status_code, + "code": code, + "message": message, + "output": None, + "usage": None, + "headers": dict(response.headers), + } + + +def post_submit( + request: Dict[str, Any], + *, + api_key: str, + base_url: str, + path: str, + timeout: int = SUBMIT_TIMEOUT, +) -> Dict[str, Any]: + """POST ``request`` and return the raw response envelope. + + The seven keys are the ones ``DashScopeAPIResponse`` carries, so the + 1.x surface can keep building ``FineTune(**envelope)`` and the 2.0 + surface can wrap the same dict in ``SubmitResult``. + + ``base_url`` is used verbatim. The RL constants hold no + ``{workspace_id}`` placeholder and ``submit_job`` never sends a + workspace, so 1.x's ``resolve_base_url`` and ``_workspace_header`` + were both no-ops on this path and are not reproduced. + + Args: + request: The job payload, serialized with ``ensure_ascii=False`` + so non-ASCII job names reach the server intact. + api_key: Bearer token; the caller resolves it. + base_url: Management-plane host, without a trailing slash. + path: Endpoint under ``base_url``. + timeout: Seconds for the whole request. + """ + url = f"{base_url.rstrip('/')}/{path.lstrip('/')}" + with requests.Session() as session: + response = session.post( + url, + data=json.dumps(request, ensure_ascii=False).encode("utf-8"), + headers=_headers(api_key), + timeout=timeout, + ) + if response.status_code in (200, 201): + return _success(response) + return _failure(response) diff --git a/dashscope/v2/finetune/agentic_rl.py b/dashscope/v2/finetune/agentic_rl.py new file mode 100644 index 00000000..8fa398cd --- /dev/null +++ b/dashscope/v2/finetune/agentic_rl.py @@ -0,0 +1,745 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The agentic-RL workflow driver, owned by the 2.0 package. + +``Client(...).rl`` is built on this class. The 1.x job-management surface +-- ``FineTunes`` / ``Deployments`` and the ``FineTune`` dataclasses -- +stays at ``dashscope.finetune``; the thin subclass there re-adds those +five verbs and swaps ``SubmitResult`` back for ``FineTune``. + +Two deliberate differences from the 1.x class this was split out of: + +- ``submit_job`` returns ``SubmitResult`` instead of a 1.x ``FineTune`` + dataclass, and posts through ``dashscope.v2.finetune._submit`` instead + of inheriting ``CreateMixin``. +- The api key is the constructor argument or ``DASHSCOPE_API_KEY``, never + the ``dashscope.api_key`` root global. ``set_api_key`` already writes + the argument into that env var and raises when it cannot, so resolution + here is unambiguous -- reading the root global could silently disagree + with the key this instance was built with. +""" + +from __future__ import annotations + +import os +import time +from typing import Union, List, Optional, ClassVar, Dict, Any +from typing_extensions import Self + +from dashscope.v2.finetune import _submit +from dashscope.v2.finetune.reinforcement import ( + AgenticRLFunctionComponent, + RolloutFunctionComponent, + RewardFunctionComponent, + Dataset, + TrainingDataset, + ValidationDataset, +) +from dashscope.v2.finetune.reinforcement import AgenticRLTuning, TuningModel +from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.v2.finetune.reinforcement import ( + FunctionType, + DatasetsType, + TrainingType, +) +from dashscope.v2.finetune.reinforcement import ( + RewardInput, + RolloutInput, + GroupRewardInput, +) +from dashscope.v2.finetune.reinforcement import logger +from dashscope.v2.finetune.reinforcement import ( + set_api_key, + generate_random_id, + get_func_type_id, + deep_remove_none, +) +from dashscope.v2.finetune.reinforcement.common.errors import ( + RegistrationError, + ValidationError, + InstanceQueryError, + RuntimeErrorWithCode, + ValueErrorWithCode, + DatasetsError, +) + +__all__ = ["AgenticRL", "SubmitOutput", "SubmitResult"] + + +class SubmitOutput: + """The ``output`` payload nested in a submit response. + + A plain attribute container on purpose: the server's job payload is + open-ended, and ``reinforcement_learning._to_job`` flattens it with + ``vars(output)``, which needs a ``__dict__``. + """ + + def __init__(self, **kwargs: Any): + self.__dict__.update(kwargs) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.__dict__!r})" + + +class SubmitResult: + """The envelope ``AgenticRL.submit_job`` returns. + + Carries the same seven fields as 1.x's ``DashScopeAPIResponse``, so + ``FineTune(**envelope)`` and ``SubmitResult(**envelope)`` accept the + same dict. A failed submit is a non-200 ``status_code`` here rather + than an exception, exactly as it was under ``FineTune``. + """ + + def __init__( + self, + status_code: int = None, + request_id: str = "", + code: str = None, + message: str = "", + output: Any = None, + usage: Any = None, + headers: Dict[str, Any] = None, + **kwargs: Any, + ): + self.__dict__.update(kwargs) + self.status_code = status_code + self.request_id = request_id + self.code = code + self.message = message + self.output = ( + SubmitOutput(**output) if isinstance(output, dict) else output + ) + self.usage = usage + self.headers = headers or {} + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(status_code={self.status_code!r}, " + f"request_id={self.request_id!r}, code={self.code!r}, " + f"message={self.message!r}, output={self.output!r}, " + f"usage={self.usage!r})" + ) + + +class AgenticRL(AgenticRLTuning): + SUB_PATH: ClassVar[str] = "fine-tunes" + + def __init__(self, api_key: str = None): + super().__init__() + + try: + set_api_key(api_key) + except Exception as e: + raise ValueErrorWithCode( + "Invalid API key configuration", + error_code=3001, + ) from e + + self._api_key = api_key + + def _resolve_api_key(self) -> str: + """The key to submit with; see the module docstring.""" + return self._api_key or os.environ.get("DASHSCOPE_API_KEY", "") + + def init(self, config_path: Optional[str] = None, **kwargs) -> Self: + """ + Initialize an AgenticRL instance from a YAML configuration file. + """ + self.tuning = TuningModel.load_from_yaml(config_path or "", **kwargs) + + return self + + async def register_functions( + self, + functions: Optional[ + Union[ + List[Union[RolloutFunctionComponent, RewardFunctionComponent]], + RolloutFunctionComponent, + RewardFunctionComponent, + ] + ] = None, + lazy_load: Optional[bool] = True, + ) -> tuple[ + List[str], + List[str], + List[str], + List[str], + List[str], + List[str], + ]: + """Register function components and return entity/instance IDs.""" + if functions: + self.tuning.functions = functions + + try: + ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) = await self.tuning.register_functions( + lazy_load=lazy_load, + ) + logger.info("Function components registered") + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RegistrationError( + "Function registration failed", + error_code=3002, + ) from e + + return ( + rollout_entity_ids, + reward_entity_ids, + group_reward_entity_ids, + rollout_instance_ids, + reward_instance_ids, + group_reward_instance_ids, + ) + + async def upload_datasets( + self, + datasets: Optional[List[Dataset]] = None, + training_files: Optional[Union[List[str], str]] = None, + validation_files: Optional[Union[List[str], str]] = None, + ) -> tuple[List[str], List[str]]: + if datasets: + self.tuning.datasets = datasets + + try: + ( + uploaded_training_ids, + uploaded_validation_ids, + ) = await self.tuning.upload_datasets( + training_files=training_files or [], + validation_files=validation_files or [], + ) + logger.info("Datasets uploaded") + except Exception as e: + raise DatasetsError( + "Datasets upload failed", + error_code=3003, + ) from e + + return uploaded_training_ids, uploaded_validation_ids + + def _build_submit_request( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + ) -> Dict[str, Any]: + """Assemble the job payload, folding the arguments into ``tuning``. + + Split out of ``submit_job`` so the 1.x surface can reuse it + without touching the transport. + """ + teacher_model = teacher_model or self.tuning.teacher_model + + # Resolve job name (fallback to class default) + if job_name: + self.tuning.name = job_name + job_name_with_suffix = f"{self.tuning.name}-{generate_random_id()[:8]}" + + # Model name + if model: + self.tuning.model.name = model + + # rollouts/rewards + if functions: + self.tuning.functions = functions + rollouts = self.tuning.combine_ids_runtimes( + functype=FunctionType.ROLLOUT, + ) + rewards = self.tuning.combine_ids_runtimes( + functype=FunctionType.REWARD, + ) + rewards.extend( + self.tuning.combine_ids_runtimes( + functype=FunctionType.GROUP_REWARD, + id_str=get_func_type_id(FunctionType.REWARD), + ), + ) + # names of functions + if not self.tuning.check_function_names(): + raise ValueErrorWithCode( + "Duplicate function names detected. All function names must " + "be unique.", + error_code=3004, + ) + + # datasets + if datasets: + self.tuning.datasets = datasets + training_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.TRAINING + ] + validation_datasets = [ + ds + for ds in self.tuning.datasets + if ds.type == DatasetsType.VALIDATION + ] + + # hyper_parameters + if hyper_parameters: + self.tuning.training.hyper_parameters = hyper_parameters + + # resources + if resources: + self.tuning.training.resources = resources + + training_type = self.tuning.training.type + if teacher_model: + training_type = TrainingType.PG_OPD + elif training_type == TrainingType.PG_OPD: + raise ValueErrorWithCode( + f"teacher_model is required when training.type is {training_type}", + error_code=3006, + ) + + request = { + "model": self.tuning.model.name, + "training_datasets": [ds.model_dump() for ds in training_datasets], + "validation_datasets": [ + ds.model_dump() for ds in validation_datasets + ], + "rollout": rollouts[0] if rollouts else None, + "rewards": rewards, + "hyper_parameters": self.tuning.training.hyper_parameters, + "resource_config": self.tuning.training.resources, + "training_type": str(training_type), + "teacher_model": teacher_model, + "job_name": job_name_with_suffix, + } + request = deep_remove_none(request) + logger.info(f"agentic_rl submit_job request: {request}") + + return request + + def _submit_envelope( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> Dict[str, Any]: + """Build the payload, POST it, and return the raw envelope. + + Shared by both surfaces: this class wraps the result in + ``SubmitResult``, the 1.x subclass in a ``FineTune`` dataclass. + ``kwargs`` are forwarded to ``post_submit`` (``path``, + ``timeout``, and overrides for ``api_key`` / ``base_url``). + """ + request = self._build_submit_request( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + ) + + kwargs.setdefault("api_key", self._resolve_api_key()) + kwargs.setdefault("base_url", DASHSCOPE_HTTP_BASE_URL) + kwargs.setdefault("path", self.SUB_PATH) + try: + return _submit.post_submit(request, **kwargs) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode( + "Job submission failed", + error_code=3005, + ) from e + + def submit_job( + self, + model: Optional[str] = None, + datasets: Optional[List[Dataset]] = None, + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: + """ + Submit RL tuning job to the platform. + """ + return SubmitResult( + **self._submit_envelope( + model=model, + datasets=datasets, + functions=functions, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ), + ) + + async def run( + self, + model: Optional[str] = None, + # Datasets parameters + training_datasets: Optional[List[TrainingDataset]] = None, + validation_datasets: Optional[List[ValidationDataset]] = None, + # Path-driven parameters (auto-register & upload) + functions: Optional[ + Union[ + List[ + Union[ + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ], + RolloutFunctionComponent, + RewardFunctionComponent, + AgenticRLFunctionComponent, + ] + ] = None, + # Common parameters + hyper_parameters: Optional[Dict[str, str]] = None, + resources: Optional[Dict[str, str]] = None, + job_name: Optional[str] = None, + teacher_model: Optional[str] = None, + **kwargs, + ) -> SubmitResult: + """ + Execute RL tuning workflow. + """ + try: + logger.info( + "Path-Driven mode: Registering functions & uploading " + "datasets...", + ) + await self.register_functions( + functions=functions, + lazy_load=True, + ) + + datasets = list(training_datasets or []) + list( + validation_datasets or [], + ) + await self.upload_datasets( + datasets=datasets, + ) + + return self.submit_job( + model=model, + datasets=datasets, + hyper_parameters=hyper_parameters, + resources=resources, + job_name=job_name, + teacher_model=teacher_model, + **kwargs, + ) + except Exception as e: + if hasattr(e, "error_code"): + raise + raise RuntimeErrorWithCode( + "RL tuning workflow failed", + error_code=3006, + ) from e + + @classmethod + async def test_functions( + cls, + instance_id: str, + functype: FunctionType, + input_data: Dict[str, Any], + api_key: str = None, + pull_logs: bool = False, + log_page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ): + """Test a deployed function instance with custom input data. + + Args: + instance_id: Target function instance ID. + functype: Function type (ROLLOUT/REWARD/GROUP_REWARD). + input_data: Test input payload. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + pull_logs: If True, pull all logs of the function instance + (with pagination) after verification and print them + between separator markers. + log_page_size: Page size used when pulling logs. + start_time: Optional start time filter (in seconds) for log + pulling. Defaults to 24 hours before ``end_time``. + end_time: Optional end time filter (in seconds) for log + pulling. Defaults to the current time (now). + """ + try: + set_api_key(api_key) + + if functype == FunctionType.ROLLOUT: + value = RolloutInput.model_validate(input_data) + elif functype == FunctionType.REWARD: + value = RewardInput.model_validate(input_data) + elif functype == FunctionType.GROUP_REWARD: + value = GroupRewardInput.model_validate(input_data) + else: + raise ValueErrorWithCode( + f"Unsupported function type: {functype}", + error_code=3007, + ) + + logger.info( + f"Starting {str(functype)} verification", + extra={ + "instance_id": instance_id, + "input_params": value.model_dump(exclude={"api_key"}), + }, + ) + + result = await AgenticRLFunctionComponent.verify_function( + value, + instance_id, + ) + + if pull_logs: + await cls._pull_function_instance_logs( + function_instance_id=instance_id, + page_size=log_page_size, + start_time=start_time, + end_time=end_time, + ) + + return result + + except Exception as e: + raise ValidationError( + "Function test failed", + error_code=3008, + ) from e + + @classmethod + async def query_function_instance_logs( + cls, + function_instance_id: str, + page_number: int = 1, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, + api_key: str = None, + ) -> Dict[str, Any]: + """Query one page of logs for a function (faas) instance. + + Args: + function_instance_id: Target function instance ID. + page_number: Page number, starting from 1. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the log query API. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_function_instance_logs( + function_instance_id=function_instance_id, + page_number=page_number, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3009, + ) from e + + @classmethod + async def query_all_function_instance_logs( + cls, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + keywords: Optional[List[str]] = None, + api_key: str = None, + ) -> List[str]: + """Fetch all logs of a function (faas) instance. + + Pages are fetched one by one via the paginated log query until + all entries are collected. + + Args: + function_instance_id: Target function instance ID. + page_size: Number of log entries per page. + start_time: Optional start time filter (in seconds). + end_time: Optional end time filter (in seconds). + keywords: Optional keyword filters for log messages. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Aggregated list of log messages across all pages. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + keywords=keywords, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance log query failed", + error_code=3010, + ) from e + + @classmethod + async def delete_function_instance( + cls, + function_instance_id: str, + api_key: str = None, + ) -> Dict[str, Any]: + """Delete a function (faas) runtime instance. + + Args: + function_instance_id: Target function instance ID. + api_key: DashScope API key (uses DASHSCOPE_API_KEY env var + if omitted). + + Returns: + Raw response dict of the delete API, containing + ``data.status`` (e.g. "deleted") and + ``data.sandbox_code``. + + Raises: + InstanceQueryError: If the deletion fails. + """ + try: + set_api_key(api_key) + + fc_component = AgenticRLFunctionComponent + return await fc_component.delete_function_instance( + function_instance_id=function_instance_id, + ) + + except Exception as e: + if hasattr(e, "error_code"): + raise + raise InstanceQueryError( + "Function instance deletion failed", + error_code=3011, + ) from e + + @classmethod + async def _pull_function_instance_logs( + cls, + function_instance_id: str, + page_size: int = 100, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + ) -> None: + """Pull all logs of a function instance and print them between + separator markers (best effort, never raises). + + When ``start_time``/``end_time`` are not provided, defaults to + pulling logs from 24 hours ago until now. + """ + if end_time is None: + end_time = int(time.time()) + if start_time is None: + start_time = end_time - 24 * 60 * 60 + logger.info( + "************start query log " + f"(function_instance_id={function_instance_id})*******", + ) + try: + logs = await cls.query_all_function_instance_logs( + function_instance_id=function_instance_id, + page_size=page_size, + start_time=start_time, + end_time=end_time, + ) + for entry in logs: + logger.info(f"[function instance log] {entry}") + logger.info( + f"Pulled {len(logs)} log entries for function instance " + f"{function_instance_id}", + ) + except Exception as e: + logger.warning( + f"Failed to pull logs for function instance " + f"{function_instance_id}: {e}", + ) + finally: + logger.info("************end query log *******") diff --git a/dashscope/v2/finetune/reinforcement/__init__.py b/dashscope/v2/finetune/reinforcement/__init__.py new file mode 100644 index 00000000..391e24fd --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/__init__.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +try: + import pydantic # noqa: F401 + import tenacity # noqa: F401 + import yaml # noqa: F401 +except ImportError as _rl_dep_err: + raise ImportError( + "Agentic RL fine-tuning dependencies are missing from this " + "install. Reinstall with: pip install -U dashscope", + ) from _rl_dep_err + +from dashscope.v2.finetune.reinforcement.common.constants import ( + BAILIAN_FILE_API, + BAILIAN_FILE_TIMEOUT, + DASHSCOPE_API_KEY, + DASHSCOPE_HTTP_BASE_URL, + LOG_LEVEL, + FC_API_KEY, + FC_DELETE_API, + FC_FILES_START, + FC_LOAD_API, + FC_PYPI_LIB, + FC_PYPI_REPO, + FC_QUERY_API, + FC_QUERY_LOG_API, + FC_REGISTER_REWARD_API, + FC_REGISTER_ROLLOUT_API, + FC_REGISTER_GROUP_REWARD_API, + FC_SERVER_CLASSPATH, + FC_UPLOAD_OSS_API, + FC_WORKERS_COUNT, + FC_LAYER_CREATE_API, + FC_LAYER_QUERY_API, + FC_LAYER_USED, + FC_LAYER_NAME, + FC_REQUIREMENTS_FILE, + FC_ZIP_EXCLUDE_PATTERNS, + FC_OSS_FILE_SIZE_WARNING, + DATASETS_FILE_SIZE_WARNING, + HTTP_REQUEST_TIMEOUT, + LOGGER_FILTER_FIELDS, +) +from dashscope.v2.finetune.reinforcement.common.errors import ( + AgenticRLError, + InputError, + OutputError, + BaseConnectionError, + OSSConnectionError, + OSSUploadError, + DeploymentError, + RegistrationError, + FunctionLoadError, + InstanceWarmupError, + InstanceQueryError, + FunctionLayerError, + ValidationError, + ConfigurationError, + BasePermissionError, + IOErrorWithCode, + RuntimeErrorWithCode, + ValueErrorWithCode, +) +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model import ( + FunctionComponentModel, + TuningModel, + FoundationModel, + FunctionComponentRuntime, + AgenticRLFunctionComponent, + RolloutFunctionComponent, + RewardFunctionComponent, + GroupRewardFunctionComponent, + Datasets, + Dataset, + TrainingDataset, + ValidationDataset, + MountStorage, + Training, + Observability, + AgenticRLTuning, + Models, +) +from dashscope.v2.finetune.reinforcement.common.model_types import ( + FileSpec, + FunctionType, + DatasetsType, + DataSourceType, + RequestFC, + ResponseFC, + Status, + StatusType, + TrainingType, +) +from dashscope.v2.finetune.reinforcement.common.utils import ( + check_file, + client_fc, + create_deployment_files, + deep_mask, + generate_random_id, + get_filepath_classname, + upload_zip_to_oss_and_by_signed_url, + set_api_key, + to_bailian_data, + zip_dir, + get_func_type_id, + deep_remove_none, + extract_reward_weights, + get_weights_from_file, + serialize_for_output, +) +from dashscope.v2.finetune.reinforcement.component import ( + reward_func, + sub_reward_func, + aggregate_func, +) +from dashscope.v2.finetune.reinforcement.component.data import ( + AgentOutput, + Reward, + RewardInput, + RewardOutput, + RolloutInput, + RolloutOutput, + GroupReward, + GroupRewardInput, + GroupRewardOutput, + TaskStatus, +) +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( + RewardProcessorMeta, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractRolloutProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor import ( + AbstractGroupRewardProcessor, +) + + +__all__ = [ + # Constants + "BAILIAN_FILE_API", + "BAILIAN_FILE_TIMEOUT", + "DASHSCOPE_API_KEY", + "DASHSCOPE_HTTP_BASE_URL", + "LOG_LEVEL", + "FC_API_KEY", + "FC_DELETE_API", + "FC_FILES_START", + "FC_LOAD_API", + "FC_PYPI_LIB", + "FC_PYPI_REPO", + "FC_QUERY_API", + "FC_QUERY_LOG_API", + "FC_REGISTER_REWARD_API", + "FC_REGISTER_ROLLOUT_API", + "FC_REGISTER_GROUP_REWARD_API", + "FC_SERVER_CLASSPATH", + "FC_UPLOAD_OSS_API", + "FC_WORKERS_COUNT", + "FC_LAYER_CREATE_API", + "FC_LAYER_QUERY_API", + "FC_LAYER_USED", + "FC_LAYER_NAME", + "FC_REQUIREMENTS_FILE", + "FC_ZIP_EXCLUDE_PATTERNS", + "FC_OSS_FILE_SIZE_WARNING", + "DATASETS_FILE_SIZE_WARNING", + "HTTP_REQUEST_TIMEOUT", + "LOGGER_FILTER_FIELDS", + # Errors + "AgenticRLError", + "InputError", + "OutputError", + "BaseConnectionError", + "OSSConnectionError", + "OSSUploadError", + "DeploymentError", + "RegistrationError", + "FunctionLoadError", + "FunctionLayerError", + "InstanceWarmupError", + "InstanceQueryError", + "ValidationError", + "ConfigurationError", + "BasePermissionError", + "IOErrorWithCode", + "RuntimeErrorWithCode", + "ValueErrorWithCode", + # Logging + "logger", + # Utilities + "check_file", + "client_fc", + "create_deployment_files", + "deep_mask", + "generate_random_id", + "get_filepath_classname", + "set_api_key", + "to_bailian_data", + "upload_zip_to_oss_and_by_signed_url", + "zip_dir", + "get_func_type_id", + "deep_remove_none", + "extract_reward_weights", + "get_weights_from_file", + "serialize_for_output", + # Model Types + "FileSpec", + "FunctionType", + "DatasetsType", + "DataSourceType", + "RequestFC", + "ResponseFC", + "Status", + "StatusType", + "TrainingType", + # Data models + "RolloutInput", + "RolloutOutput", + "RewardInput", + "RewardOutput", + "GroupRewardInput", + "GroupRewardOutput", + "GroupReward", + "Reward", + "AgentOutput", + "TaskStatus", + # Func Manager + "reward_func", + "sub_reward_func", + "aggregate_func", + "RewardProcessorMeta", + "AbstractRolloutProcessor", + "AbstractRewardProcessor", + "AbstractGroupRewardProcessor", + # Core Models + "FunctionComponentModel", + "TuningModel", + "FoundationModel", + "FunctionComponentRuntime", + "AgenticRLFunctionComponent", + "RolloutFunctionComponent", + "RewardFunctionComponent", + "GroupRewardFunctionComponent", + "Datasets", + "Dataset", + "TrainingDataset", + "ValidationDataset", + "MountStorage", + "Training", + "Observability", + "AgenticRLTuning", + "Models", +] diff --git a/dashscope/v2/finetune/reinforcement/common/__init__.py b/dashscope/v2/finetune/reinforcement/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/common/constants.py b/dashscope/v2/finetune/reinforcement/common/constants.py similarity index 90% rename from dashscope/finetune/reinforcement/common/constants.py rename to dashscope/v2/finetune/reinforcement/common/constants.py index e87de62e..dcab4d9e 100644 --- a/dashscope/finetune/reinforcement/common/constants.py +++ b/dashscope/v2/finetune/reinforcement/common/constants.py @@ -35,6 +35,9 @@ def get_int_env(env_var: str, default: int) -> int: # -------------------------- # Logging Configuration # -------------------------- +# The logger name is an observability contract: user log filters and +# existing pipelines match on it, so it keeps the published name even +# though the package itself now lives under ``dashscope.v2``. LOGGER_NAME = os.environ.get("LOGGER_NAME", "dashscope.finetune.reinforcement") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info").upper() LOGGER_FILTER_FIELDS = { @@ -112,6 +115,10 @@ def get_int_env(env_var: str, default: int) -> int: ) FC_SERVER_CLASSPATH = os.environ.get( "FC_SERVER_CLASSPATH", + # Written into the start.sh deployed to the Function Compute + # sandbox, where the installed dashscope may be any published + # version. The public path keeps resolving through the + # reverse-reference shim at dashscope/finetune/reinforcement. "dashscope.finetune.reinforcement.component.server.server", ) FC_WORKERS_COUNT = get_int_env("FC_WORKERS_COUNT", 2) diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/v2/finetune/reinforcement/common/errors.py similarity index 100% rename from dashscope/finetune/reinforcement/common/errors.py rename to dashscope/v2/finetune/reinforcement/common/errors.py diff --git a/dashscope/finetune/reinforcement/common/log.py b/dashscope/v2/finetune/reinforcement/common/log.py similarity index 91% rename from dashscope/finetune/reinforcement/common/log.py rename to dashscope/v2/finetune/reinforcement/common/log.py index 3db44a58..0dd13fb0 100644 --- a/dashscope/finetune/reinforcement/common/log.py +++ b/dashscope/v2/finetune/reinforcement/common/log.py @@ -2,7 +2,7 @@ import logging import sys -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.v2.finetune.reinforcement.common.constants import ( LOGGER_NAME, LOG_LEVEL, ) diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/v2/finetune/reinforcement/common/model.py similarity index 99% rename from dashscope/finetune/reinforcement/common/model.py rename to dashscope/v2/finetune/reinforcement/common/model.py index d0eb5a25..8baf5eb3 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/v2/finetune/reinforcement/common/model.py @@ -23,7 +23,7 @@ import yaml # Local Application -from dashscope.finetune.reinforcement.common.constants import ( +from dashscope.v2.finetune.reinforcement.common.constants import ( FC_API_KEY, FC_DELETE_API, FC_LOAD_API, @@ -40,7 +40,7 @@ FC_LAYER_QUERY_API, LOG_LEVEL, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, DatasetsType, @@ -51,7 +51,7 @@ StatusType, TrainingType, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, RolloutInput, @@ -59,7 +59,7 @@ GroupRewardInput, GroupRewardOutput, ) -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.v2.finetune.reinforcement.common.utils import ( check_file, client_fc, create_deployment_files, @@ -73,8 +73,8 @@ get_weights_from_file, deep_remove_none, ) -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.errors import ( InputError, OutputError, OSSConnectionError, diff --git a/dashscope/finetune/reinforcement/common/model_types.py b/dashscope/v2/finetune/reinforcement/common/model_types.py similarity index 100% rename from dashscope/finetune/reinforcement/common/model_types.py rename to dashscope/v2/finetune/reinforcement/common/model_types.py diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/v2/finetune/reinforcement/common/utils.py similarity index 99% rename from dashscope/finetune/reinforcement/common/utils.py rename to dashscope/v2/finetune/reinforcement/common/utils.py index a3d58a57..64a583d1 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/v2/finetune/reinforcement/common/utils.py @@ -20,9 +20,9 @@ retry_if_exception_type, ) -from dashscope.common.utils import get_sdk_headers -from dashscope.finetune.reinforcement import logger -from dashscope.finetune.reinforcement.common.errors import ( +from dashscope._shared._sdk_ident import get_sdk_headers +from dashscope.v2.finetune.reinforcement import logger +from dashscope.v2.finetune.reinforcement.common.errors import ( InputError, OutputError, ConfigurationError, @@ -30,7 +30,7 @@ RuntimeErrorWithCode, OSSUploadError, ) -from dashscope.finetune.reinforcement import ( +from dashscope.v2.finetune.reinforcement import ( LOG_LEVEL, DASHSCOPE_API_KEY, BAILIAN_FILE_API, @@ -47,7 +47,7 @@ LOGGER_FILTER_FIELDS, FC_WORKERS_COUNT, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FileSpec, FunctionType, ) diff --git a/dashscope/finetune/reinforcement/component/__init__.py b/dashscope/v2/finetune/reinforcement/component/__init__.py similarity index 74% rename from dashscope/finetune/reinforcement/component/__init__.py rename to dashscope/v2/finetune/reinforcement/component/__init__.py index 93d768ba..bc2a7334 100644 --- a/dashscope/finetune/reinforcement/component/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/__init__.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- # # Base Components # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -17,28 +17,28 @@ # -------------------------------------------------------------------------- # # Data Models # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, RewardOutput, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) # -------------------------------------------------------------------------- # # Demos # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.demo import ( +from dashscope.v2.finetune.reinforcement.component.demo import ( DemoRewardProcessor, DemoRolloutProcessor, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( reward_func, sub_reward_func, aggregate_func, @@ -47,7 +47,7 @@ # -------------------------------------------------------------------------- # # Func Manager # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.func_manager import ( +from dashscope.v2.finetune.reinforcement.component.func_manager import ( FuncManager, ) @@ -55,30 +55,30 @@ # Observability # # -------------------------------------------------------------------------- # # pylint: disable=no-name-in-module -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.v2.finetune.reinforcement.component.observability import ( observe_processor, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) # -------------------------------------------------------------------------- # # Parsers # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.parser.reward_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser.rollout_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( RolloutRequestParser, ) # -------------------------------------------------------------------------- # # Processors # # -------------------------------------------------------------------------- # -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/data/__init__.py b/dashscope/v2/finetune/reinforcement/component/data/__init__.py similarity index 70% rename from dashscope/finetune/reinforcement/component/data/__init__.py rename to dashscope/v2/finetune/reinforcement/component/data/__init__.py index 6277aa74..51727288 100644 --- a/dashscope/finetune/reinforcement/component/data/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/data/__init__.py @@ -2,7 +2,7 @@ # ========================================================================== # # Base Components # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, TaskStatus, ModelProtocol, @@ -12,10 +12,10 @@ RequestMetadata, AgentOutput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) @@ -23,21 +23,21 @@ # ========================================================================== # # Input Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) # ========================================================================== # # Output Models # # ========================================================================== # -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) diff --git a/dashscope/finetune/reinforcement/component/data/base_data_model.py b/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py similarity index 98% rename from dashscope/finetune/reinforcement/component/data/base_data_model.py rename to dashscope/v2/finetune/reinforcement/component/data/base_data_model.py index c1970c53..1b59e38b 100644 --- a/dashscope/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/v2/finetune/reinforcement/component/data/base_data_model.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_input.py b/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py similarity index 90% rename from dashscope/finetune/reinforcement/component/data/group_reward_input.py rename to dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py index acd897f6..dc03f39d 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/group_reward_input.py @@ -9,10 +9,10 @@ from typing import Any, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/group_reward_output.py b/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py similarity index 90% rename from dashscope/finetune/reinforcement/component/data/group_reward_output.py rename to dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py index 07208c84..a1fdd8bc 100644 --- a/dashscope/finetune/reinforcement/component/data/group_reward_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/group_reward_output.py @@ -8,10 +8,10 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, ) diff --git a/dashscope/finetune/reinforcement/component/data/reward_input.py b/dashscope/v2/finetune/reinforcement/component/data/reward_input.py similarity index 89% rename from dashscope/finetune/reinforcement/component/data/reward_input.py rename to dashscope/v2/finetune/reinforcement/component/data/reward_input.py index f21bd71e..ef7435a7 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/reward_input.py @@ -9,10 +9,10 @@ from typing import Any, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, AgentOutput, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/reward_output.py b/dashscope/v2/finetune/reinforcement/component/data/reward_output.py similarity index 96% rename from dashscope/finetune/reinforcement/component/data/reward_output.py rename to dashscope/v2/finetune/reinforcement/component/data/reward_output.py index 93c3061c..1264d780 100644 --- a/dashscope/finetune/reinforcement/component/data/reward_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/reward_output.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, _normalize_error_code, ) diff --git a/dashscope/finetune/reinforcement/component/data/rollout_input.py b/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py similarity index 93% rename from dashscope/finetune/reinforcement/component/data/rollout_input.py rename to dashscope/v2/finetune/reinforcement/component/data/rollout_input.py index ea7bfa1a..485da9ef 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_input.py +++ b/dashscope/v2/finetune/reinforcement/component/data/rollout_input.py @@ -9,10 +9,10 @@ from typing import Any, Dict, List, Optional from pydantic import ConfigDict, Field -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ModelResource, RequestMetadata, diff --git a/dashscope/finetune/reinforcement/component/data/rollout_output.py b/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py similarity index 95% rename from dashscope/finetune/reinforcement/component/data/rollout_output.py rename to dashscope/v2/finetune/reinforcement/component/data/rollout_output.py index 1b923558..a2518126 100644 --- a/dashscope/finetune/reinforcement/component/data/rollout_output.py +++ b/dashscope/v2/finetune/reinforcement/component/data/rollout_output.py @@ -9,7 +9,7 @@ from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, AgentOutput, _normalize_error_code, diff --git a/dashscope/v2/finetune/reinforcement/component/demo/__init__.py b/dashscope/v2/finetune/reinforcement/component/demo/__init__.py new file mode 100644 index 00000000..f43bc02a --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/demo/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.demo.group_reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoGroupRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.demo.reward_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.demo.rollout_processor_demo import ( # noqa: E501 # pylint: disable=line-too-long + DemoRolloutProcessor, +) + +__all__ = [ + "DemoRewardProcessor", + # Inherits AbstractRewardProcessor, returns RewardOutput + "DemoRolloutProcessor", + # Inherits AbstractRolloutProcessor, returns RolloutOutput + "DemoGroupRewardProcessor", + # Inherits AbstractGroupRewardProcessor, returns GroupRewardOutput +] diff --git a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py index a43688d6..dd3476d9 100644 --- a/dashscope/finetune/reinforcement/component/demo/group_reward_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/group_reward_processor_demo.py @@ -6,21 +6,21 @@ Demonstrates rule-based scoring for multiple Agent outputs in a group. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_input import ( # noqa: E501 GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.data.group_reward_output import ( # noqa: E501 GroupRewardOutput, GroupReward, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractGroupRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py index d99ad89a..2ac8bc01 100644 --- a/dashscope/finetune/reinforcement/component/demo/reward_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/reward_processor_demo.py @@ -6,18 +6,18 @@ Demonstrates rule-based scoring for Agent outputs. """ -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, Reward, ) -from dashscope.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRewardProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py b/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py similarity index 85% rename from dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py rename to dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py index 8099cc18..51a5f74f 100644 --- a/dashscope/finetune/reinforcement/component/demo/rollout_processor_demo.py +++ b/dashscope/v2/finetune/reinforcement/component/demo/rollout_processor_demo.py @@ -8,18 +8,18 @@ import time -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( AgentOutput, TaskStatus, ) -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/func_decorator.py b/dashscope/v2/finetune/reinforcement/component/func_decorator.py similarity index 98% rename from dashscope/finetune/reinforcement/component/func_decorator.py rename to dashscope/v2/finetune/reinforcement/component/func_decorator.py index e10d9464..26a99c23 100644 --- a/dashscope/finetune/reinforcement/component/func_decorator.py +++ b/dashscope/v2/finetune/reinforcement/component/func_decorator.py @@ -4,8 +4,8 @@ from functools import wraps from typing import Callable, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data import ( RewardInput, RewardOutput, Reward, diff --git a/dashscope/finetune/reinforcement/component/func_manager.py b/dashscope/v2/finetune/reinforcement/component/func_manager.py similarity index 93% rename from dashscope/finetune/reinforcement/component/func_manager.py rename to dashscope/v2/finetune/reinforcement/component/func_manager.py index a1ee558c..d135ff43 100644 --- a/dashscope/finetune/reinforcement/component/func_manager.py +++ b/dashscope/v2/finetune/reinforcement/component/func_manager.py @@ -20,31 +20,31 @@ from functools import partial from typing import Any, Dict, Optional, Type -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, trace_processor_process, async_trace_processor_process, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( BaseRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( GroupRewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( RewardRequestParser, ) -from dashscope.finetune.reinforcement.component.parser import ( +from dashscope.v2.finetune.reinforcement.component.parser import ( RolloutRequestParser, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/__init__.py b/dashscope/v2/finetune/reinforcement/component/observability/__init__.py similarity index 91% rename from dashscope/finetune/reinforcement/component/observability/__init__.py rename to dashscope/v2/finetune/reinforcement/component/observability/__init__.py index 96a00c34..5701ec1e 100644 --- a/dashscope/finetune/reinforcement/component/observability/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/__init__.py @@ -4,7 +4,7 @@ This module provides a *stable public surface* while keeping heavy observability dependencies lazily imported. User-facing imports such as:: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( observe_processor, trace_client, trace_tool, @@ -81,7 +81,7 @@ def __getattr__(name: str) -> Any: "trace_client", "trace_tool", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( genai as _genai, ) @@ -89,7 +89,7 @@ def __getattr__(name: str) -> Any: # Processor-span helpers if name in {"observe_processor", "trace_processor_span"}: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( processor_span as _processor_span, ) @@ -113,7 +113,7 @@ def __getattr__(name: str) -> Any: "span_payload_preview", "trace_processor_process", }: - from dashscope.finetune.reinforcement.component.observability import ( + from dashscope.v2.finetune.reinforcement.component.observability import ( tracing as _tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py similarity index 87% rename from dashscope/finetune/reinforcement/component/observability/genai/__init__.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py index 2beb244e..21670a57 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/__init__.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/__init__.py @@ -25,23 +25,23 @@ from typing import Any -from dashscope.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_dashscope import ( # noqa: E501 # pylint: disable=line-too-long instrument_dashscope_generation_call, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_openai import ( # noqa: E501 # pylint: disable=line-too-long instrument_openai_chat_completions, ) -from dashscope.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.llm_span import ( # noqa: E501 # pylint: disable=line-too-long genai_llm_span, observe_llm, run_with_genai_llm_span, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.observability.genai.tools import ( # noqa: E501 # pylint: disable=line-too-long observe_tool, trace_tool, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/_core.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py similarity index 100% rename from dashscope/finetune/reinforcement/component/observability/genai/_core.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/_core.py diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py similarity index 93% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py index 2676e132..b72c7d40 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_dashscope.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_dashscope.py @@ -7,16 +7,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( # noqa: E501 is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py index 3814c38f..0a0dd205 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_openai.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_openai.py @@ -16,17 +16,17 @@ from opentelemetry import trace as otel_trace from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long openai_chat_messages_to_input_messages, openai_completion_to_output_messages, unwrap_openai_completion, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, log_trace_id, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py similarity index 95% rename from dashscope/finetune/reinforcement/component/observability/genai/llm_span.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py index 62e435ce..645a39d0 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/llm_span.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/llm_span.py @@ -12,16 +12,16 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai._core import ( # noqa: E501 GENAI_AVAILABLE, get_handler, ) -from dashscope.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.observability.genai.messages import ( # noqa: E501 # pylint: disable=line-too-long dashscope_response_to_output_messages, openai_chat_messages_to_input_messages, openai_completion_to_output_messages, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( is_tracing_enabled, ) diff --git a/dashscope/finetune/reinforcement/component/observability/genai/messages.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/genai/messages.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py index 44abe0f5..e1824654 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/messages.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/messages.py @@ -27,9 +27,9 @@ import os from typing import Any, Dict, List, Optional -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import deep_mask -from dashscope.finetune.reinforcement.component.observability import genai +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.utils import deep_mask +from dashscope.v2.finetune.reinforcement.component.observability import genai # pylint: disable=protected-access _core = genai._core diff --git a/dashscope/finetune/reinforcement/component/observability/genai/tools.py b/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py similarity index 99% rename from dashscope/finetune/reinforcement/component/observability/genai/tools.py rename to dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py index c0d48b85..c6c260e5 100644 --- a/dashscope/finetune/reinforcement/component/observability/genai/tools.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/genai/tools.py @@ -50,7 +50,7 @@ from opentelemetry.trace.status import Status, StatusCode -from dashscope.finetune.reinforcement.component.observability import ( +from dashscope.v2.finetune.reinforcement.component.observability import ( genai, tracing, ) diff --git a/dashscope/finetune/reinforcement/component/observability/processor_span.py b/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py similarity index 97% rename from dashscope/finetune/reinforcement/component/observability/processor_span.py rename to dashscope/v2/finetune/reinforcement/component/observability/processor_span.py index 65fc3824..3014ab13 100644 --- a/dashscope/finetune/reinforcement/component/observability/processor_span.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/processor_span.py @@ -34,11 +34,11 @@ from functools import wraps from typing import Any, Callable, Dict, Optional, TypeVar, cast -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( _REWARD_SPAN_PREFIX, _ROLLOUT_SPAN_PREFIX, apply_processor_span_attributes_after, @@ -233,7 +233,7 @@ def observe_processor( Replaces the explicit ``@trace_processor_span(logical_kind=...)`` style. """ - from dashscope.finetune.reinforcement.component.processor import ( + from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractRewardProcessor, AbstractRolloutProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/observability/tracing.py b/dashscope/v2/finetune/reinforcement/component/observability/tracing.py similarity index 98% rename from dashscope/finetune/reinforcement/component/observability/tracing.py rename to dashscope/v2/finetune/reinforcement/component/observability/tracing.py index 1da284b7..447109e4 100644 --- a/dashscope/finetune/reinforcement/component/observability/tracing.py +++ b/dashscope/v2/finetune/reinforcement/component/observability/tracing.py @@ -36,11 +36,11 @@ from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) @@ -64,6 +64,9 @@ _DEFAULT_FORCE_FLUSH_TIMEOUT_MS = 3000 _MAX_FORCE_FLUSH_TIMEOUT_MS = 60_000 +# OpenTelemetry instrumentation scope name. Renaming it would split +# already-exported traces across two scopes, so it keeps the published +# name even though the package now lives under ``dashscope.v2``. _TRACER_NAME = "dashscope.finetune.reinforcement" # I/O preview limits for span attributes. _IO_MAX_LEN = 128 * 1024 # 128KB per input/output preview diff --git a/dashscope/v2/finetune/reinforcement/component/parser/__init__.py b/dashscope/v2/finetune/reinforcement/component/parser/__init__.py new file mode 100644 index 00000000..bb442998 --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/parser/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( + BaseRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.group_reward_parser import ( # noqa: E501 + GroupRewardRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.reward_parser import ( + RewardRequestParser, +) +from dashscope.v2.finetune.reinforcement.component.parser.rollout_parser import ( + RolloutRequestParser, +) + +__all__ = [ + "BaseRequestParser", + "RewardRequestParser", + "RolloutRequestParser", + "GroupRewardRequestParser", +] diff --git a/dashscope/finetune/reinforcement/component/parser/base_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py similarity index 90% rename from dashscope/finetune/reinforcement/component/parser/base_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/base_parser.py index fac27693..0d7d3b6f 100644 --- a/dashscope/finetune/reinforcement/component/parser/base_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/base_parser.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py similarity index 81% rename from dashscope/finetune/reinforcement/component/parser/group_reward_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py index aceb78d7..e03c850e 100644 --- a/dashscope/finetune/reinforcement/component/parser/group_reward_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/group_reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/reward_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py similarity index 79% rename from dashscope/finetune/reinforcement/component/parser/reward_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py index 0dff8195..c8d36b51 100644 --- a/dashscope/finetune/reinforcement/component/parser/reward_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/reward_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py b/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py similarity index 79% rename from dashscope/finetune/reinforcement/component/parser/rollout_parser.py rename to dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py index 0aaed03b..738b6104 100644 --- a/dashscope/finetune/reinforcement/component/parser/rollout_parser.py +++ b/dashscope/v2/finetune/reinforcement/component/parser/rollout_parser.py @@ -7,11 +7,11 @@ from typing import Any, Dict -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.parser.base_parser import ( +from dashscope.v2.finetune.reinforcement.component.parser.base_parser import ( BaseRequestParser, ) diff --git a/dashscope/v2/finetune/reinforcement/component/processor/__init__.py b/dashscope/v2/finetune/reinforcement/component/processor/__init__.py new file mode 100644 index 00000000..b579f7a4 --- /dev/null +++ b/dashscope/v2/finetune/reinforcement/component/processor/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from dashscope.v2.finetune.reinforcement.component.processor.abstract_group_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractGroupRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_reward_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRewardProcessor, +) +from dashscope.v2.finetune.reinforcement.component.processor.abstract_rollout_processor import ( # noqa: E501 # pylint: disable=line-too-long + AbstractRolloutProcessor, +) + +__all__ = [ + "AbstractProcessor", + "AbstractRewardProcessor", + "AbstractRolloutProcessor", + "AbstractGroupRewardProcessor", +] diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py similarity index 90% rename from dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py index 5a8f60a2..df618859 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_group_reward_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_group_reward_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardInput, ) -from dashscope.finetune.reinforcement.component.data import ( +from dashscope.v2.finetune.reinforcement.component.data import ( GroupRewardOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py similarity index 94% rename from dashscope/finetune/reinforcement/component/processor/abstract_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py index 8f13afd3..f4fd4f5a 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_processor.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from dashscope.finetune.reinforcement.component.data.base_data_model import ( +from dashscope.v2.finetune.reinforcement.component.data.base_data_model import ( BaseDataModel, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py similarity index 95% rename from dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py index 0b64262f..c9b4aec6 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_reward_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_reward_processor.py @@ -14,18 +14,18 @@ from concurrent.futures import ThreadPoolExecutor, Executor from typing import Optional, Dict -from dashscope.finetune.reinforcement.component.data.reward_input import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_input import ( RewardInput, ) -from dashscope.finetune.reinforcement.component.data.reward_output import ( +from dashscope.v2.finetune.reinforcement.component.data.reward_output import ( RewardOutput, ) -from dashscope.finetune.reinforcement.component.func_decorator import ( +from dashscope.v2.finetune.reinforcement.component.func_decorator import ( RewardProcessorMeta, SubRewardFunction, AggregateFunction, ) -from dashscope.finetune.reinforcement.component.processor import ( +from dashscope.v2.finetune.reinforcement.component.processor import ( AbstractProcessor, ) diff --git a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py b/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py similarity index 88% rename from dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py rename to dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py index 9c6dace9..bace5d53 100644 --- a/dashscope/finetune/reinforcement/component/processor/abstract_rollout_processor.py +++ b/dashscope/v2/finetune/reinforcement/component/processor/abstract_rollout_processor.py @@ -9,13 +9,13 @@ from abc import abstractmethod -from dashscope.finetune.reinforcement.component.data.rollout_input import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_input import ( RolloutInput, ) -from dashscope.finetune.reinforcement.component.data.rollout_output import ( +from dashscope.v2.finetune.reinforcement.component.data.rollout_output import ( RolloutOutput, ) -from dashscope.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long +from dashscope.v2.finetune.reinforcement.component.processor.abstract_processor import ( # noqa: E501 # pylint: disable=line-too-long AbstractProcessor, ) diff --git a/dashscope/v2/finetune/reinforcement/component/server/__init__.py b/dashscope/v2/finetune/reinforcement/component/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/v2/finetune/reinforcement/component/server/server.py similarity index 96% rename from dashscope/finetune/reinforcement/component/server/server.py rename to dashscope/v2/finetune/reinforcement/component/server/server.py index ce2d400d..854e2699 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/v2/finetune/reinforcement/component/server/server.py @@ -47,17 +47,19 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from dashscope.finetune.reinforcement.common.log import logger -from dashscope.finetune.reinforcement.common.utils import ( +from dashscope.v2.finetune.reinforcement.common.log import logger +from dashscope.v2.finetune.reinforcement.common.utils import ( get_fc_request_id, get_business_summary, ) -from dashscope.finetune.reinforcement.common.model_types import ( +from dashscope.v2.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) -from dashscope.finetune.reinforcement.component import BaseDataModel -from dashscope.finetune.reinforcement.component.func_manager import FuncManager -from dashscope.finetune.reinforcement.component.observability.tracing import ( +from dashscope.v2.finetune.reinforcement.component import BaseDataModel +from dashscope.v2.finetune.reinforcement.component.func_manager import ( + FuncManager, +) +from dashscope.v2.finetune.reinforcement.component.observability.tracing import ( ensure_agentic_rl_baggage_span_processor, is_tracing_enabled, maybe_force_flush_async, @@ -622,7 +624,9 @@ async def health_check_endpoint() -> JSONResponse: # Start uvicorn with calculated worker count uvicorn.run( - # app + # uvicorn resolves this import string inside its own worker + # process, so it must stay the public path used by + # examples/workspace/scripts/start_local_*.sh's ``python3 -m``. "dashscope.finetune.reinforcement.component.server.server:app", host="0.0.0.0", port=_SERVER_PORT, diff --git a/dashscope/v2/finetune/reinforcement/examples/observability/__init__.py b/dashscope/v2/finetune/reinforcement/examples/observability/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/examples/observability/smoke_observability.py b/dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/observability/smoke_observability.py rename to dashscope/v2/finetune/reinforcement/examples/observability/smoke_observability.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/README-zh.md b/dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README-zh.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/README-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/README.md b/dashscope/v2/finetune/reinforcement/examples/workspace/README.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/README.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/README.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide-zh.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide-zh.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/UserGuide.md b/dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/UserGuide.md rename to dashscope/v2/finetune/reinforcement/examples/workspace/UserGuide.md diff --git a/dashscope/finetune/reinforcement/examples/workspace/cli.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/cli.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/cli.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl b/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl rename to dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_train_min.jsonl diff --git a/dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl b/dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl rename to dashscope/v2/finetune/reinforcement/examples/workspace/data/calc_validation_min.jsonl diff --git a/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/group_reward.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/reward/reward_decorator.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py b/dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/functions/rollout/rollout_only.py diff --git a/dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml b/dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/opd-job.yaml rename to dashscope/v2/finetune/reinforcement/examples/workspace/opd-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/requirements.txt b/dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/requirements.txt rename to dashscope/v2/finetune/reinforcement/examples/workspace/requirements.txt diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/group_reward_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/group_reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_decorator_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/reward_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/reward_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json b/dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/resources/rollout_input.json rename to dashscope/v2/finetune/reinforcement/examples/workspace/resources/rollout_input.json diff --git a/dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml b/dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/rl-job.yaml rename to dashscope/v2/finetune/reinforcement/examples/workspace/rl-job.yaml diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh index 14400acc..b5454eef 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/scripts/build.sh +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/build.sh @@ -11,7 +11,7 @@ #***************************************************************# # Configuration -WORK_DIR='dashscope/finetune/reinforcement/examples/workspace/' +WORK_DIR='dashscope/v2/finetune/reinforcement/examples/workspace/' # Parse command line arguments INSTALL_FLAG=0 diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_group_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_reward_decorator.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_local_rollout.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/query_online_tuning.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_group_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_reward_decorator.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh b/dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh rename to dashscope/v2/finetune/reinforcement/examples/workspace/scripts/start_local_rollout.sh diff --git a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py similarity index 98% rename from dashscope/finetune/reinforcement/examples/workspace/submit_job.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py index c4b971c7..7311f4ad 100644 --- a/dashscope/finetune/reinforcement/examples/workspace/submit_job.py +++ b/dashscope/v2/finetune/reinforcement/examples/workspace/submit_job.py @@ -117,7 +117,7 @@ async def main_workflow(): ], # Function Component Configuration functions=[ - # RolloutFunctionComponent๏ผšEnvironment simulation/rollout + # RolloutFunctionComponent: Environment simulation/rollout # generation RolloutFunctionComponent( name="rollout-1", @@ -127,7 +127,7 @@ async def main_workflow(): ), runtime=FunctionComponentRuntime(**rollout_runtime), ), - # RewardFunctionComponent๏ผšReward calculation engine + # RewardFunctionComponent: Reward calculation engine RewardFunctionComponent( name="reward-1", weight=1.0, diff --git a/dashscope/finetune/reinforcement/examples/workspace/test_functions.py b/dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py similarity index 100% rename from dashscope/finetune/reinforcement/examples/workspace/test_functions.py rename to dashscope/v2/finetune/reinforcement/examples/workspace/test_functions.py diff --git a/dashscope/v2/resources/_agentstudio.py b/dashscope/v2/resources/_agentstudio.py index a58700b7..fc963ae0 100644 --- a/dashscope/v2/resources/_agentstudio.py +++ b/dashscope/v2/resources/_agentstudio.py @@ -2,18 +2,18 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Transparent v2 error-alignment proxy for the mounted AgentStudio sub-SDK. -Design doc M3 task 3.12. ``client.agentstudio`` lazily mounts the 1.x +Design doc M3 task 3.12. ``client.agentstudio`` lazily mounts the AgentStudio sub-SDK, which keeps its own exception family rooted at -``dashscope.common.error.DashScopeException``. That family shares no +``dashscope._shared._errors.DashScopeException``. That family shares no lineage with ``dashscope.v2.errors.DashScopeError``, so a user writing ``except dashscope.v2.errors.DashScopeError`` would miss every failure the mounted sub-SDK raises. This module closes that gap at the *mount boundary only*: it wraps the constructed AgentStudio client in a transparent proxy that catches -``AgentStudioError`` and re-raises the equivalent v2 error. No 1.x code -is touched, and the standalone ``dashscope.agentstudio.Client`` keeps its -native error contract -- only the v2 mount is translated. +``AgentStudioError`` and re-raises the equivalent v2 error. The +standalone ``dashscope.agentstudio.Client`` keeps its native error +contract -- only the v2 mount is translated. Two-branch translation (design doc ยง7): @@ -43,9 +43,9 @@ import httpx -from dashscope.agentstudio import exceptions as _as_exceptions -from dashscope.agentstudio.pagination import AsyncCursorPage, CursorPage from dashscope.v2 import errors as _v2_errors +from dashscope.v2.agentstudio import exceptions as _as_exceptions +from dashscope.v2.agentstudio.pagination import AsyncCursorPage, CursorPage _AGENTSTUDIO_FAMILY = "sdk.agentstudio" @@ -63,7 +63,7 @@ # Resource classes (and the typed event streams) live under this package; # attribute access recurses into them so nested method calls stay guarded. -_RESOURCE_PACKAGE = "dashscope.agentstudio.resources" +_RESOURCE_PACKAGE = "dashscope.v2.agentstudio.resources" # AgentStudio raises a few HTTP-family errors locally, without a response, # so they carry no ``status_code`` (the missing-``api_key`` guard in @@ -98,11 +98,12 @@ def _effective_status(exc: _as_exceptions.AgentStudioError) -> int: def translate_agentstudio_error( exc: _as_exceptions.AgentStudioError, ) -> _v2_errors.DashScopeError: - """Map a 1.x AgentStudio exception into the v2 error family.""" + """Map an AgentStudio exception into the v2 error family.""" status = getattr(exc, "status_code", None) if isinstance(exc, _as_exceptions.APIStatusError) or status is not None: - # V1 already resolved compatibility fields and header precedence. + # AgentStudio's own exception already resolved the compatibility + # fields and the header precedence. request_id = getattr(exc, "request_id", None) headers = {"x-request-id": request_id} if request_id else None error = _v2_errors.error_from_status( diff --git a/dashscope/v2/resources/_oss.py b/dashscope/v2/resources/_oss.py index 2235e4a0..42999805 100644 --- a/dashscope/v2/resources/_oss.py +++ b/dashscope/v2/resources/_oss.py @@ -4,16 +4,15 @@ DashScope serves media (image / audio / video) from URLs; a local path or ``file://`` URI must be uploaded to OSS first and the request opted into -OSS resolution. This reuses the 1.x uploader (``dashscope.utils.oss_utils``) -so 2.0 keeps the same upload behavior instead of re-implementing it. +OSS resolution. The uploader itself is ``_oss_upload``. -Two things are anchored to the v2 client rather than to 1.x globals: +Two things are anchored to the v2 client rather than to module globals: -* The upload certificate is fetched from the client's own ``base_url`` - (``base_address=`` below), so a region- or gateway-scoped client uploads - to the matching regional OSS and the returned ``oss://`` URL resolves - under the same endpoint the inference request is sent to. The 1.x global - endpoint would otherwise pin every upload to the default region. +* The upload certificate is fetched from the client's own ``base_url``, so a + region- or gateway-scoped client uploads to the matching regional OSS and + the returned ``oss://`` URL resolves under the same endpoint the inference + request is sent to. A global endpoint would otherwise pin every upload to + the default region. * The effective per-call ``api_key`` / ``workspace`` (the override, else the client default) drive both the certificate fetch and the upload, so the asset lands in the same account the request runs under. @@ -23,7 +22,7 @@ from typing import Any, Dict, List, Optional, Tuple -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.v2.resources._oss_upload import check_and_upload_local from dashscope.v2.types.messages import ( AudioPart, ImagePart, @@ -48,17 +47,16 @@ def _effective( client, api_key: Optional[str], workspace: Optional[str], -) -> Tuple[Optional[str], Optional[str], str]: +) -> Tuple[Optional[str], Optional[str]]: """Resolve the per-call override against the client defaults. - Returns ``(api_key, workspace, base_url)``. The certificate fetch and - the upload both run under these, so the asset lands in the same - account/region the request is sent to. + Returns ``(api_key, workspace)``. The certificate fetch and the upload + both run under these, so the asset lands in the same account the request + is sent to; the endpoint comes from the client itself. """ return ( api_key or client.config.api_key, workspace or client.config.workspace, - client.config.base_url, ) @@ -74,7 +72,7 @@ def upload_message_media( Returns True when any part holds an ``oss://`` URL afterwards (uploaded or already remote), so the caller opts the request into OSS resolution. """ - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) has_upload = False cert: Optional[dict] = None for message in messages: @@ -87,12 +85,12 @@ def upload_message_media( # Inline base64 audio has no URL to upload. continue is_up, new, cert = check_and_upload_local( + client, model, part.url, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: part.url = new @@ -100,12 +98,12 @@ def upload_message_media( elif isinstance(part, VideoPart): if part.url is not None: is_up, new, cert = check_and_upload_local( + client, model, part.url, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: part.url = new @@ -114,12 +112,12 @@ def upload_message_media( frames = [] for frame in part.frames: is_up, new, cert = check_and_upload_local( + client, model, frame, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) has_upload = has_upload or is_up frames.append(new) @@ -128,21 +126,21 @@ def upload_message_media( def _upload_value( + client, value: Any, model: str, api_key: Optional[str], workspace: Optional[str], - base_url: str, cert: Optional[dict], ) -> Tuple[Any, bool, Optional[dict]]: if isinstance(value, str): is_up, new, cert = check_and_upload_local( + client, model, value, - api_key, - cert, - base_address=base_url, + api_key=api_key, workspace=workspace, + certificate=cert, ) return new, is_up, cert if isinstance(value, list): @@ -150,11 +148,11 @@ def _upload_value( out = [] for item in value: new_item, is_up, cert = _upload_value( + client, item, model, api_key, workspace, - base_url, cert, ) has_upload = has_upload or is_up @@ -165,11 +163,11 @@ def _upload_value( out = {} for key, item in value.items(): new_item, is_up, cert = _upload_value( + client, item, model, api_key, workspace, - base_url, cert, ) has_upload = has_upload or is_up @@ -188,17 +186,17 @@ def upload_media_value( """Recursively upload local/oss refs in a media container. Walks strings, lists and dicts (``image_urls`` / ``media`` / - ``extra_input``); each string goes through the 1.x uploader, which only + ``extra_input``); each string goes through the uploader, which only rewrites ``file://`` URIs, existing local paths and ``oss://`` URLs and passes http(s) and other strings through unchanged. """ - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) new_value, has_upload, _ = _upload_value( + client, value, model, key, space, - base_url, None, ) return new_value, has_upload @@ -212,19 +210,19 @@ def upload_embedding_media( workspace: Optional[str] = None, ) -> None: """Upload the image/audio/video field of each embedding item in place.""" - key, space, base_url = _effective(client, api_key, workspace) + key, space = _effective(client, api_key, workspace) cert: Optional[dict] = None for item in contents: for field_key in _EMBEDDING_MEDIA_KEYS: field = item.get(field_key) if isinstance(field, str) and field: is_up, new, cert = check_and_upload_local( + client, model, field, - key, - cert, - base_address=base_url, + api_key=key, workspace=space, + certificate=cert, ) if is_up: item[field_key] = new diff --git a/dashscope/v2/resources/_oss_upload.py b/dashscope/v2/resources/_oss_upload.py new file mode 100644 index 00000000..fb1eebe2 --- /dev/null +++ b/dashscope/v2/resources/_oss_upload.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Local-file โ†’ OSS upload, native to the 2.0 SDK. + +DashScope serves media from URLs, so a local path or ``file://`` URI has to +be posted to OSS first and the resulting ``oss://`` URL sent instead. The +upload is two hops with opposite trust levels: + +* the policy fetch (``GET /uploads?action=getPolicy``) goes to DashScope and + **does** carry the credential, so it rides the client transport and picks + up its connection pool, retries and per-call api_key/workspace; +* the post-object upload goes to the OSS host and must **not** carry it. The + transport merges its default ``Authorization: Bearer`` into every request, + and httpx only strips that header on cross-origin *redirects* -- posting an + absolute OSS URL straight through the transport would hand the API key to + the storage host. Hence the separate auth-free client in ``_post_to_oss``. +""" + +from __future__ import annotations + +import mimetypes +import os +from typing import Any, Dict, Optional, Tuple +from urllib.parse import unquote_plus, urlparse + +import httpx + +from dashscope.v2._logging import get_logger +from dashscope.v2.errors import check_response, error_from_status +from dashscope.v2.transport.http import default_user_agent + +__all__ = [ + "check_and_upload_local", + "get_upload_certificate", + "upload_local_file", +] + +_FILE_URI_SCHEME = "file://" +_OSS_SCHEME = "oss://" +_CERT_PATH = "/uploads" +# The client default (300s) is far too short for a large video. +_OSS_POST_TIMEOUT = 3600.0 + +logger = get_logger("oss") + + +def _resolve_file_uri_path(file_uri: str) -> str: + """Turn a ``file://`` URI into a filesystem path.""" + parsed = urlparse(file_uri) + if parsed.netloc: + file_path = parsed.netloc + unquote_plus(parsed.path) + else: + file_path = unquote_plus(parsed.path) + + # A Windows URI parses to "/C:/dir/file"; drop the leading slash. + if ( + file_path.startswith("/") + and len(file_path) > 2 + and file_path[2] == ":" + ): + file_path = file_path[1:] + + return os.path.expanduser(file_path) + + +def get_upload_certificate( + client, + model: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, +) -> Dict[str, Any]: + """Fetch the OSS post-object policy for ``model``. + + Raises even when the client was built with ``raise_on_error=False``: + without a policy there is no URL to upload to and nothing to hand back. + """ + response = client.transport.request( + "GET", + _CERT_PATH, + params={"action": "getPolicy", "model": model}, + api_key=api_key, + workspace=workspace, + ) + check_response(response) + return response.json()["output"] + + +def _post_to_oss( + upload_host: str, + form: Dict[str, Any], + file_name: str, + file_path: str, + transport: Optional[httpx.BaseTransport] = None, +) -> httpx.Response: + """Post one file to OSS with a client that carries no DashScope auth. + + The file is streamed from a live handle rather than read into memory, so + a large video does not have to fit in the process. ``transport`` exists + for tests to inject an ``httpx.MockTransport``. + """ + headers = { + "Accept": "application/json", + "User-Agent": default_user_agent(), + } + with httpx.Client( + timeout=_OSS_POST_TIMEOUT, + transport=transport, + ) as oss_client: + with open(file_path, "rb") as handle: + return oss_client.post( + upload_host, + data=form, + files={"file": (file_name, handle)}, + headers=headers, + ) + + +def upload_local_file( + client, + model: str, + file_path: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + certificate: Optional[Dict[str, Any]] = None, +) -> Tuple[str, Dict[str, Any]]: + """Upload one local file and return ``(oss_url, certificate)``. + + Pass the returned certificate back in to reuse it across a batch instead + of fetching a fresh policy per file. + """ + cert = certificate + if cert is None: + cert = get_upload_certificate( + client, + model, + api_key=api_key, + workspace=workspace, + ) + key = cert["upload_dir"] + "/" + os.path.basename(file_path) + form = { + "OSSAccessKeyId": cert["oss_access_key_id"], + "Signature": cert["signature"], + "policy": cert["policy"], + "key": key, + "x-oss-object-acl": cert["x_oss_object_acl"], + "x-oss-forbid-overwrite": cert["x_oss_forbid_overwrite"], + "success_action_status": "200", + "x-oss-content-type": ( + mimetypes.guess_type(file_path)[0] or "application/octet-stream" + ), + } + response = _post_to_oss( + cert["upload_host"], + form, + os.path.basename(file_path), + file_path, + ) + if not response.is_success: + logger.error( + "uploading file %s to oss failed, status %s", + file_path, + response.status_code, + ) + # OSS answers with XML, so there is no business code to read and the + # error is mapped from the status code alone. + raise error_from_status( + response.status_code, + message=response.text, + headers=response.headers, + ) + return _OSS_SCHEME + key, cert + + +def check_and_upload_local( + client, + model: str, + content: str, + api_key: Optional[str] = None, + workspace: Optional[str] = None, + certificate: Optional[Dict[str, Any]] = None, +) -> Tuple[bool, str, Optional[Dict[str, Any]]]: + """Upload ``content`` to OSS when it names a local file. + + Returns ``(is_upload, url_or_content, certificate)``. An existing + ``oss://`` URL passes through untouched but still reports + ``is_upload=True``, so the caller opts the request into OSS resolution; + ``http(s)://`` and anything unrecognized pass through as ``False``. + """ + if content.startswith(_FILE_URI_SCHEME): + file_path = _resolve_file_uri_path(content) + if not os.path.isfile(file_path): + raise ValueError(f"The file: {file_path} is not exists!") + url, cert = upload_local_file( + client, + model, + file_path, + api_key=api_key, + workspace=workspace, + certificate=certificate, + ) + return True, url, cert + if content.startswith(_OSS_SCHEME): + return True, content, certificate + if not content.startswith("http"): + content = os.path.expanduser(content) + if os.path.isfile(content): + url, cert = upload_local_file( + client, + model, + content, + api_key=api_key, + workspace=workspace, + certificate=certificate, + ) + return True, url, cert + return False, content, certificate diff --git a/dashscope/v2/resources/audio/customization.py b/dashscope/v2/resources/audio/customization.py index 89675473..f96de9f3 100644 --- a/dashscope/v2/resources/audio/customization.py +++ b/dashscope/v2/resources/audio/customization.py @@ -5,7 +5,7 @@ V2 form of 1.x ``VocabularyService`` (asr hot-word tables, ``audio/asr/vocabulary.py``) and ``VoiceEnrollmentService`` (voice cloning, ``audio/tts_v2/enrollment.py``), plus the qwen voice-design -family (ๅฃฐ้Ÿณ่ฎพ่ฎก, no 1.x SDK class โ€” modeled from the official REST API). +family (no 1.x SDK class โ€” modeled from the official REST API). All three ride the same envelope: ``POST /services/audio/{asr|tts}/ customization`` with ``{model, input: {action, ...}, parameters?}``; only the action words and id fields differ per family. 1.x retried @@ -293,7 +293,7 @@ def delete( class Voices(_CustomizationBase): - """``client.audio.voices`` -- voice cloning (ๅฃฐ้Ÿณๅคๅˆป).""" + """``client.audio.voices`` -- voice cloning.""" PATH = "/services/audio/tts/customization" DEFAULT_MODEL = "voice-enrollment" @@ -453,7 +453,7 @@ def delete( class VoiceDesign(_CustomizationBase): - """``client.audio.voice_design`` -- qwen voice design (ๅฃฐ้Ÿณ่ฎพ่ฎก). + """``client.audio.voice_design`` -- qwen voice design. No 1.x SDK class; modeled from the official REST API, which shares the voices endpoint with shorter action words and a ``voice`` id diff --git a/dashscope/v2/resources/chat/_params.py b/dashscope/v2/resources/chat/_params.py new file mode 100644 index 00000000..134a0e66 --- /dev/null +++ b/dashscope/v2/resources/chat/_params.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Request-parameter rules that depend on the model name.""" + +from __future__ import annotations + +# tts / omni / qwen-deep-research honor incremental_output=False on the +# server; every other model streams incrementally only, so the SDK has to +# keep the wire incremental and assemble the cumulative view itself. +_SERVER_SIDE_MODELS = ("tts", "omni", "qwen-deep-research") + + +def should_modify_incremental_output(model_name: str) -> bool: + """Whether the SDK must build cumulative frames client-side. + + ``False`` for the models that honor ``incremental_output`` server-side. + """ + if not isinstance(model_name, str): + return True + lowered = model_name.lower() + return not any(name in lowered for name in _SERVER_SIDE_MODELS) diff --git a/dashscope/v2/resources/chat/completions.py b/dashscope/v2/resources/chat/completions.py index 17c2b1c2..258f9024 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, @@ -103,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 @@ -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/resources/files.py b/dashscope/v2/resources/files.py index 3d1b94e2..23654c6e 100644 --- a/dashscope/v2/resources/files.py +++ b/dashscope/v2/resources/files.py @@ -16,6 +16,7 @@ NotFoundError, check_response, response_error, + sdk_error, ) from dashscope.v2.types.common import ( ResponseBase, @@ -123,15 +124,24 @@ def _parse_delete(body: Dict[str, Any]) -> DeletionStatus: def _is_valid_jsonl(file_path: str) -> bool: - with open(file_path, encoding="utf-8") as f: - for line in f: - try: - json.loads(line) - except json.decoder.JSONDecodeError: - return False + try: + with open(file_path, encoding="utf-8") as f: + for line in f: + try: + json.loads(line) + except json.decoder.JSONDecodeError: + return False + except OSError as exc: + raise _io_error(exc) from exc return True +def _io_error(exc: BaseException) -> Exception: + # Local filesystem failures are SDK-side, not gateway errors: the + # catalog's sdk.IoError entry supplies message/code/solution. + return sdk_error("sdk.IoError", {"detail": str(exc)}) + + def _validate_upload(file_path: str, purpose: str) -> None: if purpose in ("fine-tune", "fine_tune"): if not _is_valid_jsonl(file_path): @@ -139,17 +149,20 @@ def _validate_upload(file_path: str, purpose: str) -> None: f"file {file_path} is not in valid jsonl format", ) if not os.path.isfile(file_path): - raise FileNotFoundError(f"file not found: {file_path}") + raise _io_error(FileNotFoundError(f"file not found: {file_path}")) def _upload_form(file_path: str, description: Optional[str]): data = {} if description is not None: data["descriptions"] = description - with open(file_path, "rb") as f: - # Bytes, not a live handle: transport retries must be able - # to re-send the full body. - content = f.read() + try: + with open(file_path, "rb") as f: + # Bytes, not a live handle: transport retries must be able + # to re-send the full body. + content = f.read() + except OSError as exc: + raise _io_error(exc) from exc return data, {"files": (os.path.basename(file_path), content)} diff --git a/dashscope/v2/resources/reinforcement_learning.py b/dashscope/v2/resources/reinforcement_learning.py index ce80824c..10b868af 100644 --- a/dashscope/v2/resources/reinforcement_learning.py +++ b/dashscope/v2/resources/reinforcement_learning.py @@ -2,12 +2,12 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """client.rl thin mount layer (design doc M3 3.7). -The agentic-RL business logic stays in the 1.x implementation -(``dashscope.finetune.agentic_rl``); this layer only mounts it on the -2.0 Client and routes both halves through the v2 error model -- job -management through the v2 transport, and the workflow steps by -translating 1.x exceptions into ``SdkError`` (catalog ``code`` plus -``solution``). One ``except DashScopeError`` therefore covers both. +The agentic-RL workflow lives in the 2.0 package +(``dashscope.v2.finetune``); this layer only mounts it on the 2.0 Client +and routes both halves through the v2 error model -- job management +through the v2 transport, and the workflow steps by translating the RL +exception family into ``SdkError`` (catalog ``code`` plus ``solution``). +One ``except DashScopeError`` therefore covers both. Two halves, matching the rest of the SDK: ``rl.jobs`` is the training job lifecycle (``create`` submits and never blocks, ``wait`` blocks, @@ -15,13 +15,20 @@ itself carries the RL-specific workflow steps that have no ``fine_tunes`` equivalent, including the function-instance queries. +The two halves also identify themselves differently on the wire, and +that split predates this layer: ``rl.jobs`` rides the v2 transport and +sends the marked ``finetune2`` module tag, while the workflow/submit +path keeps sending the unmarked ``finetune`` tag the feature has always +sent. Both are backend statistics contracts, so neither is folded into +the other. + ``register_functions`` / ``test_functions`` / ``upload_data`` / ``jobs.create`` / the function-instance methods are the SDK's only -public methods without ``timeout=``: the 1.x implementation they wrap -accepts none, so declaring it here would silently do nothing. The other -``jobs`` methods take ``timeout=`` as everywhere else. Those same -1.x-wrapped methods accept a per-call ``api_key`` but no ``workspace``: -the 1.x ``AgenticRL`` they delegate to has no workspace concept. +public methods without ``timeout=``: the workflow implementation they +wrap accepts none, so declaring it here would silently do nothing. The +other ``jobs`` methods take ``timeout=`` as everywhere else. Those same +workflow-wrapping methods accept a per-call ``api_key`` but no +``workspace``: ``AgenticRL`` has no workspace concept. """ from __future__ import annotations @@ -69,11 +76,12 @@ "UploadedData", ] -# Imported lazily below via the module object so tests can monkeypatch -# ``dashscope.finetune.agentic_rl.AgenticRL`` without touching 1.x code. -from dashscope.finetune import agentic_rl as _agentic_rl # noqa: E402 -from dashscope.finetune import reinforcement as _reinforcement # noqa: E402 -from dashscope.finetune.reinforcement.common import ( # noqa: E402 +# Imported below via the module object so tests can monkeypatch +# ``dashscope.v2.finetune.agentic_rl.AgenticRL`` without touching the +# workflow implementation. +from dashscope.v2.finetune import agentic_rl as _agentic_rl # noqa: E402 +from dashscope.v2.finetune import reinforcement as _reinforcement # noqa: E402 +from dashscope.v2.finetune.reinforcement.common import ( # noqa: E402 errors as _rl_errors, ) @@ -113,10 +121,10 @@ class UploadedData(BaseModel): # These three bases cover all 19 exception classes in common/errors.py: # AgenticRLError roots 17, and the two *WithCode classes derive from # RuntimeError / ValueError instead. Nothing wider is needed, since -# common/utils.py:139 already funnels transport, timeout and unexpected -# failures into RuntimeErrorWithCode; catching more would relabel bugs -# in this module as SDK errors. pydantic's ValidationError, which is -# also a ValueError, stays outside on purpose. +# common/utils.py's request helper already funnels transport, timeout +# and unexpected failures into RuntimeErrorWithCode; catching more would +# relabel bugs in this module as SDK errors. pydantic's ValidationError, +# which is also a ValueError, stays outside on purpose. _RL_EXCEPTIONS = ( _rl_errors.AgenticRLError, _rl_errors.RuntimeErrorWithCode, @@ -126,7 +134,7 @@ class UploadedData(BaseModel): @contextlib.contextmanager def _translate_rl_errors(): - """Keep 1.x agentic-RL failures inside the v2 exception family. + """Keep agentic-RL workflow failures inside the v2 exception family. A plain sync context manager, so it wraps ``asyncio.run(...)`` and ``await ...`` bodies alike. @@ -157,9 +165,9 @@ def _new_agentic_rl( def _to_job(result: Any) -> FineTune: - """Adapt the 1.x ``FineTune`` dataclass into the v2 model. + """Adapt the RL submit result into the v2 job model. - The 1.x submit returns ``output`` as a nested object and spells the + The submit returns ``output`` as a nested object and spells the timestamps ``create_time`` / ``end_time``; the v2 model's aliases absorb both once the envelope is flattened by hand here. """ @@ -239,14 +247,14 @@ async def _register_functions_workflow( class RlJobs: """``client.rl.jobs``: the agentic-RL training job lifecycle. - ``create`` runs the whole 1.x workflow โ€” register functions, upload + ``create`` runs the whole workflow โ€” register functions, upload datasets, submit โ€” and returns the job without blocking; ``wait`` polls it to a terminal status. The remaining verbs share the ``fine_tunes`` wire contract (RL jobs are fine-tune jobs: same backend, same response models) and ride the v2 client endpoint - like every other resource; 1.x forces a management-plane host - only in its workflow/submit path, which is a config quirk there, - not parity. + like every other resource. The workflow/submit path is the + exception: it posts to the management-plane host its own constants + name, as it always has. """ def __init__(self, client) -> None: @@ -420,10 +428,10 @@ def logs( class Rl: - """Sync mount of the 1.x agentic-RL workflow. + """Sync mount of the agentic-RL workflow. The workflow methods (``register_functions``/``test_functions``/ - ``upload_data``/``jobs.create``) wrap the 1.x async implementation + ``upload_data``/``jobs.create``) wrap the async implementation with ``asyncio.run``, so they must not be called from a thread that already runs an event loop (Jupyter, FastAPI, pytest-asyncio) โ€” use :class:`AsyncRl` there instead. @@ -471,7 +479,7 @@ def test_functions( """Returns whatever the customer's own function returns. The ``pull_logs``/``log_page_size``/``start_time``/``end_time`` - parameters forward to the 1.x log-pulling step that runs after + parameters forward to the log-pulling step that runs after verification. """ with _translate_rl_errors(): @@ -762,7 +770,7 @@ async def logs( class AsyncRl: - """Async mount of the 1.x agentic-RL workflow.""" + """Async mount of the agentic-RL workflow.""" def __init__(self, client) -> None: self._client = client @@ -804,7 +812,7 @@ async def test_functions( """Returns whatever the customer's own function returns. The ``pull_logs``/``log_page_size``/``start_time``/``end_time`` - parameters forward to the 1.x log-pulling step that runs after + parameters forward to the log-pulling step that runs after verification. """ with _translate_rl_errors(): diff --git a/dashscope/v2/resources/tingwu.py b/dashscope/v2/resources/tingwu.py index c776c97e..198f0412 100644 --- a/dashscope/v2/resources/tingwu.py +++ b/dashscope/v2/resources/tingwu.py @@ -5,7 +5,7 @@ Offline summarization (``call``, one POST returning the parsed result) and realtime speech understanding (``realtime``, a duplex websocket session used as a context manager). Mirrors the 1.x -``dashscope.multimodal.tingwu`` pair. +``dashscope.legacy_1x.multimodal.tingwu`` pair. """ # "input" is the wire-protocol field name for task payloads. diff --git a/dashscope/v2/tokenizers/__init__.py b/dashscope/v2/tokenizers/__init__.py new file mode 100644 index 00000000..4e340f37 --- /dev/null +++ b/dashscope/v2/tokenizers/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Offline tokenizers owned by the 2.0 SDK. + +Pure local counting over the bundled tiktoken vocab -- no network, no +dependency on the 1.x package. ``dashscope.tokenizers`` re-exports these +so both stacks share one implementation and one copy of the vocab. +""" + +from .tokenizer import get_tokenizer, list_tokenizers +from .tokenizer_base import Tokenizer + +__all__ = [ + "Tokenizer", + "get_tokenizer", + "list_tokenizers", +] diff --git a/dashscope/resources/qwen.tiktoken b/dashscope/v2/tokenizers/qwen.tiktoken similarity index 100% rename from dashscope/resources/qwen.tiktoken rename to dashscope/v2/tokenizers/qwen.tiktoken diff --git a/dashscope/v2/tokenizers/qwen_tokenizer.py b/dashscope/v2/tokenizers/qwen_tokenizer.py new file mode 100644 index 00000000..9a538f95 --- /dev/null +++ b/dashscope/v2/tokenizers/qwen_tokenizer.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +# mypy: disable-error-code="annotation-unchecked" + +import base64 +import unicodedata +from typing import Collection, Dict, List, Set, Union + +from .tokenizer_base import Tokenizer + +PAT_STR = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" # noqa E501 # pylint: disable=line-too-long +ENDOFTEXT = "<|endoftext|>" +IMSTART = "<|im_start|>" +IMEND = "<|im_end|>" +# as the default behavior is changed to allow special tokens in +# regular texts, the surface forms of special tokens need to be +# as different as possible to minimize the impact +EXTRAS = tuple((f"<|extra_{i}|>" for i in range(205))) +# changed to use actual index to avoid misconfiguration with vocabulary expansion # noqa: E501 +SPECIAL_START_ID = 151643 +SPECIAL_TOKENS = tuple( + enumerate( + ( + ( + ENDOFTEXT, + IMSTART, + IMEND, + ) + + EXTRAS + ), + start=SPECIAL_START_ID, + ), +) +SPECIAL_TOKENS_SET = set(t for i, t in SPECIAL_TOKENS) + +# tiktoken's BPE merges tokens recursively in Rust, which can overflow the +# call stack on very long inputs (pyo3_runtime.PanicException: StackOverflow). +# Split text into chunks below this threshold before encoding. +_CHUNK_SIZE = 100_000 + + +class QwenTokenizer(Tokenizer): + @staticmethod + def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> Dict[bytes, int]: + with open(tiktoken_bpe_file, "rb") as f: + contents = f.read() + return { + base64.b64decode(token): int(rank) + for token, rank in ( + line.split() for line in contents.splitlines() if line + ) + } + + def __init__(self, vocab_file, errors="replace", extra_vocab_file=None): + self._errors = errors + self._vocab_file = vocab_file + self._extra_vocab_file = extra_vocab_file + + self._mergeable_ranks = QwenTokenizer._load_tiktoken_bpe( + vocab_file, + ) # type: Dict[bytes, int] + self._special_tokens = { + token: index for index, token in SPECIAL_TOKENS + } + + # try load extra vocab from file + if extra_vocab_file is not None: + used_ids = set(self._mergeable_ranks.values()) | set( + self._special_tokens.values(), + ) + extra_mergeable_ranks = self._load_tiktoken_bpe(extra_vocab_file) + for token, index in extra_mergeable_ranks.items(): + if token in self._mergeable_ranks: + continue + if index in used_ids: + continue + self._mergeable_ranks[token] = index + # the index may be sparse after this, but don't worry tiktoken.Encoding will handle this # noqa: E501 # pylint: disable=line-too-long + import tiktoken + + enc = tiktoken.Encoding( + "Qwen", + pat_str=PAT_STR, + mergeable_ranks=self._mergeable_ranks, + special_tokens=self._special_tokens, + ) + assert ( + len(self._mergeable_ranks) + len(self._special_tokens) + == enc.n_vocab + ), f"{len(self._mergeable_ranks) + len(self._special_tokens)} != {enc.n_vocab} in encoding" # noqa: E501 # pylint: disable=line-too-long + + self.decoder = { + v: k for k, v in self._mergeable_ranks.items() + } # type: dict[int, bytes|str] + self.decoder.update({v: k for k, v in self._special_tokens.items()}) + + self._tokenizer = enc # type: tiktoken.Encoding + + self.eod_id = self._tokenizer.eot_token + self.im_start_id = self._special_tokens[IMSTART] + self.im_end_id = self._special_tokens[IMEND] + + def encode( # type: ignore[override] + self, + text: str, + allowed_special: Union[Set, str] = "all", + disallowed_special: Union[Collection, str] = (), + ) -> Union[List[List], List]: + text = unicodedata.normalize("NFC", text) + if len(text) <= _CHUNK_SIZE: + return self._tokenizer.encode( + text, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ) + + result = [] + for chunk in self._split_text(text): + result.extend( + self._tokenizer.encode( + chunk, + allowed_special=allowed_special, + disallowed_special=disallowed_special, + ), + ) + return result + + @staticmethod + def _split_text(text: str, chunk_size: int = _CHUNK_SIZE) -> List[str]: + """Split text into chunks at safe tokenization boundaries.""" + parts: List[str] = [] + for i, line in enumerate(text.split("\n")): + piece = line if i == 0 else "\n" + line + if len(piece) <= chunk_size: + parts.append(piece) + else: + for j in range(0, len(piece), chunk_size): + parts.append(piece[j : j + chunk_size]) + + chunks: List[str] = [] + current_chunk: List[str] = [] + current_len = 0 + for part in parts: + if current_len + len(part) <= chunk_size: + current_chunk.append(part) + current_len += len(part) + else: + if current_chunk: + chunks.append("".join(current_chunk)) + current_chunk = [part] + current_len = len(part) + if current_chunk: + chunks.append("".join(current_chunk)) + return chunks + + def decode( + self, + token_ids: Union[int, List[int]], + skip_special_tokens: bool = False, + errors: str = None, + **kwargs, + ) -> str: + if isinstance(token_ids, int): + token_ids = [token_ids] + if skip_special_tokens: + token_ids = [i for i in token_ids if i < self.eod_id] + return self._tokenizer.decode(token_ids, errors=errors or self._errors) diff --git a/dashscope/v2/tokenizers/tokenizer.py b/dashscope/v2/tokenizers/tokenizer.py new file mode 100644 index 00000000..e8625bd7 --- /dev/null +++ b/dashscope/v2/tokenizers/tokenizer.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import os +from typing import List + +from .qwen_tokenizer import QwenTokenizer +from .tokenizer_base import Tokenizer + +QWEN_SERIALS = ["qwen-7b-chat", "qwen-turbo", "qwen-plus", "qwen-max"] +# The vocab ships next to this module, so the leaf stays relocatable. +_VOCAB_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "qwen.tiktoken", +) + + +def get_tokenizer(model: str) -> Tokenizer: + """Get a tokenizer based on model name. + + Args: + model (str): The model name. + + Raises: + ValueError: Not support model + + Returns: + Tokenizer: The `Tokenizer` of the model. + """ + if model.startswith("qwen"): + return QwenTokenizer(_VOCAB_PATH) + raise ValueError( + f"Not support model: {model}, currently only support qwen models.", + ) + + +def list_tokenizers() -> List[str]: + """List support models + + Returns: + List[str]: The model list. + """ + return QWEN_SERIALS diff --git a/dashscope/v2/tokenizers/tokenizer_base.py b/dashscope/v2/tokenizers/tokenizer_base.py new file mode 100644 index 00000000..2e385bf8 --- /dev/null +++ b/dashscope/v2/tokenizers/tokenizer_base.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from typing import List + + +class Tokenizer: + """Base tokenizer interface for local tokenizers.""" + + def __init__(self): + pass + + def encode(self, text: str, **kwargs) -> List[int]: # type: ignore[empty-body] # noqa: E501 + """Encode input text string to token ids. + + Args: + text (str): The string to be encoded. + + Returns: + List[int]: The token ids. + """ + + def decode(self, token_ids: List[int], **kwargs) -> str: # type: ignore[empty-body] # pylint: disable=line-too-long # noqa: E501 + """Decode token ids to string. + + Args: + token_ids (List[int]): The input token ids. + + Returns: + str: The string of the token ids. + """ diff --git a/dashscope/v2/transport/_ident.py b/dashscope/v2/transport/_ident.py index 7d856e0d..5e1aa9d1 100644 --- a/dashscope/v2/transport/_ident.py +++ b/dashscope/v2/transport/_ident.py @@ -18,10 +18,10 @@ 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 common/_sdk_session.) -from dashscope.common._sdk_session import sdk_session_id +# 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" SDK_SESSION_ID_HEADER = "x-dashscope-sdk-session-id" @@ -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/dashscope/v2/transport/http.py b/dashscope/v2/transport/http.py index ffc58c46..7bb805e1 100644 --- a/dashscope/v2/transport/http.py +++ b/dashscope/v2/transport/http.py @@ -28,10 +28,15 @@ def _map_transport_error(exc: httpx.TransportError) -> DashScopeError: - """Map raw httpx transport failures into the SDK error hierarchy.""" + """Map raw httpx transport failures into the SDK error hierarchy. + + The message text comes from the catalog entry behind each class + (``sdk.Timeout`` / ``sdk.ConnectionError``), with the httpx error as + the ``{detail}``. + """ if isinstance(exc, httpx.TimeoutException): - return APITimeoutError(f"request timed out: {exc}") - return APIConnectionError(f"connection error: {exc}") + return APITimeoutError(detail=str(exc)) + return APIConnectionError(detail=str(exc)) def _resolve_timeout( diff --git a/dashscope/v2/transport/ws.py b/dashscope/v2/transport/ws.py index 71b2c9fa..e5f073f9 100644 --- a/dashscope/v2/transport/ws.py +++ b/dashscope/v2/transport/ws.py @@ -18,7 +18,8 @@ ) from dashscope.v2.errors import ( APIConnectionError, - DashScopeError, + WsEventError, + WsStateError, error_from_business_code, ) from dashscope.v2.transport import default_user_agent @@ -31,6 +32,7 @@ "EVENT_STARTED", "WsEventError", "WsSession", + "WsStateError", ] EVENT_STARTED = "task-started" @@ -41,10 +43,6 @@ Connector = Callable[[str, Dict[str, str]], Any] -class WsEventError(DashScopeError): - """The server sent an unexpected websocket protocol event.""" - - def _default_connector( url: str, headers: Dict[str, str], @@ -145,11 +143,11 @@ def _process_event(self, event: Dict[str, Any]) -> Dict[str, Any]: event.get("header"), dict, ): - raise WsEventError("expected a websocket event header") + raise WsEventError(detail="expected a websocket event header") header = event["header"] if header.get("task_id") not in (None, self.task_id): raise WsEventError( - "websocket event belongs to a different task", + detail="websocket event belongs to a different task", request_id=self._request_id, body=event, ) @@ -269,14 +267,14 @@ def run_task( frame["header"]["streaming"] = streaming with self._lock: if self._closed or self._run_sent: - raise RuntimeError("websocket task already started or closed") + raise WsStateError(detail="task already started or closed") self._run_sent = True self._send(frame) try: event = self.recv_event() if event["header"].get("event") != EVENT_STARTED: raise WsEventError( - "expected task-started, got " + detail="expected task-started, got " f"{event['header'].get('event')!r}", request_id=self.request_id, ) @@ -288,9 +286,9 @@ def run_task( def _check_input(self) -> None: if self._closed or not self._started or self._finished: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._input_finished: - raise RuntimeError("websocket task input is finished") + raise WsStateError(detail="task input is finished") def send_binary(self, data: bytes) -> None: if not isinstance(data, (bytes, bytearray, memoryview)): @@ -312,7 +310,7 @@ def finish_task(self, input: Optional[Dict[str, Any]] = None) -> None: """ with self._lock: if self._closed or not self._started: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._finished: return cancel = input is not None and input.get("directive") == "cancel" @@ -327,7 +325,7 @@ def recv_event(self) -> Dict[str, Any]: while True: frame = self.recv() if frame is None: - raise RuntimeError("websocket task is finished") + raise WsStateError(detail="task is finished") if isinstance(frame, dict): return frame @@ -336,7 +334,7 @@ def recv(self) -> Union[Dict[str, Any], bytes, None]: if self._finished: return None if self._closed: - raise RuntimeError("websocket session is closed") + raise WsStateError(detail="session is closed") try: raw = _recv_frame(self._ws) if raw is None or raw == "": diff --git a/dashscope/v2/transport/ws_connection.py b/dashscope/v2/transport/ws_connection.py index 450873dc..27165983 100644 --- a/dashscope/v2/transport/ws_connection.py +++ b/dashscope/v2/transport/ws_connection.py @@ -40,7 +40,11 @@ settings, workspace_ws_url, ) -from dashscope.v2.errors import APIConnectionError +from dashscope.v2.errors import ( + APIConnectionError, + WsBinaryOwnerConflictError, + WsStateError, +) from dashscope.v2.transport.ws import ( EVENT_FAILED, EVENT_FINISHED, @@ -149,14 +153,14 @@ def run_task( frame["header"]["streaming"] = streaming with self._lock: if self._closed or self._run_sent: - raise RuntimeError("websocket task already started or closed") + raise WsStateError(detail="task already started or closed") self._run_sent = True self._conn._send(frame) try: event = self.recv_event() if event["header"].get("event") != EVENT_STARTED: raise WsEventError( - "expected task-started, got " + detail="expected task-started, got " f"{event['header'].get('event')!r}", request_id=self.request_id, ) @@ -168,9 +172,9 @@ def run_task( def _check_input(self) -> None: if self._closed or not self._started or self._finished: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._input_finished: - raise RuntimeError("websocket task input is finished") + raise WsStateError(detail="task input is finished") def claim_binary_downlink(self) -> None: """Route binary output frames to this task. @@ -198,7 +202,7 @@ def finish_task(self, input: Optional[Dict[str, Any]] = None) -> None: """End input, or cancel synthesis while output is still arriving.""" with self._lock: if self._closed or not self._started: - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") if self._finished: return cancel = input is not None and input.get("directive") == "cancel" @@ -215,7 +219,7 @@ def recv_event(self) -> Dict[str, Any]: while True: frame = self.recv() if frame is None: - raise RuntimeError("websocket task is finished") + raise WsStateError(detail="task is finished") if isinstance(frame, dict): return frame @@ -309,7 +313,7 @@ def open_task(self, *, timeout: Optional[float] = None) -> WsTask: raise ValueError("timeout must be positive") with self._lock: if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") task = WsTask( self, uuid.uuid4().hex, @@ -321,7 +325,7 @@ def open_task(self, *, timeout: Optional[float] = None) -> WsTask: def _send(self, data: Any, *, opcode: int = 1) -> None: with self._send_lock: if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") try: if opcode == 2: self._ws.send(data, opcode=2) @@ -334,22 +338,23 @@ def _send(self, data: Any, *, opcode: int = 1) -> None: def _check_binary_owner(self, task: WsTask) -> None: """Validate under the connection lock; pre-start claims are allowed.""" if self._closed: - raise RuntimeError("websocket connection is closed") + raise WsStateError(detail="connection is closed") if ( self._tasks.get(task.task_id) is not task or task._closed or task._finished or task._routing_finished ): - raise RuntimeError("websocket task is not active") + raise WsStateError(detail="task is not active") def _claim_binary_uplink(self, task: WsTask) -> None: with self._lock: self._check_binary_owner(task) owner = self._binary_uplink_owner if owner is not None and owner is not task: - raise RuntimeError( - "another task owns the binary uplink on this connection", + raise WsBinaryOwnerConflictError( + detail="another task owns the binary uplink on this " + "connection", ) self._binary_uplink_owner = task @@ -358,8 +363,9 @@ def _claim_binary_downlink(self, task: WsTask) -> None: self._check_binary_owner(task) owner = self._binary_downlink_owner if owner is not None and owner is not task: - raise RuntimeError( - "another task owns the binary downlink on this connection", + raise WsBinaryOwnerConflictError( + detail="another task owns the binary downlink on this " + "connection", ) self._binary_downlink_owner = task @@ -372,7 +378,7 @@ def _release_binary(self, task: WsTask) -> None: def _detach(self, task: WsTask) -> None: with self._lock: - task._enqueue((_ERROR, RuntimeError("websocket task is closed"))) + task._enqueue((_ERROR, WsStateError(detail="task is closed"))) task._closed = True task._routing_active = False if self._tasks.get(task.task_id) is task: @@ -397,10 +403,10 @@ def _reader_main(self) -> None: def _route_event(self, event: Any) -> None: header = event.get("header") if isinstance(event, dict) else None if not isinstance(header, dict): - raise WsEventError("expected a websocket event header") + raise WsEventError(detail="expected a websocket event header") task_id = header.get("task_id") if not isinstance(task_id, str) or not task_id: - raise WsEventError("expected a non-empty websocket task_id") + raise WsEventError(detail="expected a non-empty websocket task_id") with self._lock: task = self._tasks.get(task_id) if self._closed or task is None or task._routing_finished: @@ -431,8 +437,8 @@ def _route_binary(self, data: bytes) -> None: return if len(active) != 1: raise WsEventError( - "binary frame with multiple active tasks; claim the " - "downlink with claim_binary_downlink()", + detail="binary frame with multiple active tasks; " + "claim the downlink with claim_binary_downlink()", ) owner = active[0] self._binary_downlink_owner = owner diff --git a/dashscope/v2/types/messages.py b/dashscope/v2/types/messages.py index 3d7c7043..d0fea9f5 100644 --- a/dashscope/v2/types/messages.py +++ b/dashscope/v2/types/messages.py @@ -32,8 +32,8 @@ class TextPart(BaseModel): type: Literal["text"] = "text" text: str - # Explicit context-cache marker (Bailian ไธŠไธ‹ๆ–‡็ผ“ๅญ˜): a part-level - # sibling such as {"type": "ephemeral"} on the native and + # Explicit context-cache marker (Bailian context caching): a + # part-level sibling such as {"type": "ephemeral"} on the native and # OpenAI-compatible wires; the cached prefix ends at this part. # 1.x passed content blocks through unvalidated, so the marker must # stay accepted on every part type. diff --git a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py index 55bb909a..97be0ef5 100644 --- a/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/direct_call/realtime_qwen_audio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Qwen-Audio ๅ•่ฝฎๅฎžๆ—ถ่ฏญ้Ÿณๅฏน่ฏ๏ผšๅ‘้€ WAV ๆˆ–้บฆๅ…‹้ฃŽ้Ÿณ้ข‘๏ผŒไฟๅญ˜ๅนถๆ’ญๆ”พๅ›ž็ญ”ใ€‚""" +"""Single-turn realtime voice chat with Qwen-Audio: send WAV or microphone audio, save and play the reply.""" import argparse import base64 diff --git a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py index 6e4406d7..ff1ad099 100644 --- a/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py +++ b/examples/01_atomic_capabilities/audio/realtime_qwen_audio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Qwen-Audio ๅ•่ฝฎๅฎžๆ—ถ่ฏญ้Ÿณๅฏน่ฏ๏ผšๅ‘้€ WAV ๆˆ–้บฆๅ…‹้ฃŽ้Ÿณ้ข‘๏ผŒไฟๅญ˜ๅนถๆ’ญๆ”พๅ›ž็ญ”ใ€‚""" +"""Single-turn realtime voice chat with Qwen-Audio: send WAV or microphone audio, save and play the reply.""" import argparse import base64 diff --git a/examples/legacy_1x/aio_generation/basic.py b/examples/legacy_1x/aio_generation/basic.py index e58c610b..51794a95 100644 --- a/examples/legacy_1x/aio_generation/basic.py +++ b/examples/legacy_1x/aio_generation/basic.py @@ -5,7 +5,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/custom_session.py b/examples/legacy_1x/aio_generation/custom_session.py index 940457b3..cd647fb5 100644 --- a/examples/legacy_1x/aio_generation/custom_session.py +++ b/examples/legacy_1x/aio_generation/custom_session.py @@ -10,7 +10,7 @@ import aiohttp import certifi -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/deep_research.py b/examples/legacy_1x/aio_generation/deep_research.py index e442c10c..8ce3ad50 100644 --- a/examples/legacy_1x/aio_generation/deep_research.py +++ b/examples/legacy_1x/aio_generation/deep_research.py @@ -16,7 +16,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/reasoning.py b/examples/legacy_1x/aio_generation/reasoning.py index 19e03d2e..c7534d36 100644 --- a/examples/legacy_1x/aio_generation/reasoning.py +++ b/examples/legacy_1x/aio_generation/reasoning.py @@ -6,7 +6,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_generation/tool_calls.py b/examples/legacy_1x/aio_generation/tool_calls.py index c1992ab3..a76e8448 100644 --- a/examples/legacy_1x/aio_generation/tool_calls.py +++ b/examples/legacy_1x/aio_generation/tool_calls.py @@ -5,7 +5,7 @@ import asyncio import os -from dashscope.aigc.generation import AioGeneration +from dashscope.legacy_1x.aigc.generation import AioGeneration # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/aio_image_synthesis.py b/examples/legacy_1x/aio_image_synthesis.py index 1f096a4f..b60cf764 100644 --- a/examples/legacy_1x/aio_image_synthesis.py +++ b/examples/legacy_1x/aio_image_synthesis.py @@ -9,7 +9,7 @@ import os from http import HTTPStatus -from dashscope.aigc.image_synthesis import AioImageSynthesis +from dashscope.legacy_1x.aigc.image_synthesis import AioImageSynthesis # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/aio_video_synthesis.py b/examples/legacy_1x/aio_video_synthesis.py index 92a60632..931c04da 100644 --- a/examples/legacy_1x/aio_video_synthesis.py +++ b/examples/legacy_1x/aio_video_synthesis.py @@ -10,7 +10,7 @@ import os from http import HTTPStatus -from dashscope.aigc.video_synthesis import AioVideoSynthesis +from dashscope.legacy_1x.aigc.video_synthesis import AioVideoSynthesis # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/assistant_api_file.py b/examples/legacy_1x/assistant_api_file.py index 47bf5d52..6f0d770e 100644 --- a/examples/legacy_1x/assistant_api_file.py +++ b/examples/legacy_1x/assistant_api_file.py @@ -9,7 +9,7 @@ import os from dashscope import Assistants # pylint: disable=no-name-in-module -from dashscope.assistants.files import Files +from dashscope.legacy_1x.assistants.files import Files # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), and the diff --git a/examples/legacy_1x/cv_models.py b/examples/legacy_1x/cv_models.py index f5e425c0..cc17d717 100644 --- a/examples/legacy_1x/cv_models.py +++ b/examples/legacy_1x/cv_models.py @@ -7,8 +7,8 @@ import os -from dashscope.client.base_api import BaseAsyncApi -from dashscope.utils.oss_utils import check_and_upload_local +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.utils.oss_utils import check_and_upload_local # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/examples/legacy_1x/image_generation.py b/examples/legacy_1x/image_generation.py index 9b427d49..19224315 100644 --- a/examples/legacy_1x/image_generation.py +++ b/examples/legacy_1x/image_generation.py @@ -10,8 +10,8 @@ import os -from dashscope.aigc.image_generation import ImageGeneration -from dashscope.api_entities.dashscope_response import Message, Role +from dashscope.legacy_1x.aigc.image_generation import ImageGeneration +from dashscope.legacy_1x.api_entities.dashscope_response import Message, Role # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended), or pass diff --git a/examples/legacy_1x/set_region_example.py b/examples/legacy_1x/set_region_example.py index 45b45d54..c3a74028 100644 --- a/examples/legacy_1x/set_region_example.py +++ b/examples/legacy_1x/set_region_example.py @@ -11,8 +11,8 @@ """ import dashscope -from dashscope.aigc.chat_completion import Completions -from dashscope.aigc.generation import Generation +from dashscope.legacy_1x.aigc.chat_completion import Completions +from dashscope.legacy_1x.aigc.generation import Generation def example_basic_usage(): diff --git a/examples/legacy_1x/tingwu_realtime.py b/examples/legacy_1x/tingwu_realtime.py index 0ec959f1..97cba0e8 100644 --- a/examples/legacy_1x/tingwu_realtime.py +++ b/examples/legacy_1x/tingwu_realtime.py @@ -16,7 +16,7 @@ import os import time -from dashscope.multimodal.tingwu.tingwu_realtime import ( +from dashscope.legacy_1x.multimodal.tingwu.tingwu_realtime import ( TingWuRealtime, TingWuRealtimeCallback, ) diff --git a/examples/legacy_1x/tingwu_usages.py b/examples/legacy_1x/tingwu_usages.py index c5a84f44..f0ed25bd 100644 --- a/examples/legacy_1x/tingwu_usages.py +++ b/examples/legacy_1x/tingwu_usages.py @@ -3,7 +3,7 @@ import os -from dashscope.multimodal.tingwu.tingwu import TingWu +from dashscope.legacy_1x.multimodal.tingwu.tingwu import TingWu # Authentication: set your Model Studio API key via the # DASHSCOPE_API_KEY environment variable (recommended) diff --git a/tests/integration/test_large_utf8_payload.py b/tests/integration/test_large_utf8_payload.py index 9a52320a..ef84ae99 100644 --- a/tests/integration/test_large_utf8_payload.py +++ b/tests/integration/test_large_utf8_payload.py @@ -13,7 +13,7 @@ import pytest -from dashscope.aigc.generation import Generation, AioGeneration +from dashscope.legacy_1x.aigc.generation import Generation, AioGeneration WEBSEARCH_JSON = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), diff --git a/tests/legacy/conftest.py b/tests/legacy/conftest.py index e546841c..b2e886ed 100644 --- a/tests/legacy/conftest.py +++ b/tests/legacy/conftest.py @@ -7,7 +7,7 @@ import pytest import dashscope -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV +from dashscope.legacy_1x.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV from tests.unit.mock_server import create_mock_server, run_server diff --git a/tests/legacy/test_async_api.py b/tests/legacy/test_async_api.py index e8b53594..4729f220 100644 --- a/tests/legacy/test_async_api.py +++ b/tests/legacy/test_async_api.py @@ -6,9 +6,9 @@ import pytest from typing import Union -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAsyncApi -from dashscope.common.constants import ApiProtocol, HTTPMethod, TaskStatus +from dashscope.legacy_1x.api_entities.dashscope_response import DashScopeAPIResponse +from dashscope.legacy_1x.client.base_api import BaseAsyncApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod, TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_encryption.py b/tests/legacy/test_encryption.py index 5b199612..b1ffcd6f 100644 --- a/tests/legacy/test_encryption.py +++ b/tests/legacy/test_encryption.py @@ -4,7 +4,7 @@ import pytest -from dashscope.api_entities.encryption import Encryption +from dashscope.legacy_1x.api_entities.encryption import Encryption class TestEncryption: diff --git a/tests/legacy/test_image_synthesis.py b/tests/legacy/test_image_synthesis.py index 947297b4..ff39b3ba 100644 --- a/tests/legacy/test_image_synthesis.py +++ b/tests/legacy/test_image_synthesis.py @@ -6,7 +6,7 @@ import pytest from dashscope import ImageSynthesis -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_speech_recognizer.py b/tests/legacy/test_speech_recognizer.py index 3227a169..90d89c20 100644 --- a/tests/legacy/test_speech_recognizer.py +++ b/tests/legacy/test_speech_recognizer.py @@ -11,7 +11,7 @@ import pytest import dashscope -from dashscope.audio.asr import ( +from dashscope.legacy_1x.audio.asr import ( Recognition, RecognitionCallback, RecognitionResult, @@ -119,10 +119,10 @@ def complete_url(url: str) -> str: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [base_url, "api-ws", dashscope.legacy_1x.common.env.api_version, "inference"], ) dashscope.base_http_api_url = url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/legacy/test_speech_synthesis.py b/tests/legacy/test_speech_synthesis.py index 1e4d059c..28e51fbb 100644 --- a/tests/legacy/test_speech_synthesis.py +++ b/tests/legacy/test_speech_synthesis.py @@ -10,8 +10,8 @@ import pytest import dashscope -from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse -from dashscope.audio.tts import ( +from dashscope.legacy_1x.api_entities.dashscope_response import SpeechSynthesisResponse +from dashscope.legacy_1x.audio.tts import ( ResultCallback, SpeechSynthesisResult, SpeechSynthesizer, diff --git a/tests/legacy/test_speech_transcription.py b/tests/legacy/test_speech_transcription.py index b196d526..293cc610 100644 --- a/tests/legacy/test_speech_transcription.py +++ b/tests/legacy/test_speech_transcription.py @@ -10,8 +10,8 @@ import pytest import dashscope -from dashscope.audio.asr import Transcription -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.audio.asr import Transcription +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment HTTPS_16K_CH1_WAV = "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example.wav" # noqa: * @@ -132,10 +132,10 @@ def complete_url(url: str) -> str: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [base_url, "api-ws", dashscope.legacy_1x.common.env.api_version, "inference"], ) dashscope.base_http_api_url = url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/legacy/test_video_synthesis.py b/tests/legacy/test_video_synthesis.py index 8b197469..678e2097 100644 --- a/tests/legacy/test_video_synthesis.py +++ b/tests/legacy/test_video_synthesis.py @@ -6,7 +6,7 @@ import pytest from dashscope import VideoSynthesis -from dashscope.common.constants import TaskStatus +from dashscope.legacy_1x.common.constants import TaskStatus from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/legacy/test_websocket_async_api.py b/tests/legacy/test_websocket_async_api.py index 00a6a970..4015481a 100644 --- a/tests/legacy/test_websocket_async_api.py +++ b/tests/legacy/test_websocket_async_api.py @@ -4,7 +4,7 @@ import pytest import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import TestTasks from tests.unit.websocket_task_request import WebSocketRequest diff --git a/tests/legacy/test_websocket_parameters.py b/tests/legacy/test_websocket_parameters.py index 2211ec5a..4e240b00 100644 --- a/tests/legacy/test_websocket_parameters.py +++ b/tests/legacy/test_websocket_parameters.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, diff --git a/tests/legacy/test_websocket_sync_api.py b/tests/legacy/test_websocket_sync_api.py index 46dede12..c61cdfac 100644 --- a/tests/legacy/test_websocket_sync_api.py +++ b/tests/legacy/test_websocket_sync_api.py @@ -2,7 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import dashscope -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode from tests.unit.base_test import BaseTestEnvironment from tests.unit.constants import TestTasks from tests.unit.websocket_task_request import WebSocketRequest diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 78068085..fdcb6efc 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -6,7 +6,9 @@ import pytest -from dashscope.common.constants import DASHSCOPE_DISABLE_DATA_INSPECTION_ENV +from dashscope.legacy_1x.common.constants import ( + DASHSCOPE_DISABLE_DATA_INSPECTION_ENV, +) from tests.unit.mock_server import create_mock_server, run_server diff --git a/tests/unit/http_task_request.py b/tests/unit/http_task_request.py index b8e893e6..47f4c05f 100644 --- a/tests/unit/http_task_request.py +++ b/tests/unit/http_task_request.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class HttpRequest(BaseApi, BaseAioApi): diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 0713088c..cd2e578f 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -11,7 +11,7 @@ import aiohttp from aiohttp import web -from dashscope.protocol.websocket import ActionType +from dashscope.legacy_1x.protocol.websocket import ActionType from tests.unit.constants import ( TEST_DISABLE_DATA_INSPECTION_REQUEST_ID, TEST_ENABLE_DATA_INSPECTION_REQUEST_ID, diff --git a/tests/unit/test_acli_sdk_contract.py b/tests/unit/test_acli_sdk_contract.py new file mode 100644 index 00000000..d8a32936 --- /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"{str({m: found[m] for m in added})}\n" + f" no longer imported (_ALLOWLIST is stale): {gone}" + ) + + +def test_acli_does_not_import_the_v2_package(): + v2 = { + m: loc + for m, loc in _acli_external_imports().items() + if m.split(".")[1:2] == ["v2"] + } + assert not v2, ( + "acli is a 1.x-side consumer today; moving it onto the 2.0 client " + f"is upstream agenticCLI work, not this repo's: {v2}" + ) + + +# --------------------------------------------------------------------------- +# Audio contract +# --------------------------------------------------------------------------- + + +def test_every_name_acli_imports_from_the_sdk_resolves(): + names = _acli_external_names() + assert names, "expected acli to import names from the SDK" + broken = {} + for module, wanted in sorted(names.items()): + try: + imported = importlib.import_module(module) + except ImportError as exc: + broken[module] = f"import failed: {exc}" + continue + absent = [name for name in wanted if not hasattr(imported, name)] + if absent: + broken[module] = absent + assert not broken, f"SDK surface acli relies on is incomplete: {broken}" + + +def test_tts_v2_exposes_the_names_acli_uses(): + module = importlib.import_module("dashscope.legacy_1x.audio.tts_v2") + for name in ("AudioFormat", "ResultCallback", "SpeechSynthesizer"): + assert hasattr( + module, + name, + ), f"dashscope.legacy_1x.audio.tts_v2.{name} is gone" + + +def test_asr_exposes_the_names_acli_uses(): + module = importlib.import_module("dashscope.legacy_1x.audio.asr") + for name in ("Recognition", "RecognitionCallback"): + assert hasattr( + module, + name, + ), f"dashscope.legacy_1x.audio.asr.{name} is gone" + + +# --------------------------------------------------------------------------- +# The root global acli writes +# --------------------------------------------------------------------------- + + +def test_root_api_key_is_readable_and_writable(monkeypatch): + assert hasattr(dashscope, "api_key"), ( + "the root package must keep exposing ``api_key``: acli's voice " + "output reads it and then assigns to it" + ) + monkeypatch.setattr(dashscope, "api_key", "sk-acli-contract") + assert dashscope.api_key == "sk-acli-contract" + + +def test_acli_tts_still_writes_the_root_api_key(): + attrs = {attr for attr, _ in _root_global_writes()} + assert "api_key" in attrs, ( + "acli no longer assigns ``dashscope.api_key``. If upstream moved " + "to the environment variable alone, the root-global requirement " + "has genuinely relaxed -- confirm that, then drop this test and " + "the one above it." + ) + + +def test_acli_also_sets_the_env_var(): + assert _environ_setdefault_calls(), ( + "acli is expected to os.environ.setdefault('DASHSCOPE_API_KEY', " + "...) alongside the root global, so both discovery paths work" + ) + + +# --------------------------------------------------------------------------- +# Neutrality: acli belongs to neither side +# --------------------------------------------------------------------------- + + +def test_v2_package_does_not_import_acli(): + hits = { + m: loc + for m, loc in _imports_under(str(_V2)).items() + if _is_acli_internal(m) + } + assert not hits, f"2.0 must not depend on acli: {hits}" + + +def test_package_root_does_not_import_acli(): + hits = { + m: loc + for m, loc in _imports_under(str(_ROOT_INIT)).items() + if _is_acli_internal(m) + } + assert not hits, f"the 1.x package root must not depend on acli: {hits}" + + +def test_acli_lives_at_the_top_level(): + """acli is a neutral surface: neither the 2.0 nor the legacy bucket.""" + assert _ACLI.is_dir(), "dashscope/acli/ must stay at the top level" + moved_into_v2 = (_REPO / "dashscope" / "v2" / "acli").exists() + moved_into_legacy = (_REPO / "dashscope" / "legacy" / "acli").exists() + assert not moved_into_v2, "acli must not be folded into 2.0" + assert not moved_into_legacy, "acli must not land in the legacy bucket" + + +def test_acli_is_not_reexported_at_the_package_root(): + assert "acli" not in dashscope.__all__ + source = _ROOT_INIT.read_text(encoding="utf-8") + assert "acli" not in source, ( + "acli is an independently installed entry point, not part of the " + "1.x API layer" + ) + + +# --------------------------------------------------------------------------- +# Entry point and packaging +# --------------------------------------------------------------------------- + + +def test_module_entry_point_resolvable(): + spec = importlib.util.find_spec("dashscope.acli.__main__") + assert spec is not None, "``python -m dashscope.acli`` needs __main__.py" + main = importlib.import_module("dashscope.acli.cli").main + assert callable(main) + + +def test_setup_declares_the_acli_extras(): + tree = ast.parse( + (_REPO / "setup.py").read_text(encoding="utf-8"), + "setup.py", + ) + groups = None + for node in ast.walk(tree): + if isinstance(node, ast.keyword) and node.arg == "extras_require": + groups = { + key.value + for key in node.value.keys + if isinstance(key, ast.Constant) + } + assert groups is not None, "setup.py lost its extras_require" + missing = sorted(_ACLI_EXTRAS - groups) + assert not missing, f"setup.py extras_require is missing: {missing}" diff --git a/tests/unit/test_agentic_rl_cli_opd.py b/tests/unit/test_agentic_rl_cli_opd.py index e217842e..59620f36 100644 --- a/tests/unit/test_agentic_rl_cli_opd.py +++ b/tests/unit/test_agentic_rl_cli_opd.py @@ -8,7 +8,7 @@ from typer.main import get_command from typer.testing import CliRunner -from dashscope.cli.agentic_rl import _run_workflow_async, app +from dashscope.legacy_1x.cli.agentic_rl import _run_workflow_async, app from dashscope.finetune.agentic_rl import AgenticRL @@ -16,14 +16,16 @@ def test_run_help_explains_conditional_function_requirements(): result = CliRunner().invoke(app, ["run", "--help"]) assert result.exit_code == 0 - # ๅ‚ๆ•ฐๅญ˜ๅœจๆ€ง่ตฐ typer ๅ‘ฝไปคๆจกๅž‹ๆ ก้ชŒ๏ผšๆธฒๆŸ“ๆ–‡ๆœฌๅœจไธๅŒ็ปˆ็ซฏๅฎฝๅบฆ/ๅนณๅฐไธ‹ไผš่ขซ - # rich ๆˆชๆ–ญๆˆ–่ฟžๅญ—็ฌฆๆข่กŒ๏ผˆCI Linux ๆ›พๅ› ๆญค่ฏฏๆŠฅ --teacher-model ็ผบๅคฑ๏ผ‰๏ผŒ - # ไธๆ–ญ่จ€ๆธฒๆŸ“ๅŽ็š„ help ๆ–‡ๆœฌ้‡Œๅ‡บ็ŽฐๅฎŒๆ•ด option ๅใ€‚ + # Parameter presence is asserted through typer's command model: rich + # truncates or hyphen-wraps rendered help at some terminal widths + # (CI Linux once false-reported --teacher-model as missing), so we do + # not assert full option names in the rendered text. run_cmd = get_command(app).commands["run"] assert "teacher_model" in {param.name for param in run_cmd.params} - # help ๆ–‡ๆกฃไป้กป่ฏดๆ˜Ž OPD ็š„ๆกไปถๅŒ–ๅ‡ฝๆ•ฐ่ฆๆฑ‚๏ผ›ๅ‰ฅ็ฆป ANSI ๅŽๆŒ‰็ฉบ็™ฝๅฝ’ไธ€๏ผŒ - # ๅชๆ–ญ่จ€ๆ•ฃๆ–‡ๅผๆ่ฟฐ๏ผˆๆŒ‰็ฉบๆ ผๆŠ˜่กŒ๏ผŒๅฝ’ไธ€ๅŒ–ๅŽ็จณๅฎš๏ผ‰ใ€‚ + # The help text must still document OPD's conditional function + # requirements; strip ANSI and normalize whitespace, then assert only + # the prose description (folded at spaces, stable once normalized). help_text = " ".join(re.sub(r"\x1b\[[0-9;]*m", "", result.output).split()) assert ( "1. Configuration-driven: Use -c/--config to specify a YAML file" @@ -52,7 +54,7 @@ async def inspect_config(client): with ( patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ), patch.object( AgenticRL, diff --git a/tests/unit/test_agentic_rl_submit_job.py b/tests/unit/test_agentic_rl_submit_job.py index 9fc79ffb..0da6ed38 100644 --- a/tests/unit/test_agentic_rl_submit_job.py +++ b/tests/unit/test_agentic_rl_submit_job.py @@ -392,7 +392,10 @@ async def test_main_workflow_run_exception(self): new_callable=AsyncMock, side_effect=Exception("Network error"), ): + # astroid reads __new__ as a classmethod through the 1.x->v2 MRO + # pylint: disable=no-value-for-parameter client = AgenticRL.__new__(AgenticRL) + # pylint: enable=no-value-for-parameter with pytest.raises(Exception, match="Network error"): await client.run( model="qwen3.5-9b", @@ -499,7 +502,7 @@ def test_init_with_valid_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: client = AgenticRL(api_key="sk-test-key") mock_set_key.assert_called_once_with("sk-test-key") @@ -512,7 +515,7 @@ def test_init_with_invalid_api_key(self): ) with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", side_effect=Exception("Invalid key"), ): with pytest.raises(ValueErrorWithCode): @@ -522,7 +525,7 @@ def test_init_without_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL() mock_set_key.assert_called_once_with(None) @@ -531,7 +534,7 @@ def test_init_with_api_key(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.set_api_key", + "dashscope.v2.finetune.agentic_rl.set_api_key", ) as mock_set_key: _ = AgenticRL(api_key="your_api_key_here") mock_set_key.assert_called_once_with("your_api_key_here") @@ -659,7 +662,7 @@ def test_submit_job_builds_correct_request(self): from dashscope.finetune.agentic_rl import AgenticRL with patch( - "dashscope.finetune.agentic_rl.generate_random_id", + "dashscope.v2.finetune.agentic_rl.generate_random_id", return_value="abcdef1234567890", ): client = AgenticRL() diff --git a/tests/unit/test_aio_session.py b/tests/unit/test_aio_session.py index b82061a9..e236fd90 100644 --- a/tests/unit/test_aio_session.py +++ b/tests/unit/test_aio_session.py @@ -16,7 +16,7 @@ import aiohttp import pytest -from dashscope.api_entities import aio_session +from dashscope.legacy_1x.api_entities import aio_session class TestSSLContextCaching: diff --git a/tests/unit/test_aio_session_cleanup.py b/tests/unit/test_aio_session_cleanup.py index 2e2c4c0d..3d95cf3d 100644 --- a/tests/unit/test_aio_session_cleanup.py +++ b/tests/unit/test_aio_session_cleanup.py @@ -33,7 +33,9 @@ def test_single_thread(): warnings.filterwarnings("always", category=ResourceWarning) async def simulate_multimodal_conversation(): - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) print("[Step 1] Creating shared aiohttp session...") session = await get_shared_aio_session() @@ -63,7 +65,9 @@ def run_async_in_thread(thread_id): """Run async code in a separate thread with its own event loop.""" async def create_session(): - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) session = await get_shared_aio_session() print(f" Thread {thread_id}: Session {id(session)} created") @@ -146,7 +150,9 @@ async def test_concurrent_tasks_same_loop(): print("=" * 70) print() - from dashscope.api_entities.aio_session import get_shared_aio_session + from dashscope.legacy_1x.api_entities.aio_session import ( + get_shared_aio_session, + ) num_tasks = 10 print(f"Creating {num_tasks} concurrent tasks in same event loop...") diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 480c0712..ead3b65f 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -10,7 +10,7 @@ from typing import Dict from dashscope import Application -from dashscope.app.application_response import ApplicationResponse +from dashscope.legacy_1x.app.application_response import ApplicationResponse from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_asr_phrases.py b/tests/unit/test_asr_phrases.py index 942b55bb..29d2f75c 100644 --- a/tests/unit/test_asr_phrases.py +++ b/tests/unit/test_asr_phrases.py @@ -10,7 +10,7 @@ import pytest import dashscope -from dashscope.audio.asr import AsrPhraseManager +from dashscope.legacy_1x.audio.asr import AsrPhraseManager from tests.unit.constants import TEST_JOB_ID from tests.unit.mock_request_base import MockRequestBase @@ -115,10 +115,15 @@ def complete_url(url: str) -> None: parsed = urlparse(url) base_url = "".join([parsed.scheme, "://", parsed.netloc]) dashscope.base_websocket_api_url = "/".join( - [base_url, "api-ws", dashscope.common.env.api_version, "inference"], + [ + base_url, + "api-ws", + dashscope.legacy_1x.common.env.api_version, + "inference", + ], ) dashscope.base_http_api_url = "/".join( - [base_url, "api", dashscope.common.env.api_version], + [base_url, "api", dashscope.legacy_1x.common.env.api_version], ) print("Set base_websocket_api_url: ", dashscope.base_websocket_api_url) print("Set base_http_api_url: ", dashscope.base_http_api_url) diff --git a/tests/unit/test_assistant_files.py b/tests/unit/test_assistant_files.py index 1ea08119..b6c22365 100644 --- a/tests/unit/test_assistant_files.py +++ b/tests/unit/test_assistant_files.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope.assistants.files import Files +from dashscope.legacy_1x.assistants.files import Files from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_async_custom_session.py b/tests/unit/test_async_custom_session.py index 10d31051..4d8f1757 100644 --- a/tests/unit/test_async_custom_session.py +++ b/tests/unit/test_async_custom_session.py @@ -2,16 +2,16 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """ -ๅผ‚ๆญฅ HTTP ่‡ชๅฎšไน‰ Session ๅŠŸ่ƒฝๅ•ๅ…ƒๆต‹่ฏ• +Unit tests for async HTTP custom session support. -ๆต‹่ฏ•่Œƒๅ›ด๏ผš -1. HttpRequest ๆŽฅๅ—่‡ชๅฎšไน‰ aiohttp.ClientSession ๅ‚ๆ•ฐ -2. ่‡ชๅฎšไน‰ aio_session ็š„ไฝฟ็”จๅ’Œ่ต„ๆบ็ฎก็† -3. ไธดๆ—ถ aio_session ็š„ๅˆ›ๅปบๅ’Œๆธ…็† -4. Session ไผ˜ๅ…ˆ็บง้€ป่พ‘ -5. ไธๅŒๅœบๆ™ฏไธ‹็š„ๅผ‚ๆญฅ Session ่กŒไธบ +Scope: +1. HttpRequest accepts a custom aiohttp.ClientSession parameter +2. Usage and resource management of a custom aio_session +3. Creation and cleanup of temporary aio_session instances +4. Session priority logic +5. Async session behavior across different scenarios -ๆณจๆ„๏ผšๆ‰€ๆœ‰ๆต‹่ฏ•้ƒฝไธไพ่ต–็œŸๅฎž็š„ API Key +Note: none of the tests rely on a real API key. """ # pylint: disable=protected-access,unused-argument,unused-variable @@ -24,17 +24,17 @@ import certifi import pytest -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class TestAsyncSessionBasics: - """ๆต‹่ฏ•ๅผ‚ๆญฅ Session ๅŸบๆœฌๅŠŸ่ƒฝ""" + """Basic tests for async sessions""" @pytest.mark.asyncio async def test_http_request_accepts_aio_session_parameter(self): - """ๆต‹่ฏ• HttpRequest ๆŽฅๅ— aio_session ๅ‚ๆ•ฐ""" + """Test that HttpRequest accepts the aio_session parameter""" connector = aiohttp.TCPConnector() custom_session = aiohttp.ClientSession(connector=connector) @@ -53,7 +53,7 @@ async def test_http_request_accepts_aio_session_parameter(self): @pytest.mark.asyncio async def test_http_request_without_aio_session_parameter(self): - """ๆต‹่ฏ• HttpRequest ไธไผ  aio_session ๅ‚ๆ•ฐ""" + """Test HttpRequest without the aio_session parameter""" http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -64,8 +64,8 @@ async def test_http_request_without_aio_session_parameter(self): @pytest.mark.asyncio async def test_aio_session_parameter_is_optional(self): - """ๆต‹่ฏ• aio_session ๅ‚ๆ•ฐๆ˜ฏๅฏ้€‰็š„""" - # ไธไผ  aio_session ๅ‚ๆ•ฐๅบ”่ฏฅๆญฃๅธธๅทฅไฝœ + """Test that the aio_session parameter is optional""" + # Works without the aio_session parameter http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -78,12 +78,12 @@ async def test_aio_session_parameter_is_optional(self): class TestAsyncSessionUsage: - """ๆต‹่ฏ•ๅผ‚ๆญฅ Session ็š„ๅฎž้™…ไฝฟ็”จ""" + """Tests for actual async session usage""" @pytest.mark.asyncio async def test_custom_aio_session_is_used_for_request(self): - """ๆต‹่ฏ•่‡ชๅฎšไน‰ aio_session ่ขซๅฎž้™…็”จไบŽ่ฏทๆฑ‚""" - # ๅˆ›ๅปบ mock session + """Test that the custom aio_session is actually used for the request""" + # Create a mock session mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -97,7 +97,7 @@ async def mock_request(*_args, **_kwargs): mock_session.request = mock_request - # ๅˆ›ๅปบ HttpRequest ๅนถไผ ๅ…ฅ่‡ชๅฎšไน‰ session + # Create an HttpRequest with the custom session http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -106,7 +106,7 @@ async def mock_request(*_args, **_kwargs): session=mock_session, ) - # ๆทปๅŠ ่ฏทๆฑ‚ๆ•ฐๆฎ + # Add request data request_data = ApiRequestData( model="test-model", task_group="test", @@ -119,7 +119,7 @@ async def mock_request(*_args, **_kwargs): ) http_request.data = request_data - # ๆ‰ง่กŒ่ฏทๆฑ‚ + # Execute the request async def mock_handle_response(_response): yield mock_response @@ -130,15 +130,16 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # ้ชŒ่ฏ่‡ชๅฎšไน‰ session ๆฒกๆœ‰่ขซๅ…ณ้—ญ + # Verify the custom session was not closed mock_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_is_used_when_no_custom_session( self, ): - """ๆต‹่ฏ•ๆฒกๆœ‰่‡ชๅฎšไน‰ aio_session ๆ—ถไฝฟ็”จๅ…ฑไบซ session๏ผˆไธ่ขซๅ…ณ้—ญ๏ผ‰""" - # ๅˆ›ๅปบ mock shared session + """Test that the shared session is used (and not closed) when no + custom aio_session is given""" + # Create a mock shared session mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -147,7 +148,7 @@ async def test_shared_aio_session_is_used_when_no_custom_session( mock_response.__aexit__ = AsyncMock(return_value=None) mock_session.request.return_value = mock_response - # ๅˆ›ๅปบ HttpRequest ไธไผ  aio_session + # Create an HttpRequest without aio_session http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -155,7 +156,7 @@ async def test_shared_aio_session_is_used_when_no_custom_session( stream=False, ) - # ๆทปๅŠ ่ฏทๆฑ‚ๆ•ฐๆฎ + # Add request data request_data = ApiRequestData( model="test-model", task_group="test", @@ -168,12 +169,12 @@ async def test_shared_aio_session_is_used_when_no_custom_session( ) http_request.data = request_data - # ๆ‰ง่กŒ่ฏทๆฑ‚ + # Execute the request async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -183,16 +184,17 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # ๅ…ฑไบซ session ไธๅบ”่ขซๅ…ณ้—ญ๏ผˆ็”ฑ aio_session ๆจกๅ—็ฎก็†็”Ÿๅ‘ฝๅ‘จๆœŸ๏ผ‰ + # The shared session must not be closed (lifecycle is managed + # by the aio_session module) mock_session.close.assert_not_called() class TestAsyncSessionResourceManagement: - """ๆต‹่ฏ•ๅผ‚ๆญฅ Session ่ต„ๆบ็ฎก็†""" + """Tests for async session resource management""" @pytest.mark.asyncio async def test_custom_aio_session_not_closed_by_http_request(self): - """ๆต‹่ฏ•่‡ชๅฎšไน‰ aio_session ไธไผš่ขซ HttpRequest ๅ…ณ้—ญ""" + """Test that HttpRequest does not close the custom aio_session""" custom_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -236,12 +238,13 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # ้ชŒ่ฏ่‡ชๅฎšไน‰ aio_session ๆฒกๆœ‰่ขซๅ…ณ้—ญ + # Verify the custom aio_session was not closed custom_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_not_closed_on_success(self): - """ๆต‹่ฏ•ๅ…ฑไบซ aio_session ๅœจๆˆๅŠŸๅŽไธ่ขซๅ…ณ้—ญ๏ผˆ็”ฑๆจกๅ—็ฎก็†็”Ÿๅ‘ฝๅ‘จๆœŸ๏ผ‰""" + """Test that the shared aio_session is not closed on success + (lifecycle managed by the module)""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -273,7 +276,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -283,12 +286,12 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # ๅ…ฑไบซ session ไธๅบ”่ขซๅ…ณ้—ญ + # The shared session must not be closed mock_session.close.assert_not_called() @pytest.mark.asyncio async def test_shared_aio_session_not_closed_on_exception(self): - """ๆต‹่ฏ•ๅ…ฑไบซ aio_session ๅœจๅผ‚ๅธธๆ—ถไนŸไธ่ขซๅ…ณ้—ญ""" + """Test that the shared aio_session is not closed on exceptions either""" mock_session = AsyncMock() # Make request() raise an exception @@ -316,24 +319,24 @@ async def mock_request(*_args, **_kwargs): ) http_request.data = request_data - # ๆ‰ง่กŒ่ฏทๆฑ‚ๅบ”่ฏฅๆŠ›ๅ‡บๅผ‚ๅธธ + # Executing the request should raise with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(Exception, match="Network error"): _ = await http_request.aio_call() - # ๅ…ฑไบซ session ไป็„ถไธๅบ”่ขซๅ…ณ้—ญ๏ผˆ็”ฑๆจกๅ—็ฎก็†็”Ÿๅ‘ฝๅ‘จๆœŸ๏ผ‰ + # The shared session still must not be closed (lifecycle managed by the module) mock_session.close.assert_not_called() class TestAsyncSessionWithCustomConfiguration: - """ๆต‹่ฏ•่‡ชๅฎšไน‰้…็ฝฎ็š„ๅผ‚ๆญฅ Session""" + """Tests for async sessions with custom configuration""" @pytest.mark.asyncio async def test_custom_aio_session_with_connector(self): - """ๆต‹่ฏ•ๅธฆ่‡ชๅฎšไน‰ connector ็š„ aio_session""" + """Test an aio_session with a custom connector""" connector = aiohttp.TCPConnector( limit=100, limit_per_host=30, @@ -350,14 +353,14 @@ async def test_custom_aio_session_with_connector(self): ) assert http_request._external_aio_session is custom_session - # ้ชŒ่ฏ connector ๅทฒ้…็ฝฎ + # Verify the connector is configured assert custom_session.connector is not None finally: await custom_session.close() @pytest.mark.asyncio async def test_custom_aio_session_with_headers(self): - """ๆต‹่ฏ•ๅธฆ่‡ชๅฎšไน‰ headers ็š„ aio_session""" + """Test an aio_session with custom headers""" custom_headers = { "User-Agent": "Custom-Agent/1.0", "X-Custom-Header": "custom-value", @@ -373,14 +376,14 @@ async def test_custom_aio_session_with_headers(self): ) assert http_request._external_aio_session is custom_session - # ้ชŒ่ฏ headers ๅทฒ้…็ฝฎ + # Verify the headers are configured assert "User-Agent" in custom_session.headers finally: await custom_session.close() @pytest.mark.asyncio async def test_custom_aio_session_with_timeout(self): - """ๆต‹่ฏ•ๅธฆ่‡ชๅฎšไน‰ timeout ็š„ aio_session""" + """Test an aio_session with a custom timeout""" timeout = aiohttp.ClientTimeout(total=60) custom_session = aiohttp.ClientSession(timeout=timeout) @@ -393,18 +396,18 @@ async def test_custom_aio_session_with_timeout(self): ) assert http_request._external_aio_session is custom_session - # ้ชŒ่ฏ timeout ๅทฒ้…็ฝฎ + # Verify the timeout is configured assert custom_session.timeout is not None finally: await custom_session.close() class TestAsyncSessionPriority: - """ๆต‹่ฏ•ๅผ‚ๆญฅ Session ไผ˜ๅ…ˆ็บง""" + """Tests for async session priority""" @pytest.mark.asyncio async def test_custom_aio_session_has_priority(self): - """ๆต‹่ฏ•่‡ชๅฎšไน‰ aio_session ไผ˜ๅ…ˆไบŽไธดๆ—ถ aio_session""" + """Test that a custom aio_session takes priority over a temporary one""" connector = aiohttp.TCPConnector() custom_session = aiohttp.ClientSession(connector=connector) @@ -416,7 +419,7 @@ async def test_custom_aio_session_has_priority(self): session=custom_session, ) - # ้ชŒ่ฏๅญ˜ๅ‚จไบ†่‡ชๅฎšไน‰ aio_session + # Verify the custom aio_session is stored assert http_request._external_aio_session is custom_session assert http_request._external_aio_session is not None finally: @@ -424,11 +427,11 @@ async def test_custom_aio_session_has_priority(self): class TestAsyncSessionWithDifferentMethods: - """ๆต‹่ฏ•ไธๅŒ HTTP ๆ–นๆณ•็š„ๅผ‚ๆญฅ Session ไฝฟ็”จ""" + """Tests for async session usage across HTTP methods""" @pytest.mark.asyncio async def test_custom_aio_session_with_post_request(self): - """ๆต‹่ฏ• POST ่ฏทๆฑ‚ไฝฟ็”จ่‡ชๅฎšไน‰ session""" + """Test that POST requests use the custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -475,7 +478,7 @@ async def mock_handle_response(_response): @pytest.mark.asyncio async def test_custom_aio_session_with_get_request(self): - """ๆต‹่ฏ• GET ่ฏทๆฑ‚ไฝฟ็”จ่‡ชๅฎšไน‰ session""" + """Test that GET requests use the custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -522,11 +525,11 @@ async def mock_handle_response(_response): class TestAsyncBackwardCompatibility: - """ๆต‹่ฏ•ๅผ‚ๆญฅๅ‘ๅŽๅ…ผๅฎนๆ€ง""" + """Tests for async backward compatibility""" @pytest.mark.asyncio async def test_works_without_aio_session_parameter(self): - """ๆต‹่ฏ•ไธไผ  aio_session ๅ‚ๆ•ฐๆ—ถไฟๆŒๅŽŸๆœ‰่กŒไธบ""" + """Test that omitting aio_session keeps the original behavior""" http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -534,16 +537,16 @@ async def test_works_without_aio_session_parameter(self): stream=False, ) - # ้ชŒ่ฏไธไผ  aio_session ๆ—ถ๏ผŒ_external_aio_session ไธบ None + # Verify _external_aio_session is None when aio_session is not passed assert http_request._external_aio_session is None - # ้ชŒ่ฏๅ…ถไป–ๅ‚ๆ•ฐๆญฃๅธธ + # Verify the other parameters are unaffected assert http_request.url == "http://example.com/api" assert http_request.method == HTTPMethod.POST @pytest.mark.asyncio async def test_default_behavior_uses_shared_session(self): - """ๆต‹่ฏ•้ป˜่ฎค่กŒไธบไฝฟ็”จๅ…ฑไบซ session๏ผˆไธ่ขซๅ…ณ้—ญ๏ผ‰""" + """Test that the default behavior uses the shared session (not closed)""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 @@ -552,7 +555,7 @@ async def test_default_behavior_uses_shared_session(self): mock_response.__aexit__ = AsyncMock(return_value=None) mock_session.request.return_value = mock_response - # ไธไผ  aio_session ๅ‚ๆ•ฐ + # Do not pass the aio_session parameter http_request = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -576,7 +579,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -586,16 +589,17 @@ async def mock_handle_response(_response): ): _ = await http_request.aio_call() - # ๅ…ฑไบซ session ไธๅบ”่ขซๅ…ณ้—ญ๏ผˆ็”Ÿๅ‘ฝๅ‘จๆœŸ็”ฑ aio_session ๆจกๅ—็ฎก็†๏ผ‰ + # The shared session must not be closed (lifecycle managed + # by the aio_session module) mock_session.close.assert_not_called() class TestAsyncSessionLifecycle: - """ๆต‹่ฏ•ๅผ‚ๆญฅ Session ็”Ÿๅ‘ฝๅ‘จๆœŸ""" + """Tests for the async session lifecycle""" @pytest.mark.asyncio async def test_multiple_requests_with_same_custom_session(self): - """ๆต‹่ฏ•ไฝฟ็”จๅŒไธ€ไธช่‡ชๅฎšไน‰ session ่ฟ›่กŒๅคšๆฌก่ฏทๆฑ‚""" + """Test multiple requests with the same custom session""" mock_session = AsyncMock(spec=aiohttp.ClientSession) mock_response = AsyncMock() mock_response.status = 200 @@ -608,7 +612,7 @@ async def mock_request(*_args, **_kwargs): mock_session.request = mock_request - # ็ฌฌไธ€ๆฌก่ฏทๆฑ‚ + # First request http_request1 = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -639,7 +643,7 @@ async def mock_handle_response(_response): ): _ = await http_request1.aio_call() - # ็ฌฌไบŒๆฌก่ฏทๆฑ‚ + # Second request http_request2 = HttpRequest( url="http://example.com/api", api_key="fake-api-key", @@ -667,12 +671,13 @@ async def mock_handle_response(_response): ): _ = await http_request2.aio_call() - # ้ชŒ่ฏ session ๆฒกๆœ‰่ขซๅ…ณ้—ญ๏ผˆๅ› ไธบๆ˜ฏๅค–้ƒจ session๏ผ‰ + # Verify the session was not closed (it is an external session) mock_session.close.assert_not_called() class TestAsyncConnectionRetry: - """ๆต‹่ฏ•ๅผ‚ๆญฅๆฑ ๅŒ–่ฟžๆŽฅ่ขซ่ฟœ็ซฏๅ…ณ้—ญๅŽ็š„้‡่ฏ•่กŒไธบ""" + """Tests for retry behavior after a pooled async connection is closed + by the remote peer""" def _build_post_request(self): http_request = HttpRequest( @@ -704,7 +709,7 @@ def _ok_response(): @pytest.mark.asyncio async def test_retry_once_on_connection_error(self): - """่ฟžๆŽฅ่ขซ่ฟœ็ซฏๅ…ณ้—ญๆ—ถ้‡่ฏ•ไธ€ๆฌกๅนถๆˆๅŠŸ""" + """Retries once and succeeds when the connection is closed by the remote peer""" mock_session = AsyncMock() mock_response = self._ok_response() mock_session.request.side_effect = [ @@ -718,7 +723,7 @@ async def mock_handle_response(_response): yield mock_response with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with patch.object( @@ -732,7 +737,7 @@ async def mock_handle_response(_response): @pytest.mark.asyncio async def test_retry_exhausted_raises(self): - """่ฟž็ปญไธคๆฌก ClientConnectionError ๅŽๅ‘ไธŠๆŠ›ๅ‡บ""" + """Raises after two consecutive ClientConnectionErrors""" mock_session = AsyncMock() mock_session.request.side_effect = aiohttp.ClientConnectionError( "Server disconnected", @@ -741,7 +746,7 @@ async def test_retry_exhausted_raises(self): http_request = self._build_post_request() with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(aiohttp.ClientConnectionError): @@ -751,14 +756,14 @@ async def test_retry_exhausted_raises(self): @pytest.mark.asyncio async def test_no_retry_on_other_exceptions(self): - """้ž่ฟžๆŽฅ็ฑปๅผ‚ๅธธไธ้‡่ฏ•""" + """Does not retry non-connection errors""" mock_session = AsyncMock() mock_session.request.side_effect = ValueError("bad request") http_request = self._build_post_request() with patch( - "dashscope.api_entities.http_request.get_shared_aio_session", + "dashscope.legacy_1x.api_entities.http_request.get_shared_aio_session", return_value=mock_session, ): with pytest.raises(ValueError, match="bad request"): diff --git a/tests/unit/test_cli_agent_route.py b/tests/unit/test_cli_agent_route.py index 46375f57..e5200335 100644 --- a/tests/unit/test_cli_agent_route.py +++ b/tests/unit/test_cli_agent_route.py @@ -23,7 +23,7 @@ import pytest import dashscope -from dashscope import cli +from dashscope.legacy_1x import cli @pytest.fixture() diff --git a/tests/unit/test_cli_application.py b/tests/unit/test_cli_application.py index 957f8300..71458a55 100644 --- a/tests/unit/test_cli_application.py +++ b/tests/unit/test_cli_application.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import application +from dashscope.legacy_1x.cli import application class TestCliApplication: diff --git a/tests/unit/test_cli_auth.py b/tests/unit/test_cli_auth.py index f3997b45..84e95725 100644 --- a/tests/unit/test_cli_auth.py +++ b/tests/unit/test_cli_auth.py @@ -17,9 +17,9 @@ from typer.testing import CliRunner import dashscope -from dashscope.cli import auth as auth_cli -from dashscope.common import api_key as api_key_mod -from dashscope.common.constants import ( +from dashscope.legacy_1x.cli import auth as auth_cli +from dashscope.legacy_1x.common import api_key as api_key_mod +from dashscope.legacy_1x.common.constants import ( DASHSCOPE_API_KEY_ENV, DASHSCOPE_API_KEY_FILE_PATH_ENV, ) @@ -67,7 +67,7 @@ def key_store(monkeypatch, tmp_path): key_file = cache_dir / "api_key" cache_dir.mkdir(parents=True, exist_ok=True) # ``save_api_key``/``get_default_api_key`` read the constant from - # dashscope.common.api_key, while the printed path and the source check in + # dashscope.legacy_1x.common.api_key, while the printed path and the source check in # auth read its own imported copy. Both bindings have to move, or login and # whoami silently disagree about where the key lives. monkeypatch.setattr( diff --git a/tests/unit/test_cli_code_generation.py b/tests/unit/test_cli_code_generation.py index c421e5ee..8d2435e1 100644 --- a/tests/unit/test_cli_code_generation.py +++ b/tests/unit/test_cli_code_generation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import code_generation +from dashscope.legacy_1x.cli import code_generation class TestCliCodeGeneration: diff --git a/tests/unit/test_cli_common.py b/tests/unit/test_cli_common.py index f28f7375..ca149057 100644 --- a/tests/unit/test_cli_common.py +++ b/tests/unit/test_cli_common.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -"""Unit tests for CLI exit-code classification in dashscope.cli.common.""" +"""Unit tests for CLI exit-code classification in dashscope.legacy_1x.cli.common.""" from types import SimpleNamespace from typer.testing import CliRunner -from dashscope.cli.common import _exit_code_for -from dashscope.cli import embeddings +from dashscope.legacy_1x.cli.common import _exit_code_for +from dashscope.legacy_1x.cli import embeddings # --------------------------------------------------------------------------- diff --git a/tests/unit/test_cli_embeddings.py b/tests/unit/test_cli_embeddings.py index 9fd69d39..88bbf085 100644 --- a/tests/unit/test_cli_embeddings.py +++ b/tests/unit/test_cli_embeddings.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import embeddings +from dashscope.legacy_1x.cli import embeddings class TestCliEmbeddings: diff --git a/tests/unit/test_cli_image_generation.py b/tests/unit/test_cli_image_generation.py index 6a19f296..66788fc4 100644 --- a/tests/unit/test_cli_image_generation.py +++ b/tests/unit/test_cli_image_generation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import image_generation +from dashscope.legacy_1x.cli import image_generation class TestCliImageGeneration: diff --git a/tests/unit/test_cli_image_synthesis.py b/tests/unit/test_cli_image_synthesis.py index 142d6cac..b03835f4 100644 --- a/tests/unit/test_cli_image_synthesis.py +++ b/tests/unit/test_cli_image_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import image_synthesis +from dashscope.legacy_1x.cli import image_synthesis class TestCliImageSynthesis: diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index aac880ae..f013171b 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -13,12 +13,12 @@ from typer.testing import CliRunner import dashscope -from dashscope.cli import agentic_rl -from dashscope.cli import ( +from dashscope.legacy_1x.cli import agentic_rl +from dashscope.legacy_1x.cli import ( app as cli_app, main as cli_main, ) -from dashscope.common.error import AuthenticationError +from dashscope.legacy_1x.common.error import AuthenticationError def strip_ansi_codes(text): @@ -53,7 +53,7 @@ def mock_list(**kwargs): raise AuthenticationError("No api key provided.") monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(sys, "argv", ["dashscope", "models", "list"]) @@ -77,7 +77,7 @@ def test_top_level_help_shows_global_api_key(self): def test_python_module_help_suppresses_urllib3_warning(self): result = subprocess.run( - [sys.executable, "-m", "dashscope.cli", "--help"], + [sys.executable, "-m", "dashscope.legacy_1x.cli", "--help"], check=False, capture_output=True, text=True, @@ -204,7 +204,7 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) @@ -236,7 +236,7 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) @@ -267,22 +267,25 @@ def test_rl_import_failure_is_silent_until_the_command_is_used( monkeypatch, capsys, ): - # The rl import is deferred, so importing dashscope.cli must not tell + # The rl import is deferred, so importing dashscope.legacy_1x.cli must not tell # the user about a dependency they may never ask for. - monkeypatch.setattr(dashscope.cli, "_RL_IMPORT_ERROR", None) + monkeypatch.setattr(dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", None) monkeypatch.setitem( sys.modules, - "dashscope.cli.agentic_rl", + "dashscope.legacy_1x.cli.agentic_rl", _ImportErrorOnApp( "Agentic RL fine-tuning dependencies are missing from this " "install. Reinstall with: pip install -U dashscope", ), ) - dashscope.cli._register_rl_app() + dashscope.legacy_1x.cli._register_rl_app() assert capsys.readouterr().err == "" - assert isinstance(dashscope.cli._RL_IMPORT_ERROR, ImportError) + assert isinstance( + dashscope.legacy_1x.cli._RL_IMPORT_ERROR, + ImportError, + ) def test_rl_command_prints_the_install_command_verbatim( self, @@ -292,7 +295,7 @@ def test_rl_command_prints_the_install_command_verbatim( # The error already carries its own remedy, so the fallback hint in # _rl_unavailable_detail must not be appended a second time. monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError( "Agentic RL fine-tuning dependencies are missing from this " @@ -315,7 +318,7 @@ def test_rl_command_adds_a_hint_when_the_error_has_none( capsys, ): monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError("No module named 'typer_x'"), ) @@ -334,13 +337,13 @@ def test_unrelated_command_is_not_told_about_missing_rl_dependencies( capsys, ): monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_RL_IMPORT_ERROR", ImportError("Reinstall with: pip install -U dashscope"), ) monkeypatch.setattr(sys, "argv", ["dashscope"]) monkeypatch.setattr( - dashscope.cli, + dashscope.legacy_1x.cli, "_route_to_expert", lambda *args, **kwargs: None, ) @@ -363,10 +366,13 @@ def mock_upload(model, file_path, api_key, base_address=None): return "oss://uploaded", None monkeypatch.setattr( - "dashscope.cli.oss.os.path.exists", + "dashscope.legacy_1x.cli.oss.os.path.exists", lambda path: True, ) - monkeypatch.setattr("dashscope.cli.oss.OssUtils.upload", mock_upload) + monkeypatch.setattr( + "dashscope.legacy_1x.cli.oss.OssUtils.upload", + mock_upload, + ) monkeypatch.setattr(dashscope, "api_key", "global-key") monkeypatch.setattr( sys, @@ -471,7 +477,7 @@ def mock_call(model, prompt, stream=False): ) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) monkeypatch.setattr( @@ -530,15 +536,15 @@ def mock_deployments_list(**kwargs): return result monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", + "dashscope.legacy_1x.cli.files.dashscope.Files.list", mock_files_list, ) monkeypatch.setattr( - "dashscope.cli.fine_tunes.dashscope.FineTunes.list", + "dashscope.legacy_1x.cli.fine_tunes.dashscope.FineTunes.list", mock_fine_tunes_list, ) monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.list", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.list", mock_deployments_list, ) @@ -571,7 +577,7 @@ def mock_files_list(**kwargs): return SimpleNamespace(status_code=200, output={}) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", + "dashscope.legacy_1x.cli.files.dashscope.Files.list", mock_files_list, ) monkeypatch.setattr( @@ -592,7 +598,7 @@ def test_files_upload_missing_file_exits_without_traceback( capsys, ): monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", + "dashscope.legacy_1x.cli.files.os.path.exists", lambda path: False, ) monkeypatch.setattr( @@ -620,11 +626,11 @@ def mock_upload(**kwargs): raise RuntimeError("upload failed") monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", + "dashscope.legacy_1x.cli.files.os.path.exists", lambda path: True, ) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.upload", + "dashscope.legacy_1x.cli.files.dashscope.Files.upload", mock_upload, ) monkeypatch.setattr( @@ -699,7 +705,7 @@ def mock_call(*_args, **_kwargs): return FailingStream() monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -728,7 +734,7 @@ def mock_list(**kwargs): raise RuntimeError("models failed") monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", + "dashscope.legacy_1x.cli.models.dashscope.Models.list", mock_list, ) @@ -746,7 +752,7 @@ def mock_call(**kwargs): raise RuntimeError("deployment failed") monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.call", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.call", mock_call, ) @@ -773,11 +779,11 @@ def mock_get(*_args, **_kwargs): raise RuntimeError("network down") monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.call", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.call", mock_call, ) monkeypatch.setattr( - "dashscope.cli.deployments.dashscope.Deployments.get", + "dashscope.legacy_1x.cli.deployments.dashscope.Deployments.get", mock_get, ) @@ -799,7 +805,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.application.dashscope.Application.call", + "dashscope.legacy_1x.cli.application.dashscope.Application.call", mock_call, ) @@ -817,7 +823,7 @@ def mock_call(*_args, **_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -840,7 +846,7 @@ def mock_call(*_args, **_kwargs): ) monkeypatch.setattr( - "dashscope.cli.generation.Generation.call", + "dashscope.legacy_1x.cli.generation.Generation.call", mock_call, ) @@ -869,7 +875,7 @@ def mock_call(**kwargs): raise RuntimeError("image failed") monkeypatch.setattr( - "dashscope.cli.image_generation.ImageGeneration.call", + "dashscope.legacy_1x.cli.image_generation.ImageGeneration.call", mock_call, ) @@ -897,7 +903,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.multimodal_conversation.dashscope." + "dashscope.legacy_1x.cli.multimodal_conversation.dashscope." "MultiModalConversation.call", mock_call, ) @@ -926,7 +932,7 @@ def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"embeddings": []}) monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope." + "dashscope.legacy_1x.cli.multimodal_embedding.dashscope." "MultiModalEmbedding.call", mock_call, ) @@ -955,7 +961,7 @@ def mock_call(**_kwargs): raise RuntimeError("embedding failed") monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope." + "dashscope.legacy_1x.cli.multimodal_embedding.dashscope." "MultiModalEmbedding.call", mock_call, ) diff --git a/tests/unit/test_cli_models.py b/tests/unit/test_cli_models.py index 60917fc0..7f6b0b37 100644 --- a/tests/unit/test_cli_models.py +++ b/tests/unit/test_cli_models.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import models +from dashscope.legacy_1x.cli import models class TestCliModels: diff --git a/tests/unit/test_cli_multimodal_conversation.py b/tests/unit/test_cli_multimodal_conversation.py index 245efbc6..ebbf7b64 100644 --- a/tests/unit/test_cli_multimodal_conversation.py +++ b/tests/unit/test_cli_multimodal_conversation.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import multimodal_conversation +from dashscope.legacy_1x.cli import multimodal_conversation class TestCliMultiModalConversation: diff --git a/tests/unit/test_cli_multimodal_embedding.py b/tests/unit/test_cli_multimodal_embedding.py index 0058b0e7..d52621ed 100644 --- a/tests/unit/test_cli_multimodal_embedding.py +++ b/tests/unit/test_cli_multimodal_embedding.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import multimodal_embedding +from dashscope.legacy_1x.cli import multimodal_embedding class TestCliMultiModalEmbedding: diff --git a/tests/unit/test_cli_rerank.py b/tests/unit/test_cli_rerank.py index 6380b39f..ff09ac6a 100644 --- a/tests/unit/test_cli_rerank.py +++ b/tests/unit/test_cli_rerank.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import rerank +from dashscope.legacy_1x.cli import rerank class TestCliRerank: diff --git a/tests/unit/test_cli_speech_synthesis.py b/tests/unit/test_cli_speech_synthesis.py index efb7fd1e..5a561706 100644 --- a/tests/unit/test_cli_speech_synthesis.py +++ b/tests/unit/test_cli_speech_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import speech_synthesis +from dashscope.legacy_1x.cli import speech_synthesis class TestCliSpeechSynthesis: diff --git a/tests/unit/test_cli_tokenization.py b/tests/unit/test_cli_tokenization.py index 9da078b2..69b6ec4b 100644 --- a/tests/unit/test_cli_tokenization.py +++ b/tests/unit/test_cli_tokenization.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import tokenization +from dashscope.legacy_1x.cli import tokenization class TestCliTokenization: diff --git a/tests/unit/test_cli_transcription.py b/tests/unit/test_cli_transcription.py index 20cfc425..28ce93fa 100644 --- a/tests/unit/test_cli_transcription.py +++ b/tests/unit/test_cli_transcription.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import transcription +from dashscope.legacy_1x.cli import transcription class TestCliTranscription: diff --git a/tests/unit/test_cli_understanding.py b/tests/unit/test_cli_understanding.py index c15ea145..b0ca73d8 100644 --- a/tests/unit/test_cli_understanding.py +++ b/tests/unit/test_cli_understanding.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import understanding +from dashscope.legacy_1x.cli import understanding class TestCliUnderstanding: diff --git a/tests/unit/test_cli_video_synthesis.py b/tests/unit/test_cli_video_synthesis.py index c1af60ce..16930ec2 100644 --- a/tests/unit/test_cli_video_synthesis.py +++ b/tests/unit/test_cli_video_synthesis.py @@ -5,7 +5,7 @@ from typer.testing import CliRunner -from dashscope.cli import video_synthesis +from dashscope.legacy_1x.cli import video_synthesis class TestCliVideoSynthesis: diff --git a/tests/unit/test_code_generation.py b/tests/unit/test_code_generation.py index 831ce9c4..f378ad81 100644 --- a/tests/unit/test_code_generation.py +++ b/tests/unit/test_code_generation.py @@ -5,7 +5,7 @@ from http import HTTPStatus from dashscope import CodeGeneration -from dashscope.aigc.code_generation import ( +from dashscope.legacy_1x.aigc.code_generation import ( AttachmentRoleMessageParam, UserRoleMessageParam, ) diff --git a/tests/unit/test_conversation.py b/tests/unit/test_conversation.py index 8dedcc04..c1dc3265 100644 --- a/tests/unit/test_conversation.py +++ b/tests/unit/test_conversation.py @@ -5,9 +5,17 @@ from http import HTTPStatus from dashscope import Generation -from dashscope.aigc.conversation import Conversation, History, HistoryItem -from dashscope.api_entities.dashscope_response import Choice, Message, Role -from dashscope.common.message_manager import MessageManager +from dashscope.legacy_1x.aigc.conversation import ( + Conversation, + History, + HistoryItem, +) +from dashscope.legacy_1x.api_entities.dashscope_response import ( + Choice, + Message, + Role, +) +from dashscope.legacy_1x.common.message_manager import MessageManager from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_dashscope_response.py b/tests/unit/test_dashscope_response.py index ed7a135c..599d6c5e 100644 --- a/tests/unit/test_dashscope_response.py +++ b/tests/unit/test_dashscope_response.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DictMixin +from dashscope.legacy_1x.api_entities.dashscope_response import DictMixin class TestDictMixin: diff --git a/tests/unit/test_http_speech_synthesizer.py b/tests/unit/test_http_speech_synthesizer.py index d59410df..bd72d156 100644 --- a/tests/unit/test_http_speech_synthesizer.py +++ b/tests/unit/test_http_speech_synthesizer.py @@ -5,8 +5,10 @@ import base64 from http import HTTPStatus -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.audio.http_tts.http_speech_synthesizer import ( +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, ) diff --git a/tests/unit/test_import_deprecation.py b/tests/unit/test_import_deprecation.py index 6ecce74d..86fe722c 100644 --- a/tests/unit/test_import_deprecation.py +++ b/tests/unit/test_import_deprecation.py @@ -78,19 +78,22 @@ def test_accessing_threads_emits_deprecation_warning(): def test_direct_assistants_import_emits_deprecation_warning(): - result = _run("import dashscope.assistants", error_on_deprecation=True) + result = _run( + "import dashscope.legacy_1x.assistants", + error_on_deprecation=True, + ) assert result.returncode != 0 assert "DeprecationWarning" in result.stderr def test_lazy_names_resolve_to_real_objects(): import dashscope - from dashscope.assistants import Assistant, Assistants - from dashscope.assistants.assistant_types import ( + from dashscope.legacy_1x.assistants import Assistant, Assistants + from dashscope.legacy_1x.assistants.assistant_types import ( AssistantFile, DeleteResponse, ) - from dashscope.threads import RunStepList, Threads + from dashscope.legacy_1x.threads import RunStepList, Threads assert dashscope.Assistants is Assistants assert dashscope.Assistant is Assistant diff --git a/tests/unit/test_layer_decoupling.py b/tests/unit/test_layer_decoupling.py new file mode 100644 index 00000000..a7ef2350 --- /dev/null +++ b/tests/unit/test_layer_decoupling.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""The layer boundaries the 1.x / 2.0 decoupling rests on. + +``dashscope/_shared`` is the neutral layer both stacks import from, and +the 2.0 package must not reach into the 1.x *business* surface at all. +Neither rule is enforced by the interpreter: both are import-graph +properties that a well-meaning edit undoes silently, and both only fail +at the moment the 1.x tree moves to ``legacy_1x/`` and 2.0 is promoted to +the top level. So they are surveyed here as AST rather than left as +prose. + +``test_v2_does_not_import_the_1x_business_surface`` is where the design +doc's "hits must decrease monotonically" principle lands: zero. That +zero is the precondition for the physical move, not a consequence of it. +""" + +from __future__ import annotations + +import ast +import importlib +import pathlib + +_REPO = pathlib.Path(__file__).resolve().parents[2] +_DASHSCOPE = _REPO / "dashscope" +_SHARED = _DASHSCOPE / "_shared" +_V2 = _DASHSCOPE / "v2" + +_VERSION_FILE = "dashscope/version.py" + +# Second segment of a ``dashscope.*`` module that is not 1.x surface. +# ``acli`` serves both stacks and stays at the top level; ``_shared`` is +# the neutral layer itself. +_NEUTRAL_PACKAGES = frozenset({"v2", "_shared", "acli"}) + +# ``dashscope.version`` is distribution metadata, not 1.x API surface: it +# is a bare constant module with no imports of its own, so it does not +# move to ``legacy_1x/`` and importing it cannot close a cycle. It is read by +# the neutral layer and by the three v2 identity builders. +_NEUTRAL_LEAVES = frozenset({"dashscope.version"}) + +# Reward-server payloads that live under v2 but are deliberately not +# packaged. They import the old deep paths on purpose: once the layout +# move makes those paths canonical again the shims disappear and these +# files are correct without being edited. +_SKIP_PARTS = frozenset({"examples"}) + +# ``__version__`` bindings that are not the distribution version. +_OTHER_VERSION_OWNERS = frozenset( + { + # acli versions itself and is a one-way sync copy of the + # agenticCLI repo, so it must stay independent. + "dashscope/acli/__init__.py", + # The version of the vendored error-registry data. + "dashscope/v2/_error_registry.py", + }, +) + +_DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" + + +def _parse(path: pathlib.Path) -> ast.AST: + """Parse one module. A syntax error fails rather than skips.""" + source = path.read_text(encoding="utf-8") + try: + return ast.parse(source, str(path)) + except SyntaxError as exc: + raise AssertionError(f"{path} does not parse: {exc}") from exc + + +def _modules(root: pathlib.Path): + """Every module under ``root``, minus the excluded subtrees.""" + return [ + path + for path in sorted(root.rglob("*.py")) + if not _SKIP_PARTS.intersection(path.parts) + ] + + +def _dashscope_imports(tree: ast.AST): + """[(module, lineno)] for absolute ``dashscope.*`` imports.""" + found = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found.extend((alias.name, node.lineno) for alias in node.names) + elif isinstance(node, ast.ImportFrom): + if node.level == 0 and node.module: + found.append((node.module, node.lineno)) + return [ + (module, lineno) + for module, lineno in found + if module.split(".")[0] == "dashscope" + ] + + +def _is_neutral(module: str) -> bool: + if module in _NEUTRAL_LEAVES: + return True + parts = module.split(".") + return len(parts) > 1 and parts[1] in _NEUTRAL_PACKAGES + + +def _module_level_version_owners(tree: ast.AST): + """Line numbers of module-scope ``__version__ =`` assignments. + + Function-scope bindings are deliberately excluded. Two of the four + version readers -- ``v2/transport/http.default_user_agent`` and + ``v2/agentstudio/client._user_agent`` -- degrade with a local + ``except Exception: __version__ = "0.0.0"``. That rebinds a name + inside one function; it does not define the distribution version. + """ + found = [] + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if any( + isinstance(t, ast.Name) and t.id == "__version__" + for t in node.targets + ): + found.append(node.lineno) + return found + + +def _rel(path: pathlib.Path) -> str: + return path.relative_to(_REPO).as_posix() + + +def test_v2_does_not_import_the_1x_business_surface(): + """2.0 depends on the neutral layer and on nothing else in 1.x. + + This is the decreasing gate the decoupling is measured by, and the + precondition for moving 1.x to ``legacy_1x/``: every hit here is a deep + path that stops resolving the moment the tree it points at moves. + """ + offenders = [] + for path in _modules(_V2): + for module, lineno in _dashscope_imports(_parse(path)): + if _is_neutral(module): + continue + offenders.append(f"{_rel(path)}:{lineno} -> {module}") + assert not offenders, ( + "2.0 must not import the 1.x business surface; these would break " + "the moment 1.x moves to legacy_1x/:\n " + "\n ".join(offenders) + ) + + +def test_shared_layer_imports_no_1x_business_surface(): + """``_shared`` stays below both stacks, or it cannot anchor them. + + ``common/utils.py`` reaches ``_shared._sdk_ident`` while + ``dashscope/__init__`` is still executing, so a ``_shared`` module + that imports the 1.x surface closes a cycle through a partially + initialized root. ``dashscope.version`` is the one allowed exception + and is safe only because it imports nothing -- see + ``test_version_file_has_no_imports``. + """ + offenders = [] + for path in _modules(_SHARED): + for module, lineno in _dashscope_imports(_parse(path)): + if _is_neutral(module): + continue + offenders.append(f"{_rel(path)}:{lineno} -> {module}") + assert not offenders, ( + "dashscope/_shared may import only the stdlib, itself, and " + "dashscope.version:\n " + "\n ".join(offenders) + ) + + +def test_version_file_has_no_imports(): + """``dashscope/version.py`` must stay readable without importing. + + ``setup.py`` and ``docs/source/conf.py`` ``exec()`` this file to read + the version rather than importing it, which is what lets packaging + work in an isolated build environment where the runtime dependencies + are not installed. Any import added here -- a re-export of the value + from elsewhere, for instance -- turns reading one string into loading + the whole SDK and its dependencies. + """ + imports = [ + node.lineno + for node in ast.walk(_parse(_DASHSCOPE / "version.py")) + if isinstance(node, (ast.Import, ast.ImportFrom)) + ] + assert not imports, ( + f"{_VERSION_FILE} is exec'd standalone by setup.py and " + f"docs/source/conf.py, so it must not import anything; found " + f"imports at lines {imports}" + ) + + +def test_distribution_version_is_assigned_in_one_place(): + """One distribution version, defined once. + + A second literal would let the wheel's version and the version + reported in request headers drift apart, since ``setup.py`` reads the + file directly while the SDK reads the imported value. Only + module-scope bindings count; ``_OTHER_VERSION_OWNERS`` lists the two + that mean something else. + """ + owners = [] + for path in _modules(_DASHSCOPE): + rel = _rel(path) + if rel in _OTHER_VERSION_OWNERS: + continue + for lineno in _module_level_version_owners(_parse(path)): + owners.append((rel, lineno)) + located = [f"{rel}:{lineno}" for rel, lineno in owners] + assert [rel for rel, _ in owners] == [_VERSION_FILE], ( + "the distribution version must be assigned only in " + f"{_VERSION_FILE}; found: {located}" + ) + + +def test_one_version_reaches_every_identity_string(monkeypatch): + """Every request identity carries the same version segment. + + Each of these four sites reads the version through its own + defensive fallback to ``"0.0.0"``, so a read that failed would + degrade one telemetry string instead of failing loudly -- and the + header and the user-agent would disagree within one process. This + pins that they agree, including the two user-agents whose version + segment no other test looks at. + """ + monkeypatch.delenv(_DISABLE_ENV, raising=False) + version = importlib.import_module("dashscope.version").__version__ + + root = importlib.import_module("dashscope") + assert root.__version__ == version + + ident = importlib.import_module("dashscope._shared._sdk_ident") + headers = ident.get_sdk_headers() + assert headers["x-dashscope-sdk-client"].split("/")[1] == version + assert ident.get_user_agent().startswith(f"dashscope/{version};") + + v2_ident = importlib.import_module("dashscope.v2.transport._ident") + assert v2_ident.SDK_CLIENT_VALUE.split("/")[1] == version + + http = importlib.import_module("dashscope.v2.transport.http") + assert http.default_user_agent() == f"dashscope-v2-python/{version}" + + studio = importlib.import_module("dashscope.v2.agentstudio.client") + agent = studio._user_agent("https://agentstudio.invalid") + assert agent == ( + f"dashscope-agentstudio-python/{version} " + "(+https://agentstudio.invalid)" + ) diff --git a/tests/unit/test_maas_region.py b/tests/unit/test_maas_region.py index 0e1d340b..081ebc65 100644 --- a/tests/unit/test_maas_region.py +++ b/tests/unit/test_maas_region.py @@ -4,8 +4,8 @@ import unittest from unittest.mock import patch -from dashscope.common.env import MAAS_REGIONS, resolve_base_url -from dashscope.common.error import InputRequired +from dashscope.legacy_1x.common.env import MAAS_REGIONS, resolve_base_url +from dashscope.legacy_1x.common.error import InputRequired class TestResolveBaseUrl(unittest.TestCase): @@ -24,7 +24,7 @@ def test_resolve_with_workspace(self): ) def test_resolve_without_workspace_raises(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", None): @@ -32,7 +32,7 @@ def test_resolve_without_workspace_raises(self): resolve_base_url(url, None) def test_resolve_with_env_default_workspace(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", "ws-env"): @@ -58,7 +58,7 @@ def test_resolve_rejects_invalid_workspace(self): resolve_base_url(url, bad) def test_resolve_rejects_invalid_env_default_workspace(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod url = "https://{workspace_id}.us-east-1.maas.aliyuncs.com/api/v1" with patch.object(env_mod, "_default_workspace_id", "evil.com/x"): @@ -70,13 +70,13 @@ class TestValidateWorkspaceId(unittest.TestCase): """Test validate_workspace_id character restrictions.""" def test_valid_ids(self): - from dashscope.common.env import validate_workspace_id + from dashscope.legacy_1x.common.env import validate_workspace_id for valid in ["ws-123", "llm-abc_def", "A0", "a" * 64]: self.assertEqual(validate_workspace_id(valid), valid) def test_invalid_ids(self): - from dashscope.common.env import validate_workspace_id + from dashscope.legacy_1x.common.env import validate_workspace_id for invalid in [ "", @@ -101,7 +101,7 @@ class TestValidateRegion(unittest.TestCase): """Test validate_region character restrictions.""" def test_valid_regions(self): - from dashscope.common.env import validate_region + from dashscope.legacy_1x.common.env import validate_region # Upper case is accepted: DNS is case-insensitive and httpx # lowercases the host, so rejecting it broke working callers. @@ -109,7 +109,7 @@ def test_valid_regions(self): self.assertEqual(validate_region(valid), valid) def test_invalid_regions(self): - from dashscope.common.env import validate_region + from dashscope.legacy_1x.common.env import validate_region for invalid in [ "", @@ -150,7 +150,7 @@ class TestMaasEnvLoading(unittest.TestCase): def _reload_env(self, env_vars): """Reload env module with given env vars.""" import importlib - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod with patch.dict(os.environ, env_vars, clear=False): importlib.reload(env_mod) @@ -159,7 +159,7 @@ def _reload_env(self, env_vars): def _restore_env(self): """Restore env module to default state.""" import importlib - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod with patch.dict( os.environ, @@ -271,7 +271,7 @@ def tearDown(self): dashscope.base_http_api_url = self._orig_http_url def _capture_request_url(self, func): - from dashscope.client import base_api as base_api_mod + from dashscope.legacy_1x.client import base_api as base_api_mod class ShortCircuit(Exception): pass @@ -326,7 +326,7 @@ def test_async_task_call_resolves_workspace(self): ) def test_call_without_workspace_raises_clear_error(self): - import dashscope.common.env as env_mod + import dashscope.legacy_1x.common.env as env_mod from dashscope import Generation with patch.object(env_mod, "_default_workspace_id", None): diff --git a/tests/unit/test_messages.py b/tests/unit/test_messages.py index 633ccb86..8cbf0213 100644 --- a/tests/unit/test_messages.py +++ b/tests/unit/test_messages.py @@ -5,7 +5,7 @@ import uuid from dashscope import Messages # pylint: disable=no-name-in-module -from dashscope.threads.messages.files import Files +from dashscope.legacy_1x.threads.messages.files import Files from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_multimodal_dialog.py b/tests/unit/test_multimodal_dialog.py index ceaedb59..710c08ac 100644 --- a/tests/unit/test_multimodal_dialog.py +++ b/tests/unit/test_multimodal_dialog.py @@ -5,12 +5,12 @@ import time import pytest -from dashscope.multimodal.dialog_state import DialogState -from dashscope.multimodal.multimodal_dialog import ( +from dashscope.legacy_1x.multimodal.dialog_state import DialogState +from dashscope.legacy_1x.multimodal.multimodal_dialog import ( MultiModalDialog, MultiModalCallback, ) -from dashscope.multimodal.multimodal_request_params import ( +from dashscope.legacy_1x.multimodal.multimodal_request_params import ( Upstream, Downstream, ClientInfo, @@ -29,7 +29,7 @@ conver_instance = None -# ๅฎšไน‰voice chatๆœๅŠกๅ›ž่ฐƒ +# Define the voice chat service callback class TestCallback(MultiModalCallback): def on_connected(self): logger.debug("connected with server.") @@ -43,13 +43,13 @@ def on_stopped(self): def on_state_changed(self, state: DialogState): if state == DialogState.LISTENING: - # app.update_text("ๅผ€ๅง‹ๆ”ถ้Ÿณใ€‚ใ€‚ใ€‚่ฏทๆ้—ฎ") + # app.update_text("Listening... please ask") return if state == DialogState.THINKING: - # app.update_text("ๆ€่€ƒไธญใ€‚ใ€‚ใ€‚่ฏท่€ๅฟƒ็ญ‰ๅพ…") + # app.update_text("Thinking... please wait") return if state == DialogState.RESPONDING: - # app.update_text("ๆญฃๅœจๅ›ž็ญ”ใ€‚ใ€‚ใ€‚") + # app.update_text("Answering...") return def on_speech_audio_data( @@ -64,7 +64,7 @@ def on_error(self, error): sys.exit(0) def on_responding_started(self): - # ๅผ€ๅง‹็ซฏไพงๆ’ญๆ”พ + # Start device-side playback # pcm_play.start_play() global conver_instance # pylint: disable=global-variable-not-assigned if conver_instance is not None: @@ -86,7 +86,7 @@ def on_responding_content(self, payload): logger.debug(payload) def on_request_accepted(self): - # ๆœๅŠก็ซฏๆŽฅๅ—ๆ‰“ๆ–ญๅฝ“ๅ‰ๆ’ญๆŠฅ + # The server accepts interrupting the current playback # pcm_play.cancel_play() return @@ -107,7 +107,7 @@ def setup_class(cls): @pytest.mark.skip def test_multimodal_dialog_one_turn(self): - # ๅฏน่ฏ็Šถๆ€ Listening->Thinking->Responding->Listening + # Dialog states: Listening->Thinking->Responding->Listening up_stream = Upstream( type="AudioOnly", mode="push2talk", @@ -136,7 +136,7 @@ def test_multimodal_dialog_one_turn(self): ) self.conversation.start("") - # ้ฆ–่ฝฎ่ฟ›ๅ…ฅListening + # First turn enters Listening while ( DialogState.LISTENING is not self.conversation.get_dialog_state() ): @@ -146,7 +146,7 @@ def test_multimodal_dialog_one_turn(self): "ไปŠๅคฉๅคฉๆฐ”ไธ้”™", parameters=None, ) - # ็ญ‰ๅพ…็ฌฌไบŒ่ฝฎListening + # Wait for the second Listening while ( DialogState.LISTENING is not self.conversation.get_dialog_state() ): diff --git a/tests/unit/test_opd.py b/tests/unit/test_opd.py index 95748bf7..ac600e59 100644 --- a/tests/unit/test_opd.py +++ b/tests/unit/test_opd.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """Minimal OPD contract tests.""" -# pylint: disable=redefined-outer-name # pytest fixture ๆ ‡ๅ‡†็”จๆณ• +# pylint: disable=redefined-outer-name # standard pytest fixture usage from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest -from dashscope.client.base_api import CreateMixin from dashscope.finetune.agentic_rl import AgenticRL from dashscope.finetune.reinforcement import ( DataSourceType, @@ -21,7 +20,9 @@ @pytest.fixture def rl_client(): - with patch("dashscope.finetune.agentic_rl.set_api_key"): + # ``set_api_key`` is looked up in the module that defines __init__, + # which is the 2.0 one; patching the 1.x shim's copy would miss. + with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): return AgenticRL(api_key="test-key") @@ -69,9 +70,8 @@ def test_teacher_model_enables_opd( has_rollout, reward_count, ): - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job( @@ -96,9 +96,8 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): ) rl_client.init(config_path=str(config)) - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) @@ -111,15 +110,14 @@ def test_teacher_model_can_come_from_yaml(rl_client, tmp_path): def test_opd_example_reuses_supported_rl_configuration(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) config_text = config.read_text(encoding="utf-8") rl_client.init(config_path=str(config)) - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job() @@ -160,7 +158,7 @@ def test_opd_example_reuses_supported_rl_configuration(rl_client): async def test_opd_example_uploads_local_datasets(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "opd-job.yaml" ) rl_client.init(config_path=str(config)) @@ -182,7 +180,7 @@ async def test_opd_example_uploads_local_datasets(rl_client): def test_regular_rl_example_keeps_resources_and_rl_hyperparameters(rl_client): config = ( Path(__file__).parents[2] - / "dashscope/finetune/reinforcement/examples/workspace" + / "dashscope/v2/finetune/reinforcement/examples/workspace" / "rl-job.yaml" ) rl_client.init(config_path=str(config)) @@ -212,9 +210,8 @@ def test_pg_opd_requires_teacher_model(rl_client): def test_regular_reinforcement_is_unchanged(rl_client): - with patch.object( - CreateMixin, - "call", + with patch( + "dashscope.v2.finetune._submit.post_submit", return_value=_success_response(), ) as call: rl_client.submit_job(datasets=[_training_dataset()]) diff --git a/tests/unit/test_oss_utils.py b/tests/unit/test_oss_utils.py index 505d384b..2b4eb022 100644 --- a/tests/unit/test_oss_utils.py +++ b/tests/unit/test_oss_utils.py @@ -5,9 +5,9 @@ import pytest -from dashscope.common.error import InvalidInput -from dashscope.utils import oss_utils -from dashscope.utils.oss_utils import OssUtils +from dashscope.legacy_1x.common.error import InvalidInput +from dashscope.legacy_1x.utils import oss_utils +from dashscope.legacy_1x.utils.oss_utils import OssUtils class FakeUploadResponse: diff --git a/tests/unit/test_recognition_explicit_params.py b/tests/unit/test_recognition_explicit_params.py index dcb4016b..7a9c6136 100644 --- a/tests/unit/test_recognition_explicit_params.py +++ b/tests/unit/test_recognition_explicit_params.py @@ -7,7 +7,10 @@ from contextlib import contextmanager from unittest import mock -from dashscope.audio.asr.recognition import Recognition, RecognitionCallback +from dashscope.legacy_1x.audio.asr.recognition import ( + Recognition, + RecognitionCallback, +) class MockRecognitionCallback(RecognitionCallback): diff --git a/tests/unit/test_rerank.py b/tests/unit/test_rerank.py index b7427944..fcccd575 100644 --- a/tests/unit/test_rerank.py +++ b/tests/unit/test_rerank.py @@ -18,14 +18,14 @@ def test_call(self, mock_server: MockServer): { "index": 1, "relevance_score": 0.987654, - "document": { # ๅฆ‚ๆžœreturn_documents=true + "document": { # when return_documents=true "text": "ๅ“ˆๅฐ”ๆปจๆ˜ฏไธญๅ›ฝ้ป‘้พ™ๆฑŸ็œ็š„็œไผš๏ผŒไฝไบŽไธญๅ›ฝไธœๅŒ—", }, }, { "index": 0, "relevance_score": 0.876543, - "document": { # ๅฆ‚ๆžœreturn_documents=true + "document": { # when return_documents=true "text": "้ป‘้พ™ๆฑŸ็ฆปไฟ„็ฝ—ๆ–ฏๅพˆ่ฟ‘", }, }, diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index 7a3042f3..af497df8 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -9,14 +9,14 @@ from dashscope.acli import SDK_SESSION_ID as ACLI_SESSION_ID from dashscope.acli import __version__ as acli_version from dashscope.acli.providers.tongyi import TongyiProvider -from dashscope.common.utils import _SDK_SESSION_ID, get_sdk_headers +from dashscope.legacy_1x.common.utils import _SDK_SESSION_ID, get_sdk_headers CLIENT_HEADER = "x-dashscope-sdk-client" SESSION_HEADER = "x-dashscope-sdk-session-id" DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" # client value sources: -# - "python-sdk": SDK default (_SDK_CLIENT in common/utils.py) +# - "python-sdk": SDK default (_SDK_CLIENT in _shared/_sdk_ident.py) # - "python-cli": dashscope/cli/__init__.py calls # set_sdk_client("python-cli") at CLI process startup # - "acli": acli/providers/tongyi.py builds its own header @@ -35,7 +35,10 @@ @pytest.fixture(autouse=True) def _pin_sdk_client(monkeypatch): # CLI tests switch the client to python-cli in-process; pin the default - monkeypatch.setattr("dashscope.common.utils._SDK_CLIENT", "python-sdk") + monkeypatch.setattr( + "dashscope._shared._sdk_ident._SDK_CLIENT", + "python-sdk", + ) def _check_client_header(value, client, version, module=""): @@ -59,6 +62,22 @@ 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.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 + 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 ===") @@ -90,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: @@ -107,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; " @@ -181,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(): @@ -223,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) @@ -290,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) @@ -500,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 = [ ( @@ -590,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, ) @@ -652,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") diff --git a/tests/unit/test_speech_synthesis_v2.py b/tests/unit/test_speech_synthesis_v2.py index 798db207..52dee3d3 100644 --- a/tests/unit/test_speech_synthesis_v2.py +++ b/tests/unit/test_speech_synthesis_v2.py @@ -6,9 +6,9 @@ import pytest import dashscope -from dashscope.audio.tts_v2 import ResultCallback, SpeechSynthesizer -from dashscope.common.error import RequestFailure -from dashscope.protocol.websocket import ActionType, EventType +from dashscope.legacy_1x.audio.tts_v2 import ResultCallback, SpeechSynthesizer +from dashscope.legacy_1x.common.error import RequestFailure +from dashscope.legacy_1x.protocol.websocket import ActionType, EventType from tests.unit.base_test import BaseTestEnvironment diff --git a/tests/unit/test_sync_custom_session.py b/tests/unit/test_sync_custom_session.py index 2d45475c..c9e60b26 100644 --- a/tests/unit/test_sync_custom_session.py +++ b/tests/unit/test_sync_custom_session.py @@ -24,10 +24,12 @@ import requests from requests.adapters import HTTPAdapter -from dashscope.api_entities import http_request as http_request_module -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.api_request_data import ApiRequestData -from dashscope.common.constants import ApiProtocol, HTTPMethod +from dashscope.legacy_1x.api_entities import ( + http_request as http_request_module, +) +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.api_request_data import ApiRequestData +from dashscope.legacy_1x.common.constants import ApiProtocol, HTTPMethod class TestSyncSessionBasics: @@ -121,7 +123,9 @@ def test_custom_session_is_used_for_request(self, _mock_session_class): # verify the custom session was not closed mock_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_is_used_when_no_custom_session( self, mock_get_session, @@ -214,7 +218,9 @@ def test_custom_session_not_closed_by_http_request(self): # verify the custom session was not closed custom_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_not_closed_on_success(self, mock_get_session): """Shared session is not closed after success""" mock_session = Mock() @@ -254,7 +260,9 @@ def test_shared_session_not_closed_on_success(self, mock_get_session): # verify the shared session was not closed mock_session.close.assert_not_called() - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_shared_session_not_closed_on_exception(self, mock_get_session): """Shared session is not closed on exception""" mock_session = Mock() @@ -474,7 +482,9 @@ def test_works_without_session_parameter(self): assert http_request.url == "http://example.com/api" assert http_request.method == HTTPMethod.POST - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_default_behavior_unchanged(self, mock_get_session): """Default behavior: use the shared session""" mock_session = Mock() @@ -548,7 +558,9 @@ def _ok_response(): mock_response.text = '{"status": "success"}' return mock_response - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_retry_once_on_connection_error(self, mock_get_session): """Retry once and succeed when the peer drops the connection""" mock_session = Mock() @@ -572,7 +584,9 @@ def test_retry_once_on_connection_error(self, mock_get_session): assert mock_session.post.call_count == 2 - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_retry_exhausted_raises(self, mock_get_session): """Raise after two consecutive ConnectionErrors""" mock_session = Mock() @@ -588,7 +602,9 @@ def test_retry_exhausted_raises(self, mock_get_session): assert mock_session.post.call_count == 2 - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_no_retry_on_other_exceptions(self, mock_get_session): """Non-connection exceptions are not retried""" mock_session = Mock() diff --git a/tests/unit/test_tokenizer.py b/tests/unit/test_tokenizer.py index 682c6a15..4742d56f 100644 --- a/tests/unit/test_tokenizer.py +++ b/tests/unit/test_tokenizer.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +import importlib import os -from dashscope.tokenizers.tokenizer import get_tokenizer -from dashscope.tokenizers.qwen_tokenizer import _CHUNK_SIZE +import pytest + +from dashscope.v2.tokenizers.tokenizer import get_tokenizer +from dashscope.v2.tokenizers.qwen_tokenizer import _CHUNK_SIZE class TestTokenization: @@ -78,3 +81,80 @@ def test_encode_chunk_size_exceed(self): ) decoded_with_special = tokenizer.decode(tokens_with_special) assert decoded_with_special == long_text_with_special + + +class TestLegacyTokenizerPaths: + """The published ``dashscope.tokenizers.*`` deep paths must keep working. + + The implementation moved to ``dashscope.v2.tokenizers``; the 1.x paths + are thin re-export shims, so they have to resolve to the very same + objects rather than to a second copy. + """ + + @pytest.mark.parametrize( + "legacy_path,v2_path,name", + [ + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "get_tokenizer", + ), + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "list_tokenizers", + ), + ( + "dashscope.tokenizers.tokenizer", + "dashscope.v2.tokenizers.tokenizer", + "QWEN_SERIALS", + ), + ( + "dashscope.tokenizers.qwen_tokenizer", + "dashscope.v2.tokenizers.qwen_tokenizer", + "QwenTokenizer", + ), + ( + "dashscope.tokenizers.qwen_tokenizer", + "dashscope.v2.tokenizers.qwen_tokenizer", + "SPECIAL_TOKENS_SET", + ), + ( + "dashscope.tokenizers.tokenizer_base", + "dashscope.v2.tokenizers.tokenizer_base", + "Tokenizer", + ), + ], + ) + def test_deep_imports_resolve_to_the_v2_objects( + self, + legacy_path, + v2_path, + name, + ): + legacy = importlib.import_module(legacy_path) + v2 = importlib.import_module(v2_path) + + assert getattr(legacy, name) is getattr(v2, name) + + def test_package_level_reexports_are_untouched(self): + import dashscope.tokenizers as legacy_pkg + import dashscope.v2.tokenizers as v2_pkg + + for name in v2_pkg.__all__: + assert getattr(legacy_pkg, name) is getattr(v2_pkg, name) + + def test_legacy_path_returns_a_working_tokenizer(self): + from dashscope.tokenizers.tokenizer import get_tokenizer as legacy + + tokenizer = legacy("qwen-turbo") + tokens = tokenizer.encode("hello world") + + assert isinstance(tokens, list) + assert tokenizer.decode(tokens) == "hello world" + + def test_from_import_syntax_resolves(self): + from dashscope.tokenizers.qwen_tokenizer import QwenTokenizer + from dashscope.tokenizers.tokenizer_base import Tokenizer + + assert issubclass(QwenTokenizer, Tokenizer) diff --git a/tests/unit/test_translation_recognizer.py b/tests/unit/test_translation_recognizer.py index 5f9782fe..84fc7bb9 100644 --- a/tests/unit/test_translation_recognizer.py +++ b/tests/unit/test_translation_recognizer.py @@ -6,8 +6,8 @@ import pytest -from dashscope.audio.asr import TranslationRecognizerCallback -from dashscope.audio.asr.translation_recognizer import ( +from dashscope.legacy_1x.audio.asr import TranslationRecognizerCallback +from dashscope.legacy_1x.audio.asr.translation_recognizer import ( TranscriptionResult, TranslationRecognizerRealtime, TranslationResult, diff --git a/tests/unit/test_translation_recognizer_explicit_params.py b/tests/unit/test_translation_recognizer_explicit_params.py index 8be53bbe..63f50a5c 100644 --- a/tests/unit/test_translation_recognizer_explicit_params.py +++ b/tests/unit/test_translation_recognizer_explicit_params.py @@ -3,7 +3,7 @@ """Test explicit parameters for TranslationRecognizer classes.""" # pylint: disable=protected-access -from dashscope.audio.asr.translation_recognizer import ( +from dashscope.legacy_1x.audio.asr.translation_recognizer import ( TranslationRecognizerRealtime, TranslationRecognizerChat, TranslationRecognizerCallback, diff --git a/tests/unit/test_utf8_encoding.py b/tests/unit/test_utf8_encoding.py index 32dcaf2d..c91f5cf2 100644 --- a/tests/unit/test_utf8_encoding.py +++ b/tests/unit/test_utf8_encoding.py @@ -15,9 +15,9 @@ import pytest -from dashscope.api_entities.http_request import HttpRequest -from dashscope.api_entities.aiohttp_request import AioHttpRequest -from dashscope.common.constants import HTTPMethod +from dashscope.legacy_1x.api_entities.http_request import HttpRequest +from dashscope.legacy_1x.api_entities.aiohttp_request import AioHttpRequest +from dashscope.legacy_1x.common.constants import HTTPMethod NON_ASCII_PAYLOAD = { @@ -162,7 +162,9 @@ def test_accept_has_charset(self): class TestHttpRequestUtf8Body: # pylint: disable=protected-access - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_body_is_utf8_bytes(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -186,7 +188,9 @@ def test_body_is_utf8_bytes(self, mock_session_cls): assert isinstance(sent_body, bytes), "Body must be bytes, not str" assert sent_body == EXPECTED_BODY - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_no_unicode_escapes_in_body(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -212,7 +216,9 @@ def test_no_unicode_escapes_in_body(self, mock_session_cls): assert "ะžั†ะตะฝะธ ะธะดะตัŽ" in body_str assert "ใ“ใ‚“ใซใกใฏ" in body_str - @patch("dashscope.api_entities.http_request._get_shared_sync_session") + @patch( + "dashscope.legacy_1x.api_entities.http_request._get_shared_sync_session", + ) def test_body_is_smaller_than_ascii_escaped(self, mock_session_cls): mock_session = _make_mock_sync_session() mock_session_cls.return_value = mock_session @@ -311,7 +317,9 @@ class TestWebSocketRequestUtf8: # pylint: disable=protected-access def test_build_up_message_no_unicode_escapes(self): - from dashscope.api_entities.websocket_request import WebSocketRequest + from dashscope.legacy_1x.api_entities.websocket_request import ( + WebSocketRequest, + ) req = WebSocketRequest( url="wss://example.com/api-ws/v1/test", @@ -330,7 +338,9 @@ def test_build_up_message_no_unicode_escapes(self): assert "ไฝ ๅฅฝไธ–็•Œ" in message def test_build_up_message_smaller_than_ascii_escaped(self): - from dashscope.api_entities.websocket_request import WebSocketRequest + from dashscope.legacy_1x.api_entities.websocket_request import ( + WebSocketRequest, + ) req = WebSocketRequest( url="wss://example.com/api-ws/v1/test", diff --git a/tests/unit/websocket_mock_server_task_handler.py b/tests/unit/websocket_mock_server_task_handler.py index 80743d18..f54f07ff 100644 --- a/tests/unit/websocket_mock_server_task_handler.py +++ b/tests/unit/websocket_mock_server_task_handler.py @@ -6,8 +6,8 @@ import aiohttp -from dashscope.common.error import UnexpectedMessageReceived -from dashscope.protocol.websocket import ( +from dashscope.legacy_1x.common.error import UnexpectedMessageReceived +from dashscope.legacy_1x.protocol.websocket import ( ACTION_KEY, EVENT_KEY, ActionType, diff --git a/tests/unit/websocket_task_request.py b/tests/unit/websocket_task_request.py index 00d1b1e2..78221c29 100644 --- a/tests/unit/websocket_task_request.py +++ b/tests/unit/websocket_task_request.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import BaseAioApi, BaseApi -from dashscope.common.constants import ApiProtocol -from dashscope.protocol.websocket import WebsocketStreamingMode +from dashscope.legacy_1x.api_entities.dashscope_response import ( + DashScopeAPIResponse, +) +from dashscope.legacy_1x.client.base_api import BaseAioApi, BaseApi +from dashscope.legacy_1x.common.constants import ApiProtocol +from dashscope.legacy_1x.protocol.websocket import WebsocketStreamingMode class WebSocketRequest(BaseApi, BaseAioApi): diff --git a/tests/v2/test_agentstudio_shim.py b/tests/v2/test_agentstudio_shim.py new file mode 100644 index 00000000..9e3e442c --- /dev/null +++ b/tests/v2/test_agentstudio_shim.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the AgentStudio relocation into the 2.0 package (D7). + +The sub-SDK moved to ``dashscope.v2.agentstudio``; ``dashscope.agentstudio`` +is now a shim. Three contracts are pinned here: + +* the old import path -- including deep submodule paths -- resolves to the + very same module and class objects, so nothing downstream sees a fork; +* the pieces AgentStudio used to borrow from the 1.x ``common`` package now + come from the neutral ``dashscope._shared`` layer, with identity intact + (the exception base class and the mutable sdk-client override are both + load-bearing singletons); +* AgentStudio keeps emitting the *unmarked* ``agentstudio`` module tag, and + resolves its api key without reading the 1.x ``dashscope.api_key`` global. +""" + +from __future__ import annotations + +import sys + +import pytest + +import dashscope.agentstudio as shim +import dashscope.v2.agentstudio as impl +from dashscope._shared import _sdk_ident, _validation +from dashscope.agentstudio import exceptions as shim_exc +from dashscope.agentstudio.transport import build_headers +from dashscope.legacy_1x.common import error as legacy_error +from dashscope.legacy_1x.common import env as legacy_env +from dashscope.legacy_1x.common import utils as legacy_utils +from dashscope.v2.agentstudio import client as as_client +from dashscope.v2.agentstudio import exceptions as impl_exc +from dashscope.v2.resources._agentstudio import _is_resource, wrap_agentstudio + +BASE_URL = "https://as.test.local" + +# Deep paths the ecosystem relies on: the unit tests, the examples and the +# acli skill docs all import these directly. +SUBMODULES = [ + "client", + "constants", + "exceptions", + "pagination", + "streaming", + "transport", + "resources", + "resources._helpers", + "resources.agents", + "resources.deployments", + "resources.environments", + "resources.files", + "resources.memory_stores", + "resources.security", + "resources.session_events", + "resources.session_resources", + "resources.session_threads", + "resources.sessions", + "resources.skills", + "resources.vaults", + "resources.webhook_endpoints", + "types", + "types.models", + "types.params", +] + + +def _make_client(**kwargs): + kwargs.setdefault("api_key", "sk-test") + kwargs.setdefault("base_url", BASE_URL) + return impl.Client(**kwargs) + + +@pytest.fixture +def _clean_env(monkeypatch): + """Keep ambient env vars from changing base-url resolution.""" + + for var in ( + "DASHSCOPE_WORKSPACE", + "DASHSCOPE_AGENTSTUDIO_URL", + "AGENTSTUDIO_URL", + ): + monkeypatch.delenv(var, raising=False) + + +# --------------------------------------------------------------------------- +# The shim resolves to one and the same implementation +# --------------------------------------------------------------------------- + + +def test_public_names_are_the_same_objects(): + assert shim.__all__ == impl.__all__ + for name in impl.__all__: + assert getattr(shim, name) is getattr(impl, name), name + + +def test_client_classes_are_the_same_objects(): + assert shim.Client is impl.Client + assert shim.AsyncClient is impl.AsyncClient + assert shim.Client.__module__ == "dashscope.v2.agentstudio.client" + + +def test_exception_classes_are_the_same_objects(): + assert shim_exc.AgentStudioError is impl_exc.AgentStudioError + assert shim_exc.AuthenticationError is impl_exc.AuthenticationError + assert shim_exc.NotFoundError is impl_exc.NotFoundError + + +@pytest.mark.parametrize("name", SUBMODULES) +def test_deep_imports_alias_the_v2_module(name): + __import__(f"dashscope.agentstudio.{name}") + assert ( + sys.modules[f"dashscope.agentstudio.{name}"] + is sys.modules[f"dashscope.v2.agentstudio.{name}"] + ) + + +@pytest.mark.parametrize("name", SUBMODULES) +def test_submodules_are_reachable_as_attributes(name): + """sys.modules alone is not enough -- attribute access must work too.""" + + __import__("dashscope.agentstudio") + obj = shim + for part in name.split("."): + obj = getattr(obj, part) + assert obj is sys.modules[f"dashscope.v2.agentstudio.{name}"] + + +def test_deep_import_from_syntax_resolves(): + from dashscope.agentstudio.resources.agents import Agents + from dashscope.agentstudio.types.models import Agent + from dashscope.v2.agentstudio.resources.agents import Agents as V2Agents + from dashscope.v2.agentstudio.types.models import Agent as V2Agent + + assert Agents is V2Agents + assert Agent is V2Agent + + +# --------------------------------------------------------------------------- +# The neutral layer holds the singletons AgentStudio depends on +# --------------------------------------------------------------------------- + + +def test_exception_base_class_identity_is_preserved(): + """Both stacks must inherit from one and the same class object.""" + + from dashscope._shared._errors import DashScopeException + + assert legacy_error.DashScopeException is DashScopeException + assert issubclass( + impl_exc.AgentStudioError, + legacy_error.DashScopeException, + ) + assert issubclass( + legacy_error.InvalidParameter, + legacy_error.DashScopeException, + ) + + +def test_validators_are_shared_and_still_reject_host_breakouts(): + assert legacy_env.validate_region is _validation.validate_region + assert ( + legacy_env.validate_workspace_id is _validation.validate_workspace_id + ) + # These values would move the effective host off *.maas.aliyuncs.com + # while the transport still sends Authorization: Bearer . + breakout = ("a@evil.com/#", "ws/evil", "ws evil", "", "-lead") + for bad in breakout + ("x" * 65,): + with pytest.raises(ValueError): + _validation.validate_workspace_id(bad) + for bad in breakout + ("x" * 64, "trail-"): + with pytest.raises(ValueError): + _validation.validate_region(bad) + + +def test_workspace_and_region_still_build_a_maas_host(_clean_env): + client = impl.Client( + api_key="sk-test", + workspace="ws-demo", + region="cn-hangzhou", + ) + assert client.transport.base_url == ( + "https://ws-demo.cn-hangzhou.maas.aliyuncs.com/api/v1/agentstudio" + ) + + +def test_untrusted_workspace_is_rejected_before_the_host_is_built(_clean_env): + with pytest.raises(ValueError): + impl.Client(api_key="sk-test", workspace="a@evil.com/#") + + +# --------------------------------------------------------------------------- +# The unmarked module tag, and the CLI's process-wide override +# --------------------------------------------------------------------------- + + +def test_agentstudio_tag_carries_no_v2_marker(monkeypatch): + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-sdk") + value = build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-client"] + assert value.endswith("/agentstudio") + assert not value.endswith("agentstudio2") + + +def test_cli_sdk_client_override_reaches_agentstudio(monkeypatch): + """``_SDK_CLIENT`` is process-wide mutable state: one copy only.""" + + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-cli") + value = build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-client"] + assert value.split("/")[0] == "python-cli" + # the 1.x re-export reads the very same state + assert ( + legacy_utils.get_sdk_headers(module="agentstudio")[ + "x-dashscope-sdk-client" + ] + == value + ) + + +def test_session_id_is_shared_with_the_v2_transport(): + from dashscope.v2.transport._ident import sdk_headers + + assert ( + build_headers( + api_key="sk-test", + uid=None, + user_agent="ua", + )["x-dashscope-sdk-session-id"] + == sdk_headers("aigc")["x-dashscope-sdk-session-id"] + ) + + +# --------------------------------------------------------------------------- +# api key resolution no longer reads the 1.x ``dashscope.api_key`` global +# --------------------------------------------------------------------------- + + +@pytest.fixture +def _no_key_anywhere(monkeypatch): + monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: None) + monkeypatch.setattr(as_client.settings, "api_key", None) + + +def test_api_key_prefers_settings_over_the_key_file(monkeypatch): + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: "sk-file") + assert as_client._default_api_key() == "sk-env" + + +def test_api_key_falls_back_to_the_key_file(monkeypatch): + monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + monkeypatch.setattr(as_client, "_api_key_from_file", lambda: "sk-file") + monkeypatch.setattr(as_client.settings, "api_key", None) + assert as_client._default_api_key() == "sk-file" + + +def test_missing_api_key_resolves_to_none(_no_key_anywhere): + assert as_client._default_api_key() is None + + +def test_missing_api_key_surfaces_at_request_time(_no_key_anywhere): + """Construction succeeds; the transport raises the native error.""" + + client = impl.Client(base_url=BASE_URL) + assert client.transport.api_key is None + with pytest.raises(impl_exc.AuthenticationError): + build_headers( + api_key=client.transport.api_key, + uid=None, + user_agent="ua", + ) + + +# --------------------------------------------------------------------------- +# The v2 error-alignment proxy still recognises sub-resources +# --------------------------------------------------------------------------- + + +def test_resource_detection_survives_the_move(): + """``_RESOURCE_PACKAGE`` must track the new module path. + + If it still pointed at ``dashscope.agentstudio.resources`` the proxy + would silently stop wrapping sub-resources, and their errors would + escape ``except dashscope.v2.errors.DashScopeError``. + """ + + client = _make_client() + assert _is_resource(client.agents) + assert _is_resource(client.sessions) + assert not _is_resource(client.transport) + + +def test_wrapped_resource_is_proxied(): + wrapped = wrap_agentstudio(_make_client()) + assert wrapped.agents is not _make_client().agents + assert type(wrapped.agents).__module__.startswith( + "dashscope.v2.resources._agentstudio", + ) diff --git a/tests/v2/test_audio.py b/tests/v2/test_audio.py index e918f513..3a9ea729 100644 --- a/tests/v2/test_audio.py +++ b/tests/v2/test_audio.py @@ -13,7 +13,7 @@ import pytest from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import APIConnectionError, DashScopeError +from dashscope.v2.errors import APIConnectionError, DashScopeError, WsStateError from dashscope.v2.resources import audio as audio_resources from dashscope.v2.resources.audio._callbacks import run_with_callback from dashscope.v2.resources.audio.asr import RecognitionResult @@ -463,9 +463,9 @@ def connector(url, headers): rec.send_audio_frame(b"second") rec.stop() rec.stop() - with pytest.raises(RuntimeError, match="input is finished"): + with pytest.raises(WsStateError, match="input is finished"): rec.send_audio_frame(b"late") - with pytest.raises(RuntimeError, match="input is finished"): + with pytest.raises(WsStateError, match="input is finished"): rec.update_context(context) assert next(events)["header"]["event"] == "task-finished" assert list(events) == [] @@ -543,7 +543,7 @@ def send(self, payload, opcode=1): with pytest.raises(OSError, match="send failed"): rec.send_audio_frame(b"audio") assert ws.closed - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): rec.send_audio_frame(b"more") diff --git a/tests/v2/test_audio_connection.py b/tests/v2/test_audio_connection.py index d8d962d4..29d8194c 100644 --- a/tests/v2/test_audio_connection.py +++ b/tests/v2/test_audio_connection.py @@ -10,7 +10,7 @@ import pytest from dashscope.v2.client import Client -from dashscope.v2.errors import DashScopeError +from dashscope.v2.errors import DashScopeError, WsBinaryOwnerConflictError BASE_URL = "https://api.test.local/api/v1" WS_URL = "wss://ws.test.local/api-ws/v1/inference" @@ -269,7 +269,7 @@ def test_two_syntheses_on_one_connection_fail_fast(): tts1 = client.audio.tts.connect(model="cosyvoice-v1", connection=conn) tts1.start() tts2 = client.audio.tts.connect(model="cosyvoice-v1", connection=conn) - with pytest.raises(RuntimeError, match="binary downlink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary downlink"): tts2.start() # The first session is unaffected. tts1.send_text("็ปง็ปญ") @@ -502,7 +502,7 @@ def test_shared_recognition_close_after_uplink_conflict_does_not_break_owner( first.start() second.start() first.send_audio_frame(b"first") - with pytest.raises(RuntimeError, match="binary uplink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary uplink"): second.send_audio_frame(b"blocked") second.close() first.send_audio_frame(b"still first") diff --git a/tests/v2/test_audio_management.py b/tests/v2/test_audio_management.py index f51386b3..3d4700b8 100644 --- a/tests/v2/test_audio_management.py +++ b/tests/v2/test_audio_management.py @@ -227,7 +227,7 @@ def handler(request): assert all(call["model"] == "voice-enrollment" for call in calls) -# โ”€โ”€ voice design (ๅฃฐ้Ÿณ่ฎพ่ฎก, qwen-voice-design) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# โ”€โ”€ voice design (qwen-voice-design) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def test_voice_design_crud(): diff --git a/tests/v2/test_audio_tts_session.py b/tests/v2/test_audio_tts_session.py index cafdebd3..34b0875b 100644 --- a/tests/v2/test_audio_tts_session.py +++ b/tests/v2/test_audio_tts_session.py @@ -15,7 +15,7 @@ import pytest from dashscope.v2 import Client -from dashscope.v2.errors import APIConnectionError, BadRequestError +from dashscope.v2.errors import APIConnectionError, BadRequestError, WsStateError from dashscope.v2.resources.audio.tts import SynthesisSession @@ -278,7 +278,7 @@ def test_no_more_text_or_flush_after_input_ends(client, ending, operation): with _session(client, ws) as session: getattr(session, ending)() sent_count = len(ws.sent) - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): if operation == "send_text": session.send_text("too late") else: diff --git a/tests/v2/test_chat.py b/tests/v2/test_chat.py index f9ce1c93..0bc51ed4 100644 --- a/tests/v2/test_chat.py +++ b/tests/v2/test_chat.py @@ -1532,14 +1532,14 @@ def handler(request): def test_media_upload_uses_the_per_call_api_key(monkeypatch): - # The OSS upload must run under the per-call key (1.x behavior); - # uploading with the client key would park the file in the wrong - # account when the two differ. + # The OSS upload must run under the per-call key; uploading with the + # client key would park the file in the wrong account when the two + # differ. seen = {} - def fake_upload(model, content, api_key, cert, **kwargs): - seen["api_key"] = api_key - seen["base_address"] = kwargs.get("base_address") + def fake_upload(client, model, content, **kwargs): + seen["client"] = client + seen["api_key"] = kwargs.get("api_key") return True, "oss://bucket/cat.png", {} monkeypatch.setattr( @@ -1563,9 +1563,9 @@ def handler(request): api_key="sk-override", ) assert seen["api_key"] == "sk-override" - # The certificate is fetched from the client's own base_url so a + # The certificate is fetched through the client's own transport, so a # region/gateway-scoped client uploads to the matching regional OSS. - assert seen["base_address"] == client.config.base_url + assert seen["client"] is client assert captured["auth"] == "Bearer sk-override" assert captured["oss"] == "enable" assert captured["body"]["input"]["messages"][0]["content"] == [ diff --git a/tests/v2/test_chat_params.py b/tests/v2/test_chat_params.py new file mode 100644 index 00000000..9f85ce6e --- /dev/null +++ b/tests/v2/test_chat_params.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Unit tests for the model-name rule behind incremental_output. + +The streaming behaviour this feeds is covered end to end in test_chat.py +(``test_incremental_output_false_merges_client_side`` and the exempt-model +cases); this pins the rule itself. +""" + +from __future__ import annotations + +import pytest + +from dashscope.v2.resources.chat._params import ( + should_modify_incremental_output, +) + + +@pytest.mark.parametrize( + "model", + [ + "qwen-tts", + "CosyVoice-TTS-v1", + "qwen-omni-turbo", + "Qwen2.5-Omni-7B", + "qwen-deep-research", + "qwen-DEEP-Research", + ], +) +def test_server_side_models_are_exempt(model): + assert should_modify_incremental_output(model) is False + + +@pytest.mark.parametrize( + "model", + ["qwen-plus", "qwen-max", "qwen-vl-max", "qwen3-coder-plus", ""], +) +def test_incremental_only_models_merge_client_side(model): + assert should_modify_incremental_output(model) is True + + +@pytest.mark.parametrize("model", [None, 123, ["qwen-tts"]]) +def test_a_missing_model_name_merges_client_side(model): + # Anything that is not a model name falls back to the common case + # rather than guessing an exemption. + assert should_modify_incremental_output(model) is True diff --git a/tests/v2/test_config.py b/tests/v2/test_config.py index da5a64c6..b1df7548 100644 --- a/tests/v2/test_config.py +++ b/tests/v2/test_config.py @@ -462,6 +462,40 @@ 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_error_registry.py b/tests/v2/test_error_registry.py index 3ba08e95..89f2815f 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``. @@ -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", } @@ -83,11 +84,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__ == "cd873b6" assert registry.__snapshot__ is False - assert len(PUBLIC) == 16 - assert len(SDK) == 32 + assert len(PUBLIC) == 17 + assert len(SDK) == 41 def test_generated_header_survives_vendoring(): @@ -97,7 +98,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``, @@ -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 @@ -287,10 +288,59 @@ 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.")) + if not e.name.startswith( + ("sdk.agentic_rl.", "sdk.agentstudio.", "sdk.ws.") + ) + ] + assert others == [ + "sdk.InvalidApiKey", + "sdk.ConnectionError", + "sdk.ConnectionInterrupted", + "sdk.Timeout", + "sdk.IoError", ] - assert others == ["sdk.InvalidApiKey"] + + +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 f22b3ca5..4568e6a8 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, @@ -34,11 +36,16 @@ PermissionDeniedError, RateLimitError, RequestTooLargeError, + ResponseError, SdkError, ServiceUnavailableError, + WsBinaryOwnerConflictError, + WsEventError, + WsStateError, check_response, error_from_business_code, error_from_sdk_exception, + sdk_error, ) from dashscope.v2.errors import ( _CODE_MAP, @@ -656,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(): @@ -667,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. @@ -774,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 @@ -828,6 +883,47 @@ def test_sdk_error_has_no_http_semantics(): assert SdkError("boom").status_code is None +def test_transport_errors_are_catalog_backed_sdk_errors(): + assert issubclass(APIConnectionError, SdkError) + assert issubclass(APITimeoutError, APIConnectionError) + + conn = APIConnectionError("boom") + conn_entry = _SDK_ERRORS["sdk.ConnectionError"] + assert conn.code == conn_entry.name + assert conn.solution == conn_entry.solution + assert conn.allow_retry == conn_entry.allow_retry + assert conn.status_code is None + + timeout = APITimeoutError("boom") + timeout_entry = _SDK_ERRORS["sdk.Timeout"] + assert timeout.code == timeout_entry.name + assert timeout.solution == timeout_entry.solution + assert timeout.allow_retry == timeout_entry.allow_retry + + +def test_transport_error_detail_fills_the_catalog_template(): + err = APIConnectionError(detail="dns blew up") + assert err.message == "Connection failed: dns blew up." + err = APITimeoutError(detail="slow") + assert err.message == "Operation timed out: slow." + + +def test_transport_error_explicit_fields_win_over_catalog_defaults(): + err = APIConnectionError("boom", code="custom", allow_retry=False) + assert err.code == "custom" + assert err.allow_retry is False + + +def test_sdk_error_factory_builds_classless_entries_from_the_catalog(): + entry = _SDK_ERRORS["sdk.IoError"] + err = sdk_error("sdk.IoError", {"detail": "disk full"}) + assert type(err) is SdkError + assert err.message == "Local I/O failed: disk full." + assert err.code == entry.name + assert err.solution == entry.solution + assert err.allow_retry is entry.allow_retry + + def test_sdk_error_translation_resolves_the_catalog_by_class_name(): err = error_from_sdk_exception( OSSUploadError("bucket denied"), diff --git a/tests/v2/test_files_models.py b/tests/v2/test_files_models.py index ff76efbe..ee76334d 100644 --- a/tests/v2/test_files_models.py +++ b/tests/v2/test_files_models.py @@ -8,7 +8,7 @@ import pytest from dashscope.v2 import AsyncClient, Client -from dashscope.v2.errors import BadRequestError, NotFoundError +from dashscope.v2.errors import BadRequestError, NotFoundError, SdkError BASE_URL = "https://api.test.local/api/v1" @@ -64,8 +64,11 @@ def handler(request): def test_files_upload_validation(tmp_path): client = _make_client(lambda r: httpx.Response(200, json={"data": {}})) - with pytest.raises(FileNotFoundError): + with pytest.raises(SdkError) as missing: client.files.upload(file_path=str(tmp_path / "nope.jsonl")) + assert missing.value.code == "sdk.IoError" + assert "nope.jsonl" in missing.value.message + assert missing.value.allow_retry is False bad = tmp_path / "bad.jsonl" bad.write_text("not json\n", encoding="utf-8") @@ -76,6 +79,24 @@ def test_files_upload_validation(tmp_path): assert resp is not None +def test_files_upload_unreadable_file_is_catalog_io_error( + tmp_path, + monkeypatch, +): + client = _make_client(lambda r: httpx.Response(200, json={"data": {}})) + path = tmp_path / "x.jsonl" + path.write_text("{}\n", encoding="utf-8") + + def _deny(*args, **kwargs): + raise PermissionError("denied") + + monkeypatch.setattr("builtins.open", _deny) + with pytest.raises(SdkError) as ei: + client.files.upload(file_path=str(path)) + assert ei.value.code == "sdk.IoError" + assert ei.value.message == "Local I/O failed: denied." + + def test_files_upload_retry_resends_full_body(tmp_path, monkeypatch): bodies = [] diff --git a/tests/v2/test_finetune_rl_shim.py b/tests/v2/test_finetune_rl_shim.py new file mode 100644 index 00000000..e1af41cd --- /dev/null +++ b/tests/v2/test_finetune_rl_shim.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Reverse-reference shims and the submit transport for the RL move. + +``dashscope.finetune.reinforcement`` and ``dashscope.finetune.agentic_rl`` +are now compatibility surfaces over ``dashscope.v2.finetune``. The old +paths have to keep resolving *by name* in processes this repo does not +control -- a Function Compute sandbox running ``FC_SERVER_CLASSPATH``, and +``uvicorn`` / ``python -m`` resolving the reward-server import string -- +so what is pinned here is object identity, laziness and the wire format, +not just the happy path. +""" + +from __future__ import annotations + +import importlib +import importlib.util +import json +import pathlib +import platform +import runpy +import sys +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +import dashscope +from dashscope._shared import _sdk_ident +from dashscope._shared._sdk_ident import set_sdk_client +from dashscope.finetune.agentic_rl import AgenticRL as LegacyAgenticRL +from dashscope.version import __version__ +from dashscope.v2.errors import SdkError +from dashscope.v2.finetune import _submit +from dashscope.v2.finetune.agentic_rl import AgenticRL as V2AgenticRL +from dashscope.v2.finetune.agentic_rl import SubmitOutput, SubmitResult +from dashscope.v2.finetune.reinforcement import DASHSCOPE_HTTP_BASE_URL +from dashscope.v2.finetune.reinforcement import TrainingType +from dashscope.v2.finetune.reinforcement.common.errors import ( + ValueErrorWithCode, +) +from dashscope.v2.resources import reinforcement_learning as _rl_mount + +LEGACY = "dashscope.finetune.reinforcement" +V2 = "dashscope.v2.finetune.reinforcement" + +# The modules the package imports eagerly, so both spellings are already +# in sys.modules before anything asks for them. The nine that are *not* +# here (component.server.* and component.observability.genai.*) are the +# reason the shim is a meta_path finder rather than an eager walk. +EAGER_SUBMODULES = [ + "common.constants", + "common.errors", + "common.log", + "common.model", + "common.model_types", + "common.utils", + "component.data.base_data_model", + "component.func_decorator", + "component.func_manager", + "component.observability.tracing", + "component.parser.reward_parser", + "component.processor.abstract_reward_processor", +] + +SUBMIT_PATH = f"{LEGACY}.component.server.server" + +ENVELOPE = { + "status_code": 200, + "request_id": "req-1", + "code": None, + "message": "", + "output": { + "job_id": "ft-1", + "status": "RUNNING", + "create_time": "2026-01-02 03:04:05", + "finetuned_output": "qwen-ft", + }, + "usage": None, + "headers": {"x-request-id": "h-1"}, +} + + +class _StubTuning: + """The smallest ``tuning`` that ``_build_submit_request`` accepts.""" + + def __init__(self): + self.teacher_model = None + self.name = "stub-job" + self.model = SimpleNamespace(name="qwen-test") + self.functions = [] + self.datasets = [] + self.training = SimpleNamespace( + hyper_parameters=None, + resources=None, + type=TrainingType.TRAINING_TYPE, + ) + + def combine_ids_runtimes(self, functype, id_str=None): + return [] + + def check_function_names(self): + return True + + +def _stub(cls, api_key="sk-submit"): + # ``set_api_key`` writes DASHSCOPE_API_KEY for real; these tests are + # about the submit call, not about credential discovery. + with patch("dashscope.v2.finetune.agentic_rl.set_api_key"): + instance = cls(api_key=api_key) + instance.tuning = _StubTuning() + return instance + + +class _FakeResponse: + status_code = 200 + headers = {"content-type": "application/json"} + content = b"" + + @staticmethod + def json(): + return {"request_id": "req-1", "output": {"job_id": "ft-1"}} + + +@pytest.fixture(name="post_calls") +def fixture_post_calls(monkeypatch): + """Record every ``post_submit`` call instead of hitting the network.""" + calls = [] + + # ``_SDK_CLIENT`` is process-wide mutable state, and importing the CLI + # (which ``test_app_is_reachable_only_from_the_legacy_path`` does) + # marks the whole process ``python-cli``. Pinning it here keeps these + # assertions independent of test order; monkeypatch restores whatever + # the ambient value was. + monkeypatch.setattr(_sdk_ident, "_SDK_CLIENT", "python-sdk") + + class _Session: + def __enter__(self): + return self + + def __exit__(self, *exc_info): + return False + + def post(self, url, data=None, headers=None, timeout=None): + calls.append( + { + "url": url, + "data": data, + "headers": headers, + "timeout": timeout, + }, + ) + return _FakeResponse() + + monkeypatch.setattr(_submit.requests, "Session", _Session) + return calls + + +# -- identity ------------------------------------------------------------- + + +def test_every_exported_name_is_the_same_object(): + legacy = importlib.import_module(LEGACY) + v2 = importlib.import_module(V2) + assert v2.__all__ + assert legacy.__all__ == list(v2.__all__) + ["app"] + for name in v2.__all__: + assert getattr(legacy, name) is getattr(v2, name), name + + +def test_legacy_agentic_rl_subclasses_the_v2_one(): + assert LegacyAgenticRL is not V2AgenticRL + assert issubclass(LegacyAgenticRL, V2AgenticRL) + + +def test_only_the_1x_surface_keeps_the_job_verbs(): + assert "CreateMixin" not in [c.__name__ for c in V2AgenticRL.__mro__] + for verb in ("cancel", "list", "get", "delete", "logs"): + assert verb not in V2AgenticRL.__dict__, verb + assert verb in LegacyAgenticRL.__dict__, verb + + +@pytest.mark.parametrize("submodule", EAGER_SUBMODULES) +def test_eager_deep_imports_alias_to_the_same_module(submodule): + legacy = importlib.import_module(f"{LEGACY}.{submodule}") + v2 = importlib.import_module(f"{V2}.{submodule}") + assert legacy is v2 + # The shared object keeps reporting the v2 name, so pickling and + # reload resolve against the sys.modules key that owns it. + assert legacy.__name__ == f"{V2}.{submodule}" + assert sys.modules[f"{LEGACY}.{submodule}"] is v2 + + +def test_deep_import_works_as_an_attribute_expression(): + legacy = importlib.import_module(LEGACY) + v2_utils = importlib.import_module(f"{V2}.common.utils") + assert legacy.common.utils.set_api_key is v2_utils.set_api_key + + +# -- laziness ------------------------------------------------------------- + + +def test_server_module_resolves_without_importing_fastapi(): + spec = importlib.util.find_spec(SUBMIT_PATH) + assert spec is not None + assert "dashscope/v2/finetune/reinforcement" in spec.origin.replace( + "\\", + "/", + ) + assert spec.loader.get_code(SUBMIT_PATH) is not None + assert "fastapi" not in sys.modules + + +def test_python_dash_m_resolves_the_legacy_server_path(): + # ``runpy`` refuses a loader that yields no code object, which is how + # examples/workspace/scripts/start_local_*.sh launches the server. + name, _, code = runpy._get_module_details(SUBMIT_PATH) + assert name == SUBMIT_PATH + assert code is not None + assert "fastapi" not in sys.modules + + +@pytest.mark.skipif( + importlib.util.find_spec("opentelemetry") is not None, + reason="opentelemetry is installed, so the guard cannot be observed", +) +def test_genai_stays_behind_its_optional_dependency_on_both_paths(): + # ``genai/__init__`` imports ``llm_dashscope`` eagerly, which needs + # opentelemetry. Both spellings must fail identically rather than one + # quietly succeeding through the alias. + for name in ( + f"{LEGACY}.component.observability.genai.llm_dashscope", + f"{V2}.component.observability.genai.llm_dashscope", + ): + with pytest.raises(ModuleNotFoundError) as ei: + importlib.import_module(name) + assert "opentelemetry" in str(ei.value) + + +def test_app_is_reachable_only_from_the_legacy_path(): + v2 = importlib.import_module(V2) + assert not hasattr(v2, "app") + + from dashscope.legacy_1x.cli.agentic_rl import app as cli_app + + assert importlib.import_module(LEGACY).app is cli_app + + +# -- the submit POST ------------------------------------------------------ + + +def test_submit_post_keeps_the_published_wire_format(post_calls): + _stub(V2AgenticRL).submit_job(job_name="่ฎก็ฎ—ไปปๅŠก") + + assert len(post_calls) == 1 + call = post_calls[0] + assert call["url"] == f"{DASHSCOPE_HTTP_BASE_URL}/fine-tunes" + assert call["timeout"] == _submit.SUBMIT_TIMEOUT + + headers = call["headers"] + # Insertion order is part of the contract: it is the order 1.x's + # CreateMixin produced, and the backend sees the same bytes. + assert list(headers) == [ + "Content-Type", + "user-agent", + "x-dashscope-sdk-client", + "x-dashscope-sdk-session-id", + "Authorization", + "Accept", + ] + assert headers["Content-Type"] == "application/json; charset=utf-8" + assert headers["Accept"] == headers["Content-Type"] + assert headers["Authorization"] == "Bearer sk-submit" + + # Unmarked module tag: the v2 transport's ``finetune2`` belongs to + # client.rl.jobs, not to the submit path. + assert headers["x-dashscope-sdk-client"] == ( + f"python-sdk/{__version__}/finetune" + ) + + user_agent = headers["user-agent"] + assert user_agent.startswith("dashscope/") + assert f"python/{platform.python_version()}" in user_agent + + body = call["data"] + assert isinstance(body, bytes) + payload = json.loads(body.decode("utf-8")) + # ensure_ascii=False: a non-ASCII job name must not be escaped. + assert "่ฎก็ฎ—ไปปๅŠก".encode("utf-8") in body + assert payload["job_name"].startswith("่ฎก็ฎ—ไปปๅŠก-") + assert payload["model"] == "qwen-test" + assert payload["training_type"] == str(TrainingType.TRAINING_TYPE) + assert "teacher_model" not in payload + + +def test_cli_sdk_client_override_reaches_the_submit_tag(post_calls): + # ``set_sdk_client`` is the CLI's own entry point into the mutable + # global; the ``post_calls`` fixture pins and restores it. + set_sdk_client("python-cli") + + _stub(V2AgenticRL).submit_job() + + assert post_calls[0]["headers"]["x-dashscope-sdk-client"] == ( + f"python-cli/{__version__}/finetune" + ) + + +def test_sdk_header_kill_switch_still_applies(monkeypatch, post_calls): + monkeypatch.setenv("DASHSCOPE_DISABLE_SDK_HEADERS", "1") + + _stub(V2AgenticRL).submit_job() + + headers = post_calls[0]["headers"] + assert "x-dashscope-sdk-client" not in headers + assert "x-dashscope-sdk-session-id" not in headers + assert headers["Authorization"] == "Bearer sk-submit" + + +def test_session_id_is_one_value_across_both_stacks(): + from dashscope.legacy_1x.common.utils import get_sdk_headers + from dashscope.v2.transport._ident import sdk_headers + + legacy = get_sdk_headers("finetune")["x-dashscope-sdk-session-id"] + v2 = sdk_headers("finetune")["x-dashscope-sdk-session-id"] + assert legacy == v2 + + +# -- api key resolution --------------------------------------------------- + + +def test_submit_ignores_the_1x_root_api_key_global(monkeypatch, post_calls): + monkeypatch.setattr(dashscope, "api_key", "sk-root-global", raising=False) + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + + rl = _stub(V2AgenticRL, api_key="sk-submit") + assert rl._api_key == "sk-submit" + rl.submit_job() + + assert post_calls[0]["headers"]["Authorization"] == "Bearer sk-submit" + + +def test_submit_falls_back_to_the_environment(monkeypatch, post_calls): + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-env") + + rl = _stub(V2AgenticRL, api_key=None) + assert rl._api_key is None + rl.submit_job() + + assert post_calls[0]["headers"]["Authorization"] == "Bearer sk-env" + + +# -- the two return types ------------------------------------------------- + + +def test_v2_submit_returns_submit_result_and_feeds_to_job(): + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(ENVELOPE), + ): + result = _stub(V2AgenticRL).submit_job() + + assert isinstance(result, SubmitResult) + assert isinstance(result.output, SubmitOutput) + assert result.output.job_id == "ft-1" + assert result.request_id == "req-1" + assert result.headers == {"x-request-id": "h-1"} + + job = _rl_mount._to_job(result) + assert job.job_id == "ft-1" + assert job.status == "RUNNING" + # The v2 model's aliases absorb the submit's 1.x-era field names. + assert job.created_at == "2026-01-02 03:04:05" + assert job.finetuned_model == "qwen-ft" + assert job.request_id == "req-1" + + +def test_legacy_submit_returns_the_finetune_dataclass(): + from dashscope.finetune.customize_types import FineTune + + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(ENVELOPE), + ): + result = _stub(LegacyAgenticRL).submit_job() + + assert isinstance(result, FineTune) + assert result.status_code == 200 + assert result.output.job_id == "ft-1" + assert result.output.finetuned_output == "qwen-ft" + + +def test_a_failed_submit_is_an_envelope_not_an_exception(): + failure = { + "status_code": 400, + "request_id": "req-2", + "code": "InvalidParameter", + "message": "bad job", + "output": None, + "usage": None, + "headers": {}, + } + with patch( + "dashscope.v2.finetune._submit.post_submit", + return_value=dict(failure), + ): + result = _stub(V2AgenticRL).submit_job() + + assert result.status_code == 400 + assert result.code == "InvalidParameter" + assert result.output is None + + +# -- single implementation ------------------------------------------------ + + +def test_the_legacy_package_directory_holds_only_the_shim(): + legacy = importlib.import_module(LEGACY) + package_dir = pathlib.Path(legacy.__file__).parent + assert [p.name for p in package_dir.rglob("*.py")] == ["__init__.py"] + + +def test_the_v2_mount_points_at_the_v2_workflow_modules(): + assert _rl_mount._agentic_rl is importlib.import_module( + "dashscope.v2.finetune.agentic_rl", + ) + assert _rl_mount._reinforcement is importlib.import_module(V2) + + +def test_the_rl_error_module_is_shared_by_both_paths(): + legacy_errors = importlib.import_module(f"{LEGACY}.common.errors") + assert _rl_mount._rl_errors is legacy_errors + assert _rl_mount._rl_errors is importlib.import_module( + f"{V2}.common.errors", + ) + for name in _rl_mount._RL_EXCEPTIONS: + assert getattr(legacy_errors, name.__name__) is name + + +def test_translate_rl_errors_still_maps_the_family(): + with pytest.raises(SdkError) as ei: + with _rl_mount._translate_rl_errors(): + raise ValueErrorWithCode("duplicate names", error_code=3004) + + assert ei.value.code == "sdk.agentic_rl.ValueErrorWithCode" + assert "duplicate names" in ei.value.message + assert isinstance(ei.value.__cause__, ValueErrorWithCode) diff --git a/tests/v2/test_images_videos.py b/tests/v2/test_images_videos.py index c8fdb497..a7dc46eb 100644 --- a/tests/v2/test_images_videos.py +++ b/tests/v2/test_images_videos.py @@ -665,9 +665,9 @@ def handler(request): def test_image_jobs_create_uploads_with_the_per_call_api_key(monkeypatch): seen = {} - def fake_upload(model, content, api_key, cert, **kwargs): - seen["api_key"] = api_key - seen["base_address"] = kwargs.get("base_address") + def fake_upload(client, model, content, **kwargs): + seen["client"] = client + seen["api_key"] = kwargs.get("api_key") return True, "oss://bucket/ref.png", {} monkeypatch.setattr( @@ -690,7 +690,7 @@ def handler(request): api_key="sk-override", ) assert seen["api_key"] == "sk-override" - assert seen["base_address"] == client.config.base_url + assert seen["client"] is client assert captured["auth"] == "Bearer sk-override" assert captured["oss"] == "enable" content = captured["body"]["input"]["messages"][0]["content"] diff --git a/tests/v2/test_oss_upload.py b/tests/v2/test_oss_upload.py new file mode 100644 index 00000000..b75c190a --- /dev/null +++ b/tests/v2/test_oss_upload.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the v2-native local-file โ†’ OSS uploader.""" + +# Mock handlers intentionally ignore the request argument. +# pylint: disable=unused-argument + +from __future__ import annotations + +import os + +import httpx +import pytest + +from dashscope.v2 import Client +from dashscope.v2.errors import AuthenticationError, PermissionDeniedError +from dashscope.v2.resources import _oss_upload + +BASE_URL = "https://api.test.local/api/v1" +OSS_HOST = "https://dashscope-oss.oss-cn-beijing.aliyuncs.com" + +_CERT = { + "oss_access_key_id": "STS.access-key-id", + "signature": "sig==", + "policy": "pol==", + "upload_dir": "upload/2026/abc", + "x_oss_object_acl": "private", + "x_oss_forbid_overwrite": "true", + "upload_host": OSS_HOST, +} + + +def _make_client(handler, **kwargs): + mock = httpx.MockTransport(handler) + http_client = httpx.Client(transport=mock, base_url=BASE_URL) + kwargs.setdefault("base_url", BASE_URL) + return Client(api_key="sk-test", http_client=http_client, **kwargs) + + +def _policy_client(status=200): + """Client whose transport answers ``GET /uploads`` with a policy.""" + calls = [] + + def handler(request): + calls.append(request) + if status != 200: + return httpx.Response(status, json={"message": "denied"}) + return httpx.Response( + 200, + json={"request_id": "r-1", "output": _CERT}, + ) + + return _make_client(handler), calls + + +def _capture_oss(monkeypatch, status=200): + """Send the OSS POST through a MockTransport and record what it carried.""" + seen = {} + real_post = _oss_upload._post_to_oss + + def handler(request): + seen["url"] = str(request.url) + seen["headers"] = request.headers + seen["body"] = request.content.decode("utf-8", "replace") + if status == 200: + return httpx.Response(200, text="") + return httpx.Response( + status, + text="AccessDenied", + ) + + def patched(upload_host, form, file_name, file_path, transport=None): + seen["form"] = form + seen["file_name"] = file_name + return real_post( + upload_host, + form, + file_name, + file_path, + transport=httpx.MockTransport(handler), + ) + + monkeypatch.setattr(_oss_upload, "_post_to_oss", patched) + return seen + + +def _png(tmp_path, name="cat.png"): + path = tmp_path / name + path.write_bytes(b"\x89PNG\r\n\x1a\n") + return str(path) + + +def test_certificate_fetch_rides_the_client_transport(): + client, calls = _policy_client() + + cert = _oss_upload.get_upload_certificate( + client, + "qwen-vl-max", + api_key="sk-override", + workspace="ws-1", + ) + + assert cert == _CERT + request = calls[0] + assert request.url.path == "/api/v1/uploads" + assert request.url.params["action"] == "getPolicy" + assert request.url.params["model"] == "qwen-vl-max" + # This hop talks to DashScope, so it does carry the per-call credential. + assert request.headers.get("Authorization") == "Bearer sk-override" + assert request.headers.get("X-DashScope-WorkSpace") == "ws-1" + + +def test_certificate_fetch_failure_raises(): + client, _ = _policy_client(status=401) + with pytest.raises(AuthenticationError): + _oss_upload.get_upload_certificate(client, "qwen-vl-max") + + +def test_upload_local_file_builds_the_oss_form(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, calls = _policy_client() + file_path = _png(tmp_path) + + url, cert = _oss_upload.upload_local_file( + client, + "qwen-vl-max", + file_path, + ) + + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + assert cert == _CERT + assert seen["url"] == OSS_HOST + assert seen["form"] == { + "OSSAccessKeyId": _CERT["oss_access_key_id"], + "Signature": _CERT["signature"], + "policy": _CERT["policy"], + "key": _CERT["upload_dir"] + "/cat.png", + "x-oss-object-acl": "private", + "x-oss-forbid-overwrite": "true", + "success_action_status": "200", + "x-oss-content-type": "image/png", + } + assert seen["file_name"] == "cat.png" + # OSS rejects a post-object request whose "file" part is not last. + assert seen["body"].rindex('name="key"') < seen["body"].rindex( + 'name="file"', + ) + assert len(calls) == 1 + + +def test_unknown_extension_falls_back_to_octet_stream(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, _ = _policy_client() + file_path = str(tmp_path / "blob") + with open(file_path, "wb") as handle: + handle.write(b"x") + + _oss_upload.upload_local_file(client, "qwen-vl-max", file_path) + + assert seen["form"]["x-oss-content-type"] == "application/octet-stream" + + +def test_oss_post_carries_no_dashscope_credential(tmp_path, monkeypatch): + seen = _capture_oss(monkeypatch) + client, _ = _policy_client() + + _oss_upload.upload_local_file(client, "qwen-vl-max", _png(tmp_path)) + + # The credential boundary: the storage host must never see the API key, + # so this hop uses its own auth-free client rather than the transport. + assert seen["headers"].get("Authorization") is None + assert "sk-test" not in seen["body"] + assert seen["headers"].get("Accept") == "application/json" + user_agent = seen["headers"].get("User-Agent", "") + assert user_agent.startswith("dashscope-v2-python/") + + +def test_oss_post_failure_maps_to_a_dashscope_error(tmp_path, monkeypatch): + _capture_oss(monkeypatch, status=403) + client, _ = _policy_client() + + with pytest.raises(PermissionDeniedError): + _oss_upload.upload_local_file(client, "qwen-vl-max", _png(tmp_path)) + + +def test_certificate_is_reused_across_uploads(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, calls = _policy_client() + first = _png(tmp_path, "a.png") + second = _png(tmp_path, "b.png") + + url_a, cert = _oss_upload.upload_local_file(client, "m", first) + url_b, _ = _oss_upload.upload_local_file( + client, + "m", + second, + certificate=cert, + ) + + assert url_a == f"oss://{_CERT['upload_dir']}/a.png" + assert url_b == f"oss://{_CERT['upload_dir']}/b.png" + # Only the first upload needed a policy. + assert len(calls) == 1 + + +def test_check_and_upload_local_keeps_remote_urls_untouched(): + client, calls = _policy_client() + + for content in ( + "https://example.com/cat.png", + "http://example.com/cat.png", + ): + assert _oss_upload.check_and_upload_local(client, "m", content) == ( + False, + content, + None, + ) + assert calls == [] + + +def test_check_and_upload_local_flags_an_existing_oss_url(): + client, calls = _policy_client() + + result = _oss_upload.check_and_upload_local( + client, + "m", + "oss://bucket/cat.png", + ) + + # Nothing was uploaded, but the caller still has to opt the request into + # OSS resolution. + assert result == (True, "oss://bucket/cat.png", None) + assert calls == [] + + +def test_check_and_upload_local_uploads_a_file_uri(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, calls = _policy_client() + file_path = _png(tmp_path) + + is_up, url, cert = _oss_upload.check_and_upload_local( + client, + "qwen-vl-max", + f"file://{file_path}", + api_key="sk-override", + ) + + assert is_up is True + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + assert cert == _CERT + assert len(calls) == 1 + assert calls[0].headers.get("Authorization") == "Bearer sk-override" + + +def test_check_and_upload_local_uploads_a_bare_path(tmp_path, monkeypatch): + _capture_oss(monkeypatch) + client, _ = _policy_client() + + is_up, url, _ = _oss_upload.check_and_upload_local( + client, + "m", + _png(tmp_path), + ) + + assert is_up is True + assert url == f"oss://{_CERT['upload_dir']}/cat.png" + + +def test_check_and_upload_local_expands_a_path_that_is_not_a_file(): + client, calls = _policy_client() + + is_up, value, cert = _oss_upload.check_and_upload_local( + client, + "m", + "~/nope.png", + ) + + # Passed through, but with ~ expanded as in 1.x. + assert (is_up, cert) == (False, None) + assert value == os.path.expanduser("~/nope.png") + assert calls == [] + + +def test_missing_file_uri_raises_value_error(tmp_path): + client, calls = _policy_client() + missing = (tmp_path / "gone.png").as_uri() + + with pytest.raises(ValueError, match="is not exists"): + _oss_upload.check_and_upload_local(client, "m", missing) + assert calls == [] + + +@pytest.mark.parametrize( + "uri,expected", + [ + ("file:///tmp/a%20b.png", "/tmp/a b.png"), + ("file://localhost/tmp/a.png", "localhost/tmp/a.png"), + ("file:///C:/Users/me/a.png", "C:/Users/me/a.png"), + ], +) +def test_resolve_file_uri_path(uri, expected): + assert _oss_upload._resolve_file_uri_path(uri) == expected diff --git a/tests/v2/test_package_root.py b/tests/v2/test_package_root.py index f363ed69..7cd65572 100644 --- a/tests/v2/test_package_root.py +++ b/tests/v2/test_package_root.py @@ -50,31 +50,43 @@ 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_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.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.__module__.startswith("dashscope.v2.tokenizers") assert v2.get_tokenizer is get_1x assert v2.list_tokenizers is list_1x - for name in ("get_tokenizer", "list_tokenizers", "save_api_key"): + assert dashscope.get_tokenizer is v2.get_tokenizer + 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.legacy_1x.common.api_key import save_api_key as save_1x + + assert v2.save_api_key.__module__ == "dashscope.v2._config" + assert v2.save_api_key is not save_1x + assert dashscope.save_api_key is save_1x + assert "save_api_key" in v2.__all__ + + def test_v2_get_tokenizer_encodes_locally(): from dashscope import v2 - from dashscope.common.error import UnsupportedModel tokenizer = v2.get_tokenizer("qwen-turbo") ids = tokenizer.encode("hello world") assert ids and all(isinstance(i, int) for i in ids) assert tokenizer.decode(ids) == "hello world" assert "qwen-turbo" in v2.list_tokenizers() - with pytest.raises(UnsupportedModel): + with pytest.raises(ValueError): v2.get_tokenizer("not-a-qwen-model") diff --git a/tests/v2/test_rl.py b/tests/v2/test_rl.py index 29e189b8..dd982c92 100644 --- a/tests/v2/test_rl.py +++ b/tests/v2/test_rl.py @@ -10,7 +10,7 @@ import httpx import pytest -from dashscope.finetune import agentic_rl as agentic_rl_module +from dashscope.v2.finetune import agentic_rl as agentic_rl_module from dashscope.finetune.reinforcement.common.errors import ( AgenticRLError, InstanceQueryError, @@ -644,7 +644,7 @@ def test_rl_long_form_alias(): assert async_client.reinforcement_learning is async_client.rl -# -- 1.x exceptions translated into the v2 error family ----------------- +# -- RL exceptions translated into the v2 error family ------------------- @pytest.fixture(name="fail_step") @@ -710,8 +710,8 @@ def test_exact_catalog_hit_carries_its_own_solution(fail_step): def test_uncatalogued_rl_class_falls_back_to_the_family_entry(fail_step): - # AgenticRLError is the 1.x base but has no catalog entry of its own, - # so it must land on the family fallback rather than escape raw. + # AgenticRLError is the RL family's base but has no catalog entry of + # its own, so it must land on the family fallback, not escape raw. fail_step("register_functions", AgenticRLError("something odd")) client = _make_client(lambda request: httpx.Response(200, json={})) with pytest.raises(SdkError) as ei: diff --git a/tests/v2/test_transport.py b/tests/v2/test_transport.py index faf964f1..f4820014 100644 --- a/tests/v2/test_transport.py +++ b/tests/v2/test_transport.py @@ -133,6 +133,8 @@ def test_connection_error_retried_then_raises(no_sleep): with pytest.raises(APIConnectionError) as exc_info: t.request("GET", "/x") assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + assert exc_info.value.code == "sdk.ConnectionError" + assert exc_info.value.allow_retry is True assert rec.calls == 3 assert len(no_sleep) == 2 @@ -148,14 +150,17 @@ def test_timeout_maps_to_api_timeout_error(no_sleep): t.request("GET", "/x") assert isinstance(exc_info.value, APIConnectionError) assert isinstance(exc_info.value.__cause__, httpx.ConnectTimeout) + assert exc_info.value.code == "sdk.Timeout" + assert exc_info.value.allow_retry is True assert rec.calls == 3 def test_open_stream_transport_error_mapped(no_sleep): rec = _Recorder() t = _make_sync(rec, [httpx.ConnectError("boom")], max_retries=2) - with pytest.raises(APIConnectionError): + with pytest.raises(APIConnectionError) as exc_info: t.open_stream("POST", "/x") + assert exc_info.value.code == "sdk.ConnectionError" # Streams are never retried. assert rec.calls == 1 @@ -430,7 +435,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 diff --git a/tests/v2/test_ws_connection.py b/tests/v2/test_ws_connection.py index 5716cb4c..f7235866 100644 --- a/tests/v2/test_ws_connection.py +++ b/tests/v2/test_ws_connection.py @@ -10,7 +10,12 @@ import pytest -from dashscope.v2.errors import APIConnectionError, DashScopeError +from dashscope.v2.errors import ( + APIConnectionError, + DashScopeError, + WsBinaryOwnerConflictError, + WsStateError, +) from dashscope.v2.transport.ws import WsEventError, WsSession from dashscope.v2.transport.ws_connection import WsConnection @@ -161,7 +166,7 @@ def test_binary_downlink_two_active_tasks_requires_claim(): _started(conn, t1) _started(conn, t2) t2.claim_binary_downlink() - with pytest.raises(RuntimeError, match="binary downlink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary downlink"): t1.claim_binary_downlink() conn._ws.inbox.put(b"audio-for-t2") assert t2.recv() == b"audio-for-t2" @@ -183,7 +188,7 @@ def test_binary_downlink_ambiguous_fails_connection(): with pytest.raises(WsEventError, match="claim_binary_downlink"): t2.recv() assert conn.closed - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): conn.open_task() @@ -194,7 +199,7 @@ def test_binary_uplink_single_owner_and_release_on_finish(): _started(conn, t1) _started(conn, t2) t1.send_binary(b"frame-1") - with pytest.raises(RuntimeError, match="binary uplink"): + with pytest.raises(WsBinaryOwnerConflictError, match="binary uplink"): t2.send_binary(b"frame-2") assert conn._ws.sent[-1] == b"frame-1" @@ -250,7 +255,7 @@ def test_task_close_detaches_and_late_events_drop(): conn._ws.inbox.put(_event("result-generated", t2.task_id, {"ok": 1})) assert t2.recv_event()["payload"] == {"ok": 1} assert not conn.closed - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): t1.recv() conn.close() @@ -433,7 +438,7 @@ def send(self, payload, opcode=1): assert conn.closed and ws.closed with pytest.raises(APIConnectionError): other.recv() - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): conn.open_task() task.close() other.close() @@ -547,7 +552,7 @@ def test_inactive_task_cannot_reclaim_binary_downlink( else: conn._ws.inbox.put(_event(terminal, task.task_id)) task.recv_event() - with pytest.raises(RuntimeError): + with pytest.raises(WsStateError): task.claim_binary_downlink() next_task.claim_binary_downlink() conn._ws.inbox.put(b"next-output") @@ -572,7 +577,7 @@ def test_connection_shutdown_releases_binary_owners( task.recv() assert conn._binary_uplink_owner is None assert conn._binary_downlink_owner is None - with pytest.raises(RuntimeError, match="closed"): + with pytest.raises(WsStateError, match="closed"): task.claim_binary_downlink() assert conn._binary_downlink_owner is None @@ -601,9 +606,9 @@ def test_reader_releases_terminal_owners_before_consumer_finishes( assert not first.finished # The consumer must still drain its frames. assert conn._binary_uplink_owner is None assert conn._binary_downlink_owner is None - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): first.claim_binary_downlink() - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): first.send_binary(b"too late") second.send_binary(b"second-input") if claim: @@ -634,7 +639,7 @@ def test_binary_owner_must_be_attached_to_this_connection( foreign._task_id = ( task.task_id ) # Matching ids alone do not imply attachment. - with pytest.raises(RuntimeError, match="not active"): + with pytest.raises(WsStateError, match="not active"): getattr(conn, "_claim_binary_" + direction)(foreign) assert getattr(conn, "_binary_" + direction + "_owner") is None task.claim_binary_downlink() @@ -676,7 +681,7 @@ def receive(): receiver.join(timeout=1) assert not receiver.is_alive(), "close did not wake the receiver" expected = ( - RuntimeError if close_target == "task" else APIConnectionError + WsStateError if close_target == "task" else APIConnectionError ) assert len(errors) == 1 and isinstance(errors[0], expected), errors finally: @@ -742,7 +747,7 @@ def test_error_is_persistent_but_preserves_earlier_frames( barrier.recv_event() # Earlier frames have reached the task's queue. if shutdown in ("task", "connection"): (task if shutdown == "task" else conn).close() - expected = RuntimeError if shutdown == "task" else APIConnectionError + expected = WsStateError if shutdown == "task" else APIConnectionError else: conn._ws.inbox.put( OSError("receive failed") if shutdown == "recv" else "{",