mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 01:01:42 +00:00
fix(coding-agent): allow provider-scoped custom model ids (#1759)
This commit is contained in:
parent
693187a3fb
commit
6f4bd814b8
2 changed files with 73 additions and 7 deletions
|
|
@ -113,6 +113,22 @@ export interface ParsedModelResult {
|
||||||
warning: string | undefined;
|
warning: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFallbackModel(provider: string, modelId: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
||||||
|
const providerModels = availableModels.filter((m) => m.provider === provider);
|
||||||
|
if (providerModels.length === 0) return undefined;
|
||||||
|
|
||||||
|
const defaultId = defaultModelPerProvider[provider as KnownProvider];
|
||||||
|
const baseModel = defaultId
|
||||||
|
? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])
|
||||||
|
: providerModels[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseModel,
|
||||||
|
id: modelId,
|
||||||
|
name: modelId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a pattern to extract model and thinking level.
|
* Parse a pattern to extract model and thinking level.
|
||||||
* Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).
|
* Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).
|
||||||
|
|
@ -387,6 +403,16 @@ export function resolveCliModel(options: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
const fallbackModel = buildFallbackModel(provider, pattern, availableModels);
|
||||||
|
if (fallbackModel) {
|
||||||
|
const fallbackWarning = warning
|
||||||
|
? `${warning} Model "${pattern}" not found for provider "${provider}". Using custom model id.`
|
||||||
|
: `Model "${pattern}" not found for provider "${provider}". Using custom model id.`;
|
||||||
|
return { model: fallbackModel, thinkingLevel: undefined, warning: fallbackWarning, error: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const display = provider ? `${provider}/${pattern}` : cliModel;
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
||||||
return {
|
return {
|
||||||
model: undefined,
|
model: undefined,
|
||||||
|
|
@ -436,12 +462,18 @@ export async function findInitialModel(options: {
|
||||||
|
|
||||||
// 1. CLI args take priority
|
// 1. CLI args take priority
|
||||||
if (cliProvider && cliModel) {
|
if (cliProvider && cliModel) {
|
||||||
const found = modelRegistry.find(cliProvider, cliModel);
|
const resolved = resolveCliModel({
|
||||||
if (!found) {
|
cliProvider,
|
||||||
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
cliModel,
|
||||||
|
modelRegistry,
|
||||||
|
});
|
||||||
|
if (resolved.error) {
|
||||||
|
console.error(chalk.red(resolved.error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
return { model: found, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
if (resolved.model) {
|
||||||
|
return { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Use first model from scoped models (skip if continuing/resuming)
|
// 2. Use first model from scoped models (skip if continuing/resuming)
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ describe("resolveCliModel", () => {
|
||||||
expect(result.model?.id).toBe("openai/gpt-4o:extended");
|
expect(result.model?.id).toBe("openai/gpt-4o:extended");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not strip invalid :suffix as thinking level in --model (fail fast)", () => {
|
test("does not strip invalid :suffix as thinking level in --model (treat as raw id)", () => {
|
||||||
const registry = {
|
const registry = {
|
||||||
getAll: () => allModels,
|
getAll: () => allModels,
|
||||||
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
||||||
|
|
@ -279,8 +279,25 @@ describe("resolveCliModel", () => {
|
||||||
modelRegistry: registry,
|
modelRegistry: registry,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.model).toBeUndefined();
|
expect(result.error).toBeUndefined();
|
||||||
expect(result.error).toContain("not found");
|
expect(result.model?.provider).toBe("openai");
|
||||||
|
expect(result.model?.id).toBe("gpt-4o:extended");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows custom model ids for explicit providers without double prefixing", () => {
|
||||||
|
const registry = {
|
||||||
|
getAll: () => allModels,
|
||||||
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
||||||
|
|
||||||
|
const result = resolveCliModel({
|
||||||
|
cliProvider: "openrouter",
|
||||||
|
cliModel: "openrouter/openai/ghost-model",
|
||||||
|
modelRegistry: registry,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
expect(result.model?.provider).toBe("openrouter");
|
||||||
|
expect(result.model?.id).toBe("openai/ghost-model");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns a clear error when there are no models", () => {
|
test("returns a clear error when there are no models", () => {
|
||||||
|
|
@ -360,6 +377,23 @@ describe("default model selection", () => {
|
||||||
expect(defaultModelPerProvider["vercel-ai-gateway"]).toBe("anthropic/claude-opus-4-6");
|
expect(defaultModelPerProvider["vercel-ai-gateway"]).toBe("anthropic/claude-opus-4-6");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("findInitialModel accepts explicit provider custom model ids", async () => {
|
||||||
|
const registry = {
|
||||||
|
getAll: () => allModels,
|
||||||
|
} as unknown as Parameters<typeof findInitialModel>[0]["modelRegistry"];
|
||||||
|
|
||||||
|
const result = await findInitialModel({
|
||||||
|
cliProvider: "openrouter",
|
||||||
|
cliModel: "openrouter/openai/ghost-model",
|
||||||
|
scopedModels: [],
|
||||||
|
isContinuing: false,
|
||||||
|
modelRegistry: registry,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.model?.provider).toBe("openrouter");
|
||||||
|
expect(result.model?.id).toBe("openai/ghost-model");
|
||||||
|
});
|
||||||
|
|
||||||
test("findInitialModel selects ai-gateway default when available", async () => {
|
test("findInitialModel selects ai-gateway default when available", async () => {
|
||||||
const aiGatewayModel: Model<"anthropic-messages"> = {
|
const aiGatewayModel: Model<"anthropic-messages"> = {
|
||||||
id: "anthropic/claude-opus-4-6",
|
id: "anthropic/claude-opus-4-6",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue