From 0af0e69a3b97d92de375831d05c53860503cb354 Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:18:44 +0300 Subject: [PATCH 1/2] fix(core): send catalog models to a configured provider baseURL The models catalog now gives every opencode and opencode-go model its own endpoint, and a model's settings win over its provider's when a model is resolved. A provider-level settings.baseURL from config was only stripped from models listed under that provider's models, so for everything else the catalog endpoint won and the configured gateway never saw a request. Drop the catalog endpoint from every model of a provider whose config sets baseURL. A baseURL configured on a model still wins. --- packages/core/src/config/plugin/provider.ts | 7 ++++ packages/core/test/config/provider.test.ts | 40 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 0c485c42280e..ba84a3ffa500 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -74,6 +74,13 @@ export const Plugin = define({ if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers) if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body) }) + // Catalog models can carry their own endpoint, which would otherwise win over the configured one. + if (item.settings?.baseURL !== undefined) + for (const [id, model] of providers.get(providerID)?.models ?? []) + if (model.settings?.baseURL !== undefined) + providers.models.update(providerID, id, (model) => { + if (model.settings) delete model.settings.baseURL + }) const definitions = new Map() for (const [id, config] of Object.entries(item.models ?? {})) { const base = source?.models.get(config.modelID ?? id) ?? source?.models.get(id) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 763ebd2ff69c..174465a2fdde 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -665,6 +665,46 @@ describe("ConfigProviderPlugin.Plugin", () => { }), ) + it.effect("sends catalog models with their own endpoint to the configured provider baseURL", () => + Effect.gen(function* () { + const providers = yield* Provider.Service + const models = yield* Model.Service + const providerID = Provider.ID.make("custom") + yield* providers.transform((editor) => { + editor.update(providerID, (provider) => { + provider.package = "@opencode/ai/providers/openai/chat" + provider.settings = { baseURL: "https://catalog.example/v1" } + }) + for (const id of ["chat", "listed"]) + editor.models.update(providerID, Model.ID.make(id), (model) => { + model.settings = { baseURL: "https://catalog.example/v1" } + }) + }) + yield* addPlugin([ + new Document({ + type: "document", + info: decode({ + providers: { + custom: { + settings: { baseURL: "https://gateway.example/v1" }, + models: { listed: { settings: { baseURL: "https://listed.example/v1" } } }, + }, + }, + }), + }), + ]) + + const resolver = yield* ModelResolver.Service + const endpoint = (id: string) => + models.get(providerID, Model.ID.make(id)).pipe( + Effect.flatMap((model) => resolver.resolveModel(required(model))), + Effect.map((resolved) => resolved.model.route.endpoint.baseURL), + ) + expect(yield* endpoint("chat")).toBe("https://gateway.example/v1") + expect(yield* endpoint("listed")).toBe("https://listed.example/v1") + }).pipe(Effect.provide(ModelResolver.layer)), + ) + it.effect("generates variants after rewriting a configured model package", () => Effect.gen(function* () { const models = yield* Model.Service From def3b5e891367628f2a66d5938630e320295c445 Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:06:28 +0300 Subject: [PATCH 2/2] fix(core): only drop a catalog model endpoint that shares the provider's origin Models on another origin, or under a provider with no catalog baseURL (Azure, Bedrock, Vertex partner models), keep their own endpoint when the config sets a provider baseURL. --- packages/core/src/config/plugin/provider.ts | 17 ++++++-- packages/core/test/config/provider.test.ts | 44 +++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 5f07c7a38bf2..998a96c9f252 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -58,12 +58,14 @@ export const Plugin = define({ const current = providers.get(providerID) const source = providers.get(item.canonical ?? current?.provider.canonical ?? providerID) const changed = item.canonical !== undefined && item.canonical !== current?.provider.canonical + let catalogBaseURL: string | undefined providers.update(providerID, (provider) => { if (changed && source && source.provider !== provider) Object.assign(provider, structuredClone(source.provider), { id: provider.id, integrationID: provider.integrationID, }) + catalogBaseURL = provider.settings?.baseURL provider.activation = "enabled" if (item.canonical !== undefined) provider.canonical = item.canonical if (item.name) provider.name = item.name @@ -74,10 +76,11 @@ export const Plugin = define({ if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers) if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body) }) - // Catalog models can carry their own endpoint, which would otherwise win over the configured one. - if (item.settings?.baseURL !== undefined) + // A catalog model can carry a variant of the provider endpoint, which would otherwise win over the + // configured one. A model on another origin (Azure, Bedrock, Vertex partner models) keeps its own. + if (item.settings?.baseURL !== undefined && catalogBaseURL !== undefined) for (const [id, model] of providers.get(providerID)?.models ?? []) - if (model.settings?.baseURL !== undefined) + if (model.settings?.baseURL !== undefined && sameOrigin(model.settings.baseURL, catalogBaseURL)) providers.models.update(providerID, id, (model) => { if (model.settings) delete model.settings.baseURL }) @@ -203,3 +206,11 @@ function configuredProviders(entries: readonly Entry[]) { .filter((entry): entry is Document => entry.type === "document") .flatMap((file) => Object.entries(file.info.providers ?? {})) } + +const sameOrigin = (a: string, b: string) => { + try { + return new URL(a).origin === new URL(b).origin + } catch { + return false + } +} diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 177e07bb83ce..5efc67d1abdd 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -730,6 +730,50 @@ describe("ConfigProviderPlugin.Plugin", () => { }).pipe(Effect.provide(ModelResolver.layer)), ) + it.effect("keeps a catalog model endpoint on another origin, or under a provider with no catalog baseURL", () => + Effect.gen(function* () { + const providers = yield* Provider.Service + const models = yield* Model.Service + const withBase = Provider.ID.make("custom") + const withoutBase = Provider.ID.make("partner") + yield* providers.transform((editor) => { + editor.update(withBase, (provider) => { + provider.package = "@opencode/ai/providers/openai/chat" + provider.settings = { baseURL: "https://catalog.example/v1" } + }) + editor.models.update(withBase, Model.ID.make("partner-model"), (model) => { + model.settings = { baseURL: "https://partner.example/models" } + }) + editor.update(withoutBase, (provider) => { + provider.package = "@opencode/ai/providers/openai/chat" + }) + editor.models.update(withoutBase, Model.ID.make("claude"), (model) => { + model.settings = { baseURL: "https://resource.example/anthropic/v1" } + }) + }) + yield* addPlugin([ + new Document({ + type: "document", + info: decode({ + providers: { + custom: { settings: { baseURL: "https://gateway.example/v1" } }, + partner: { settings: { baseURL: "https://vpc.example/v1" } }, + }, + }), + }), + ]) + + const resolver = yield* ModelResolver.Service + const endpoint = (providerID: Provider.ID, id: string) => + models.get(providerID, Model.ID.make(id)).pipe( + Effect.flatMap((model) => resolver.resolveModel(required(model))), + Effect.map((resolved) => resolved.model.route.endpoint.baseURL), + ) + expect(yield* endpoint(withBase, "partner-model")).toBe("https://partner.example/models") + expect(yield* endpoint(withoutBase, "claude")).toBe("https://resource.example/anthropic/v1") + }).pipe(Effect.provide(ModelResolver.layer)), + ) + it.effect("generates variants after rewriting a configured model package", () => Effect.gen(function* () { const models = yield* Model.Service