diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 9b7d40de..fe9d764b 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -70,6 +70,7 @@ const ModelDefinitionSchema = Type.Object({ id: Type.String({ minLength: 1 }), name: Type.Optional(Type.String({ minLength: 1 })), api: Type.Optional(Type.String({ minLength: 1 })), + baseUrl: 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( @@ -469,15 +470,15 @@ export class ModelRegistry { } } - // baseUrl is validated to exist for providers with models - // Apply defaults for optional fields + // Provider baseUrl is required when custom models are defined. + // Individual models can override it with modelDef.baseUrl. const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; models.push({ id: modelDef.id, name: modelDef.name ?? modelDef.id, api: api as Api, provider: providerName, - baseUrl: providerConfig.baseUrl!, + baseUrl: modelDef.baseUrl ?? providerConfig.baseUrl!, reasoning: modelDef.reasoning ?? false, input: (modelDef.input ?? ["text"]) as ("text" | "image")[], cost: modelDef.cost ?? defaultCost, @@ -682,6 +683,7 @@ export interface ProviderConfigInput { id: string; name: string; api?: Api; + baseUrl?: string; reasoning: boolean; input: ("text" | "image")[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index f66b718f..f1fd7bbd 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -237,6 +237,43 @@ describe("ModelRegistry", () => { } }); + test("model-level baseUrl overrides provider-level baseUrl for custom models", () => { + writeRawModelsJson({ + "opencode-go": { + baseUrl: "https://opencode.ai/zen/go/v1", + apiKey: "TEST_KEY", + models: [ + { + id: "minimax-m2.5", + api: "anthropic-messages", + baseUrl: "https://opencode.ai/zen/go", + reasoning: true, + input: ["text"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 }, + contextWindow: 204800, + maxTokens: 131072, + }, + { + id: "glm-5", + api: "openai-completions", + reasoning: true, + input: ["text"], + cost: { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 }, + contextWindow: 204800, + maxTokens: 131072, + }, + ], + }, + }); + + const registry = new ModelRegistry(authStorage, modelsJsonPath); + const m25 = registry.find("opencode-go", "minimax-m2.5"); + const glm5 = registry.find("opencode-go", "glm-5"); + + expect(m25?.baseUrl).toBe("https://opencode.ai/zen/go"); + expect(glm5?.baseUrl).toBe("https://opencode.ai/zen/go/v1"); + }); + test("modelOverrides still apply when provider also defines models", () => { writeRawModelsJson({ openrouter: {