From 81e34fe751c4f525cd0a7e616bb374d34e5f0a00 Mon Sep 17 00:00:00 2001 From: Vlad Ionescu Date: Tue, 5 May 2026 17:30:17 +0300 Subject: [PATCH] opencode: Support interleaved_reasoning and fix DeepSeek (#55574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode API endpoints for DeepSeek were [moved from Anthropic-compatible to OpenAI-compatible](https://github.com/anomalyco/opencode/pull/24500) and DeepSeek requires interleaved reasoning enabled to work. I ran a _"rename this variable to potato"_ test and I can confirm DeepSeek V4 Flash and Pro both work now 🎉 Some other OpenCode Go models were marked [on models.dev](https://github.com/anomalyco/models.dev/tree/dev/providers/opencode-go/models) as supporting `interleaved_reasoning` so they too got that enabled. Kimi K2.5 and Kimi K2.6 continue to fail with https://github.com/zed-industries/zed/issues/51743 (https://github.com/zed-industries/zed/pull/55085 seems to hint at this being [an OpenCode issue](https://github.com/zed-industries/zed/issues/51743#issuecomment-4336785765)?), but all other models seem to work fine both with `interleaved_reasoning` and without it 🤷 I assume it's better to have that turned on? Again, the intersection of OpenAI Chat Completions API, different models, different inference providers, how they all work together is something I know nothing about! Self-Review Checklist: - [X] I've reviewed my own diff for quality, security, and reliability - [X] Unsafe blocks (if any) have justifying comments - [X] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [X] Performance impact has been considered and is acceptable Release Notes: - OpenCode Go: use correct DeepSeek endpoints - OpenCode: add support for interleaved_reasoning --- .../language_models/src/provider/opencode.rs | 3 +- crates/opencode/src/opencode.rs | 35 ++++++++++++++----- crates/settings_content/src/language_model.rs | 3 ++ docs/src/ai/llm-providers.md | 2 ++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/crates/language_models/src/provider/opencode.rs b/crates/language_models/src/provider/opencode.rs index 6c0d4a5d3f..4380d2e1a1 100644 --- a/crates/language_models/src/provider/opencode.rs +++ b/crates/language_models/src/provider/opencode.rs @@ -278,6 +278,7 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider { protocol, reasoning_effort_levels: model.reasoning_effort_levels.clone(), custom_model_api_url: model.custom_model_api_url.clone(), + interleaved_reasoning: model.interleaved_reasoning, }; let key = format!("{}/{}", subscription.id_prefix(), model.name); models.insert(key, (custom_model, subscription)); @@ -664,7 +665,7 @@ impl LanguageModel for OpenCodeLanguageModel { false, self.model.max_output_tokens(), reasoning_effort, - false, + self.model.interleaved_reasoning(), ); let stream = self.stream_openai_chat(openai_request, http_client, cx); async move { diff --git a/crates/opencode/src/opencode.rs b/crates/opencode/src/opencode.rs index 5ac3441101..9563b77611 100644 --- a/crates/opencode/src/opencode.rs +++ b/crates/opencode/src/opencode.rs @@ -168,6 +168,7 @@ pub enum Model { protocol: ApiProtocol, reasoning_effort_levels: Option>, custom_model_api_url: Option, + interleaved_reasoning: bool, }, } @@ -385,8 +386,6 @@ impl Model { Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google, - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => ApiProtocol::Anthropic, - Self::MiniMaxM2_5Free | Self::Glm5 | Self::Glm5_1 @@ -398,6 +397,8 @@ impl Model { | Self::MimoV2_5 | Self::Qwen3_5Plus | Self::Qwen3_6Plus + | Self::DeepSeekV4Pro + | Self::DeepSeekV4Flash | Self::BigPickle | Self::Nemotron3SuperFree | Self::Ling2_6FlashFree @@ -407,6 +408,27 @@ impl Model { } } + pub fn interleaved_reasoning(&self) -> bool { + match self { + Self::DeepSeekV4Pro + | Self::DeepSeekV4Flash + | Self::KimiK2_5 + | Self::KimiK2_6 + | Self::MimoV2Omni + | Self::MimoV2_5 + | Self::MimoV2_5Pro + | Self::Glm5 + | Self::Glm5_1 => true, + + Self::Custom { + interleaved_reasoning, + .. + } => *interleaved_reasoning, + + _ => false, + } + } + pub fn max_token_count(&self) -> u64 { match self { // Anthropic models @@ -487,9 +509,6 @@ impl Model { // Google models Self::Gemini3_1Pro | Self::Gemini3Flash => Some(65_536), - // Anthropic-compatible models - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000), - // OpenAI-compatible models Self::MiniMaxM2_7 => Some(131_072), Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(131_072), @@ -497,6 +516,7 @@ impl Model { Self::BigPickle => Some(128_000), Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536), Self::Qwen3_5Plus | Self::Qwen3_6Plus => Some(65_536), + Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000), Self::Nemotron3SuperFree => Some(128_000), Self::MimoV2_5Pro | Self::MimoV2_5 | Self::MimoV2Pro | Self::MimoV2Omni => { Some(128_000) @@ -565,14 +585,13 @@ impl Model { | Self::MiniMaxM2_7 | Self::MimoV2Pro | Self::MimoV2_5Pro + | Self::DeepSeekV4Pro + | Self::DeepSeekV4Flash | Self::BigPickle | Self::Nemotron3SuperFree | Self::Ling2_6FlashFree | Self::Hy3PreviewFree => false, - // DeepSeek models (Anthropic protocol) don't support images - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => false, - Self::Custom { protocol, .. } => matches!( protocol, ApiProtocol::Anthropic diff --git a/crates/settings_content/src/language_model.rs b/crates/settings_content/src/language_model.rs index 0fe922ae0b..469be983f0 100644 --- a/crates/settings_content/src/language_model.rs +++ b/crates/settings_content/src/language_model.rs @@ -181,6 +181,9 @@ pub struct OpenCodeAvailableModel { pub custom_model_api_url: Option, /// Supported reasoning effort levels, for example `["low", "medium", "high"]. pub reasoning_effort_levels: Option>, + /// When using OpenAiChat protocol, whether thinking tokens are sent as a dedicated `reasoning_content` field or inline in message text. + #[serde(default)] + pub interleaved_reasoning: bool, } #[with_fallible_options] diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index b5dd98afe1..b32c433803 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -663,6 +663,7 @@ The Zed agent comes pre-configured with OpenCode models. If you wish to use newe "max_output_tokens": 98765, "protocol": "openai_chat", "reasoning_effort_levels": ["low", "medium", "high"], + "interleaved_reasoning": false, "subscription": "go", "custom_model_api_url": "https://example.com/zen" } @@ -680,6 +681,7 @@ The available configuration options for custom models are: - `max_output_tokens` (optional): maximum tokens the model can generate, for example `64000` - `protocol` (required): model API protocol, one of `"anthropic"`, `"openai_responses"`, `"openai_chat"`, or `"google"` - `reasoning_effort_levels` (optional): list of supported reasoning effort levels, for example `["low", "medium", "high"]`. The latest value in the list is used as the default +- `interleaved_reasoning` (optional, default `false`): if thinking tokens are sent as a dedicated `reasoning_content` field (`true`) or inline in message text (`false`). Applies only when using the `openai_chat` protocol - `subscription` (optional): `"zen"`, `"go"`, or `"free"` (defaults to `"zen"`) - `custom_model_api_url` (optional): custom API base URL to use instead of the default OpenCode API