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. 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/__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 34d5c1d..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": getattr(result.output, "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..ba7fb3f 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -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): diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index 6664c73..1130ad3 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,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 ( @@ -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: @@ -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) 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 0a7f60d..d09d4a1 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,12 +41,31 @@ 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) + # During polling, only check HTTP success, not business errors + output = ensure_ok(rsp, check_business_error=False) status = output.status if status == TaskStatus.FAILED: @@ -86,12 +107,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 +150,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 +234,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/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/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/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/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/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/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/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" 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/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") 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}, 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,