Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/cli/src/nonInteractive/control/ControlDispatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
CLIControlSetModelRequest,
CLIControlSupportedCommandsRequest,
CLIControlGetContextUsageRequest,
CLIControlGetAvailableModelsRequest,
} from '../types.js';

/**
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/nonInteractive/control/ControlDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down Expand Up @@ -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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
CLIControlSetModelRequest,
CLIMcpServerConfig,
CLIControlGetContextUsageRequest,
CLIControlGetAvailableModelsRequest,
} from '../../types.js';
import { getAvailableCommands } from '../../../nonInteractiveCliCommands.js';
import {
Expand Down Expand Up @@ -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`);
}
Expand Down Expand Up @@ -125,6 +132,42 @@ export class SystemController extends BaseController {
}
}

private async handleGetAvailableModels(
_payload: CLIControlGetAvailableModelsRequest,
signal: AbortSignal,
): Promise<Record<string, unknown>> {
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
*
Expand Down Expand Up @@ -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,
};
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/nonInteractive/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export interface CLIControlGetContextUsageRequest {
show_details?: boolean;
}

export interface CLIControlGetAvailableModelsRequest {
subtype: 'get_available_models';
}

export type ControlRequestPayload =
| CLIControlInterruptRequest
| CLIControlContinueLastTurnRequest
Expand All @@ -442,7 +446,8 @@ export type ControlRequestPayload =
| CLIControlSetModelRequest
| CLIControlMcpStatusRequest
| CLIControlSupportedCommandsRequest
| CLIControlGetContextUsageRequest;
| CLIControlGetContextUsageRequest
| CLIControlGetAvailableModelsRequest;

export interface CLIControlRequest {
type: 'control_request';
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ class CLIControlSupportedCommandsRequest(TypedDict):
subtype: Literal["supported_commands"]


class CLIControlGetAvailableModelsRequest(TypedDict):
subtype: Literal["get_available_models"]


ControlRequestPayload: TypeAlias = (
CLIControlInterruptRequest
| CLIControlPermissionRequest
Expand All @@ -246,6 +250,7 @@ class CLIControlSupportedCommandsRequest(TypedDict):
| CLIControlSetModelRequest
| CLIControlMcpStatusRequest
| CLIControlSupportedCommandsRequest
| CLIControlGetAvailableModelsRequest
| dict[str, Any]
)

Expand Down
4 changes: 4 additions & 0 deletions packages/sdk-python/src/qwen_code_sdk/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/sdk-python/tests/unit/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk-typescript/src/query/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,16 @@ export class Query implements AsyncIterable<SDKMessage> {
});
}

/**
* 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<Record<string, unknown> | null> {
return this.sendControlRequest(ControlRequestType.GET_AVAILABLE_MODELS);
}

/**
* Get list of control commands supported by the CLI
*
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-typescript/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down