From a2b64dea2a6d2a4694f405ff1685bd4901f57680 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 31 Jul 2026 08:44:43 -0400 Subject: [PATCH 1/4] fix(model): prioritize Modal over Kimi --- lib/foundation/fabro-model/src/catalog.rs | 15 ++++++++++++--- .../fabro-model/src/catalog/providers/modal.toml | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/foundation/fabro-model/src/catalog.rs b/lib/foundation/fabro-model/src/catalog.rs index 21aa04888..5cf218a34 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -3580,7 +3580,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, @@ -3695,7 +3695,7 @@ enabled = true } #[test] - fn builtin_kimi_k3_selection_prefers_direct_kimi_then_modal_over_openrouter() { + fn builtin_kimi_k3_selection_prefers_modal_then_direct_kimi_over_openrouter() { let kimi = ProviderId::new("kimi"); let modal = ProviderId::new("modal"); let openrouter = ProviderId::new("openrouter"); @@ -3716,7 +3716,16 @@ enabled = true None, &HashSet::from([kimi.clone(), modal.clone(), openrouter.clone()]), ) - .expect("direct Kimi should win portable Kimi K3 selection"); + .expect("Modal should win portable Kimi K3 selection"); + assert_eq!(selected.provider, modal); + + let selected = catalog + .select( + "kimi-k3", + None, + &HashSet::from([kimi.clone(), openrouter.clone()]), + ) + .expect("direct Kimi should win when Modal is unavailable"); assert_eq!(selected.provider, kimi); let selected = catalog 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] From d4cefb16c937b45a60c4bd39a24198c8455ef295 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 31 Jul 2026 08:47:00 -0400 Subject: [PATCH 2/4] fix(web): update Modal and OpenRouter logos Render Modal with the theme color and replace the legacy OpenRouter mark with the current 2026 brand glyph. --- apps/fabro-web/public/images/providers/modal.svg | 2 +- apps/fabro-web/public/images/providers/openrouter.svg | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) 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/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 @@ - - - - - + + From 24f9ac6bd18787c65ec4c4188413c88475bba948 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 31 Jul 2026 09:00:52 -0400 Subject: [PATCH 3/4] refactor(model): rename Kimi provider to Moonshot --- .../providers/{kimi.svg => moonshot.svg} | 7 ++- .../administration/server-configuration.mdx | 2 +- docs/public/changelog/2026-02-28.mdx | 2 +- docs/public/changelog/2026-07-22.mdx | 2 +- docs/public/core-concepts/models.mdx | 6 +- docs/public/execution/run-configuration.mdx | 4 +- docs/public/reference/sdk.mdx | 2 +- lib/apps/fabro-cli/src/commands/install.rs | 2 +- lib/apps/fabro-cli/src/commands/model.rs | 2 +- .../fabro-cli/src/shared/provider_auth.rs | 2 +- lib/apps/fabro-server/src/run_manifest.rs | 41 +++++++------ lib/apps/fabro-server/src/server/tests.rs | 14 ++--- lib/apps/fabro-server/tests/it/api/install.rs | 14 +++-- lib/components/fabro-agent/src/compaction.rs | 6 +- .../fabro-agent/src/profiles/kimi.rs | 6 +- .../fabro-agent/src/profiles/mod.rs | 2 +- .../fabro-agent/src/profiles/openai.rs | 8 +-- ...t_file_and_web_search_prompt_snapshot.snap | 2 +- ...sts__openai_edit_file_prompt_snapshot.snap | 2 +- .../fabro-agent/tests/it/parity_matrix.rs | 14 ++--- .../fabro-llm/src/adapter_registry.rs | 6 +- lib/components/fabro-llm/src/client.rs | 17 +++--- lib/components/fabro-llm/src/codec/mod.rs | 2 +- .../src/codec/openai_compatible/mod.rs | 2 +- .../src/codec/openai_compatible/request.rs | 10 ++-- .../fabro-llm/src/providers/anthropic.rs | 9 +-- lib/components/fabro-llm/tests/integration.rs | 2 +- .../fabro-llm/tests/it/wire/anthropic.rs | 14 ++--- .../tests/it/wire/openai_compatible.rs | 12 ++-- ...c__custom_named_stream_error_identity.snap | 2 +- ...thropic__custom_named_stream_identity.snap | 2 +- .../fabro-workflow/src/handler/llm/api.rs | 2 +- .../fabro-workflow/src/model_fallback.rs | 10 ++-- lib/foundation/fabro-config/src/layers/llm.rs | 58 ++++++++++--------- lib/foundation/fabro-config/src/parse.rs | 8 +-- lib/foundation/fabro-model/src/billing.rs | 7 ++- lib/foundation/fabro-model/src/catalog.rs | 24 ++++---- .../providers/{kimi.toml => moonshot.toml} | 24 ++++---- lib/foundation/fabro-model/src/ids.rs | 4 +- 39 files changed, 186 insertions(+), 169 deletions(-) rename apps/fabro-web/public/images/providers/{kimi.svg => moonshot.svg} (98%) rename lib/foundation/fabro-model/src/catalog/providers/{kimi.toml => moonshot.toml} (70%) 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/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index c806406f7..23013c749 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 | +| `KIMI_API_KEY` | Moonshot AI | | `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 a4befa37f..47be54ee4 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 | | `laguna-s-2.1` | poolside | `laguna`, `laguna-s` | 1M | $0.10 / $0.20 | n/a | | `laguna-xs-2.1` | poolside | `laguna-xs` | 262K | $0.10 / $0.20 | n/a | | `glm-5.2` | zai | `glm`, `glm5`, `glm52`, `glm5.2` | 1M | $1.40 / $4.40 | n/a | @@ -227,7 +227,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 feeea0c0a..929840cd5 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 | +| `KIMI_API_KEY` | Moonshot AI | | `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 3486b35f5..2db9d4470 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-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index fd9a1f50c..ac3b94071 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -1625,7 +1625,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( @@ -1637,13 +1637,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] @@ -1686,15 +1685,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}" @@ -1706,7 +1705,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() @@ -1723,7 +1722,7 @@ enabled = true .unwrap_or_default(); ready_providers.sort(); assert_eq!(ready_providers, vec![ - ProviderId::new("kimi"), + ProviderId::new("moonshot"), ProviderId::new("openrouter") ]); @@ -2748,7 +2747,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; @@ -2772,18 +2771,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; @@ -2802,10 +2801,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 b3da5be63..cbe469354 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -7098,7 +7098,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(); @@ -15910,7 +15910,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); @@ -15923,7 +15923,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, @@ -15982,7 +15982,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); @@ -15993,7 +15993,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": [{ @@ -16122,7 +16122,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); @@ -16133,7 +16133,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 26ff6d70b..1a09a31e7 100644 --- a/lib/components/fabro-llm/src/adapter_registry.rs +++ b/lib/components/fabro-llm/src/adapter_registry.rs @@ -425,10 +425,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] @@ -489,7 +489,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 3c80ef796..8362d101b 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 1294e2e8a..490aff18a 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 5dad09d34..e3245eed6 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -3330,7 +3330,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-config/src/layers/llm.rs b/lib/foundation/fabro-config/src/layers/llm.rs index 1e49fe1c9..d232c40b7 100644 --- a/lib/foundation/fabro-config/src/layers/llm.rs +++ b/lib/foundation/fabro-config/src/layers/llm.rs @@ -3,16 +3,16 @@ //! 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.models."kimi-k2.5"] //! ... //! ``` //! @@ -489,33 +489,39 @@ 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] +[providers.moonshot.auth] credentials = ["env:KIMI_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("KIMI_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 +590,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 +617,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 +720,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 +731,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 +908,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 +916,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 +924,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 5cf218a34..f0d8719f4 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -3695,8 +3695,8 @@ enabled = true } #[test] - fn builtin_kimi_k3_selection_prefers_modal_then_direct_kimi_over_openrouter() { - let kimi = ProviderId::new("kimi"); + 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( @@ -3714,7 +3714,7 @@ enabled = true .select( "kimi-k3", None, - &HashSet::from([kimi.clone(), modal.clone(), openrouter.clone()]), + &HashSet::from([moonshot.clone(), modal.clone(), openrouter.clone()]), ) .expect("Modal should win portable Kimi K3 selection"); assert_eq!(selected.provider, modal); @@ -3723,10 +3723,10 @@ enabled = true .select( "kimi-k3", None, - &HashSet::from([kimi.clone(), openrouter.clone()]), + &HashSet::from([moonshot.clone(), openrouter.clone()]), ) - .expect("direct Kimi should win when Modal is unavailable"); - assert_eq!(selected.provider, kimi); + .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])) @@ -4214,7 +4214,7 @@ enabled = true ); assert_eq!( catalog - .provider(&ProviderId::new("kimi")) + .provider(&ProviderId::new("moonshot")) .unwrap() .base_url .as_deref(), @@ -4273,7 +4273,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); @@ -6729,7 +6729,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 { @@ -6785,7 +6785,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 { @@ -7136,11 +7136,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/kimi.toml b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml similarity index 70% rename from lib/foundation/fabro-model/src/catalog/providers/kimi.toml rename to lib/foundation/fabro-model/src/catalog/providers/moonshot.toml index 7826caab9..ddc1633e4 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/kimi.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml @@ -1,26 +1,26 @@ -[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] +[providers.moonshot.auth] credentials = ["env:KIMI_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 +28,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 +53,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); } From fd55ff03adca311e7d24524b215f836f82a42f4b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 31 Jul 2026 09:14:58 -0400 Subject: [PATCH 4/4] fix(model): prefer MOONSHOT_API_KEY Keep KIMI_API_KEY as a legacy fallback for env and vault credentials. --- .env.example | 2 +- .../administration/server-configuration.mdx | 2 +- docs/public/reference/sdk.mdx | 2 +- lib/apps/fabro-cli/tests/it/cmd/model_test.rs | 1 + lib/foundation/fabro-auth/src/resolve.rs | 54 +++++++++++++++++++ lib/foundation/fabro-config/src/layers/llm.rs | 18 ++++++- lib/foundation/fabro-model/src/catalog.rs | 20 +++++++ .../src/catalog/providers/moonshot.toml | 7 ++- lib/foundation/fabro-static/src/env_vars.rs | 2 + .../fabro-static/src/secret_registry.rs | 2 + 10 files changed, 104 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index ea69ab76b..1093d69ee 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ DAYTONA_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/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index 23013c749..2b526ada9 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` | Moonshot AI | +| `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/reference/sdk.mdx b/docs/public/reference/sdk.mdx index 929840cd5..402953e5c 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` | Moonshot AI | +| `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/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/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 d232c40b7..298968a62 100644 --- a/lib/foundation/fabro-config/src/layers/llm.rs +++ b/lib/foundation/fabro-config/src/layers/llm.rs @@ -7,11 +7,18 @@ //! 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-ai"] //! +//! [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"] //! ... //! ``` @@ -499,7 +506,12 @@ enabled = true aliases = ["moonshot-ai"] [providers.moonshot.auth] -credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] +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 moonshot = layer.providers.get("moonshot").unwrap(); @@ -509,7 +521,9 @@ credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] 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!( diff --git a/lib/foundation/fabro-model/src/catalog.rs b/lib/foundation/fabro-model/src/catalog.rs index f0d8719f4..5c4bfaa10 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -3694,6 +3694,26 @@ enabled = true ]); } + #[test] + 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"); diff --git a/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml index ddc1633e4..aaa4d7ec4 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/moonshot.toml @@ -7,7 +7,12 @@ base_url = "https://api.moonshot.ai/v1" priority = 70 [providers.moonshot.auth] -credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"] +credentials = [ + "env:MOONSHOT_API_KEY", + "env:KIMI_API_KEY", + "vault:MOONSHOT_API_KEY", + "vault:KIMI_API_KEY", +] [providers.moonshot.models."kimi-k2.5"] display_name = "Kimi K2.5" diff --git a/lib/foundation/fabro-static/src/env_vars.rs b/lib/foundation/fabro-static/src/env_vars.rs index 849ec6842..e6f00e3b9 100644 --- a/lib/foundation/fabro-static/src/env_vars.rs +++ b/lib/foundation/fabro-static/src/env_vars.rs @@ -56,6 +56,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"; @@ -206,6 +207,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 03dec8e12..8c823b112 100644 --- a/lib/foundation/fabro-static/src/secret_registry.rs +++ b/lib/foundation/fabro-static/src/secret_registry.rs @@ -30,6 +30,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, @@ -97,6 +98,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,