diff --git a/packages/cli/src/nonInteractive/control/ControlDispatcher.test.ts b/packages/cli/src/nonInteractive/control/ControlDispatcher.test.ts index aa2cbe70c69..c2454264d86 100644 --- a/packages/cli/src/nonInteractive/control/ControlDispatcher.test.ts +++ b/packages/cli/src/nonInteractive/control/ControlDispatcher.test.ts @@ -20,6 +20,7 @@ import type { CLIControlSetModelRequest, CLIControlSupportedCommandsRequest, CLIControlGetContextUsageRequest, + CLIControlGetAvailableModelsRequest, } from '../types.js'; /** @@ -315,6 +316,40 @@ describe('ControlDispatcher', () => { }); }); + it('should route get_available_models request to system controller', async () => { + const request: CLIControlRequest = { + type: 'control_request', + request_id: 'req-models', + request: { + subtype: 'get_available_models', + } as CLIControlGetAvailableModelsRequest, + }; + + const mockResponse = { + subtype: 'get_available_models', + models: [], + }; + + vi.mocked(mockSystemController.handleRequest).mockResolvedValue( + mockResponse, + ); + + await dispatcher.dispatch(request); + + expect(mockSystemController.handleRequest).toHaveBeenCalledWith( + request.request, + 'req-models', + ); + expect(mockContext.streamJson.send).toHaveBeenCalledWith({ + type: 'control_response', + response: { + subtype: 'success', + request_id: 'req-models', + response: mockResponse, + }, + }); + }); + it('should send error response when controller throws error', async () => { const request: CLIControlRequest = { type: 'control_request', diff --git a/packages/cli/src/nonInteractive/control/ControlDispatcher.ts b/packages/cli/src/nonInteractive/control/ControlDispatcher.ts index af133bcad27..9f780e23a64 100644 --- a/packages/cli/src/nonInteractive/control/ControlDispatcher.ts +++ b/packages/cli/src/nonInteractive/control/ControlDispatcher.ts @@ -14,7 +14,7 @@ * which wraps these controllers with a stable programmatic API. * * Controllers: - * - SystemController: initialize, interrupt, set_model, supported_commands, get_context_usage + * - SystemController: initialize, interrupt, set_model, supported_commands, get_context_usage, get_available_models * - PermissionController: can_use_tool, set_permission_mode * - SdkMcpController: mcp_server_status (mcp_message handled via callback) * @@ -377,6 +377,7 @@ export class ControlDispatcher implements IPendingRequestRegistry { case 'set_model': case 'supported_commands': case 'get_context_usage': + case 'get_available_models': return this.systemController; case 'can_use_tool': diff --git a/packages/cli/src/nonInteractive/control/controllers/systemController.ts b/packages/cli/src/nonInteractive/control/controllers/systemController.ts index 1949e08198f..4faee1f2a15 100644 --- a/packages/cli/src/nonInteractive/control/controllers/systemController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/systemController.ts @@ -20,6 +20,7 @@ import type { CLIControlSetModelRequest, CLIMcpServerConfig, CLIControlGetContextUsageRequest, + CLIControlGetAvailableModelsRequest, } from '../../types.js'; import { getAvailableCommands } from '../../../nonInteractiveCliCommands.js'; import { @@ -79,6 +80,12 @@ export class SystemController extends BaseController { signal, ); + case 'get_available_models': + return this.handleGetAvailableModels( + payload as CLIControlGetAvailableModelsRequest, + signal, + ); + default: throw new Error(`Unsupported request subtype in SystemController`); } @@ -125,6 +132,42 @@ export class SystemController extends BaseController { } } + private async handleGetAvailableModels( + _payload: CLIControlGetAvailableModelsRequest, + signal: AbortSignal, + ): Promise> { + if (signal.aborted) { + throw new Error('Request aborted'); + } + + try { + const models = this.context.config.getAvailableModels(); + if (signal.aborted) { + throw new Error('Request aborted'); + } + + return { + subtype: 'get_available_models', + models: models.map((m) => ({ + id: m.id, + label: m.label, + description: m.description, + isVision: m.isVision ?? m.capabilities?.vision ?? false, + })), + }; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to get available models'; + debugLogger.error( + '[SystemController] Failed to get available models:', + error, + ); + throw new Error(errorMessage); + } + } + /** * Handle initialize request * @@ -282,6 +325,7 @@ export class SystemController extends BaseController { typeof this.context.config.setApprovalMode === 'function', can_set_model: typeof this.context.config.setModel === 'function', can_get_context_usage: true, + can_get_available_models: true, // SDK MCP servers are supported - messages routed through control plane can_handle_mcp_message: true, }; diff --git a/packages/cli/src/nonInteractive/types.ts b/packages/cli/src/nonInteractive/types.ts index 4f8556eaebd..1d81d38e69c 100644 --- a/packages/cli/src/nonInteractive/types.ts +++ b/packages/cli/src/nonInteractive/types.ts @@ -431,6 +431,10 @@ export interface CLIControlGetContextUsageRequest { show_details?: boolean; } +export interface CLIControlGetAvailableModelsRequest { + subtype: 'get_available_models'; +} + export type ControlRequestPayload = | CLIControlInterruptRequest | CLIControlContinueLastTurnRequest @@ -442,7 +446,8 @@ export type ControlRequestPayload = | CLIControlSetModelRequest | CLIControlMcpStatusRequest | CLIControlSupportedCommandsRequest - | CLIControlGetContextUsageRequest; + | CLIControlGetContextUsageRequest + | CLIControlGetAvailableModelsRequest; export interface CLIControlRequest { type: 'control_request'; diff --git a/packages/sdk-python/src/qwen_code_sdk/protocol.py b/packages/sdk-python/src/qwen_code_sdk/protocol.py index 7e5e50b701f..5f2b75b867b 100644 --- a/packages/sdk-python/src/qwen_code_sdk/protocol.py +++ b/packages/sdk-python/src/qwen_code_sdk/protocol.py @@ -238,6 +238,10 @@ class CLIControlSupportedCommandsRequest(TypedDict): subtype: Literal["supported_commands"] +class CLIControlGetAvailableModelsRequest(TypedDict): + subtype: Literal["get_available_models"] + + ControlRequestPayload: TypeAlias = ( CLIControlInterruptRequest | CLIControlPermissionRequest @@ -246,6 +250,7 @@ class CLIControlSupportedCommandsRequest(TypedDict): | CLIControlSetModelRequest | CLIControlMcpStatusRequest | CLIControlSupportedCommandsRequest + | CLIControlGetAvailableModelsRequest | dict[str, Any] ) diff --git a/packages/sdk-python/src/qwen_code_sdk/query.py b/packages/sdk-python/src/qwen_code_sdk/query.py index 9bef8578145..1fcec04f4a5 100644 --- a/packages/sdk-python/src/qwen_code_sdk/query.py +++ b/packages/sdk-python/src/qwen_code_sdk/query.py @@ -482,6 +482,10 @@ async def mcp_server_status(self) -> dict[str, Any] | None: await self._ensure_started() return await self._send_control_request("mcp_server_status") + async def get_available_models(self) -> dict[str, Any] | None: + await self._ensure_started() + return await self._send_control_request("get_available_models") + @property def control_request_timeout(self) -> float: return self._options.timeout.control_request diff --git a/packages/sdk-python/tests/unit/test_transport.py b/packages/sdk-python/tests/unit/test_transport.py index 340d1c1e662..a390bf2734e 100644 --- a/packages/sdk-python/tests/unit/test_transport.py +++ b/packages/sdk-python/tests/unit/test_transport.py @@ -82,6 +82,15 @@ def test_cli_argument_precedence_prefers_resume_then_continue_then_session_id() assert "--session-id" not in args +def test_get_available_models_protocol_type() -> None: + from qwen_code_sdk.protocol import CLIControlGetAvailableModelsRequest + + req: CLIControlGetAvailableModelsRequest = { + "subtype": "get_available_models" + } + assert req["subtype"] == "get_available_models" + + def test_prepare_spawn_info_uses_runtime_for_python_scripts(tmp_path: Path) -> None: script_path = tmp_path / "fake-qwen.py" script_path.write_text("print('ok')\n", encoding="utf-8") diff --git a/packages/sdk-typescript/src/query/Query.ts b/packages/sdk-typescript/src/query/Query.ts index f4c006b4c67..dbc3a5f6d6a 100644 --- a/packages/sdk-typescript/src/query/Query.ts +++ b/packages/sdk-typescript/src/query/Query.ts @@ -983,6 +983,16 @@ export class Query implements AsyncIterable { }); } + /** + * Get the list of available models for the current auth type + * + * @returns Promise resolving to the list of available models + * @throws Error if query is closed + */ + async getAvailableModels(): Promise | null> { + return this.sendControlRequest(ControlRequestType.GET_AVAILABLE_MODELS); + } + /** * Get list of control commands supported by the CLI * diff --git a/packages/sdk-typescript/src/types/protocol.ts b/packages/sdk-typescript/src/types/protocol.ts index 012b35bd497..2bd9506399e 100644 --- a/packages/sdk-typescript/src/types/protocol.ts +++ b/packages/sdk-typescript/src/types/protocol.ts @@ -606,6 +606,7 @@ export enum ControlRequestType { SET_MODEL = 'set_model', SUPPORTED_COMMANDS = 'supported_commands', GET_CONTEXT_USAGE = 'get_context_usage', + GET_AVAILABLE_MODELS = 'get_available_models', // PermissionController requests CAN_USE_TOOL = 'can_use_tool',