fix(coding-agent): honor --model selection, thinking, and --api-key

This commit is contained in:
Armin Ronacher 2026-02-06 23:55:22 +01:00 committed by Mario Zechner
parent 7ccf809a5d
commit 56342258e1
4 changed files with 286 additions and 21 deletions

View file

@ -126,7 +126,11 @@ export interface ParsedModelResult {
*
* @internal Exported for testing
*/
export function parseModelPattern(pattern: string, availableModels: Model<Api>[]): ParsedModelResult {
export function parseModelPattern(
pattern: string,
availableModels: Model<Api>[],
options?: { allowInvalidThinkingLevelFallback?: boolean },
): ParsedModelResult {
// Try exact match first
const exactMatch = tryMatchModel(pattern, availableModels);
if (exactMatch) {
@ -145,7 +149,7 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
if (isValidThinkingLevel(suffix)) {
// Valid thinking level - recurse on prefix and use this level
const result = parseModelPattern(prefix, availableModels);
const result = parseModelPattern(prefix, availableModels, options);
if (result.model) {
// Only use this thinking level if no warning from inner recursion
return {
@ -156,8 +160,16 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
}
return result;
} else {
// Invalid suffix - recurse on prefix and warn
const result = parseModelPattern(prefix, availableModels);
// Invalid suffix
const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;
if (!allowFallback) {
// In strict mode (CLI --model parsing), treat it as part of the model id and fail.
// This avoids accidentally resolving to a different model.
return { model: undefined, thinkingLevel: undefined, warning: undefined };
}
// Scope mode: recurse on prefix and warn
const result = parseModelPattern(prefix, availableModels, options);
if (result.model) {
return {
model: result.model,
@ -240,6 +252,116 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
return scopedModels;
}
export interface ResolveCliModelResult {
model: Model<Api> | undefined;
thinkingLevel?: ThinkingLevel;
warning: string | undefined;
/**
* Error message suitable for CLI display.
* When set, model will be undefined.
*/
error: string | undefined;
}
/**
* Resolve a single model from CLI flags.
*
* Supports:
* - --provider <provider> --model <pattern>
* - --model <provider>/<pattern>
* - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)
*
* Note: This does not apply the thinking level by itself, but it may *parse* and
* return a thinking level from "<pattern>:<thinking>" so the caller can apply it.
*/
export async function resolveCliModel(options: {
cliProvider?: string;
cliModel?: string;
modelRegistry: ModelRegistry;
}): Promise<ResolveCliModelResult> {
const { cliProvider, cliModel, modelRegistry } = options;
if (!cliModel) {
return { model: undefined, warning: undefined, error: undefined };
}
// Important: use *all* models here, not just models with pre-configured auth.
// This allows "--api-key" to be used for first-time setup.
const availableModels = modelRegistry.getAll();
if (availableModels.length === 0) {
return {
model: undefined,
warning: undefined,
error: "No models available. Check your installation or add models to models.json.",
};
}
// Build canonical provider lookup (case-insensitive)
const providerMap = new Map<string, string>();
for (const m of availableModels) {
providerMap.set(m.provider.toLowerCase(), m.provider);
}
let provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;
if (cliProvider && !provider) {
return {
model: undefined,
warning: undefined,
error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
};
}
// If no explicit --provider, first try exact matches without any provider inference.
// This avoids misinterpreting model IDs that themselves contain slashes (e.g. OpenRouter-style IDs).
if (!provider) {
const lower = cliModel.toLowerCase();
const exact = availableModels.find(
(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,
);
if (exact) {
return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
}
}
let pattern = cliModel;
// If no explicit --provider, allow --model provider/<pattern>
if (!provider) {
const slashIndex = cliModel.indexOf("/");
if (slashIndex !== -1) {
const maybeProvider = cliModel.substring(0, slashIndex);
const canonical = providerMap.get(maybeProvider.toLowerCase());
if (canonical) {
provider = canonical;
pattern = cliModel.substring(slashIndex + 1);
}
}
} else {
// If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix
const prefix = `${provider}/`;
if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {
pattern = cliModel.substring(prefix.length);
}
}
const candidates = provider ? availableModels.filter((m) => m.provider === provider) : availableModels;
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {
allowInvalidThinkingLevelFallback: false,
});
if (!model) {
const display = provider ? `${provider}/${pattern}` : cliModel;
return {
model: undefined,
thinkingLevel: undefined,
warning,
error: `Model "${display}" not found. Use --list-models to see available models.`,
};
}
return { model, thinkingLevel, warning, error: undefined };
}
export interface InitialModelResult {
model: Model<Api> | undefined;
thinkingLevel: ThinkingLevel;