Skip to content
Merged
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ const layer: Layer.Layer<
model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved: model.interleaved ?? false,
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? false,
},
cost: {
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
Expand Down
71 changes: 67 additions & 4 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ function sdkKey(npm: string): string | undefined {
return undefined
}

function isDeepSeekModelID(id: string) {
return /(^|[/:])deepseek(?:[-/]|$)/.test(id.toLowerCase())
}

function isLegacyDeepSeekVariantID(id: string) {
return /(^|[/:])deepseek-(?:chat|reasoner|r1|v3)(?:[-/]|$)/.test(id.toLowerCase())
}

function isDeepSeekV4ID(id: string) {
return /(^|[/:])deepseek-v4(?:[-/]|$)/.test(id.toLowerCase())
}

function normalizeMessages(
msgs: ModelMessage[],
model: Provider.Model,
Expand Down Expand Up @@ -175,7 +187,28 @@ function normalizeMessages(
return result
}

if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
if (isDeepSeekModelID(model.api.id)) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
if (msg.content.some((part) => part.type === "reasoning")) return msg
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
}
return {
...msg,
content: [
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
{ type: "reasoning" as const, text: "" },
],
}
})
}

if (
typeof model.capabilities.interleaved === "object" &&
model.capabilities.interleaved.field &&
model.api.npm !== "@openrouter/ai-sdk-provider"
) {
const field = model.capabilities.interleaved.field
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
Expand Down Expand Up @@ -400,7 +433,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
const id = model.id.toLowerCase()
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
if (
id.includes("deepseek") ||
isLegacyDeepSeekVariantID(id) ||
id.includes("minimax") ||
id.includes("glm") ||
id.includes("mistral") ||
Expand Down Expand Up @@ -526,8 +559,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible":
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
case "@ai-sdk/openai-compatible": {
const openaiCompatibleEfforts = [...WIDELY_SUPPORTED_EFFORTS]
if (isDeepSeekV4ID(model.api.id)) openaiCompatibleEfforts.push("max")
return Object.fromEntries(openaiCompatibleEfforts.map((effort) => [effort, { reasoningEffort: effort }]))
}

case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
Expand Down Expand Up @@ -788,6 +824,13 @@ export function options(input: {
}): Record<string, any> {
const result: Record<string, any> = {}

if (
input.model.api.npm === "@ai-sdk/google-vertex/anthropic" ||
(!input.model.api.id.includes("claude") && input.model.api.npm === "@ai-sdk/anthropic")
) {
result["toolStreaming"] = false
}

// openai and providers using openai package should set store to false by default.
if (
input.model.providerID === "openai" ||
Expand Down Expand Up @@ -1023,6 +1066,26 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
}
*/

if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
const sanitizeMoonshot = (obj: unknown): unknown => {
if (obj === null || typeof obj !== "object") return obj
if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
const result = Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]),
) as Record<string, unknown>
if (typeof result.$ref === "string") {
const refOnly: Record<string, unknown> = { $ref: result.$ref }
if (result.$defs && typeof result.$defs === "object") refOnly.$defs = result.$defs
if (result.definitions && typeof result.definitions === "object") refOnly.definitions = result.definitions
return refOnly
}
if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
return result
}

schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema | JSONSchema7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Convert integer enums to string enums for Google/Gemini
if (model.providerID === "google" || model.api.id.includes("gemini")) {
const isPlainObject = (node: unknown): node is Record<string, any> =>
Expand Down
36 changes: 36 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,42 @@ test("uses doubao-seed-2.0-code as the Volcano Coding Plan default model", async
expect(Provider.defaultModelID(Provider.fromModelsDevProvider(provider))).toBe(VOLCENGINE_PLAN_DEFAULT_MODEL_ID)
})

test("config model overrides preserve existing interleaved metadata", 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: {
zhipuai: {
models: {
"glm-5": {
name: "GLM 5 Override",
},
},
},
},
}),
)
},
})

await Instance.provide({
directory: tmp.path,
init: async () => {
set("ZHIPU_API_KEY", "test-zhipu-key")
},
fn: async () => {
const providers = await list()
const model = providers[ProviderID.make("zhipuai")].models[ModelID.make("glm-5")]

expect(model.name).toBe("GLM 5 Override")
expect(model.capabilities.interleaved).toEqual({ field: "reasoning_content" })
},
})
})

test("does not change defaults for non-Volcano providers with the same Doubao model id", () => {
const providers = {
"qiniu-ai": {
Expand Down
Loading
Loading