diff --git a/.env.example b/.env.example index b5fecbdb2..01249dcb6 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ export ANTHROPIC_API_KEY= export BRAVE_SEARCH_API_KEY= export DAYTONA_API_KEY= export GEMINI_API_KEY= +export INCEPTION_API_KEY= export KIMI_API_KEY= export MINIMAX_API_KEY= export OPENAI_API_KEY= diff --git a/crates/arc-agent/src/cli.rs b/crates/arc-agent/src/cli.rs index 0ed1f3172..29e22020b 100644 --- a/crates/arc-agent/src/cli.rs +++ b/crates/arc-agent/src/cli.rs @@ -17,7 +17,7 @@ pub struct AgentArgs { /// Task prompt pub prompt: String, - /// LLM provider (anthropic, openai, gemini, kimi, zai, minimax) + /// LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception) #[arg(long, default_value = "anthropic")] pub provider: String, @@ -78,6 +78,7 @@ fn default_model(provider: Provider) -> &'static str { Provider::Kimi => "kimi-k2.5", Provider::Zai => "glm-4.7", Provider::Minimax => "minimax-m2.5", + Provider::Inception => "mercury", } } @@ -159,6 +160,7 @@ fn summarizer_model_id(provider: Provider) -> ModelId { Provider::Kimi => ModelId::new(Provider::Kimi, "kimi-k2.5"), Provider::Zai => ModelId::new(Provider::Zai, "glm-4.7"), Provider::Minimax => ModelId::new(Provider::Minimax, "minimax-m2.5"), + Provider::Inception => ModelId::new(Provider::Inception, "mercury"), } } @@ -174,7 +176,7 @@ fn build_profile(provider: Provider, model: &str, llm_client: Option) -> let summarizer = build_summarizer(provider, llm_client); match provider { Provider::OpenAi => Box::new(OpenAiProfile::with_summarizer(model, summarizer)), - Provider::Kimi | Provider::Zai | Provider::Minimax => Box::new( + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => Box::new( OpenAiProfile::with_summarizer(model, summarizer).with_provider(provider), ), Provider::Gemini => Box::new(GeminiProfile::with_summarizer(model, summarizer)), @@ -192,6 +194,7 @@ fn validate_api_key(provider: Provider) -> bool { Provider::Kimi => std::env::var("KIMI_API_KEY").is_ok(), Provider::Zai => std::env::var("ZAI_API_KEY").is_ok(), Provider::Minimax => std::env::var("MINIMAX_API_KEY").is_ok(), + Provider::Inception => std::env::var("INCEPTION_API_KEY").is_ok(), } } @@ -400,7 +403,7 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> { Provider::OpenAi => { Arc::new(OpenAiProfile::with_summarizer(&factory_model, child_summarizer)) } - Provider::Kimi | Provider::Zai | Provider::Minimax => Arc::new( + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => Arc::new( OpenAiProfile::with_summarizer(&factory_model, child_summarizer) .with_provider(provider), ), @@ -650,6 +653,11 @@ mod tests { assert_eq!(default_model(Provider::Minimax), "minimax-m2.5"); } + #[test] + fn default_model_inception() { + assert_eq!(default_model(Provider::Inception), "mercury"); + } + // build_tool_approval non-interactive tests #[test] diff --git a/crates/arc-agent/src/project_docs.rs b/crates/arc-agent/src/project_docs.rs index 658a57ecf..45dc7cbab 100644 --- a/crates/arc-agent/src/project_docs.rs +++ b/crates/arc-agent/src/project_docs.rs @@ -13,7 +13,7 @@ pub async fn discover_project_docs( let candidate_filenames: Vec<&str> = match provider { Provider::Anthropic => vec!["AGENTS.md", "CLAUDE.md"], - Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { vec!["AGENTS.md", ".codex/instructions.md"] } Provider::Gemini => vec!["AGENTS.md", "GEMINI.md"], diff --git a/crates/arc-agent/tests/parity_matrix.rs b/crates/arc-agent/tests/parity_matrix.rs index 51a000663..5a2cb14a5 100644 --- a/crates/arc-agent/tests/parity_matrix.rs +++ b/crates/arc-agent/tests/parity_matrix.rs @@ -10,7 +10,7 @@ use arc_llm::provider::{ModelId, Provider}; fn summarizer_model_id(provider: Provider) -> ModelId { match provider { - Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { ModelId::new(Provider::OpenAi, "gpt-4o-mini") } Provider::Gemini => ModelId::new(Provider::Gemini, "gemini-2.0-flash"), @@ -32,7 +32,7 @@ fn build_profile(provider: Provider, model: &str, client: &Client) -> Box Box::new(AnthropicProfile::with_summarizer(model, summarizer)), Provider::OpenAi => Box::new(OpenAiProfile::with_summarizer(model, summarizer)), - Provider::Kimi | Provider::Zai | Provider::Minimax => Box::new( + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => Box::new( OpenAiProfile::with_summarizer(model, summarizer).with_provider(provider), ), Provider::Gemini => Box::new(GeminiProfile::with_summarizer(model, summarizer)), @@ -60,7 +60,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session { Provider::OpenAi => { Arc::new(OpenAiProfile::with_summarizer(&factory_model, summarizer)) } - Provider::Kimi | Provider::Zai | Provider::Minimax => Arc::new( + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => Arc::new( OpenAiProfile::with_summarizer(&factory_model, summarizer) .with_provider(provider), ), diff --git a/crates/arc-attractor/src/cli/backend.rs b/crates/arc-attractor/src/cli/backend.rs index 708fe5a1a..3aca438c2 100644 --- a/crates/arc-attractor/src/cli/backend.rs +++ b/crates/arc-attractor/src/cli/backend.rs @@ -76,7 +76,7 @@ impl AgentBackend { let factory: SessionFactory = Arc::new(move || { let child_profile: Arc = match factory_provider { Provider::OpenAi => Arc::new(OpenAiProfile::new(&factory_model)), - Provider::Kimi | Provider::Zai | Provider::Minimax => Arc::new( + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => Arc::new( OpenAiProfile::new(&factory_model).with_provider(factory_provider), ), Provider::Gemini => Arc::new(GeminiProfile::new(&factory_model)), @@ -104,7 +104,7 @@ impl AgentBackend { fn build_profile(&self) -> Box { match self.provider { Provider::OpenAi => Box::new(OpenAiProfile::new(&self.model)), - Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { Box::new(OpenAiProfile::new(&self.model).with_provider(self.provider)) } Provider::Gemini => Box::new(GeminiProfile::new(&self.model)), diff --git a/crates/arc-attractor/src/cli/cli_backend.rs b/crates/arc-attractor/src/cli/cli_backend.rs index 491ae0fcd..50ebf5dcf 100644 --- a/crates/arc-attractor/src/cli/cli_backend.rs +++ b/crates/arc-attractor/src/cli/cli_backend.rs @@ -31,7 +31,7 @@ pub fn cli_command_for_provider(provider: Provider, model: &str, prompt_file: &s String::new() } else { match provider { - Provider::OpenAi | Provider::Gemini | Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::OpenAi | Provider::Gemini | Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { format!(" -m {model}") } Provider::Anthropic => format!(" --model {model}"), @@ -39,7 +39,7 @@ pub fn cli_command_for_provider(provider: Provider, model: &str, prompt_file: &s }; match provider { // --full-auto: sandboxed auto-execution, escalates on request - Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { format!("codex exec --json --full-auto{model_flag} < {prompt_file}") } // --yolo: auto-approve all tool calls @@ -176,7 +176,7 @@ fn parse_gemini_json(output: &str) -> Option { /// Parse CLI output, choosing the right parser based on provider. pub fn parse_cli_response(provider: Provider, output: &str) -> Option { match provider { - Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax => { + Provider::OpenAi | Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => { parse_codex_ndjson(output) } Provider::Gemini => parse_gemini_json(output), diff --git a/crates/arc-llm/src/catalog.json b/crates/arc-llm/src/catalog.json index e1d9203f3..739bfefe6 100644 --- a/crates/arc-llm/src/catalog.json +++ b/crates/arc-llm/src/catalog.json @@ -154,5 +154,31 @@ "input_cost_per_million": null, "output_cost_per_million": null, "aliases": ["minimax"] + }, + { + "id": "mercury", + "provider": "inception", + "display_name": "Mercury", + "context_window": 32768, + "max_output": null, + "supports_tools": true, + "supports_vision": false, + "supports_reasoning": false, + "input_cost_per_million": null, + "output_cost_per_million": null, + "aliases": [] + }, + { + "id": "mercury-coder", + "provider": "inception", + "display_name": "Mercury Coder", + "context_window": 32768, + "max_output": null, + "supports_tools": true, + "supports_vision": false, + "supports_reasoning": false, + "input_cost_per_million": null, + "output_cost_per_million": null, + "aliases": [] } ] diff --git a/crates/arc-llm/src/catalog.rs b/crates/arc-llm/src/catalog.rs index ccc177981..89514d382 100644 --- a/crates/arc-llm/src/catalog.rs +++ b/crates/arc-llm/src/catalog.rs @@ -62,7 +62,7 @@ mod tests { #[test] fn list_models_all() { let models = list_models(None); - assert_eq!(models.len(), 12); + assert_eq!(models.len(), 14); } #[test] @@ -106,6 +106,22 @@ mod tests { assert_eq!(m.provider, "minimax"); } + #[test] + fn mercury_in_catalog() { + let m = get_model_info("mercury").unwrap(); + assert_eq!(m.provider, "inception"); + assert_eq!(m.context_window, 32768); + assert!(m.supports_tools); + assert!(!m.supports_vision); + assert!(!m.supports_reasoning); + } + + #[test] + fn mercury_coder_in_catalog() { + let m = get_model_info("mercury-coder").unwrap(); + assert_eq!(m.provider, "inception"); + } + #[test] fn model_info_costs() { let claude = get_model_info("claude-opus-4-6").unwrap(); diff --git a/crates/arc-llm/src/client.rs b/crates/arc-llm/src/client.rs index 1d86b8e8f..d5b01cae5 100644 --- a/crates/arc-llm/src/client.rs +++ b/crates/arc-llm/src/client.rs @@ -93,6 +93,12 @@ impl Client { .with_name("minimax"); client.register_provider(Arc::new(adapter)).await?; } + if let Ok(key) = std::env::var("INCEPTION_API_KEY") { + let adapter = + providers::OpenAiCompatibleAdapter::new(key, "https://api.inceptionlabs.ai/v1") + .with_name("inception"); + client.register_provider(Arc::new(adapter)).await?; + } Ok(client) } diff --git a/crates/arc-llm/src/provider.rs b/crates/arc-llm/src/provider.rs index 898244889..9bf84d711 100644 --- a/crates/arc-llm/src/provider.rs +++ b/crates/arc-llm/src/provider.rs @@ -20,6 +20,7 @@ pub enum Provider { Kimi, Zai, Minimax, + Inception, } impl Provider { @@ -34,6 +35,7 @@ impl Provider { Self::Kimi => "kimi", Self::Zai => "zai", Self::Minimax => "minimax", + Self::Inception => "inception", } } } @@ -55,6 +57,7 @@ impl FromStr for Provider { "kimi" => Ok(Self::Kimi), "zai" => Ok(Self::Zai), "minimax" => Ok(Self::Minimax), + "inception" | "inception_labs" => Ok(Self::Inception), other => Err(format!("unknown provider: {other}")), } } @@ -182,4 +185,15 @@ mod tests { fn minimax_as_str() { assert_eq!(Provider::Minimax.as_str(), "minimax"); } + + #[test] + fn parse_inception() { + assert_eq!("inception".parse::().unwrap(), Provider::Inception); + assert_eq!("inception_labs".parse::().unwrap(), Provider::Inception); + } + + #[test] + fn inception_as_str() { + assert_eq!(Provider::Inception.as_str(), "inception"); + } }