opencode: Model updates + fixes (#60526)

**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
This commit is contained in:
Vlad Ionescu 2026-07-07 13:16:42 +03:00 committed by GitHub
parent 64c55b038f
commit 452e1cb273
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 191 additions and 49 deletions

View file

@ -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<LanguageModelEffortLevel> {
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();

View file

@ -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<Vec<ReasoningEffort>> {
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,
..

View file

@ -217,6 +217,18 @@ pub struct OpenCodeSettingsContent {
pub show_free_models: Option<bool>,
}
#[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<String>,
pub max_tokens: u64,
pub max_output_tokens: Option<u64>,
/// 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<OpenCodeApiProtocol>,
/// The subscription for this model: "zen", "go", or "free". Defaults to Zen.
pub subscription: Option<OpenCodeModelSubscription>,
/// Custom Model API URL to use for this model.

View file

@ -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