From ba068931f4d26758621744ebbffc3808e63231d2 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Wed, 2 Sep 2026 16:53:19 +0800 Subject: [PATCH 1/7] Merge SDK headers into single x-dashscope-sdk-client field --- dashscope/acli/providers/tongyi.py | 8 ++++++-- dashscope/agentstudio/transport.py | 2 +- dashscope/common/utils.py | 11 ++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/dashscope/acli/providers/tongyi.py b/dashscope/acli/providers/tongyi.py index bebd19c..b93fa19 100644 --- a/dashscope/acli/providers/tongyi.py +++ b/dashscope/acli/providers/tongyi.py @@ -48,12 +48,14 @@ def __init__( request_timeout: int = 60, protocol: str = "openai", base_url: str | None = None, + module: str = "", ): self.model = model self.api_key = api_key self.request_timeout = request_timeout self.protocol = protocol self.base_url = (base_url or DASHSCOPE_BASE_URL).rstrip("/") + self.module = module def _convert_tools(self, tools: list[dict] | None) -> list[dict] | None: if not tools: @@ -126,8 +128,10 @@ def _get_headers(self) -> dict: if self.api_key: headers["Authorization"] = f"Bearer {self.api_key}" if not os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): - headers["x-dashscope-sdk-client"] = "acli" - headers["x-dashscope-sdk-version"] = __version__ + parts = ["acli", __version__] + if self.module: + parts.append(self.module) + headers["x-dashscope-sdk-client"] = "/".join(parts) headers["x-dashscope-sdk-session-id"] = SDK_SESSION_ID return headers diff --git a/dashscope/agentstudio/transport.py b/dashscope/agentstudio/transport.py index dbdce09..9083ba4 100644 --- a/dashscope/agentstudio/transport.py +++ b/dashscope/agentstudio/transport.py @@ -230,7 +230,7 @@ def build_headers( HEADER_AUTHORIZATION: f"Bearer {api_key}", HEADER_USER_AGENT: user_agent, } - headers.update(get_sdk_headers()) + headers.update(get_sdk_headers(module="agentstudio")) if json_body: headers["Content-Type"] = "application/json" headers["Accept"] = "application/json" diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index bb157e9..76c5ca0 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -163,16 +163,21 @@ def get_user_agent(): return ua -def get_sdk_headers() -> Dict[str, str]: +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 = ["python-sdk", __version__] + if module: + parts.append(module) return { - "x-dashscope-sdk-client": "python-sdk", - "x-dashscope-sdk-version": __version__, + "x-dashscope-sdk-client": "/".join(parts), "x-dashscope-sdk-session-id": _SDK_SESSION_ID, } From dfead96c406f9c041a38232b59a38d30c30f6199 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 3 Sep 2026 15:24:00 +0800 Subject: [PATCH 2/7] Add unit tests for x-dashscope-sdk-client header format --- tests/unit/test_sdk_headers.py | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/unit/test_sdk_headers.py diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py new file mode 100644 index 0000000..1bdc99d --- /dev/null +++ b/tests/unit/test_sdk_headers.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import pytest + +from dashscope import __version__ as sdk_version +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 + +CLIENT_HEADER = "x-dashscope-sdk-client" +SESSION_HEADER = "x-dashscope-sdk-session-id" +DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" + +# module 取值在代码库中的实际使用点: +# - "" (无 module): default_headers / base_request / api_request_factory +# - "agentstudio": agentstudio/transport.py +# - acli 侧由 TongyiProvider(module=...) 构造参数传入(如 "app") +SDK_MODULES = ["", "agentstudio"] +ACLI_MODULES = ["", "app"] + + +def _check_client_header(value, client, version, module=""): + parts = value.split("/") + assert parts[0] == client + assert parts[1] == version + if module: + assert len(parts) == 3 + assert parts[2] == module + else: + assert len(parts) == 2 + + +def test_python_sdk_client_header(): + print("\n=== python-sdk: x-dashscope-sdk-client ===") + for module in SDK_MODULES: + headers = get_sdk_headers(module=module) + value = headers[CLIENT_HEADER] + print(f"module={module or '(none)':<15} -> {value}") + _check_client_header(value, "python-sdk", sdk_version, module) + assert headers[SESSION_HEADER] == _SDK_SESSION_ID + + +def test_acli_client_header(): + # pylint: disable=protected-access + print("\n=== acli: x-dashscope-sdk-client ===") + for module in ACLI_MODULES: + provider = TongyiProvider( + model="qwen-plus", + api_key="sk-x", + module=module, + ) + headers = provider._get_headers() + value = headers[CLIENT_HEADER] + print(f"module={module or '(none)':<15} -> {value}") + _check_client_header(value, "acli", acli_version, module) + assert headers[SESSION_HEADER] == ACLI_SESSION_ID + + +def test_sdk_headers_disabled(monkeypatch): + # pylint: disable=protected-access + monkeypatch.setenv(DISABLE_ENV, "1") + assert not get_sdk_headers() + assert not get_sdk_headers(module="agentstudio") + provider = TongyiProvider(model="qwen-plus", api_key="sk-x", module="app") + assert CLIENT_HEADER not in provider._get_headers() + assert SESSION_HEADER not in provider._get_headers() + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) From ca4cbbd9617cd9151f7fa17bd05eb7bed695e0c1 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 3 Sep 2026 17:01:47 +0800 Subject: [PATCH 3/7] feat: add SDK client identifier override and deployment plan parameters --- dashscope/cli/__init__.py | 3 ++ dashscope/cli/deployments.py | 17 +++++--- dashscope/common/utils.py | 16 +++++++- dashscope/finetune/deployments.py | 11 ++++++ tests/unit/handle_deployment_request.py | 2 + tests/unit/test_cli_main.py | 2 +- tests/unit/test_http_deployments_api.py | 15 +++++++ tests/unit/test_sdk_headers.py | 52 +++++++++++++++++++++++++ 8 files changed, 111 insertions(+), 7 deletions(-) diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index 6f4ce4a..151f120 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -22,6 +22,7 @@ import dashscope # noqa: E402 from dashscope.cli.common import 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, @@ -43,6 +44,8 @@ video_synthesis, ) +set_sdk_client("python-cli") + # --------------------------------------------------------------------------- # Legacy command compatibility layer diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index 44f2a58..4450d0e 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/cli/deployments.py @@ -129,10 +129,11 @@ def create( "--capacity", help="The target capacity", ), - plan: Optional[str] = typer.Option( - None, + plan: str = typer.Option( + ..., "--plan", - help="Deployment plan or template ID", + help="Required. Deployment plan: ptu (PTU reserved), " + "mu (model unit), or Lora (LoRA deployment).", ), template_id: Optional[str] = typer.Option( None, @@ -145,9 +146,8 @@ def create( "model": model, "capacity": capacity, "suffix": suffix, + "plan": plan, } - if plan is not None: - kwargs["plan"] = plan if template_id is not None: kwargs["template_id"] = template_id @@ -194,12 +194,19 @@ def call( "--capacity", help="The target capacity", ), + plan: str = typer.Option( + ..., + "--plan", + help="Required. Deployment plan: ptu (PTU reserved), " + "mu (model unit), or Lora (LoRA deployment).", + ), ): """(Deprecated: use 'create' instead) Create a model deployment.""" create( model=model, suffix=suffix, capacity=capacity, + plan=plan, ) diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index 76c5ca0..ed26127 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -163,6 +163,20 @@ def get_user_agent(): return ua +_SDK_CLIENT = "python-sdk" + + +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. @@ -173,7 +187,7 @@ def get_sdk_headers(module: str = "") -> Dict[str, str]: """ if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"): return {} - parts = ["python-sdk", __version__] + parts = [_SDK_CLIENT, __version__] if module: parts.append(module) return { diff --git a/dashscope/finetune/deployments.py b/dashscope/finetune/deployments.py index ea3819b..182062a 100644 --- a/dashscope/finetune/deployments.py +++ b/dashscope/finetune/deployments.py @@ -37,6 +37,8 @@ def call( # pylint: disable=arguments-renamed # type: ignore[override] capacity: int, version: str = None, suffix: Optional[str] = None, + plan: Optional[str] = None, + template_id: Optional[str] = None, api_key: str = None, workspace: str = None, **kwargs, @@ -51,6 +53,11 @@ def call( # pylint: disable=arguments-renamed # type: ignore[override] If specified, the final model name will be model_suffix. Defaults to None. capacity (int, optional): The model service capacity. + plan (str, optional): The deployment plan required by the + service: "ptu" (PTU reserved), "mu" (model unit) or + "Lora" (LoRA deployment). Defaults to None. + template_id (str, optional): Template ID for deployment + configuration. Defaults to None. api_key (str, optional): The api-key. Defaults to None. workspace (str): The dashscope workspace id. @@ -63,6 +70,10 @@ def call( # pylint: disable=arguments-renamed # type: ignore[override] req["model_version"] = version if suffix is not None: req["suffix"] = suffix + if plan is not None: + req["plan"] = plan + if template_id is not None: + req["template_id"] = template_id response = super().call( req, api_key=api_key, diff --git a/tests/unit/handle_deployment_request.py b/tests/unit/handle_deployment_request.py index 5a5d65e..1930d02 100644 --- a/tests/unit/handle_deployment_request.py +++ b/tests/unit/handle_deployment_request.py @@ -21,6 +21,8 @@ async def create_deployment_handler(request: aiohttp.request): "deployed_model": "deploy123456", "status": "PENDING", "model_name": "qwen-turbo-ft-202307121513-5dde", + "plan": body.get("plan"), + "template_id": body.get("template_id"), }, } diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index 802291d..60722b5 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -648,7 +648,7 @@ def mock_call(**kwargs): result = CliRunner().invoke( cli_app, - ["deployments", "create", "--model", "qwen"], + ["deployments", "create", "--model", "qwen", "--plan", "ptu"], ) assert result.exit_code == 1 diff --git a/tests/unit/test_http_deployments_api.py b/tests/unit/test_http_deployments_api.py index ab50aff..3119344 100644 --- a/tests/unit/test_http_deployments_api.py +++ b/tests/unit/test_http_deployments_api.py @@ -21,6 +21,21 @@ def test_create_deployment_tune_job(self, http_server): assert resp.output["deployed_model"] == "deploy123456" assert resp.output["status"] == "PENDING" + def test_create_deployment_with_plan(self, http_server): + # Regression: plan/template_id must reach the request body (verified + # via mock echo; previously dropped silently, causing "plan is blank") + resp = Deployments.call( + model="gpt", + suffix="1", + capacity=2, + plan="plan-standard", + template_id="tpl-001", + ) + assert resp.status_code == HTTPStatus.OK + assert resp.output["deployed_model"] == "deploy123456" + assert resp.output["plan"] == "plan-standard" + assert resp.output["template_id"] == "tpl-001" + def test_list_deployment_job(self, http_server): rsp = Deployments.list() assert rsp.status_code == HTTPStatus.OK diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index 1bdc99d..5a960b5 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -13,6 +13,11 @@ SESSION_HEADER = "x-dashscope-sdk-session-id" DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" +# client 取值来源: +# - "python-sdk": SDK 默认(common/utils.py 的 _SDK_CLIENT) +# - "python-cli": dashscope/cli/__init__.py 在 CLI 进程启动时调用 +# set_sdk_client("python-cli") 覆盖 +# - "acli": acli/providers/tongyi.py 自建 header # module 取值在代码库中的实际使用点: # - "" (无 module): default_headers / base_request / api_request_factory # - "agentstudio": agentstudio/transport.py @@ -21,6 +26,12 @@ ACLI_MODULES = ["", "app"] +@pytest.fixture(autouse=True) +def _pin_sdk_client(monkeypatch): + # CLI 测试会在同进程内把 client 改成 python-cli,这里钉住默认值 + monkeypatch.setattr("dashscope.common.utils._SDK_CLIENT", "python-sdk") + + def _check_client_header(value, client, version, module=""): parts = value.split("/") assert parts[0] == client @@ -68,5 +79,46 @@ def test_sdk_headers_disabled(monkeypatch): assert SESSION_HEADER not in provider._get_headers() +def test_python_cli_client_header(): + from dashscope.common.utils import set_sdk_client + + set_sdk_client("python-cli") + try: + headers = get_sdk_headers() + value = headers[CLIENT_HEADER] + print(f"\npython-cli -> {value}") + _check_client_header(value, "python-cli", sdk_version) + # module 段同样适用 + value = get_sdk_headers(module="agentstudio")[CLIENT_HEADER] + print(f"python-cli -> {value}") + _check_client_header(value, "python-cli", sdk_version, "agentstudio") + finally: + set_sdk_client("python-sdk") + + +def test_cli_import_marks_process(): + # 子进程隔离验证:加载 dashscope.cli 后,进程内请求的 + # client 标识应变为 python-cli(与同进程其他测试的执行顺序无关) + import subprocess + import sys + + code = ( + "import dashscope.cli; " + "from dashscope.common.utils import get_sdk_headers; " + "h = get_sdk_headers(); " + "c = h['x-dashscope-sdk-client']; " + "assert c.startswith('python-cli/'), c; " + "print(c)" + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + print(f"\nCLI process -> {result.stdout.strip()}") + + if __name__ == "__main__": pytest.main([__file__, "-v", "-s"]) From 30e7cb545a8dd1f397b72f3041db10a812b6904b Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 3 Sep 2026 18:26:29 +0800 Subject: [PATCH 4/7] feat(cli): add fetch, wait, cancel, and list commands for async tasks - Add fetch command to get task status or result for image generation, synthesis, and transcription - Add wait command to wait for task completion for all three task types - Implement cancel command to cancel pending tasks with success confirmation - Add list command to list tasks with filtering options for all three task types - Update CLI modules to import and use success helper for cancel command feedback - Add comprehensive unit tests for new commands in image generation, synthesis, and transcription CLI tests --- dashscope/cli/image_generation.py | 105 +++++++++++++++++ dashscope/cli/image_synthesis.py | 111 +++++++++++++++++- dashscope/cli/transcription.py | 111 +++++++++++++++++- tests/unit/test_cli_image_generation.py | 143 +++++++++++++++++++++++ tests/unit/test_cli_image_synthesis.py | 143 +++++++++++++++++++++++ tests/unit/test_cli_transcription.py | 149 ++++++++++++++++++++++++ 6 files changed, 760 insertions(+), 2 deletions(-) diff --git a/dashscope/cli/image_generation.py b/dashscope/cli/image_generation.py index 19bef67..a2efb62 100644 --- a/dashscope/cli/image_generation.py +++ b/dashscope/cli/image_generation.py @@ -12,6 +12,7 @@ ensure_ok, handle_sdk_error, normalize_local_path_or_url, + success, ) app = typer.Typer( @@ -86,3 +87,107 @@ def create( usage = getattr(response, "usage", None) if usage: console.print_json(json.dumps(usage, ensure_ascii=False)) + + +@app.command("fetch") +@handle_sdk_error("Fetch image generation task failed") +def fetch( + task_id: str = typer.Argument(..., help="The image generation task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Fetch image generation task status or result.""" + response = ImageGeneration.fetch(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("wait") +@handle_sdk_error("Wait image generation task failed") +def wait( + task_id: str = typer.Argument(..., help="The image generation task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Wait for an image generation task to complete.""" + response = ImageGeneration.wait(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("cancel") +@handle_sdk_error("Cancel image generation task failed") +def cancel( + task_id: str = typer.Argument(..., help="The image generation task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Cancel a pending image generation task.""" + ensure_ok(ImageGeneration.cancel(task_id, workspace=workspace)) + success(f"Cancel image generation task: {task_id} success") + + +@app.command("list") +@handle_sdk_error("List image generation tasks failed") +def list_tasks( + start_time: Optional[str] = typer.Option( + None, + "--start-time", + help="Task start time", + ), + end_time: Optional[str] = typer.Option( + None, + "--end-time", + help="Task end time", + ), + model_name: Optional[str] = typer.Option( + None, + "--model-name", + help="Model name", + ), + api_key_id: Optional[str] = typer.Option( + None, + "--api-key-id", + help="API key id", + ), + region: Optional[str] = typer.Option( + None, + "--region", + help="Service region", + ), + status: Optional[str] = typer.Option(None, "--status", help="Task status"), + page: int = typer.Option(1, "-p", "--page", help="Page number"), + size: int = typer.Option(10, "-s", "--size", help="Page size"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List image generation tasks.""" + response = ImageGeneration.list( + start_time=start_time, + end_time=end_time, + model_name=model_name, + api_key_id=api_key_id, + region=region, + status=status, + page_no=page, + page_size=size, + workspace=workspace, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) diff --git a/dashscope/cli/image_synthesis.py b/dashscope/cli/image_synthesis.py index a4d247c..63e393c 100644 --- a/dashscope/cli/image_synthesis.py +++ b/dashscope/cli/image_synthesis.py @@ -6,7 +6,12 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + success, +) app = typer.Typer( name="image-synthesis", @@ -66,3 +71,107 @@ def create( usage = getattr(response, "usage", None) if usage: console.print_json(json.dumps(usage, ensure_ascii=False)) + + +@app.command("fetch") +@handle_sdk_error("Fetch image synthesis task failed") +def fetch( + task_id: str = typer.Argument(..., help="The image synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Fetch image synthesis task status or result.""" + response = dashscope.ImageSynthesis.fetch(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("wait") +@handle_sdk_error("Wait image synthesis task failed") +def wait( + task_id: str = typer.Argument(..., help="The image synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Wait for an image synthesis task to complete.""" + response = dashscope.ImageSynthesis.wait(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("cancel") +@handle_sdk_error("Cancel image synthesis task failed") +def cancel( + task_id: str = typer.Argument(..., help="The image synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Cancel a pending image synthesis task.""" + ensure_ok(dashscope.ImageSynthesis.cancel(task_id, workspace=workspace)) + success(f"Cancel image synthesis task: {task_id} success") + + +@app.command("list") +@handle_sdk_error("List image synthesis tasks failed") +def list_tasks( + start_time: Optional[str] = typer.Option( + None, + "--start-time", + help="Task start time", + ), + end_time: Optional[str] = typer.Option( + None, + "--end-time", + help="Task end time", + ), + model_name: Optional[str] = typer.Option( + None, + "--model-name", + help="Model name", + ), + api_key_id: Optional[str] = typer.Option( + None, + "--api-key-id", + help="API key id", + ), + region: Optional[str] = typer.Option( + None, + "--region", + help="Service region", + ), + status: Optional[str] = typer.Option(None, "--status", help="Task status"), + page: int = typer.Option(1, "-p", "--page", help="Page number"), + size: int = typer.Option(10, "-s", "--size", help="Page size"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List image synthesis tasks.""" + response = dashscope.ImageSynthesis.list( + start_time=start_time, + end_time=end_time, + model_name=model_name, + api_key_id=api_key_id, + region=region, + status=status, + page_no=page, + page_size=size, + workspace=workspace, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) diff --git a/dashscope/cli/transcription.py b/dashscope/cli/transcription.py index c1665df..af25d27 100644 --- a/dashscope/cli/transcription.py +++ b/dashscope/cli/transcription.py @@ -6,7 +6,12 @@ import typer import dashscope -from dashscope.cli.common import console, ensure_ok, handle_sdk_error +from dashscope.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + success, +) app = typer.Typer( name="transcription", @@ -100,3 +105,107 @@ def create( usage = getattr(response, "usage", None) if usage: console.print_json(json.dumps(usage, ensure_ascii=False)) + + +@app.command("fetch") +@handle_sdk_error("Fetch transcription task failed") +def fetch( + task_id: str = typer.Argument(..., help="The transcription task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Fetch transcription task status or result.""" + response = dashscope.Transcription.fetch(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("wait") +@handle_sdk_error("Wait transcription task failed") +def wait( + task_id: str = typer.Argument(..., help="The transcription task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Wait for a transcription task to complete.""" + response = dashscope.Transcription.wait(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("cancel") +@handle_sdk_error("Cancel transcription task failed") +def cancel( + task_id: str = typer.Argument(..., help="The transcription task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Cancel a pending transcription task.""" + ensure_ok(dashscope.Transcription.cancel(task_id, workspace=workspace)) + success(f"Cancel transcription task: {task_id} success") + + +@app.command("list") +@handle_sdk_error("List transcription tasks failed") +def list_tasks( + start_time: Optional[str] = typer.Option( + None, + "--start-time", + help="Task start time", + ), + end_time: Optional[str] = typer.Option( + None, + "--end-time", + help="Task end time", + ), + model_name: Optional[str] = typer.Option( + None, + "--model-name", + help="Model name", + ), + api_key_id: Optional[str] = typer.Option( + None, + "--api-key-id", + help="API key id", + ), + region: Optional[str] = typer.Option( + None, + "--region", + help="Service region", + ), + status: Optional[str] = typer.Option(None, "--status", help="Task status"), + page: int = typer.Option(1, "-p", "--page", help="Page number"), + size: int = typer.Option(10, "-s", "--size", help="Page size"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List transcription tasks.""" + response = dashscope.Transcription.list( + start_time=start_time, + end_time=end_time, + model_name=model_name, + api_key_id=api_key_id, + region=region, + status=status, + page_no=page, + page_size=size, + workspace=workspace, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) diff --git a/tests/unit/test_cli_image_generation.py b/tests/unit/test_cli_image_generation.py index f43a066..6a19f29 100644 --- a/tests/unit/test_cli_image_generation.py +++ b/tests/unit/test_cli_image_generation.py @@ -79,3 +79,146 @@ def mock_call(**kwargs): ] assert "generated.png" in result.output assert "input_tokens" in result.output + + def test_fetch(self, monkeypatch): + captured_request = {} + + def mock_fetch(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={"task_id": task_id, "task_status": "RUNNING"}, + ) + + monkeypatch.setattr( + image_generation.ImageGeneration, + "fetch", + mock_fetch, + ) + + result = CliRunner().invoke( + image_generation.app, + ["fetch", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "RUNNING" in result.output + + def test_wait(self, monkeypatch): + captured_request = {} + + def mock_wait(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={ + "task_id": task_id, + "task_status": "SUCCEEDED", + "choices": [], + }, + ) + + monkeypatch.setattr( + image_generation.ImageGeneration, + "wait", + mock_wait, + ) + + result = CliRunner().invoke( + image_generation.app, + ["wait", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "SUCCEEDED" in result.output + + def test_cancel(self, monkeypatch): + captured_request = {} + + def mock_cancel(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace(status_code=200, output={"deleted": True}) + + monkeypatch.setattr( + image_generation.ImageGeneration, + "cancel", + mock_cancel, + ) + + result = CliRunner().invoke( + image_generation.app, + ["cancel", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "success" in result.output + + def test_list(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={"tasks": [{"task_id": "task-1234"}]}, + ) + + monkeypatch.setattr( + image_generation.ImageGeneration, + "list", + mock_list, + ) + + result = CliRunner().invoke( + image_generation.app, + [ + "list", + "--start-time", + "20240101000000", + "--end-time", + "20240102000000", + "--model-name", + "wan2.6-image", + "--api-key-id", + "ak-id", + "--region", + "cn-beijing", + "--status", + "SUCCEEDED", + "--page", + "2", + "--size", + "20", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "start_time": "20240101000000", + "end_time": "20240102000000", + "model_name": "wan2.6-image", + "api_key_id": "ak-id", + "region": "cn-beijing", + "status": "SUCCEEDED", + "page_no": 2, + "page_size": 20, + "workspace": "workspace-id", + } + assert "task-1234" in result.output diff --git a/tests/unit/test_cli_image_synthesis.py b/tests/unit/test_cli_image_synthesis.py index 2afbb17..142d6ca 100644 --- a/tests/unit/test_cli_image_synthesis.py +++ b/tests/unit/test_cli_image_synthesis.py @@ -64,3 +64,146 @@ def mock_call(**kwargs): } assert "task-1234" in result.output assert "image_count" in result.output + + def test_fetch(self, monkeypatch): + captured_request = {} + + def mock_fetch(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={"task_id": task_id, "task_status": "RUNNING"}, + ) + + monkeypatch.setattr( + image_synthesis.dashscope.ImageSynthesis, + "fetch", + mock_fetch, + ) + + result = CliRunner().invoke( + image_synthesis.app, + ["fetch", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "RUNNING" in result.output + + def test_wait(self, monkeypatch): + captured_request = {} + + def mock_wait(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={ + "task_id": task_id, + "task_status": "SUCCEEDED", + "results": [{"url": "https://example.com/image.png"}], + }, + ) + + monkeypatch.setattr( + image_synthesis.dashscope.ImageSynthesis, + "wait", + mock_wait, + ) + + result = CliRunner().invoke( + image_synthesis.app, + ["wait", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "SUCCEEDED" in result.output + + def test_cancel(self, monkeypatch): + captured_request = {} + + def mock_cancel(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace(status_code=200, output={"deleted": True}) + + monkeypatch.setattr( + image_synthesis.dashscope.ImageSynthesis, + "cancel", + mock_cancel, + ) + + result = CliRunner().invoke( + image_synthesis.app, + ["cancel", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "success" in result.output + + def test_list(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={"tasks": [{"task_id": "task-1234"}]}, + ) + + monkeypatch.setattr( + image_synthesis.dashscope.ImageSynthesis, + "list", + mock_list, + ) + + result = CliRunner().invoke( + image_synthesis.app, + [ + "list", + "--start-time", + "20240101000000", + "--end-time", + "20240102000000", + "--model-name", + "wanx", + "--api-key-id", + "ak-id", + "--region", + "cn-beijing", + "--status", + "SUCCEEDED", + "--page", + "2", + "--size", + "20", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "start_time": "20240101000000", + "end_time": "20240102000000", + "model_name": "wanx", + "api_key_id": "ak-id", + "region": "cn-beijing", + "status": "SUCCEEDED", + "page_no": 2, + "page_size": 20, + "workspace": "workspace-id", + } + assert "task-1234" in result.output diff --git a/tests/unit/test_cli_transcription.py b/tests/unit/test_cli_transcription.py index bd8c663..20cfc42 100644 --- a/tests/unit/test_cli_transcription.py +++ b/tests/unit/test_cli_transcription.py @@ -79,3 +79,152 @@ def mock_call(**kwargs): } assert "task-1234" in result.output assert "audio_count" in result.output + + def test_fetch(self, monkeypatch): + captured_request = {} + + def mock_fetch(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={"task_id": task_id, "task_status": "RUNNING"}, + ) + + monkeypatch.setattr( + transcription.dashscope.Transcription, + "fetch", + mock_fetch, + ) + + result = CliRunner().invoke( + transcription.app, + ["fetch", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "RUNNING" in result.output + + def test_wait(self, monkeypatch): + captured_request = {} + + def mock_wait(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={ + "task_id": task_id, + "task_status": "SUCCEEDED", + "results": [ + { + "transcription_url": ( + "https://example.com/result.json" + ), + }, + ], + }, + ) + + monkeypatch.setattr( + transcription.dashscope.Transcription, + "wait", + mock_wait, + ) + + result = CliRunner().invoke( + transcription.app, + ["wait", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "SUCCEEDED" in result.output + + def test_cancel(self, monkeypatch): + captured_request = {} + + def mock_cancel(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace(status_code=200, output={"deleted": True}) + + monkeypatch.setattr( + transcription.dashscope.Transcription, + "cancel", + mock_cancel, + ) + + result = CliRunner().invoke( + transcription.app, + ["cancel", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "success" in result.output + + def test_list(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={"tasks": [{"task_id": "task-1234"}]}, + ) + + monkeypatch.setattr( + transcription.dashscope.Transcription, + "list", + mock_list, + ) + + result = CliRunner().invoke( + transcription.app, + [ + "list", + "--start-time", + "20240101000000", + "--end-time", + "20240102000000", + "--model-name", + "paraformer-v1", + "--api-key-id", + "ak-id", + "--region", + "cn-beijing", + "--status", + "SUCCEEDED", + "--page", + "2", + "--size", + "20", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "start_time": "20240101000000", + "end_time": "20240102000000", + "model_name": "paraformer-v1", + "api_key_id": "ak-id", + "region": "cn-beijing", + "status": "SUCCEEDED", + "page_no": 2, + "page_size": 20, + "workspace": "workspace-id", + } + assert "task-1234" in result.output From 4f86b8fafe581d22f84e296ab88ae5b2c6a6099a Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 3 Sep 2026 19:46:31 +0800 Subject: [PATCH 5/7] feat: tag x-dashscope-sdk-client with module on all request paths Derive the module segment from the API class package (threads grouped into assistants), tag /tasks/* operations as "tasks", add sdk headers to standalone WebSocket classes (tts_v2, qwen_omni, qwen_tts_realtime, multimodal_dialog, tingwu_realtime) and to encryption/reinforcement/ http-tts paths, and default acli TongyiProvider module to "app". --- dashscope/acli/providers/tongyi.py | 2 +- dashscope/api_entities/api_request_factory.py | 4 +- dashscope/api_entities/encryption.py | 2 + dashscope/app/application.py | 1 + .../audio/http_tts/http_speech_synthesizer.py | 5 +- dashscope/audio/qwen_omni/omni_realtime.py | 3 +- .../qwen_tts_realtime/qwen_tts_realtime.py | 3 +- dashscope/audio/tts_v2/speech_synthesizer.py | 3 +- dashscope/client/base_api.py | 40 +- dashscope/common/utils.py | 24 +- .../finetune/reinforcement/common/utils.py | 7 +- dashscope/models.py | 3 +- dashscope/multimodal/multimodal_dialog.py | 3 +- dashscope/multimodal/tingwu/tingwu.py | 1 + .../multimodal/tingwu/tingwu_realtime.py | 3 +- tests/unit/test_sdk_headers.py | 416 +++++++++++++++++- 16 files changed, 481 insertions(+), 39 deletions(-) diff --git a/dashscope/acli/providers/tongyi.py b/dashscope/acli/providers/tongyi.py index b93fa19..da45cd4 100644 --- a/dashscope/acli/providers/tongyi.py +++ b/dashscope/acli/providers/tongyi.py @@ -48,7 +48,7 @@ def __init__( request_timeout: int = 60, protocol: str = "openai", base_url: str | None = None, - module: str = "", + module: str = "app", ): self.model = model self.api_key = api_key diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/api_entities/api_request_factory.py index 121c5d8..c1d2649 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/api_entities/api_request_factory.py @@ -53,6 +53,8 @@ def _build_api_request( # pylint: disable=too-many-branches task_id: Optional[str] = None, enable_encryption: bool = False, pre_task_id: Optional[str] = None, + # SDK module tag for the x-dashscope-sdk-client header + sdk_module: str = "", # Additional parameters for API request data **kwargs, ): @@ -192,7 +194,7 @@ def _build_api_request( # pylint: disable=too-many-branches "websocket]", ) - merged_headers = dict(get_sdk_headers()) + merged_headers = dict(get_sdk_headers(module=sdk_module)) if headers is not None: merged_headers.update(headers) if merged_headers: diff --git a/dashscope/api_entities/encryption.py b/dashscope/api_entities/encryption.py index e1017c0..4848aa1 100644 --- a/dashscope/api_entities/encryption.py +++ b/dashscope/api_entities/encryption.py @@ -18,6 +18,7 @@ DEFAULT_REQUEST_TIMEOUT_SECONDS, ) from dashscope.common.logging import logger +from dashscope.common.utils import get_sdk_headers class Encryption: @@ -90,6 +91,7 @@ def _get_public_keys(): url = dashscope.base_http_api_url + "/public-keys/latest" headers = { "Authorization": f"Bearer {dashscope.api_key}", + **get_sdk_headers(module="utils"), } response = requests.get( diff --git a/dashscope/app/application.py b/dashscope/app/application.py index 9ee99b1..6afb72c 100644 --- a/dashscope/app/application.py +++ b/dashscope/app/application.py @@ -240,6 +240,7 @@ def call( # type: ignore[override] workspace=workspace, api_key=api_key, is_service=False, + sdk_module="app", **parameters, ) # call request service. diff --git a/dashscope/audio/http_tts/http_speech_synthesizer.py b/dashscope/audio/http_tts/http_speech_synthesizer.py index df10d93..fdf3b28 100644 --- a/dashscope/audio/http_tts/http_speech_synthesizer.py +++ b/dashscope/audio/http_tts/http_speech_synthesizer.py @@ -203,7 +203,7 @@ def _http_call( # Get base URL import dashscope - from dashscope.common.utils import join_url + from dashscope.common.utils import get_sdk_headers, join_url if url: base_url = url @@ -233,6 +233,9 @@ def _http_call( stream=stream, ) + # Tag sdk module (custom headers below may still override) + request.add_headers(get_sdk_headers(module="audio")) + # Add custom headers if headers: request.add_headers(headers) diff --git a/dashscope/audio/qwen_omni/omni_realtime.py b/dashscope/audio/qwen_omni/omni_realtime.py index 4bddc53..f541b2c 100644 --- a/dashscope/audio/qwen_omni/omni_realtime.py +++ b/dashscope/audio/qwen_omni/omni_realtime.py @@ -14,7 +14,7 @@ import dashscope from dashscope.common.error import ModelRequired from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.common.utils import get_sdk_headers, get_user_agent class OmniRealtimeCallback: @@ -259,6 +259,7 @@ def _get_websocket_header(self): headers = { "user-agent": ua, "Authorization": "Bearer " + self.apikey, + **get_sdk_headers(module="audio"), } if self.user_headers: headers = {**self.user_headers, **headers} diff --git a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py b/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py index 3d5c374..31eb038 100644 --- a/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py +++ b/dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py @@ -12,7 +12,7 @@ import dashscope from dashscope.common.error import ModelRequired from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.common.utils import get_sdk_headers, get_user_agent class QwenTtsRealtimeCallback: @@ -117,6 +117,7 @@ def _get_websocket_header(self): headers = { "user-agent": ua, "Authorization": "Bearer " + self.apikey, + **get_sdk_headers(module="audio"), } if self.user_headers: headers = {**self.user_headers, **headers} diff --git a/dashscope/audio/tts_v2/speech_synthesizer.py b/dashscope/audio/tts_v2/speech_synthesizer.py index a63d6a4..ab4ac34 100644 --- a/dashscope/audio/tts_v2/speech_synthesizer.py +++ b/dashscope/audio/tts_v2/speech_synthesizer.py @@ -22,7 +22,7 @@ RequestFailure, ) from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.common.utils import get_sdk_headers, get_user_agent from dashscope.protocol.websocket import ( ACTION_KEY, ERROR_MESSAGE, @@ -183,6 +183,7 @@ def get_websocket_headers(self, headers, workspace): self.headers = { "user-agent": ua, "Authorization": "Bearer " + self.apikey, + **get_sdk_headers(module="audio"), } if headers: self.headers = {**self.headers, **headers} diff --git a/dashscope/client/base_api.py b/dashscope/client/base_api.py index fc067fc..c36336e 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/client/base_api.py @@ -28,6 +28,7 @@ _handle_http_response, _handle_http_stream_response, default_headers, + get_api_module, join_url, ) @@ -69,7 +70,7 @@ def _handle_kwargs( custom_headers = kwargs.pop("headers", None) headers = { **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module="tasks"), } if custom_headers: headers = { @@ -133,6 +134,7 @@ async def async_call( task=task, function=function, api_key=api_key, + sdk_module=get_api_module(cls.__module__), **kwargs, ) # call request service. @@ -373,7 +375,7 @@ async def list( api_key = get_default_api_key() headers = { **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module="tasks"), } async with aiohttp.ClientSession(trust_env=True) as session: response = await session.get( @@ -486,6 +488,7 @@ async def call( task=task, function=function, api_key=api_key, + sdk_module=get_api_module(cls.__module__), **kwargs, ) # call request service. @@ -552,6 +555,7 @@ def call( task=task, function=function, api_key=api_key, + sdk_module=get_api_module(cls.__module__), **kwargs, ) # call request service. @@ -588,7 +592,7 @@ def _get( custom_headers = kwargs.pop("headers", None) headers = { **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module="tasks"), } if custom_headers: headers = { @@ -687,7 +691,7 @@ def cancel( url, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module="tasks"), }, ) return _handle_http_response(response) @@ -751,7 +755,7 @@ def list( params=params, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module="tasks"), }, ) if response.status_code == HTTPStatus.OK: @@ -934,6 +938,7 @@ def async_call( api_key=api_key, async_request=True, query=False, + sdk_module=get_api_module(cls.__module__), **kwargs, ) return request.call() @@ -946,6 +951,7 @@ def _get( api_key=None, flattened_output=False, workspace: str = None, + module: str = "", **kwargs, ) -> Union[DashScopeAPIResponse, Dict]: timeout = kwargs.pop( @@ -958,7 +964,7 @@ def _get( url, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=module), **kwargs.pop("headers", {}), }, params=params, @@ -1025,6 +1031,7 @@ def list( params=params, api_key=api_key, workspace=workspace, + module=get_api_module(cls.__module__), **kwargs, ) @@ -1060,6 +1067,7 @@ def list( params=params, api_key=api_key, workspace=workspace, + module=get_api_module(cls.__module__), **kwargs, ) @@ -1099,6 +1107,7 @@ def logs( # pylint: disable=unused-argument params=params, api_key=api_key, workspace=workspace, + module=get_api_module(cls.__module__), **kwargs, ) @@ -1142,6 +1151,7 @@ def get( params=params, flattened_output=flattened_output, workspace=workspace, + module=get_api_module(cls.__module__), **kwargs, ) @@ -1181,6 +1191,7 @@ def get( api_key=api_key, flattened_output=flattened_output, workspace=workspace, + module=get_api_module(cls.__module__), **kwargs, ) @@ -1225,7 +1236,7 @@ def delete( url, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1275,7 +1286,7 @@ def call( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1332,6 +1343,7 @@ def update( flattened_output = kwargs.pop("flattened_output", False) import json as _json # pylint: disable=reimported + module = get_api_module(cls.__module__) with requests.Session() as session: logger.debug("Starting request: %s", url) body = _json.dumps(json, ensure_ascii=False).encode("utf-8") @@ -1342,7 +1354,7 @@ def update( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=module), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1354,7 +1366,7 @@ def update( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=module), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1409,7 +1421,7 @@ def put( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1461,7 +1473,7 @@ def upload( # pylint: disable=unused-argument data=js, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, files=files, @@ -1511,7 +1523,7 @@ def cancel( url, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1618,7 +1630,7 @@ def stream_events( url, headers={ **_workspace_header(workspace), - **default_headers(api_key), + **default_headers(api_key, module=get_api_module(cls.__module__)), **kwargs.pop("headers", {}), }, stream=True, diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index ed26127..8c2d79f 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -165,6 +165,26 @@ def get_user_agent(): _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). +_SDK_MODULE_PACKAGE_OVERRIDES = { + "threads": "assistants", +} + + +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". + """ + parts = module_name.split(".") + if len(parts) < 2 or parts[0] != "dashscope": + return "" + pkg = parts[1] + 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. @@ -196,10 +216,10 @@ def get_sdk_headers(module: str = "") -> Dict[str, str]: } -def default_headers(api_key: str = None) -> Dict[str, str]: +def default_headers(api_key: str = None, module: str = "") -> Dict[str, str]: ua = get_user_agent() headers = {"user-agent": ua} - headers.update(get_sdk_headers()) + headers.update(get_sdk_headers(module=module)) if api_key is None: api_key = get_default_api_key() headers["Authorization"] = f"Bearer {api_key}" diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/finetune/reinforcement/common/utils.py index f0915c9..ffadda3 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/finetune/reinforcement/common/utils.py @@ -20,6 +20,7 @@ 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 ( InputError, @@ -175,6 +176,7 @@ async def client_fc( headers={ "Content-Type": content_type, "Authorization": "Bearer " + api_key, + **get_sdk_headers(module="finetune"), }, data=request_data, timeout=HTTP_REQUEST_TIMEOUT, @@ -595,7 +597,10 @@ async def to_bailian_data(files: List[FileSpec]) -> List[str]: Raises: OutputError: If file upload fails """ - headers = {"Authorization": f"Bearer {DASHSCOPE_API_KEY}"} + headers = { + "Authorization": f"Bearer {DASHSCOPE_API_KEY}", + **get_sdk_headers(module="finetune"), + } form_data = FormData() uploaded_files = [] diff --git a/dashscope/models.py b/dashscope/models.py index 5a1a57e..fee38a4 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -3,7 +3,7 @@ from dashscope.api_entities.dashscope_response import DashScopeAPIResponse from dashscope.client.base_api import GetMixin, ListMixin, _get -from dashscope.common.utils import join_url +from dashscope.common.utils import get_api_module, join_url import dashscope @@ -38,6 +38,7 @@ def get( # type: ignore[override] url, params=params, api_key=api_key, + module=get_api_module(cls.__module__), **kwargs, ) diff --git a/dashscope/multimodal/multimodal_dialog.py b/dashscope/multimodal/multimodal_dialog.py index 1517e97..3ba7ad5 100644 --- a/dashscope/multimodal/multimodal_dialog.py +++ b/dashscope/multimodal/multimodal_dialog.py @@ -9,7 +9,7 @@ import dashscope from dashscope.common.logging import logger from dashscope.common.error import InputRequired -from dashscope.common.utils import get_user_agent +from dashscope.common.utils import get_sdk_headers, get_user_agent from dashscope.multimodal import dialog_state from dashscope.multimodal.multimodal_constants import ( RESPONSE_NAME_STARTED, @@ -451,6 +451,7 @@ def get_websocket_header(self, api_key): "User-Agent": ua, "Authorization": f"Bearer {api_key}", "Accept": "application/json", + **get_sdk_headers(module="aigc"), } log_headers = self.ws_headers.copy() log_headers["Authorization"] = "REDACTED" diff --git a/dashscope/multimodal/tingwu/tingwu.py b/dashscope/multimodal/tingwu/tingwu.py index 15896f0..fbceeb7 100644 --- a/dashscope/multimodal/tingwu/tingwu.py +++ b/dashscope/multimodal/tingwu/tingwu.py @@ -59,6 +59,7 @@ def call( # type: ignore[override] task=TingWu.task, # type: ignore[arg-type] function=TingWu.function, # type: ignore[arg-type] is_service=False, + sdk_module="multimodal", **parameters, ) response = request.call() diff --git a/dashscope/multimodal/tingwu/tingwu_realtime.py b/dashscope/multimodal/tingwu/tingwu_realtime.py index 71ed4ab..a9f7719 100644 --- a/dashscope/multimodal/tingwu/tingwu_realtime.py +++ b/dashscope/multimodal/tingwu/tingwu_realtime.py @@ -14,7 +14,7 @@ # pylint: disable=ungrouped-imports from dashscope.common.logging import logger -from dashscope.common.utils import get_user_agent +from dashscope.common.utils import get_sdk_headers, get_user_agent from dashscope.protocol.websocket import ActionType @@ -343,6 +343,7 @@ def get_websocket_header(self, api_key): "User-Agent": ua, "Authorization": f"Bearer {api_key}", "Accept": "application/json", + **get_sdk_headers(module="multimodal"), } log_headers = self.ws_headers.copy() log_headers["Authorization"] = "REDACTED" diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index 5a960b5..b2211ec 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -13,22 +13,26 @@ SESSION_HEADER = "x-dashscope-sdk-session-id" DISABLE_ENV = "DASHSCOPE_DISABLE_SDK_HEADERS" -# client 取值来源: -# - "python-sdk": SDK 默认(common/utils.py 的 _SDK_CLIENT) -# - "python-cli": dashscope/cli/__init__.py 在 CLI 进程启动时调用 -# set_sdk_client("python-cli") 覆盖 -# - "acli": acli/providers/tongyi.py 自建 header -# module 取值在代码库中的实际使用点: -# - "" (无 module): default_headers / base_request / api_request_factory -# - "agentstudio": agentstudio/transport.py -# - acli 侧由 TongyiProvider(module=...) 构造参数传入(如 "app") +# client value sources: +# - "python-sdk": SDK default (_SDK_CLIENT in common/utils.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 +# module rules (see get_api_module in common/utils.py): +# - service calls (BaseApi/BaseAioApi/BaseAsyncApi/BaseAsyncAioApi.call): +# first package segment of the API class, e.g. aigc/audio/embeddings/ +# nlp/rerank/app/tokenizers/assistants(threads pkg)/finetune/models/files +# - task management (fetch/cancel/wait/list on /tasks/* endpoints): "tasks" +# - agentstudio: passed explicitly by agentstudio/transport.py +# - realtime/dialog WebSocket: passed explicitly where headers are built +# - acli side: TongyiProvider ctor arg, defaults to "app" SDK_MODULES = ["", "agentstudio"] ACLI_MODULES = ["", "app"] @pytest.fixture(autouse=True) def _pin_sdk_client(monkeypatch): - # CLI 测试会在同进程内把 client 改成 python-cli,这里钉住默认值 + # CLI tests switch the client to python-cli in-process; pin the default monkeypatch.setattr("dashscope.common.utils._SDK_CLIENT", "python-sdk") @@ -56,6 +60,10 @@ def test_python_sdk_client_header(): def test_acli_client_header(): # pylint: disable=protected-access print("\n=== acli: x-dashscope-sdk-client ===") + default = TongyiProvider(model="qwen-plus", api_key="sk-x") + value = default._get_headers()[CLIENT_HEADER] + print(f"module=(default app) -> {value}") + _check_client_header(value, "acli", acli_version, "app") for module in ACLI_MODULES: provider = TongyiProvider( model="qwen-plus", @@ -88,7 +96,7 @@ def test_python_cli_client_header(): value = headers[CLIENT_HEADER] print(f"\npython-cli -> {value}") _check_client_header(value, "python-cli", sdk_version) - # module 段同样适用 + # the module segment applies as well value = get_sdk_headers(module="agentstudio")[CLIENT_HEADER] print(f"python-cli -> {value}") _check_client_header(value, "python-cli", sdk_version, "agentstudio") @@ -97,8 +105,8 @@ def test_python_cli_client_header(): def test_cli_import_marks_process(): - # 子进程隔离验证:加载 dashscope.cli 后,进程内请求的 - # client 标识应变为 python-cli(与同进程其他测试的执行顺序无关) + # subprocess isolation check: after importing dashscope.cli, requests in + # that process must carry client=python-cli (independent of test order) import subprocess import sys @@ -120,5 +128,387 @@ def test_cli_import_marks_process(): print(f"\nCLI process -> {result.stdout.strip()}") +# --------------------------------------------------------------------------- +# module segment: full API coverage checks +# --------------------------------------------------------------------------- + + +def test_get_api_module_mapping(): + from dashscope.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.tokenizers.tokenization": "tokenizers", + "dashscope.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.finetune.finetunes": "finetune", + "dashscope.multimodal.tingwu.tingwu": "multimodal", + "dashscope.common.utils": "common", # non-API classes never reach this + "other.lib": "", + } + for module_name, expected in cases.items(): + assert get_api_module(module_name) == expected, module_name + + +class _CapturedRequest(Exception): + """Sentinel: transport layer captured the outgoing request headers.""" + + def __init__(self, headers): + super().__init__("captured") + self.headers = headers or {} + + +@pytest.fixture +def capture_headers(monkeypatch): + """Patch all transport boundaries to capture headers without network.""" + import aiohttp + import requests + + from dashscope.api_entities.http_request import HttpRequest + from dashscope.api_entities.websocket_request import WebSocketRequest + + def _raise(headers): + raise _CapturedRequest(headers) + + def http_call(self): + _raise(self.headers) + + async def http_aio_call(self): + _raise(self.headers) + + def ws_call(self): + _raise(self.headers) + + monkeypatch.setattr(HttpRequest, "call", http_call) + monkeypatch.setattr(HttpRequest, "aio_call", http_aio_call) + monkeypatch.setattr(WebSocketRequest, "call", ws_call) + + def session_request(self, *args, **kwargs): + _raise(kwargs.get("headers")) + + for method in ("get", "post", "delete", "put", "patch"): + monkeypatch.setattr(requests.Session, method, session_request) + + def aio_get(self, *args, **kwargs): + _raise(kwargs.get("headers")) + + monkeypatch.setattr(aiohttp.ClientSession, "get", aio_get) + + monkeypatch.setattr("dashscope.api_key", "sk-test") + return None + + +def _capture(fn): + try: + fn() + except _CapturedRequest as e: + return e.headers + raise AssertionError("no request was issued") + + +def _check_module(headers, module, label): + value = headers.get(CLIENT_HEADER) + assert value, f"{label}: missing {CLIENT_HEADER} in {headers}" + expected = f"python-sdk/{sdk_version}/{module}" + print(f"{label:<45} -> {value}") + assert value == expected, f"{label}: {value} != {expected}" + + +def test_service_api_modules(capture_headers, tmp_path): + # pylint: disable=unused-argument,import-outside-toplevel + import asyncio + + from dashscope import ( + Application, + CodeGeneration, + Conversation, + Generation, + ImageSynthesis, + MultiModalConversation, + MultiModalEmbedding, + TextEmbedding, + TextReRank, + Tokenization, + Understanding, + VideoSynthesis, + ) + from dashscope.aigc.generation import AioGeneration + from dashscope.aigc.multimodal_conversation import ( + AioMultiModalConversation, + ) + from dashscope.audio.asr.recognition import ( + Recognition, + RecognitionCallback, + ) + from dashscope.audio.asr.transcription import Transcription + from dashscope.audio.asr.translation_recognizer import ( + TranslationRecognizerCallback, + TranslationRecognizerRealtime, + ) + from dashscope.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 ( + AioMultiModalEmbedding, + ) + from dashscope.rerank.text_rerank import AioTextReRank + + audio_file = tmp_path / "a.wav" + audio_file.write_bytes(b"\x00" * 128) + + cases = [ + # aigc/ + ("aigc: Generation.call", + lambda: Generation.call(model="m", prompt="hi"), "aigc"), + ("aigc: AioGeneration.call", + lambda: asyncio.run(AioGeneration.call(model="m", prompt="hi")), + "aigc"), + ("aigc: Conversation.call", + lambda: Conversation().call(model="m", prompt="hi"), "aigc"), + ("aigc: CodeGeneration.call", + lambda: CodeGeneration.call( + model="m", + scene="custom", + message=[{"role": "user", "content": "hi"}], + ), "aigc"), + ("aigc: ImageSynthesis.async_call", + lambda: ImageSynthesis.async_call(model="m", prompt="a cat"), + "aigc"), + ("aigc: MultiModalConversation.call", + lambda: MultiModalConversation.call( + model="m", + messages=[{"role": "user", "content": [{"text": "hi"}]}], + ), "aigc"), + ("aigc: AioMultiModalConversation.call", + lambda: asyncio.run(AioMultiModalConversation.call( + model="m", + messages=[{"role": "user", "content": [{"text": "hi"}]}], + )), "aigc"), + ("aigc: VideoSynthesis.async_call", + lambda: VideoSynthesis.async_call(model="m", prompt="a cat"), + "aigc"), + # audio/ + ("audio: SpeechSynthesizer.call (ws)", + lambda: SpeechSynthesizer.call(model="m", text="hi"), "audio"), + ("audio: HttpSpeechSynthesizer.call", + lambda: HttpSpeechSynthesizer.call(model="m", text="hi", voice="v"), + "audio"), + ("audio: Transcription.async_call", + lambda: Transcription.async_call( + model="m", file_urls=["https://example.com/a.wav"], + ), "audio"), + ("audio: Recognition.call (ws)", + lambda: Recognition( + model="m", format="wav", sample_rate=16000, + callback=RecognitionCallback(), + ).call(file=str(audio_file)), "audio"), + ("audio: TranslationRecognizerRealtime.call (ws)", + lambda: TranslationRecognizerRealtime( + model="m", format="wav", sample_rate=16000, + callback=TranslationRecognizerCallback(), + ).call(file=str(audio_file)), "audio"), + ("audio: QwenTranscription.async_call", + lambda: QwenTranscription.async_call( + model="m", file_url="https://example.com/a.wav", + ), "audio"), + # embeddings/ + ("embeddings: TextEmbedding.call", + lambda: TextEmbedding.call(model="m", input="hi"), "embeddings"), + ("embeddings: BatchTextEmbedding.call", + lambda: BatchTextEmbedding.call( + model="m", url="https://example.com/input.txt", + ), "embeddings"), + ("embeddings: MultiModalEmbedding.call", + lambda: MultiModalEmbedding.call( + model="m", input=[{"text": "hi"}], + ), "embeddings"), + ("embeddings: AioMultiModalEmbedding.call", + lambda: asyncio.run(AioMultiModalEmbedding.call( + model="m", input=[{"text": "hi"}], + )), "embeddings"), + # nlp/ + ("nlp: Understanding.call", + lambda: Understanding.call(model="m", sentence="hi", labels="lbl"), + "nlp"), + # rerank/ + ("rerank: TextReRank.call", + lambda: TextReRank.call(model="m", query="q", documents=["d"]), + "rerank"), + ("rerank: AioTextReRank.call", + lambda: asyncio.run(AioTextReRank.call( + model="m", query="q", documents=["d"], + )), "rerank"), + # app/ + ("app: Application.call", + lambda: Application.call(app_id="app1", prompt="hi"), "app"), + # tokenizers/ + ("tokenizers: Tokenization.call", + lambda: Tokenization.call(model="m", prompt="hi"), "tokenizers"), + ] + print("\n=== service calls: python-sdk// ===") + for label, fn, module in cases: + _check_module(_capture(fn), module, label) + + +def test_tasks_module(capture_headers): + # pylint: disable=unused-argument,import-outside-toplevel + import asyncio + + from dashscope import ImageSynthesis + from dashscope.aigc.image_synthesis import AioImageSynthesis + from dashscope.aigc.video_synthesis import VideoSynthesis + + cases = [ + ("tasks: ImageSynthesis.fetch", + lambda: ImageSynthesis.fetch("task-1")), + ("tasks: ImageSynthesis.cancel", + lambda: ImageSynthesis.cancel("task-1")), + ("tasks: ImageSynthesis.list", lambda: ImageSynthesis.list()), + ("tasks: VideoSynthesis.fetch", + lambda: VideoSynthesis.fetch("task-1")), + ("tasks: AioImageSynthesis.fetch", + lambda: asyncio.run(AioImageSynthesis.fetch("task-1"))), + ("tasks: AioImageSynthesis.list", + lambda: asyncio.run(AioImageSynthesis.list())), + ] + print("\n=== task management (/tasks/*): module=tasks ===") + for label, fn in cases: + _check_module(_capture(fn), "tasks", label) + + +def test_resource_api_modules(capture_headers): + # pylint: disable=unused-argument,import-outside-toplevel + from dashscope import ( + Assistants, + Deployments, + Files, + FineTunes, + Messages, + Models, + Runs, + Steps, + Threads, + ) + + cases = [ + ("models: Models.list", lambda: Models.list(), "models"), + ("models: Models.get", lambda: Models.get("m"), "models"), + ("files: Files.list", lambda: Files.list(), "files"), + ("finetune: FineTunes.list", lambda: FineTunes.list(), "finetune"), + ("finetune: FineTunes.cancel", + lambda: FineTunes.cancel("job-1"), "finetune"), + ("finetune: Deployments.list", lambda: Deployments.list(), "finetune"), + ("assistants: Assistants.list", lambda: Assistants.list(), + "assistants"), + ("assistants: Threads.create", lambda: Threads.create(), + "assistants"), + ("assistants: Messages.list", + lambda: Messages.list(thread_id="t1"), "assistants"), + ("assistants: Runs.list", lambda: Runs.list(thread_id="t1"), + "assistants"), + ("assistants: Steps.list", + lambda: Steps.list(thread_id="t1", run_id="r1"), "assistants"), + ] + print("\n=== resource management APIs ===") + for label, fn, module in cases: + _check_module(_capture(fn), module, label) + + +def test_realtime_websocket_modules(): + # pylint: disable=import-outside-toplevel + import dashscope + from dashscope.audio.qwen_omni.omni_realtime import ( + OmniRealtimeCallback, + OmniRealtimeConversation, + ) + from dashscope.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 ( + _Request as DialogRequest, + ) + from dashscope.multimodal.tingwu.tingwu_realtime import ( + _Request as TingwuRequest, + ) + + dashscope.api_key = "sk-test" + + class _Cb(OmniRealtimeCallback): + pass + + cases = [ + ("audio: tts_v2 SpeechSynthesizer", + TtsV2Req(apikey="sk-test", model="m", voice="v") + .get_websocket_headers(None, None), "audio"), + ("audio: OmniRealtimeConversation", + OmniRealtimeConversation(model="m", callback=_Cb()) + ._get_websocket_header(), "audio"), + ("audio: QwenTtsRealtime", + QwenTtsRealtime(model="m")._get_websocket_header(), "audio"), + # MultiModalDialog is grouped into aigc per the mapping table + ("aigc: MultiModalDialog", + DialogRequest().get_websocket_header("sk-test"), "aigc"), + ("multimodal: TingWuRealtime", + TingwuRequest().get_websocket_header("sk-test"), "multimodal"), + ] + print("\n=== realtime/dialog WebSocket ===") + for label, headers, module in cases: + _check_module(headers, module, label) + + +def test_encryption_module(monkeypatch): + # pylint: disable=import-outside-toplevel + from dashscope.api_entities.encryption import Encryption + + def fake_get(url, headers=None, timeout=None): + raise _CapturedRequest(headers) + + monkeypatch.setattr("dashscope.api_entities.encryption.requests.get", + fake_get) + print("\n=== misc ===") + try: + Encryption._get_public_keys() # pylint: disable=protected-access + except _CapturedRequest as e: + _check_module(e.headers, "utils", "utils: Encryption public-keys") + else: + raise AssertionError("no request was issued") + + +def test_finetune_rl_module(monkeypatch): + # pylint: disable=import-outside-toplevel + import asyncio + + import dashscope.finetune.reinforcement.common.utils as rl_utils + + captured = {} + + async def fake_async_http_request(**kwargs): + captured.update(kwargs) + return {} + + monkeypatch.setattr(rl_utils, "async_http_request", + fake_async_http_request) + asyncio.run( + rl_utils.client_fc(api_key="sk-test", url="http://x", request_data={}) + ) + _check_module(captured.get("headers"), "finetune", + "finetune: rl client_fc") + + if __name__ == "__main__": pytest.main([__file__, "-v", "-s"]) From dbee0b52deea506da757cf1a2028b5c61e2b2ebe Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 3 Sep 2026 19:58:35 +0800 Subject: [PATCH 6/7] style: apply pre-commit hooks (black, add-trailing-comma, pylint) --- dashscope/client/base_api.py | 30 ++- tests/unit/test_sdk_headers.py | 402 ++++++++++++++++++++++----------- 2 files changed, 295 insertions(+), 137 deletions(-) diff --git a/dashscope/client/base_api.py b/dashscope/client/base_api.py index c36336e..5ed1474 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/client/base_api.py @@ -1236,7 +1236,10 @@ def delete( url, headers={ **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1286,7 +1289,10 @@ def call( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1421,7 +1427,10 @@ def put( headers={ "Content-Type": "application/json; charset=utf-8", **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1473,7 +1482,10 @@ def upload( # pylint: disable=unused-argument data=js, headers={ **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, files=files, @@ -1523,7 +1535,10 @@ def cancel( url, headers={ **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, timeout=timeout, @@ -1630,7 +1645,10 @@ def stream_events( url, headers={ **_workspace_header(workspace), - **default_headers(api_key, module=get_api_module(cls.__module__)), + **default_headers( + api_key, + module=get_api_module(cls.__module__), + ), **kwargs.pop("headers", {}), }, stream=True, diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index b2211ec..607ab0d 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -172,6 +172,7 @@ def __init__(self, headers): @pytest.fixture def capture_headers(monkeypatch): """Patch all transport boundaries to capture headers without network.""" + # pylint: disable=unused-argument import aiohttp import requests @@ -206,7 +207,6 @@ def aio_get(self, *args, **kwargs): monkeypatch.setattr(aiohttp.ClientSession, "get", aio_get) monkeypatch.setattr("dashscope.api_key", "sk-test") - return None def _capture(fn): @@ -226,7 +226,7 @@ def _check_module(headers, module, label): def test_service_api_modules(capture_headers, tmp_path): - # pylint: disable=unused-argument,import-outside-toplevel + # pylint: disable=unused-argument,import-outside-toplevel,redefined-outer-name import asyncio from dashscope import ( @@ -272,92 +272,176 @@ def test_service_api_modules(capture_headers, tmp_path): cases = [ # aigc/ - ("aigc: Generation.call", - lambda: Generation.call(model="m", prompt="hi"), "aigc"), - ("aigc: AioGeneration.call", - lambda: asyncio.run(AioGeneration.call(model="m", prompt="hi")), - "aigc"), - ("aigc: Conversation.call", - lambda: Conversation().call(model="m", prompt="hi"), "aigc"), - ("aigc: CodeGeneration.call", - lambda: CodeGeneration.call( - model="m", - scene="custom", - message=[{"role": "user", "content": "hi"}], - ), "aigc"), - ("aigc: ImageSynthesis.async_call", - lambda: ImageSynthesis.async_call(model="m", prompt="a cat"), - "aigc"), - ("aigc: MultiModalConversation.call", - lambda: MultiModalConversation.call( - model="m", - messages=[{"role": "user", "content": [{"text": "hi"}]}], - ), "aigc"), - ("aigc: AioMultiModalConversation.call", - lambda: asyncio.run(AioMultiModalConversation.call( - model="m", - messages=[{"role": "user", "content": [{"text": "hi"}]}], - )), "aigc"), - ("aigc: VideoSynthesis.async_call", - lambda: VideoSynthesis.async_call(model="m", prompt="a cat"), - "aigc"), + ( + "aigc: Generation.call", + lambda: Generation.call(model="m", prompt="hi"), + "aigc", + ), + ( + "aigc: AioGeneration.call", + lambda: asyncio.run(AioGeneration.call(model="m", prompt="hi")), + "aigc", + ), + ( + "aigc: Conversation.call", + lambda: Conversation().call(model="m", prompt="hi"), + "aigc", + ), + ( + "aigc: CodeGeneration.call", + lambda: CodeGeneration.call( + model="m", + scene="custom", + message=[{"role": "user", "content": "hi"}], + ), + "aigc", + ), + ( + "aigc: ImageSynthesis.async_call", + lambda: ImageSynthesis.async_call(model="m", prompt="a cat"), + "aigc", + ), + ( + "aigc: MultiModalConversation.call", + lambda: MultiModalConversation.call( + model="m", + messages=[{"role": "user", "content": [{"text": "hi"}]}], + ), + "aigc", + ), + ( + "aigc: AioMultiModalConversation.call", + lambda: asyncio.run( + AioMultiModalConversation.call( + model="m", + messages=[{"role": "user", "content": [{"text": "hi"}]}], + ), + ), + "aigc", + ), + ( + "aigc: VideoSynthesis.async_call", + lambda: VideoSynthesis.async_call(model="m", prompt="a cat"), + "aigc", + ), # audio/ - ("audio: SpeechSynthesizer.call (ws)", - lambda: SpeechSynthesizer.call(model="m", text="hi"), "audio"), - ("audio: HttpSpeechSynthesizer.call", - lambda: HttpSpeechSynthesizer.call(model="m", text="hi", voice="v"), - "audio"), - ("audio: Transcription.async_call", - lambda: Transcription.async_call( - model="m", file_urls=["https://example.com/a.wav"], - ), "audio"), - ("audio: Recognition.call (ws)", - lambda: Recognition( - model="m", format="wav", sample_rate=16000, - callback=RecognitionCallback(), - ).call(file=str(audio_file)), "audio"), - ("audio: TranslationRecognizerRealtime.call (ws)", - lambda: TranslationRecognizerRealtime( - model="m", format="wav", sample_rate=16000, - callback=TranslationRecognizerCallback(), - ).call(file=str(audio_file)), "audio"), - ("audio: QwenTranscription.async_call", - lambda: QwenTranscription.async_call( - model="m", file_url="https://example.com/a.wav", - ), "audio"), + ( + "audio: SpeechSynthesizer.call (ws)", + lambda: SpeechSynthesizer.call(model="m", text="hi"), + "audio", + ), + ( + "audio: HttpSpeechSynthesizer.call", + lambda: HttpSpeechSynthesizer.call( + model="m", + text="hi", + voice="v", + ), + "audio", + ), + ( + "audio: Transcription.async_call", + lambda: Transcription.async_call( + model="m", + file_urls=["https://example.com/a.wav"], + ), + "audio", + ), + ( + "audio: Recognition.call (ws)", + lambda: Recognition( + model="m", + format="wav", + sample_rate=16000, + callback=RecognitionCallback(), + ).call(file=str(audio_file)), + "audio", + ), + ( + "audio: TranslationRecognizerRealtime.call (ws)", + lambda: TranslationRecognizerRealtime( + model="m", + format="wav", + sample_rate=16000, + callback=TranslationRecognizerCallback(), + ).call(file=str(audio_file)), + "audio", + ), + ( + "audio: QwenTranscription.async_call", + lambda: QwenTranscription.async_call( + model="m", + file_url="https://example.com/a.wav", + ), + "audio", + ), # embeddings/ - ("embeddings: TextEmbedding.call", - lambda: TextEmbedding.call(model="m", input="hi"), "embeddings"), - ("embeddings: BatchTextEmbedding.call", - lambda: BatchTextEmbedding.call( - model="m", url="https://example.com/input.txt", - ), "embeddings"), - ("embeddings: MultiModalEmbedding.call", - lambda: MultiModalEmbedding.call( - model="m", input=[{"text": "hi"}], - ), "embeddings"), - ("embeddings: AioMultiModalEmbedding.call", - lambda: asyncio.run(AioMultiModalEmbedding.call( - model="m", input=[{"text": "hi"}], - )), "embeddings"), + ( + "embeddings: TextEmbedding.call", + lambda: TextEmbedding.call(model="m", input="hi"), + "embeddings", + ), + ( + "embeddings: BatchTextEmbedding.call", + lambda: BatchTextEmbedding.call( + model="m", + url="https://example.com/input.txt", + ), + "embeddings", + ), + ( + "embeddings: MultiModalEmbedding.call", + lambda: MultiModalEmbedding.call( + model="m", + input=[{"text": "hi"}], + ), + "embeddings", + ), + ( + "embeddings: AioMultiModalEmbedding.call", + lambda: asyncio.run( + AioMultiModalEmbedding.call( + model="m", + input=[{"text": "hi"}], + ), + ), + "embeddings", + ), # nlp/ - ("nlp: Understanding.call", - lambda: Understanding.call(model="m", sentence="hi", labels="lbl"), - "nlp"), + ( + "nlp: Understanding.call", + lambda: Understanding.call(model="m", sentence="hi", labels="lbl"), + "nlp", + ), # rerank/ - ("rerank: TextReRank.call", - lambda: TextReRank.call(model="m", query="q", documents=["d"]), - "rerank"), - ("rerank: AioTextReRank.call", - lambda: asyncio.run(AioTextReRank.call( - model="m", query="q", documents=["d"], - )), "rerank"), + ( + "rerank: TextReRank.call", + lambda: TextReRank.call(model="m", query="q", documents=["d"]), + "rerank", + ), + ( + "rerank: AioTextReRank.call", + lambda: asyncio.run( + AioTextReRank.call( + model="m", + query="q", + documents=["d"], + ), + ), + "rerank", + ), # app/ - ("app: Application.call", - lambda: Application.call(app_id="app1", prompt="hi"), "app"), + ( + "app: Application.call", + lambda: Application.call(app_id="app1", prompt="hi"), + "app", + ), # tokenizers/ - ("tokenizers: Tokenization.call", - lambda: Tokenization.call(model="m", prompt="hi"), "tokenizers"), + ( + "tokenizers: Tokenization.call", + lambda: Tokenization.call(model="m", prompt="hi"), + "tokenizers", + ), ] print("\n=== service calls: python-sdk// ===") for label, fn, module in cases: @@ -365,7 +449,7 @@ def test_service_api_modules(capture_headers, tmp_path): def test_tasks_module(capture_headers): - # pylint: disable=unused-argument,import-outside-toplevel + # pylint: disable=unused-argument,import-outside-toplevel,redefined-outer-name import asyncio from dashscope import ImageSynthesis @@ -373,17 +457,27 @@ def test_tasks_module(capture_headers): from dashscope.aigc.video_synthesis import VideoSynthesis cases = [ - ("tasks: ImageSynthesis.fetch", - lambda: ImageSynthesis.fetch("task-1")), - ("tasks: ImageSynthesis.cancel", - lambda: ImageSynthesis.cancel("task-1")), - ("tasks: ImageSynthesis.list", lambda: ImageSynthesis.list()), - ("tasks: VideoSynthesis.fetch", - lambda: VideoSynthesis.fetch("task-1")), - ("tasks: AioImageSynthesis.fetch", - lambda: asyncio.run(AioImageSynthesis.fetch("task-1"))), - ("tasks: AioImageSynthesis.list", - lambda: asyncio.run(AioImageSynthesis.list())), + ( + "tasks: ImageSynthesis.fetch", + lambda: ImageSynthesis.fetch("task-1"), + ), + ( + "tasks: ImageSynthesis.cancel", + lambda: ImageSynthesis.cancel("task-1"), + ), + ("tasks: ImageSynthesis.list", ImageSynthesis.list), + ( + "tasks: VideoSynthesis.fetch", + lambda: VideoSynthesis.fetch("task-1"), + ), + ( + "tasks: AioImageSynthesis.fetch", + lambda: asyncio.run(AioImageSynthesis.fetch("task-1")), + ), + ( + "tasks: AioImageSynthesis.list", + lambda: asyncio.run(AioImageSynthesis.list()), + ), ] print("\n=== task management (/tasks/*): module=tasks ===") for label, fn in cases: @@ -391,7 +485,7 @@ def test_tasks_module(capture_headers): def test_resource_api_modules(capture_headers): - # pylint: disable=unused-argument,import-outside-toplevel + # pylint: disable=unused-argument,import-outside-toplevel,redefined-outer-name from dashscope import ( Assistants, Deployments, @@ -405,23 +499,41 @@ def test_resource_api_modules(capture_headers): ) cases = [ - ("models: Models.list", lambda: Models.list(), "models"), + ("models: Models.list", Models.list, "models"), ("models: Models.get", lambda: Models.get("m"), "models"), - ("files: Files.list", lambda: Files.list(), "files"), - ("finetune: FineTunes.list", lambda: FineTunes.list(), "finetune"), - ("finetune: FineTunes.cancel", - lambda: FineTunes.cancel("job-1"), "finetune"), - ("finetune: Deployments.list", lambda: Deployments.list(), "finetune"), - ("assistants: Assistants.list", lambda: Assistants.list(), - "assistants"), - ("assistants: Threads.create", lambda: Threads.create(), - "assistants"), - ("assistants: Messages.list", - lambda: Messages.list(thread_id="t1"), "assistants"), - ("assistants: Runs.list", lambda: Runs.list(thread_id="t1"), - "assistants"), - ("assistants: Steps.list", - lambda: Steps.list(thread_id="t1", run_id="r1"), "assistants"), + ("files: Files.list", Files.list, "files"), + ("finetune: FineTunes.list", FineTunes.list, "finetune"), + ( + "finetune: FineTunes.cancel", + lambda: FineTunes.cancel("job-1"), + "finetune", + ), + ("finetune: Deployments.list", Deployments.list, "finetune"), + ( + "assistants: Assistants.list", + Assistants.list, + "assistants", + ), + ( + "assistants: Threads.create", + Threads.create, + "assistants", + ), + ( + "assistants: Messages.list", + lambda: Messages.list(thread_id="t1"), + "assistants", + ), + ( + "assistants: Runs.list", + lambda: Runs.list(thread_id="t1"), + "assistants", + ), + ( + "assistants: Steps.list", + lambda: Steps.list(thread_id="t1", run_id="r1"), + "assistants", + ), ] print("\n=== resource management APIs ===") for label, fn, module in cases: @@ -429,7 +541,7 @@ def test_resource_api_modules(capture_headers): def test_realtime_websocket_modules(): - # pylint: disable=import-outside-toplevel + # pylint: disable=import-outside-toplevel,protected-access import dashscope from dashscope.audio.qwen_omni.omni_realtime import ( OmniRealtimeCallback, @@ -452,19 +564,39 @@ class _Cb(OmniRealtimeCallback): pass cases = [ - ("audio: tts_v2 SpeechSynthesizer", - TtsV2Req(apikey="sk-test", model="m", voice="v") - .get_websocket_headers(None, None), "audio"), - ("audio: OmniRealtimeConversation", - OmniRealtimeConversation(model="m", callback=_Cb()) - ._get_websocket_header(), "audio"), - ("audio: QwenTtsRealtime", - QwenTtsRealtime(model="m")._get_websocket_header(), "audio"), + ( + "audio: tts_v2 SpeechSynthesizer", + TtsV2Req( + apikey="sk-test", + model="m", + voice="v", + ).get_websocket_headers(None, None), + "audio", + ), + ( + "audio: OmniRealtimeConversation", + OmniRealtimeConversation( + model="m", + callback=_Cb(), + )._get_websocket_header(), + "audio", + ), + ( + "audio: QwenTtsRealtime", + QwenTtsRealtime(model="m")._get_websocket_header(), + "audio", + ), # MultiModalDialog is grouped into aigc per the mapping table - ("aigc: MultiModalDialog", - DialogRequest().get_websocket_header("sk-test"), "aigc"), - ("multimodal: TingWuRealtime", - TingwuRequest().get_websocket_header("sk-test"), "multimodal"), + ( + "aigc: MultiModalDialog", + DialogRequest().get_websocket_header("sk-test"), + "aigc", + ), + ( + "multimodal: TingWuRealtime", + TingwuRequest().get_websocket_header("sk-test"), + "multimodal", + ), ] print("\n=== realtime/dialog WebSocket ===") for label, headers, module in cases: @@ -478,8 +610,10 @@ def test_encryption_module(monkeypatch): def fake_get(url, headers=None, timeout=None): raise _CapturedRequest(headers) - monkeypatch.setattr("dashscope.api_entities.encryption.requests.get", - fake_get) + monkeypatch.setattr( + "dashscope.api_entities.encryption.requests.get", + fake_get, + ) print("\n=== misc ===") try: Encryption._get_public_keys() # pylint: disable=protected-access @@ -501,13 +635,19 @@ async def fake_async_http_request(**kwargs): captured.update(kwargs) return {} - monkeypatch.setattr(rl_utils, "async_http_request", - fake_async_http_request) + monkeypatch.setattr( + rl_utils, + "async_http_request", + fake_async_http_request, + ) asyncio.run( - rl_utils.client_fc(api_key="sk-test", url="http://x", request_data={}) + rl_utils.client_fc(api_key="sk-test", url="http://x", request_data={}), + ) + _check_module( + captured.get("headers"), + "finetune", + "finetune: rl client_fc", ) - _check_module(captured.get("headers"), "finetune", - "finetune: rl client_fc") if __name__ == "__main__": From 13a16db752b8762063c46e899a22b71774504f4b Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Fri, 4 Sep 2026 10:39:23 +0800 Subject: [PATCH 7/7] chore(version): bump version to 1.27.4 --- dashscope/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashscope/version.py b/dashscope/version.py index 7ad7e41..dbdebdc 100644 --- a/dashscope/version.py +++ b/dashscope/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -__version__ = "1.27.3" +__version__ = "1.27.4"