diff --git a/.env.example b/.env.example index c9f059ea1..bdb08589e 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ DEEPSEEK_API_KEY= FIREWORKS_API_KEY= GEMINI_API_KEY= INCEPTION_API_KEY= -KIMI_API_KEY= +MOONSHOT_API_KEY= MINIMAX_API_KEY= MODAL_KIMI_K3_BASE_URL= MODAL_TOKEN_ID= diff --git a/apps/fabro-web/public/images/providers/modal.svg b/apps/fabro-web/public/images/providers/modal.svg index 6499237e9..8360e8aa7 100644 --- a/apps/fabro-web/public/images/providers/modal.svg +++ b/apps/fabro-web/public/images/providers/modal.svg @@ -1,3 +1,3 @@ - + diff --git a/apps/fabro-web/public/images/providers/kimi.svg b/apps/fabro-web/public/images/providers/moonshot.svg similarity index 98% rename from apps/fabro-web/public/images/providers/kimi.svg rename to apps/fabro-web/public/images/providers/moonshot.svg index 3cdf7c868..0670e864e 100644 --- a/apps/fabro-web/public/images/providers/kimi.svg +++ b/apps/fabro-web/public/images/providers/moonshot.svg @@ -1,3 +1,8 @@ - + diff --git a/apps/fabro-web/public/images/providers/openrouter.svg b/apps/fabro-web/public/images/providers/openrouter.svg index 4069034cd..ec0d854e6 100644 --- a/apps/fabro-web/public/images/providers/openrouter.svg +++ b/apps/fabro-web/public/images/providers/openrouter.svg @@ -1,6 +1,3 @@ - - - - - + + diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index 9f107d046..b6fa8d2b7 100644 --- a/docs/public/administration/server-configuration.mdx +++ b/docs/public/administration/server-configuration.mdx @@ -383,7 +383,7 @@ fabro secret set GEMINI_API_KEY AI... | `ANTHROPIC_API_KEY` | Anthropic (Claude) | | `OPENAI_API_KEY` | OpenAI (GPT) | | `GEMINI_API_KEY` | Google (Gemini) | -| `KIMI_API_KEY` | Kimi | +| `MOONSHOT_API_KEY` or `KIMI_API_KEY` | Moonshot AI; `MOONSHOT_API_KEY` takes precedence | | `ZAI_API_KEY` | Zai (GLM) | | `MINIMAX_API_KEY` | Minimax | | `INCEPTION_API_KEY` | Inception (Mercury) | diff --git a/docs/public/changelog/2026-02-28.mdx b/docs/public/changelog/2026-02-28.mdx index 261594e32..78fadc46b 100644 --- a/docs/public/changelog/2026-02-28.mdx +++ b/docs/public/changelog/2026-02-28.mdx @@ -26,7 +26,7 @@ Previously, you could only see the final output of a run. Now you can trace exac -- Kimi, Z.AI, and Minimax LLM providers added to model catalog +- Moonshot AI, Z.AI, and Minimax LLM providers added to model catalog - Runs use time-sortable ULIDs instead of UUIDs, making it easier to find recent runs in logs and the API - Asymmetric JWT authentication between fabro-web and the API server, with GitHub identity in the token diff --git a/docs/public/changelog/2026-07-22.mdx b/docs/public/changelog/2026-07-22.mdx index a84beaea1..261e7f4b1 100644 --- a/docs/public/changelog/2026-07-22.mdx +++ b/docs/public/changelog/2026-07-22.mdx @@ -29,7 +29,7 @@ Schema upgrades now create a consistent `fabro.sqlite3.pre-migration.bak` snapsh -- Added Kimi K3 through the direct Kimi provider and OpenRouter +- Added Kimi K3 through the direct Moonshot AI provider and OpenRouter - Added GLM 5.2 through Z.AI and OpenRouter - Left-to-right run graphs now zoom to 400% and preserve separate viewport positions for left-to-right and top-to-bottom layouts diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index 84f00bb1e..df78987d7 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -57,8 +57,8 @@ Fabro performs this selection once when creating a run and persists the chosen p | `gemini-3.5-flash` | gemini | `gemini-35-flash` | 1M | $1.50 / $9.00 | 150 tok/s | | `gemini-3-flash-preview` | gemini | `gemini-flash` | 1M | $0.50 / $3.00 | 150 tok/s | | `gemini-3.1-flash-lite` | gemini | `gemini-flash-lite`, `gemini-3.1-flash-lite-preview` | 1M | $0.25 / $1.50 | 200 tok/s | -| `kimi-k2.5` | kimi | | 262K | $0.60 / $3.00 | 50 tok/s | -| `kimi-k3` | kimi | `kimi` | 1M | $3.00 / $15.00 | n/a | +| `kimi-k2.5` | moonshot | | 262K | $0.60 / $3.00 | 50 tok/s | +| `kimi-k3` | moonshot | `kimi` | 1M | $3.00 / $15.00 | n/a | | `deepseek-v4-flash` | deepseek | `deepseek`, `deepseek-v4`, `deepseek-flash` | 1,048,576 | $0.14 / $0.28 | n/a | | `deepseek-v4-pro` | deepseek | | 1,048,576 | $0.435 / $0.87 | n/a | | `laguna-s-2.1` | poolside | `laguna`, `laguna-s` | 1M | $0.10 / $0.20 | n/a | @@ -229,7 +229,7 @@ When no model or provider is specified, Fabro chooses the default offering on th | `anthropic` | `claude-sonnet-4-6` | | `openai` | `gpt-5.6-sol` | | `gemini` | `gemini-3.5-flash` | -| `kimi` | `kimi-k3` | +| `moonshot` | `kimi-k3` | | `poolside` | `laguna-s-2.1` | | `zai` | `glm-5.2` | | `minimax` | `minimax-m2.5` | diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index dd8819052..56e719ce0 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -151,7 +151,7 @@ For a qualified fallback, the selector may be that provider's canonical model ID ```toml title="run.toml" [run.model.fallbacks] "kimi-k3" = [ - "kimi:kimi-k3", + "moonshot:kimi-k3", "openrouter:kimi-k3", "claude-opus", ] @@ -169,7 +169,7 @@ For example, a server that selects Modal as the primary `kimi-k3` offering can d ```toml title="settings.toml" [run.model.fallbacks] -"kimi-k3" = ["kimi:kimi-k3", "openrouter:kimi-k3", "claude-opus"] +"kimi-k3" = ["moonshot:kimi-k3", "openrouter:kimi-k3", "claude-opus"] "glm-5.2" = ["gpt-sol"] "gpt-sol" = ["claude-opus"] "claude-opus" = ["gpt-sol"] diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index ad4eedb0b..12dee0001 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -366,7 +366,7 @@ For env-backed usage, `EnvCredentialSource` checks for API key environment varia | `ANTHROPIC_API_KEY` | Anthropic | | `OPENAI_API_KEY` | OpenAI | | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini | -| `KIMI_API_KEY` | Kimi | +| `MOONSHOT_API_KEY` or `KIMI_API_KEY` | Moonshot AI; `MOONSHOT_API_KEY` takes precedence | | `ZAI_API_KEY` | ZAI | | `MINIMAX_API_KEY` | Minimax | | `INCEPTION_API_KEY` | Inception | diff --git a/lib/apps/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs index adfb2d97c..973524899 100644 --- a/lib/apps/fabro-cli/src/commands/install.rs +++ b/lib/apps/fabro-cli/src/commands/install.rs @@ -3515,7 +3515,7 @@ root = "{}" assert!(ids.contains(&ProviderId::anthropic())); assert!(ids.contains(&ProviderId::openai())); assert!(ids.contains(&ProviderId::gemini())); - assert!(ids.contains(&ProviderId::new("kimi"))); + assert!(ids.contains(&ProviderId::new("moonshot"))); assert!(ids.contains(&ProviderId::new("zai"))); assert!(ids.contains(&ProviderId::new("minimax"))); assert!(ids.contains(&ProviderId::new("inception"))); diff --git a/lib/apps/fabro-cli/src/commands/model.rs b/lib/apps/fabro-cli/src/commands/model.rs index 774feae42..dcff19c81 100644 --- a/lib/apps/fabro-cli/src/commands/model.rs +++ b/lib/apps/fabro-cli/src/commands/model.rs @@ -723,7 +723,7 @@ mod tests { .body( serde_json::json!({ "model_id": "kimi-k2.5", - "provider": "kimi", + "provider": "moonshot", "status": "skip" }) .to_string(), diff --git a/lib/apps/fabro-cli/src/shared/provider_auth.rs b/lib/apps/fabro-cli/src/shared/provider_auth.rs index 1d3992f1f..f61663e84 100644 --- a/lib/apps/fabro-cli/src/shared/provider_auth.rs +++ b/lib/apps/fabro-cli/src/shared/provider_auth.rs @@ -386,7 +386,7 @@ mod tests { ProviderId::anthropic(), ProviderId::openai(), ProviderId::gemini(), - ProviderId::new("kimi"), + ProviderId::new("moonshot"), ProviderId::new("zai"), ProviderId::new("minimax"), ProviderId::new("inception"), diff --git a/lib/apps/fabro-cli/tests/it/cmd/model_test.rs b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs index acaae9851..53fd86fb2 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/model_test.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs @@ -18,6 +18,7 @@ fn remove_provider_env(cmd: &mut Command) -> &mut Command { .env_remove("OPENAI_API_KEY") .env_remove("GEMINI_API_KEY") .env_remove("GOOGLE_API_KEY") + .env_remove("MOONSHOT_API_KEY") .env_remove("KIMI_API_KEY") .env_remove("ZAI_API_KEY") .env_remove("MINIMAX_API_KEY") diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index 7c1a71951..642199d54 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -1542,7 +1542,7 @@ enabled = true let mut checks = Vec::new(); let configured = std::collections::BTreeMap::from([( "kimi-k3".to_string(), - model_refs(&["kimi:kimi-k3", "openrouter:kimi-k3"]), + model_refs(&["moonshot:kimi-k3", "openrouter:kimi-k3"]), )]); assert!(run_model_fallback_check( @@ -1554,13 +1554,12 @@ enabled = true let check = checks.last().expect("fallback check should be present"); assert_eq!(check.status, CheckStatus::Warning); - assert!( - check - .details - .iter() - .any(|detail| detail.warn - && detail.text.contains("provider `kimi` is not configured")) - ); + assert!(check.details.iter().any(|detail| { + detail.warn + && detail + .text + .contains("provider `moonshot` is not configured") + })); } #[test] @@ -1603,15 +1602,15 @@ enabled = true }) } - fn ready_kimi_and_openrouter_state( + fn ready_moonshot_and_openrouter_state( server: &httpmock::MockServer, ) -> Arc { - let kimi_url = server.url("/kimi/v1"); + let moonshot_url = server.url("/moonshot/v1"); let openrouter_url = server.url("/openrouter/v1"); let llm_catalog_settings: LlmCatalogSettings = toml::from_str(&format!( r#" -[providers.kimi] -base_url = "{kimi_url}" +[providers.moonshot] +base_url = "{moonshot_url}" [providers.openrouter] base_url = "{openrouter_url}" @@ -1623,7 +1622,7 @@ enabled = true crate::test_support::TestAppStateBuilder::new() .llm_catalog_settings(llm_catalog_settings) .vault_entries([ - (EnvVars::KIMI_API_KEY, "test-kimi-key"), + (EnvVars::KIMI_API_KEY, "test-moonshot-key"), (EnvVars::OPENROUTER_API_KEY, "test-openrouter-key"), ]) .build() @@ -1640,7 +1639,7 @@ enabled = true .unwrap_or_default(); ready_providers.sort(); assert_eq!(ready_providers, vec![ - ProviderId::new("kimi"), + ProviderId::new("moonshot"), ProviderId::new("openrouter") ]); @@ -2663,7 +2662,7 @@ digraph Demo { .json_body(openai_compatible_completion("anthropic/claude-fable-5")); }) .await; - let state = ready_kimi_and_openrouter_state(&server); + let state = ready_moonshot_and_openrouter_state(&server); let (response, _ok) = preflight_for_model(&state, "claude-fable").await; @@ -2687,18 +2686,18 @@ digraph Demo { #[tokio::test] async fn preflight_uses_ready_providers_for_unknown_unqualified_model() { let server = httpmock::MockServer::start_async().await; - let kimi_probe = server + let moonshot_probe = server .mock_async(|when, then| { when.method(httpmock::Method::POST) - .path("/kimi/v1/chat/completions") - .header("authorization", "Bearer test-kimi-key") + .path("/moonshot/v1/chat/completions") + .header("authorization", "Bearer test-moonshot-key") .json_body_includes(r#"{"model":"provider-private-preview"}"#); then.status(200) .header("content-type", "application/json") .json_body(openai_compatible_completion("provider-private-preview")); }) .await; - let state = ready_kimi_and_openrouter_state(&server); + let state = ready_moonshot_and_openrouter_state(&server); let (response, _ok) = preflight_for_model(&state, "provider-private-preview").await; @@ -2717,10 +2716,10 @@ digraph Demo { .iter() .map(|detail| detail.text.as_str()) .find(|detail| detail.starts_with("Provider: ")), - Some("Provider: kimi") + Some("Provider: moonshot") ); assert_eq!(llm_check.status, types::PreflightCheckResultStatus::Pass); - kimi_probe.assert_async().await; + moonshot_probe.assert_async().await; } #[test] diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 491fb2da9..1c04eb4a1 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -7446,7 +7446,7 @@ async fn list_models_exposes_reasoning_effort_controls() { let req = Request::builder() .method("GET") - .uri(api("/models?provider=kimi")) + .uri(api("/models?provider=moonshot")) .body(Body::empty()) .unwrap(); @@ -16443,7 +16443,7 @@ async fn create_completion_unsupported_reasoning_efforts_return_bad_request() { then.status(500); }); let state = TestAppStateBuilder::new() - .provider_base_url("kimi", upstream.url("/v1")) + .provider_base_url("moonshot", upstream.url("/v1")) .vault_entries([(EnvVars::KIMI_API_KEY, "test-kimi-api-key")]) .build(); let app = crate::test_support::build_test_router(state); @@ -16456,7 +16456,7 @@ async fn create_completion_unsupported_reasoning_efforts_return_bad_request() { .header("content-type", "application/json") .body(Body::from( serde_json::json!({ - "provider": "kimi", + "provider": "moonshot", "model": "kimi-k3", "reasoning_effort": effort, "stream": stream, @@ -16515,7 +16515,7 @@ async fn create_completion_returns_disjoint_usage_buckets() { })); }); let state = TestAppStateBuilder::new() - .provider_base_url("kimi", upstream.base_url()) + .provider_base_url("moonshot", upstream.base_url()) .vault_entries([(EnvVars::KIMI_API_KEY, "test-kimi-api-key")]) .build(); let app = crate::test_support::build_test_router(state); @@ -16526,7 +16526,7 @@ async fn create_completion_returns_disjoint_usage_buckets() { .header("content-type", "application/json") .body(Body::from( json!({ - "provider": "kimi", + "provider": "moonshot", "model": "kimi-k3", "stream": false, "messages": [{ @@ -16655,7 +16655,7 @@ async fn create_completion_structured_output_forwards_reasoning_effort() { })); }); let state = TestAppStateBuilder::new() - .provider_base_url("kimi", upstream.base_url()) + .provider_base_url("moonshot", upstream.base_url()) .vault_entries([(EnvVars::KIMI_API_KEY, "test-kimi-api-key")]) .build(); let app = crate::test_support::build_test_router(state); @@ -16666,7 +16666,7 @@ async fn create_completion_structured_output_forwards_reasoning_effort() { .header("content-type", "application/json") .body(Body::from( serde_json::json!({ - "provider": "kimi", + "provider": "moonshot", "model": "kimi-k3", "reasoning_effort": "high", "stream": false, diff --git a/lib/apps/fabro-server/tests/it/api/install.rs b/lib/apps/fabro-server/tests/it/api/install.rs index 8dde87fdd..cfd23dc14 100644 --- a/lib/apps/fabro-server/tests/it/api/install.rs +++ b/lib/apps/fabro-server/tests/it/api/install.rs @@ -143,7 +143,7 @@ async fn mock_anthropic_install_validation(server: &MockServer) -> httpmock::Moc .await } -async fn mock_kimi_install_validation(server: &MockServer) -> httpmock::Mock<'_> { +async fn mock_moonshot_install_validation(server: &MockServer) -> httpmock::Mock<'_> { server .mock_async(|when, then| { when.method("POST") @@ -1355,11 +1355,13 @@ async fn token_install_finish_invokes_shutdown_callback_after_accepting() { #[tokio::test] async fn install_llm_accepts_catalog_openai_compatible_provider() { let llm_mock = MockServer::start_async().await; - mock_kimi_install_validation(&llm_mock).await; + mock_moonshot_install_validation(&llm_mock).await; let app = build_install_router( - InstallAppState::for_test("test-install-token") - .with_provider_base_url(ProviderId::new("kimi"), format!("{}/v1", llm_mock.url(""))), + InstallAppState::for_test("test-install-token").with_provider_base_url( + ProviderId::new("moonshot"), + format!("{}/v1", llm_mock.url("")), + ), ); let test_response = app @@ -1371,7 +1373,7 @@ async fn install_llm_accepts_catalog_openai_compatible_provider() { .header("authorization", "Bearer test-install-token") .header("content-type", "application/json") .body(Body::from( - r#"{"provider":"kimi","api_key":"kimi-test-key"}"#, + r#"{"provider":"moonshot","api_key":"kimi-test-key"}"#, )) .unwrap(), ) @@ -1388,7 +1390,7 @@ async fn install_llm_accepts_catalog_openai_compatible_provider() { .header("authorization", "Bearer test-install-token") .header("content-type", "application/json") .body(Body::from( - r#"{"providers":[{"provider":"kimi","api_key":"kimi-test-key"}]}"#, + r#"{"providers":[{"provider":"moonshot","api_key":"kimi-test-key"}]}"#, )) .unwrap(), ) diff --git a/lib/components/fabro-agent/src/compaction.rs b/lib/components/fabro-agent/src/compaction.rs index 2b09192b4..ad634955d 100644 --- a/lib/components/fabro-agent/src/compaction.rs +++ b/lib/components/fabro-agent/src/compaction.rs @@ -443,12 +443,12 @@ mod tests { #[test] fn summary_budget_for_always_reasoning_route_without_effort_adds_headroom() { - let kimi = ProviderId::new("kimi"); - let model = catalog_model(&kimi, "kimi-k2.5"); + let moonshot = ProviderId::new("moonshot"); + let model = catalog_model(&moonshot, "kimi-k2.5"); assert!(model.supports_reasoning()); assert!(!model.supports_reasoning_effort()); assert_eq!( - builtin_summary_max_tokens(&kimi, "kimi-k2.5"), + builtin_summary_max_tokens(&moonshot, "kimi-k2.5"), SUMMARY_MAX_TOKENS + REASONING_HEADROOM_TOKENS ); } diff --git a/lib/components/fabro-agent/src/profiles/kimi.rs b/lib/components/fabro-agent/src/profiles/kimi.rs index a37131ff5..e04a7d19c 100644 --- a/lib/components/fabro-agent/src/profiles/kimi.rs +++ b/lib/components/fabro-agent/src/profiles/kimi.rs @@ -100,7 +100,7 @@ impl KimiProfile { Self { base: BaseProfile { profile_kind: AgentProfileKind::Kimi, - provider_id: ProviderId::new("kimi"), + provider_id: ProviderId::new("moonshot"), model: model.into(), catalog: None, registry, @@ -178,8 +178,8 @@ mod tests { #[test] fn kimi_models_select_the_kimi_profile_on_every_provider() { for (catalog, provider, model) in [ - (catalog(), "kimi", "kimi-k3"), - (catalog(), "kimi", "kimi-k2.5"), + (catalog(), "moonshot", "kimi-k3"), + (catalog(), "moonshot", "kimi-k2.5"), (catalog_with_openrouter(), "openrouter", "kimi-k3"), (catalog_with_openrouter(), "openrouter", "kimi-k2.6"), ] { diff --git a/lib/components/fabro-agent/src/profiles/mod.rs b/lib/components/fabro-agent/src/profiles/mod.rs index b2402f066..dbc0d0c54 100644 --- a/lib/components/fabro-agent/src/profiles/mod.rs +++ b/lib/components/fabro-agent/src/profiles/mod.rs @@ -548,7 +548,7 @@ mod tests { let options = native_tool_options(AgentProfileKind::OpenAi, has_web_search); let deps = ProfileDeps::standalone(options); OpenAiProfile::with_native_tools("kimi-k2.5", &deps).with_route( - ProviderId::new("kimi"), + ProviderId::new("moonshot"), Arc::new(Catalog::from_builtin().unwrap()), ) } diff --git a/lib/components/fabro-agent/src/profiles/openai.rs b/lib/components/fabro-agent/src/profiles/openai.rs index 5f1029e87..d3010c859 100644 --- a/lib/components/fabro-agent/src/profiles/openai.rs +++ b/lib/components/fabro-agent/src/profiles/openai.rs @@ -229,19 +229,19 @@ mod tests { } #[test] - fn kimi_provider_prompt_uses_catalog_display_name() { + fn moonshot_provider_prompt_uses_catalog_display_name() { let profile = - OpenAiProfile::new("kimi-k2.5").with_route(ProviderId::new("kimi"), test_catalog()); + OpenAiProfile::new("kimi-k2.5").with_route(ProviderId::new("moonshot"), test_catalog()); let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); - assert!(prompt.contains("powered by Kimi")); + assert!(prompt.contains("powered by Moonshot AI")); assert!(!prompt.contains("powered by OpenAI")); } #[test] fn openai_compatible_profile_uses_json_schema_edit_tool() { let profile = - OpenAiProfile::new("kimi-k2.5").with_route(ProviderId::new("kimi"), test_catalog()); + OpenAiProfile::new("kimi-k2.5").with_route(ProviderId::new("moonshot"), test_catalog()); let names = profile.tool_registry().names(); assert!(names.contains(&"edit_file".to_string())); diff --git a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap index d2406f238..43e52f68b 100644 --- a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap +++ b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap @@ -2,7 +2,7 @@ source: lib/components/fabro-agent/src/profiles/mod.rs expression: system_prompt(&openai_edit_file_profile(true)) --- -You are a coding agent powered by Kimi, running in a terminal-based agentic coding assistant. You are expected to be precise, safe, and helpful. +You are a coding agent powered by Moonshot AI, running in a terminal-based agentic coding assistant. You are expected to be precise, safe, and helpful. You can receive user prompts and context such as files in the workspace, communicate with the user by streaming thinking and responses, and emit function calls to run terminal commands and edit files. diff --git a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_prompt_snapshot.snap b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_prompt_snapshot.snap index 478a8b2d4..2d192032c 100644 --- a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_prompt_snapshot.snap +++ b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_prompt_snapshot.snap @@ -2,7 +2,7 @@ source: lib/components/fabro-agent/src/profiles/mod.rs expression: system_prompt(&openai_edit_file_profile(false)) --- -You are a coding agent powered by Kimi, running in a terminal-based agentic coding assistant. You are expected to be precise, safe, and helpful. +You are a coding agent powered by Moonshot AI, running in a terminal-based agentic coding assistant. You are expected to be precise, safe, and helpful. You can receive user prompts and context such as files in the workspace, communicate with the user by streaming thinking and responses, and emit function calls to run terminal commands and edit files. diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index e37a29003..7f64a29fb 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -31,7 +31,7 @@ struct OpenAiTwinOptions { fn summarizer_model_id(provider: &Provider) -> ModelHandle { match provider.as_str() { - ProviderId::OPENAI | "kimi" | "zai" | "minimax" | "inception" => ModelHandle::ByName { + ProviderId::OPENAI | "moonshot" | "zai" | "minimax" | "inception" => ModelHandle::ByName { provider: ProviderId::openai(), model: "gpt-5.4-mini".to_string(), }, @@ -279,7 +279,7 @@ macro_rules! provider_tests { ); provider_test!( $scenario, - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi, keys = ["KIMI_API_KEY"] @@ -413,7 +413,7 @@ provider_test!( ); provider_test!( web_fetch, - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi, keys = ["KIMI_API_KEY", "OPENAI_API_KEY"] @@ -461,7 +461,7 @@ web_search_provider_test!( keys = ["GEMINI_API_KEY", "BRAVE_SEARCH_API_KEY"] ); web_search_provider_test!( - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi, keys = ["KIMI_API_KEY", "BRAVE_SEARCH_API_KEY"] @@ -518,7 +518,7 @@ macro_rules! non_openai_provider_tests { ); provider_test!( $scenario, - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi, keys = ["KIMI_API_KEY"] @@ -800,7 +800,7 @@ reasoning_effort_tests!( keys = ["GEMINI_API_KEY"] ); reasoning_effort_tests!( - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi_reasoning_effort, keys = ["KIMI_API_KEY"] @@ -882,7 +882,7 @@ loop_detection_tests!( keys = ["GEMINI_API_KEY"] ); loop_detection_tests!( - ProviderId::new("kimi"), + ProviderId::new("moonshot"), "kimi-k2.5", kimi_loop_detection, keys = ["KIMI_API_KEY"] diff --git a/lib/components/fabro-llm/src/adapter_registry.rs b/lib/components/fabro-llm/src/adapter_registry.rs index bb1b893ce..a306c846b 100644 --- a/lib/components/fabro-llm/src/adapter_registry.rs +++ b/lib/components/fabro-llm/src/adapter_registry.rs @@ -427,10 +427,10 @@ mod tests { fn openai_compatible_factory_uses_provider_id_for_name() { let config = AdapterConfig { base_url: Some("https://api.moonshot.ai/v1".to_string()), - ..AdapterConfig::new("kimi", ApiKeyHeader::Bearer("k".to_string())) + ..AdapterConfig::new("moonshot", ApiKeyHeader::Bearer("k".to_string())) }; let adapter = factory_for(AdapterKind::OpenAiCompatible)(config).unwrap(); - assert_eq!(adapter.name(), "kimi"); + assert_eq!(adapter.name(), "moonshot"); } #[test] @@ -491,7 +491,7 @@ mod tests { #[test] fn openai_compatible_factory_errors_without_base_url() { - let config = AdapterConfig::new("kimi", ApiKeyHeader::Bearer("k".to_string())); + let config = AdapterConfig::new("moonshot", ApiKeyHeader::Bearer("k".to_string())); let Err(err) = factory_for(AdapterKind::OpenAiCompatible)(config) else { panic!("expected missing base_url error"); }; diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index e71fd9309..1c0d3e46e 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -1529,13 +1529,16 @@ base_url = "{}/v1" let mut client = Client::new(HashMap::new(), None, vec![]); client.catalog = Some(Arc::clone(&catalog)); client - .register_provider(Arc::new(MockProvider::new("kimi", "should not dispatch"))) + .register_provider(Arc::new(MockProvider::new( + "moonshot", + "should not dispatch", + ))) .await .unwrap(); let mut request = test_request(); request.model = "kimi-k2.5".to_string(); - request.provider = Some("kimi".to_string()); + request.provider = Some("moonshot".to_string()); request.reasoning_effort = Some(ReasoningEffort::High); let err = client.complete(&request).await.unwrap_err(); @@ -1554,13 +1557,13 @@ base_url = "{}/v1" let mut client = Client::new(HashMap::new(), None, vec![]); client.catalog = Some(Arc::clone(&catalog)); client - .register_provider(Arc::new(MockProvider::new("kimi", "accepted"))) + .register_provider(Arc::new(MockProvider::new("moonshot", "accepted"))) .await .unwrap(); let mut request = test_request(); request.model = "kimi-k3".to_string(); - request.provider = Some("kimi".to_string()); + request.provider = Some("moonshot".to_string()); request.reasoning_effort = Some(ReasoningEffort::High); let response = client.complete(&request).await.unwrap(); @@ -1724,7 +1727,7 @@ base_url = "{}/v1" let catalog = catalog_with(""); let client = Client::from_credentials( vec![ApiCredential { - provider: ProviderId::new("kimi"), + provider: ProviderId::new("moonshot"), auth_header: Some(ApiKeyHeader::Bearer("kimi-key".to_string())), extra_headers: HashMap::new(), base_url: None, @@ -1737,8 +1740,8 @@ base_url = "{}/v1" .await .unwrap(); - assert_eq!(client.provider_names(), vec!["kimi"]); - assert_eq!(client.default_provider(), Some("kimi")); + assert_eq!(client.provider_names(), vec!["moonshot"]); + assert_eq!(client.default_provider(), Some("moonshot")); } #[tokio::test] diff --git a/lib/components/fabro-llm/src/codec/mod.rs b/lib/components/fabro-llm/src/codec/mod.rs index 97bf842b9..0e90c471f 100644 --- a/lib/components/fabro-llm/src/codec/mod.rs +++ b/lib/components/fabro-llm/src/codec/mod.rs @@ -73,7 +73,7 @@ pub(crate) struct CodecCtx<'a> { /// the stream model fallback uses `request.model`). pub request: &'a Request, /// Identity stamped into `Response.provider`, and the `provider_options` - /// namespace key for the openai_compatible codec (kimi/zai/…). + /// namespace key for the openai_compatible codec (moonshot/zai/…). pub provider_name: &'a str, /// The model id to send on the wire — catalog `api_id`, resolved by the /// route (today `api_id == id` everywhere). diff --git a/lib/components/fabro-llm/src/codec/openai_compatible/mod.rs b/lib/components/fabro-llm/src/codec/openai_compatible/mod.rs index 42a2fde09..6bcae4492 100644 --- a/lib/components/fabro-llm/src/codec/openai_compatible/mod.rs +++ b/lib/components/fabro-llm/src/codec/openai_compatible/mod.rs @@ -1,6 +1,6 @@ //! The OpenAI Chat Completions (`/chat/completions`) codec. //! -//! Serves every "OpenAI-compatible" route (kimi, zai, minimax, venice, +//! Serves every "OpenAI-compatible" route (moonshot, zai, minimax, venice, //! inception, ollama, litellm, …). Pure translation: no HTTP, auth, or base //! URL — the adapter shell owns those. Count-tokens and error mapping use the //! `Codec` trait defaults (this dialect has no count route and uses the shared diff --git a/lib/components/fabro-llm/src/codec/openai_compatible/request.rs b/lib/components/fabro-llm/src/codec/openai_compatible/request.rs index f6bbf3b16..8db03603f 100644 --- a/lib/components/fabro-llm/src/codec/openai_compatible/request.rs +++ b/lib/components/fabro-llm/src/codec/openai_compatible/request.rs @@ -110,7 +110,7 @@ fn apply_cache_breakpoints(messages: &mut [ChatMessage]) { /// Merge `provider_options.` fields into the serialized API /// request body. /// -/// The provider name is configurable (e.g. "groq", "together", "kimi"), +/// The provider name is configurable (e.g. "groq", "together", "moonshot"), /// allowing each instance to have its own namespace in `provider_options`. pub(super) fn merge_provider_options( body: &mut serde_json::Value, @@ -221,7 +221,7 @@ mod tests { let mut request = minimal_request(); request.reasoning_effort = Some(ReasoningEffort::High); - let body = encode_body(&request, "kimi", false); + let body = encode_body(&request, "moonshot", false); assert_eq!(body["reasoning_effort"], "high"); } @@ -229,7 +229,7 @@ mod tests { #[test] fn encode_omits_sampling_params_for_models_that_reject_them() { let model = Catalog::builtin() - .get_on_provider(&ProviderId::new("kimi"), "kimi-k3") + .get_on_provider(&ProviderId::new("moonshot"), "kimi-k3") .unwrap(); let mut request = minimal_request(); request.model = model.id.to_string(); @@ -238,7 +238,7 @@ mod tests { let params = CodecParams::default(); let ctx = CodecCtx { request: &request, - provider_name: "kimi", + provider_name: "moonshot", deployment_id: model.id.as_str(), model: Some(model), params: ¶ms, @@ -262,7 +262,7 @@ mod tests { let deployment_id = request.model.clone(); let ctx = CodecCtx { request: &request, - provider_name: "kimi", + provider_name: "moonshot", deployment_id: &deployment_id, model: None, params: ¶ms, diff --git a/lib/components/fabro-llm/src/providers/anthropic.rs b/lib/components/fabro-llm/src/providers/anthropic.rs index 83894e9a9..723e12441 100644 --- a/lib/components/fabro-llm/src/providers/anthropic.rs +++ b/lib/components/fabro-llm/src/providers/anthropic.rs @@ -169,7 +169,8 @@ impl Adapter { /// Collect a streaming response into a single [`Response`]. /// - /// Used by non-Anthropic providers (e.g. Kimi) that require `stream=true`. + /// Used by non-Anthropic providers (e.g. Moonshot) that require + /// `stream=true`. async fn complete_via_stream(&self, request: &Request) -> Result { use futures::StreamExt; @@ -266,7 +267,7 @@ impl ProviderAdapter for Adapter { self.validate_request(request)?; let route = self.route_config(); - // Non-Anthropic providers (e.g. Kimi) require stream=true even for + // Non-Anthropic providers (e.g. Moonshot) require stream=true even for // blocking calls. Collect the stream into a single Response. if route.force_streaming { return self.complete_via_stream(request).await; @@ -367,8 +368,8 @@ mod tests { #[test] fn adapter_with_name() { - let adapter = Adapter::new("key").with_name("kimi"); - assert_eq!(adapter.name(), "kimi"); + let adapter = Adapter::new("key").with_name("moonshot"); + assert_eq!(adapter.name(), "moonshot"); } #[test] diff --git a/lib/components/fabro-llm/tests/integration.rs b/lib/components/fabro-llm/tests/integration.rs index b9790b5ad..507913d1b 100644 --- a/lib/components/fabro-llm/tests/integration.rs +++ b/lib/components/fabro-llm/tests/integration.rs @@ -178,7 +178,7 @@ async fn openai_gpt_5_5_pro_complete() { async fn kimi_k3_reasoning_tool_round_trip() { let api_key = std::env::var(EnvVars::KIMI_API_KEY).expect("KIMI_API_KEY must be set"); let adapter = OpenAiCompatibleAdapter::new(api_key, "https://api.moonshot.ai/v1") - .with_name("kimi") + .with_name("moonshot") .with_catalog(Arc::new(Catalog::from_builtin().unwrap())); let tool = ToolDefinition::function( "multiply", diff --git a/lib/components/fabro-llm/tests/it/wire/anthropic.rs b/lib/components/fabro-llm/tests/it/wire/anthropic.rs index ba73edaef..706893f16 100644 --- a/lib/components/fabro-llm/tests/it/wire/anthropic.rs +++ b/lib/components/fabro-llm/tests/it/wire/anthropic.rs @@ -835,11 +835,11 @@ async fn stream_without_message_stop_emits_no_finish() { } // --------------------------------------------------------------------------- -// Custom-named route (the Kimi-over-anthropic shape) +// Custom-named route (the Moonshot Kimi-over-anthropic shape) // --------------------------------------------------------------------------- -/// Shared setup for a custom-named anthropic-dialect (Kimi) stream route; the -/// request and event halves are pinned by separate tests. +/// Shared setup for a custom-named Moonshot Kimi stream route using the +/// Anthropic dialect; separate tests pin the request and event halves. async fn custom_named_stream_capture() -> (WireCapture, Vec) { let sse = support::sse_transcript(&[ ( @@ -865,15 +865,15 @@ async fn custom_named_stream_capture() -> (WireCapture, Vec) ("message_stop", r#"{"type":"message_stop"}"#), ]); stream_capture( - adapter().with_name("kimi"), + adapter().with_name("moonshot"), &base_request("kimi-test"), &sse, ) .await } -/// A custom-named (Kimi) route authenticates with a bearer token and sends no -/// `anthropic-version` header. This pins that route shape on the wire. +/// A custom-named Moonshot route authenticates with a bearer token and sends +/// no `anthropic-version` header. This pins that route shape on the wire. #[tokio::test] async fn custom_named_stream_route() { let (capture, _) = custom_named_stream_capture().await; @@ -903,7 +903,7 @@ async fn custom_named_stream_error_identity() { ), ]); let (_capture, events) = stream_capture( - adapter().with_name("kimi"), + adapter().with_name("moonshot"), &base_request("kimi-test"), &sse, ) diff --git a/lib/components/fabro-llm/tests/it/wire/openai_compatible.rs b/lib/components/fabro-llm/tests/it/wire/openai_compatible.rs index 1074cc222..0ff337cc5 100644 --- a/lib/components/fabro-llm/tests/it/wire/openai_compatible.rs +++ b/lib/components/fabro-llm/tests/it/wire/openai_compatible.rs @@ -253,7 +253,7 @@ async fn encode_kimi_k3_uses_catalog_reasoning_and_sampling_controls() { ..base_request(MODEL) }; let capture = encode_capture_with(&request, move |adapter| { - adapter.with_name("kimi").with_catalog(catalog) + adapter.with_name("moonshot").with_catalog(catalog) }) .await; @@ -398,9 +398,9 @@ async fn encode_openrouter_claude_auto_cache_opt_out() { async fn encode_provider_options_keyed_by_adapter_name() { let request = corpus_provider_options( MODEL, - serde_json::json!({"kimi": {"repetition_penalty": 1.2}}), + serde_json::json!({"moonshot": {"repetition_penalty": 1.2}}), ); - let capture = encode_capture_with(&request, |adapter| adapter.with_name("kimi")).await; + let capture = encode_capture_with(&request, |adapter| adapter.with_name("moonshot")).await; fabro_test::fabro_json_snapshot!(capture.body); } @@ -411,7 +411,7 @@ async fn encode_provider_options_other_namespace_ignored() { MODEL, serde_json::json!({"openai": {"repetition_penalty": 1.2}}), ); - let capture = encode_capture_with(&request, |adapter| adapter.with_name("kimi")).await; + let capture = encode_capture_with(&request, |adapter| adapter.with_name("moonshot")).await; fabro_test::fabro_json_snapshot!(capture.body); } @@ -904,11 +904,11 @@ async fn stream_without_done_or_content_synthesizes_nothing() { async fn custom_named_complete_identity() { let server = MockServer::start(); let (mock, _slot) = mount_capture(&server, "/chat/completions", minimal_body()); - let adapter = adapter(&server).with_name("kimi"); + let adapter = adapter(&server).with_name("moonshot"); let response = adapter .complete(&base_request(MODEL)) .await .expect("complete should succeed"); mock.assert(); - assert_eq!(response.provider, "kimi"); + assert_eq!(response.provider, "moonshot"); } diff --git a/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_error_identity.snap b/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_error_identity.snap index 9fb304d73..d9e90487b 100644 --- a/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_error_identity.snap +++ b/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_error_identity.snap @@ -7,7 +7,7 @@ expression: rendered "type": "stream_start" }, { - "stream_item_error": "Server error from kimi: Overloaded", + "stream_item_error": "Server error from moonshot: Overloaded", "retryable": true, "failover_eligible": true } diff --git a/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_identity.snap b/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_identity.snap index 6bb0e2544..83360e789 100644 --- a/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_identity.snap +++ b/lib/components/fabro-llm/tests/it/wire/snapshots/it__wire__anthropic__custom_named_stream_identity.snap @@ -32,7 +32,7 @@ expression: rendered "response": { "id": "msg_kimi", "model": "kimi-test", - "provider": "kimi", + "provider": "moonshot", "message": { "role": "assistant", "content": [ diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index acc35a52f..099f568cd 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -3348,7 +3348,7 @@ enabled = true let policy = ModelFallbackPolicy::new(std::collections::BTreeMap::from([( "kimi-k3".to_string(), vec![ - FallbackTarget::new("kimi", "kimi-k3"), + FallbackTarget::new("moonshot", "kimi-k3"), FallbackTarget::new("openrouter", "kimi-k3"), FallbackTarget::new("anthropic", "claude-opus-5"), ], diff --git a/lib/components/fabro-workflow/src/model_fallback.rs b/lib/components/fabro-workflow/src/model_fallback.rs index b795cb6d5..f6de2fcd4 100644 --- a/lib/components/fabro-workflow/src/model_fallback.rs +++ b/lib/components/fabro-workflow/src/model_fallback.rs @@ -474,7 +474,7 @@ enabled = true let eligible = [ProviderId::new("openrouter")]; let configured = BTreeMap::from([( "kimi-k3".to_string(), - references(&["kimi:kimi-k3", "openrouter:kimi-k3"]), + references(&["moonshot:kimi-k3", "openrouter:kimi-k3"]), )]); let resolved = resolve_model_fallbacks(&catalog, &eligible, &configured).unwrap(); @@ -491,7 +491,7 @@ enabled = true requested_model, provider, .. - }] if requested_model == "kimi-k3" && provider == &ProviderId::new("kimi") + }] if requested_model == "kimi-k3" && provider == &ProviderId::new("moonshot") )); } @@ -512,13 +512,13 @@ enabled = true }; let eligible = [ ProviderId::new("modal"), - ProviderId::new("kimi"), + ProviderId::new("moonshot"), ProviderId::new("openrouter"), ]; let configured = BTreeMap::from([ ( "kimi-k3".to_string(), - references(&["kimi:kimi-k3", "openrouter:kimi-k3", "claude-opus"]), + references(&["moonshot:kimi-k3", "openrouter:kimi-k3", "claude-opus"]), ), ("glm-5.2".to_string(), references(&["gpt-sol"])), ("gpt-sol".to_string(), references(&["claude-opus"])), @@ -541,7 +541,7 @@ enabled = true .expect("requested model should have a chain") }; assert_eq!(chain("kimi-k3"), [ - FallbackTarget::new("kimi", "kimi-k3"), + FallbackTarget::new("moonshot", "kimi-k3"), FallbackTarget::new("openrouter", "kimi-k3"), FallbackTarget::new("openrouter", "claude-opus-5"), ]); diff --git a/lib/foundation/fabro-auth/src/resolve.rs b/lib/foundation/fabro-auth/src/resolve.rs index 4f6ada34b..dd12d1a1e 100644 --- a/lib/foundation/fabro-auth/src/resolve.rs +++ b/lib/foundation/fabro-auth/src/resolve.rs @@ -630,6 +630,60 @@ base_url = "https://example--kimi-k3.modal.run/v1" ); } + #[tokio::test] + async fn resolve_moonshot_api_request_prefers_moonshot_env_key() { + let dir = tempfile::tempdir().unwrap(); + let vault = Vault::load(dir.path().join("secrets.json")).unwrap(); + let resolver = test_resolver( + vault, + Arc::new(|name| match name { + EnvVars::MOONSHOT_API_KEY => Some("moonshot-key".to_string()), + EnvVars::KIMI_API_KEY => Some("kimi-key".to_string()), + _ => None, + }), + ); + + let resolved = resolver + .resolve( + ProviderId::new("moonshot"), + CredentialUsage::ApiRequest, + &default_catalog(), + ) + .await + .unwrap(); + + let ResolvedCredential::Api(api) = resolved; + assert_eq!( + api.auth_header, + Some(ApiKeyHeader::Bearer("moonshot-key".to_string())) + ); + } + + #[tokio::test] + async fn resolve_moonshot_api_request_falls_back_to_kimi_env_key() { + let dir = tempfile::tempdir().unwrap(); + let vault = Vault::load(dir.path().join("secrets.json")).unwrap(); + let resolver = test_resolver( + vault, + Arc::new(|name| (name == EnvVars::KIMI_API_KEY).then(|| "kimi-key".to_string())), + ); + + let resolved = resolver + .resolve( + ProviderId::new("moonshot"), + CredentialUsage::ApiRequest, + &default_catalog(), + ) + .await + .unwrap(); + + let ResolvedCredential::Api(api) = resolved; + assert_eq!( + api.auth_header, + Some(ApiKeyHeader::Bearer("kimi-key".to_string())) + ); + } + #[tokio::test] async fn resolve_openai_api_request_falls_back_to_codex_oauth_credential() { let dir = tempfile::tempdir().unwrap(); diff --git a/lib/foundation/fabro-config/src/layers/llm.rs b/lib/foundation/fabro-config/src/layers/llm.rs index 1e49fe1c9..298968a62 100644 --- a/lib/foundation/fabro-config/src/layers/llm.rs +++ b/lib/foundation/fabro-config/src/layers/llm.rs @@ -3,16 +3,23 @@ //! Holds the trusted, mergeable LLM provider/model catalog data: //! //! ```toml -//! [llm.providers.kimi] -//! display_name = "Kimi" +//! [llm.providers.moonshot] +//! display_name = "Moonshot AI" //! adapter = "openai_compatible" //! base_url = "https://api.moonshot.ai/v1" -//! auth = { credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] } //! priority = 60 //! enabled = true -//! aliases = ["moonshot"] +//! aliases = ["moonshot-ai"] //! -//! [llm.providers.kimi.models."kimi-k2.5"] +//! [llm.providers.moonshot.auth] +//! credentials = [ +//! "env:MOONSHOT_API_KEY", +//! "env:KIMI_API_KEY", +//! "vault:MOONSHOT_API_KEY", +//! "vault:KIMI_API_KEY", +//! ] +//! +//! [llm.providers.moonshot.models."kimi-k2.5"] //! ... //! ``` //! @@ -489,33 +496,46 @@ agent_profile = "gemini" #[test] fn parses_minimal_provider_entry() { let toml = r#" -[providers.kimi] -display_name = "Kimi" +[providers.moonshot] +display_name = "Moonshot AI" adapter = "openai_compatible" agent_profile = "openai" base_url = "https://api.moonshot.ai/v1" priority = 60 enabled = true -aliases = ["moonshot"] +aliases = ["moonshot-ai"] -[providers.kimi.auth] -credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] +[providers.moonshot.auth] +credentials = [ + "env:MOONSHOT_API_KEY", + "env:KIMI_API_KEY", + "vault:MOONSHOT_API_KEY", + "vault:KIMI_API_KEY", +] "#; let layer: LlmLayer = toml::from_str(toml).unwrap(); - let kimi = layer.providers.get("kimi").unwrap(); - assert_eq!(kimi.display_name.as_deref(), Some("Kimi")); - assert_eq!(kimi.adapter.as_deref(), Some("openai_compatible")); - assert_eq!(kimi.agent_profile, Some(AgentProfileKind::OpenAi)); - let auth = kimi.auth.as_ref().expect("expected api_key auth"); + let moonshot = layer.providers.get("moonshot").unwrap(); + assert_eq!(moonshot.display_name.as_deref(), Some("Moonshot AI")); + assert_eq!(moonshot.adapter.as_deref(), Some("openai_compatible")); + assert_eq!(moonshot.agent_profile, Some(AgentProfileKind::OpenAi)); + let auth = moonshot.auth.as_ref().expect("expected api_key auth"); assert_eq!(auth.header, ApiKeyHeaderPolicy::Bearer); assert_eq!(auth.credentials, vec![ + CredentialRef::Env("MOONSHOT_API_KEY".to_string()), CredentialRef::Env("KIMI_API_KEY".to_string()), + CredentialRef::Vault("MOONSHOT_API_KEY".to_string()), CredentialRef::Vault("KIMI_API_KEY".to_string()), ]); - assert_eq!(kimi.base_url.as_deref(), Some("https://api.moonshot.ai/v1")); - assert_eq!(kimi.priority, Some(60)); - assert_eq!(kimi.enabled, Some(true)); - assert_eq!(kimi.aliases.as_deref(), Some(&["moonshot".to_string()][..])); + assert_eq!( + moonshot.base_url.as_deref(), + Some("https://api.moonshot.ai/v1") + ); + assert_eq!(moonshot.priority, Some(60)); + assert_eq!(moonshot.enabled, Some(true)); + assert_eq!( + moonshot.aliases.as_deref(), + Some(&["moonshot-ai".to_string()][..]) + ); } #[test] @@ -584,7 +604,7 @@ x-portkey-api-key = "sk-portkey-literal" fn parses_full_model_entry() { let toml = r#" [models."kimi-k2.5"] -provider = "kimi" +provider = "moonshot" api_id = "kimi-k2.5" display_name = "Kimi K2.5" family = "kimi" @@ -611,7 +631,7 @@ cache_input_cost_per_mtok = 0.15 "#; let layer: LlmLayer = toml::from_str(toml).unwrap(); let m = layer.models.get("kimi-k2.5").unwrap(); - assert_eq!(m.provider.as_deref(), Some("kimi")); + assert_eq!(m.provider.as_deref(), Some("moonshot")); assert_eq!(m.api_id.as_deref(), Some("kimi-k2.5")); assert_eq!(m.display_name.as_deref(), Some("Kimi K2.5")); assert_eq!(m.family.as_deref(), Some("kimi")); @@ -714,7 +734,7 @@ cache_input_cost_per_mtok = 9.0 #[test] fn rejects_unknown_provider_field() { let toml = r#" -[providers.kimi] +[providers.moonshot] adapter = "openai_compatible" unknown_field = true "#; @@ -725,7 +745,7 @@ unknown_field = true #[test] fn rejects_removed_provider_base_url_env_field() { let toml = r#" -[providers.kimi] +[providers.moonshot] adapter = "openai_compatible" base_url_env = "KIMI_BASE_URL" "#; @@ -902,7 +922,7 @@ mystery = 1 fn merge_map_field_merges_per_provider_id() { let mut high_map: std::collections::HashMap = std::collections::HashMap::new(); - high_map.insert("kimi".to_string(), ProviderSettings { + high_map.insert("moonshot".to_string(), ProviderSettings { base_url: Some("https://override".to_string()), ..ProviderSettings::default() }); @@ -910,7 +930,7 @@ mystery = 1 let mut low_map: std::collections::HashMap = std::collections::HashMap::new(); - low_map.insert("kimi".to_string(), ProviderSettings { + low_map.insert("moonshot".to_string(), ProviderSettings { adapter: Some("openai_compatible".to_string()), base_url: Some("https://defaults".to_string()), ..ProviderSettings::default() @@ -918,9 +938,9 @@ mystery = 1 let low: MergeMap = MergeMap::from(low_map); let merged = high.combine(low); - let kimi = merged.get("kimi").unwrap(); - assert_eq!(kimi.adapter.as_deref(), Some("openai_compatible")); - assert_eq!(kimi.base_url.as_deref(), Some("https://override")); + let moonshot = merged.get("moonshot").unwrap(); + assert_eq!(moonshot.adapter.as_deref(), Some("openai_compatible")); + assert_eq!(moonshot.base_url.as_deref(), Some("https://override")); } #[test] diff --git a/lib/foundation/fabro-config/src/parse.rs b/lib/foundation/fabro-config/src/parse.rs index 47b88b728..8ed2273bd 100644 --- a/lib/foundation/fabro-config/src/parse.rs +++ b/lib/foundation/fabro-config/src/parse.rs @@ -292,15 +292,15 @@ mod tests { #[test] fn accepts_new_llm_providers_subtree() { - let parsed = "[llm.providers.kimi]\nadapter = \"openai_compatible\"\n" + let parsed = "[llm.providers.moonshot]\nadapter = \"openai_compatible\"\n" .parse::() .unwrap(); - assert!(parsed.llm.unwrap().providers.contains_key("kimi")); + assert!(parsed.llm.unwrap().providers.contains_key("moonshot")); } #[test] fn accepts_new_llm_models_subtree() { - let parsed = "[llm.providers.kimi.models.\"foo\"]\n" + let parsed = "[llm.providers.moonshot.models.\"foo\"]\n" .parse::() .unwrap(); assert!( @@ -308,7 +308,7 @@ mod tests { .llm .unwrap() .providers - .get("kimi") + .get("moonshot") .unwrap() .models .contains_key("foo") diff --git a/lib/foundation/fabro-model/src/billing.rs b/lib/foundation/fabro-model/src/billing.rs index 6e4de19f0..f1c5867fa 100644 --- a/lib/foundation/fabro-model/src/billing.rs +++ b/lib/foundation/fabro-model/src/billing.rs @@ -1414,9 +1414,10 @@ output_cost_per_mtok = 2.0 #[test] fn old_provider_tagged_pricing_policy_is_rejected() { - let error = - serde_json::from_value::(openai_pricing_json("provider", "kimi")) - .unwrap_err(); + let error = serde_json::from_value::(openai_pricing_json( + "provider", "moonshot", + )) + .unwrap_err(); assert!(error.to_string().contains("algorithm")); } diff --git a/lib/foundation/fabro-model/src/catalog.rs b/lib/foundation/fabro-model/src/catalog.rs index 82878ab1b..a941cc5b7 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -3743,7 +3743,7 @@ enabled = true assert_eq!(provider.codec, CodecKind::OpenAiCompatible); assert_eq!(provider.agent_profile, AgentProfileKind::Kimi); assert_eq!(provider.billing_policy, BillingPolicy::OpenAi); - assert_eq!(provider.priority, 30); + assert_eq!(provider.priority, 75); assert!(provider.auth.is_none()); assert_eq!( provider.extra_headers, @@ -3858,8 +3858,28 @@ enabled = true } #[test] - fn builtin_kimi_k3_selection_prefers_direct_kimi_then_modal_over_openrouter() { - let kimi = ProviderId::new("kimi"); + fn builtin_moonshot_provider_prefers_moonshot_api_key() { + let moonshot = ProviderId::new("moonshot"); + let catalog = Catalog::builtin(); + let provider = catalog + .provider(&moonshot) + .expect("Moonshot provider should be present"); + + assert_eq!(provider.auth.as_ref().unwrap().credentials, vec![ + CredentialRef::Env("MOONSHOT_API_KEY".to_string()), + CredentialRef::Env("KIMI_API_KEY".to_string()), + CredentialRef::Vault("MOONSHOT_API_KEY".to_string()), + CredentialRef::Vault("KIMI_API_KEY".to_string()), + ]); + assert_eq!( + catalog.provider_vault_secret_name(&moonshot), + Some("MOONSHOT_API_KEY") + ); + } + + #[test] + fn builtin_kimi_k3_selection_prefers_modal_then_moonshot_over_openrouter() { + let moonshot = ProviderId::new("moonshot"); let modal = ProviderId::new("modal"); let openrouter = ProviderId::new("openrouter"); let catalog = Catalog::from_builtin_with_overrides(&minimal_settings( @@ -3877,10 +3897,19 @@ enabled = true .select( "kimi-k3", None, - &HashSet::from([kimi.clone(), modal.clone(), openrouter.clone()]), + &HashSet::from([moonshot.clone(), modal.clone(), openrouter.clone()]), ) - .expect("direct Kimi should win portable Kimi K3 selection"); - assert_eq!(selected.provider, kimi); + .expect("Modal should win portable Kimi K3 selection"); + assert_eq!(selected.provider, modal); + + let selected = catalog + .select( + "kimi-k3", + None, + &HashSet::from([moonshot.clone(), openrouter.clone()]), + ) + .expect("Moonshot should win when Modal is unavailable"); + assert_eq!(selected.provider, moonshot); let selected = catalog .select("kimi-k3", None, &HashSet::from([modal.clone(), openrouter])) @@ -4405,7 +4434,7 @@ enabled = true ); assert_eq!( catalog - .provider(&ProviderId::new("kimi")) + .provider(&ProviderId::new("moonshot")) .unwrap() .base_url .as_deref(), @@ -4464,7 +4493,7 @@ enabled = true ("anthropic", CodecKind::AnthropicMessages), ("openai", CodecKind::OpenAiResponses), ("gemini", CodecKind::GeminiGenerate), - ("kimi", CodecKind::OpenAiCompatible), + ("moonshot", CodecKind::OpenAiCompatible), ] { let provider_id = ProviderId::new(provider); assert_eq!(catalog.provider(&provider_id).unwrap().codec, expected); @@ -6920,7 +6949,7 @@ sampling_params = false insta::assert_debug_snapshot!(m, @r#" Model { id: "kimi-k2.5", - provider: kimi, + provider: moonshot, family: "kimi-k2", display_name: "Kimi K2.5", limits: ModelLimits { @@ -6976,7 +7005,7 @@ sampling_params = false insta::assert_debug_snapshot!(m, @r#" Model { id: "kimi-k3", - provider: kimi, + provider: moonshot, family: "kimi-k3", display_name: "Kimi K3", limits: ModelLimits { @@ -7327,11 +7356,11 @@ context_window = 1050000 } #[test] - fn closest_model_haiku_to_kimi() { + fn closest_model_haiku_to_moonshot() { let haiku = Catalog::builtin().get("claude-haiku-4-5").unwrap(); assert!( Catalog::builtin() - .closest(&ProviderId::new("kimi"), haiku) + .closest(&ProviderId::new("moonshot"), haiku) .is_none() ); } diff --git a/lib/foundation/fabro-model/src/catalog/providers/modal.toml b/lib/foundation/fabro-model/src/catalog/providers/modal.toml index 332dd9bfc..d00e40bd1 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/modal.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/modal.toml @@ -3,7 +3,7 @@ display_name = "Modal" adapter = "openai_compatible" agent_profile = "kimi" api_key_url = "https://modal.com/docs/guide/endpoints#proxy-tokens" -priority = 30 +priority = 75 enabled = false [providers.modal.extra_headers] diff --git a/lib/foundation/fabro-model/src/catalog/providers/kimi.toml b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml similarity index 64% rename from lib/foundation/fabro-model/src/catalog/providers/kimi.toml rename to lib/foundation/fabro-model/src/catalog/providers/moonshot.toml index 7826caab9..aaa4d7ec4 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/kimi.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml @@ -1,26 +1,31 @@ -[providers.kimi] -display_name = "Kimi" +[providers.moonshot] +display_name = "Moonshot AI" adapter = "openai_compatible" agent_profile = "kimi" api_key_url = "https://platform.kimi.ai/console/api-keys" base_url = "https://api.moonshot.ai/v1" priority = 70 -[providers.kimi.auth] -credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] +[providers.moonshot.auth] +credentials = [ + "env:MOONSHOT_API_KEY", + "env:KIMI_API_KEY", + "vault:MOONSHOT_API_KEY", + "vault:KIMI_API_KEY", +] -[providers.kimi.models."kimi-k2.5"] +[providers.moonshot.models."kimi-k2.5"] display_name = "Kimi K2.5" family = "kimi-k2" training = "2025-10-01" knowledge_cutoff = "October 2025" estimated_output_tps = 50 -[providers.kimi.models."kimi-k2.5".limits] +[providers.moonshot.models."kimi-k2.5".limits] context_window = 262144 max_output = 32768 -[providers.kimi.models."kimi-k2.5".features] +[providers.moonshot.models."kimi-k2.5".features] tools = true vision = true reasoning = true @@ -28,24 +33,24 @@ reasoning_by_default = true prompt_cache = true sampling_params = false -[providers.kimi.models."kimi-k2.5".costs] +[providers.moonshot.models."kimi-k2.5".costs] input_cost_per_mtok = 0.6 output_cost_per_mtok = 3.0 cache_input_cost_per_mtok = 0.1 -[providers.kimi.models."kimi-k3"] +[providers.moonshot.models."kimi-k3"] display_name = "Kimi K3" family = "kimi-k3" default = true aliases = ["kimi"] -[providers.kimi.models."kimi-k3".limits] +[providers.moonshot.models."kimi-k3".limits] context_window = 1048576 # K3 accepts explicit completion budgets up to 1048576, but Fabro also uses # max_output as the default request budget. Match Kimi's 131072-token default. max_output = 131072 -[providers.kimi.models."kimi-k3".features] +[providers.moonshot.models."kimi-k3".features] tools = true vision = true reasoning = true @@ -53,10 +58,10 @@ reasoning_effort = "always_adaptive" prompt_cache = true sampling_params = false -[providers.kimi.models."kimi-k3".controls] +[providers.moonshot.models."kimi-k3".controls] reasoning_effort = ["low", "high", "max"] -[providers.kimi.models."kimi-k3".costs] +[providers.moonshot.models."kimi-k3".costs] input_cost_per_mtok = 3.0 output_cost_per_mtok = 15.0 cache_input_cost_per_mtok = 0.3 diff --git a/lib/foundation/fabro-model/src/ids.rs b/lib/foundation/fabro-model/src/ids.rs index 02d6db0d7..b46d89cfe 100644 --- a/lib/foundation/fabro-model/src/ids.rs +++ b/lib/foundation/fabro-model/src/ids.rs @@ -174,9 +174,9 @@ mod tests { #[test] fn provider_id_is_transparent_string_in_json() { - let id = ProviderId::new("kimi"); + let id = ProviderId::new("moonshot"); let json = serde_json::to_string(&id).unwrap(); - assert_eq!(json, "\"kimi\""); + assert_eq!(json, "\"moonshot\""); let back: ProviderId = serde_json::from_str(&json).unwrap(); assert_eq!(back, id); } diff --git a/lib/foundation/fabro-static/src/env_vars.rs b/lib/foundation/fabro-static/src/env_vars.rs index fc5d2312a..bb8c5d078 100644 --- a/lib/foundation/fabro-static/src/env_vars.rs +++ b/lib/foundation/fabro-static/src/env_vars.rs @@ -57,6 +57,7 @@ impl EnvVars { pub const INCEPTION_API_KEY: &'static str = "INCEPTION_API_KEY"; pub const KIMI_API_KEY: &'static str = "KIMI_API_KEY"; pub const MINIMAX_API_KEY: &'static str = "MINIMAX_API_KEY"; + pub const MOONSHOT_API_KEY: &'static str = "MOONSHOT_API_KEY"; pub const MODAL_TOKEN_ID: &'static str = "MODAL_TOKEN_ID"; pub const MODAL_TOKEN_SECRET: &'static str = "MODAL_TOKEN_SECRET"; pub const OPENAI_API_KEY: &'static str = "OPENAI_API_KEY"; @@ -208,6 +209,7 @@ mod tests { EnvVars::INCEPTION_API_KEY, EnvVars::KIMI_API_KEY, EnvVars::MINIMAX_API_KEY, + EnvVars::MOONSHOT_API_KEY, EnvVars::OPENAI_API_KEY, EnvVars::OPENAI_BASE_URL, EnvVars::OPENAI_ORGANIZATION, diff --git a/lib/foundation/fabro-static/src/secret_registry.rs b/lib/foundation/fabro-static/src/secret_registry.rs index 3bfbf92be..22cf26177 100644 --- a/lib/foundation/fabro-static/src/secret_registry.rs +++ b/lib/foundation/fabro-static/src/secret_registry.rs @@ -31,6 +31,7 @@ const OPTIONAL_VAULT_SECRETS: &[&str] = &[ EnvVars::INCEPTION_API_KEY, EnvVars::KIMI_API_KEY, EnvVars::MINIMAX_API_KEY, + EnvVars::MOONSHOT_API_KEY, EnvVars::OPENAI_API_KEY, EnvVars::OPENROUTER_API_KEY, EnvVars::POOLSIDE_API_KEY, @@ -99,6 +100,7 @@ mod tests { EnvVars::INCEPTION_API_KEY, EnvVars::KIMI_API_KEY, EnvVars::MINIMAX_API_KEY, + EnvVars::MOONSHOT_API_KEY, EnvVars::OPENAI_API_KEY, EnvVars::OPENROUTER_API_KEY, EnvVars::POOLSIDE_API_KEY,