Skip to content
Open
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
64 changes: 48 additions & 16 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ export namespace ProviderTransform {
return result
}

// altimate_change start — upstream_fix: ensure deepseek assistant messages always have a reasoning part
// Ported from upstream-opencode commit 86715fecc4 (#24180). DeepSeek's OpenAI-compatible
// protocol requires every assistant message to carry a `reasoning` part — without it the API
// returns 400 on multi-turn sessions. Drop this marker if upstream merges the same shape.
if (model.api.id.includes("deepseek")) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
if (msg.content.some((part: any) => part.type === "reasoning")) return msg
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" } as any] }
}
return {
...msg,
content: [
...(msg.content ? [{ type: "text" as const, text: msg.content as unknown as string }] : []),
{ type: "reasoning" as const, text: "" } as any,
] as any,
}
})
}
// altimate_change end

if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
const field = model.capabilities.interleaved.field
return msgs.map((msg) => {
Expand All @@ -161,25 +183,23 @@ export namespace ProviderTransform {
// Filter out reasoning parts from content
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")

// Include reasoning_content | reasoning_details directly on the message for all assistant messages
if (reasoningText) {
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...(msg.providerOptions as any)?.openaiCompatible,
[field]: reasoningText,
},
},
}
}

// altimate_change start — upstream_fix: always set the interleaved reasoning field
// even when empty. Ported from upstream-opencode commit 923af96d26 (#24146). DeepSeek
// V4 thinking mode may emit empty reasoning_content; the field must still be present
// on subsequent requests or the API rejects the message. Drop this marker if upstream
// ships the same shape.
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...(msg.providerOptions as any)?.openaiCompatible,
[field]: reasoningText,
},
},
}
// altimate_change end
}

return msg
Expand Down Expand Up @@ -283,8 +303,10 @@ export namespace ProviderTransform {
msgs = normalizeMessages(msgs, model, options)
if (
(model.providerID === "anthropic" ||
// altimate_change start — altimate-specific Anthropic provider IDs
model.providerID === "google-vertex-anthropic" ||
model.providerID === "altimate-backend" ||
// altimate_change end
Comment on lines 305 to +309
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Carry the Anthropic-family expansion into applyCaching().

This widens the outer gate, but applyCaching() still only treats providerID === "anthropic" / bedrock as message-level on Line 235. For google-vertex-anthropic and altimate-backend, array-content messages will still get cache metadata on the last content part instead of following the Anthropic path, so the new support stays partial.

Suggested fix
+    const isAnthropicLikeProvider =
+      model.providerID === "anthropic" ||
+      model.providerID === "google-vertex-anthropic" ||
+      model.providerID === "altimate-backend"
+
     for (const msg of unique([...system, ...final])) {
       const useMessageLevelOptions =
-        model.providerID === "anthropic" ||
+        isAnthropicLikeProvider ||
         model.providerID.includes("bedrock") ||
         model.api.npm === "`@ai-sdk/amazon-bedrock`"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(model.providerID === "anthropic" ||
// altimate_change start — altimate-specific Anthropic provider IDs
model.providerID === "google-vertex-anthropic" ||
model.providerID === "altimate-backend" ||
// altimate_change end
const isAnthropicLikeProvider =
model.providerID === "anthropic" ||
model.providerID === "google-vertex-anthropic" ||
model.providerID === "altimate-backend"
for (const msg of unique([...system, ...final])) {
const useMessageLevelOptions =
isAnthropicLikeProvider ||
model.providerID.includes("bedrock") ||
model.api.npm === "`@ai-sdk/amazon-bedrock`"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/provider/transform.ts` around lines 305 - 309, The
Anthropic-family provider IDs added to the model gating (e.g.,
"google-vertex-anthropic" and "altimate-backend") must also be included in the
caching logic inside applyCaching() so they follow the same Anthropic
message-level handling; update the conditional in applyCaching() that currently
checks providerID === "anthropic" (and the bedrock branch) to OR in the new IDs
and ensure that when messages are an array the cache metadata is attached via
the Anthropic path instead of only on the last content part—search for the
applyCaching function and the providerID === "anthropic" check and extend that
predicate to include "google-vertex-anthropic" and "altimate-backend".

model.api.id.includes("anthropic") ||
model.api.id.includes("claude") ||
model.id.includes("anthropic") ||
Expand Down Expand Up @@ -503,7 +525,17 @@ export namespace ProviderTransform {
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 }]))
// altimate_change start — upstream_fix: add "max" reasoning effort for deepseek-v4
// Ported from upstream-opencode commit f8e939d96f (#24163). DeepSeek-v4 advertises a
// `max` reasoning effort that the default WIDELY_SUPPORTED_EFFORTS list omits. Drop
// this marker if upstream lifts the cap or ships the same shape.
return Object.fromEntries(
[
...WIDELY_SUPPORTED_EFFORTS,
...(model.api.id.includes("deepseek-v4") ? ["max"] : []),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The model.api.id.includes("deepseek-v4") check here is dead code for the models it's meant to support. An earlier guard in this function returns {} for any model whose id includes "deepseek", which means deepseek-v4 models never reach this @ai-sdk/openai-compatible case. You need to exclude deepseek-v4 from that early return (e.g., id.includes("deepseek") && !id.includes("deepseek-v4")) for this branch to be reachable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/provider/transform.ts, line 535:

<comment>The `model.api.id.includes("deepseek-v4")` check here is dead code for the models it's meant to support. An earlier guard in this function returns `{}` for any model whose `id` includes `"deepseek"`, which means `deepseek-v4` models never reach this `@ai-sdk/openai-compatible` case. You need to exclude `deepseek-v4` from that early return (e.g., `id.includes("deepseek") && !id.includes("deepseek-v4")`) for this branch to be reachable.</comment>

<file context>
@@ -503,7 +525,17 @@ export namespace ProviderTransform {
+        return Object.fromEntries(
+          [
+            ...WIDELY_SUPPORTED_EFFORTS,
+            ...(model.api.id.includes("deepseek-v4") ? ["max"] : []),
+          ].map((effort) => [effort, { reasoningEffort: effort }]),
+        )
</file context>

].map((effort) => [effort, { reasoningEffort: effort }]),
)
// altimate_change end
Comment on lines +528 to +538
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

deepseek-v4 still never reaches this branch.

Line 402 returns {} for every model.id containing deepseek, so this new @ai-sdk/openai-compatible case is dead for the models it is meant to unlock. As written, deepseek-v4 still exposes no reasoning-effort variants.

Suggested fix
+    const isDeepSeekV4 = id.includes("deepseek-v4") || model.api.id.includes("deepseek-v4")
     if (
-      id.includes("deepseek") ||
+      (id.includes("deepseek") && !isDeepSeekV4) ||
       id.includes("minimax") ||
       id.includes("glm") ||
       id.includes("mistral") ||
       id.includes("kimi") ||
       id.includes("k2p5") ||
       id.includes("qwen")
     )
       return {}


case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
Expand Down
Loading