* feat(sdk-python): add pypi release workflow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): build cli before smoke test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten release conflict handling Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): harden python release workflow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten stable release guards Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): harden prerelease publish flow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): reuse release branches on rerun Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): resume incomplete releases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(release): tighten missing-release checks Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): resume stable release reruns Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten release recovery guards Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(sdk-python): cover release version edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address release workflow review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * refactor(sdk-python): address review feedback on release version script - Remove unreachable `if (type === 'stable')` branch in bumpVersion(); the stable path was dead code since getVersion() throws for all stable conflicts before calling bumpVersion(). Move nightly conflict throw to the call site for symmetry. - Rename getNextPatchBaseVersion → getNextBaseVersion to reflect that the function can return a prerelease base without incrementing patch. - Add test for preview+nightly coexistence where nightly base is higher. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): address remaining review feedback on release workflow - Fix failure-issue gate to read github.event.inputs.dry_run directly instead of steps.vars.outputs.is_dry_run (which is empty when early steps fail). Add --repo flag for gh issue create when checkout failed. - Add diagnostic state table to failure-issue body (RELEASE_TAG, PACKAGE_VERSION, PUBLISH_CHANNEL, RESUME_EXISTING_RELEASE, etc.) - Fix release-notes error swallow: only silence release not found / Not Found / HTTP 404, emit :⚠️: for other gh release view errors. - Improve validateVersion error messages to use human-readable format keys (X.Y.Z, X.Y.Z-preview.N) matching TS sibling convention. - Filter fully-yanked versions in getAllVersionsFromPyPI. - Add console.error log when stable is derived from nightly. - Add bash regex guard for inputs.version to prevent shell injection. - Use per-release-type concurrency groups (nightly/preview/stable). - Add jq null-guard checks for all 6 field extractions. - Remove misleading --follow-tags from git push (lightweight tags). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): rename misleading test description The test asserts that preview/nightly releases return empty previousReleaseTag, but the name said "same-channel previous release tags" which implied non-empty values. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): address unresolved review comments on release workflow - Remove -z check in extract_field() that blocked preview/nightly releases (previousReleaseTag is legitimately empty for non-stable releases) - Use static environment.url since step outputs aren't available at job startup - Use skip-existing for resumed PyPI publish to fill in missing artifacts - Add AbortSignal.timeout(30s) to PyPI fetch to prevent indefinite hangs - Add downgrade guard for stable_version_override - Use GHA :⚠️: annotation instead of console.error for visibility - Separate yanked/non-yanked version lists so conflict detection includes yanked versions (PyPI still reserves those slots) - Filter current release from previousReleaseTag to avoid self-reference on resume - Add tests for yanked conflict detection, downgrade guard, and resume previousReleaseTag Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address final review round on release version script - Fix getNextBaseVersion() first-release skip: use pyproject.toml version directly when PyPI has no stable versions instead of unconditionally incrementing - Fix getNextBaseVersion() off-by-one: change > to >= so equal prerelease base continues the existing line instead of incrementing patch - Add :⚠️: annotation when preview auto-bumps due to orphan git tags (tag exists without PyPI version or GitHub release) - Add set -euo pipefail to 5 workflow steps missing it: release_branch, persist_source, Create GitHub release, Delete prerelease branch, Create issue on failure - Fix 2 existing tests affected by first-release change, add 4 new tests 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): use stderr for GHA warning annotations to avoid corrupting JSON stdout console.log writes to stdout, which gets captured by VERSION_JSON=$(node ...) in the workflow and corrupts the JSON output for jq. Switch to console.error so :⚠️: annotations go to stderr (GHA recognizes workflow commands on both streams). Also add set -euo pipefail to the "Get the version" step for consistency with other workflow steps. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| test/unit | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@qwen-code/sdk
A minimum experimental TypeScript SDK for programmatic access to Qwen Code.
Feel free to submit a feature request/issue/PR.
Installation
npm install @qwen-code/sdk
Requirements
- Node.js >= 20.0.0
From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.
Quick Start
import { query } from '@qwen-code/sdk';
// Single-turn query
const result = query({
prompt: 'What files are in the current directory?',
options: {
cwd: '/path/to/project',
},
});
// Iterate over messages
for await (const message of result) {
if (message.type === 'assistant') {
console.log('Assistant:', message.message.content);
} else if (message.type === 'result') {
console.log('Result:', message.result);
}
}
API Reference
query(config)
Creates a new query session with the Qwen Code.
Parameters
prompt:string | AsyncIterable<SDKUserMessage>- The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.options:QueryOptions- Configuration options for the query session.
QueryOptions
| Option | Type | Default | Description |
|---|---|---|---|
cwd |
string |
process.cwd() |
The working directory for the query session. Determines the context in which file operations and commands are executed. |
model |
string |
- | The AI model to use (e.g., 'qwen-max', 'qwen-plus', 'qwen-turbo'). Takes precedence over OPENAI_MODEL and QWEN_MODEL environment variables. |
pathToQwenExecutable |
string |
Auto-detected | Path to the Qwen Code executable. Supports multiple formats: 'qwen' (native binary from PATH), '/path/to/qwen' (explicit path), '/path/to/cli.js' (Node.js bundle), 'node:/path/to/cli.js' (force Node.js runtime), 'bun:/path/to/cli.js' (force Bun runtime). If not provided, auto-detects from: QWEN_CODE_CLI_PATH env var, ~/.volta/bin/qwen, ~/.npm-global/bin/qwen, /usr/local/bin/qwen, ~/.local/bin/qwen, ~/node_modules/.bin/qwen, ~/.yarn/bin/qwen. |
permissionMode |
'default' | 'plan' | 'auto-edit' | 'yolo' |
'default' |
Permission mode controlling tool execution approval. See Permission Modes for details. |
canUseTool |
CanUseTool |
- | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See Custom Permission Handler. |
env |
Record<string, string> |
- | Environment variables to pass to the Qwen Code process. Merged with the current process environment. |
systemPrompt |
string | QuerySystemPromptPreset |
- | System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions. |
mcpServers |
Record<string, McpServerConfig> |
- | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like command, args, url, httpUrl, etc. SDK servers use { type: 'sdk', name: string, instance: Server }. |
abortController |
AbortController |
- | Controller to cancel the query session. Call abortController.abort() to terminate the session and cleanup resources. |
debug |
boolean |
false |
Enable debug mode for verbose logging from the CLI process. |
maxSessionTurns |
number |
-1 (unlimited) |
Maximum number of conversation turns before the session automatically terminates. A turn consists of a user message and an assistant response. |
coreTools |
string[] |
- | Equivalent to permissions.allow in settings.json as an allowlist. If specified, only these tools will be available to the AI (all other tools are disabled at registry level). Supports tool name aliases and pattern matching. Example: ['Read', 'Edit', 'Bash(git *)']. |
excludeTools |
string[] |
- | Equivalent to permissions.deny in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name ('write_file'), shell command prefix ('Bash(rm *)'), or path patterns ('Read(.env)', 'Edit(/src/**)'). |
allowedTools |
string[] |
- | Equivalent to permissions.allow in settings.json. Matching tools bypass canUseTool callback and execute automatically. Only applies when tool requires confirmation. Supports same pattern matching as excludeTools. Example: ['ShellTool(git status)', 'ShellTool(npm test)']. |
authType |
'openai' | 'qwen-oauth' |
'openai' |
Authentication type for the AI service. Using 'qwen-oauth' in SDK is not recommended as credentials are stored in ~/.qwen and may need periodic refresh. |
agents |
SubagentConfig[] |
- | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains. |
includePartialMessages |
boolean |
false |
When true, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response. |
resume |
string |
- | Resume a previous session by providing its session ID. Equivalent to CLI's --resume flag. |
sessionId |
string |
- | Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's --session-id flag. |
Tip
If you need to configure
coreTools,excludeTools, orallowedTools, it is strongly recommended to read the permissions configuration documentation first, especially the Tool name aliases and Rule syntax examples sections, to understand the available aliases and pattern matching syntax (e.g.,Bash(git *),Read(.env),Edit(/src/**)).
Timeouts
The SDK enforces the following default timeouts:
| Timeout | Default | Description |
|---|---|---|
canUseTool |
1 minute | Maximum time for canUseTool callback to respond. If exceeded, the tool request is auto-denied. |
mcpRequest |
1 minute | Maximum time for SDK MCP tool calls to complete. |
controlRequest |
1 minute | Maximum time for control operations like initialize(), setModel(), setPermissionMode(), and interrupt() to complete. |
streamClose |
1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. |
You can customize these timeouts via the timeout option:
const query = qwen.query('Your prompt', {
timeout: {
canUseTool: 60000, // 60 seconds for permission callback
mcpRequest: 600000, // 10 minutes for MCP tool calls
controlRequest: 60000, // 60 seconds for control requests
streamClose: 15000, // 15 seconds for stream close wait
},
});
Message Types
The SDK provides type guards to identify different message types:
import {
isSDKUserMessage,
isSDKAssistantMessage,
isSDKSystemMessage,
isSDKResultMessage,
isSDKPartialAssistantMessage,
} from '@qwen-code/sdk';
for await (const message of result) {
if (isSDKAssistantMessage(message)) {
// Handle assistant message
} else if (isSDKResultMessage(message)) {
// Handle result message
}
}
Query Instance Methods
The Query instance returned by query() provides several methods:
const q = query({ prompt: 'Hello', options: {} });
// Get session ID
const sessionId = q.getSessionId();
// Check if closed
const closed = q.isClosed();
// Interrupt the current operation
await q.interrupt();
// Change permission mode mid-session
await q.setPermissionMode('yolo');
// Change model mid-session
await q.setModel('qwen-max');
// Close the session
await q.close();
Permission Modes
The SDK supports different permission modes for controlling tool execution:
default: Write tools are denied unless approved viacanUseToolcallback or inallowedTools. Read-only tools execute without confirmation.plan: Blocks all write tools, instructing AI to present a plan first.auto-edit: Auto-approve edit tools (edit, write_file) while other tools require confirmation.yolo: All tools execute automatically without confirmation.
Permission Priority Chain
Decision priority (highest first): deny > ask > allow > (default/interactive mode)
The first matching rule wins.
excludeTools/permissions.deny- Blocks tools completely (returns permission error)permissions.ask- Always requires user confirmationpermissionMode: 'plan'- Blocks all non-read-only toolspermissionMode: 'yolo'- Auto-approves all toolsallowedTools/permissions.allow- Auto-approves matching toolscanUseToolcallback - Custom approval logic (if provided, not called for allowed tools)- Default behavior - Auto-deny in SDK mode (write tools require explicit approval)
Examples
Multi-turn Conversation
import { query, type SDKUserMessage } from '@qwen-code/sdk';
async function* generateMessages(): AsyncIterable<SDKUserMessage> {
yield {
type: 'user',
session_id: 'my-session',
message: { role: 'user', content: 'Create a hello.txt file' },
parent_tool_use_id: null,
};
// Wait for some condition or user input
yield {
type: 'user',
session_id: 'my-session',
message: { role: 'user', content: 'Now read the file back' },
parent_tool_use_id: null,
};
}
const result = query({
prompt: generateMessages(),
options: {
permissionMode: 'auto-edit',
},
});
for await (const message of result) {
console.log(message);
}
Custom Permission Handler
import { query, type CanUseTool } from '@qwen-code/sdk';
const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
// Allow all read operations
if (toolName.startsWith('read_')) {
return { behavior: 'allow', updatedInput: input };
}
// Prompt user for write operations (in a real app)
const userApproved = await promptUser(`Allow ${toolName}?`);
if (userApproved) {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'User denied the operation' };
};
const result = query({
prompt: 'Create a new file',
options: {
canUseTool,
},
});
With External MCP Servers
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Use the custom tool from my MCP server',
options: {
mcpServers: {
'my-server': {
command: 'node',
args: ['path/to/mcp-server.js'],
env: { PORT: '3000' },
},
},
},
});
Override the System Prompt
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Say hello in one sentence.',
options: {
systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
},
});
Append to the Built-in System Prompt
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Review the current directory.',
options: {
systemPrompt: {
type: 'preset',
preset: 'qwen_code',
append: 'Be terse and focus on concrete findings.',
},
},
});
With SDK-Embedded MCP Servers
The SDK provides tool and createSdkMcpServer to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.
tool(name, description, inputSchema, handler)
Creates a tool definition with Zod schema type inference.
| Parameter | Type | Description |
|---|---|---|
name |
string |
Tool name (1-64 chars, starts with letter, alphanumeric and underscores) |
description |
string |
Human-readable description of what the tool does |
inputSchema |
ZodRawShape |
Zod schema object defining the tool's input parameters |
handler |
(args, extra) => Promise<Result> |
Async function that executes the tool and returns MCP content blocks |
The handler must return a CallToolResult object with the following structure:
{
content: Array<
| { type: 'text'; text: string }
| { type: 'image'; data: string; mimeType: string }
| { type: 'resource'; uri: string; mimeType?: string; text?: string }
>;
isError?: boolean;
}
createSdkMcpServer(options)
Creates an SDK-embedded MCP server instance.
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
Required | Unique name for the MCP server |
version |
string |
'1.0.0' |
Server version |
tools |
SdkMcpToolDefinition[] |
- | Array of tools created with tool() |
Returns a McpSdkServerConfigWithInstance object that can be passed directly to the mcpServers option.
Example
import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@qwen-code/sdk';
// Define a tool with Zod schema
const calculatorTool = tool(
'calculate_sum',
'Add two numbers',
{ a: z.number(), b: z.number() },
async (args) => ({
content: [{ type: 'text', text: String(args.a + args.b) }],
}),
);
// Create the MCP server
const server = createSdkMcpServer({
name: 'calculator',
tools: [calculatorTool],
});
// Use the server in a query
const result = query({
prompt: 'What is 42 + 17?',
options: {
permissionMode: 'yolo',
mcpServers: {
calculator: server,
},
},
});
for await (const message of result) {
console.log(message);
}
Abort a Query
import { query, isAbortError } from '@qwen-code/sdk';
const abortController = new AbortController();
const result = query({
prompt: 'Long running task...',
options: {
abortController,
},
});
// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);
try {
for await (const message of result) {
console.log(message);
}
} catch (error) {
if (isAbortError(error)) {
console.log('Query was aborted');
} else {
throw error;
}
}
Error Handling
The SDK provides an AbortError class for handling aborted queries:
import { AbortError, isAbortError } from '@qwen-code/sdk';
try {
// ... query operations
} catch (error) {
if (isAbortError(error)) {
// Handle abort
} else {
// Handle other errors
}
}
FAQ / Troubleshooting
Version 0.1.0 Requirements
If you're using SDK version 0.1.0, please note the following requirements:
Qwen Code Installation Required
Version 0.1.0 requires Qwen Code >= 0.4.0 to be installed separately and accessible in your PATH.
# Install Qwen Code globally
npm install -g qwen-code@^0.4.0
Note: From version 0.1.1 onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed.
License
Apache-2.0 - see LICENSE for details.