opencode: Support interleaved_reasoning and fix DeepSeek (#55574)

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
This commit is contained in:
Vlad Ionescu 2026-05-05 17:30:17 +03:00 committed by GitHub
parent 646672c35a
commit a42374250f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 34 additions and 9 deletions

View file

@ -278,6 +278,7 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider {
protocol, protocol,
reasoning_effort_levels: model.reasoning_effort_levels.clone(), reasoning_effort_levels: model.reasoning_effort_levels.clone(),
custom_model_api_url: model.custom_model_api_url.clone(), custom_model_api_url: model.custom_model_api_url.clone(),
interleaved_reasoning: model.interleaved_reasoning,
}; };
let key = format!("{}/{}", subscription.id_prefix(), model.name); let key = format!("{}/{}", subscription.id_prefix(), model.name);
models.insert(key, (custom_model, subscription)); models.insert(key, (custom_model, subscription));
@ -664,7 +665,7 @@ impl LanguageModel for OpenCodeLanguageModel {
false, false,
self.model.max_output_tokens(), self.model.max_output_tokens(),
reasoning_effort, reasoning_effort,
false, self.model.interleaved_reasoning(),
); );
let stream = self.stream_openai_chat(openai_request, http_client, cx); let stream = self.stream_openai_chat(openai_request, http_client, cx);
async move { async move {

View file

@ -168,6 +168,7 @@ pub enum Model {
protocol: ApiProtocol, protocol: ApiProtocol,
reasoning_effort_levels: Option<Vec<ReasoningEffort>>, reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
custom_model_api_url: Option<String>, custom_model_api_url: Option<String>,
interleaved_reasoning: bool,
}, },
} }
@ -385,8 +386,6 @@ impl Model {
Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google, Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google,
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => ApiProtocol::Anthropic,
Self::MiniMaxM2_5Free Self::MiniMaxM2_5Free
| Self::Glm5 | Self::Glm5
| Self::Glm5_1 | Self::Glm5_1
@ -398,6 +397,8 @@ impl Model {
| Self::MimoV2_5 | Self::MimoV2_5
| Self::Qwen3_5Plus | Self::Qwen3_5Plus
| Self::Qwen3_6Plus | Self::Qwen3_6Plus
| Self::DeepSeekV4Pro
| Self::DeepSeekV4Flash
| Self::BigPickle | Self::BigPickle
| Self::Nemotron3SuperFree | Self::Nemotron3SuperFree
| Self::Ling2_6FlashFree | 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 { pub fn max_token_count(&self) -> u64 {
match self { match self {
// Anthropic models // Anthropic models
@ -487,9 +509,6 @@ impl Model {
// Google models // Google models
Self::Gemini3_1Pro | Self::Gemini3Flash => Some(65_536), Self::Gemini3_1Pro | Self::Gemini3Flash => Some(65_536),
// Anthropic-compatible models
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000),
// OpenAI-compatible models // OpenAI-compatible models
Self::MiniMaxM2_7 => Some(131_072), Self::MiniMaxM2_7 => Some(131_072),
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(131_072), Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(131_072),
@ -497,6 +516,7 @@ impl Model {
Self::BigPickle => Some(128_000), Self::BigPickle => Some(128_000),
Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536), Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536),
Self::Qwen3_5Plus | Self::Qwen3_6Plus => 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::Nemotron3SuperFree => Some(128_000),
Self::MimoV2_5Pro | Self::MimoV2_5 | Self::MimoV2Pro | Self::MimoV2Omni => { Self::MimoV2_5Pro | Self::MimoV2_5 | Self::MimoV2Pro | Self::MimoV2Omni => {
Some(128_000) Some(128_000)
@ -565,14 +585,13 @@ impl Model {
| Self::MiniMaxM2_7 | Self::MiniMaxM2_7
| Self::MimoV2Pro | Self::MimoV2Pro
| Self::MimoV2_5Pro | Self::MimoV2_5Pro
| Self::DeepSeekV4Pro
| Self::DeepSeekV4Flash
| Self::BigPickle | Self::BigPickle
| Self::Nemotron3SuperFree | Self::Nemotron3SuperFree
| Self::Ling2_6FlashFree | Self::Ling2_6FlashFree
| Self::Hy3PreviewFree => false, | Self::Hy3PreviewFree => false,
// DeepSeek models (Anthropic protocol) don't support images
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => false,
Self::Custom { protocol, .. } => matches!( Self::Custom { protocol, .. } => matches!(
protocol, protocol,
ApiProtocol::Anthropic ApiProtocol::Anthropic

View file

@ -181,6 +181,9 @@ pub struct OpenCodeAvailableModel {
pub custom_model_api_url: Option<String>, pub custom_model_api_url: Option<String>,
/// Supported reasoning effort levels, for example `["low", "medium", "high"]. /// Supported reasoning effort levels, for example `["low", "medium", "high"].
pub reasoning_effort_levels: Option<Vec<ReasoningEffort>>, pub reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
/// 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] #[with_fallible_options]

View file

@ -663,6 +663,7 @@ The Zed agent comes pre-configured with OpenCode models. If you wish to use newe
"max_output_tokens": 98765, "max_output_tokens": 98765,
"protocol": "openai_chat", "protocol": "openai_chat",
"reasoning_effort_levels": ["low", "medium", "high"], "reasoning_effort_levels": ["low", "medium", "high"],
"interleaved_reasoning": false,
"subscription": "go", "subscription": "go",
"custom_model_api_url": "https://example.com/zen" "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` - `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"` - `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 - `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"`) - `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 - `custom_model_api_url` (optional): custom API base URL to use instead of the default OpenCode API