feat(ai,coding-agent): add faux provider and ModelRegistry factories

This commit is contained in:
Mario Zechner
2026-03-29 21:08:50 +02:00
parent fa890e3f94
commit ef6af5ebbd
31 changed files with 1425 additions and 94 deletions

View File

@@ -94,7 +94,7 @@ describe("ModelRegistry", () => {
anthropic: overrideConfig("https://my-proxy.example.com/v1"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const anthropicModels = getModelsForProvider(registry, "anthropic");
// Should have multiple built-in models, not just one
@@ -107,7 +107,7 @@ describe("ModelRegistry", () => {
anthropic: overrideConfig("https://my-proxy.example.com/v1"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const anthropicModels = getModelsForProvider(registry, "anthropic");
// All models should have the new baseUrl
@@ -123,7 +123,7 @@ describe("ModelRegistry", () => {
}),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const anthropicModels = getModelsForProvider(registry, "anthropic");
for (const model of anthropicModels) {
@@ -140,7 +140,7 @@ describe("ModelRegistry", () => {
anthropic: overrideConfig("https://my-proxy.example.com/v1"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const googleModels = getModelsForProvider(registry, "google");
// Google models should still have their original baseUrl
@@ -160,7 +160,7 @@ describe("ModelRegistry", () => {
),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
// Anthropic: multiple built-in models with new baseUrl
const anthropicModels = getModelsForProvider(registry, "anthropic");
@@ -177,7 +177,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
anthropic: overrideConfig("https://first-proxy.example.com/v1"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(getModelsForProvider(registry, "anthropic")[0].baseUrl).toBe("https://first-proxy.example.com/v1");
@@ -197,7 +197,7 @@ describe("ModelRegistry", () => {
anthropic: providerConfig("https://my-proxy.example.com/v1", [{ id: "claude-custom" }]),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const anthropicModels = getModelsForProvider(registry, "anthropic");
expect(anthropicModels.length).toBeGreaterThan(1);
@@ -214,7 +214,7 @@ describe("ModelRegistry", () => {
),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnetModels = models.filter((m) => m.id === "anthropic/claude-sonnet-4");
@@ -227,7 +227,7 @@ describe("ModelRegistry", () => {
anthropic: providerConfig("https://my-proxy.example.com/v1", [{ id: "claude-custom" }]),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(getModelsForProvider(registry, "google").length).toBeGreaterThan(0);
expect(getModelsForProvider(registry, "openai").length).toBeGreaterThan(0);
@@ -238,7 +238,7 @@ describe("ModelRegistry", () => {
anthropic: providerConfig("https://merged-proxy.example.com/v1", [{ id: "claude-custom" }]),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const anthropicModels = getModelsForProvider(registry, "anthropic");
for (const model of anthropicModels) {
@@ -269,7 +269,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
expect(compat?.supportsUsageInStreaming).toBe(false);
@@ -303,7 +303,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
expect(compat?.supportsUsageInStreaming).toBe(true);
@@ -320,7 +320,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
expect(models.length).toBeGreaterThan(0);
@@ -357,7 +357,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
expect(registry.getError()).toBeUndefined();
@@ -394,7 +394,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const m25 = registry.find("opencode-go", "minimax-m2.5");
const glm5 = registry.find("opencode-go", "glm-5");
@@ -427,7 +427,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
expect(models.some((m) => m.id === "custom/openrouter-model")).toBe(true);
@@ -440,7 +440,7 @@ describe("ModelRegistry", () => {
writeModelsJson({
anthropic: providerConfig("https://first-proxy.example.com/v1", [{ id: "claude-custom" }]),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(getModelsForProvider(registry, "anthropic").some((m) => m.id === "claude-custom")).toBe(true);
// Update and refresh
@@ -459,7 +459,7 @@ describe("ModelRegistry", () => {
writeModelsJson({
anthropic: providerConfig("https://proxy.example.com/v1", [{ id: "claude-custom" }]),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(getModelsForProvider(registry, "anthropic").some((m) => m.id === "claude-custom")).toBe(true);
// Remove custom models and refresh
@@ -484,7 +484,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -508,7 +508,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -529,7 +529,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -552,7 +552,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -576,7 +576,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -601,7 +601,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
// Should not create a new model
@@ -621,7 +621,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
@@ -642,7 +642,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
expect(sonnet).toBeDefined();
@@ -665,7 +665,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4")?.name,
).toBe("First Name");
@@ -698,7 +698,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const customName = getModelsForProvider(registry, "openrouter").find(
(m) => m.id === "anthropic/claude-sonnet-4",
)?.name;
@@ -717,7 +717,7 @@ describe("ModelRegistry", () => {
describe("dynamic provider lifecycle", () => {
test("failed registerProvider does not persist invalid streamSimple config", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(() =>
registry.registerProvider("broken-provider", {
@@ -731,7 +731,7 @@ describe("ModelRegistry", () => {
});
test("failed registerProvider does not remove existing provider models", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
registry.registerProvider("demo-provider", {
baseUrl: "https://provider.test/v1",
@@ -776,7 +776,7 @@ describe("ModelRegistry", () => {
});
test("unregisterProvider removes custom OAuth provider and restores built-in OAuth provider", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
registry.registerProvider("anthropic", {
oauth: {
@@ -799,7 +799,7 @@ describe("ModelRegistry", () => {
});
test("unregisterProvider removes custom streamSimple override and restores built-in API stream handler", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
registry.registerProvider("stream-override-provider", {
api: "openai-completions",
@@ -855,7 +855,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!echo test-api-key-from-command"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("test-api-key-from-command");
@@ -866,7 +866,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!echo ' spaced-key '"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("spaced-key");
@@ -877,7 +877,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!printf 'line1\\nline2'"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("line1\nline2");
@@ -888,7 +888,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!exit 1"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBeUndefined();
@@ -899,7 +899,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!nonexistent-command-12345"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBeUndefined();
@@ -910,7 +910,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!printf ''"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBeUndefined();
@@ -925,7 +925,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("TEST_API_KEY_12345"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("env-api-key-value");
@@ -946,7 +946,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("literal_api_key_value"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("literal_api_key_value");
@@ -957,7 +957,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey("!echo 'hello world' | tr ' ' '-'"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const apiKey = await registry.getApiKeyForProvider("custom-provider");
expect(apiKey).toBe("hello-world");
@@ -974,7 +974,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey(command),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
await registry.getApiKeyForProvider("custom-provider");
await registry.getApiKeyForProvider("custom-provider");
await registry.getApiKeyForProvider("custom-provider");
@@ -993,10 +993,10 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey(command),
});
const registry1 = new ModelRegistry(authStorage, modelsJsonPath);
const registry1 = ModelRegistry.create(authStorage, modelsJsonPath);
await registry1.getApiKeyForProvider("custom-provider");
const registry2 = new ModelRegistry(authStorage, modelsJsonPath);
const registry2 = ModelRegistry.create(authStorage, modelsJsonPath);
await registry2.getApiKeyForProvider("custom-provider");
const count = parseInt(readFileSync(counterFile, "utf-8").trim(), 10);
@@ -1009,7 +1009,7 @@ describe("ModelRegistry", () => {
"provider-b": providerWithApiKey("!echo key-b"),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const keyA = await registry.getApiKeyForProvider("provider-a");
const keyB = await registry.getApiKeyForProvider("provider-b");
@@ -1028,7 +1028,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey(command),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const key1 = await registry.getApiKeyForProvider("custom-provider");
const key2 = await registry.getApiKeyForProvider("custom-provider");
@@ -1050,7 +1050,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey(envVarName),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const key1 = await registry.getApiKeyForProvider("custom-provider");
expect(key1).toBe("first-value");
@@ -1078,7 +1078,7 @@ describe("ModelRegistry", () => {
"custom-provider": providerWithApiKey(command),
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const available = registry.getAvailable();
expect(available.some((m) => m.provider === "custom-provider")).toBe(true);
@@ -1098,7 +1098,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const model = registry.find("custom-provider", "test-model");
expect(model).toBeDefined();
@@ -1127,7 +1127,7 @@ describe("ModelRegistry", () => {
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const model = registry.find("custom-provider", "test-model");
expect(model).toBeDefined();