#![allow(dead_code)]
use crate::config::{
ApiProvider, Config, ProviderCapability, RequestPayloadMode, has_api_key_for,
kimi_cli_credentials_present, model_completion_names_for_provider, provider_capability,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderReadiness {
Configured,
NeedsKey,
OauthReady,
OptionalKeyLocal,
Unreachable,
AuthFailed,
Unknown,
}
impl ProviderReadiness {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Configured => "configured",
Self::NeedsKey => "needs key",
Self::OauthReady => "OAuth ready",
Self::OptionalKeyLocal => "optional key (local)",
Self::Unreachable => "unreachable",
Self::AuthFailed => "auth failed",
Self::Unknown => "unknown",
}
}
#[must_use]
pub fn is_usable(self) -> bool {
matches!(
self,
Self::Configured | Self::OauthReady | Self::OptionalKeyLocal
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelProvenance {
Default,
Saved,
Custom,
Catalog,
Unknown,
}
impl ModelProvenance {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Default => "default",
Self::Saved => "saved",
Self::Custom => "custom",
Self::Catalog => "catalog",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ProviderReadinessRow {
pub provider: ApiProvider,
pub display_name: &'static str,
pub is_active: bool,
pub has_key: bool,
pub readiness: ProviderReadiness,
pub resolved_model: Option<String>,
pub model_provenance: ModelProvenance,
pub base_url: Option<String>,
pub context_window: Option<u32>,
pub max_output: Option<u32>,
pub thinking_supported: bool,
pub cache_telemetry_supported: bool,
pub request_payload_mode: RequestPayloadMode,
pub streaming_supported: bool,
pub local: bool,
}
impl ProviderReadinessRow {
#[must_use]
pub fn for_provider(config: &Config, provider: ApiProvider) -> Self {
let active_provider = config.api_provider();
let is_active = provider == active_provider;
let has_key = has_api_key_for(config, provider);
let local = is_local_provider(provider);
let (resolved_model, model_provenance) = resolve_model(config, provider, is_active);
let capability: Option<ProviderCapability> = resolved_model
.as_deref()
.map(|model| provider_capability(provider, model));
let readiness = classify_readiness(provider, has_key, local);
let base_url = saved_base_url(config, provider);
let (context_window, max_output, thinking_supported, cache_telemetry_supported, mode) =
match capability {
Some(cap) => (
Some(cap.context_window),
Some(cap.max_output),
cap.thinking_supported,
cap.cache_telemetry_supported,
cap.request_payload_mode,
),
None => (
None,
None,
false,
false,
RequestPayloadMode::ChatCompletions,
),
};
Self {
provider,
display_name: provider.display_name(),
is_active,
has_key,
readiness,
resolved_model,
model_provenance,
base_url,
context_window,
max_output,
thinking_supported,
cache_telemetry_supported,
request_payload_mode: mode,
streaming_supported: true,
local,
}
}
}
#[must_use]
pub fn provider_readiness_rows(config: &Config) -> Vec<ProviderReadinessRow> {
ApiProvider::all()
.iter()
.map(|provider| ProviderReadinessRow::for_provider(config, *provider))
.collect()
}
fn is_local_provider(provider: ApiProvider) -> bool {
matches!(
provider,
ApiProvider::Sglang | ApiProvider::Vllm | ApiProvider::Ollama
)
}
fn uses_cli_oauth(provider: ApiProvider) -> bool {
match provider {
ApiProvider::OpenaiCodex => true,
ApiProvider::Moonshot => kimi_cli_credentials_present(),
_ => false,
}
}
fn classify_readiness(provider: ApiProvider, has_key: bool, local: bool) -> ProviderReadiness {
if local {
return ProviderReadiness::OptionalKeyLocal;
}
if !has_key {
return ProviderReadiness::NeedsKey;
}
if uses_cli_oauth(provider) {
return ProviderReadiness::OauthReady;
}
ProviderReadiness::Configured
}
fn resolve_model(
config: &Config,
provider: ApiProvider,
is_active: bool,
) -> (Option<String>, ModelProvenance) {
if is_active {
let model = config.default_model();
let trimmed = model.trim();
if trimmed.is_empty() {
return (None, ModelProvenance::Unknown);
}
let provenance = if saved_model(config, provider).is_some() {
ModelProvenance::Saved
} else {
ModelProvenance::Default
};
return (Some(trimmed.to_string()), provenance);
}
if let Some(saved) = saved_model(config, provider) {
return (Some(saved), ModelProvenance::Saved);
}
match model_completion_names_for_provider(provider).first() {
Some(model) => (Some((*model).to_string()), ModelProvenance::Default),
None => (None, ModelProvenance::Unknown),
}
}
fn saved_model(config: &Config, provider: ApiProvider) -> Option<String> {
config
.provider_config_for(provider)
.and_then(|entry| entry.model.clone())
.map(|model| model.trim().to_string())
.filter(|model| !model.is_empty())
}
fn saved_base_url(config: &Config, provider: ApiProvider) -> Option<String> {
config
.provider_config_for(provider)
.and_then(|entry| entry.base_url.clone())
.map(|url| url.trim().to_string())
.filter(|url| !url.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
fn base_config() -> Config {
Config::default()
}
#[test]
fn local_provider_is_optional_key_and_usable() {
let config = base_config();
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Ollama);
assert_eq!(row.provider, ApiProvider::Ollama);
assert!(row.local);
assert_eq!(row.readiness, ProviderReadiness::OptionalKeyLocal);
assert!(row.readiness.is_usable());
assert!(row.has_key, "self-hosted providers report a usable key");
assert!(row.streaming_supported);
}
#[test]
fn hosted_provider_without_key_needs_key_and_is_not_usable() {
let _guard = EnvGuard::remove(&["FIREWORKS_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY"]);
let config = base_config();
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Fireworks);
assert!(!row.has_key);
assert_eq!(row.readiness, ProviderReadiness::NeedsKey);
assert!(!row.readiness.is_usable());
}
#[test]
fn rows_cover_every_provider_in_canonical_order() {
let config = base_config();
let rows = provider_readiness_rows(&config);
let providers: Vec<ApiProvider> = rows.iter().map(|row| row.provider).collect();
assert_eq!(providers, ApiProvider::all().to_vec());
assert_eq!(rows.len(), ApiProvider::all().len());
}
#[test]
fn active_provider_is_flagged_and_resolves_a_model() {
let config = base_config();
let active = config.api_provider();
let rows = provider_readiness_rows(&config);
let active_rows: Vec<&ProviderReadinessRow> =
rows.iter().filter(|row| row.is_active).collect();
assert_eq!(active_rows.len(), 1, "exactly one active provider");
let row = active_rows[0];
assert_eq!(row.provider, active);
assert!(
row.resolved_model.is_some(),
"active provider must resolve a model"
);
assert!(row.context_window.unwrap_or(0) > 0);
assert!(row.max_output.unwrap_or(0) > 0);
}
#[test]
fn deepseek_v4_active_model_reports_million_window_and_thinking() {
let mut config = base_config();
config.provider = Some("deepseek".to_string());
config.default_text_model = Some("deepseek-v4-pro".to_string());
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Deepseek);
assert!(row.is_active);
assert_eq!(
row.resolved_model.as_deref(),
Some("deepseek-v4-pro"),
"active DeepSeek model resolves through default_model"
);
assert_eq!(
row.context_window,
Some(crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS)
);
assert_eq!(row.max_output, Some(384_000));
assert!(row.thinking_supported);
assert!(row.cache_telemetry_supported);
assert_eq!(
row.request_payload_mode,
RequestPayloadMode::ChatCompletions
);
}
#[test]
fn inactive_provider_falls_back_to_catalog_default_model() {
let config = base_config();
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Moonshot);
assert!(!row.is_active);
let expected = model_completion_names_for_provider(ApiProvider::Moonshot)
.first()
.map(|m| (*m).to_string());
assert_eq!(row.resolved_model, expected);
assert_eq!(row.model_provenance, ModelProvenance::Default);
}
#[test]
fn unknown_pricing_and_context_are_explicit_when_no_model_known() {
let config = base_config();
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Ollama);
assert!(!row.is_active);
assert_eq!(row.resolved_model, None);
assert_eq!(row.model_provenance, ModelProvenance::Unknown);
assert_eq!(row.context_window, None);
assert_eq!(row.max_output, None);
assert!(!row.thinking_supported);
}
#[test]
fn saved_base_url_surfaces_for_self_hosted_provider() {
let mut config = base_config();
config.provider_config_for_mut(ApiProvider::Vllm).base_url =
Some("http://gpu-box.internal:8000/v1".to_string());
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Vllm);
assert_eq!(
row.base_url.as_deref(),
Some("http://gpu-box.internal:8000/v1")
);
assert!(row.local);
assert_eq!(row.readiness, ProviderReadiness::OptionalKeyLocal);
}
#[test]
fn saved_provider_model_marks_saved_provenance() {
let mut config = base_config();
config
.provider_config_for_mut(ApiProvider::Openrouter)
.model = Some("some-vendor/custom-model".to_string());
let row = ProviderReadinessRow::for_provider(&config, ApiProvider::Openrouter);
assert!(!row.is_active);
assert_eq!(
row.resolved_model.as_deref(),
Some("some-vendor/custom-model")
);
assert_eq!(row.model_provenance, ModelProvenance::Saved);
}
#[test]
fn readiness_labels_are_stable() {
assert_eq!(ProviderReadiness::Configured.label(), "configured");
assert_eq!(ProviderReadiness::NeedsKey.label(), "needs key");
assert_eq!(ProviderReadiness::OauthReady.label(), "OAuth ready");
assert_eq!(
ProviderReadiness::OptionalKeyLocal.label(),
"optional key (local)"
);
assert_eq!(ProviderReadiness::Unreachable.label(), "unreachable");
assert_eq!(ProviderReadiness::AuthFailed.label(), "auth failed");
assert_eq!(ProviderReadiness::Unknown.label(), "unknown");
assert!(ProviderReadiness::Configured.is_usable());
assert!(ProviderReadiness::OauthReady.is_usable());
assert!(ProviderReadiness::OptionalKeyLocal.is_usable());
assert!(!ProviderReadiness::NeedsKey.is_usable());
assert!(!ProviderReadiness::Unreachable.is_usable());
assert!(!ProviderReadiness::AuthFailed.is_usable());
assert!(!ProviderReadiness::Unknown.is_usable());
assert_eq!(ModelProvenance::Default.label(), "default");
assert_eq!(ModelProvenance::Saved.label(), "saved");
assert_eq!(ModelProvenance::Custom.label(), "custom");
assert_eq!(ModelProvenance::Catalog.label(), "catalog");
assert_eq!(ModelProvenance::Unknown.label(), "unknown");
}
struct EnvGuard {
saved: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn remove(keys: &[&str]) -> Self {
let saved = keys
.iter()
.map(|key| {
let prior = std::env::var(key).ok();
unsafe {
std::env::remove_var(key);
}
((*key).to_string(), prior)
})
.collect();
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, prior) in &self.saved {
match prior {
Some(value) => unsafe { std::env::set_var(key, value) },
None => unsafe { std::env::remove_var(key) },
}
}
}
}
}