fix(coding-agent): custom models for built-in providers and list-models error surfacing (#3072)

* fix(coding-agent): surface models.json load errors on stderr in --list-models

When models.json has validation errors, --list-models silently
discarded custom models and overrides without any user-visible
feedback. Now prints the error to stderr via chalk.yellow warning
before listing available models.

* fix(coding-agent): allow custom models for built-in providers in models.json

Built-in providers (openrouter, anthropic, etc.) already have api and
baseUrl on every model, and auth comes from env vars / auth storage.
Relax validation so custom models under built-in providers don't need
redundant baseUrl, apiKey, or api fields. Inherit them from the first
built-in model for that provider.

fixes #2921

---------

Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
Aliou Diallo
2026-04-14 20:32:18 +02:00
committed by GitHub
parent dcb1f422c4
commit ec4d413fc9
4 changed files with 81 additions and 8 deletions

View File

@@ -192,6 +192,49 @@ describe("ModelRegistry", () => {
});
describe("custom models merge behavior", () => {
test("built-in provider custom models inherit api and baseUrl without explicit fields", () => {
// Built-in providers already have api/baseUrl on every model, and auth
// comes from env vars / auth storage. No need to specify them.
writeRawModelsJson({
openrouter: {
models: [
{
id: "fake-provider/fake-model",
name: "Fake model",
reasoning: true,
input: ["text"],
},
],
},
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(registry.getError()).toBeUndefined();
const model = registry.find("openrouter", "fake-provider/fake-model");
expect(model).toBeDefined();
expect(model?.api).toBe("openai-completions");
expect(model?.baseUrl).toBe("https://openrouter.ai/api/v1");
});
test("non-built-in provider custom models still require baseUrl and apiKey", () => {
writeRawModelsJson({
"my-custom-provider": {
models: [
{
id: "my-model",
api: "openai-completions",
reasoning: false,
input: ["text"],
},
],
},
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(registry.getError()).toContain("baseUrl");
});
test("custom provider with same name as built-in merges with built-in models", () => {
writeModelsJson({
anthropic: providerConfig("https://my-proxy.example.com/v1", [{ id: "claude-custom" }]),