fix(provider): resolve fallback provider credentials independently
Fallback providers in create_resilient_provider_with_options() were created via create_provider_with_options() which passed the primary provider's api_key as credential_override. This caused resolve_provider_credential() to short-circuit on the override and never check the fallback provider's own env var (e.g. DEEPSEEK_API_KEY for a deepseek fallback), resulting in auth failures (401) when the primary and fallback use different API services. Switch to create_provider_with_url(fallback, None, None) so each fallback resolves its own credential via provider-specific env vars. This also enables custom: URL prefixes (e.g. custom:http://host.docker.internal:1234/v1) to work as fallback entries, which was previously impossible through the options path. Add three focused tests covering independent credential resolution, custom URL fallbacks, and mixed fallback chains.
This commit is contained in:
parent
f9e1ffe634
commit
bb22bdc8fb
1 changed files with 81 additions and 2 deletions
|
|
@ -820,8 +820,17 @@ pub fn create_resilient_provider_with_options(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Fallback providers don't use the custom api_url (it's specific to primary).
|
||||
match create_provider_with_options(fallback, api_key, options) {
|
||||
// Each fallback provider resolves its own credential via provider-
|
||||
// specific env vars (e.g. DEEPSEEK_API_KEY for "deepseek") instead
|
||||
// of inheriting the primary provider's key. Passing `None` lets
|
||||
// `resolve_provider_credential` check the correct env var for the
|
||||
// fallback provider name.
|
||||
//
|
||||
// Route through `create_provider_with_url` (not
|
||||
// `create_provider_with_options`) so that `custom:` URL prefixes
|
||||
// (e.g. "custom:http://host.docker.internal:1234/v1") work as
|
||||
// fallback entries.
|
||||
match create_provider_with_url(fallback, None, None) {
|
||||
Ok(provider) => providers.push((fallback.clone(), provider)),
|
||||
Err(_error) => {
|
||||
tracing::warn!(
|
||||
|
|
@ -1673,6 +1682,76 @@ mod tests {
|
|||
assert!(provider.is_err());
|
||||
}
|
||||
|
||||
/// Fallback providers resolve their own credentials via provider-specific
|
||||
/// env vars rather than inheriting the primary provider's key. A provider
|
||||
/// that requires no key (e.g. lmstudio, ollama) must initialize
|
||||
/// successfully even when the primary uses a completely different key.
|
||||
#[test]
|
||||
fn resilient_fallback_resolves_own_credential() {
|
||||
let reliability = crate::config::ReliabilityConfig {
|
||||
provider_retries: 1,
|
||||
provider_backoff_ms: 100,
|
||||
fallback_providers: vec!["lmstudio".into(), "ollama".into()],
|
||||
api_keys: Vec::new(),
|
||||
model_fallbacks: std::collections::HashMap::new(),
|
||||
channel_initial_backoff_secs: 2,
|
||||
channel_max_backoff_secs: 60,
|
||||
scheduler_poll_secs: 15,
|
||||
scheduler_retries: 2,
|
||||
};
|
||||
|
||||
// Primary uses a ZAI key; fallbacks (lmstudio, ollama) should NOT
|
||||
// receive this key — they resolve their own credentials independently.
|
||||
let provider = create_resilient_provider("zai", Some("zai-test-key"), None, &reliability);
|
||||
assert!(provider.is_ok());
|
||||
}
|
||||
|
||||
/// `custom:` URL entries work as fallback providers, enabling arbitrary
|
||||
/// OpenAI-compatible endpoints (e.g. local LM Studio on a Docker host).
|
||||
#[test]
|
||||
fn resilient_fallback_supports_custom_url() {
|
||||
let reliability = crate::config::ReliabilityConfig {
|
||||
provider_retries: 1,
|
||||
provider_backoff_ms: 100,
|
||||
fallback_providers: vec!["custom:http://host.docker.internal:1234/v1".into()],
|
||||
api_keys: Vec::new(),
|
||||
model_fallbacks: std::collections::HashMap::new(),
|
||||
channel_initial_backoff_secs: 2,
|
||||
channel_max_backoff_secs: 60,
|
||||
scheduler_poll_secs: 15,
|
||||
scheduler_retries: 2,
|
||||
};
|
||||
|
||||
let provider =
|
||||
create_resilient_provider("openai", Some("openai-test-key"), None, &reliability);
|
||||
assert!(provider.is_ok());
|
||||
}
|
||||
|
||||
/// Mixed fallback chain: named providers, custom URLs, and invalid entries
|
||||
/// all coexist. Invalid entries are silently ignored; valid ones initialize.
|
||||
#[test]
|
||||
fn resilient_fallback_mixed_chain() {
|
||||
let reliability = crate::config::ReliabilityConfig {
|
||||
provider_retries: 1,
|
||||
provider_backoff_ms: 100,
|
||||
fallback_providers: vec![
|
||||
"deepseek".into(),
|
||||
"custom:http://localhost:8080/v1".into(),
|
||||
"nonexistent-provider".into(),
|
||||
"lmstudio".into(),
|
||||
],
|
||||
api_keys: Vec::new(),
|
||||
model_fallbacks: std::collections::HashMap::new(),
|
||||
channel_initial_backoff_secs: 2,
|
||||
channel_max_backoff_secs: 60,
|
||||
scheduler_poll_secs: 15,
|
||||
scheduler_retries: 2,
|
||||
};
|
||||
|
||||
let provider = create_resilient_provider("zai", Some("zai-test-key"), None, &reliability);
|
||||
assert!(provider.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ollama_with_custom_url() {
|
||||
let provider = create_provider_with_url("ollama", None, Some("http://10.100.2.32:11434"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue