From 5a58f5cee842a89dafd7c8d4798fdd0c06d4e65e Mon Sep 17 00:00:00 2001 From: nubscarson Date: Wed, 20 May 2026 01:13:56 +0000 Subject: [PATCH] fix(provider): detect configured Cerebras endpoints --- packages/opencode/src/provider/provider.ts | 27 ++++++---- packages/opencode/src/provider/transform.ts | 9 +++- .../opencode/test/provider/provider.test.ts | 39 +++++++++++++- .../opencode/test/provider/transform.test.ts | 54 +++++++++++++++++++ 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9e0149a123c3..d58908264281 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -52,8 +52,7 @@ function privateIPv6(hostname: string) { const host = hostname.toLowerCase() if (host === "::1") return true if (host.startsWith("fc") || host.startsWith("fd")) return true - if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb")) - return true + if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb")) return true return false } @@ -107,6 +106,14 @@ function shouldUseCopilotResponsesApi(modelID: string): boolean { return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") } +function configuredProviderEndpoint(provider: { options?: Record } | undefined): string | undefined { + const endpoint = provider?.options?.endpoint + if (typeof endpoint === "string" && endpoint) return endpoint + const baseURL = provider?.options?.baseURL + if (typeof baseURL === "string" && baseURL) return baseURL + return undefined +} + function defaultOpenAICompatibleInterleaved( apiNpm: string, apiID: string, @@ -116,9 +123,7 @@ function defaultOpenAICompatibleInterleaved( const id = apiID.toLowerCase() const usesReasoningContent = - id.includes("deepseek") || - id.includes("kimi") || - /(^|[/:])glm-(4\.7|5(?:\.1)?|5v)(?:[^a-z0-9]|$)/.test(id) + id.includes("deepseek") || id.includes("kimi") || /(^|[/:])glm-(4\.7|5(?:\.1)?|5v)(?:[^a-z0-9]|$)/.test(id) return usesReasoningContent ? { field: "reasoning_content" } : false } @@ -1388,15 +1393,19 @@ export const layer = Layer.effect( const reasoning = model.reasoning ?? existingModel?.capabilities.reasoning ?? false const defaultInterleaved = defaultOpenAICompatibleInterleaved(apiNpm, apiID, reasoning) const existingInterleaved = existingModel?.capabilities.interleaved - const interleaved = - model.interleaved ?? - (existingInterleaved ? existingInterleaved : defaultInterleaved) + const interleaved = model.interleaved ?? (existingInterleaved ? existingInterleaved : defaultInterleaved) const parsedModel: Model = { id: ModelID.make(modelID), api: { id: apiID, npm: apiNpm, - url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "", + url: + model.provider?.api ?? + configuredProviderEndpoint(provider) ?? + provider?.api ?? + existingModel?.api.url ?? + modelsDev[providerID]?.api ?? + "", }, status: model.status ?? existingModel?.status ?? "active", name, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index ec3f6bdd135e..23bb54c40fd1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -67,8 +67,13 @@ const REASONING_REPLAY_FIELDS = ["reasoning_content", "reasoning_details"] as co function isCerebrasCompatibleEndpoint(model: Provider.Model) { if (model.api.npm === "@ai-sdk/cerebras") return true const providerID = model.providerID.toLowerCase() - const apiURL = model.api.url.toLowerCase() - return providerID.includes("cerebras") || apiURL.includes("cerebras") + if (providerID.includes("cerebras")) return true + try { + const hostname = new URL(model.api.url).hostname.toLowerCase() + return hostname === "cerebras.ai" || hostname.endsWith(".cerebras.ai") + } catch { + return false + } } function cerebrasReasoningText(text: string, model: Provider.Model) { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index d88c6ffa7d6d..0ad2e72c148d 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -386,7 +386,6 @@ experimentalModels.instance( { config: alphaProviderConfig }, ) - test("custom OpenAI-compatible reasoning_content models default interleaved reasoning field", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -866,6 +865,44 @@ test("provider api field sets model api.url", async () => { }) }) +test("provider baseURL sets model api.url when provider api is not declared", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-endpoint": { + name: "Custom Endpoint", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "model-1": { + name: "Model 1", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { + baseURL: "https://api.example.com/v1", + apiKey: "test-key", + }, + }, + }, + }), + ) + }, + }) + await withTestInstance({ + directory: tmp.path, + fn: async (ctx) => { + const providers = await list(ctx) + expect(providers[ProviderID.make("custom-endpoint")].models["model-1"].api.url).toBe("https://api.example.com/v1") + }, + }) +}) + test("explicit baseURL overrides api field", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 5c30a9609b97..d4db6cb6e3b7 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1884,6 +1884,60 @@ describe("ProviderTransform.message - Cerebras reasoning replay", () => { expect(result[0].content).toBe("Create the remaining files before verifying.") expect((result[0] as any).reasoning_content).toBeUndefined() }) + + test("does not treat Cerebras text in a non-Cerebras URL path as a Cerebras endpoint", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Keep this in providerOptions." }, + { type: "text", text: "Answer" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message( + msgs, + { + id: ModelID.make("generic/gpt-oss-120b"), + providerID: ProviderID.make("generic"), + api: { + id: "gpt-oss-120b", + url: "https://proxy.example/v1/cerebras.ai", + npm: "@ai-sdk/openai-compatible", + }, + name: "GPT OSS 120B", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 4096, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2025-08-05", + }, + {}, + ) + + expect(result[0].content).toEqual([{ type: "text", text: "Answer" }]) + expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Keep this in providerOptions.") + }) }) describe("ProviderTransform.message - OpenAI-compatible reasoning replay", () => {