From 43a9a423d4daf9449a9383e4fc03c82ef0bbd701 Mon Sep 17 00:00:00 2001 From: Anderson Leal Date: Wed, 22 Jul 2026 13:18:55 -0300 Subject: [PATCH 1/4] feat(llm-router): add pure credential env-var fallback helper --- .../src/provider_scaffold/router_client.rs | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/llm-router/src/provider_scaffold/router_client.rs b/llm-router/src/provider_scaffold/router_client.rs index 3e07ee4db..802dd7b2e 100644 --- a/llm-router/src/provider_scaffold/router_client.rs +++ b/llm-router/src/provider_scaffold/router_client.rs @@ -1,8 +1,9 @@ //! Thin wrappers over the router's provider-protocol functions. All calls //! carry the registration token (identity binding, spec adaptation #1). //! `provider_id` is the caller's declared provider id (e.g. "anthropic"). +use crate::types::credential::Credential; use crate::types::model::Model; -use crate::types::router::ProviderResolveResponse; +use crate::types::router::{CredentialSource, ProviderResolveResponse}; use iii_sdk::errors::Error; use iii_sdk::protocol::TriggerRequest; use iii_sdk::IIIClient; @@ -70,3 +71,87 @@ pub async fn models_get(iii: &IIIClient, provider_id: &str, model_id: &str) -> O pub async fn register(iii: &IIIClient, declaration: Value) -> Result { call(iii, "router::provider::register", declaration).await } + +/// Inject an env-sourced ApiKey only when the router resolved nothing. +/// Pure: takes the already-read value so tests never touch process env. +fn with_api_key_fallback( + mut resp: ProviderResolveResponse, + key: Option, +) -> ProviderResolveResponse { + if resp.credential.is_some() { + return resp; // router / config credential always wins + } + if let Some(k) = key { + let k = k.trim(); + if !k.is_empty() { + resp.credential = Some(Credential::ApiKey { key: k.to_string() }); + resp.source = CredentialSource::Env; + resp.configured = true; + } + } + resp +} + +/// Read the provider's declared env var and apply the fallback. +pub fn apply_credential_env_fallback( + resp: ProviderResolveResponse, + credential_env_var: Option<&str>, +) -> ProviderResolveResponse { + let key = credential_env_var.and_then(|name| std::env::var(name).ok()); + with_api_key_fallback(resp, key) +} + +#[cfg(test)] +mod fallback_tests { + use crate::types::credential::Credential; + use crate::types::router::{CredentialSource, ProviderResolveResponse}; + use super::with_api_key_fallback; + + fn none_resp() -> ProviderResolveResponse { + ProviderResolveResponse { + configured: false, + source: CredentialSource::None, + credential: None, + api_url: None, + max_tokens: None, + } + } + + #[test] + fn router_credential_wins_over_env() { + let mut resp = none_resp(); + resp.credential = Some(Credential::ApiKey { key: "from-router".into() }); + resp.source = CredentialSource::Config; + resp.configured = true; + let out = with_api_key_fallback(resp, Some("from-env".into())); + assert_eq!(out.credential, Some(Credential::ApiKey { key: "from-router".into() })); + assert_eq!(out.source, CredentialSource::Config); + } + + #[test] + fn injects_env_when_router_has_none() { + let out = with_api_key_fallback(none_resp(), Some("sk-abc".into())); + assert_eq!(out.credential, Some(Credential::ApiKey { key: "sk-abc".into() })); + assert_eq!(out.source, CredentialSource::Env); + assert!(out.configured); + } + + #[test] + fn no_key_leaves_none() { + let out = with_api_key_fallback(none_resp(), None); + assert_eq!(out.credential, None); + assert!(!out.configured); + } + + #[test] + fn empty_and_whitespace_are_not_injected() { + assert_eq!(with_api_key_fallback(none_resp(), Some("".into())).credential, None); + assert_eq!(with_api_key_fallback(none_resp(), Some(" \n".into())).credential, None); + } + + #[test] + fn injected_key_is_trimmed() { + let out = with_api_key_fallback(none_resp(), Some(" sk-abc\n".into())); + assert_eq!(out.credential, Some(Credential::ApiKey { key: "sk-abc".into() })); + } +} From ce2539ec612d1e101fa2b0eba004e628ea5b3d01 Mon Sep 17 00:00:00 2001 From: Anderson Leal Date: Wed, 22 Jul 2026 13:25:50 -0300 Subject: [PATCH 2/4] refactor(providers): hoist credential env var into CREDENTIAL_ENV_VAR const --- provider-anthropic/src/register.rs | 14 +++++++++++++- provider-kimi/src/register.rs | 14 +++++++++++++- provider-llamacpp/src/register.rs | 14 +++++++++++++- provider-openai/src/register.rs | 14 +++++++++++++- provider-xai/src/register.rs | 14 +++++++++++++- provider-zai/src/register.rs | 14 +++++++++++++- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/provider-anthropic/src/register.rs b/provider-anthropic/src/register.rs index af29cef3e..d2cff315e 100644 --- a/provider-anthropic/src/register.rs +++ b/provider-anthropic/src/register.rs @@ -18,11 +18,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "ANTHROPIC_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("Anthropic".into()), - credential_env_var: Some("ANTHROPIC_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -210,4 +213,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("IMPORTANT: NEVER invent function ids")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "ANTHROPIC_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } diff --git a/provider-kimi/src/register.rs b/provider-kimi/src/register.rs index b7853741c..66954902e 100644 --- a/provider-kimi/src/register.rs +++ b/provider-kimi/src/register.rs @@ -17,11 +17,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "MOONSHOT_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("Kimi".into()), - credential_env_var: Some("MOONSHOT_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -184,4 +187,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("Never invent function ids")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "MOONSHOT_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } diff --git a/provider-llamacpp/src/register.rs b/provider-llamacpp/src/register.rs index 53135c770..75326acca 100644 --- a/provider-llamacpp/src/register.rs +++ b/provider-llamacpp/src/register.rs @@ -18,11 +18,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "LLAMACPP_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("llama.cpp".into()), - credential_env_var: Some("LLAMACPP_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -213,4 +216,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("IMPORTANT: NEVER invent function ids")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "LLAMACPP_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } diff --git a/provider-openai/src/register.rs b/provider-openai/src/register.rs index 1be350f72..d6a00cd7e 100644 --- a/provider-openai/src/register.rs +++ b/provider-openai/src/register.rs @@ -18,11 +18,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "OPENAI_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("OpenAI".into()), - credential_env_var: Some("OPENAI_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -223,4 +226,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("## Autonomy and persistence")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "OPENAI_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } diff --git a/provider-xai/src/register.rs b/provider-xai/src/register.rs index ef032da80..5cfb8c62b 100644 --- a/provider-xai/src/register.rs +++ b/provider-xai/src/register.rs @@ -18,11 +18,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "XAI_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("xAI".into()), - credential_env_var: Some("XAI_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -247,4 +250,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("## Autonomy and persistence")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "XAI_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } diff --git a/provider-zai/src/register.rs b/provider-zai/src/register.rs index 812f71df0..d21e96c28 100644 --- a/provider-zai/src/register.rs +++ b/provider-zai/src/register.rs @@ -18,11 +18,14 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; use std::time::Duration; +/// Env var the router (and, as a fallback, this provider) reads for the key. +pub const CREDENTIAL_ENV_VAR: &str = "ZAI_API_KEY"; + pub fn declaration() -> ProviderDeclaration { ProviderDeclaration { id: PROVIDER_ID.into(), display_name: Some("Z.AI".into()), - credential_env_var: Some("ZAI_API_KEY".into()), + credential_env_var: Some(CREDENTIAL_ENV_VAR.into()), defaults: Some(ProviderDefaults { api_url: Some(DEFAULT_API_URL.into()), max_tokens: Some(DEFAULT_MAX_TOKENS), @@ -210,4 +213,13 @@ mod tests { assert!(prompt.contains("agent_trigger")); assert!(prompt.contains("IMPORTANT: NEVER invent function ids")); } + + #[test] + fn declaration_uses_credential_env_var_const() { + assert_eq!(super::CREDENTIAL_ENV_VAR, "ZAI_API_KEY"); + assert_eq!( + declaration().credential_env_var.as_deref(), + Some(super::CREDENTIAL_ENV_VAR) + ); + } } From 0d3a3da65f5a4807dd0fb4bb441feb14a1255a57 Mon Sep 17 00:00:00 2001 From: Anderson Leal Date: Wed, 22 Jul 2026 13:42:15 -0300 Subject: [PATCH 3/4] feat(providers): fall back to provider-process env var when router resolves no credential --- llm-router/src/provider_scaffold/cache.rs | 4 +++- llm-router/src/provider_scaffold/router_client.rs | 6 ++++-- provider-anthropic/src/router_client.rs | 8 +++++++- provider-anthropic/src/stream_fn.rs | 7 ++++++- provider-kimi/src/router_client.rs | 8 ++++++-- provider-llamacpp/src/embed.rs | 7 ++++++- provider-llamacpp/src/router_client.rs | 8 +++++++- provider-llamacpp/src/stream_fn.rs | 7 ++++++- provider-openai-codex/src/router_client.rs | 2 +- provider-openai-codex/src/stream_fn.rs | 2 +- provider-openai/src/embed.rs | 7 ++++++- provider-openai/src/router_client.rs | 8 +++++++- provider-openai/src/stream_fn.rs | 7 ++++++- provider-xai/src/router_client.rs | 8 +++++++- provider-xai/src/stream_fn.rs | 7 ++++++- provider-zai/src/router_client.rs | 8 +++++++- provider-zai/src/stream_fn.rs | 7 ++++++- 17 files changed, 92 insertions(+), 19 deletions(-) diff --git a/llm-router/src/provider_scaffold/cache.rs b/llm-router/src/provider_scaffold/cache.rs index f8b5a3155..b9655222c 100644 --- a/llm-router/src/provider_scaffold/cache.rs +++ b/llm-router/src/provider_scaffold/cache.rs @@ -68,13 +68,15 @@ impl ScaffoldCache { iii: &IIIClient, provider_id: &str, token: Option<&str>, + credential_env_var: Option<&str>, ) -> Result { if let Some(resolved) = self.fresh_resolve() { return Ok(resolved); } // The lock is never held across the await; concurrent misses may // duplicate one resolve, which is harmless. - let resolved = super::router_client::resolve(iii, provider_id, token).await?; + let resolved = + super::router_client::resolve(iii, provider_id, token, credential_env_var).await?; *self.resolve.write().expect("resolve cache lock poisoned") = Some((resolved.clone(), Instant::now())); Ok(resolved) diff --git a/llm-router/src/provider_scaffold/router_client.rs b/llm-router/src/provider_scaffold/router_client.rs index 802dd7b2e..1bf891709 100644 --- a/llm-router/src/provider_scaffold/router_client.rs +++ b/llm-router/src/provider_scaffold/router_client.rs @@ -24,17 +24,19 @@ pub async fn resolve( iii: &IIIClient, provider_id: &str, token: Option<&str>, + credential_env_var: Option<&str>, ) -> Result { let mut payload = json!({ "id": provider_id }); if let Some(t) = token { payload["token"] = json!(t); } let raw = call(iii, "router::provider::resolve", payload).await?; - serde_json::from_value(raw).map_err(|e| Error::Remote { + let resp: ProviderResolveResponse = serde_json::from_value(raw).map_err(|e| Error::Remote { code: "provider/bad_resolve_response".into(), message: e.to_string(), stacktrace: None, - }) + })?; + Ok(apply_credential_env_fallback(resp, credential_env_var)) } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-anthropic/src/router_client.rs b/provider-anthropic/src/router_client.rs index 175f9e844..8b777c0f5 100644 --- a/provider-anthropic/src/router_client.rs +++ b/provider-anthropic/src/router_client.rs @@ -14,7 +14,13 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve( + iii, + PROVIDER_ID, + token, + Some(crate::register::CREDENTIAL_ENV_VAR), + ) + .await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-anthropic/src/stream_fn.rs b/provider-anthropic/src/stream_fn.rs index 3c85e5301..ae025278a 100644 --- a/provider-anthropic/src/stream_fn.rs +++ b/provider-anthropic/src/stream_fn.rs @@ -74,7 +74,12 @@ async fn run_stream_call( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, diff --git a/provider-kimi/src/router_client.rs b/provider-kimi/src/router_client.rs index 373c350e5..f7b5e8b47 100644 --- a/provider-kimi/src/router_client.rs +++ b/provider-kimi/src/router_client.rs @@ -28,11 +28,15 @@ pub async fn resolve( payload["token"] = json!(t); } let raw = call(iii, "router::provider::resolve", payload).await?; - serde_json::from_value(raw).map_err(|e| Error::Remote { + let resp: ProviderResolveResponse = serde_json::from_value(raw).map_err(|e| Error::Remote { code: "provider/bad_resolve_response".into(), message: e.to_string(), stacktrace: None, - }) + })?; + Ok(llm_router::provider_scaffold::router_client::apply_credential_env_fallback( + resp, + Some(crate::register::CREDENTIAL_ENV_VAR), + )) } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-llamacpp/src/embed.rs b/provider-llamacpp/src/embed.rs index 3b49e6db9..73005bed7 100644 --- a/provider-llamacpp/src/embed.rs +++ b/provider-llamacpp/src/embed.rs @@ -94,7 +94,12 @@ pub async fn handle( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await .inspect_err(|e| { if classify_bus_error(e) == ErrorKind::AuthExpired { diff --git a/provider-llamacpp/src/router_client.rs b/provider-llamacpp/src/router_client.rs index 175f9e844..8b777c0f5 100644 --- a/provider-llamacpp/src/router_client.rs +++ b/provider-llamacpp/src/router_client.rs @@ -14,7 +14,13 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve( + iii, + PROVIDER_ID, + token, + Some(crate::register::CREDENTIAL_ENV_VAR), + ) + .await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-llamacpp/src/stream_fn.rs b/provider-llamacpp/src/stream_fn.rs index b721f34fb..53d525f77 100644 --- a/provider-llamacpp/src/stream_fn.rs +++ b/provider-llamacpp/src/stream_fn.rs @@ -64,7 +64,12 @@ async fn run_stream_call( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, diff --git a/provider-openai-codex/src/router_client.rs b/provider-openai-codex/src/router_client.rs index b082daec8..487c83a40 100644 --- a/provider-openai-codex/src/router_client.rs +++ b/provider-openai-codex/src/router_client.rs @@ -66,7 +66,7 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve(iii, PROVIDER_ID, token, None).await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-openai-codex/src/stream_fn.rs b/provider-openai-codex/src/stream_fn.rs index 0a95c2f0b..387ff7323 100644 --- a/provider-openai-codex/src/stream_fn.rs +++ b/provider-openai-codex/src/stream_fn.rs @@ -81,7 +81,7 @@ async fn run_stream_call( // An auth-classified failure drops the cache so the next attempt // re-resolves fresh — retrying stays the router's job. let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve(iii, crate::PROVIDER_ID, token.as_deref(), None) .await { Ok(r) => r, diff --git a/provider-openai/src/embed.rs b/provider-openai/src/embed.rs index e94a3cee8..540460651 100644 --- a/provider-openai/src/embed.rs +++ b/provider-openai/src/embed.rs @@ -93,7 +93,12 @@ pub async fn handle( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, diff --git a/provider-openai/src/router_client.rs b/provider-openai/src/router_client.rs index 175f9e844..8b777c0f5 100644 --- a/provider-openai/src/router_client.rs +++ b/provider-openai/src/router_client.rs @@ -14,7 +14,13 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve( + iii, + PROVIDER_ID, + token, + Some(crate::register::CREDENTIAL_ENV_VAR), + ) + .await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-openai/src/stream_fn.rs b/provider-openai/src/stream_fn.rs index 720dc685b..a8c4f249e 100644 --- a/provider-openai/src/stream_fn.rs +++ b/provider-openai/src/stream_fn.rs @@ -82,7 +82,12 @@ async fn run_stream_call( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, diff --git a/provider-xai/src/router_client.rs b/provider-xai/src/router_client.rs index 175f9e844..8b777c0f5 100644 --- a/provider-xai/src/router_client.rs +++ b/provider-xai/src/router_client.rs @@ -14,7 +14,13 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve( + iii, + PROVIDER_ID, + token, + Some(crate::register::CREDENTIAL_ENV_VAR), + ) + .await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-xai/src/stream_fn.rs b/provider-xai/src/stream_fn.rs index ce1b2acd6..7fd6406bf 100644 --- a/provider-xai/src/stream_fn.rs +++ b/provider-xai/src/stream_fn.rs @@ -103,7 +103,12 @@ async fn run_stream_call( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, diff --git a/provider-zai/src/router_client.rs b/provider-zai/src/router_client.rs index 175f9e844..8b777c0f5 100644 --- a/provider-zai/src/router_client.rs +++ b/provider-zai/src/router_client.rs @@ -14,7 +14,13 @@ pub async fn resolve( iii: &IIIClient, token: Option<&str>, ) -> Result { - scaffold::resolve(iii, PROVIDER_ID, token).await + scaffold::resolve( + iii, + PROVIDER_ID, + token, + Some(crate::register::CREDENTIAL_ENV_VAR), + ) + .await } /// `router::models::reconcile` — replace this provider's catalog slice. diff --git a/provider-zai/src/stream_fn.rs b/provider-zai/src/stream_fn.rs index d30940cfd..c6ba79f5c 100644 --- a/provider-zai/src/stream_fn.rs +++ b/provider-zai/src/stream_fn.rs @@ -65,7 +65,12 @@ async fn run_stream_call( // stays the router's job. let token = cache.load_token(iii, state::STATE_SCOPE).await; let resolved = match cache - .resolve(iii, crate::PROVIDER_ID, token.as_deref()) + .resolve( + iii, + crate::PROVIDER_ID, + token.as_deref(), + Some(crate::register::CREDENTIAL_ENV_VAR), + ) .await { Ok(r) => r, From 111c4ef97f7c70f033802afde85f5bc3eb5a7aba Mon Sep 17 00:00:00 2001 From: Anderson Leal Date: Wed, 22 Jul 2026 15:27:50 -0300 Subject: [PATCH 4/4] style(llm-router,kimi): rustfmt the credential fallback code --- .../src/provider_scaffold/router_client.rs | 37 +++++++++++++++---- provider-kimi/src/router_client.rs | 10 +++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/llm-router/src/provider_scaffold/router_client.rs b/llm-router/src/provider_scaffold/router_client.rs index 1bf891709..c098a5222 100644 --- a/llm-router/src/provider_scaffold/router_client.rs +++ b/llm-router/src/provider_scaffold/router_client.rs @@ -105,9 +105,9 @@ pub fn apply_credential_env_fallback( #[cfg(test)] mod fallback_tests { + use super::with_api_key_fallback; use crate::types::credential::Credential; use crate::types::router::{CredentialSource, ProviderResolveResponse}; - use super::with_api_key_fallback; fn none_resp() -> ProviderResolveResponse { ProviderResolveResponse { @@ -122,18 +122,30 @@ mod fallback_tests { #[test] fn router_credential_wins_over_env() { let mut resp = none_resp(); - resp.credential = Some(Credential::ApiKey { key: "from-router".into() }); + resp.credential = Some(Credential::ApiKey { + key: "from-router".into(), + }); resp.source = CredentialSource::Config; resp.configured = true; let out = with_api_key_fallback(resp, Some("from-env".into())); - assert_eq!(out.credential, Some(Credential::ApiKey { key: "from-router".into() })); + assert_eq!( + out.credential, + Some(Credential::ApiKey { + key: "from-router".into() + }) + ); assert_eq!(out.source, CredentialSource::Config); } #[test] fn injects_env_when_router_has_none() { let out = with_api_key_fallback(none_resp(), Some("sk-abc".into())); - assert_eq!(out.credential, Some(Credential::ApiKey { key: "sk-abc".into() })); + assert_eq!( + out.credential, + Some(Credential::ApiKey { + key: "sk-abc".into() + }) + ); assert_eq!(out.source, CredentialSource::Env); assert!(out.configured); } @@ -147,13 +159,24 @@ mod fallback_tests { #[test] fn empty_and_whitespace_are_not_injected() { - assert_eq!(with_api_key_fallback(none_resp(), Some("".into())).credential, None); - assert_eq!(with_api_key_fallback(none_resp(), Some(" \n".into())).credential, None); + assert_eq!( + with_api_key_fallback(none_resp(), Some("".into())).credential, + None + ); + assert_eq!( + with_api_key_fallback(none_resp(), Some(" \n".into())).credential, + None + ); } #[test] fn injected_key_is_trimmed() { let out = with_api_key_fallback(none_resp(), Some(" sk-abc\n".into())); - assert_eq!(out.credential, Some(Credential::ApiKey { key: "sk-abc".into() })); + assert_eq!( + out.credential, + Some(Credential::ApiKey { + key: "sk-abc".into() + }) + ); } } diff --git a/provider-kimi/src/router_client.rs b/provider-kimi/src/router_client.rs index f7b5e8b47..6cd49b7ae 100644 --- a/provider-kimi/src/router_client.rs +++ b/provider-kimi/src/router_client.rs @@ -33,10 +33,12 @@ pub async fn resolve( message: e.to_string(), stacktrace: None, })?; - Ok(llm_router::provider_scaffold::router_client::apply_credential_env_fallback( - resp, - Some(crate::register::CREDENTIAL_ENV_VAR), - )) + Ok( + llm_router::provider_scaffold::router_client::apply_credential_env_fallback( + resp, + Some(crate::register::CREDENTIAL_ENV_VAR), + ), + ) } /// `router::models::reconcile` — replace this provider's catalog slice.