From b5601a24c1d46b7bdb2e4330ae491e760253e5f1 Mon Sep 17 00:00:00 2001 From: ximi Date: Wed, 4 Mar 2026 17:56:13 +0800 Subject: [PATCH 1/3] feat: add MiniMax provider with Anthropic-compatible API Add support for MiniMax AI models (MiniMax-M2.5 and MiniMax-M2.5-highspeed) via the Anthropic-compatible API endpoint. Includes provider implementation, registry registration, and UI logo assets. Made-with: Cursor Signed-off-by: ximi --- .../canonical/data/canonical_models.json | 2 +- crates/goose/src/providers/init.rs | 2 + crates/goose/src/providers/minimax.rs | 156 ++++++++++++++++++ crates/goose/src/providers/mod.rs | 1 + .../modal/subcomponents/ProviderLogo.tsx | 2 + .../modal/subcomponents/icons/minimax.png | Bin 0 -> 923 bytes .../modal/subcomponents/icons/minimax@2x.png | Bin 0 -> 1874 bytes .../modal/subcomponents/icons/minimax@3x.png | Bin 0 -> 3563 bytes 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 crates/goose/src/providers/minimax.rs create mode 100644 ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax.png create mode 100644 ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@2x.png create mode 100644 ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@3x.png diff --git a/crates/goose/src/providers/canonical/data/canonical_models.json b/crates/goose/src/providers/canonical/data/canonical_models.json index 1d6c689a9a84..e6cde4aa6552 100644 --- a/crates/goose/src/providers/canonical/data/canonical_models.json +++ b/crates/goose/src/providers/canonical/data/canonical_models.json @@ -86880,4 +86880,4 @@ "output": 131072 } } -] \ No newline at end of file +] diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 8300f5a6df5a..d9283cc1a2ff 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -17,6 +17,7 @@ use super::{ lead_worker::LeadWorkerProvider, litellm::LiteLLMProvider, local_inference::LocalInferenceProvider, + minimax::MiniMaxProvider, ollama::OllamaProvider, openai::OpenAiProvider, openrouter::OpenRouterProvider, @@ -59,6 +60,7 @@ async fn init_registry() -> RwLock { registry.register::(false); registry.register::(true); registry.register::(false); + registry.register::(true); registry.register::(true); registry.register::(true); registry.register::(true); diff --git a/crates/goose/src/providers/minimax.rs b/crates/goose/src/providers/minimax.rs new file mode 100644 index 000000000000..c5d1f91da5f5 --- /dev/null +++ b/crates/goose/src/providers/minimax.rs @@ -0,0 +1,156 @@ +use anyhow::Result; +use async_stream::try_stream; +use async_trait::async_trait; +use futures::TryStreamExt; +use std::io; +use tokio::pin; +use tokio_util::io::StreamReader; + +use super::api_client::{ApiClient, AuthMethod}; +use super::base::{ConfigKey, MessageStream, ModelInfo, Provider, ProviderDef, ProviderMetadata}; +use super::errors::ProviderError; +use super::formats::anthropic::create_request; +use super::formats::anthropic::response_to_streaming_message; +use super::openai_compatible::handle_status_openai_compat; +use super::retry::ProviderRetry; +use crate::conversation::message::Message; +use crate::model::ModelConfig; +use crate::providers::utils::RequestLog; +use futures::future::BoxFuture; +use rmcp::model::Tool; +use serde_json::Value; + +const MINIMAX_PROVIDER_NAME: &str = "minimax"; +pub const MINIMAX_API_HOST: &str = "https://api.minimax.io/anthropic"; +pub const MINIMAX_DEFAULT_MODEL: &str = "MiniMax-M2.5"; +const MINIMAX_DEFAULT_FAST_MODEL: &str = "MiniMax-M2.5-highspeed"; +const MINIMAX_KNOWN_MODELS: &[(&str, usize)] = &[ + ("MiniMax-M2.5", 204_800), + ("MiniMax-M2.5-highspeed", 204_800), +]; + +const MINIMAX_DOC_URL: &str = "https://platform.minimax.io/docs/guides/models-intro"; +const ANTHROPIC_API_VERSION: &str = "2023-06-01"; + +#[derive(serde::Serialize)] +pub struct MiniMaxProvider { + #[serde(skip)] + api_client: ApiClient, + model: ModelConfig, +} + +impl MiniMaxProvider { + pub async fn from_env(model: ModelConfig) -> Result { + let model = model.with_fast(MINIMAX_DEFAULT_FAST_MODEL, MINIMAX_PROVIDER_NAME)?; + + let config = crate::config::Config::global(); + let api_key: String = config.get_secret("MINIMAX_API_KEY")?; + let host: String = config + .get_param("MINIMAX_HOST") + .unwrap_or_else(|_| MINIMAX_API_HOST.to_string()); + + let auth = AuthMethod::ApiKey { + header_name: "x-api-key".to_string(), + key: api_key, + }; + + let api_client = + ApiClient::new(host, auth)?.with_header("anthropic-version", ANTHROPIC_API_VERSION)?; + + Ok(Self { api_client, model }) + } +} + +impl ProviderDef for MiniMaxProvider { + type Provider = Self; + + fn metadata() -> ProviderMetadata { + let models = MINIMAX_KNOWN_MODELS + .iter() + .map(|(name, limit)| ModelInfo::new(*name, *limit)) + .collect(); + + ProviderMetadata::with_models( + MINIMAX_PROVIDER_NAME, + "MiniMax", + "MiniMax AI models with long context support via Anthropic-compatible API", + MINIMAX_DEFAULT_MODEL, + models, + MINIMAX_DOC_URL, + vec![ + ConfigKey::new("MINIMAX_API_KEY", true, true, None, true), + ConfigKey::new("MINIMAX_HOST", false, false, Some(MINIMAX_API_HOST), false), + ], + ) + } + + fn from_env( + model: ModelConfig, + _extensions: Vec, + ) -> BoxFuture<'static, Result> { + Box::pin(Self::from_env(model)) + } +} + +#[async_trait] +impl Provider for MiniMaxProvider { + fn get_name(&self) -> &str { + MINIMAX_PROVIDER_NAME + } + + fn get_model_config(&self) -> ModelConfig { + self.model.clone() + } + + async fn fetch_supported_models( + &self, + ) -> Result, super::errors::ProviderError> { + Ok(MINIMAX_KNOWN_MODELS + .iter() + .map(|(name, _)| name.to_string()) + .collect()) + } + + async fn stream( + &self, + model_config: &ModelConfig, + session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + let mut payload = create_request(model_config, system, messages, tools)?; + payload + .as_object_mut() + .unwrap() + .insert("stream".to_string(), Value::Bool(true)); + + let mut log = RequestLog::start(model_config, &payload)?; + + let response = self + .with_retry(|| async { + let request = self.api_client.request(Some(session_id), "v1/messages"); + let resp = request.response_post(&payload).await?; + handle_status_openai_compat(resp).await + }) + .await + .inspect_err(|e| { + let _ = log.error(e); + })?; + + let stream = response.bytes_stream().map_err(io::Error::other); + + Ok(Box::pin(try_stream! { + let stream_reader = StreamReader::new(stream); + let framed = tokio_util::codec::FramedRead::new(stream_reader, tokio_util::codec::LinesCodec::new()).map_err(anyhow::Error::from); + + let message_stream = response_to_streaming_message(framed); + pin!(message_stream); + while let Some(message) = futures::StreamExt::next(&mut message_stream).await { + let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + yield (message, usage); + } + })) + } +} diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 6b2ce20528ae..cec14c036844 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -25,6 +25,7 @@ mod init; pub mod lead_worker; pub mod litellm; pub mod local_inference; +pub mod minimax; pub mod oauth; pub mod ollama; pub mod openai; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx index 8f18c721db1e..e81a0a7dcf64 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx @@ -7,6 +7,7 @@ import DatabricksLogo from './icons/databricks@3x.png'; import OpenRouterLogo from './icons/openrouter@3x.png'; import SnowflakeLogo from './icons/snowflake@3x.png'; import XaiLogo from './icons/xai@3x.png'; +import MiniMaxLogo from './icons/minimax@3x.png'; import DefaultLogo from './icons/default@3x.png'; // Map provider names to their logos @@ -20,6 +21,7 @@ const providerLogos: Record = { openrouter: OpenRouterLogo, snowflake: SnowflakeLogo, xai: XaiLogo, + minimax: MiniMaxLogo, default: DefaultLogo, }; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax.png new file mode 100644 index 0000000000000000000000000000000000000000..b25f38f7160f1096a48b91352399b7ef899fd103 GIT binary patch literal 923 zcmV;M17!S(P))nypRKi~Iv&T*Sl(CN+`!AR()wIEbt7E6$_pcP)6 z%#}$OfmvPIm1Uji!W)ZO5fK^LNRv^LiDeX-RO^pY%ncI@!WlI!ntG1&?EHT3)5STE zbK4omGv`HpuYSDm^E}_r?|EO|2mb3)xe1m^1w$uhwVBz>D6TK&fA!|Mi}y3jS0`OO zx71uXFmvCYLsJTHs(F2FP(AXAB<3c@%$4Qt%#k7=fLK>NSy$dPCH?*17@%hqX*P6a`Y0Kx=NHDfp`E`_LAqq<4}24SJH z&ljOfdX(3JE1;_m5W952mZf8ZCo^kNxJ0tvv)*sJ?&p_tQ(n$82re~S;qI13+Ij|ja{`msbL>;)I z064R&_Z~Rl0wQUoox>Un%XWUc0Ahx{CwSnuYjVeBwm>59a34$;fUEYgb-){f8sM1B zRyO>yrEj_bV)duf8Q_uWG8_X?NwS$bL3Njt>%~r?{)zzR^lX27LW3ZdaTbF4W=o|K zBmP_qfWaAnSl7XAP~kW}v-F{7hi-lF)$#Uf7Tx%x;0L7pa$)v7SUadEvFp>HE{>Z7 zK$&9LLQACrGc5#sHbb`fH}?YfkDHlJ`+PjO6L?MVz2KF27`0zIRZAX$;_Suqx5-}k z^H|+e3j91!cSCPiwlsJ_vv%)31w2|Rrep__eOm`{Zkb7Y!A=_XVm^Q2;qhv?VG9b> zR@OY3^b#ifw)RqJ^;n6cz)eGKtGDwo>Le*k)QH2VIau`K`q002ovPDHLkV1f;ryi@=H literal 0 HcmV?d00001 diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..556261d9d307e827243df1e7b63a7955cfce5584 GIT binary patch literal 1874 zcmV-Y2d(&tP)?BT15NZm;%THe_T>=%A&w1q!ye zz3+MagZ_AL=}+E!ds(=j^Xokqk|arzC;6 z7@dG9h$0>Dh~08UTOXcyxV)t|-{!f+)5X{w2-CWK6fugTRi4q3QI(YqLG+m*&I57b z?v*pF*uB7n-tq-mI?ANIti5~@|0`OYi?APLTCcvs*Hd7x-g|Fq5RGvSfEaHRkkMsB z9dH!zI@45G*gR+1!k)enld=vN=Syk>J_4*S?Vgs9`$!OtaSOo3f`{@1`AS!UuzU2? z=1Dnky*R^&odx)B`(=J#EqX9b&|+WGap2C(SN@y__8y?AGsCDx`3{x11;9Eo)o-iF zdO2R+29S05Pt_=Y?aG6w+q{|Zoj{r|>6y%ZJI@1T?ydM2=u3V14wbkCz{5@P+v4e! zr~;ywd{!tb3+qM~)u!lt)7Y2qP>E|F@JS#UfJ6&OG|2!YT0o+?nD*d2CeJhJG!r zm%ppxj06i|udQzCsvVAXem3^xvmXZ$7%#$0zP7{&Ya)2l41(uG@Rk+5qoS+bvezqN z5I=#M2|phbj)V>g{0JE7%L)M{o)bCJ@cBol_Tio|!b|2oCCGMq%oG*w7O3!6%qfLI z1i-lMzegm$pCY&0h-}y5Vkem7*wI@2jrpSdvj0-DUcXZ~tKR^enOHg*k#Bb;D5piV z1d&Cc)!lVzLBsoF=LeG=JB&)H&$OUxRb`%vZUD5anAOsL`vBv}Y|yR_NDL)+eDK_x z0OzLs^bJSaYk+I(ag{>gFR}a}H-Z)yJ8gP-cBg1TUvRg*wz|WtiY)u;Bw$I0UD)p( zU@Tctc53Uf&g)N|+}Hq|?`jyy39LRQ)TiTHAX&^Yf3B#!>oao(=mT`1;YSC^j}6ww zt({wbB9sh3q6H)x_sqD}kBiZ@h&|@2-Z#GXH}x^8*|SQlW(EPL!u?i_uog%M`I0AQ zX}lV`SusF*x22getJ#U+x0W0Q;zu$7_@1 zSade`C+To)^FuW1=}g_knrC}`i1|szfb83>GjLiwov0(c$x5agi3ue?ZPdhUNV98Rpv_|ett=Y6#1BCxLG6&%Ni?-;;M zOZ?^m0}g}Y&mX=>NmRRAp7v~{JmW7bMo;utSHc3I`Ng42qtlq%-FYprps`@(T>uyI9~mL4t9(UU zuVl2J3DQqZ++y536&^x)9_2YvU6ID;7 zO*=Y|$6kks?d-%=d^^*CH8#We50Hy#52~{-Vm1#L_ui(1z`?vuOmTRoVh#?S59Koz4m7ai-^95Xf~qbMHGx8qSYg+ zc6>QrIubS!8eLs>8sxV(EQ9QCUat`vG;Ld@J+Sodf=_ zC+!H?FPlk|arzBuSk3KZ)I6u0E_NP5=M^ M07*qoM6N<$g4*r3A^-pY literal 0 HcmV?d00001 diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/minimax@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ab3faa546c5cb88cf863258afeb694ae50628c GIT binary patch literal 3563 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 Date: Wed, 4 Mar 2026 19:26:07 +0800 Subject: [PATCH 2/3] fix: apply cargo fmt to minimax provider Made-with: Cursor Signed-off-by: ximi --- crates/goose/src/providers/minimax.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/goose/src/providers/minimax.rs b/crates/goose/src/providers/minimax.rs index c5d1f91da5f5..e5e3eb35bf34 100644 --- a/crates/goose/src/providers/minimax.rs +++ b/crates/goose/src/providers/minimax.rs @@ -102,9 +102,7 @@ impl Provider for MiniMaxProvider { self.model.clone() } - async fn fetch_supported_models( - &self, - ) -> Result, super::errors::ProviderError> { + async fn fetch_supported_models(&self) -> Result, super::errors::ProviderError> { Ok(MINIMAX_KNOWN_MODELS .iter() .map(|(name, _)| name.to_string()) From aa7151c092e42ee15fb007401ebf052cd473f0c4 Mon Sep 17 00:00:00 2001 From: ximi Date: Fri, 6 Mar 2026 11:55:08 +0800 Subject: [PATCH 3/3] refactor: convert MiniMax to declarative provider Replace Rust implementation with JSON config as suggested in review. MiniMax uses Anthropic-compatible API, so declarative provider is cleaner. Signed-off-by: ximi Made-with: Cursor --- .../src/providers/declarative/minimax.json | 19 +++ crates/goose/src/providers/init.rs | 2 - crates/goose/src/providers/minimax.rs | 154 ------------------ crates/goose/src/providers/mod.rs | 1 - 4 files changed, 19 insertions(+), 157 deletions(-) create mode 100644 crates/goose/src/providers/declarative/minimax.json delete mode 100644 crates/goose/src/providers/minimax.rs diff --git a/crates/goose/src/providers/declarative/minimax.json b/crates/goose/src/providers/declarative/minimax.json new file mode 100644 index 000000000000..199b60fd2420 --- /dev/null +++ b/crates/goose/src/providers/declarative/minimax.json @@ -0,0 +1,19 @@ +{ + "name": "minimax", + "engine": "anthropic", + "display_name": "MiniMax", + "description": "MiniMax AI models with long context support via Anthropic-compatible API", + "api_key_env": "MINIMAX_API_KEY", + "base_url": "https://api.minimax.io/anthropic", + "models": [ + { + "name": "MiniMax-M2.5", + "context_limit": 204800 + }, + { + "name": "MiniMax-M2.5-highspeed", + "context_limit": 204800 + } + ], + "supports_streaming": true +} diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index d9283cc1a2ff..8300f5a6df5a 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -17,7 +17,6 @@ use super::{ lead_worker::LeadWorkerProvider, litellm::LiteLLMProvider, local_inference::LocalInferenceProvider, - minimax::MiniMaxProvider, ollama::OllamaProvider, openai::OpenAiProvider, openrouter::OpenRouterProvider, @@ -60,7 +59,6 @@ async fn init_registry() -> RwLock { registry.register::(false); registry.register::(true); registry.register::(false); - registry.register::(true); registry.register::(true); registry.register::(true); registry.register::(true); diff --git a/crates/goose/src/providers/minimax.rs b/crates/goose/src/providers/minimax.rs deleted file mode 100644 index e5e3eb35bf34..000000000000 --- a/crates/goose/src/providers/minimax.rs +++ /dev/null @@ -1,154 +0,0 @@ -use anyhow::Result; -use async_stream::try_stream; -use async_trait::async_trait; -use futures::TryStreamExt; -use std::io; -use tokio::pin; -use tokio_util::io::StreamReader; - -use super::api_client::{ApiClient, AuthMethod}; -use super::base::{ConfigKey, MessageStream, ModelInfo, Provider, ProviderDef, ProviderMetadata}; -use super::errors::ProviderError; -use super::formats::anthropic::create_request; -use super::formats::anthropic::response_to_streaming_message; -use super::openai_compatible::handle_status_openai_compat; -use super::retry::ProviderRetry; -use crate::conversation::message::Message; -use crate::model::ModelConfig; -use crate::providers::utils::RequestLog; -use futures::future::BoxFuture; -use rmcp::model::Tool; -use serde_json::Value; - -const MINIMAX_PROVIDER_NAME: &str = "minimax"; -pub const MINIMAX_API_HOST: &str = "https://api.minimax.io/anthropic"; -pub const MINIMAX_DEFAULT_MODEL: &str = "MiniMax-M2.5"; -const MINIMAX_DEFAULT_FAST_MODEL: &str = "MiniMax-M2.5-highspeed"; -const MINIMAX_KNOWN_MODELS: &[(&str, usize)] = &[ - ("MiniMax-M2.5", 204_800), - ("MiniMax-M2.5-highspeed", 204_800), -]; - -const MINIMAX_DOC_URL: &str = "https://platform.minimax.io/docs/guides/models-intro"; -const ANTHROPIC_API_VERSION: &str = "2023-06-01"; - -#[derive(serde::Serialize)] -pub struct MiniMaxProvider { - #[serde(skip)] - api_client: ApiClient, - model: ModelConfig, -} - -impl MiniMaxProvider { - pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(MINIMAX_DEFAULT_FAST_MODEL, MINIMAX_PROVIDER_NAME)?; - - let config = crate::config::Config::global(); - let api_key: String = config.get_secret("MINIMAX_API_KEY")?; - let host: String = config - .get_param("MINIMAX_HOST") - .unwrap_or_else(|_| MINIMAX_API_HOST.to_string()); - - let auth = AuthMethod::ApiKey { - header_name: "x-api-key".to_string(), - key: api_key, - }; - - let api_client = - ApiClient::new(host, auth)?.with_header("anthropic-version", ANTHROPIC_API_VERSION)?; - - Ok(Self { api_client, model }) - } -} - -impl ProviderDef for MiniMaxProvider { - type Provider = Self; - - fn metadata() -> ProviderMetadata { - let models = MINIMAX_KNOWN_MODELS - .iter() - .map(|(name, limit)| ModelInfo::new(*name, *limit)) - .collect(); - - ProviderMetadata::with_models( - MINIMAX_PROVIDER_NAME, - "MiniMax", - "MiniMax AI models with long context support via Anthropic-compatible API", - MINIMAX_DEFAULT_MODEL, - models, - MINIMAX_DOC_URL, - vec![ - ConfigKey::new("MINIMAX_API_KEY", true, true, None, true), - ConfigKey::new("MINIMAX_HOST", false, false, Some(MINIMAX_API_HOST), false), - ], - ) - } - - fn from_env( - model: ModelConfig, - _extensions: Vec, - ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) - } -} - -#[async_trait] -impl Provider for MiniMaxProvider { - fn get_name(&self) -> &str { - MINIMAX_PROVIDER_NAME - } - - fn get_model_config(&self) -> ModelConfig { - self.model.clone() - } - - async fn fetch_supported_models(&self) -> Result, super::errors::ProviderError> { - Ok(MINIMAX_KNOWN_MODELS - .iter() - .map(|(name, _)| name.to_string()) - .collect()) - } - - async fn stream( - &self, - model_config: &ModelConfig, - session_id: &str, - system: &str, - messages: &[Message], - tools: &[Tool], - ) -> Result { - let mut payload = create_request(model_config, system, messages, tools)?; - payload - .as_object_mut() - .unwrap() - .insert("stream".to_string(), Value::Bool(true)); - - let mut log = RequestLog::start(model_config, &payload)?; - - let response = self - .with_retry(|| async { - let request = self.api_client.request(Some(session_id), "v1/messages"); - let resp = request.response_post(&payload).await?; - handle_status_openai_compat(resp).await - }) - .await - .inspect_err(|e| { - let _ = log.error(e); - })?; - - let stream = response.bytes_stream().map_err(io::Error::other); - - Ok(Box::pin(try_stream! { - let stream_reader = StreamReader::new(stream); - let framed = tokio_util::codec::FramedRead::new(stream_reader, tokio_util::codec::LinesCodec::new()).map_err(anyhow::Error::from); - - let message_stream = response_to_streaming_message(framed); - pin!(message_stream); - while let Some(message) = futures::StreamExt::next(&mut message_stream).await { - let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; - yield (message, usage); - } - })) - } -} diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index cec14c036844..6b2ce20528ae 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -25,7 +25,6 @@ mod init; pub mod lead_worker; pub mod litellm; pub mod local_inference; -pub mod minimax; pub mod oauth; pub mod ollama; pub mod openai;