Skip to content

Support per-model overrides in models.json without full provider replacement #1062

@charles-cooper

Description

@charles-cooper

Problem

Currently, models.json has two modes for providers:

  1. Override-only: Set baseUrl/apiKey/headers without models array → keeps all built-in models
  2. Full replacement: Define models array → replaces ALL models for that provider

This makes it impossible to customize a single model (e.g., set OpenRouter routing for one model) without replacing the entire provider's model list.

Use case: I want to route anthropic/claude-sonnet-4 through Bedrock on OpenRouter while keeping all other OpenRouter models unchanged:

{
  "providers": {
    "openrouter": {
      "models": [...]  // Would need to copy ALL openrouter models just to change one
    }
  }
}

Proposal

Add modelOverrides field that merges with built-in models:

{
  "providers": {
    "openrouter": {
      "modelOverrides": {
        "anthropic/claude-sonnet-4": {
          "compat": { "openRouterRouting": { "only": ["amazon-bedrock"] } }
        },
        "anthropic/claude-opus-4": {
          "compat": { "openRouterRouting": { "order": ["anthropic", "amazon-bedrock"] } }
        }
      }
    }
  }
}

Override fields would be deep-merged with the built-in model definition.

Implementation Sketch

1. Schema changes (model-registry.ts)

// Add override schema (subset of ModelDefinitionSchema, all optional)
const ModelOverrideSchema = Type.Object({
  name: Type.Optional(Type.String()),
  reasoning: Type.Optional(Type.Boolean()),
  cost: Type.Optional(Type.Partial(/* cost fields */)),
  contextWindow: Type.Optional(Type.Number()),
  maxTokens: Type.Optional(Type.Number()),
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
  compat: Type.Optional(OpenAICompatSchema),
});

const ProviderConfigSchema = Type.Object({
  baseUrl: Type.Optional(Type.String()),
  apiKey: Type.Optional(Type.String()),
  api: Type.Optional(Type.String()),
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
  authHeader: Type.Optional(Type.Boolean()),
  models: Type.Optional(Type.Array(ModelDefinitionSchema)),
  modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),  // NEW
});

2. Loading logic (loadCustomModels)

// In CustomModelsResult, add:
modelOverrides: Map<string, Map<string, ModelOverride>>;  // provider -> modelId -> override

// When parsing config:
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
  if (providerConfig.models?.length) {
    replacedProviders.add(providerName);
  } else {
    overrides.set(providerName, { baseUrl, headers, apiKey });
    if (providerConfig.modelOverrides) {
      modelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));
    }
  }
}

3. Apply overrides (loadBuiltInModels)

private loadBuiltInModels(
  replacedProviders: Set<string>,
  overrides: Map<string, ProviderOverride>,
  modelOverrides: Map<string, Map<string, ModelOverride>>  // NEW
): Model<Api>[] {
  return getProviders()
    .filter((p) => !replacedProviders.has(p))
    .flatMap((provider) => {
      const models = getModels(provider as KnownProvider);
      const override = overrides.get(provider);
      const perModelOverrides = modelOverrides.get(provider);
      
      return models.map((m) => {
        let model = override ? applyProviderOverride(m, override) : m;
        const modelOverride = perModelOverrides?.get(m.id);
        if (modelOverride) {
          model = deepMerge(model, modelOverride);  // Deep merge compat, headers, etc.
        }
        return model;
      });
    });
}

I'm willing to implement this if the proposal is accepted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions