Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -107,6 +106,14 @@ function shouldUseCopilotResponsesApi(modelID: string): boolean {
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
}

function configuredProviderEndpoint(provider: { options?: Record<string, unknown> } | 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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
39 changes: 38 additions & 1 deletion packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
54 changes: 54 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading