Skip to content
Merged
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
169 changes: 169 additions & 0 deletions packages/core/src/models/modelConfigResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,4 +685,173 @@ describe('modelConfigResolver', () => {
expect(result.errors[0].message).toContain('envKey');
});
});

describe('[Regression] timeout env override refactor', () => {
it('[Regression] OAuth path must apply QWEN_CODE_API_TIMEOUT_MS (was broken before fix #3629)', () => {
// Guards against the original bug where resolveQwenOAuthConfig()
// returned before applying the env override.
const result = resolveModelConfig({
authType: AuthType.QWEN_OAUTH,
cli: {},
settings: {},
env: {
QWEN_CODE_API_TIMEOUT_MS: '45000',
},
});

expect(result.config.timeout).toBe(45000);
expect(result.sources['timeout']).toBeDefined();
expect(result.sources['timeout'].kind).toBe('env');
expect(result.sources['timeout'].envKey).toBe('QWEN_CODE_API_TIMEOUT_MS');
expect(result.config.model).toBe(DEFAULT_QWEN_MODEL);
});

it('[Regression] non-OAuth path must apply QWEN_CODE_API_TIMEOUT_MS', () => {
const result = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: { apiKey: 'key' },
env: {
OPENAI_API_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '900000',
},
});

expect(result.config.timeout).toBe(900000);
expect(result.sources['timeout'].kind).toBe('env');
expect(result.sources['timeout'].envKey).toBe('QWEN_CODE_API_TIMEOUT_MS');
});

it('[Regression] modelProvider timeout must win over env in both paths', () => {
// Non-OAuth
const nonOAuth = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: {},
env: {
MY_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '900000',
},
modelProvider: {
id: 'model',
name: 'Model',
envKey: 'MY_KEY',
baseUrl: 'https://api.example.com',
generationConfig: { timeout: 60000 },
},
});
expect(nonOAuth.config.timeout).toBe(60000);
expect(nonOAuth.sources['timeout'].kind).toBe('modelProviders');

// OAuth
const oauth = resolveModelConfig({
authType: AuthType.QWEN_OAUTH,
cli: {},
settings: {},
env: {
QWEN_CODE_API_TIMEOUT_MS: '45000',
},
modelProvider: {
id: 'qwen-oauth',
name: 'Qwen OAuth',
generationConfig: { timeout: 120000 },
},
});
expect(oauth.config.timeout).toBe(120000);
expect(oauth.sources['timeout'].kind).toBe('modelProviders');
});

it('[Regression] refactor must not alter precedence: env > settings', () => {
const result = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: {
apiKey: 'key',
generationConfig: { timeout: 30000 },
},
env: {
OPENAI_API_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '900000',
},
});

// env must override settings
expect(result.config.timeout).toBe(900000);
expect(result.sources['timeout'].kind).toBe('env');
});
});

describe('[Additional] timeout env override edge cases', () => {
it('handles scientific notation in QWEN_CODE_API_TIMEOUT_MS', () => {
const result = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: { apiKey: 'key' },
env: {
OPENAI_API_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '1.5e5',
},
});

expect(result.config.timeout).toBe(150000);
expect(result.sources['timeout'].kind).toBe('env');
});

it('handles hex values in QWEN_CODE_API_TIMEOUT_MS', () => {
const result = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: { apiKey: 'key', generationConfig: { timeout: 30000 } },
env: {
OPENAI_API_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '0x2BF20', // 180000 in hex
},
});

expect(result.config.timeout).toBe(180000);
expect(result.sources['timeout'].kind).toBe('env');
});

it('ignores empty string QWEN_CODE_API_TIMEOUT_MS', () => {
const result = resolveModelConfig({
authType: AuthType.USE_OPENAI,
cli: {},
settings: { apiKey: 'key', generationConfig: { timeout: 30000 } },
env: {
OPENAI_API_KEY: 'key',
QWEN_CODE_API_TIMEOUT_MS: '',
},
});

expect(result.config.timeout).toBe(30000);
expect(result.sources['timeout'].kind).toBe('settings');
});

it('applies env override for every supported auth type', () => {
const authTypes = [
{ type: AuthType.USE_OPENAI, env: { OPENAI_API_KEY: 'key' } },
{
type: AuthType.USE_ANTHROPIC,
env: {
ANTHROPIC_API_KEY: 'key',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
},
},
];

for (const { type, env } of authTypes) {
const result = resolveModelConfig({
authType: type,
cli: {},
settings: {
...(type === AuthType.USE_OPENAI ? { apiKey: 'key' } : {}),
},
env: { ...env, QWEN_CODE_API_TIMEOUT_MS: '99999' },
});

expect(result.config.timeout).toBe(99999);
expect(result.sources['timeout'].kind).toBe('env');
}
});
});
});
60 changes: 28 additions & 32 deletions packages/core/src/models/modelConfigResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ export interface ModelConfigResolutionResult {
warnings: string[];
}

/**
* Applies QWEN_CODE_API_TIMEOUT_MS env override if modelProvider has not set a timeout.
* Precedence: modelProvider > env > settings > default
* Mutates generationConfig and sources in-place.
*/
function applyTimeoutEnvOverride(
env: Record<string, string | undefined>,
generationConfig: Partial<ContentGeneratorConfig>,
sources: ConfigSources,
modelProvider?: ModelProviderConfig,
): void {
if (modelProvider?.generationConfig?.timeout !== undefined) return;

const raw = env['QWEN_CODE_API_TIMEOUT_MS'];
if (raw === undefined) return;

const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed > 0) {
generationConfig.timeout = Math.floor(parsed);
sources['timeout'] = {
kind: 'env',
envKey: 'QWEN_CODE_API_TIMEOUT_MS',
};
}
}

/**
* Resolve model configuration from all input sources.
*
Expand Down Expand Up @@ -247,22 +273,7 @@ export function resolveModelConfig(
);

// ---- Env override: QWEN_CODE_API_TIMEOUT_MS ----
// Precedence: modelProvider > env > settings > default (CLI doesn't set timeout)
const modelProviderSetTimeout =
modelProvider?.generationConfig?.timeout !== undefined;
if (!modelProviderSetTimeout) {
const envTimeout = env['QWEN_CODE_API_TIMEOUT_MS'];
if (envTimeout !== undefined) {
const parsed = Number(envTimeout);
if (Number.isFinite(parsed) && parsed > 0) {
generationConfig.timeout = Math.floor(parsed);
sources['timeout'] = {
kind: 'env',
envKey: 'QWEN_CODE_API_TIMEOUT_MS',
};
}
}
}
applyTimeoutEnvOverride(env, generationConfig, sources, modelProvider);

// Build final config
const config: ContentGeneratorConfig = {
Expand Down Expand Up @@ -343,22 +354,7 @@ function resolveQwenOAuthConfig(
);

// ---- Env override: QWEN_CODE_API_TIMEOUT_MS ----
// Precedence: modelProvider > env > settings > default
const modelProviderSetTimeoutOAuth =
modelProvider?.generationConfig?.timeout !== undefined;
if (!modelProviderSetTimeoutOAuth) {
const envTimeoutOAuth = input.env['QWEN_CODE_API_TIMEOUT_MS'];
if (envTimeoutOAuth !== undefined) {
const parsed = Number(envTimeoutOAuth);
if (Number.isFinite(parsed) && parsed > 0) {
generationConfig.timeout = Math.floor(parsed);
sources['timeout'] = {
kind: 'env',
envKey: 'QWEN_CODE_API_TIMEOUT_MS',
};
}
}
}
applyTimeoutEnvOverride(input.env, generationConfig, sources, modelProvider);

const config: ContentGeneratorConfig = {
authType: AuthType.QWEN_OAUTH,
Expand Down
Loading