mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 06:04:51 +00:00
feat(coding-agent): support per-model overrides in models.json
Add modelOverrides field to provider config that allows customizing
individual built-in models without replacing the entire provider.
Example:
{
"providers": {
"openrouter": {
"modelOverrides": {
"anthropic/claude-sonnet-4": {
"compat": { "openRouterRouting": { "only": ["amazon-bedrock"] } }
}
}
}
}
}
Overrides are deep-merged with built-in model definitions. Supports:
- name, reasoning, input, contextWindow, maxTokens
- Partial cost overrides (e.g. only change input cost)
- headers (merged with existing)
- compat settings (merged with existing)
Works alongside baseUrl overrides on the same provider.
closes #1062
This commit is contained in:
parent
f5b9eeb514
commit
2cd55b2d35
2 changed files with 339 additions and 18 deletions
|
|
@ -81,6 +81,27 @@ const ModelDefinitionSchema = Type.Object({
|
||||||
compat: Type.Optional(OpenAICompatSchema),
|
compat: Type.Optional(OpenAICompatSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Schema for per-model overrides (all fields optional, merged with built-in model)
|
||||||
|
const ModelOverrideSchema = Type.Object({
|
||||||
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
|
reasoning: Type.Optional(Type.Boolean()),
|
||||||
|
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||||
|
cost: Type.Optional(
|
||||||
|
Type.Object({
|
||||||
|
input: Type.Optional(Type.Number()),
|
||||||
|
output: Type.Optional(Type.Number()),
|
||||||
|
cacheRead: Type.Optional(Type.Number()),
|
||||||
|
cacheWrite: Type.Optional(Type.Number()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
contextWindow: Type.Optional(Type.Number()),
|
||||||
|
maxTokens: Type.Optional(Type.Number()),
|
||||||
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
|
compat: Type.Optional(OpenAICompatSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
||||||
|
|
||||||
const ProviderConfigSchema = Type.Object({
|
const ProviderConfigSchema = Type.Object({
|
||||||
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
|
|
@ -88,6 +109,7 @@ const ProviderConfigSchema = Type.Object({
|
||||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
authHeader: Type.Optional(Type.Boolean()),
|
authHeader: Type.Optional(Type.Boolean()),
|
||||||
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
||||||
|
modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ModelsConfigSchema = Type.Object({
|
const ModelsConfigSchema = Type.Object({
|
||||||
|
|
@ -110,11 +132,51 @@ interface CustomModelsResult {
|
||||||
replacedProviders: Set<string>;
|
replacedProviders: Set<string>;
|
||||||
/** Providers with only baseUrl/headers override (no custom models) */
|
/** Providers with only baseUrl/headers override (no custom models) */
|
||||||
overrides: Map<string, ProviderOverride>;
|
overrides: Map<string, ProviderOverride>;
|
||||||
|
/** Per-model overrides: provider -> modelId -> override */
|
||||||
|
modelOverrides: Map<string, Map<string, ModelOverride>>;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyCustomModelsResult(error?: string): CustomModelsResult {
|
function emptyCustomModelsResult(error?: string): CustomModelsResult {
|
||||||
return { models: [], replacedProviders: new Set(), overrides: new Map(), error };
|
return { models: [], replacedProviders: new Set(), overrides: new Map(), modelOverrides: new Map(), error };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep merge a model override into a model.
|
||||||
|
* Handles nested objects (cost, compat) by merging rather than replacing.
|
||||||
|
*/
|
||||||
|
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
||||||
|
const result = { ...model };
|
||||||
|
|
||||||
|
// Simple field overrides
|
||||||
|
if (override.name !== undefined) result.name = override.name;
|
||||||
|
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
||||||
|
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
||||||
|
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
||||||
|
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
||||||
|
|
||||||
|
// Merge cost (partial override)
|
||||||
|
if (override.cost) {
|
||||||
|
result.cost = {
|
||||||
|
input: override.cost.input ?? model.cost.input,
|
||||||
|
output: override.cost.output ?? model.cost.output,
|
||||||
|
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
||||||
|
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge headers
|
||||||
|
if (override.headers) {
|
||||||
|
const resolvedHeaders = resolveHeaders(override.headers);
|
||||||
|
result.headers = resolvedHeaders ? { ...model.headers, ...resolvedHeaders } : model.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep merge compat
|
||||||
|
if (override.compat) {
|
||||||
|
result.compat = model.compat ? { ...model.compat, ...override.compat } : override.compat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the config value command cache. Exported for testing. */
|
/** Clear the config value command cache. Exported for testing. */
|
||||||
|
|
@ -172,6 +234,7 @@ export class ModelRegistry {
|
||||||
models: customModels,
|
models: customModels,
|
||||||
replacedProviders,
|
replacedProviders,
|
||||||
overrides,
|
overrides,
|
||||||
|
modelOverrides,
|
||||||
error,
|
error,
|
||||||
} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();
|
} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();
|
||||||
|
|
||||||
|
|
@ -180,7 +243,7 @@ export class ModelRegistry {
|
||||||
// Keep built-in models even if custom models failed to load
|
// Keep built-in models even if custom models failed to load
|
||||||
}
|
}
|
||||||
|
|
||||||
const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
|
const builtInModels = this.loadBuiltInModels(replacedProviders, overrides, modelOverrides);
|
||||||
let combined = [...builtInModels, ...customModels];
|
let combined = [...builtInModels, ...customModels];
|
||||||
|
|
||||||
// Let OAuth providers modify their models (e.g., update baseUrl)
|
// Let OAuth providers modify their models (e.g., update baseUrl)
|
||||||
|
|
@ -195,21 +258,39 @@ export class ModelRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load built-in models, skipping replaced providers and applying overrides */
|
/** Load built-in models, skipping replaced providers and applying overrides */
|
||||||
private loadBuiltInModels(replacedProviders: Set<string>, overrides: Map<string, ProviderOverride>): Model<Api>[] {
|
private loadBuiltInModels(
|
||||||
|
replacedProviders: Set<string>,
|
||||||
|
overrides: Map<string, ProviderOverride>,
|
||||||
|
modelOverrides: Map<string, Map<string, ModelOverride>>,
|
||||||
|
): Model<Api>[] {
|
||||||
return getProviders()
|
return getProviders()
|
||||||
.filter((provider) => !replacedProviders.has(provider))
|
.filter((provider) => !replacedProviders.has(provider))
|
||||||
.flatMap((provider) => {
|
.flatMap((provider) => {
|
||||||
const models = getModels(provider as KnownProvider) as Model<Api>[];
|
const models = getModels(provider as KnownProvider) as Model<Api>[];
|
||||||
const override = overrides.get(provider);
|
const providerOverride = overrides.get(provider);
|
||||||
if (!override) return models;
|
const perModelOverrides = modelOverrides.get(provider);
|
||||||
|
|
||||||
// Apply baseUrl/headers override to all models of this provider
|
return models.map((m) => {
|
||||||
const resolvedHeaders = resolveHeaders(override.headers);
|
let model = m;
|
||||||
return models.map((m) => ({
|
|
||||||
...m,
|
// Apply provider-level baseUrl/headers override
|
||||||
baseUrl: override.baseUrl ?? m.baseUrl,
|
if (providerOverride) {
|
||||||
headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
|
const resolvedHeaders = resolveHeaders(providerOverride.headers);
|
||||||
}));
|
model = {
|
||||||
|
...model,
|
||||||
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
||||||
|
headers: resolvedHeaders ? { ...model.headers, ...resolvedHeaders } : model.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply per-model override
|
||||||
|
const modelOverride = perModelOverrides?.get(m.id);
|
||||||
|
if (modelOverride) {
|
||||||
|
model = applyModelOverride(model, modelOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,6 +319,7 @@ export class ModelRegistry {
|
||||||
// Separate providers into "full replacement" (has models) vs "override-only" (no models)
|
// Separate providers into "full replacement" (has models) vs "override-only" (no models)
|
||||||
const replacedProviders = new Set<string>();
|
const replacedProviders = new Set<string>();
|
||||||
const overrides = new Map<string, ProviderOverride>();
|
const overrides = new Map<string, ProviderOverride>();
|
||||||
|
const modelOverrides = new Map<string, Map<string, ModelOverride>>();
|
||||||
|
|
||||||
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
||||||
if (providerConfig.models && providerConfig.models.length > 0) {
|
if (providerConfig.models && providerConfig.models.length > 0) {
|
||||||
|
|
@ -255,9 +337,14 @@ export class ModelRegistry {
|
||||||
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect per-model overrides (works with both full replacement and override-only)
|
||||||
|
if (providerConfig.modelOverrides) {
|
||||||
|
modelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { models: this.parseModels(config), replacedProviders, overrides, error: undefined };
|
return { models: this.parseModels(config), replacedProviders, overrides, modelOverrides, error: undefined };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SyntaxError) {
|
if (error instanceof SyntaxError) {
|
||||||
return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
|
return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
|
||||||
|
|
@ -272,13 +359,13 @@ export class ModelRegistry {
|
||||||
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
||||||
const hasProviderApi = !!providerConfig.api;
|
const hasProviderApi = !!providerConfig.api;
|
||||||
const models = providerConfig.models ?? [];
|
const models = providerConfig.models ?? [];
|
||||||
|
const hasModelOverrides =
|
||||||
|
providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
|
||||||
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
// Override-only config: just needs baseUrl (to override built-in)
|
// Override-only config: needs baseUrl OR modelOverrides (or both)
|
||||||
if (!providerConfig.baseUrl) {
|
if (!providerConfig.baseUrl && !hasModelOverrides) {
|
||||||
throw new Error(
|
throw new Error(`Provider ${providerName}: must specify "baseUrl", "modelOverrides", or "models".`);
|
||||||
`Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Full replacement: needs baseUrl and apiKey
|
// Full replacement: needs baseUrl and apiKey
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,240 @@ describe("ModelRegistry", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("modelOverrides (per-model customization)", () => {
|
||||||
|
test("model override applies to a single built-in model", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
name: "Custom Sonnet Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
expect(sonnet?.name).toBe("Custom Sonnet Name");
|
||||||
|
|
||||||
|
// Other models should be unchanged
|
||||||
|
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
|
||||||
|
expect(opus?.name).not.toBe("Custom Sonnet Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override with compat.openRouterRouting", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
compat: {
|
||||||
|
openRouterRouting: { only: ["amazon-bedrock"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
expect((sonnet?.compat as any)?.openRouterRouting).toEqual({ only: ["amazon-bedrock"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override deep merges compat settings", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
compat: {
|
||||||
|
openRouterRouting: { order: ["anthropic", "together"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
|
||||||
|
// Should have both the new routing AND preserve other compat settings
|
||||||
|
expect((sonnet?.compat as any)?.openRouterRouting).toEqual({ order: ["anthropic", "together"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multiple model overrides on same provider", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
compat: { openRouterRouting: { only: ["amazon-bedrock"] } },
|
||||||
|
},
|
||||||
|
"anthropic/claude-opus-4": {
|
||||||
|
compat: { openRouterRouting: { only: ["anthropic"] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
|
||||||
|
|
||||||
|
expect((sonnet?.compat as any)?.openRouterRouting).toEqual({ only: ["amazon-bedrock"] });
|
||||||
|
expect((opus?.compat as any)?.openRouterRouting).toEqual({ only: ["anthropic"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override combined with baseUrl override", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
baseUrl: "https://my-proxy.example.com/v1",
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
name: "Proxied Sonnet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
|
||||||
|
// Both overrides should apply
|
||||||
|
expect(sonnet?.baseUrl).toBe("https://my-proxy.example.com/v1");
|
||||||
|
expect(sonnet?.name).toBe("Proxied Sonnet");
|
||||||
|
|
||||||
|
// Other models should have the baseUrl but not the name override
|
||||||
|
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
|
||||||
|
expect(opus?.baseUrl).toBe("https://my-proxy.example.com/v1");
|
||||||
|
expect(opus?.name).not.toBe("Proxied Sonnet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override for non-existent model ID is ignored", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"nonexistent/model-id": {
|
||||||
|
name: "This should not appear",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
|
||||||
|
// Should not create a new model
|
||||||
|
expect(models.find((m) => m.id === "nonexistent/model-id")).toBeUndefined();
|
||||||
|
// Should not crash or show error
|
||||||
|
expect(registry.getError()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override can change cost fields partially", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
cost: { input: 99 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
|
||||||
|
// Input cost should be overridden
|
||||||
|
expect(sonnet?.cost.input).toBe(99);
|
||||||
|
// Other cost fields should be preserved from built-in
|
||||||
|
expect(sonnet?.cost.output).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("model override can add headers", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
headers: { "X-Custom-Model-Header": "value" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const models = getModelsForProvider(registry, "openrouter");
|
||||||
|
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||||
|
|
||||||
|
expect(sonnet?.headers?.["X-Custom-Model-Header"]).toBe("value");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("refresh() picks up model override changes", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
name: "First Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
expect(
|
||||||
|
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4")?.name,
|
||||||
|
).toBe("First Name");
|
||||||
|
|
||||||
|
// Update and refresh
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
name: "Second Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
registry.refresh();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4")?.name,
|
||||||
|
).toBe("Second Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removing model override restores built-in values", () => {
|
||||||
|
writeRawModelsJson({
|
||||||
|
openrouter: {
|
||||||
|
modelOverrides: {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
name: "Custom Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||||
|
const customName = getModelsForProvider(registry, "openrouter").find(
|
||||||
|
(m) => m.id === "anthropic/claude-sonnet-4",
|
||||||
|
)?.name;
|
||||||
|
expect(customName).toBe("Custom Name");
|
||||||
|
|
||||||
|
// Remove override and refresh
|
||||||
|
writeRawModelsJson({});
|
||||||
|
registry.refresh();
|
||||||
|
|
||||||
|
const restoredName = getModelsForProvider(registry, "openrouter").find(
|
||||||
|
(m) => m.id === "anthropic/claude-sonnet-4",
|
||||||
|
)?.name;
|
||||||
|
expect(restoredName).not.toBe("Custom Name");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("API key resolution", () => {
|
describe("API key resolution", () => {
|
||||||
/** Create provider config with custom apiKey */
|
/** Create provider config with custom apiKey */
|
||||||
function providerWithApiKey(apiKey: string) {
|
function providerWithApiKey(apiKey: string) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue