diff --git a/Cargo.lock b/Cargo.lock index 86f04c0c889fe7..2180e281022440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11898,6 +11898,7 @@ dependencies = [ "futures 0.3.32", "google_ai", "http_client", + "language_model_core", "schemars 1.0.4", "serde", "serde_json", diff --git a/crates/language_models/src/provider/opencode.rs b/crates/language_models/src/provider/opencode.rs index f8097b1798d863..6c0d4a5d3fe1b8 100644 --- a/crates/language_models/src/provider/opencode.rs +++ b/crates/language_models/src/provider/opencode.rs @@ -7,9 +7,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::{AsyncBody, HttpClient, http}; use language_model::{ ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, - LanguageModelRequest, LanguageModelToolChoice, RateLimiter, env_var, + LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter, + ReasoningEffort, env_var, }; use opencode::{ApiProtocol, OPENCODE_API_URL, OpenCodeSubscription}; pub use settings::OpenCodeAvailableModel as AvailableModel; @@ -29,6 +30,27 @@ use crate::provider::open_ai::{ OpenAiEventMapper, OpenAiResponseEventMapper, into_open_ai, into_open_ai_response, }; +fn normalize_reasoning_effort(effort: &str) -> Option { + match effort.trim().to_ascii_lowercase().as_str() { + "minimal" => Some(ReasoningEffort::Minimal), + "low" => Some(ReasoningEffort::Low), + "medium" => Some(ReasoningEffort::Medium), + "high" => Some(ReasoningEffort::High), + "max" | "xhigh" => Some(ReasoningEffort::XHigh), + _ => None, + } +} + +fn reasoning_effort_display(effort: ReasoningEffort) -> (&'static str, &'static str) { + match effort { + ReasoningEffort::Minimal => ("Minimal", "minimal"), + ReasoningEffort::Low => ("Low", "low"), + ReasoningEffort::Medium => ("Medium", "medium"), + ReasoningEffort::High => ("High", "high"), + ReasoningEffort::XHigh => ("Max", "max"), + } +} + const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("opencode"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenCode"); @@ -254,6 +276,7 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider { max_tokens: model.max_tokens, max_output_tokens: model.max_output_tokens, protocol, + reasoning_effort_levels: model.reasoning_effort_levels.clone(), custom_model_api_url: model.custom_model_api_url.clone(), }; let key = format!("{}/{}", subscription.id_prefix(), model.name); @@ -522,6 +545,36 @@ impl LanguageModel for OpenCodeLanguageModel { self.model.supports_images() } + fn supports_thinking(&self) -> bool { + self.model + .supported_reasoning_effort_levels() + .is_some_and(|levels| !levels.is_empty()) + } + + fn supported_effort_levels(&self) -> Vec { + self.model + .supported_reasoning_effort_levels() + .map(|levels| { + if levels.is_empty() { + return Vec::new(); + } + let default_index = levels.len() - 1; + levels + .into_iter() + .enumerate() + .map(|(i, effort)| { + let (name, value) = reasoning_effort_display(effort); + LanguageModelEffortLevel { + name: name.into(), + value: value.into(), + is_default: i == default_index, + } + }) + .collect() + }) + .unwrap_or_default() + } + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { LanguageModelToolChoice::Auto | LanguageModelToolChoice::Any => true, @@ -576,12 +629,17 @@ impl LanguageModel for OpenCodeLanguageModel { match self.model.protocol(self.subscription) { ApiProtocol::Anthropic => { + let mode = if self.supports_thinking() && request.thinking_allowed { + anthropic::AnthropicModelMode::AdaptiveThinking + } else { + anthropic::AnthropicModelMode::Default + }; let anthropic_request = into_anthropic( request, self.model.id().to_string(), 1.0, self.model.max_output_tokens().unwrap_or(8192), - anthropic::AnthropicModelMode::Default, + mode, ); let stream = self.stream_anthropic(anthropic_request, http_client, cx); async move { @@ -591,13 +649,21 @@ impl LanguageModel for OpenCodeLanguageModel { .boxed() } ApiProtocol::OpenAiChat => { + let reasoning_effort = if request.thinking_allowed { + request + .thinking_effort + .as_deref() + .and_then(normalize_reasoning_effort) + } else { + None + }; let openai_request = into_open_ai( request, self.model.id(), false, false, self.model.max_output_tokens(), - None, + reasoning_effort, false, ); let stream = self.stream_openai_chat(openai_request, http_client, cx); @@ -608,13 +674,21 @@ impl LanguageModel for OpenCodeLanguageModel { .boxed() } ApiProtocol::OpenAiResponses => { + let reasoning_effort = if request.thinking_allowed { + request + .thinking_effort + .as_deref() + .and_then(normalize_reasoning_effort) + } else { + None + }; let response_request = into_open_ai_response( request, self.model.id(), false, false, self.model.max_output_tokens(), - None, + reasoning_effort, ); let stream = self.stream_openai_response(response_request, http_client, cx); async move { diff --git a/crates/opencode/Cargo.toml b/crates/opencode/Cargo.toml index 758d2f2479b9f8..035d78d53f4d7c 100644 --- a/crates/opencode/Cargo.toml +++ b/crates/opencode/Cargo.toml @@ -21,6 +21,7 @@ anyhow.workspace = true futures.workspace = true google_ai.workspace = true http_client.workspace = true +language_model_core.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true diff --git a/crates/opencode/src/opencode.rs b/crates/opencode/src/opencode.rs index 9278d81677b08f..5ac344110115f7 100644 --- a/crates/opencode/src/opencode.rs +++ b/crates/opencode/src/opencode.rs @@ -1,6 +1,7 @@ use anyhow::{Result, anyhow}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; +use language_model_core::ReasoningEffort; use serde::{Deserialize, Serialize}; use strum::EnumIter; @@ -76,6 +77,10 @@ pub enum Model { Claude3_5Haiku, // -- OpenAI Responses API models -- + #[serde(rename = "gpt-5.5")] + Gpt5_5, + #[serde(rename = "gpt-5.5-pro")] + Gpt5_5Pro, #[serde(rename = "gpt-5.4")] Gpt5_4, #[serde(rename = "gpt-5.4-pro")] @@ -114,6 +119,14 @@ pub enum Model { Gemini3Flash, // -- OpenAI Chat Completions protocol models -- + #[serde(rename = "deepseek-v4-pro")] + DeepSeekV4Pro, + #[serde(rename = "deepseek-v4-flash")] + DeepSeekV4Flash, + #[serde(rename = "ling-2.6-flash-free")] + Ling2_6FlashFree, + #[serde(rename = "hy3-preview-free")] + Hy3PreviewFree, #[serde(rename = "minimax-m2.5")] MiniMaxM2_5, #[serde(rename = "minimax-m2.5-free")] @@ -132,6 +145,10 @@ pub enum Model { MimoV2Pro, #[serde(rename = "mimo-v2-omni")] MimoV2Omni, + #[serde(rename = "mimo-v2.5-pro")] + MimoV2_5Pro, + #[serde(rename = "mimo-v2.5")] + MimoV2_5, #[serde(rename = "big-pickle")] BigPickle, #[serde(rename = "nemotron-3-super-free")] @@ -149,6 +166,7 @@ pub enum Model { max_tokens: u64, max_output_tokens: Option, protocol: ApiProtocol, + reasoning_effort_levels: Option>, custom_model_api_url: Option, }, } @@ -186,12 +204,20 @@ impl Model { | Self::Qwen3_6Plus => &[OpenCodeSubscription::Zen, OpenCodeSubscription::Go], // Go-only models - Self::MiniMaxM2_7 | Self::MimoV2Pro | Self::MimoV2Omni => &[OpenCodeSubscription::Go], + Self::MiniMaxM2_7 + | Self::MimoV2_5Pro + | Self::MimoV2_5 + | Self::MimoV2Pro + | Self::MimoV2Omni + | Self::DeepSeekV4Pro + | Self::DeepSeekV4Flash => &[OpenCodeSubscription::Go], // Free models - Self::MiniMaxM2_5Free | Self::Nemotron3SuperFree | Self::BigPickle => { - &[OpenCodeSubscription::Free] - } + Self::MiniMaxM2_5Free + | Self::Nemotron3SuperFree + | Self::BigPickle + | Self::Ling2_6FlashFree + | Self::Hy3PreviewFree => &[OpenCodeSubscription::Free], // Custom models get their subscription from settings, not from here Self::Custom { .. } => &[], @@ -213,6 +239,8 @@ impl Model { Self::ClaudeHaiku4_5 => "claude-haiku-4-5", Self::Claude3_5Haiku => "claude-3-5-haiku", + Self::Gpt5_5 => "gpt-5.5", + Self::Gpt5_5Pro => "gpt-5.5-pro", Self::Gpt5_4 => "gpt-5.4", Self::Gpt5_4Pro => "gpt-5.4-pro", Self::Gpt5_4Mini => "gpt-5.4-mini", @@ -232,6 +260,10 @@ impl Model { Self::Gemini3_1Pro => "gemini-3.1-pro", Self::Gemini3Flash => "gemini-3-flash", + Self::DeepSeekV4Pro => "deepseek-v4-pro", + Self::DeepSeekV4Flash => "deepseek-v4-flash", + Self::Ling2_6FlashFree => "ling-2.6-flash-free", + Self::Hy3PreviewFree => "hy3-preview-free", Self::MiniMaxM2_5 => "minimax-m2.5", Self::MiniMaxM2_5Free => "minimax-m2.5-free", Self::Glm5 => "glm-5", @@ -241,6 +273,8 @@ impl Model { Self::MiniMaxM2_7 => "minimax-m2.7", Self::MimoV2Pro => "mimo-v2-pro", Self::MimoV2Omni => "mimo-v2-omni", + Self::MimoV2_5Pro => "mimo-v2.5-pro", + Self::MimoV2_5 => "mimo-v2.5", Self::Qwen3_5Plus => "qwen3.5-plus", Self::Qwen3_6Plus => "qwen3.6-plus", Self::BigPickle => "big-pickle", @@ -262,6 +296,8 @@ impl Model { Self::ClaudeHaiku4_5 => "Claude Haiku 4.5", Self::Claude3_5Haiku => "Claude Haiku 3.5", + Self::Gpt5_5 => "GPT 5.5", + Self::Gpt5_5Pro => "GPT 5.5 Pro", Self::Gpt5_4 => "GPT 5.4", Self::Gpt5_4Pro => "GPT 5.4 Pro", Self::Gpt5_4Mini => "GPT 5.4 Mini", @@ -281,6 +317,10 @@ impl Model { Self::Gemini3_1Pro => "Gemini 3.1 Pro", Self::Gemini3Flash => "Gemini 3 Flash", + Self::DeepSeekV4Pro => "DeepSeek V4 Pro", + Self::DeepSeekV4Flash => "DeepSeek V4 Flash", + Self::Ling2_6FlashFree => "Ling 2.6 Flash Free", + Self::Hy3PreviewFree => "Hy3 Preview Free", Self::MiniMaxM2_5 => "MiniMax M2.5", Self::MiniMaxM2_5Free => "MiniMax M2.5 Free", Self::Glm5 => "GLM 5", @@ -290,6 +330,8 @@ impl Model { Self::MiniMaxM2_7 => "MiniMax M2.7", Self::MimoV2Pro => "MiMo V2 Pro", Self::MimoV2Omni => "MiMo V2 Omni", + Self::MimoV2_5Pro => "MiMo V2.5 Pro", + Self::MimoV2_5 => "MiMo V2.5", Self::Qwen3_5Plus => "Qwen3.5 Plus", Self::Qwen3_6Plus => "Qwen3.6 Plus", Self::BigPickle => "Big Pickle", @@ -323,7 +365,9 @@ impl Model { | Self::ClaudeHaiku4_5 | Self::Claude3_5Haiku => ApiProtocol::Anthropic, - Self::Gpt5_4 + Self::Gpt5_5 + | Self::Gpt5_5Pro + | Self::Gpt5_4 | Self::Gpt5_4Pro | Self::Gpt5_4Mini | Self::Gpt5_4Nano @@ -341,6 +385,8 @@ impl Model { Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google, + Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => ApiProtocol::Anthropic, + Self::MiniMaxM2_5Free | Self::Glm5 | Self::Glm5_1 @@ -348,10 +394,14 @@ impl Model { | Self::KimiK2_6 | Self::MimoV2Pro | Self::MimoV2Omni + | Self::MimoV2_5Pro + | Self::MimoV2_5 | Self::Qwen3_5Plus | Self::Qwen3_6Plus | Self::BigPickle - | Self::Nemotron3SuperFree => ApiProtocol::OpenAiChat, + | Self::Nemotron3SuperFree + | Self::Ling2_6FlashFree + | Self::Hy3PreviewFree => ApiProtocol::OpenAiChat, Self::Custom { protocol, .. } => *protocol, } @@ -369,6 +419,7 @@ impl Model { Self::Claude3_5Haiku => 200_000, // OpenAI models + Self::Gpt5_5 | Self::Gpt5_5Pro => 1_050_000, Self::Gpt5_4 | Self::Gpt5_4Pro => 1_050_000, Self::Gpt5_4Mini | Self::Gpt5_4Nano => 400_000, Self::Gpt5_3Codex => 400_000, @@ -386,13 +437,17 @@ impl Model { // OpenAI-compatible models Self::MiniMaxM2_7 => 204_800, Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => 204_800, - Self::Glm5 | Self::Glm5_1 => 204_800, + Self::Glm5 | Self::Glm5_1 => 202_725, Self::KimiK2_6 | Self::KimiK2_5 => 262_144, - Self::MimoV2Pro => 1_048_576, + Self::MimoV2_5Pro | Self::MimoV2Pro => 1_048_576, + Self::MimoV2_5 => 1_000_000, Self::MimoV2Omni => 262_144, Self::Qwen3_5Plus | Self::Qwen3_6Plus => 262_144, Self::BigPickle => 200_000, Self::Nemotron3SuperFree => 204_800, + Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => 1_000_000, + Self::Ling2_6FlashFree => 262_100, + Self::Hy3PreviewFree => 256_000, Self::Custom { max_tokens, .. } => *max_tokens, } @@ -411,7 +466,9 @@ impl Model { Self::Claude3_5Haiku => Some(8_192), // OpenAI models - Self::Gpt5_4 + Self::Gpt5_5 + | Self::Gpt5_5Pro + | Self::Gpt5_4 | Self::Gpt5_4Pro | Self::Gpt5_4Mini | Self::Gpt5_4Nano @@ -430,15 +487,22 @@ 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), - Self::Glm5 | Self::Glm5_1 => Some(131_072), + Self::Glm5 | Self::Glm5_1 => Some(32_768), Self::BigPickle => Some(128_000), Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536), Self::Qwen3_5Plus | Self::Qwen3_6Plus => Some(65_536), Self::Nemotron3SuperFree => Some(128_000), - Self::MimoV2Pro | Self::MimoV2Omni => Some(64_000), + Self::MimoV2_5Pro | Self::MimoV2_5 | Self::MimoV2Pro | Self::MimoV2Omni => { + Some(128_000) + } + Self::Ling2_6FlashFree => Some(32_800), + Self::Hy3PreviewFree => Some(64_000), Self::Custom { max_output_tokens, .. @@ -464,7 +528,9 @@ impl Model { | Self::Claude3_5Haiku => true, // OpenAI models support images - Self::Gpt5_4 + Self::Gpt5_5 + | Self::Gpt5_5Pro + | Self::Gpt5_4 | Self::Gpt5_4Pro | Self::Gpt5_4Mini | Self::Gpt5_4Nano @@ -487,6 +553,7 @@ impl Model { Self::KimiK2_6 | Self::KimiK2_5 | Self::MimoV2Omni + | Self::MimoV2_5 | Self::Qwen3_5Plus | Self::Qwen3_6Plus => true, @@ -497,8 +564,14 @@ impl Model { | Self::Glm5_1 | Self::MiniMaxM2_7 | Self::MimoV2Pro + | Self::MimoV2_5Pro | Self::BigPickle - | Self::Nemotron3SuperFree => false, + | 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, @@ -509,6 +582,34 @@ impl Model { ), } } + + pub fn supported_reasoning_effort_levels(&self) -> Option> { + match self { + Self::MimoV2_5Pro + | Self::MimoV2_5 + | Self::MimoV2Pro + | Self::MimoV2Omni + | Self::Hy3PreviewFree => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ]), + + Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(vec![ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ]), + + Self::Custom { + reasoning_effort_levels, + .. + } => reasoning_effort_levels.clone(), + + _ => None, + } + } } /// Stream generate content for Google models via OpenCode. diff --git a/crates/settings_content/src/language_model.rs b/crates/settings_content/src/language_model.rs index 1a16c5264a70bd..619e9d72f84703 100644 --- a/crates/settings_content/src/language_model.rs +++ b/crates/settings_content/src/language_model.rs @@ -1,5 +1,6 @@ use crate::merge_from::MergeFrom; use collections::HashMap; +use language_model_core::ReasoningEffort; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings_macros::{MergeFrom, with_fallible_options}; @@ -179,6 +180,8 @@ pub struct OpenCodeAvailableModel { pub subscription: Option, /// Custom Model API URL to use for this model. pub custom_model_api_url: Option, + /// Supported reasoning effort levels, for example `["low", "medium", "high"]. + pub reasoning_effort_levels: Option>, } #[with_fallible_options] diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index fad9ace28aa5da..b38874730ecd5b 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_tokens": 123456, "max_output_tokens": 98765, "protocol": "openai_chat", + "reasoning_effort_levels": ["low", "medium", "high"], "subscription": "go", "custom_model_api_url": "https://example.com/zen" } @@ -679,6 +680,7 @@ The available configuration options for custom models are: - `max_tokens` (required): maximum model context window size, for example `1000000` - `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 - `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