Compare commits

..

16 Commits

Author SHA1 Message Date
tanzhenxin
ced1b1db80 add Session.test.ts 2026-01-17 11:13:45 +08:00
tanzhenxin
cf140b1b9d update acp-integration.test.ts to add session/set_model command 2026-01-17 10:59:46 +08:00
tanzhenxin
1f1e78aa3b Refactor model handling in ACP agent and update auth error message
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-01-17 10:53:57 +08:00
tanzhenxin
511269446f fix(acp): implement session/set_model method for JetBrains compatibility 2026-01-17 10:15:47 +08:00
tanzhenxin
0681c71894 Merge pull request #1490 from QwenLM/fix/mcp-server-remove
fix: unable to remove MCP server when only one element exists
2026-01-16 17:22:42 +08:00
tanzhenxin
155c4b9728 Merge pull request #1508 from PJ-568/main
fix: mistranslation of token
2026-01-16 15:50:00 +08:00
tanzhenxin
57ca2823b3 Merge pull request #1497 from Antovex/feat/settings-for-experimental-skills
feat(cli): add settings support for experimental skills
2026-01-16 15:49:34 +08:00
tanzhenxin
620341eeae remove -x alias and fix whitespace issue
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-01-16 15:28:33 +08:00
PJ568
c6c33233c5 fix: mistranslation of token 2026-01-15 18:16:31 +08:00
Antarin Ghosal
106b69e5c0 docs: update experimental skills configuration in skills.md 2026-01-15 15:02:14 +05:30
Antarin Ghosal
6afe0f8c29 docs: update setting name in configuration docs 2026-01-15 14:59:52 +05:30
Antarin Ghosal
0b3be1a82c fix: update settings path to tools.experimental.skills 2026-01-15 14:58:31 +05:30
Antarin Ghosal
8af43e3ac3 refactor: nest skills under tools.experimental 2026-01-15 14:57:02 +05:30
Antarin Ghosal
63406b4ba4 Update command options for skills feature
Fixed a typo
2026-01-14 19:13:35 +05:30
Antarin Ghosal
52db3a766d feat(cli): add settings support for experimental skills
- Add tools.experimentalSkills setting in settingsSchema
- Read default from settings in config. ts
- Add --skills as shorter alias for --experimental-skills
- Update documentation for new setting

Fixes #1493
2026-01-14 18:49:17 +05:30
LaZzyMan
6f33d92b2c fix: can not remove the mcp server when there is only one element 2026-01-14 16:27:45 +08:00
17 changed files with 364 additions and 215 deletions

View File

@@ -275,6 +275,7 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
| `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes |
| `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes |
| `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | |
| `tools.experimental.skills` | boolean | Enable experimental Agent Skills feature | `false` | |
#### mcp

View File

@@ -11,12 +11,29 @@ This guide shows you how to create, use, and manage Agent Skills in **Qwen Code*
## Prerequisites
- Qwen Code (recent version)
- Run with the experimental flag enabled:
## How to enable
### Via CLI flag
```bash
qwen --experimental-skills
```
### Via settings.json
Add to your `~/.qwen/settings.json` or project's `.qwen/settings.json`:
```json
{
"tools": {
"experimental": {
"skills": true
}
}
}
```
- Basic familiarity with Qwen Code ([Quickstart](../quickstart.md))
## What are Agent Skills?

View File

@@ -311,9 +311,9 @@ function setupAcpTest(
}
});
it('returns modes on initialize and allows setting approval mode', async () => {
it('returns modes on initialize and allows setting mode and model', async () => {
const rig = new TestRig();
rig.setup('acp approval mode');
rig.setup('acp mode and model');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig);
@@ -366,8 +366,14 @@ function setupAcpTest(
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
})) as {
sessionId: string;
models: {
availableModels: Array<{ modelId: string }>;
};
};
expect(newSession.sessionId).toBeTruthy();
expect(newSession.models.availableModels.length).toBeGreaterThan(0);
// Test 4: Set approval mode to 'yolo'
const setModeResult = (await sendRequest('session/set_mode', {
@@ -392,6 +398,15 @@ function setupAcpTest(
})) as { modeId: string };
expect(setModeResult3).toBeDefined();
expect(setModeResult3.modeId).toBe('default');
// Test 7: Set model using first available model
const firstModel = newSession.models.availableModels[0];
const setModelResult = (await sendRequest('session/set_model', {
sessionId: newSession.sessionId,
modelId: firstModel.modelId,
})) as { modelId: string };
expect(setModelResult).toBeDefined();
expect(setModelResult.modelId).toBeTruthy();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));

View File

@@ -70,6 +70,13 @@ export class AgentSideConnection implements Client {
const validatedParams = schema.setModeRequestSchema.parse(params);
return agent.setMode(validatedParams);
}
case schema.AGENT_METHODS.session_set_model: {
if (!agent.setModel) {
throw RequestError.methodNotFound();
}
const validatedParams = schema.setModelRequestSchema.parse(params);
return agent.setModel(validatedParams);
}
default:
throw RequestError.methodNotFound(method);
}
@@ -408,4 +415,5 @@ export interface Agent {
prompt(params: schema.PromptRequest): Promise<schema.PromptResponse>;
cancel(params: schema.CancelNotification): Promise<void>;
setMode?(params: schema.SetModeRequest): Promise<schema.SetModeResponse>;
setModel?(params: schema.SetModelRequest): Promise<schema.SetModelResponse>;
}

View File

@@ -165,30 +165,11 @@ class GeminiAgent {
this.setupFileSystem(config);
const session = await this.createAndStoreSession(config);
const configuredModel = (
config.getModel() ||
this.config.getModel() ||
''
).trim();
const modelId = configuredModel || 'default';
const modelName = configuredModel || modelId;
const availableModels = this.buildAvailableModels(config);
return {
sessionId: session.getId(),
models: {
currentModelId: modelId,
availableModels: [
{
modelId,
name: modelName,
description: null,
_meta: {
contextLimit: tokenLimit(modelId),
},
},
],
_meta: null,
},
models: availableModels,
};
}
@@ -305,15 +286,29 @@ class GeminiAgent {
async setMode(params: acp.SetModeRequest): Promise<acp.SetModeResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
throw acp.RequestError.invalidParams(
`Session not found for id: ${params.sessionId}`,
);
}
return session.setMode(params);
}
async setModel(params: acp.SetModelRequest): Promise<acp.SetModelResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw acp.RequestError.invalidParams(
`Session not found for id: ${params.sessionId}`,
);
}
return session.setModel(params);
}
private async ensureAuthenticated(config: Config): Promise<void> {
const selectedType = this.settings.merged.security?.auth?.selectedType;
if (!selectedType) {
throw acp.RequestError.authRequired('No Selected Type');
throw acp.RequestError.authRequired(
'Use Qwen Code CLI to authenticate first.',
);
}
try {
@@ -382,4 +377,43 @@ class GeminiAgent {
return session;
}
private buildAvailableModels(
config: Config,
): acp.NewSessionResponse['models'] {
const currentModelId = (
config.getModel() ||
this.config.getModel() ||
''
).trim();
const availableModels = config.getAvailableModels();
const mappedAvailableModels = availableModels.map((model) => ({
modelId: model.id,
name: model.label,
description: model.description ?? null,
_meta: {
contextLimit: tokenLimit(model.id),
},
}));
if (
currentModelId &&
!mappedAvailableModels.some((model) => model.modelId === currentModelId)
) {
mappedAvailableModels.unshift({
modelId: currentModelId,
name: currentModelId,
description: null,
_meta: {
contextLimit: tokenLimit(currentModelId),
},
});
}
return {
currentModelId,
availableModels: mappedAvailableModels,
};
}
}

View File

@@ -15,6 +15,7 @@ export const AGENT_METHODS = {
session_prompt: 'session/prompt',
session_list: 'session/list',
session_set_mode: 'session/set_mode',
session_set_model: 'session/set_model',
};
export const CLIENT_METHODS = {
@@ -266,6 +267,18 @@ export const modelInfoSchema = z.object({
name: z.string(),
});
export const setModelRequestSchema = z.object({
sessionId: z.string(),
modelId: z.string(),
});
export const setModelResponseSchema = z.object({
modelId: z.string(),
});
export type SetModelRequest = z.infer<typeof setModelRequestSchema>;
export type SetModelResponse = z.infer<typeof setModelResponseSchema>;
export const sessionModelStateSchema = z.object({
_meta: acpMetaSchema,
availableModels: z.array(modelInfoSchema),
@@ -592,6 +605,7 @@ export const agentResponseSchema = z.union([
promptResponseSchema,
listSessionsResponseSchema,
setModeResponseSchema,
setModelResponseSchema,
]);
export const requestPermissionRequestSchema = z.object({
@@ -624,6 +638,7 @@ export const agentRequestSchema = z.union([
promptRequestSchema,
listSessionsRequestSchema,
setModeRequestSchema,
setModelRequestSchema,
]);
export const agentNotificationSchema = sessionNotificationSchema;

View File

@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Session } from './Session.js';
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import type * as acp from '../acp.js';
import type { LoadedSettings } from '../../config/settings.js';
import * as nonInteractiveCliCommands from '../../nonInteractiveCliCommands.js';
vi.mock('../../nonInteractiveCliCommands.js', () => ({
getAvailableCommands: vi.fn(),
handleSlashCommand: vi.fn(),
}));
describe('Session', () => {
let mockChat: GeminiChat;
let mockConfig: Config;
let mockClient: acp.Client;
let mockSettings: LoadedSettings;
let session: Session;
let currentModel: string;
let setModelSpy: ReturnType<typeof vi.fn>;
let getAvailableCommandsSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
currentModel = 'qwen3-code-plus';
setModelSpy = vi.fn().mockImplementation(async (modelId: string) => {
currentModel = modelId;
});
mockChat = {
sendMessageStream: vi.fn(),
addHistory: vi.fn(),
} as unknown as GeminiChat;
mockConfig = {
setApprovalMode: vi.fn(),
setModel: setModelSpy,
getModel: vi.fn().mockImplementation(() => currentModel),
} as unknown as Config;
mockClient = {
sessionUpdate: vi.fn().mockResolvedValue(undefined),
requestPermission: vi.fn().mockResolvedValue({
outcome: { outcome: 'selected', optionId: 'proceed_once' },
}),
sendCustomNotification: vi.fn().mockResolvedValue(undefined),
} as unknown as acp.Client;
mockSettings = {
merged: {},
} as LoadedSettings;
getAvailableCommandsSpy = vi.mocked(nonInteractiveCliCommands)
.getAvailableCommands as unknown as ReturnType<typeof vi.fn>;
getAvailableCommandsSpy.mockResolvedValue([]);
session = new Session(
'test-session-id',
mockChat,
mockConfig,
mockClient,
mockSettings,
);
});
describe('setMode', () => {
it.each([
['plan', ApprovalMode.PLAN],
['default', ApprovalMode.DEFAULT],
['auto-edit', ApprovalMode.AUTO_EDIT],
['yolo', ApprovalMode.YOLO],
] as const)('maps %s mode', async (modeId, expected) => {
const result = await session.setMode({
sessionId: 'test-session-id',
modeId,
});
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(expected);
expect(result).toEqual({ modeId });
});
});
describe('setModel', () => {
it('sets model via config and returns current model', async () => {
const result = await session.setModel({
sessionId: 'test-session-id',
modelId: ' qwen3-coder-plus ',
});
expect(mockConfig.setModel).toHaveBeenCalledWith('qwen3-coder-plus', {
reason: 'user_request_acp',
context: 'session/set_model',
});
expect(mockConfig.getModel).toHaveBeenCalled();
expect(result).toEqual({ modelId: 'qwen3-coder-plus' });
});
it('rejects empty/whitespace model IDs', async () => {
await expect(
session.setModel({
sessionId: 'test-session-id',
modelId: ' ',
}),
).rejects.toThrow('Invalid params');
expect(mockConfig.setModel).not.toHaveBeenCalled();
});
it('propagates errors from config.setModel', async () => {
const configError = new Error('Invalid model');
setModelSpy.mockRejectedValueOnce(configError);
await expect(
session.setModel({
sessionId: 'test-session-id',
modelId: 'invalid-model',
}),
).rejects.toThrow('Invalid model');
});
});
describe('sendAvailableCommandsUpdate', () => {
it('sends available_commands_update from getAvailableCommands()', async () => {
getAvailableCommandsSpy.mockResolvedValueOnce([
{
name: 'init',
description: 'Initialize project context',
},
]);
await session.sendAvailableCommandsUpdate();
expect(getAvailableCommandsSpy).toHaveBeenCalledWith(
mockConfig,
expect.any(AbortSignal),
);
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'test-session-id',
update: {
sessionUpdate: 'available_commands_update',
availableCommands: [
{
name: 'init',
description: 'Initialize project context',
input: null,
},
],
},
});
});
it('swallows errors and does not throw', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
getAvailableCommandsSpy.mockRejectedValueOnce(
new Error('Command discovery failed'),
);
await expect(
session.sendAvailableCommandsUpdate(),
).resolves.toBeUndefined();
expect(mockClient.sessionUpdate).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
});

View File

@@ -52,6 +52,8 @@ import type {
AvailableCommandsUpdate,
SetModeRequest,
SetModeResponse,
SetModelRequest,
SetModelResponse,
ApprovalModeValue,
CurrentModeUpdate,
} from '../schema.js';
@@ -348,6 +350,31 @@ export class Session implements SessionContext {
return { modeId: params.modeId };
}
/**
* Sets the model for the current session.
* Validates the model ID and switches the model via Config.
*/
async setModel(params: SetModelRequest): Promise<SetModelResponse> {
const modelId = params.modelId.trim();
if (!modelId) {
throw acp.RequestError.invalidParams('modelId cannot be empty');
}
// Attempt to set the model using config
await this.config.setModel(modelId, {
reason: 'user_request_acp',
context: 'session/set_model',
});
// Get updated model info
const currentModel = this.config.getModel();
return {
modelId: currentModel,
};
}
/**
* Sends a current_mode_update notification to the client.
* Called after the agent switches modes (e.g., from exit_plan_mode tool).

View File

@@ -334,7 +334,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
.option('experimental-skills', {
type: 'boolean',
description: 'Enable experimental Skills feature',
default: false,
default: settings.tools?.experimental?.skills ?? false,
})
.option('channel', {
type: 'string',

View File

@@ -981,6 +981,27 @@ const SETTINGS_SCHEMA = {
description: 'The number of lines to keep when truncating tool output.',
showInDialog: true,
},
experimental: {
type: 'object',
label: 'Experimental',
category: 'Tools',
requiresRestart: true,
default: {},
description: 'Experimental tool features.',
showInDialog: false,
properties: {
skills: {
type: 'boolean',
label: 'Skills',
category: 'Tools',
requiresRestart: true,
default: false,
description:
'Enable experimental Agent Skills feature. When enabled, Qwen Code can use Skills from .qwen/skills/ and ~/.qwen/skills/.',
showInDialog: true,
},
},
},
},
},

View File

@@ -873,11 +873,11 @@ export default {
'Session Stats': '会话统计',
'Model Usage': '模型使用情况',
Reqs: '请求数',
'Input Tokens': '输入令牌',
'Output Tokens': '输出令牌',
'Input Tokens': '输入 token 数',
'Output Tokens': '输出 token 数',
'Savings Highlight:': '节省亮点:',
'of input tokens were served from the cache, reducing costs.':
'的输入令牌来自缓存,降低了成本',
'从缓存载入 token ,降低了成本',
'Tip: For a full token breakdown, run `/stats model`.':
'提示:要查看完整的令牌明细,请运行 `/stats model`',
'Model Stats For Nerds': '模型统计(技术细节)',

View File

@@ -8,7 +8,10 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { updateSettingsFilePreservingFormat } from './commentJson.js';
import {
updateSettingsFilePreservingFormat,
applyUpdates,
} from './commentJson.js';
describe('commentJson', () => {
let tempDir: string;
@@ -180,3 +183,18 @@ describe('commentJson', () => {
});
});
});
describe('applyUpdates', () => {
it('should apply updates correctly', () => {
const original = { a: 1, b: { c: 2 } };
const updates = { b: { c: 3 } };
const result = applyUpdates(original, updates);
expect(result).toEqual({ a: 1, b: { c: 3 } });
});
it('should apply updates correctly when empty', () => {
const original = { a: 1, b: { c: 2 } };
const updates = { b: {} };
const result = applyUpdates(original, updates);
expect(result).toEqual({ a: 1, b: {} });
});
});

View File

@@ -38,7 +38,7 @@ export function updateSettingsFilePreservingFormat(
fs.writeFileSync(filePath, updatedContent, 'utf-8');
}
function applyUpdates(
export function applyUpdates(
current: Record<string, unknown>,
updates: Record<string, unknown>,
): Record<string, unknown> {
@@ -50,6 +50,7 @@ function applyUpdates(
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).length > 0 &&
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key])

View File

@@ -28,7 +28,6 @@ type RawMessageStreamEvent = Anthropic.RawMessageStreamEvent;
import { getDefaultTokenizer } from '../../utils/request-tokenizer/index.js';
import { safeJsonParse } from '../../utils/safeJsonParse.js';
import { AnthropicContentConverter } from './converter.js';
import { buildRuntimeFetchOptions } from '../../utils/runtimeFetchOptions.js';
type StreamingBlockState = {
type: string;
@@ -55,9 +54,6 @@ export class AnthropicContentGenerator implements ContentGenerator {
) {
const defaultHeaders = this.buildHeaders();
const baseURL = contentGeneratorConfig.baseUrl;
// Configure runtime options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let Anthropic SDK timeout control the request
const runtimeOptions = buildRuntimeFetchOptions('anthropic');
this.client = new Anthropic({
apiKey: contentGeneratorConfig.apiKey,
@@ -65,7 +61,6 @@ export class AnthropicContentGenerator implements ContentGenerator {
timeout: contentGeneratorConfig.timeout,
maxRetries: contentGeneratorConfig.maxRetries,
defaultHeaders,
...runtimeOptions,
});
this.converter = new AnthropicContentConverter(

View File

@@ -16,7 +16,6 @@ import type {
ChatCompletionContentPartWithCache,
ChatCompletionToolWithCache,
} from './types.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
export class DashScopeOpenAICompatibleProvider
implements OpenAICompatibleProvider
@@ -69,16 +68,12 @@ export class DashScopeOpenAICompatibleProvider
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;
const defaultHeaders = this.buildHeaders();
// Configure fetch options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
const fetchOptions = buildRuntimeFetchOptions('openai');
return new OpenAI({
apiKey,
baseURL: baseUrl,
timeout,
maxRetries,
defaultHeaders,
...(fetchOptions ? { fetchOptions } : {}),
});
}

View File

@@ -4,7 +4,6 @@ import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import type { OpenAICompatibleProvider } from './types.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
/**
* Default provider for standard OpenAI-compatible APIs
@@ -44,16 +43,12 @@ export class DefaultOpenAICompatibleProvider
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;
const defaultHeaders = this.buildHeaders();
// Configure fetch options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
const fetchOptions = buildRuntimeFetchOptions('openai');
return new OpenAI({
apiKey,
baseURL: baseUrl,
timeout,
maxRetries,
defaultHeaders,
...(fetchOptions ? { fetchOptions } : {}),
});
}

View File

@@ -1,167 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { EnvHttpProxyAgent } from 'undici';
/**
* JavaScript runtime type
*/
export type Runtime = 'node' | 'bun' | 'unknown';
/**
* Detect the current JavaScript runtime
*/
export function detectRuntime(): Runtime {
if (typeof process !== 'undefined' && process.versions?.['bun']) {
return 'bun';
}
if (typeof process !== 'undefined' && process.versions?.node) {
return 'node';
}
return 'unknown';
}
/**
* Runtime fetch options for OpenAI SDK
*/
export type OpenAIRuntimeFetchOptions =
| {
dispatcher?: EnvHttpProxyAgent;
timeout?: false;
}
| undefined;
/**
* Runtime fetch options for Anthropic SDK
*/
export type AnthropicRuntimeFetchOptions = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
httpAgent?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetch?: any;
};
/**
* SDK type identifier
*/
export type SDKType = 'openai' | 'anthropic';
/**
* Build runtime-specific fetch options for OpenAI SDK
*/
export function buildRuntimeFetchOptions(
sdkType: 'openai',
): OpenAIRuntimeFetchOptions;
/**
* Build runtime-specific fetch options for Anthropic SDK
*/
export function buildRuntimeFetchOptions(
sdkType: 'anthropic',
): AnthropicRuntimeFetchOptions;
/**
* Build runtime-specific fetch options based on the detected runtime and SDK type
* This function applies runtime-specific configurations to handle timeout differences
* across Node.js and Bun, ensuring user-configured timeout works as expected.
*
* @param sdkType - The SDK type ('openai' or 'anthropic') to determine return type
* @returns Runtime-specific options compatible with the specified SDK
*/
export function buildRuntimeFetchOptions(
sdkType: SDKType,
): OpenAIRuntimeFetchOptions | AnthropicRuntimeFetchOptions {
const runtime = detectRuntime();
// Always disable bodyTimeout (set to 0) to let SDK's timeout parameter
// control the total request time. bodyTimeout only monitors intervals between
// data chunks, not the total request time, so we disable it to ensure user-configured
// timeout works as expected for both streaming and non-streaming requests.
switch (runtime) {
case 'bun': {
if (sdkType === 'openai') {
// Bun: Disable built-in 300s timeout to let OpenAI SDK timeout control
// This ensures user-configured timeout works as expected without interference
return {
timeout: false,
};
} else {
// Bun: Use custom fetch to disable built-in 300s timeout
// This allows Anthropic SDK timeout to control the request
// Note: Bun's fetch automatically uses proxy settings from environment variables
// (HTTP_PROXY, HTTPS_PROXY, NO_PROXY), so proxy behavior is preserved
const bunFetch: typeof fetch = async (
input: RequestInfo | URL,
init?: RequestInit,
) => {
const bunFetchOptions: RequestInit = {
...init,
// @ts-expect-error - Bun-specific timeout option
timeout: false,
};
return fetch(input, bunFetchOptions);
};
return {
fetch: bunFetch,
};
}
}
case 'node': {
// Node.js: Use EnvHttpProxyAgent to configure proxy and disable bodyTimeout
// EnvHttpProxyAgent automatically reads proxy settings from environment variables
// (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, etc.) to preserve proxy functionality
// bodyTimeout is always 0 (disabled) to let SDK timeout control the request
try {
const agent = new EnvHttpProxyAgent({
bodyTimeout: 0, // Disable to let SDK timeout control total request time
});
if (sdkType === 'openai') {
return {
dispatcher: agent,
};
} else {
return {
httpAgent: agent,
};
}
} catch {
// If undici is not available, return appropriate default
if (sdkType === 'openai') {
return undefined;
} else {
return {};
}
}
}
default: {
// Unknown runtime: Try to use EnvHttpProxyAgent if available
// EnvHttpProxyAgent automatically reads proxy settings from environment variables
try {
const agent = new EnvHttpProxyAgent({
bodyTimeout: 0, // Disable to let SDK timeout control total request time
});
if (sdkType === 'openai') {
return {
dispatcher: agent,
};
} else {
return {
httpAgent: agent,
};
}
} catch {
if (sdkType === 'openai') {
return undefined;
} else {
return {};
}
}
}
}
}