qwen-code/packages/cli/src/utils/modelConfigUtils.test.ts
jinye 27f8f2c95d
feat(cli): Add serve env isolation and total admission (#6416)
* feat(cli): add serve env isolation and total admission

Add runtime-local serve env snapshots, explicit env injection for low-cost workspace-scoped consumers, and sourceEnv support for ACP child spawn.

Add a daemon-wide maxTotalSessions admission reservation hook for fresh session creation while keeping multi-workspace sessions gated.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6416)

Reject fractional maxTotalSessions values so the daemon-wide session cap remains an integer count and matches the documented limit semantics.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address PR review feedback (#6416)

Always pass the runtime env to A2UI stdio transports, keep daemon runtime env metadata coherent after env reload fallback, and tighten total-admission coverage.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address total admission review feedback (#6416)

Add retryable ACP error data for total session limits, log total-admission REST rejections, and keep session-limit response scopes explicit.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address env review feedback (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): restore scheduled task serve deps (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): isolate runtime env reload base (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6416)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix CI failure on PR #6416

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address daemon admission review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address runtime env review feedback

Scrub daemon bearer tokens from A2UI stdio MCP environments and prune reload-owned keys from the daemon runtime base before rebuilding runtime env snapshots.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): preserve daemon env base on reload

Keep runtime env rebuilds anchored to the boot-time daemon base snapshot, preventing reload-owned key pruning from dropping valid shell-exported values. Also carry env file read failure details into runtime metadata and daemon logs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): satisfy env metadata lint rules

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-08 00:52:36 +00:00

1828 lines
53 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
AuthType,
resolveModelConfig,
type ProviderModelConfig,
} from '@qwen-code/qwen-code-core';
import {
getAuthTypeFromEnv,
resolveCliGenerationConfig,
} from './modelConfigUtils.js';
import type { Settings } from '../config/settings.js';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...original,
resolveModelConfig: vi.fn(),
};
});
vi.mock('./stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
writeStdoutLine: vi.fn(),
clearScreen: vi.fn(),
}));
describe('modelConfigUtils', () => {
describe('getAuthTypeFromEnv', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
// Start with a clean env - getAuthTypeFromEnv only checks auth-related vars
process.env = {};
});
afterEach(() => {
process.env = originalEnv;
});
it('should return USE_OPENAI when all OpenAI env vars are set', () => {
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['OPENAI_MODEL'] = 'gpt-4';
process.env['OPENAI_BASE_URL'] = 'https://api.openai.com';
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_OPENAI);
});
it('uses an injected env instead of process.env when provided', () => {
process.env['OPENAI_API_KEY'] = 'process-key';
process.env['OPENAI_MODEL'] = 'process-model';
process.env['OPENAI_BASE_URL'] = 'https://process.example';
expect(
getAuthTypeFromEnv({
GEMINI_API_KEY: 'runtime-key',
GEMINI_MODEL: 'runtime-model',
}),
).toBe(AuthType.USE_GEMINI);
});
it('should return USE_OPENAI when the model is given via QWEN_MODEL', () => {
// QWEN_MODEL is a valid USE_OPENAI model var (see AUTH_ENV_MODEL_VARS),
// so a config that sets it instead of OPENAI_MODEL must still resolve.
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['QWEN_MODEL'] = 'qwen3-coder-plus';
process.env['OPENAI_BASE_URL'] =
'https://dashscope.aliyuncs.com/compatible-mode/v1';
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_OPENAI);
});
it('should return undefined when OpenAI env vars are incomplete', () => {
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['OPENAI_MODEL'] = 'gpt-4';
// Missing OPENAI_BASE_URL
expect(getAuthTypeFromEnv()).toBeUndefined();
});
it('should return QWEN_OAUTH when QWEN_OAUTH is set', () => {
process.env['QWEN_OAUTH'] = 'true';
expect(getAuthTypeFromEnv()).toBe(AuthType.QWEN_OAUTH);
});
it('should return USE_GEMINI when Gemini env vars are set', () => {
process.env['GEMINI_API_KEY'] = 'test-key';
process.env['GEMINI_MODEL'] = 'gemini-pro';
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_GEMINI);
});
it('should return undefined when Gemini env vars are incomplete', () => {
process.env['GEMINI_API_KEY'] = 'test-key';
// Missing GEMINI_MODEL
expect(getAuthTypeFromEnv()).toBeUndefined();
});
it('should return USE_VERTEX_AI when Google env vars are set', () => {
process.env['GOOGLE_API_KEY'] = 'test-key';
process.env['GOOGLE_MODEL'] = 'vertex-model';
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_VERTEX_AI);
});
it('should return undefined when Google env vars are incomplete', () => {
process.env['GOOGLE_API_KEY'] = 'test-key';
// Missing GOOGLE_MODEL
expect(getAuthTypeFromEnv()).toBeUndefined();
});
it('should return USE_ANTHROPIC when Anthropic env vars are set', () => {
process.env['ANTHROPIC_API_KEY'] = 'test-key';
process.env['ANTHROPIC_MODEL'] = 'claude-3';
process.env['ANTHROPIC_BASE_URL'] = 'https://api.anthropic.com';
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_ANTHROPIC);
});
it('should return undefined when Anthropic env vars are incomplete', () => {
process.env['ANTHROPIC_API_KEY'] = 'test-key';
process.env['ANTHROPIC_MODEL'] = 'claude-3';
// Missing ANTHROPIC_BASE_URL
expect(getAuthTypeFromEnv()).toBeUndefined();
});
it('should prioritize QWEN_OAUTH over other auth types when explicitly set', () => {
process.env['QWEN_OAUTH'] = 'true';
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['OPENAI_MODEL'] = 'gpt-4';
process.env['OPENAI_BASE_URL'] = 'https://api.openai.com';
// QWEN_OAUTH is checked first, so it should be returned even when other auth vars are set
expect(getAuthTypeFromEnv()).toBe(AuthType.QWEN_OAUTH);
});
it('should return undefined when no auth env vars are set', () => {
expect(getAuthTypeFromEnv()).toBeUndefined();
});
});
describe('resolveCliGenerationConfig', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
delete process.env['OPENAI_MODEL'];
delete process.env['QWEN_MODEL'];
mockWriteStderrLine.mockClear();
});
afterEach(() => {
process.env = originalEnv;
vi.clearAllMocks();
});
function makeMockSettings(overrides?: Partial<Settings>): Settings {
return {
model: { name: 'default-model' },
security: {
auth: {
apiKey: 'settings-api-key',
baseUrl: 'https://settings.example.com',
},
},
...overrides,
} as Settings;
}
it('should resolve config from argv with highest precedence', () => {
const argv = {
model: 'argv-model',
openaiApiKey: 'argv-key',
openaiBaseUrl: 'https://argv.example.com',
};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'argv-model',
apiKey: 'argv-key',
baseUrl: 'https://argv.example.com',
},
sources: {
model: { kind: 'cli', detail: '--model' },
apiKey: { kind: 'cli', detail: '--openaiApiKey' },
baseUrl: { kind: 'cli', detail: '--openaiBaseUrl' },
},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.model).toBe('argv-model');
expect(result.apiKey).toBe('argv-key');
expect(result.baseUrl).toBe('https://argv.example.com');
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
cli: {
model: 'argv-model',
apiKey: 'argv-key',
baseUrl: 'https://argv.example.com',
},
}),
);
});
it('should resolve config from settings when argv is not provided', () => {
const argv = {};
const settings = makeMockSettings({
model: { name: 'settings-model' },
security: {
auth: {
apiKey: 'settings-key',
baseUrl: 'https://settings.example.com',
},
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'settings-model',
apiKey: 'settings-key',
baseUrl: 'https://settings.example.com',
},
sources: {
model: { kind: 'settings', detail: 'model.name' },
apiKey: { kind: 'settings', detail: 'security.auth.apiKey' },
baseUrl: { kind: 'settings', detail: 'security.auth.baseUrl' },
},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.model).toBe('settings-model');
expect(result.apiKey).toBe('settings-key');
expect(result.baseUrl).toBe('https://settings.example.com');
});
it('should merge generationConfig from settings', () => {
const argv = {};
const settings = makeMockSettings({
model: {
name: 'test-model',
generationConfig: {
samplingParams: {
temperature: 0.7,
max_tokens: 1000,
},
timeout: 5000,
} as Record<string, unknown>,
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
samplingParams: {
temperature: 0.7,
max_tokens: 1000,
},
timeout: 5000,
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.generationConfig.samplingParams?.temperature).toBe(0.7);
expect(result.generationConfig.samplingParams?.max_tokens).toBe(1000);
expect(result.generationConfig.timeout).toBe(5000);
});
it('should resolve OpenAI logging from argv', () => {
const argv = {
openaiLogging: true,
openaiLoggingDir: '/custom/log/dir',
};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.generationConfig.enableOpenAILogging).toBe(true);
expect(result.generationConfig.openAILoggingDir).toBe('/custom/log/dir');
});
it('should resolve OpenAI logging from settings when argv is undefined', () => {
const argv = {};
const settings = makeMockSettings({
model: {
name: 'test-model',
enableOpenAILogging: true,
openAILoggingDir: '/settings/log/dir',
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.generationConfig.enableOpenAILogging).toBe(true);
expect(result.generationConfig.openAILoggingDir).toBe(
'/settings/log/dir',
);
});
it('should default OpenAI logging to false when not provided', () => {
const argv = {};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.generationConfig.enableOpenAILogging).toBe(false);
});
it('should find modelProvider from settings when authType and model match', () => {
const argv = { model: 'provider-model' };
const modelProvider: ProviderModelConfig = {
id: 'provider-model',
name: 'Provider Model',
generationConfig: {
samplingParams: { temperature: 0.8 },
},
};
const settings = makeMockSettings({
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'provider-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider,
}),
);
});
it('finds a custom-id provider via providerProtocol (honors its envKey/metadata)', () => {
const argv = { model: 'qwen3.7-max' };
const modelProvider: ProviderModelConfig = {
id: 'qwen3.7-max',
name: 'Idealab Max',
envKey: 'IDEALAB_KEY',
generationConfig: { samplingParams: { temperature: 0.5 } },
};
// Stored under the custom key `idealab`, NOT under `openai`.
const settings = makeMockSettings({
modelProviders: {
idealab: [modelProvider],
} as unknown as Settings['modelProviders'],
providerProtocol: {
idealab: 'openai',
} as unknown as Settings['providerProtocol'],
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'qwen3.7-max', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
// Without the reverse lookup, modelProviders['openai'] is empty and the
// custom provider's envKey would be lost.
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider }),
);
});
it('warns when a provider id has models but no resolvable protocol', () => {
const settings = makeMockSettings({
model: { name: 'some-model' },
modelProviders: {
idealab: [{ id: 'qwen3.7-max' }],
} as unknown as Settings['modelProviders'],
// No providerProtocol entry for `idealab`.
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'some-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(
result.warnings.some(
(w) => w.includes('idealab') && w.includes('providerProtocol'),
),
).toBe(true);
});
it('warns when providerProtocol maps to an unknown protocol', () => {
const settings = makeMockSettings({
model: { name: 'some-model' },
modelProviders: {
idealab: [{ id: 'qwen3.7-max' }],
} as unknown as Settings['modelProviders'],
providerProtocol: {
idealab: 'not-a-protocol',
} as unknown as Settings['providerProtocol'],
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'some-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(result.warnings).toEqual(
expect.arrayContaining([
expect.stringContaining(
'providerProtocol["idealab"] = "not-a-protocol" is not a known protocol',
),
]),
);
});
it('warns when a custom provider maps to qwen-oauth', () => {
const settings = makeMockSettings({
model: { name: 'some-model' },
modelProviders: {
idealab: [{ id: 'qwen3.7-max' }],
} as unknown as Settings['modelProviders'],
providerProtocol: {
idealab: AuthType.QWEN_OAUTH,
} as unknown as Settings['providerProtocol'],
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'some-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(result.warnings).toEqual(
expect.arrayContaining([
expect.stringContaining(
'modelProviders provider "idealab" maps to "qwen-oauth"',
),
]),
);
});
it('should find modelProvider from settings.model.name when argv.model is not provided', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'settings-model',
name: 'Settings Model',
generationConfig: {
samplingParams: { temperature: 0.9 },
},
};
const settings = makeMockSettings({
model: { name: 'settings-model' },
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'settings-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider,
}),
);
});
describe('provider disambiguation by settings.model.baseUrl', () => {
const tokenPlan: ProviderModelConfig = {
id: 'qwen3.7-max',
name: '[Token Plan] qwen3.7-max',
baseUrl: 'https://token-plan.example.com/v1',
envKey: 'TOKEN_PLAN_KEY',
};
const ideaLab: ProviderModelConfig = {
id: 'qwen3.7-max',
name: '[IdeaLab] qwen3.7-max',
baseUrl: 'https://idealab.example.com/v1',
envKey: 'IDEALAB_KEY',
};
function mockResolved() {
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'qwen3.7-max', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
}
it('selects the provider matching the persisted baseUrl, not the first id match', () => {
mockResolved();
const settings = makeMockSettings({
model: {
name: 'qwen3.7-max',
baseUrl: 'https://idealab.example.com/v1',
},
modelProviders: {
[AuthType.USE_OPENAI]: [tokenPlan, ideaLab],
},
});
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
// The IdeaLab provider (not the first-listed Token Plan) must be passed
// to resolveModelConfig, which is what makes sources.baseUrl.kind become
// 'modelProviders' and lets syncAfterAuthRefresh keep it after refresh.
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider: ideaLab }),
);
});
it('falls back to the first id match when no baseUrl is persisted (backward compat)', () => {
mockResolved();
const settings = makeMockSettings({
model: { name: 'qwen3.7-max' },
modelProviders: {
[AuthType.USE_OPENAI]: [tokenPlan, ideaLab],
},
});
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider: tokenPlan }),
);
});
it('falls back to the first id match and emits a warning when the persisted baseUrl no longer matches any provider', () => {
mockResolved();
const settings = makeMockSettings({
model: {
name: 'qwen3.7-max',
baseUrl: 'https://removed.example.com/v1',
},
modelProviders: {
[AuthType.USE_OPENAI]: [tokenPlan, ideaLab],
},
});
const result = resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider: tokenPlan }),
);
expect(result.warnings).toEqual(
expect.arrayContaining([
expect.stringContaining('https://removed.example.com/v1'),
]),
);
});
it('treats an empty-string tombstone as no disambiguator (first id match)', () => {
// A cleared selection persists model.baseUrl: '' so it can override a
// stale lower-scope value on merge; the resolver must treat '' as "no
// baseUrl" and fall back to the first id match rather than matching a
// provider whose baseUrl is literally empty.
mockResolved();
const settings = makeMockSettings({
model: { name: 'qwen3.7-max', baseUrl: '' },
modelProviders: {
[AuthType.USE_OPENAI]: [tokenPlan, ideaLab],
},
});
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider: tokenPlan }),
);
});
it('ignores the persisted baseUrl when the model comes from argv.model, not settings', () => {
mockResolved();
const settings = makeMockSettings({
model: {
name: 'something-else',
baseUrl: 'https://idealab.example.com/v1',
},
modelProviders: {
[AuthType.USE_OPENAI]: [tokenPlan, ideaLab],
},
});
resolveCliGenerationConfig({
argv: { model: 'qwen3.7-max' },
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
// argv-resolved model uses id-only lookup → first match.
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({ modelProvider: tokenPlan }),
);
});
});
it('strips a runtime snapshot prefix from settings.model.name (read-side self-heal)', () => {
const modelProvider: ProviderModelConfig = {
id: 'gpt-4o',
name: 'GPT-4o',
};
const settings = makeMockSettings({
model: { name: '$runtime|openai|gpt-4o' },
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'gpt-4o', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
// Both read-side strip sites: the bare id is used for the provider
// lookup and passed through to resolveModelConfig as settings.model.
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider,
settings: expect.objectContaining({ model: 'gpt-4o' }),
}),
);
});
it('collapses a stacked runtime snapshot prefix from settings.model.name', () => {
const modelProvider: ProviderModelConfig = {
id: 'gpt-4o',
name: 'GPT-4o',
};
const settings = makeMockSettings({
model: { name: '$runtime|openai|$runtime|openai|gpt-4o' },
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'gpt-4o', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider,
settings: expect.objectContaining({ model: 'gpt-4o' }),
}),
);
});
it('should not find modelProvider when authType is undefined', () => {
const argv = { model: 'test-model' };
const settings = makeMockSettings({
modelProviders: {
[AuthType.USE_OPENAI]: [{ id: 'test-model', name: 'Test Model' }],
},
});
const selectedAuthType = undefined;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: undefined,
}),
);
});
it('should not find modelProvider when modelProviders is not an array', () => {
const argv = { model: 'test-model' };
const settings = makeMockSettings({
modelProviders: {
[AuthType.USE_OPENAI]: null as unknown as ProviderModelConfig[],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: undefined,
}),
);
});
it('should return warnings from resolveModelConfig', () => {
const argv = {};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: ['Warning 1', 'Warning 2'],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toEqual(['Warning 1', 'Warning 2']);
});
it('should warn when top-level generationConfig fields are ignored for a provider model', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'qwen3.6-27b',
name: 'qwen3.6-27b',
baseUrl: 'http://localhost:8080/v1',
envKey: 'IK_LLAMA_API_KEY',
};
const settings = makeMockSettings({
model: {
name: 'qwen3.6-27b',
generationConfig: {
contextWindowSize: 192000,
extra_body: {
enable_thinking: true,
},
} as Record<string, unknown>,
},
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'qwen3.6-27b',
apiKey: '',
baseUrl: 'http://localhost:8080/v1',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain(
'model.generationConfig.contextWindowSize',
);
expect(result.warnings[0]).toContain('model.generationConfig.extra_body');
expect(result.warnings[0]).toContain(
'provider model "qwen3.6-27b" from modelProviders.openai',
);
expect(result.warnings[0]).toContain(
'modelProviders.openai[].generationConfig',
);
});
it('names the custom provider in ignored generationConfig warnings', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'qwen3.6-27b',
name: 'qwen3.6-27b',
generationConfig: {},
};
const settings = makeMockSettings({
model: {
name: 'qwen3.6-27b',
generationConfig: {
contextWindowSize: 192000,
} as Record<string, unknown>,
},
modelProviders: {
idealab: [modelProvider],
} as unknown as Settings['modelProviders'],
providerProtocol: {
idealab: AuthType.USE_OPENAI,
} as unknown as Settings['providerProtocol'],
});
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'qwen3.6-27b',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType: AuthType.USE_OPENAI,
});
expect(result.warnings[0]).toContain(
'provider model "qwen3.6-27b" from modelProviders.idealab',
);
expect(result.warnings[0]).toContain(
'modelProviders.idealab[].generationConfig',
);
expect(result.warnings[0]).not.toContain('modelProviders.openai');
});
it('should not warn for top-level generationConfig on runtime models', () => {
const argv = {};
const settings = makeMockSettings({
model: {
name: 'runtime-model',
generationConfig: {
contextWindowSize: 192000,
} as Record<string, unknown>,
},
modelProviders: {
[AuthType.USE_OPENAI]: [{ id: 'other-model', name: 'Other Model' }],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'runtime-model',
apiKey: '',
baseUrl: '',
contextWindowSize: 192000,
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toEqual([]);
});
it('should not warn for generationConfig fields defined by the provider model', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'provider-model',
name: 'Provider Model',
generationConfig: {
contextWindowSize: 192000,
},
};
const settings = makeMockSettings({
model: {
name: 'provider-model',
generationConfig: {
contextWindowSize: 128000,
} as Record<string, unknown>,
},
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'provider-model',
apiKey: '',
baseUrl: '',
contextWindowSize: 192000,
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toEqual([]);
});
it('should not warn for unknown top-level generationConfig fields', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'provider-model',
name: 'Provider Model',
};
const settings = makeMockSettings({
model: {
name: 'provider-model',
generationConfig: {
contextWindwSize: 128000,
} as Record<string, unknown>,
},
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'provider-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toEqual([]);
});
it('should append ignored generationConfig warnings to resolver warnings', () => {
const argv = {};
const modelProvider: ProviderModelConfig = {
id: 'provider-model',
name: 'Provider Model',
};
const settings = makeMockSettings({
model: {
name: 'provider-model',
generationConfig: {
modalities: { image: false },
} as Record<string, unknown>,
},
modelProviders: {
[AuthType.USE_OPENAI]: [modelProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'provider-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: ['Resolver warning'],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.warnings).toHaveLength(2);
expect(result.warnings[0]).toBe('Resolver warning');
expect(result.warnings[1]).toContain(
'model.generationConfig.modalities is ignored',
);
expect(result.warnings[1]).toContain('Move this field');
});
it('should use custom env when provided', () => {
const argv = {};
const settings = makeMockSettings({
model: undefined as unknown as Settings['model'],
});
const selectedAuthType = AuthType.USE_OPENAI;
const customEnv = {
OPENAI_API_KEY: 'custom-key',
OPENAI_MODEL: 'custom-model',
};
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'custom-model',
apiKey: 'custom-key',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: customEnv,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
env: customEnv,
}),
);
});
it('should use process.env (filtered) when env is not provided', () => {
const argv = {};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
// process.env is filtered: model env vars stripped since model came from settings
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.not.objectContaining({
OPENAI_MODEL: expect.anything(),
QWEN_MODEL: expect.anything(),
}),
}),
);
});
it('should return empty strings for missing model, apiKey, and baseUrl', () => {
const argv = {};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: '',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.model).toBe('');
expect(result.apiKey).toBe('');
expect(result.baseUrl).toBe('');
});
it('should merge resolved config with logging settings', () => {
const argv = {
openaiLogging: true,
};
const settings = makeMockSettings({
model: {
name: 'test-model',
generationConfig: {
timeout: 5000,
} as Record<string, unknown>,
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: 'test-model',
apiKey: 'test-key',
baseUrl: 'https://test.com',
samplingParams: { temperature: 0.5 },
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.generationConfig).toEqual({
model: 'test-model',
apiKey: 'test-key',
baseUrl: 'https://test.com',
samplingParams: { temperature: 0.5 },
enableOpenAILogging: true,
openAILoggingDir: undefined,
});
});
it('should handle settings without model property', () => {
const argv = {};
const settings = makeMockSettings({
model: undefined as unknown as Settings['model'],
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: '',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(result.model).toBe('');
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
settings: expect.objectContaining({
model: undefined,
}),
}),
);
});
it('should handle settings without security.auth property', () => {
const argv = {};
const settings = makeMockSettings({
security: undefined,
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: {
model: '',
apiKey: '',
baseUrl: '',
},
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
settings: expect.objectContaining({
apiKey: undefined,
baseUrl: undefined,
}),
}),
);
});
// Case A: settings.model.name wins over OPENAI_MODEL when it matches a modelProvider
it('Case A: should use settings.model.name for modelProvider lookup even when OPENAI_MODEL is set', () => {
const argv = {};
const settingsProvider: ProviderModelConfig = {
id: 'settings-model',
name: 'Settings Model',
generationConfig: { samplingParams: { temperature: 0.9 } },
};
const envProvider: ProviderModelConfig = {
id: 'env-model',
name: 'Env Model',
generationConfig: { samplingParams: { temperature: 0.5 } },
};
const settings = makeMockSettings({
model: { name: 'settings-model' },
modelProviders: {
[AuthType.USE_OPENAI]: [settingsProvider, envProvider],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'settings-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'env-model' },
});
// settings.model.name should win - modelProvider should be settingsProvider
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: settingsProvider,
}),
);
});
// Case B: OPENAI_MODEL is honored when settings.model.name is not set
it('Case B: should use OPENAI_MODEL when settings.model.name is not set', () => {
const argv = {};
const envProvider: ProviderModelConfig = {
id: 'env-model',
name: 'Env Model',
generationConfig: { samplingParams: { temperature: 0.7 } },
};
const settings = makeMockSettings({
model: undefined as unknown as Settings['model'],
modelProviders: {
[AuthType.USE_OPENAI]: [
{ id: 'other-model', name: 'Other Model' },
envProvider,
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'env-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'env-model' },
});
// OPENAI_MODEL should be used since settings.model.name is not set
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: envProvider,
}),
);
});
// Edge Case 1: argv.model overrides everything including settings.model.name and OPENAI_MODEL
it('Edge Case 1: argv.model should override settings.model.name and OPENAI_MODEL', () => {
const argv = { model: 'cli-model' };
const cliProvider: ProviderModelConfig = {
id: 'cli-model',
name: 'CLI Model',
};
const settings = makeMockSettings({
model: { name: 'settings-model' },
modelProviders: {
[AuthType.USE_OPENAI]: [
{ id: 'settings-model', name: 'Settings Model' },
{ id: 'env-model', name: 'Env Model' },
cliProvider,
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'cli-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'env-model' },
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: cliProvider,
}),
);
});
// Edge Case 2: QWEN_MODEL is used as final fallback when OPENAI_MODEL is not set
it('Edge Case 2: QWEN_MODEL should be used as fallback when OPENAI_MODEL is not set', () => {
const argv = {};
const qwenProvider: ProviderModelConfig = {
id: 'qwen-env-model',
name: 'Qwen Env Model',
};
const settings = makeMockSettings({
model: undefined as unknown as Settings['model'],
modelProviders: {
[AuthType.USE_OPENAI]: [
{ id: 'other-model', name: 'Other Model' },
qwenProvider,
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'qwen-env-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { QWEN_MODEL: 'qwen-env-model' },
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: qwenProvider,
}),
);
});
// Edge Case 3: OPENAI_MODEL over QWEN_MODEL when both are set and settings.model.name is not set
it('Edge Case 3: OPENAI_MODEL should win over QWEN_MODEL when both set', () => {
const argv = {};
const openAIProvider: ProviderModelConfig = {
id: 'openai-env-model',
name: 'OpenAI Env Model',
};
const qwenProvider: ProviderModelConfig = {
id: 'qwen-env-model',
name: 'Qwen Env Model',
};
const settings = makeMockSettings({
model: undefined as unknown as Settings['model'],
modelProviders: {
[AuthType.USE_OPENAI]: [
{ id: 'other-model', name: 'Other Model' },
openAIProvider,
qwenProvider,
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'openai-env-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: {
OPENAI_MODEL: 'openai-env-model',
QWEN_MODEL: 'qwen-env-model',
},
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: openAIProvider,
}),
);
});
// Edge Case 4: Non-OpenAI auth should ignore OPENAI_MODEL
it('Edge Case 4: non-OpenAI auth should ignore OPENAI_MODEL', () => {
const argv = {};
const settingsProvider: ProviderModelConfig = {
id: 'settings-model',
name: 'Settings Model',
};
const settings = makeMockSettings({
model: { name: 'settings-model' },
modelProviders: {
[AuthType.USE_ANTHROPIC]: [settingsProvider],
},
});
const selectedAuthType = AuthType.USE_ANTHROPIC;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'settings-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'some-other-model' },
});
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: settingsProvider,
}),
);
});
// Edge Case 5: settings.model.name does NOT fall back to OPENAI_MODEL when unmatched
it('Edge Case 5: settings.model.name should NOT fall back to OPENAI_MODEL when unmatched', () => {
const argv = {};
const envProvider: ProviderModelConfig = {
id: 'env-model',
name: 'Env Model',
};
const settings = makeMockSettings({
model: { name: 'non-existent-model' },
modelProviders: {
[AuthType.USE_OPENAI]: [
{ id: 'other-model', name: 'Other Model' },
envProvider,
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
vi.mocked(resolveModelConfig).mockReturnValue({
config: { model: 'non-existent-model', apiKey: '', baseUrl: '' },
sources: {},
warnings: [],
});
resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'env-model' },
});
// settings.model.name takes precedence even when unmatched - no provider fallback
expect(vi.mocked(resolveModelConfig)).toHaveBeenCalledWith(
expect.objectContaining({
modelProvider: undefined,
}),
);
});
// Integration test: settings.model.name unmatched + OPENAI_MODEL matched
it('Integration: settings.model.name wins over OPENAI_MODEL even when unmatched', () => {
const argv = {};
const settings = makeMockSettings({
model: { name: 'custom-model' },
modelProviders: {
[AuthType.USE_OPENAI]: [
{
id: 'gpt-4',
name: 'GPT-4',
generationConfig: { samplingParams: { temperature: 0.5 } },
},
],
},
});
const selectedAuthType = AuthType.USE_OPENAI;
// Mock resolveModelConfig to simulate real behavior:
// modelProvider.id > cli.model > env > settings.model
vi.mocked(resolveModelConfig).mockImplementation((input) => {
const model =
input?.modelProvider?.id ||
input?.cli?.model ||
input?.env?.['OPENAI_MODEL'] ||
input?.settings?.model ||
'';
return {
config: { model, apiKey: '', baseUrl: '' },
sources: {
model:
model === 'custom-model'
? { kind: 'settings' as const, path: 'model.name' }
: { kind: 'env' as const, envKey: 'OPENAI_MODEL' },
},
warnings: [],
};
});
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env: { OPENAI_MODEL: 'gpt-4' },
});
// settings.model.name should be used (no provider found, so modelProvider is undefined)
expect(result.model).toBe('custom-model');
});
describe('[Regression] model precedence', () => {
it('[Regression] settings.model.name must NOT be overridden by OPENAI_MODEL', () => {
// This is the core bug: settings.model.name (set via /model)
// must take precedence over OPENAI_MODEL.
const settings = makeMockSettings({
model: { name: 'settings-model' },
});
const selectedAuthType = AuthType.USE_OPENAI;
const env = { OPENAI_MODEL: 'env-model', OPENAI_API_KEY: 'key' };
// Mock: settings.model.name should be used, not OPENAI_MODEL
vi.mocked(resolveModelConfig).mockImplementation(() => ({
config: { model: 'settings-model', apiKey: 'key', baseUrl: '' },
sources: {
model: { kind: 'settings' as const, path: 'model.name' },
},
warnings: [],
}));
const result = resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType,
env,
});
expect(result.model).toBe('settings-model');
// Verify OPENAI_MODEL was filtered from env passed to resolveModelConfig
const callArgs = vi.mocked(resolveModelConfig).mock.calls[0][0];
expect(callArgs.env?.['OPENAI_MODEL']).toBeUndefined();
});
it('[Regression] OPENAI_MODEL used when settings.model.name not set', () => {
const settings = makeMockSettings({ model: { name: undefined } });
const selectedAuthType = AuthType.USE_OPENAI;
const env = { OPENAI_MODEL: 'env-model', OPENAI_API_KEY: 'key' };
vi.mocked(resolveModelConfig).mockImplementation(() => ({
config: { model: 'env-model', apiKey: 'key', baseUrl: '' },
sources: {
model: { kind: 'env' as const, envKey: 'OPENAI_MODEL' },
},
warnings: [],
}));
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType,
env,
});
// OPENAI_MODEL should NOT be filtered (it's the source of the model)
const callArgs = vi.mocked(resolveModelConfig).mock.calls[0][0];
expect(callArgs.env?.['OPENAI_MODEL']).toBe('env-model');
});
it('[Regression] argv.model overrides both settings and OPENAI_MODEL', () => {
const argv = { model: 'argv-model' };
const settings = makeMockSettings({
model: { name: 'settings-model' },
});
const selectedAuthType = AuthType.USE_OPENAI;
const env = { OPENAI_MODEL: 'env-model', OPENAI_API_KEY: 'key' };
vi.mocked(resolveModelConfig).mockImplementation(() => ({
config: { model: 'argv-model', apiKey: 'key', baseUrl: '' },
sources: { model: { kind: 'cli' as const, detail: '--model' } },
warnings: [],
}));
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env,
});
expect(result.model).toBe('argv-model');
// Both settings and env should be filtered when argv.model is set
const callArgs = vi.mocked(resolveModelConfig).mock.calls[0][0];
expect(callArgs.env?.['OPENAI_MODEL']).toBeUndefined();
});
it('[Regression] QWEN_MODEL as fallback when OPENAI_MODEL not set', () => {
const settings = makeMockSettings({ model: { name: undefined } });
const selectedAuthType = AuthType.USE_OPENAI;
const env = { QWEN_MODEL: 'qwen-model', OPENAI_API_KEY: 'key' };
vi.mocked(resolveModelConfig).mockImplementation(() => ({
config: { model: 'qwen-model', apiKey: 'key', baseUrl: '' },
sources: { model: { kind: 'env' as const, envKey: 'QWEN_MODEL' } },
warnings: [],
}));
resolveCliGenerationConfig({
argv: {},
settings,
selectedAuthType,
env,
});
// QWEN_MODEL should be passed to resolveModelConfig
const callArgs = vi.mocked(resolveModelConfig).mock.calls[0][0];
expect(callArgs.env?.['QWEN_MODEL']).toBe('qwen-model');
});
it('[Regression] Non-OpenAI auth ignores OPENAI_MODEL', () => {
const argv = {};
const settings = makeMockSettings();
const selectedAuthType = AuthType.USE_ANTHROPIC;
const env = {
OPENAI_MODEL: 'should-be-ignored',
ANTHROPIC_API_KEY: 'key',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
};
vi.mocked(resolveModelConfig).mockImplementation(() => ({
config: {
model: 'claude-3',
apiKey: 'key',
baseUrl: 'https://api.anthropic.com',
},
sources: {
model: { kind: 'env' as const, envKey: 'ANTHROPIC_MODEL' },
},
warnings: [],
}));
const result = resolveCliGenerationConfig({
argv,
settings,
selectedAuthType,
env,
});
// For non-OpenAI auth, OPENAI_MODEL should not be in the model resolution
expect(result.model).toBe('claude-3');
const callArgs = vi.mocked(resolveModelConfig).mock.calls[0][0];
expect(callArgs.env?.['OPENAI_MODEL']).toBeUndefined();
});
});
});
});