opencode: Add support for OpenCode Go (#53651)
**TL;DR**: add support for OpenCode Go (flat-rate monthly subscription) along the already-implemented OpenCode Zed (pay-as-you-go billing). > [!WARNING] > This code was written by LLMs, under the supervision of a so-called developer that never wrote Rust profesionally and that spends more time in Pages&Keynote than in an IDE. 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 ## Background OpenCode offers a few different ways to access models: - **free access to 3 models**, with feedback and data used to improve the model. These models use the OpenCode Zen API endpoints, but have different usage limits (200 requests per 5 hours) and have a different privacy policy. Some people disable or block the free models, some people are super-excited to have access to LLMs for free, and some people like using the free models to test new LLMs (at launch MiMo-V2 had 2 free weeks of usage, for example). - **pay-as-you-go access to 30 models** as part of the [OpenCode Zen](https://opencode.ai/zen) subscription. These models use the same OpenCode Zen API endpoints. - **flat-rate monthly access to 7 models** as part of the [OpenCode Go](https://opencode.ai/go) subscription. These models use the OpenCode Zen API endpoints with an extra `/go` appended to the path. There are 5-hour, weekly, and monthly usage limits and, additionally, users can toggle a switch in the OpenCode Console to use Zen models with their pay-as-you-go billing after the Go limits are hit. There's also a currently-paused [OpenCode Black](https://opencode.ai/black) flat-rate subscription with way higher usage limits and with access to more models, with $100 and $200 monthly plans. The whole thing is a bit messy, but it's great value and highly reliable LLM access! <br> https://github.com/zed-industries/zed/pull/49589 added support for OpenCode Zen by implementing a new `opencode` provider. OpenCode Go [could be used by overriding the API URL](https://github.com/zed-industries/zed/pull/49589#issuecomment-4130300454), but that is a terrible user experience: some models have to be manually added, the model list always shows the 30-something OpenCode Zen models, and free models cannot be used at all. I was annoyed by the experience of using OpenCode Go with Zed and this past week I had to test a bunch of LLMs and providers and harnesses, so I took this on as a test case 🙂 ## Implementation This PR makes the OpenCode provider more general (not just for Zen) and adds an `OpenCodeModelSubscription` concept which is then used to implement support for OpenCode Go. The free models are also broken out into their own subscription for a prettier model list. For a better user experience, the different subscriptions can be enabled or disabled, both in the settings file and in the UX: <img width="434" height="176" alt="Screenshot showing the OpenCode provider configuration, with the newly added toggles" src="https://github.com/user-attachments/assets/3520e114-00c8-4794-84bf-35cd72d9c57e" /> The code was written by LLMs, but I do understand it and I did a bunch of "manual" iterations and "manual" tweaks. Still, my Rust experience is non-existent so **I won't feel offended if y'all reject this PR**! I did consider alternatives (adding a new `opencode-go` provider and renaming this to `opencode-zen`, for example, or adding support for custom API URLs in OpenCode custom models which would've been the smallest code change but a terrible user experience, and so on) but all alternatives would have been, in my opinion, a worse user experience. **Tests I did**: - confirmed OpenCode Go models work as expected - confirmed OpenCode Zen Free models work as expected - confirmed I get an error when trying to use OpenCode Zen models since I don't have that subscription - confirmed the subcription toggles work as expected (model are shown/hidden, settings file is updated) **Notes**: - this PR is best reviewed commit-by-commit. I did not create a separate PR for the model updates to minimize delays - my exeprience with Rust is roughly zero, but I tried to strike a balance between idiomatic Rust and easy-to-read code - users of the OpenCode provider might have to do some re-configuration after this PR is merged since the model identifiers now include the subscription, eg `claude-haiku-4-5` is now `zen/claude-haiku-4-5`. Since this is a relatively new provider and the impact is small, I preffered that rather than adding complex migration/mapping logic. - does changing the provider name from "OpenCode Zen" to "OpenCode" break anything for y'all at Zed? - does changing the telemetry id from `"opencode/<model-id>"` to `"opencode/<subscription>/<model-id>"` break anything for y'all at Zed? --- Release Notes: - OpenCode provider: add support for OpenCode Go --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
1 parent
280f8b1048
commit
f59c122bee
5 files changed
+521
-100
No files matched your search
+163
-52
@@ -17,6 +17,40 @@ pub enum ApiProtocol {
|
||||
Google,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OpenCodeSubscription {
|
||||
Zen,
|
||||
Go,
|
||||
Free,
|
||||
}
|
||||
|
||||
impl OpenCodeSubscription {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Zen => "Zen",
|
||||
Self::Go => "Go",
|
||||
Self::Free => "Free",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Zen => "zen",
|
||||
Self::Go => "go",
|
||||
Self::Free => "free",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_path_suffix(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Zen | Self::Free => "",
|
||||
Self::Go => "/go",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
@@ -86,20 +120,26 @@ pub enum Model {
|
||||
MiniMaxM2_5Free,
|
||||
#[serde(rename = "glm-5")]
|
||||
Glm5,
|
||||
#[serde(rename = "glm-5.1")]
|
||||
Glm5_1,
|
||||
#[serde(rename = "kimi-k2.5")]
|
||||
KimiK2_5,
|
||||
#[serde(rename = "mimo-v2-pro-free")]
|
||||
MimoV2ProFree,
|
||||
#[serde(rename = "mimo-v2-omni-free")]
|
||||
MimoV2OmniFree,
|
||||
#[serde(rename = "mimo-v2-flash-free")]
|
||||
MimoV2FlashFree,
|
||||
#[serde(rename = "trinity-large-preview-free")]
|
||||
TrinityLargePreviewFree,
|
||||
#[serde(rename = "kimi-k2.6")]
|
||||
KimiK2_6,
|
||||
#[serde(rename = "minimax-m2.7")]
|
||||
MiniMaxM2_7,
|
||||
#[serde(rename = "mimo-v2-pro")]
|
||||
MimoV2Pro,
|
||||
#[serde(rename = "mimo-v2-omni")]
|
||||
MimoV2Omni,
|
||||
#[serde(rename = "big-pickle")]
|
||||
BigPickle,
|
||||
#[serde(rename = "nemotron-3-super-free")]
|
||||
Nemotron3SuperFree,
|
||||
#[serde(rename = "qwen3.5-plus")]
|
||||
Qwen3_5Plus,
|
||||
#[serde(rename = "qwen3.6-plus")]
|
||||
Qwen3_6Plus,
|
||||
|
||||
// -- Custom model --
|
||||
#[serde(rename = "custom")]
|
||||
@@ -109,6 +149,7 @@ pub enum Model {
|
||||
max_tokens: u64,
|
||||
max_output_tokens: Option<u64>,
|
||||
protocol: ApiProtocol,
|
||||
custom_model_api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,6 +158,49 @@ impl Model {
|
||||
Self::ClaudeHaiku4_5
|
||||
}
|
||||
|
||||
pub fn default_go() -> Self {
|
||||
Self::KimiK2_5
|
||||
}
|
||||
|
||||
pub fn default_go_fast() -> Self {
|
||||
Self::MiniMaxM2_5
|
||||
}
|
||||
|
||||
pub fn default_free() -> Self {
|
||||
Self::BigPickle
|
||||
}
|
||||
|
||||
pub fn default_free_fast() -> Self {
|
||||
Self::MiniMaxM2_5Free
|
||||
}
|
||||
|
||||
pub fn available_subscriptions(&self) -> &'static [OpenCodeSubscription] {
|
||||
match self {
|
||||
// Models available in both Zen and Go
|
||||
Self::Glm5
|
||||
| Self::Glm5_1
|
||||
| Self::KimiK2_6
|
||||
| Self::KimiK2_5
|
||||
| Self::MiniMaxM2_5
|
||||
| Self::Qwen3_5Plus
|
||||
| Self::Qwen3_6Plus => &[OpenCodeSubscription::Zen, OpenCodeSubscription::Go],
|
||||
|
||||
// Go-only models
|
||||
Self::MiniMaxM2_7 | Self::MimoV2Pro | Self::MimoV2Omni => &[OpenCodeSubscription::Go],
|
||||
|
||||
// Free models
|
||||
Self::MiniMaxM2_5Free | Self::Nemotron3SuperFree | Self::BigPickle => {
|
||||
&[OpenCodeSubscription::Free]
|
||||
}
|
||||
|
||||
// Custom models get their subscription from settings, not from here
|
||||
Self::Custom { .. } => &[],
|
||||
|
||||
// All other built-in models are Zen-only
|
||||
_ => &[OpenCodeSubscription::Zen],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeOpus4_7 => "claude-opus-4-7",
|
||||
@@ -151,11 +235,14 @@ impl Model {
|
||||
Self::MiniMaxM2_5 => "minimax-m2.5",
|
||||
Self::MiniMaxM2_5Free => "minimax-m2.5-free",
|
||||
Self::Glm5 => "glm-5",
|
||||
Self::Glm5_1 => "glm-5.1",
|
||||
Self::KimiK2_5 => "kimi-k2.5",
|
||||
Self::MimoV2ProFree => "mimo-v2-pro-free",
|
||||
Self::MimoV2OmniFree => "mimo-v2-omni-free",
|
||||
Self::MimoV2FlashFree => "mimo-v2-flash-free",
|
||||
Self::TrinityLargePreviewFree => "trinity-large-preview-free",
|
||||
Self::KimiK2_6 => "kimi-k2.6",
|
||||
Self::MiniMaxM2_7 => "minimax-m2.7",
|
||||
Self::MimoV2Pro => "mimo-v2-pro",
|
||||
Self::MimoV2Omni => "mimo-v2-omni",
|
||||
Self::Qwen3_5Plus => "qwen3.5-plus",
|
||||
Self::Qwen3_6Plus => "qwen3.6-plus",
|
||||
Self::BigPickle => "big-pickle",
|
||||
Self::Nemotron3SuperFree => "nemotron-3-super-free",
|
||||
|
||||
@@ -197,11 +284,14 @@ impl Model {
|
||||
Self::MiniMaxM2_5 => "MiniMax M2.5",
|
||||
Self::MiniMaxM2_5Free => "MiniMax M2.5 Free",
|
||||
Self::Glm5 => "GLM 5",
|
||||
Self::Glm5_1 => "GLM 5.1",
|
||||
Self::KimiK2_5 => "Kimi K2.5",
|
||||
Self::MimoV2ProFree => "MiMo V2 Pro Free",
|
||||
Self::MimoV2OmniFree => "MiMo V2 Omni Free",
|
||||
Self::MimoV2FlashFree => "MiMo V2 Flash Free",
|
||||
Self::TrinityLargePreviewFree => "Trinity Large Preview Free",
|
||||
Self::KimiK2_6 => "Kimi K2.6",
|
||||
Self::MiniMaxM2_7 => "MiniMax M2.7",
|
||||
Self::MimoV2Pro => "MiMo V2 Pro",
|
||||
Self::MimoV2Omni => "MiMo V2 Omni",
|
||||
Self::Qwen3_5Plus => "Qwen3.5 Plus",
|
||||
Self::Qwen3_6Plus => "Qwen3.6 Plus",
|
||||
Self::BigPickle => "Big Pickle",
|
||||
Self::Nemotron3SuperFree => "Nemotron 3 Super Free",
|
||||
|
||||
@@ -211,8 +301,18 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn protocol(&self) -> ApiProtocol {
|
||||
pub fn protocol(&self, subscription: OpenCodeSubscription) -> ApiProtocol {
|
||||
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 => {
|
||||
if subscription == OpenCodeSubscription::Zen {
|
||||
ApiProtocol::OpenAiChat
|
||||
} else {
|
||||
ApiProtocol::Anthropic
|
||||
}
|
||||
}
|
||||
|
||||
Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeOpus4_5
|
||||
@@ -241,14 +341,15 @@ impl Model {
|
||||
|
||||
Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google,
|
||||
|
||||
Self::MiniMaxM2_5
|
||||
| Self::MiniMaxM2_5Free
|
||||
Self::MiniMaxM2_5Free
|
||||
| Self::Glm5
|
||||
| Self::Glm5_1
|
||||
| Self::KimiK2_5
|
||||
| Self::MimoV2ProFree
|
||||
| Self::MimoV2OmniFree
|
||||
| Self::MimoV2FlashFree
|
||||
| Self::TrinityLargePreviewFree
|
||||
| Self::KimiK2_6
|
||||
| Self::MimoV2Pro
|
||||
| Self::MimoV2Omni
|
||||
| Self::Qwen3_5Plus
|
||||
| Self::Qwen3_6Plus
|
||||
| Self::BigPickle
|
||||
| Self::Nemotron3SuperFree => ApiProtocol::OpenAiChat,
|
||||
|
||||
@@ -259,10 +360,12 @@ impl Model {
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
// Anthropic models
|
||||
Self::ClaudeOpus4_7 | Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6 => 1_000_000,
|
||||
Self::ClaudeOpus4_5 | Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4 => 200_000,
|
||||
Self::ClaudeOpus4_7 => 1_000_000,
|
||||
Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6 => 1_000_000,
|
||||
Self::ClaudeSonnet4_5 => 1_000_000,
|
||||
Self::ClaudeOpus4_5 | Self::ClaudeHaiku4_5 => 200_000,
|
||||
Self::ClaudeOpus4_1 => 200_000,
|
||||
Self::ClaudeHaiku4_5 => 200_000,
|
||||
Self::ClaudeSonnet4 => 1_000_000,
|
||||
Self::Claude3_5Haiku => 200_000,
|
||||
|
||||
// OpenAI models
|
||||
@@ -281,14 +384,15 @@ impl Model {
|
||||
Self::Gemini3Flash => 1_048_576,
|
||||
|
||||
// OpenAI-compatible models
|
||||
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => 196_608,
|
||||
Self::Glm5 => 200_000,
|
||||
Self::KimiK2_5 => 262_144,
|
||||
Self::MimoV2ProFree => 1_048_576,
|
||||
Self::MimoV2OmniFree | Self::MimoV2FlashFree => 262_144,
|
||||
Self::TrinityLargePreviewFree => 131_072,
|
||||
Self::MiniMaxM2_7 => 204_800,
|
||||
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => 204_800,
|
||||
Self::Glm5 | Self::Glm5_1 => 204_800,
|
||||
Self::KimiK2_6 | Self::KimiK2_5 => 262_144,
|
||||
Self::MimoV2Pro => 1_048_576,
|
||||
Self::MimoV2Omni => 262_144,
|
||||
Self::Qwen3_5Plus | Self::Qwen3_6Plus => 262_144,
|
||||
Self::BigPickle => 200_000,
|
||||
Self::Nemotron3SuperFree => 262_144,
|
||||
Self::Nemotron3SuperFree => 204_800,
|
||||
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
@@ -298,12 +402,12 @@ impl Model {
|
||||
match self {
|
||||
// Anthropic models
|
||||
Self::ClaudeOpus4_7 | Self::ClaudeOpus4_6 => Some(128_000),
|
||||
Self::ClaudeSonnet4_6 => Some(64_000),
|
||||
Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeSonnet4_6
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeHaiku4_5 => Some(64_000),
|
||||
| Self::ClaudeHaiku4_5
|
||||
| Self::ClaudeSonnet4 => Some(64_000),
|
||||
Self::ClaudeOpus4_1 => Some(32_000),
|
||||
Self::Claude3_5Haiku => Some(8_192),
|
||||
|
||||
// OpenAI models
|
||||
@@ -327,12 +431,14 @@ impl Model {
|
||||
Self::Gemini3_1Pro | Self::Gemini3Flash => Some(65_536),
|
||||
|
||||
// OpenAI-compatible models
|
||||
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(65_536),
|
||||
Self::Glm5 | Self::BigPickle => Some(128_000),
|
||||
Self::KimiK2_5 => Some(65_536),
|
||||
Self::MimoV2ProFree => Some(131_072),
|
||||
Self::MimoV2OmniFree | Self::MimoV2FlashFree => Some(65_536),
|
||||
Self::TrinityLargePreviewFree | Self::Nemotron3SuperFree => Some(16_384),
|
||||
Self::MiniMaxM2_7 => Some(131_072),
|
||||
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(131_072),
|
||||
Self::Glm5 | Self::Glm5_1 => Some(131_072),
|
||||
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::Custom {
|
||||
max_output_tokens, ..
|
||||
@@ -377,15 +483,20 @@ impl Model {
|
||||
// Google models support images
|
||||
Self::Gemini3_1Pro | Self::Gemini3Flash => true,
|
||||
|
||||
// OpenAI-compatible models — conservative default
|
||||
// OpenAI-compatible models with image support
|
||||
Self::KimiK2_6
|
||||
| Self::KimiK2_5
|
||||
| Self::MimoV2Omni
|
||||
| Self::Qwen3_5Plus
|
||||
| Self::Qwen3_6Plus => true,
|
||||
|
||||
// OpenAI-compatible models without image support
|
||||
Self::MiniMaxM2_5
|
||||
| Self::MiniMaxM2_5Free
|
||||
| Self::Glm5
|
||||
| Self::KimiK2_5
|
||||
| Self::MimoV2ProFree
|
||||
| Self::MimoV2OmniFree
|
||||
| Self::MimoV2FlashFree
|
||||
| Self::TrinityLargePreviewFree
|
||||
| Self::Glm5_1
|
||||
| Self::MiniMaxM2_7
|
||||
| Self::MimoV2Pro
|
||||
| Self::BigPickle
|
||||
| Self::Nemotron3SuperFree => false,
|
||||
|
||||
@@ -400,12 +511,12 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream generate content for Google models via OpenCode Zen.
|
||||
/// Stream generate content for Google models via OpenCode.
|
||||
///
|
||||
/// Unlike `google_ai::stream_generate_content()`, this uses:
|
||||
/// - `/v1/models/{model}` path (not `/v1beta/models/{model}`)
|
||||
/// - `Authorization: Bearer` header (not `key=` query param)
|
||||
pub async fn stream_generate_content_zen(
|
||||
pub async fn stream_generate_content(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
@@ -451,7 +562,7 @@ pub async fn stream_generate_content_zen(
|
||||
let mut text = String::new();
|
||||
response.body_mut().read_to_string(&mut text).await?;
|
||||
Err(anyhow!(
|
||||
"error during streamGenerateContent via OpenCode Zen, status code: {:?}, body: {}",
|
||||
"error during streamGenerateContent via OpenCode, status code: {:?}, body: {}",
|
||||
response.status(),
|
||||
text
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user