Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions LICENSE.certifi
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
This package contains a modified version of ca-bundle.crt:

ca-bundle.crt -- Bundle of CA Root Certificates

This is a bundle of X.509 certificates of public Certificate Authorities
(CA). These were automatically extracted from Mozilla's root certificates
file (certdata.txt). This file can be found in the mozilla source tree:
https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt
It contains the certificates in PEM format and therefore
can be directly used with curl / libcurl / php_curl, or with
an Apache+mod_ssl webserver for SSL client authentication.
Just configure this file as the SSLCACertificateFile.#

***** BEGIN LICENSE BLOCK *****
This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
one at http://mozilla.org/MPL/2.0/.

***** END LICENSE BLOCK *****
@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
include dashscope/resources/qwen.tiktoken
include NOTICE
include LICENSE.certifi
3 changes: 3 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This product depends on certifi (https://github.com/certifi/python-certifi),
which is provided under the Mozilla Public License 2.0.
See bundled LICENSE.certifi for details.
50 changes: 49 additions & 1 deletion dashscope/audio/http_tts/http_speech_synthesizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@


class HttpSpeechSynthesisResult:
"""The result of HTTP speech synthesis."""
"""The result of HTTP speech synthesis.

This class wraps the actual SpeechSynthesisResponse and provides
convenient access to both synthesis-specific data and standard
DashScopeAPIResponse attributes for compatibility with CLI tools.
"""

def __init__(
self,
Expand Down Expand Up @@ -62,6 +67,49 @@ def response(self) -> Optional[SpeechSynthesisResponse]:
"""Get the full API response."""
return self._response

# Proxy standard DashScopeAPIResponse attributes for CLI compatibility
@property
def request_id(self) -> str:
"""Get the request ID from the underlying response."""
if self._response:
return self._response.request_id
return ""

@property
def status_code(self) -> int:
"""Get the HTTP status code from the underlying response."""
if self._response:
return self._response.status_code
return 0

@property
def code(self) -> str:
"""Get the error code from the underlying response."""
if self._response:
return self._response.code
return ""

@property
def message(self) -> str:
"""Get the error message from the underlying response."""
if self._response:
return self._response.message
return ""

@property
def output(self):
"""Get the output from the underlying response."""
if self._response:
return self._response.output
return None

@property
def usage(self):
"""Get the usage from the underlying response."""
if self._response:
return self._response.usage
return None


class HttpSpeechSynthesizer(BaseApi):
"""HTTP-based text-to-speech interface for CosyVoice."""
Expand Down
6 changes: 0 additions & 6 deletions dashscope/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from dashscope.common.error import AuthenticationError # noqa: E402
from dashscope.cli import ( # noqa: E402
application,
code_generation,
deployments,
embeddings,
files,
Expand All @@ -39,7 +38,6 @@
speech_synthesis,
tokenization,
transcription,
understanding,
video_synthesis,
)

Expand Down Expand Up @@ -97,9 +95,7 @@
"embeddings",
"tokenization",
"models",
"understanding",
"application",
"code-generation",
"image-synthesis",
"video-synthesis",
"image-generation",
Expand Down Expand Up @@ -276,9 +272,7 @@ def callback(
app.add_typer(embeddings.app)
app.add_typer(tokenization.app)
app.add_typer(models.app)
app.add_typer(understanding.app)
app.add_typer(application.app)
app.add_typer(code_generation.app)
app.add_typer(image_synthesis.app)
app.add_typer(video_synthesis.app)
app.add_typer(image_generation.app)
Expand Down
15 changes: 14 additions & 1 deletion dashscope/cli/agentic_rl.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ def run(
description="[green]✅ Job submitted successfully![/green]",
)

# Validate output is not None before accessing attributes
if result.output is None:
raise OutputError("API returned success but output is empty")

format_output(
{
"job_id": result.output.job_id,
Expand Down Expand Up @@ -597,6 +601,10 @@ def get(
f"API returned status {result.status_code}: {result.message}",
)

# Validate output is not None before accessing attributes
if result.output is None:
raise OutputError("API returned success but output is empty")

format_output(
{
"job_id": result.output.job_id,
Expand Down Expand Up @@ -669,8 +677,13 @@ def logs(
f"API returned status {result.status_code}: {result.message}",
)

# Validate output is not None before accessing attributes
logs_data = ""
if result.output is not None and isinstance(result.output, dict):
logs_data = result.output.get("logs", "")

format_output(
{"job_id": job_id, "logs": getattr(result.output, "logs", "")},
{"job_id": job_id, "logs": logs_data},
fmt=output_format,
)
except Exception as e:
Expand Down
5 changes: 3 additions & 2 deletions dashscope/cli/code_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def callback(ctx: typer.Context):
typer.echo(ctx.get_help())


@app.command("create")
@app.command("create", hidden=True)
@handle_sdk_error("Code generation request failed")
def create(
model: str = typer.Option(..., "-m", "--model", help="The model to call"),
Expand Down Expand Up @@ -56,7 +56,8 @@ def create(
),
n: int = typer.Option(1, "-n", "--n", help="The number of output results"),
):
"""Call code generation API."""
"""[DEPRECATED] No independent endpoint available.
This command is hidden and will be removed."""
messages = [UserRoleMessageParam(content=content)]
if attachment_meta is not None:
try:
Expand Down
47 changes: 42 additions & 5 deletions dashscope/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,54 @@ def print_failed_message(rsp):
)


def ensure_ok(rsp):
def ensure_ok(rsp, check_business_error: bool = True):
"""Return *rsp.output* when the response is OK; otherwise print the error
and exit with code 1.

This eliminates the repetitive ``if rsp.status_code == OK … else …``
pattern that appears in every command handler.

Enhanced to check both HTTP status and business-level error codes:
- HTTP 200 but InvalidParameter → still treated as failure
- HTTP 4xx/5xx → clear error message

Args:
rsp: The API response object
check_business_error: If True (default), check for business-level
error codes in the output. Set to False for
async task creation where we only care about
HTTP success, not task execution.
"""
if rsp.status_code == HTTPStatus.OK:
return rsp.output
print_failed_message(rsp)
raise typer.Exit(1)
if rsp.status_code != HTTPStatus.OK:
print_failed_message(rsp)
raise typer.Exit(1)

# Check for business-level errors even when HTTP status is 200
output = rsp.output
if output is None:
print_failed_message(rsp)
raise typer.Exit(1)

# Only check business-level errors if explicitly requested
if check_business_error:
# Some APIs return error info in output even with HTTP 200
if isinstance(output, dict):
error_code = output.get("code")
message = output.get("message", "Unknown error")
else:
error_code = getattr(output, "code", None)
message = getattr(output, "message", "Unknown error")

if error_code and error_code != "":
err_console.print(
f"[red]Business Error[/red] request_id: {rsp.request_id}, "
f"status_code: {rsp.status_code}, "
f"code: {error_code}, "
f"message: {message}",
)
raise typer.Exit(1)

return output


def success(message: str):
Expand Down
83 changes: 72 additions & 11 deletions dashscope/cli/deployments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""``deployments`` sub-command group."""
import time
from http import HTTPStatus
from typing import Optional

import typer
Expand All @@ -12,8 +13,10 @@
console,
err_console,
ensure_ok,
error,
handle_sdk_error,
logger,
print_failed_message,
success,
)

Expand All @@ -37,12 +40,34 @@ def callback(ctx: typer.Context):
# ---------------------------------------------------------------------------


def _wait_for_deployment(deployed_model: str):
"""Block until the deployment reaches a non-pending state."""
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_WAIT_TIMEOUT = 3600 # 1 hour default timeout for waiting


def _wait_for_deployment(
deployed_model: str,
timeout: int = DEFAULT_WAIT_TIMEOUT,
):
"""Block until the deployment reaches a non-pending state or times out."""
start_time = time.time()
try:
while True:
# Check timeout
elapsed = time.time() - start_time
if elapsed > timeout:
err_console.print(
"[red]Timeout:[/red] Deployment "
f"{deployed_model} did not complete within "
f"{timeout} seconds. You can check status later via: "
f"[cyan]dashscope deployments get {deployed_model}[/cyan]",
)
raise typer.Exit(1)

rsp = dashscope.Deployments.get(deployed_model)
output = ensure_ok(rsp)
# During polling, only check HTTP success, not business errors
output = ensure_ok(rsp, check_business_error=False)
status = output.status

if status in (
Expand All @@ -67,7 +92,12 @@ def _wait_for_deployment(deployed_model: str):

def _print_deployments(output):
"""Pretty-print a list of deployments from *output*."""
if output is None or not output.deployments:
if (
output is None
or not isinstance(output, dict)
or "deployments" not in output
or not output["deployments"]
):
console.print("There is no deployed model!")
return
for dep in output.deployments:
Expand Down Expand Up @@ -99,15 +129,46 @@ def create(
"--capacity",
help="The target capacity",
),
plan: Optional[str] = typer.Option(
None,
"--plan",
help="Deployment plan or template ID",
),
template_id: Optional[str] = typer.Option(
None,
"--template-id",
help="Template ID for deployment configuration",
),
):
"""Create a model deployment."""
rsp = dashscope.Deployments.call(
model=model,
capacity=capacity,
suffix=suffix, # type: ignore[arg-type]
)
output = ensure_ok(rsp)
deployed_model = output.deployed_model
kwargs = {
"model": model,
"capacity": capacity,
"suffix": suffix,
}
if plan is not None:
kwargs["plan"] = plan
if template_id is not None:
kwargs["template_id"] = template_id

rsp = dashscope.Deployments.call(**kwargs)

# Enhanced error checking: verify both HTTP status and response content
if rsp.status_code != HTTPStatus.OK:
print_failed_message(rsp)
raise typer.Exit(1)

output = rsp.output
if output is None:
error("Deployment creation returned empty response")

deployed_model = output.get("deployed_model")
if not deployed_model:
error(
"Deployment creation succeeded but missing deployed_model "
f"in response. Response: {output}",
)

success(f"Create model: {deployed_model} deployment")
_wait_for_deployment(deployed_model)

Expand Down
9 changes: 8 additions & 1 deletion dashscope/cli/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def upload(
file_path = os.path.expanduser(file)
if not os.path.exists(file_path):
error(f"File {file_path} does not exist")
return # unreachable, but makes intent clear

rsp = dashscope.Files.upload(
file_path=file_path,
Expand All @@ -71,7 +72,13 @@ def upload(
base_address=base_url,
)
output = ensure_ok(rsp)
file_id = output["uploaded_files"][0]["file_id"]

# Validate uploaded_files exists and is not empty
uploaded_files = output.get("uploaded_files", [])
if not uploaded_files:
error("Upload succeeded but no file_id returned in response")

file_id = uploaded_files[0]["file_id"]
success(f"Upload success, file id: {file_id}")


Expand Down
Loading
Loading