From a422ed197f4eb4e5137e8813484d9affa6b25cd8 Mon Sep 17 00:00:00 2001 From: ravshansbox Date: Fri, 3 Apr 2026 16:39:53 +0300 Subject: [PATCH 1/6] feat: add OpenCode Go provider support - Add OPENCODE_GO provider ID constant with display name 'OpenCode Go' - Create OpenCodeGoResponseRepository for handling OpenCode Go API requests - Add OpenCode Go to provider routing in ForgeChatRepository - Configure OpenCode Go provider in provider.json with 6 models: - GLM 5, Kimi K2.5, MiMo V2 Pro, MiMo V2 Omni, MiniMax M2.7, MiniMax M2.5 - Add OpenCode Go to pipeline transformer for strict schema handling - Fix display names for OpenCode Zen and OpenCode Go (proper casing) Co-Authored-By: ForgeCode --- .../src/dto/openai/transformers/pipeline.rs | 18 +++- crates/forge_domain/src/provider.rs | 15 +++ crates/forge_repo/src/provider/chat.rs | 15 ++- crates/forge_repo/src/provider/mod.rs | 1 + crates/forge_repo/src/provider/opencode_go.rs | 96 +++++++++++++++++++ crates/forge_repo/src/provider/provider.json | 70 ++++++++++++++ 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 crates/forge_repo/src/provider/opencode_go.rs diff --git a/crates/forge_app/src/dto/openai/transformers/pipeline.rs b/crates/forge_app/src/dto/openai/transformers/pipeline.rs index 7b1c309c0e..016b5740cc 100644 --- a/crates/forge_app/src/dto/openai/transformers/pipeline.rs +++ b/crates/forge_app/src/dto/openai/transformers/pipeline.rs @@ -72,7 +72,9 @@ impl Transformer for ProviderPipeline<'_> { let strict_schema = EnforceStrictToolSchema .pipe(EnforceStrictResponseFormatSchema) .when(move |_| { - provider.id == ProviderId::FIREWORKS_AI || provider.id == ProviderId::OPENCODE_ZEN + provider.id == ProviderId::FIREWORKS_AI + || provider.id == ProviderId::OPENCODE_ZEN + || provider.id == ProviderId::OPENCODE_GO }); let mut combined = zai_thinking @@ -275,6 +277,20 @@ mod tests { } } + fn opencode_go(key: &str) -> Provider { + Provider { + id: ProviderId::OPENCODE_GO, + provider_type: Default::default(), + response: Some(ProviderResponse::OpenAI), + url: Url::parse("https://opencode.ai/zen/go/v1/chat/completions").unwrap(), + auth_methods: vec![forge_domain::AuthMethod::ApiKey], + url_params: vec![], + credential: make_credential(ProviderId::OPENCODE_GO, key), + custom_headers: None, + models: Some(ModelSource::Hardcoded(vec![])), + } + } + fn fireworks_ai(key: &str) -> Provider { Provider { id: ProviderId::FIREWORKS_AI, diff --git a/crates/forge_domain/src/provider.rs b/crates/forge_domain/src/provider.rs index 93291e1ea3..f32a057279 100644 --- a/crates/forge_domain/src/provider.rs +++ b/crates/forge_domain/src/provider.rs @@ -70,6 +70,7 @@ impl ProviderId { pub const MINIMAX: ProviderId = ProviderId(Cow::Borrowed("minimax")); pub const CODEX: ProviderId = ProviderId(Cow::Borrowed("codex")); pub const OPENCODE_ZEN: ProviderId = ProviderId(Cow::Borrowed("opencode_zen")); + pub const OPENCODE_GO: ProviderId = ProviderId(Cow::Borrowed("opencode_go")); pub const FIREWORKS_AI: ProviderId = ProviderId(Cow::Borrowed("fireworks-ai")); pub const NOVITA: ProviderId = ProviderId(Cow::Borrowed("novita")); @@ -102,6 +103,7 @@ impl ProviderId { ProviderId::MINIMAX, ProviderId::CODEX, ProviderId::OPENCODE_ZEN, + ProviderId::OPENCODE_GO, ProviderId::FIREWORKS_AI, ProviderId::NOVITA, ] @@ -127,6 +129,8 @@ impl ProviderId { "io_intelligence" => "IOIntelligence".to_string(), "minimax" => "MiniMax".to_string(), "codex" => "Codex".to_string(), + "opencode_zen" => "OpenCode Zen".to_string(), + "opencode_go" => "OpenCode Go".to_string(), "fireworks-ai" => "FireworksAI".to_string(), "novita" => "Novita".to_string(), _ => { @@ -171,6 +175,7 @@ impl std::str::FromStr for ProviderId { "io_intelligence" => ProviderId::IO_INTELLIGENCE, "minimax" => ProviderId::MINIMAX, "codex" => ProviderId::CODEX, + "opencode_go" => ProviderId::OPENCODE_GO, "fireworks-ai" => ProviderId::FIREWORKS_AI, "novita" => ProviderId::NOVITA, // For custom providers, use Cow::Owned to avoid memory leaks @@ -544,6 +549,8 @@ mod tests { assert_eq!(ProviderId::IO_INTELLIGENCE.to_string(), "IOIntelligence"); assert_eq!(ProviderId::CODEX.to_string(), "Codex"); assert_eq!(ProviderId::FIREWORKS_AI.to_string(), "FireworksAI"); + assert_eq!(ProviderId::OPENCODE_ZEN.to_string(), "OpenCode Zen"); + assert_eq!(ProviderId::OPENCODE_GO.to_string(), "OpenCode Go"); } #[test] @@ -560,12 +567,20 @@ mod tests { assert_eq!(actual, expected); } + #[test] + fn test_opencode_go_from_str() { + let actual = ProviderId::from_str("opencode_go").unwrap(); + let expected = ProviderId::OPENCODE_GO; + assert_eq!(actual, expected); + } + #[test] fn test_codex_in_built_in_providers() { let built_in = ProviderId::built_in_providers(); assert!(built_in.contains(&ProviderId::CODEX)); assert!(built_in.contains(&ProviderId::OPENAI_RESPONSES_COMPATIBLE)); assert!(built_in.contains(&ProviderId::FIREWORKS_AI)); + assert!(built_in.contains(&ProviderId::OPENCODE_GO)); } #[test] diff --git a/crates/forge_repo/src/provider/chat.rs b/crates/forge_repo/src/provider/chat.rs index 28e208203f..c117c642a5 100644 --- a/crates/forge_repo/src/provider/chat.rs +++ b/crates/forge_repo/src/provider/chat.rs @@ -14,6 +14,7 @@ use crate::provider::bedrock::BedrockResponseRepository; use crate::provider::google::GoogleResponseRepository; use crate::provider::openai::OpenAIResponseRepository; use crate::provider::openai_responses::OpenAIResponsesResponseRepository; +use crate::provider::opencode_go::OpenCodeGoResponseRepository; use crate::provider::opencode_zen::OpenCodeZenResponseRepository; /// Repository responsible for routing chat requests to the appropriate provider @@ -46,6 +47,8 @@ impl ForgeChatRepository { GoogleResponseRepository::new(infra.clone()).retry_config(retry_config.clone()); let opencode_zen_repo = OpenCodeZenResponseRepository::new(infra.clone()).retry_config(retry_config.clone()); + let opencode_go_repo = + OpenCodeGoResponseRepository::new(infra.clone()).retry_config(retry_config.clone()); let model_cache = Arc::new(CacacheStorage::new( env.cache_dir().join("model_cache"), @@ -60,6 +63,7 @@ impl ForgeChatRepository { bedrock_repo, google_repo, opencode_zen_repo, + opencode_go_repo, }), model_cache, bg_refresh: BgRefresh::default(), @@ -130,6 +134,7 @@ struct ProviderRouter { bedrock_repo: BedrockResponseRepository, google_repo: GoogleResponseRepository, opencode_zen_repo: OpenCodeZenResponseRepository, + opencode_go_repo: OpenCodeGoResponseRepository, } impl ProviderRouter { @@ -151,6 +156,8 @@ impl ProviderRouter { } else if provider.id == ProviderId::CODEX { // All Codex provider models use the Responses API self.codex_repo.chat(model_id, context, provider).await + } else if provider.id == ProviderId::OPENCODE_GO { + self.opencode_go_repo.chat(model_id, context, provider).await } else { self.openai_repo.chat(model_id, context, provider).await } @@ -181,7 +188,13 @@ impl ProviderRouter { async fn models(&self, provider: Provider) -> anyhow::Result> { match provider.response { - Some(ProviderResponse::OpenAI) => self.openai_repo.models(provider).await, + Some(ProviderResponse::OpenAI) => { + if provider.id == ProviderId::OPENCODE_GO { + self.opencode_go_repo.models(provider).await + } else { + self.openai_repo.models(provider).await + } + } Some(ProviderResponse::OpenAIResponses) => self.codex_repo.models(provider).await, Some(ProviderResponse::Anthropic) => self.anthropic_repo.models(provider).await, Some(ProviderResponse::Bedrock) => self.bedrock_repo.models(provider).await, diff --git a/crates/forge_repo/src/provider/mod.rs b/crates/forge_repo/src/provider/mod.rs index 411bfd3079..9069b662a6 100644 --- a/crates/forge_repo/src/provider/mod.rs +++ b/crates/forge_repo/src/provider/mod.rs @@ -8,6 +8,7 @@ mod google; mod mock_server; mod openai; mod openai_responses; +mod opencode_go; mod opencode_zen; mod provider_repo; mod retry; diff --git a/crates/forge_repo/src/provider/opencode_go.rs b/crates/forge_repo/src/provider/opencode_go.rs new file mode 100644 index 0000000000..74411a7ce1 --- /dev/null +++ b/crates/forge_repo/src/provider/opencode_go.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; + +use anyhow::Result; +use derive_setters::Setters; +use forge_app::HttpInfra; +use forge_app::domain::{ + ChatCompletionMessage, Context as ChatContext, Model, ModelId, Provider, ProviderResponse, + ResultStream, +}; +use forge_config::RetryConfig; +use forge_domain::ChatRepository; +use url::Url; + +use crate::provider::openai::OpenAIResponseRepository; + +#[derive(Setters)] +#[setters(strip_option, into)] +pub struct OpenCodeGoResponseRepository { + openai_repo: OpenAIResponseRepository, + retry_config: Arc, +} + +impl OpenCodeGoResponseRepository { + pub fn new(infra: Arc) -> Self { + Self { + openai_repo: OpenAIResponseRepository::new(infra), + retry_config: Arc::new(RetryConfig::default()), + } + } + + fn build_provider(&self, provider: &Provider) -> Provider { + let mut new_provider = provider.clone(); + + new_provider.url = Url::parse("https://opencode.ai/zen/go/v1/chat/completions").unwrap(); + new_provider.response = Some(ProviderResponse::OpenAI); + + new_provider + } +} + +impl OpenCodeGoResponseRepository { + pub async fn chat( + &self, + model_id: &ModelId, + context: ChatContext, + provider: Provider, + ) -> ResultStream { + let adapted_provider = self.build_provider(&provider); + + self.openai_repo + .chat(model_id, context, adapted_provider) + .await + } + + pub async fn models(&self, provider: Provider) -> Result> { + if let Some(models) = provider.models() { + match models { + forge_domain::ModelSource::Hardcoded(models) => Ok(models.clone()), + forge_domain::ModelSource::Url(_) => { + Ok(vec![]) + } + } + } else { + Ok(vec![]) + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use std::str::FromStr; + use url::Url; + + use forge_app::domain::ProviderResponse; + use forge_domain::ProviderId; + + #[test] + fn test_opencode_go_provider_url() { + let url = Url::parse("https://opencode.ai/zen/go/v1/chat/completions").unwrap(); + assert_eq!(url.as_str(), "https://opencode.ai/zen/go/v1/chat/completions"); + } + + #[test] + fn test_opencode_go_provider_id_from_str() { + let actual = ProviderId::from_str("opencode_go").unwrap(); + let expected = ProviderId::OPENCODE_GO; + assert_eq!(actual, expected); + } + + #[test] + fn test_opencode_go_response_type() { + let response = ProviderResponse::OpenAI; + assert_eq!(format!("{:?}", response), "OpenAI"); + } +} diff --git a/crates/forge_repo/src/provider/provider.json b/crates/forge_repo/src/provider/provider.json index 2805c1f8a5..51cc9aebec 100644 --- a/crates/forge_repo/src/provider/provider.json +++ b/crates/forge_repo/src/provider/provider.json @@ -2426,6 +2426,76 @@ ], "auth_methods": ["api_key"] }, + { + "id": "opencode_go", + "api_key_vars": "OPENCODE_API_KEY", + "url_param_vars": [], + "response_type": "OpenAI", + "url": "https://opencode.ai/zen/go/v1/chat/completions", + "models": [ + { + "id": "glm-5", + "name": "GLM 5", + "description": "Zhipu AI's flagship model with 204K context, reasoning, and tool calling capabilities", + "context_length": 204800, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text"] + }, + { + "id": "kimi-k2.5", + "name": "Kimi K2.5", + "description": "Moonshot AI's flagship model with 262K context, vision, and reasoning capabilities", + "context_length": 262144, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text", "image"] + }, + { + "id": "mimo-v2-pro", + "name": "MiMo V2 Pro", + "description": "Xiaomi's flagship foundation model with 1M context, reasoning, and tool calling capabilities", + "context_length": 1000000, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text"] + }, + { + "id": "mimo-v2-omni", + "name": "MiMo V2 Omni", + "description": "Xiaomi's omni-modal model that natively processes image, video, and audio inputs", + "context_length": 262100, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text", "image"] + }, + { + "id": "minimax-m2.7", + "name": "MiniMax M2.7", + "description": "MiniMax's latest model with enhanced reasoning and 200K context", + "context_length": 204800, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text"] + }, + { + "id": "minimax-m2.5", + "name": "MiniMax M2.5", + "description": "MiniMax's model with 204K context and reasoning capabilities", + "context_length": 204800, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text"] + } + ], + "auth_methods": ["api_key"] + }, { "id": "alibaba_coding", "provider_type": "llm", From 26ef1cc228085df1069da772631885a83be4f7f5 Mon Sep 17 00:00:00 2001 From: ravshansbox Date: Fri, 3 Apr 2026 16:56:15 +0300 Subject: [PATCH 2/6] fix: suppress dead code warning for opencode_go test fixture Co-Authored-By: ForgeCode --- crates/forge_app/src/dto/openai/transformers/pipeline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/forge_app/src/dto/openai/transformers/pipeline.rs b/crates/forge_app/src/dto/openai/transformers/pipeline.rs index 016b5740cc..98f105ee3a 100644 --- a/crates/forge_app/src/dto/openai/transformers/pipeline.rs +++ b/crates/forge_app/src/dto/openai/transformers/pipeline.rs @@ -277,6 +277,7 @@ mod tests { } } + #[allow(dead_code)] fn opencode_go(key: &str) -> Provider { Provider { id: ProviderId::OPENCODE_GO, From 56f9d6718be1fc095f5dc83216eefe5a42dcc9f8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:58:05 +0000 Subject: [PATCH 3/6] [autofix.ci] apply automated fixes --- crates/forge_repo/src/provider/chat.rs | 4 +++- crates/forge_repo/src/provider/opencode_go.rs | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/forge_repo/src/provider/chat.rs b/crates/forge_repo/src/provider/chat.rs index c117c642a5..0304273b39 100644 --- a/crates/forge_repo/src/provider/chat.rs +++ b/crates/forge_repo/src/provider/chat.rs @@ -157,7 +157,9 @@ impl ProviderRouter { // All Codex provider models use the Responses API self.codex_repo.chat(model_id, context, provider).await } else if provider.id == ProviderId::OPENCODE_GO { - self.opencode_go_repo.chat(model_id, context, provider).await + self.opencode_go_repo + .chat(model_id, context, provider) + .await } else { self.openai_repo.chat(model_id, context, provider).await } diff --git a/crates/forge_repo/src/provider/opencode_go.rs b/crates/forge_repo/src/provider/opencode_go.rs index 74411a7ce1..80308298c8 100644 --- a/crates/forge_repo/src/provider/opencode_go.rs +++ b/crates/forge_repo/src/provider/opencode_go.rs @@ -56,9 +56,7 @@ impl OpenCodeGoResponseRepository { if let Some(models) = provider.models() { match models { forge_domain::ModelSource::Hardcoded(models) => Ok(models.clone()), - forge_domain::ModelSource::Url(_) => { - Ok(vec![]) - } + forge_domain::ModelSource::Url(_) => Ok(vec![]), } } else { Ok(vec![]) @@ -68,17 +66,20 @@ impl OpenCodeGoResponseRepository { #[cfg(test)] mod tests { - use pretty_assertions::assert_eq; use std::str::FromStr; - use url::Url; use forge_app::domain::ProviderResponse; use forge_domain::ProviderId; + use pretty_assertions::assert_eq; + use url::Url; #[test] fn test_opencode_go_provider_url() { let url = Url::parse("https://opencode.ai/zen/go/v1/chat/completions").unwrap(); - assert_eq!(url.as_str(), "https://opencode.ai/zen/go/v1/chat/completions"); + assert_eq!( + url.as_str(), + "https://opencode.ai/zen/go/v1/chat/completions" + ); } #[test] From d8e3fd37dee0b7536a4a74af2ebd876021020069 Mon Sep 17 00:00:00 2001 From: ravshansbox Date: Fri, 3 Apr 2026 18:46:16 +0300 Subject: [PATCH 4/6] chore: remove unused opencode_go test fixture Co-Authored-By: ForgeCode --- .../src/dto/openai/transformers/pipeline.rs | 15 --------------- forgecode | 1 + 2 files changed, 1 insertion(+), 15 deletions(-) create mode 160000 forgecode diff --git a/crates/forge_app/src/dto/openai/transformers/pipeline.rs b/crates/forge_app/src/dto/openai/transformers/pipeline.rs index 98f105ee3a..ad1693cbaa 100644 --- a/crates/forge_app/src/dto/openai/transformers/pipeline.rs +++ b/crates/forge_app/src/dto/openai/transformers/pipeline.rs @@ -277,21 +277,6 @@ mod tests { } } - #[allow(dead_code)] - fn opencode_go(key: &str) -> Provider { - Provider { - id: ProviderId::OPENCODE_GO, - provider_type: Default::default(), - response: Some(ProviderResponse::OpenAI), - url: Url::parse("https://opencode.ai/zen/go/v1/chat/completions").unwrap(), - auth_methods: vec![forge_domain::AuthMethod::ApiKey], - url_params: vec![], - credential: make_credential(ProviderId::OPENCODE_GO, key), - custom_headers: None, - models: Some(ModelSource::Hardcoded(vec![])), - } - } - fn fireworks_ai(key: &str) -> Provider { Provider { id: ProviderId::FIREWORKS_AI, diff --git a/forgecode b/forgecode new file mode 160000 index 0000000000..f3f90d958b --- /dev/null +++ b/forgecode @@ -0,0 +1 @@ +Subproject commit f3f90d958bf4a904e7fb533334c40dac5baf398b From f96c3a7e1cbea8929acdd7f427ac9114f4feee26 Mon Sep 17 00:00:00 2001 From: ravshansbox Date: Fri, 3 Apr 2026 18:47:26 +0300 Subject: [PATCH 5/6] chore: remove accidental embedded forgecode directory Co-Authored-By: ForgeCode --- forgecode | 1 - 1 file changed, 1 deletion(-) delete mode 160000 forgecode diff --git a/forgecode b/forgecode deleted file mode 160000 index f3f90d958b..0000000000 --- a/forgecode +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f3f90d958bf4a904e7fb533334c40dac5baf398b From 11427dcf10d5f4d113dba1c0acbbb773f6bf231e Mon Sep 17 00:00:00 2001 From: ravshansbox Date: Fri, 3 Apr 2026 18:56:59 +0300 Subject: [PATCH 6/6] Update crates/forge_repo/src/provider/provider.json Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- crates/forge_repo/src/provider/provider.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_repo/src/provider/provider.json b/crates/forge_repo/src/provider/provider.json index 51cc9aebec..2306709889 100644 --- a/crates/forge_repo/src/provider/provider.json +++ b/crates/forge_repo/src/provider/provider.json @@ -2476,7 +2476,7 @@ { "id": "minimax-m2.7", "name": "MiniMax M2.7", - "description": "MiniMax's latest model with enhanced reasoning and 200K context", + "description": "MiniMax's latest model with enhanced reasoning and 204K context", "context_length": 204800, "tools_supported": true, "supports_parallel_tool_calls": true,