From 114307768e6dab98907ae19fb36f83dc442c83a2 Mon Sep 17 00:00:00 2001 From: Vlad Ionescu Date: Tue, 7 Jul 2026 13:16:42 +0300 Subject: [PATCH] opencode: Model updates + fixes (#60526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **TL;DR**: model updates + reasoning levels + fixes discovered when working on https://github.com/zed-industries/zed/pull/60373 # Objective Since the model auto-discovery PR was [cancelled](https://github.com/zed-industries/zed/pull/60373#issuecomment-4886521448), here is a manual model list update! I also copied the stand-alone bugfixes/enhancements from that PR. ## Solution A lot of manual work 😅 **OpenCode Zen**: - added Fable 5 and Sonnet 5 - added models that were previously only available on OpenCode Go: GLM 5.2, Kimi K2.7 Code, and Minimax M3 - added reasoning levels for all models. I started from the data on [`Models.dev`](https://models.dev) (the `/api.json` raw data), and then I matched that with what is shown in the OpenCode CLI and what I know to be true **OpenCode Go**: - added reasoning levels for GLM 5.2 **OpenCode in general**: - added `protocol` validation for the settings, by moving from a random `String` to an `enum`, for both nicer error messages (random strings or typos will get an error instead of using `openai_chat` by default) and to avoid issues like [folks saying non-existent protocols are a thing](https://github.com/zed-industries/zed/issues/56869#issuecomment-4550154554) - enabled parallel tool calls by default. As per [OpenCode developer on Discord](https://discord.com/channels/1391832426048651334/1471233160993050918/1472020924881702912), _"almost all models worth using support parallel tool calling natively"_. Manual tests confirmed all OpenCode Go models support this correctly (and was enabled by default on the OpenCode side for all but 1 model). I initially wanted to skip this from the release notes, but I added it so folks are aware of it in case any issues are caused by this being enabled for all models - allegedly fixed Google thinking since reasoning levels / thinking effort levels were added for Google models and an auto-checker LLM highlighted that was not properly configured - added support for the new-ish `supports_disabling_thinking` so thinking-only models don't get a no-impact toggle to disable thinking I have no idea if any of the Free models will disappear in 2 days or not, so I did not update those 🤷 (as per decision in https://github.com/zed-industries/zed/issues/56869#issuecomment-4466637648) ## Testing The Zen and Google changes were not tested as I don't have a Zen subscription and I stubbornly refuse to get one. The Free&Go changes were tested by running a "_rename this variable for me. add a function. delete the function_" test with a few different models. ## 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 adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Agent: OpenCode settings now validate `protocol` values - Agent: OpenCode only shows the "Disable thinking" toggle if thinking can indeed be disabled/enabled - Agent: OpenCode models now enable parallel tool calls by default - Agent: Updated OpenCode Zen models (added Fable 5, Sonnet 5, GLM 5.2, Kimi K2.7 Code, and Minimax M3) - Agent: Added OpenCode Go GLM 5.2 reasoning effort levels - Agent: Added reasoning effort levels for all OpenCode Zen models - Agent: Fixed thinking for OpenCode Zen Google models --- .../language_models/src/provider/opencode.rs | 36 ++-- crates/opencode/src/opencode.rs | 184 +++++++++++++++--- crates/settings_content/src/language_model.rs | 16 +- docs/src/ai/use-api-access.md | 4 +- 4 files changed, 191 insertions(+), 49 deletions(-) diff --git a/crates/language_models/src/provider/opencode.rs b/crates/language_models/src/provider/opencode.rs index 94689cc0d6..c3e5c0f8da 100644 --- a/crates/language_models/src/provider/opencode.rs +++ b/crates/language_models/src/provider/opencode.rs @@ -14,6 +14,7 @@ use language_model::{ SubPageProviderSettings, env_var, }; use opencode::{ApiProtocol, OPENCODE_API_URL, OpenCodeSubscription}; +pub use settings::OpenCodeApiProtocol; pub use settings::OpenCodeAvailableModel as AvailableModel; use settings::{Settings, SettingsStore, update_settings_file}; use std::sync::{Arc, LazyLock}; @@ -263,12 +264,12 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider { } for model in &settings.available_models { - let protocol = match model.protocol.as_str() { - "anthropic" => ApiProtocol::Anthropic, - "openai_responses" => ApiProtocol::OpenAiResponses, - "openai_chat" => ApiProtocol::OpenAiChat, - "google" => ApiProtocol::Google, - _ => ApiProtocol::OpenAiChat, // default fallback + let protocol = match model.protocol { + Some(OpenCodeApiProtocol::Anthropic) => ApiProtocol::Anthropic, + Some(OpenCodeApiProtocol::OpenAiResponses) => ApiProtocol::OpenAiResponses, + Some(OpenCodeApiProtocol::OpenAiChat) => ApiProtocol::OpenAiChat, + Some(OpenCodeApiProtocol::Google) => ApiProtocol::Google, + None => ApiProtocol::OpenAiChat, // default fallback }; let subscription = match model.subscription { Some(settings::OpenCodeModelSubscription::Go) => OpenCodeSubscription::Go, @@ -574,6 +575,12 @@ impl LanguageModel for OpenCodeLanguageModel { .is_some_and(|levels| levels.iter().any(|effort| *effort != ReasoningEffort::None)) } + fn supports_disabling_thinking(&self) -> bool { + self.model + .supported_reasoning_effort_levels() + .is_some_and(|levels| levels.contains(&ReasoningEffort::None)) + } + fn supported_effort_levels(&self) -> Vec { self.model .supported_reasoning_effort_levels() @@ -692,7 +699,7 @@ impl LanguageModel for OpenCodeLanguageModel { let openai_request = into_open_ai( request, self.model.id(), - false, + true, false, self.model.max_output_tokens(self.subscription), ChatCompletionMaxTokensParameter::MaxCompletionTokens, @@ -715,7 +722,7 @@ impl LanguageModel for OpenCodeLanguageModel { let response_request = into_open_ai_response( request, self.model.id(), - false, + true, false, self.model.max_output_tokens(self.subscription), None, @@ -730,11 +737,14 @@ impl LanguageModel for OpenCodeLanguageModel { .boxed() } ApiProtocol::Google => { - let google_request = into_google( - request, - self.model.id().to_string(), - google_ai::GoogleModelMode::Default, - ); + let mode = if self.supports_thinking() && request.thinking_allowed { + google_ai::GoogleModelMode::Thinking { + budget_tokens: None, + } + } else { + google_ai::GoogleModelMode::Default + }; + let google_request = into_google(request, self.model.id().to_string(), mode); let stream = self.stream_google(google_request, http_client, extra_headers, cx); async move { let mapper = GoogleEventMapper::new(); diff --git a/crates/opencode/src/opencode.rs b/crates/opencode/src/opencode.rs index ab030721e3..79d95b9cd2 100644 --- a/crates/opencode/src/opencode.rs +++ b/crates/opencode/src/opencode.rs @@ -77,6 +77,10 @@ pub enum Model { ClaudeSonnet4, #[serde(rename = "claude-haiku-4-5")] ClaudeHaiku4_5, + #[serde(rename = "claude-sonnet-5")] + ClaudeSonnet5, + #[serde(rename = "claude-fable-5")] + ClaudeFable5, // -- OpenAI Responses API models -- #[serde(rename = "gpt-5.5")] @@ -203,23 +207,24 @@ impl Model { match self { // Models available in both Zen and Go Self::Glm5_1 + | Self::Glm5_2 | Self::KimiK2_6 + | Self::KimiK2_7Code | Self::MiniMaxM2_7 + | Self::MiniMaxM3 | Self::DeepSeekV4Pro | Self::DeepSeekV4Flash | Self::Qwen3_6Plus => &[OpenCodeSubscription::Zen, OpenCodeSubscription::Go], // Go-only models - Self::MimoV2_5Pro - | Self::MimoV2_5 - | Self::Qwen3_7Plus - | Self::Qwen3_7Max - | Self::KimiK2_7Code - | Self::Glm5_2 - | Self::MiniMaxM3 => &[OpenCodeSubscription::Go], + Self::MimoV2_5Pro | Self::MimoV2_5 | Self::Qwen3_7Plus | Self::Qwen3_7Max => { + &[OpenCodeSubscription::Go] + } // Deprecated on Go (per models.dev); still offered on Zen - Self::Glm5 | Self::KimiK2_5 | Self::MiniMaxM2_5 => &[OpenCodeSubscription::Zen], + Self::Glm5 | Self::KimiK2_5 | Self::MiniMaxM2_5 | Self::Qwen3_5Plus => { + &[OpenCodeSubscription::Zen] + } // Free models Self::Nemotron3UltraFree | Self::BigPickle => &[OpenCodeSubscription::Free], @@ -243,6 +248,8 @@ impl Model { Self::ClaudeSonnet4_5 => "claude-sonnet-4-5", Self::ClaudeSonnet4 => "claude-sonnet-4", Self::ClaudeHaiku4_5 => "claude-haiku-4-5", + Self::ClaudeSonnet5 => "claude-sonnet-5", + Self::ClaudeFable5 => "claude-fable-5", Self::Gpt5_5 => "gpt-5.5", Self::Gpt5_5Pro => "gpt-5.5-pro", @@ -302,6 +309,8 @@ impl Model { Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5", Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeHaiku4_5 => "Claude Haiku 4.5", + Self::ClaudeSonnet5 => "Claude Sonnet 5", + Self::ClaudeFable5 => "Claude Fable 5", Self::Gpt5_5 => "GPT 5.5", Self::Gpt5_5Pro => "GPT 5.5 Pro", @@ -356,7 +365,7 @@ impl Model { match self { // Models offered by OpenCode have the same configuration across subscriptions // with one outlier: non-free MiniMax models - Self::MiniMaxM2_7 | Self::MiniMaxM2_5 => { + Self::MiniMaxM3 | Self::MiniMaxM2_7 | Self::MiniMaxM2_5 => { if subscription == OpenCodeSubscription::Zen { ApiProtocol::OpenAiChat } else { @@ -364,11 +373,13 @@ impl Model { } } - Self::ClaudeOpus4_8 + Self::ClaudeFable5 + | Self::ClaudeOpus4_8 | Self::ClaudeOpus4_7 | Self::ClaudeOpus4_6 | Self::ClaudeOpus4_5 | Self::ClaudeOpus4_1 + | Self::ClaudeSonnet5 | Self::ClaudeSonnet4_6 | Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4 @@ -394,9 +405,9 @@ impl Model { Self::Gemini3_1Pro | Self::Gemini3Flash | Self::Gemini3_5Flash => ApiProtocol::Google, - Self::Qwen3_7Max | Self::Qwen3_7Plus => ApiProtocol::Anthropic, - - Self::MiniMaxM3 => ApiProtocol::Anthropic, + Self::Qwen3_7Max | Self::Qwen3_7Plus | Self::Qwen3_6Plus | Self::Qwen3_5Plus => { + ApiProtocol::Anthropic + } Self::Glm5 | Self::Glm5_1 @@ -407,8 +418,6 @@ impl Model { | Self::KimiK2_7Code | Self::MimoV2_5Pro | Self::MimoV2_5 - | Self::Qwen3_5Plus - | Self::Qwen3_6Plus | Self::DeepSeekV4Pro | Self::DeepSeekV4Flash | Self::BigPickle @@ -432,6 +441,7 @@ impl Model { | Self::Glm5_2 | Self::MiniMaxM2_5 | Self::MiniMaxM2_7 + | Self::MiniMaxM3 | Self::Nemotron3UltraFree | Self::BigPickle => true, @@ -453,6 +463,8 @@ impl Model { Self::ClaudeOpus4_5 | Self::ClaudeHaiku4_5 => 200_000, Self::ClaudeOpus4_1 => 200_000, Self::ClaudeSonnet4 => 1_000_000, + Self::ClaudeSonnet5 => 1_000_000, + Self::ClaudeFable5 => 1_000_000, // OpenAI models Self::Gpt5_5 | Self::Gpt5_5Pro => 1_050_000, @@ -473,7 +485,13 @@ impl Model { // OpenAI-compatible models Self::MiniMaxM2_7 => 204_800, - Self::MiniMaxM3 => 512_000, + Self::MiniMaxM3 => { + if subscription == OpenCodeSubscription::Go { + 1_000_000 + } else { + 512_000 + } + } Self::MiniMaxM2_5 => 204_800, Self::Glm5 | Self::Glm5_1 => { if subscription == OpenCodeSubscription::Go { @@ -514,6 +532,8 @@ impl Model { | Self::ClaudeHaiku4_5 | Self::ClaudeSonnet4 => Some(64_000), Self::ClaudeOpus4_1 => Some(32_000), + Self::ClaudeSonnet5 => Some(128_000), + Self::ClaudeFable5 => Some(128_000), // OpenAI models Self::Gpt5_5 @@ -539,7 +559,13 @@ impl Model { // OpenAI-compatible models Self::MiniMaxM2_7 => Some(131_072), - Self::MiniMaxM3 => Some(131_072), + Self::MiniMaxM3 => { + if subscription == OpenCodeSubscription::Go { + Some(131_072) + } else { + Some(128_000) + } + } Self::MiniMaxM2_5 => { if subscription == OpenCodeSubscription::Go { Some(65_536) @@ -587,7 +613,9 @@ impl Model { | Self::ClaudeSonnet4_6 | Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4 - | Self::ClaudeHaiku4_5 => true, + | Self::ClaudeHaiku4_5 + | Self::ClaudeSonnet5 + | Self::ClaudeFable5 => true, // OpenAI models support images Self::Gpt5_5 @@ -649,20 +677,11 @@ impl Model { pub fn supported_reasoning_effort_levels(&self) -> Option> { match self { - Self::ClaudeOpus4_8 => Some(vec![ - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ReasoningEffort::XHigh, - ]), - - Self::Nemotron3UltraFree | Self::MimoV2_5Pro | Self::MimoV2_5 => Some(vec![ - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ]), - - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(vec![ + // Anthropic models + Self::ClaudeFable5 + | Self::ClaudeOpus4_8 + | Self::ClaudeOpus4_7 + | Self::ClaudeSonnet5 => Some(vec![ ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High, @@ -670,6 +689,107 @@ impl Model { ReasoningEffort::Max, ]), + Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6 => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::Max, + ]), + + Self::ClaudeOpus4_5 => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + // OpenAI models + Self::Gpt5_5 + | Self::Gpt5_4 + | Self::Gpt5_4Mini + | Self::Gpt5_4Nano + | Self::Gpt5_3Codex + | Self::Gpt5_2 => Some(vec![ + ReasoningEffort::None, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ]), + + Self::Gpt5_5Pro | Self::Gpt5_4Pro => Some(vec![ + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ]), + + Self::Gpt5_2Codex | Self::Gpt5_3Spark | Self::Gpt5_1CodexMax => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ]), + + Self::Gpt5_1 => Some(vec![ + ReasoningEffort::None, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + Self::Gpt5Codex | Self::Gpt5_1Codex | Self::Gpt5_1CodexMini => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + Self::Gpt5 | Self::Gpt5Nano => Some(vec![ + ReasoningEffort::Minimal, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + // Google models + Self::Gemini3Flash | Self::Gemini3_5Flash => Some(vec![ + ReasoningEffort::Minimal, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + Self::Gemini3_1Pro => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + // DeepSeek models + Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(vec![ + // OpenCode also supports Low&Medium but as per DeepSeek those are mapped to High + ReasoningEffort::High, + ReasoningEffort::Max, + ]), + + // MiniMax models + Self::MiniMaxM3 => Some(vec![ReasoningEffort::None]), + + // NVIDIA models + Self::Nemotron3UltraFree => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + // Xiaomi MiMo models + Self::MimoV2_5Pro | Self::MimoV2_5 => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + // Z AI models + Self::Glm5_2 => Some(vec![ReasoningEffort::High, ReasoningEffort::Max]), + Self::Custom { reasoning_effort_levels, .. diff --git a/crates/settings_content/src/language_model.rs b/crates/settings_content/src/language_model.rs index e58f3dcc02..f61d80ead7 100644 --- a/crates/settings_content/src/language_model.rs +++ b/crates/settings_content/src/language_model.rs @@ -217,6 +217,18 @@ pub struct OpenCodeSettingsContent { pub show_free_models: Option, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] +pub enum OpenCodeApiProtocol { + #[serde(rename = "anthropic")] + Anthropic, + #[serde(rename = "openai_responses", alias = "open_ai_responses")] + OpenAiResponses, + #[serde(rename = "openai_chat", alias = "open_ai_chat")] + OpenAiChat, + #[serde(rename = "google")] + Google, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] #[serde(rename_all = "snake_case")] pub enum OpenCodeModelSubscription { @@ -232,8 +244,8 @@ pub struct OpenCodeAvailableModel { pub display_name: Option, pub max_tokens: u64, pub max_output_tokens: Option, - /// The API protocol to use for this model: "anthropic", "openai_responses", "openai_chat", or "google". - pub protocol: String, + /// The API protocol to use for this model: "anthropic", "openai_responses", "openai_chat", or "google". Defaults to "openai_chat". + pub protocol: Option, /// The subscription for this model: "zen", "go", or "free". Defaults to Zen. pub subscription: Option, /// Custom Model API URL to use for this model. diff --git a/docs/src/ai/use-api-access.md b/docs/src/ai/use-api-access.md index 0ddf85ba33..7af0c0e68a 100644 --- a/docs/src/ai/use-api-access.md +++ b/docs/src/ai/use-api-access.md @@ -406,8 +406,8 @@ The available configuration options for custom OpenCode models are: - `display_name` (optional): human-readable model name shown in the UI, such as `Custom GLM 9000` - `max_tokens` (required): maximum model context window size, such as `1000000` - `max_output_tokens` (optional): maximum tokens the model can generate, such as `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, such as `["low", "medium", "high", "max"]`. The last value in the list is used as the default +- `protocol` (optional, default `"openai_chat"`): model API protocol, one of `"anthropic"`, `"openai_responses"`, `"openai_chat"`, or `"google"` +- `reasoning_effort_levels` (optional): list of supported reasoning effort levels, such as `["none", "low", "medium", "high", "xhigh", "max"]`. The last value in the list is used as the default - `interleaved_reasoning` (optional, default `false`): whether thinking tokens are sent as a dedicated `reasoning_content` field. 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