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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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
Expand Down
93 changes: 52 additions & 41 deletions dashscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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",
}


Expand Down
29 changes: 29 additions & 0 deletions dashscope/_shared/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
"""
21 changes: 21 additions & 0 deletions dashscope/_shared/_errors.py
Original file line number Diff line number Diff line change
@@ -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"]
121 changes: 121 additions & 0 deletions dashscope/_shared/_sdk_ident.py
Original file line number Diff line number Diff line change
@@ -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/<version>; 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/<version>``). 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions dashscope/_shared/_validation.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading