From bf6a0dcf7150f01f7135d1e6d5d23f043a3d93a6 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 7 Jul 2026 13:44:06 +0800 Subject: [PATCH 1/8] feat(cli): enhance CLI error handling and robustness --- dashscope/cli/__init__.py | 6 -- dashscope/cli/agentic_rl.py | 15 ++++- dashscope/cli/code_generation.py | 5 +- dashscope/cli/common.py | 30 ++++++++-- dashscope/cli/deployments.py | 74 ++++++++++++++++++++++--- dashscope/cli/files.py | 9 ++- dashscope/cli/fine_tunes.py | 63 ++++++++++++++++++--- dashscope/cli/speech_synthesis.py | 20 ++++++- dashscope/cli/understanding.py | 5 +- tests/unit/test_cli_speech_synthesis.py | 1 + 10 files changed, 194 insertions(+), 34 deletions(-) diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index 276e805..f742257 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -23,7 +23,6 @@ from dashscope.common.error import AuthenticationError # noqa: E402 from dashscope.cli import ( # noqa: E402 application, - code_generation, deployments, embeddings, files, @@ -39,7 +38,6 @@ speech_synthesis, tokenization, transcription, - understanding, video_synthesis, ) @@ -97,9 +95,7 @@ "embeddings", "tokenization", "models", - "understanding", "application", - "code-generation", "image-synthesis", "video-synthesis", "image-generation", @@ -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) diff --git a/dashscope/cli/agentic_rl.py b/dashscope/cli/agentic_rl.py index ece3953..cd8386c 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/cli/agentic_rl.py @@ -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, @@ -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, @@ -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": result.output.get("logs", "")}, + {"job_id": job_id, "logs": logs_data}, fmt=output_format, ) except Exception as e: diff --git a/dashscope/cli/code_generation.py b/dashscope/cli/code_generation.py index 4eb2cad..414941f 100644 --- a/dashscope/cli/code_generation.py +++ b/dashscope/cli/code_generation.py @@ -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"), @@ -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: diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index b4e7e66..8bd26b1 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -47,11 +47,33 @@ def ensure_ok(rsp): 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 """ - 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) + + # Some APIs return error info in output even with HTTP 200 + error_code = output.get("code") if isinstance(output, dict) else None + 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: {output.get('message', 'Unknown error')}", + ) + raise typer.Exit(1) + + return output def success(message: str): diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index d17bdd0..4514b9e 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/cli/deployments.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """``deployments`` sub-command group.""" import time +from http import HTTPStatus from typing import Optional import typer @@ -12,8 +13,10 @@ console, err_console, ensure_ok, + error, handle_sdk_error, logger, + print_failed_message, success, ) @@ -37,10 +40,31 @@ 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) status = output["status"] @@ -69,6 +93,7 @@ def _print_deployments(output): """Pretty-print a list of deployments from *output*.""" if ( output is None + or not isinstance(output, dict) or "deployments" not in output or not output["deployments"] ): @@ -103,15 +128,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) diff --git a/dashscope/cli/files.py b/dashscope/cli/files.py index 40e28ee..32fdd9a 100644 --- a/dashscope/cli/files.py +++ b/dashscope/cli/files.py @@ -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, @@ -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}") diff --git a/dashscope/cli/fine_tunes.py b/dashscope/cli/fine_tunes.py index cdd44df..85b100c 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/cli/fine_tunes.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """``fine-tunes`` sub-command group.""" import time +from http import HTTPStatus from typing import Optional, List import typer @@ -13,6 +14,7 @@ console, err_console, ensure_ok, + error, handle_sdk_error, logger, print_failed_message, @@ -39,10 +41,28 @@ def callback(ctx: typer.Context): # --------------------------------------------------------------------------- -def _wait_for_job(job_id: str): - """Block until the fine-tune job reaches a terminal state.""" +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +DEFAULT_WAIT_TIMEOUT = 3600 # 1 hour default timeout for waiting + + +def _wait_for_job(job_id: str, timeout: int = DEFAULT_WAIT_TIMEOUT): + """Block until the fine-tune job reaches a terminal 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] Job " + f"{job_id} did not complete within " + f"{timeout} seconds. You can check status later via: " + f"[cyan]dashscope fine-tunes get {job_id}[/cyan]", + ) + raise typer.Exit(1) + rsp = dashscope.FineTunes.get(job_id) output = ensure_ok(rsp) status = output["status"] @@ -86,12 +106,21 @@ def _stream_events(job_id: str): print_failed_message(rsp) return - if rsp.output["status"] in ( + # Validate output is not None and is a dict before accessing + if rsp.output is None or not isinstance(rsp.output, dict): + err_console.print( + f"[red]Error:[/red] Invalid response for job {job_id}. " + f"Request ID: {rsp.request_id}", + ) + return + + status = rsp.output.get("status") + if status in ( TaskStatus.FAILED, TaskStatus.CANCELED, TaskStatus.SUCCEEDED, ): - console.print(f"Fine-tune job: {job_id} is {rsp.output['status']}") + console.print(f"Fine-tune job: {job_id} is {status}") _dump_logs(job_id) return @@ -120,9 +149,12 @@ def _dump_logs(job_id: str): line=LOG_PAGE_SIZE, ) output = ensure_ok(rsp) - for line in output["logs"]: + logs = output.get("logs", []) + if not logs: + break + for line in logs: console.print(line, highlight=False) - if len(output["logs"]) < LOG_PAGE_SIZE: + if len(logs) < LOG_PAGE_SIZE: break offset += LOG_PAGE_SIZE @@ -201,8 +233,23 @@ def create( mode=mode, # type: ignore[arg-type] hyper_parameters=params if params else None, # type: ignore[arg-type] ) - output = ensure_ok(rsp) - job_id = output["job_id"] + + # Enhanced error checking with detailed validation + if rsp.status_code != HTTPStatus.OK: + print_failed_message(rsp) + raise typer.Exit(1) + + output = rsp.output + if output is None: + error("Fine-tune creation returned empty response") + + job_id = output.get("job_id") + if not job_id: + error( + "Fine-tune creation succeeded but missing job_id in response. " + f"Response: {output}", + ) + success(f"Create fine-tune job success, job_id: {job_id}") _wait_for_job(job_id) diff --git a/dashscope/cli/speech_synthesis.py b/dashscope/cli/speech_synthesis.py index dc04dac..6978c39 100644 --- a/dashscope/cli/speech_synthesis.py +++ b/dashscope/cli/speech_synthesis.py @@ -1,12 +1,18 @@ # -*- coding: utf-8 -*- """``speech-synthesis`` sub-command group.""" import json +from http import HTTPStatus from typing import Optional import typer import dashscope -from dashscope.cli.common import console, handle_sdk_error +from dashscope.cli.common import ( + console, + error, + handle_sdk_error, + print_failed_message, +) app = typer.Typer( name="speech-synthesis", @@ -75,6 +81,18 @@ def create( rate=rate, pitch=pitch, ) + + # Validate response status and required fields + if ( + not hasattr(result, "status_code") + or result.status_code != HTTPStatus.OK + ): + print_failed_message(result) + raise typer.Exit(1) + + if not result.audio_url: + error("Speech synthesis succeeded but missing audio_url in response") + output = { "audio_url": result.audio_url, "audio_id": result.audio_id, diff --git a/dashscope/cli/understanding.py b/dashscope/cli/understanding.py index f7c2dd8..30fdfca 100644 --- a/dashscope/cli/understanding.py +++ b/dashscope/cli/understanding.py @@ -23,7 +23,7 @@ def callback(ctx: typer.Context): typer.echo(ctx.get_help()) -@app.command("create") +@app.command("create", hidden=True) @handle_sdk_error("Understanding request failed") def create( model: str = typer.Option(..., "-m", "--model", help="The model to call"), @@ -46,7 +46,8 @@ def create( help="Task type, such as extraction or classification", ), ): - """Call natural language understanding API.""" + """[DEPRECATED] OpenNLU endpoint is offline. + This command is hidden and will be removed.""" response = dashscope.Understanding.call( model=model, sentence=sentence, diff --git a/tests/unit/test_cli_speech_synthesis.py b/tests/unit/test_cli_speech_synthesis.py index 2351ced..efb7fd1 100644 --- a/tests/unit/test_cli_speech_synthesis.py +++ b/tests/unit/test_cli_speech_synthesis.py @@ -15,6 +15,7 @@ def test_create(self, monkeypatch): def mock_call(**kwargs): captured_request.update(kwargs) return SimpleNamespace( + status_code=200, audio_url="https://example.com/audio.wav", audio_id="audio-1234", expires_at=1893456000, From 7f4b318e197ec26015b45a4acf4f6565a9e9fef9 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 7 Jul 2026 15:34:42 +0800 Subject: [PATCH 2/8] feat: add DASHSCOPE protocol and update samples/tests - Add DASHSCOPE to ModelProtocol enum for native DashScope API support - Update agentic RL tests to use DashScope protocol instead of OpenAI - Add clarifying comments for qwen3-asr-flash model in samples - Remove unnecessary base_address override in tingwu sample Co-Authored-By: Claude Opus 4.7 --- .../reinforcement/component/data/base_data_model.py | 1 + samples/test_aio_multimodal_conversation.py | 1 + samples/test_multimodal_conversation.py | 1 + samples/test_qwen_asr.py | 1 + samples/test_tingwu_usages.py | 1 - tests/unit/test_agentic_rl_components.py | 7 ++++--- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dashscope/finetune/reinforcement/component/data/base_data_model.py b/dashscope/finetune/reinforcement/component/data/base_data_model.py index 8cc56e2..b09b4f9 100644 --- a/dashscope/finetune/reinforcement/component/data/base_data_model.py +++ b/dashscope/finetune/reinforcement/component/data/base_data_model.py @@ -43,6 +43,7 @@ class ModelProtocol(str, Enum): OPENAI = "openai" ANTHROPIC = "anthropic" + DASHSCOPE = "dashscope" # ========================================================================== # diff --git a/samples/test_aio_multimodal_conversation.py b/samples/test_aio_multimodal_conversation.py index 9633f39..7f5ac28 100644 --- a/samples/test_aio_multimodal_conversation.py +++ b/samples/test_aio_multimodal_conversation.py @@ -250,6 +250,7 @@ async def test_qwen_asr(): ] # Call AioMultiModalConversation API with ASR options + # qwen3-asr-flash is a public model available on Alibaba Cloud's Bailian platform response = await dashscope.AioMultiModalConversation.call( model="qwen3-asr-flash", messages=messages, diff --git a/samples/test_multimodal_conversation.py b/samples/test_multimodal_conversation.py index 14906b1..b294546 100644 --- a/samples/test_multimodal_conversation.py +++ b/samples/test_multimodal_conversation.py @@ -208,6 +208,7 @@ def test_qwen_asr(): ] # Call MultiModalConversation API with ASR options + # qwen3-asr-flash is a public model available on Alibaba Cloud's Bailian platform response = dashscope.MultiModalConversation.call( model="qwen3-asr-flash", messages=messages, diff --git a/samples/test_qwen_asr.py b/samples/test_qwen_asr.py index 9acda21..69aafa1 100644 --- a/samples/test_qwen_asr.py +++ b/samples/test_qwen_asr.py @@ -19,6 +19,7 @@ }, ] dashscope.base_http_api_url = "https://dashscope.aliyuncs.com/api/v1/" +# qwen3-asr-flash is a public model available on Alibaba Cloud's Bailian platform response = dashscope.MultiModalConversation.call( model="qwen3-asr-flash", messages=messages, diff --git a/samples/test_tingwu_usages.py b/samples/test_tingwu_usages.py index f2718f7..49e1bd8 100644 --- a/samples/test_tingwu_usages.py +++ b/samples/test_tingwu_usages.py @@ -12,6 +12,5 @@ "appid": "123456", }, api_key=os.getenv("DASHSCOPE_API_KEY"), - base_address="https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", ) print(resp) diff --git a/tests/unit/test_agentic_rl_components.py b/tests/unit/test_agentic_rl_components.py index 86842f0..b248abe 100644 --- a/tests/unit/test_agentic_rl_components.py +++ b/tests/unit/test_agentic_rl_components.py @@ -140,6 +140,7 @@ def test_task_status_values(self): def test_model_protocol_values(self): assert ModelProtocol.OPENAI == "openai" assert ModelProtocol.ANTHROPIC == "anthropic" + assert ModelProtocol.DASHSCOPE == "dashscope" def test_model_resource_creation(self, model_resource): assert model_resource.model_name == "qwen-max" @@ -185,10 +186,10 @@ def test_resource_defaults(self): def test_resource_custom_values(self): resource = Resource( - model_name="gpt-4o", - base_url="https://api.openai.com/v1", + model_name="qwen-max", + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=SecretStr("sk-key"), - protocol=ModelProtocol.OPENAI, + protocol=ModelProtocol.DASHSCOPE, max_tokens=4096, max_turns=50, sampling_params={"temperature": 0.7, "top_p": 0.9}, From 7183876a937b44390748ec317e5f56451fc7d874 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 7 Jul 2026 16:31:58 +0800 Subject: [PATCH 3/8] fix(cli): handle non-dict output in business error check --- dashscope/cli/common.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index 8bd26b1..cbe0e14 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -63,13 +63,19 @@ def ensure_ok(rsp): raise typer.Exit(1) # Some APIs return error info in output even with HTTP 200 - error_code = output.get("code") if isinstance(output, dict) else None + 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: {output.get('message', 'Unknown error')}", + f"message: {message}", ) raise typer.Exit(1) From 5969977436737518048a327f644b2e2c81d6a10b Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 8 Jul 2026 11:24:06 +0800 Subject: [PATCH 4/8] fix: Fix error handling and response compatibility for async task creation commands --- .../audio/http_tts/http_speech_synthesizer.py | 50 +++++++++++++++++- dashscope/cli/common.py | 43 +++++++++------ dashscope/cli/deployments.py | 3 +- dashscope/cli/fine_tunes.py | 3 +- dashscope/cli/image_synthesis.py | 3 +- dashscope/cli/transcription.py | 4 +- dashscope/cli/video_synthesis.py | 4 +- tests/integration/test_large_utf8_payload.py | 52 ++++++++++++++----- 8 files changed, 125 insertions(+), 37 deletions(-) diff --git a/dashscope/audio/http_tts/http_speech_synthesizer.py b/dashscope/audio/http_tts/http_speech_synthesizer.py index 62581bb..285f8c2 100644 --- a/dashscope/audio/http_tts/http_speech_synthesizer.py +++ b/dashscope/audio/http_tts/http_speech_synthesizer.py @@ -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, @@ -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.""" diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index cbe0e14..ba7fb3f 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -41,7 +41,7 @@ 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. @@ -51,6 +51,13 @@ def ensure_ok(rsp): 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: print_failed_message(rsp) @@ -62,22 +69,24 @@ def ensure_ok(rsp): print_failed_message(rsp) raise typer.Exit(1) - # 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) + # 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 diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index a59d014..1130ad3 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/cli/deployments.py @@ -66,7 +66,8 @@ def _wait_for_deployment( 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 ( diff --git a/dashscope/cli/fine_tunes.py b/dashscope/cli/fine_tunes.py index 5fb0093..d09d4a1 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/cli/fine_tunes.py @@ -64,7 +64,8 @@ def _wait_for_job(job_id: str, timeout: int = DEFAULT_WAIT_TIMEOUT): raise typer.Exit(1) rsp = dashscope.FineTunes.get(job_id) - 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 == TaskStatus.FAILED: diff --git a/dashscope/cli/image_synthesis.py b/dashscope/cli/image_synthesis.py index 6c6651f..a4d247c 100644 --- a/dashscope/cli/image_synthesis.py +++ b/dashscope/cli/image_synthesis.py @@ -60,7 +60,8 @@ def create( n=n, size=size, ) - output = ensure_ok(response) + # For async task creation, only check HTTP success, not business errors + output = ensure_ok(response, check_business_error=False) console.print_json(json.dumps(output, ensure_ascii=False)) usage = getattr(response, "usage", None) if usage: diff --git a/dashscope/cli/transcription.py b/dashscope/cli/transcription.py index fb2e449..c1665df 100644 --- a/dashscope/cli/transcription.py +++ b/dashscope/cli/transcription.py @@ -93,7 +93,9 @@ def create( special_word_filter=special_word_filter, audio_event_detection_enabled=audio_event_detection_enabled, ) - output = ensure_ok(response) + # For async task creation, only check HTTP success, not business errors + # Business errors will be reported when fetching/waiting for task results + output = ensure_ok(response, check_business_error=False) console.print_json(json.dumps(output, ensure_ascii=False)) usage = getattr(response, "usage", None) if usage: diff --git a/dashscope/cli/video_synthesis.py b/dashscope/cli/video_synthesis.py index 3039832..8c46492 100644 --- a/dashscope/cli/video_synthesis.py +++ b/dashscope/cli/video_synthesis.py @@ -104,7 +104,9 @@ def create( resolution=resolution, ratio=ratio, ) - output = ensure_ok(response) + # For async task creation, only check HTTP success, not business errors + # Business errors will be reported when fetching/waiting for task results + output = ensure_ok(response, check_business_error=False) console.print_json(json.dumps(output, ensure_ascii=False)) usage = getattr(response, "usage", None) if usage: diff --git a/tests/integration/test_large_utf8_payload.py b/tests/integration/test_large_utf8_payload.py index 3ab07c7..39d6ebc 100644 --- a/tests/integration/test_large_utf8_payload.py +++ b/tests/integration/test_large_utf8_payload.py @@ -11,6 +11,8 @@ import os import time +import pytest + from dashscope.aigc.generation import Generation, AioGeneration WEBSEARCH_JSON = os.path.join( @@ -22,6 +24,28 @@ MAX_OUTPUT_TOKENS = 1280 +@pytest.fixture(scope="module") +def messages(): + """Load messages from websearch.json for testing.""" + if not os.path.exists(WEBSEARCH_JSON): + pytest.skip(f"Test data file not found: {WEBSEARCH_JSON}") + + with open(WEBSEARCH_JSON, "r", encoding="utf-8") as f: + data = json.load(f) + + # websearch.json stores some fields as JSON strings; parse them so the API + # schema (which expects objects) is satisfied. + for msg in data["messages"]: + for key in ("extra", "files", "childrenIds"): + if key in msg and isinstance(msg[key], str): + try: + msg[key] = json.loads(msg[key]) + except json.JSONDecodeError: + pass + + return data["messages"] + + def _print_result(resp, elapsed): # pylint: disable=unused-argument print(f"Status: {resp.status_code}") print(f"Request ID: {resp.request_id}") @@ -46,7 +70,7 @@ def _print_result(resp, elapsed): # pylint: disable=unused-argument return False -def test_sync(messages): +def test_sync(test_messages): print("\n" + "=" * 60) print("SYNC TEST") print("=" * 60) @@ -54,7 +78,7 @@ def test_sync(messages): try: resp = Generation.call( model=MODEL, - messages=messages, + messages=test_messages, max_tokens=MAX_OUTPUT_TOKENS, result_format="message", ) @@ -70,7 +94,7 @@ def test_sync(messages): return _print_result(resp, elapsed) -async def test_async(messages): +async def test_async(test_messages): print("\n" + "=" * 60) print("ASYNC TEST") print("=" * 60) @@ -78,7 +102,7 @@ async def test_async(messages): try: resp = await AioGeneration.call( model=MODEL, - messages=messages, + messages=test_messages, max_tokens=MAX_OUTPUT_TOKENS, result_format="message", ) @@ -94,7 +118,7 @@ async def test_async(messages): return _print_result(resp, elapsed) -def test_stream(messages): +def test_stream(test_messages): print("\n" + "=" * 60) print("STREAM TEST") print("=" * 60) @@ -103,7 +127,7 @@ def test_stream(messages): try: responses = Generation.call( model=MODEL, - messages=messages, + messages=test_messages, max_tokens=MAX_OUTPUT_TOKENS, result_format="message", stream=True, @@ -136,10 +160,10 @@ def test_stream(messages): return True -def test_websocket(messages): +def test_websocket(test_messages): # WebSocket has a smaller message size limit than HTTP; # use a subset that still contains non-ASCII content. - ws_messages = messages[:5] + ws_messages = test_messages[:5] print("\n" + "=" * 60) print( f"WEBSOCKET TEST ({len(ws_messages)}/{len(messages)} messages)", @@ -180,10 +204,10 @@ def main(): except json.JSONDecodeError: pass - messages = data["messages"] + test_messages = data["messages"] print(f"Model: {MODEL}") - print(f"Messages: {len(messages)}") + print(f"Messages: {len(test_messages)}") print( f"API URL: {os.environ.get('DASHSCOPE_HTTP_BASE_URL', 'default')}", ) @@ -191,10 +215,10 @@ def main(): print("-" * 60) results = [] - results.append(("SYNC", test_sync(messages))) - results.append(("ASYNC", asyncio.run(test_async(messages)))) - results.append(("STREAM", test_stream(messages))) - results.append(("WEBSOCKET", test_websocket(messages))) + results.append(("SYNC", test_sync(test_messages))) + results.append(("ASYNC", asyncio.run(test_async(test_messages)))) + results.append(("STREAM", test_stream(test_messages))) + results.append(("WEBSOCKET", test_websocket(test_messages))) print("\n" + "=" * 60) print("SUMMARY") From 75550ace3dbf1170eedeef3c2609348efd332617 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Wed, 8 Jul 2026 11:47:09 +0800 Subject: [PATCH 5/8] fix: add certifi MPL-2.0 license compliance (NOTICE + LICENSE.certifi) Add NOTICE file declaring certifi dependency under MPL-2.0 and include LICENSE.certifi from the official repository. Update MANIFEST.in to ensure both files are bundled in distribution packages. Co-Authored-By: Claude Opus 4.6 --- LICENSE.certifi | 20 ++++++++++++++++++++ MANIFEST.in | 2 ++ NOTICE | 3 +++ 3 files changed, 25 insertions(+) create mode 100644 LICENSE.certifi create mode 100644 NOTICE diff --git a/LICENSE.certifi b/LICENSE.certifi new file mode 100644 index 0000000..62b076c --- /dev/null +++ b/LICENSE.certifi @@ -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 $ diff --git a/MANIFEST.in b/MANIFEST.in index ca2fd33..9ab58c4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ include dashscope/resources/qwen.tiktoken +include NOTICE +include LICENSE.certifi diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..56fea63 --- /dev/null +++ b/NOTICE @@ -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. From 90fd2ccb013c05a687963932de7ca8811fe30c1a Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 9 Jul 2026 13:17:41 +0800 Subject: [PATCH 6/8] Improve error handling and exception message presentation --- dashscope/api_entities/http_request.py | 22 ++-- dashscope/api_entities/websocket_request.py | 34 ++++-- dashscope/cli/agentic_rl.py | 56 +++++++++- dashscope/cli/common.py | 118 +++++++++++++++++--- dashscope/cli/files.py | 1 - dashscope/cli/generation.py | 6 +- dashscope/client/base_api.py | 15 ++- dashscope/common/utils.py | 13 ++- 8 files changed, 215 insertions(+), 50 deletions(-) diff --git a/dashscope/api_entities/http_request.py b/dashscope/api_entities/http_request.py index 285c6ab..9a7f45d 100644 --- a/dashscope/api_entities/http_request.py +++ b/dashscope/api_entities/http_request.py @@ -228,8 +228,10 @@ async def _handle_aio_request(self): # pylint: disable=too-many-branches if should_close: await session.close() except Exception as e: - logger.debug(e) - raise e + logger.error(f"Async request failed: {e}", exc_info=True) + from dashscope.common.error import DashScopeException + + raise DashScopeException(str(e)) from e @staticmethod def __handle_parameters(params: dict) -> dict: @@ -411,10 +413,12 @@ def _handle_response( # pylint: disable=too-many-branches request_id=request_id, status_code=status_code, output=None, - code=msg["code"] - if "code" in msg - else None, # noqa E501 - message=msg["message"] if "message" in msg else None, + code=msg.get("code") + or msg.get("error_code") + or f"http_{status_code}", + message=msg.get("message") + or msg.get("error_message") + or f"HTTP {status_code} error", headers=headers, ) # noqa E501 else: @@ -517,5 +521,7 @@ def _handle_request(self): # pylint: disable=too-many-branches if should_close: session.close() except Exception as e: - logger.debug(e) - raise e + logger.error(f"Sync request failed: {e}", exc_info=True) + from dashscope.common.error import DashScopeException + + raise DashScopeException(str(e)) from e diff --git a/dashscope/api_entities/websocket_request.py b/dashscope/api_entities/websocket_request.py index a3ceb97..e277c1d 100644 --- a/dashscope/api_entities/websocket_request.py +++ b/dashscope/api_entities/websocket_request.py @@ -112,7 +112,9 @@ async def aio_call(self): pass return result - async def connection_handler(self): # pylint: disable=too-many-branches + async def connection_handler( + self, + ): # pylint: disable=too-many-branches,too-many-statements try: task_id = None async with aiohttp.ClientSession( @@ -203,26 +205,42 @@ async def connection_handler(self): # pylint: disable=too-many-branches ) except aiohttp.WSServerHandshakeError as e: code = e.status - msg = e.message + original_msg = e.message or "" + if e.status in [HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED]: - msg = "Unauthorized, your api-key is invalid!" + friendly_hint = "Unauthorized, your api-key may be invalid!" + msg = ( + f"{friendly_hint} (Server details: {original_msg})" + if original_msg + else friendly_hint + ) elif e.status == HTTPStatus.SERVICE_UNAVAILABLE: - msg = SERVICE_503_MESSAGE + friendly_hint = SERVICE_503_MESSAGE + msg = ( + f"{friendly_hint} (Server details: {original_msg})" + if original_msg + else friendly_hint + ) else: - pass + msg = ( + original_msg + or f"WebSocket handshake failed with status {e.status}" + ) + yield DashScopeAPIResponse( request_id=task_id, status_code=code, - code=code, + code=f"http_{code}" if code else "websocket_handshake_error", message=msg, ) except BaseException as e: logger.exception(e) + exception_name = type(e).__name__ yield DashScopeAPIResponse( request_id="", status_code=-1, - code="Unknown", - message=f"Error type: {type(e)}, message: {e}", + code="", + message=f"[SDK Internal Error] {exception_name}: {e}", ) def _to_DashScopeAPIResponse(self, task_id, is_binary, result): diff --git a/dashscope/cli/agentic_rl.py b/dashscope/cli/agentic_rl.py index cd8386c..803c9f6 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/cli/agentic_rl.py @@ -536,8 +536,16 @@ def run( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}:" - f" {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) progress.update( @@ -598,7 +606,16 @@ def get( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) # Validate output is not None before accessing attributes @@ -636,7 +653,16 @@ def cancel( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) err_console.print( @@ -674,7 +700,16 @@ def logs( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) # Validate output is not None before accessing attributes @@ -716,7 +751,16 @@ def list_jobs( # Handle API response errors if result.status_code != 200: raise OutputError( - f"API returned status {result.status_code}: {result.message}", + ( + f"API error [status={result.status_code}, " + f"code={result.code}]: {result.message}" + ), + response={ + "status_code": result.status_code, + "code": result.code, + "message": result.message, + "request_id": result.request_id, + }, ) output_data = serialize_for_output( diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index ba7fb3f..80ad426 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -10,6 +10,8 @@ import typer from rich.console import Console +from dashscope.common.error import DashScopeException + logger = logging.getLogger("dashscope.cli") CommandFunction = TypeVar("CommandFunction", bound=Callable) @@ -33,12 +35,50 @@ def print_failed_message(rsp): - """Print a standardised error message for a failed API response.""" - err_console.print( - f"[red]Failed[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " - f"code: {rsp.code}, message: {rsp.message}", - ) + """Print a standardised error message for a failed API response. + + Safely handles responses with missing or None attributes. + """ + # Use try-except to handle missing attributes gracefully (works with Mock + # objects) + try: + request_id = rsp.request_id + except AttributeError: + request_id = None + + try: + status_code = rsp.status_code + except AttributeError: + status_code = None + + try: + code = rsp.code + except AttributeError: + code = None + + try: + message = rsp.message + except AttributeError: + message = None + + # Normalize None and empty strings + request_id = request_id if request_id else "N/A" + status_code = status_code if status_code is not None else "N/A" + code = code if code else "" + message = message if message else "" + + # Build error parts dynamically to avoid showing empty fields + parts = ["[red]Failed[/red]"] + if request_id != "N/A": + parts.append(f"request_id: {request_id}") + if status_code != "N/A": + parts.append(f"status_code: {status_code}") + if code: + parts.append(f"code: {code}") + if message: + parts.append(f"message: {message}") + + err_console.print(", ".join(parts)) def ensure_ok(rsp, check_business_error: bool = True): @@ -59,14 +99,20 @@ def ensure_ok(rsp, check_business_error: bool = True): async task creation where we only care about HTTP success, not task execution. """ + # Check HTTP status first 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 + # Check if output exists output = rsp.output if output is None: - print_failed_message(rsp) + # HTTP 200 but no output - this is unusual, treat as error + err_console.print( + f"[red]Error[/red] " + f"request_id: {getattr(rsp, 'request_id', 'N/A')}, " + f"HTTP 200 but response has no output data", + ) raise typer.Exit(1) # Only check business-level errors if explicitly requested @@ -74,17 +120,24 @@ def ensure_ok(rsp, check_business_error: bool = True): # 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") + message = output.get("message") else: error_code = getattr(output, "code", None) - message = getattr(output, "message", "Unknown error") - - if error_code and error_code != "": + message = getattr(output, "message", None) + + # Only report if there's an actual error code + if error_code: + # Provide better fallback message + display_message = ( + message + if message + else "API returned error code without message" + ) err_console.print( - f"[red]Business Error[/red] request_id: {rsp.request_id}, " - f"status_code: {rsp.status_code}, " + f"[red]Business Error[/red] " + f"request_id: {getattr(rsp, 'request_id', 'N/A')}, " f"code: {error_code}, " - f"message: {message}", + f"message: {display_message}", ) raise typer.Exit(1) @@ -108,7 +161,11 @@ def error(message: str, exit_code: int = 1) -> NoReturn: def handle_sdk_error(action: str): - """Convert unexpected SDK exceptions into friendly CLI errors.""" + """Convert unexpected SDK exceptions into friendly CLI errors. + + Preserves full exception context including stack trace for debugging, + and provides differentiated handling for known DashScope exception types. + """ def decorator(command_function: CommandFunction) -> CommandFunction: @wraps(command_function) @@ -116,9 +173,36 @@ def wrapper(*args, **kwargs): try: return command_function(*args, **kwargs) except typer.Exit: + # Re-raise intentional exits without modification raise + except DashScopeException as exception: + # Handle known DashScope exceptions with structured error info + request_id = getattr(exception, "request_id", "N/A") or "N/A" + code = getattr(exception, "code", "N/A") or "N/A" + message = getattr(exception, "message", str(exception)) or str( + exception, + ) + + err_console.print( + f"[red]{action}[/red] " + f"(request_id: {request_id}, code: {code})\n" + f" {message}", + ) + # Log full traceback for debugging + logger.debug( + f"{action} failed with DashScopeException", + exc_info=True, + ) + raise typer.Exit(1) from exception except Exception as exception: - error(f"{action}: {exception}") + # Handle unexpected exceptions with full context + err_console.print(f"[red]{action}:[/red] {exception}") + # Log full traceback for debugging unexpected errors + logger.debug( + f"{action} failed with unexpected exception", + exc_info=True, + ) + raise typer.Exit(1) from exception return wrapper # type: ignore[return-value] diff --git a/dashscope/cli/files.py b/dashscope/cli/files.py index 32fdd9a..245e353 100644 --- a/dashscope/cli/files.py +++ b/dashscope/cli/files.py @@ -63,7 +63,6 @@ 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, diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index b624acc..d0c2bd0 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -7,7 +7,7 @@ from dashscope.aigc import Generation from dashscope.cli.common import ( - error, + err_console, handle_sdk_error, print_failed_message, ) @@ -43,7 +43,9 @@ def _build_generation_kwargs( try: kwargs["messages"] = json.loads(messages) except json.JSONDecodeError as exc: - error("--messages must be a valid JSON string") + err_console.print( + "[red]Error:[/red] --messages must be a valid JSON string", + ) raise typer.Exit(1) from exc # Group simple parameters to reduce branches diff --git a/dashscope/client/base_api.py b/dashscope/client/base_api.py index fc067fc..2aae5ce 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/client/base_api.py @@ -1556,12 +1556,23 @@ def _handle_response(cls, response: requests.Response): ): for is_error, status_code, data in cls._handle_stream(response): if is_error: + try: + error_data = json.loads(data) + code = error_data.get("code") or error_data.get( + "error_code", + ) + message = error_data.get("message") or error_data.get( + "error_message", + ) + except json.JSONDecodeError: + code = "Unknown" + message = data yield DashScopeAPIResponse( request_id=request_id, status_code=status_code, output=None, - code="", - message="", + code=code or "", + message=message or "", ) # noqa E501 else: yield DashScopeAPIResponse( diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index 7a87125..7e1dda4 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -122,11 +122,12 @@ def iter_thread(loop, message_queue): finished, error, obj = message_queue.get() if finished: if error is not None: + exception_name = type(error).__name__ yield DashScopeAPIResponse( -1, "", - "Unknown", - message=f"Error type: {type(error)}, message: {error}", + "", + message=f"[SDK Internal Error] {exception_name}: {error}", ) break yield obj # pylint: disable=no-else-break @@ -319,7 +320,7 @@ def _handle_http_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code="Unknown", + code=f"http_{response.status_code}", message=msgs, headers=headers, ) @@ -330,7 +331,7 @@ def _handle_http_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code="Unknown", + code=f"http_{response.status_code}", message=msg, headers=headers, ) @@ -385,7 +386,7 @@ async def _handle_aiohttp_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code="Unknown", + code=f"http_{response.status}", message="Empty SSE error response", headers=headers, ) @@ -402,7 +403,7 @@ async def _handle_aiohttp_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code="Unknown", + code=f"http_{response.status}", message=msg, headers=headers, ) From be59a8763f12472e7ab8725b0d645bfb55011745 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 9 Jul 2026 14:50:11 +0800 Subject: [PATCH 7/8] Revert "Improve error handling and exception message presentation" This reverts commit 90fd2ccb013c05a687963932de7ca8811fe30c1a. --- dashscope/api_entities/http_request.py | 22 ++-- dashscope/api_entities/websocket_request.py | 34 ++---- dashscope/cli/agentic_rl.py | 56 +--------- dashscope/cli/common.py | 118 +++----------------- dashscope/cli/files.py | 1 + dashscope/cli/generation.py | 6 +- dashscope/client/base_api.py | 15 +-- dashscope/common/utils.py | 13 +-- 8 files changed, 50 insertions(+), 215 deletions(-) diff --git a/dashscope/api_entities/http_request.py b/dashscope/api_entities/http_request.py index 9a7f45d..285c6ab 100644 --- a/dashscope/api_entities/http_request.py +++ b/dashscope/api_entities/http_request.py @@ -228,10 +228,8 @@ async def _handle_aio_request(self): # pylint: disable=too-many-branches if should_close: await session.close() except Exception as e: - logger.error(f"Async request failed: {e}", exc_info=True) - from dashscope.common.error import DashScopeException - - raise DashScopeException(str(e)) from e + logger.debug(e) + raise e @staticmethod def __handle_parameters(params: dict) -> dict: @@ -413,12 +411,10 @@ def _handle_response( # pylint: disable=too-many-branches request_id=request_id, status_code=status_code, output=None, - code=msg.get("code") - or msg.get("error_code") - or f"http_{status_code}", - message=msg.get("message") - or msg.get("error_message") - or f"HTTP {status_code} error", + code=msg["code"] + if "code" in msg + else None, # noqa E501 + message=msg["message"] if "message" in msg else None, headers=headers, ) # noqa E501 else: @@ -521,7 +517,5 @@ def _handle_request(self): # pylint: disable=too-many-branches if should_close: session.close() except Exception as e: - logger.error(f"Sync request failed: {e}", exc_info=True) - from dashscope.common.error import DashScopeException - - raise DashScopeException(str(e)) from e + logger.debug(e) + raise e diff --git a/dashscope/api_entities/websocket_request.py b/dashscope/api_entities/websocket_request.py index e277c1d..a3ceb97 100644 --- a/dashscope/api_entities/websocket_request.py +++ b/dashscope/api_entities/websocket_request.py @@ -112,9 +112,7 @@ async def aio_call(self): pass return result - async def connection_handler( - self, - ): # pylint: disable=too-many-branches,too-many-statements + async def connection_handler(self): # pylint: disable=too-many-branches try: task_id = None async with aiohttp.ClientSession( @@ -205,42 +203,26 @@ async def connection_handler( ) except aiohttp.WSServerHandshakeError as e: code = e.status - original_msg = e.message or "" - + msg = e.message if e.status in [HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED]: - friendly_hint = "Unauthorized, your api-key may be invalid!" - msg = ( - f"{friendly_hint} (Server details: {original_msg})" - if original_msg - else friendly_hint - ) + msg = "Unauthorized, your api-key is invalid!" elif e.status == HTTPStatus.SERVICE_UNAVAILABLE: - friendly_hint = SERVICE_503_MESSAGE - msg = ( - f"{friendly_hint} (Server details: {original_msg})" - if original_msg - else friendly_hint - ) + msg = SERVICE_503_MESSAGE else: - msg = ( - original_msg - or f"WebSocket handshake failed with status {e.status}" - ) - + pass yield DashScopeAPIResponse( request_id=task_id, status_code=code, - code=f"http_{code}" if code else "websocket_handshake_error", + code=code, message=msg, ) except BaseException as e: logger.exception(e) - exception_name = type(e).__name__ yield DashScopeAPIResponse( request_id="", status_code=-1, - code="", - message=f"[SDK Internal Error] {exception_name}: {e}", + code="Unknown", + message=f"Error type: {type(e)}, message: {e}", ) def _to_DashScopeAPIResponse(self, task_id, is_binary, result): diff --git a/dashscope/cli/agentic_rl.py b/dashscope/cli/agentic_rl.py index 803c9f6..cd8386c 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/cli/agentic_rl.py @@ -536,16 +536,8 @@ def run( # Handle API response errors if result.status_code != 200: raise OutputError( - ( - f"API error [status={result.status_code}, " - f"code={result.code}]: {result.message}" - ), - response={ - "status_code": result.status_code, - "code": result.code, - "message": result.message, - "request_id": result.request_id, - }, + f"API returned status {result.status_code}:" + f" {result.message}", ) progress.update( @@ -606,16 +598,7 @@ def get( # Handle API response errors if result.status_code != 200: raise OutputError( - ( - f"API error [status={result.status_code}, " - f"code={result.code}]: {result.message}" - ), - response={ - "status_code": result.status_code, - "code": result.code, - "message": result.message, - "request_id": result.request_id, - }, + f"API returned status {result.status_code}: {result.message}", ) # Validate output is not None before accessing attributes @@ -653,16 +636,7 @@ def cancel( # Handle API response errors if result.status_code != 200: raise OutputError( - ( - f"API error [status={result.status_code}, " - f"code={result.code}]: {result.message}" - ), - response={ - "status_code": result.status_code, - "code": result.code, - "message": result.message, - "request_id": result.request_id, - }, + f"API returned status {result.status_code}: {result.message}", ) err_console.print( @@ -700,16 +674,7 @@ def logs( # Handle API response errors if result.status_code != 200: raise OutputError( - ( - f"API error [status={result.status_code}, " - f"code={result.code}]: {result.message}" - ), - response={ - "status_code": result.status_code, - "code": result.code, - "message": result.message, - "request_id": result.request_id, - }, + f"API returned status {result.status_code}: {result.message}", ) # Validate output is not None before accessing attributes @@ -751,16 +716,7 @@ def list_jobs( # Handle API response errors if result.status_code != 200: raise OutputError( - ( - f"API error [status={result.status_code}, " - f"code={result.code}]: {result.message}" - ), - response={ - "status_code": result.status_code, - "code": result.code, - "message": result.message, - "request_id": result.request_id, - }, + f"API returned status {result.status_code}: {result.message}", ) output_data = serialize_for_output( diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index 80ad426..ba7fb3f 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -10,8 +10,6 @@ import typer from rich.console import Console -from dashscope.common.error import DashScopeException - logger = logging.getLogger("dashscope.cli") CommandFunction = TypeVar("CommandFunction", bound=Callable) @@ -35,50 +33,12 @@ def print_failed_message(rsp): - """Print a standardised error message for a failed API response. - - Safely handles responses with missing or None attributes. - """ - # Use try-except to handle missing attributes gracefully (works with Mock - # objects) - try: - request_id = rsp.request_id - except AttributeError: - request_id = None - - try: - status_code = rsp.status_code - except AttributeError: - status_code = None - - try: - code = rsp.code - except AttributeError: - code = None - - try: - message = rsp.message - except AttributeError: - message = None - - # Normalize None and empty strings - request_id = request_id if request_id else "N/A" - status_code = status_code if status_code is not None else "N/A" - code = code if code else "" - message = message if message else "" - - # Build error parts dynamically to avoid showing empty fields - parts = ["[red]Failed[/red]"] - if request_id != "N/A": - parts.append(f"request_id: {request_id}") - if status_code != "N/A": - parts.append(f"status_code: {status_code}") - if code: - parts.append(f"code: {code}") - if message: - parts.append(f"message: {message}") - - err_console.print(", ".join(parts)) + """Print a standardised error message for a failed API response.""" + err_console.print( + f"[red]Failed[/red] request_id: {rsp.request_id}, " + f"status_code: {rsp.status_code}, " + f"code: {rsp.code}, message: {rsp.message}", + ) def ensure_ok(rsp, check_business_error: bool = True): @@ -99,20 +59,14 @@ def ensure_ok(rsp, check_business_error: bool = True): async task creation where we only care about HTTP success, not task execution. """ - # Check HTTP status first if rsp.status_code != HTTPStatus.OK: print_failed_message(rsp) raise typer.Exit(1) - # Check if output exists + # Check for business-level errors even when HTTP status is 200 output = rsp.output if output is None: - # HTTP 200 but no output - this is unusual, treat as error - err_console.print( - f"[red]Error[/red] " - f"request_id: {getattr(rsp, 'request_id', 'N/A')}, " - f"HTTP 200 but response has no output data", - ) + print_failed_message(rsp) raise typer.Exit(1) # Only check business-level errors if explicitly requested @@ -120,24 +74,17 @@ def ensure_ok(rsp, check_business_error: bool = True): # Some APIs return error info in output even with HTTP 200 if isinstance(output, dict): error_code = output.get("code") - message = output.get("message") + message = output.get("message", "Unknown error") else: error_code = getattr(output, "code", None) - message = getattr(output, "message", None) - - # Only report if there's an actual error code - if error_code: - # Provide better fallback message - display_message = ( - message - if message - else "API returned error code without message" - ) + message = getattr(output, "message", "Unknown error") + + if error_code and error_code != "": err_console.print( - f"[red]Business Error[/red] " - f"request_id: {getattr(rsp, 'request_id', 'N/A')}, " + f"[red]Business Error[/red] request_id: {rsp.request_id}, " + f"status_code: {rsp.status_code}, " f"code: {error_code}, " - f"message: {display_message}", + f"message: {message}", ) raise typer.Exit(1) @@ -161,11 +108,7 @@ def error(message: str, exit_code: int = 1) -> NoReturn: def handle_sdk_error(action: str): - """Convert unexpected SDK exceptions into friendly CLI errors. - - Preserves full exception context including stack trace for debugging, - and provides differentiated handling for known DashScope exception types. - """ + """Convert unexpected SDK exceptions into friendly CLI errors.""" def decorator(command_function: CommandFunction) -> CommandFunction: @wraps(command_function) @@ -173,36 +116,9 @@ def wrapper(*args, **kwargs): try: return command_function(*args, **kwargs) except typer.Exit: - # Re-raise intentional exits without modification raise - except DashScopeException as exception: - # Handle known DashScope exceptions with structured error info - request_id = getattr(exception, "request_id", "N/A") or "N/A" - code = getattr(exception, "code", "N/A") or "N/A" - message = getattr(exception, "message", str(exception)) or str( - exception, - ) - - err_console.print( - f"[red]{action}[/red] " - f"(request_id: {request_id}, code: {code})\n" - f" {message}", - ) - # Log full traceback for debugging - logger.debug( - f"{action} failed with DashScopeException", - exc_info=True, - ) - raise typer.Exit(1) from exception except Exception as exception: - # Handle unexpected exceptions with full context - err_console.print(f"[red]{action}:[/red] {exception}") - # Log full traceback for debugging unexpected errors - logger.debug( - f"{action} failed with unexpected exception", - exc_info=True, - ) - raise typer.Exit(1) from exception + error(f"{action}: {exception}") return wrapper # type: ignore[return-value] diff --git a/dashscope/cli/files.py b/dashscope/cli/files.py index 245e353..32fdd9a 100644 --- a/dashscope/cli/files.py +++ b/dashscope/cli/files.py @@ -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, diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index d0c2bd0..b624acc 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -7,7 +7,7 @@ from dashscope.aigc import Generation from dashscope.cli.common import ( - err_console, + error, handle_sdk_error, print_failed_message, ) @@ -43,9 +43,7 @@ def _build_generation_kwargs( try: kwargs["messages"] = json.loads(messages) except json.JSONDecodeError as exc: - err_console.print( - "[red]Error:[/red] --messages must be a valid JSON string", - ) + error("--messages must be a valid JSON string") raise typer.Exit(1) from exc # Group simple parameters to reduce branches diff --git a/dashscope/client/base_api.py b/dashscope/client/base_api.py index 2aae5ce..fc067fc 100644 --- a/dashscope/client/base_api.py +++ b/dashscope/client/base_api.py @@ -1556,23 +1556,12 @@ def _handle_response(cls, response: requests.Response): ): for is_error, status_code, data in cls._handle_stream(response): if is_error: - try: - error_data = json.loads(data) - code = error_data.get("code") or error_data.get( - "error_code", - ) - message = error_data.get("message") or error_data.get( - "error_message", - ) - except json.JSONDecodeError: - code = "Unknown" - message = data yield DashScopeAPIResponse( request_id=request_id, status_code=status_code, output=None, - code=code or "", - message=message or "", + code="", + message="", ) # noqa E501 else: yield DashScopeAPIResponse( diff --git a/dashscope/common/utils.py b/dashscope/common/utils.py index 7e1dda4..7a87125 100644 --- a/dashscope/common/utils.py +++ b/dashscope/common/utils.py @@ -122,12 +122,11 @@ def iter_thread(loop, message_queue): finished, error, obj = message_queue.get() if finished: if error is not None: - exception_name = type(error).__name__ yield DashScopeAPIResponse( -1, "", - "", - message=f"[SDK Internal Error] {exception_name}: {error}", + "Unknown", + message=f"Error type: {type(error)}, message: {error}", ) break yield obj # pylint: disable=no-else-break @@ -320,7 +319,7 @@ def _handle_http_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code=f"http_{response.status_code}", + code="Unknown", message=msgs, headers=headers, ) @@ -331,7 +330,7 @@ def _handle_http_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status_code, - code=f"http_{response.status_code}", + code="Unknown", message=msg, headers=headers, ) @@ -386,7 +385,7 @@ async def _handle_aiohttp_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code=f"http_{response.status}", + code="Unknown", message="Empty SSE error response", headers=headers, ) @@ -403,7 +402,7 @@ async def _handle_aiohttp_failed_response( return DashScopeAPIResponse( request_id=request_id, status_code=response.status, - code=f"http_{response.status}", + code="Unknown", message=msg, headers=headers, ) From f18106b0709489b3aa42bb577f06060d89bd8487 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 9 Jul 2026 15:21:07 +0800 Subject: [PATCH 8/8] chore: bump version to 1.26.3 --- dashscope/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashscope/version.py b/dashscope/version.py index d3e950a..4e29843 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.26.2" +__version__ = "1.26.3"