use std::path::PathBuf;
use secrecy::SecretString;
use crate::bootstrap::ironclaw_base_dir;
use crate::llm::registry::ProviderProtocol;
use crate::llm::session::SessionConfig;
pub const OAUTH_PLACEHOLDER: &str = "oauth-placeholder";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CacheRetention {
None,
#[default]
Short,
Long,
}
impl std::str::FromStr for CacheRetention {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" | "off" | "disabled" => Ok(Self::None),
"short" | "5m" | "ephemeral" => Ok(Self::Short),
"long" | "1h" => Ok(Self::Long),
_ => Err(format!(
"invalid cache retention '{}', expected one of: none, short, long",
s
)),
}
}
}
impl std::fmt::Display for CacheRetention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Short => write!(f, "short"),
Self::Long => write!(f, "long"),
}
}
}
#[derive(Debug, Clone)]
pub struct RegistryProviderConfig {
pub protocol: ProviderProtocol,
pub provider_id: String,
pub api_key: Option<SecretString>,
pub base_url: String,
pub model: String,
pub extra_headers: Vec<(String, String)>,
pub oauth_token: Option<SecretString>,
pub is_codex_chatgpt: bool,
pub refresh_token: Option<SecretString>,
pub auth_path: Option<PathBuf>,
pub cache_retention: CacheRetention,
pub unsupported_params: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OpenAiCodexConfig {
pub model: String,
pub auth_endpoint: String,
pub api_base_url: String,
pub client_id: String,
pub session_path: PathBuf,
pub token_refresh_margin_secs: u64,
}
impl Default for OpenAiCodexConfig {
fn default() -> Self {
Self {
model: "gpt-5.3-codex".to_string(),
auth_endpoint: "https://auth.openai.com".to_string(),
api_base_url: "https://chatgpt.com/backend-api/codex".to_string(),
client_id: "app_EMoamEEZ73f0CkXaXp7hrann".to_string(),
session_path: ironclaw_base_dir().join("openai_codex_session.json"),
token_refresh_margin_secs: 300,
}
}
}
#[derive(Debug, Clone)]
pub struct BedrockConfig {
pub region: String,
pub model: String,
pub cross_region: Option<String>,
pub profile: Option<String>,
}
#[derive(Debug, Clone)]
pub struct LlmConfig {
pub backend: String,
pub session: SessionConfig,
pub nearai: NearAiConfig,
pub provider: Option<RegistryProviderConfig>,
pub bedrock: Option<BedrockConfig>,
pub gemini_oauth: Option<GeminiOauthConfig>,
pub openai_codex: Option<OpenAiCodexConfig>,
pub request_timeout_secs: u64,
pub cheap_model: Option<String>,
pub smart_routing_cascade: bool,
}
impl LlmConfig {
pub fn cheap_model_name(&self) -> Option<&str> {
self.cheap_model.as_deref().or_else(|| {
if self.backend == "nearai" {
self.nearai.cheap_model.as_deref()
} else {
None
}
})
}
}
#[derive(Debug, Clone)]
pub struct NearAiConfig {
pub model: String,
pub cheap_model: Option<String>,
pub base_url: String,
pub api_key: Option<SecretString>,
pub fallback_model: Option<String>,
pub max_retries: u32,
pub circuit_breaker_threshold: Option<u32>,
pub circuit_breaker_recovery_secs: u64,
pub response_cache_enabled: bool,
pub response_cache_ttl_secs: u64,
pub response_cache_max_entries: usize,
pub failover_cooldown_secs: u64,
pub failover_cooldown_threshold: u32,
pub smart_routing_cascade: bool,
}
impl NearAiConfig {
pub(crate) fn for_model_discovery() -> Self {
let api_key = crate::config::helpers::env_or_override("NEARAI_API_KEY")
.filter(|k| !k.is_empty())
.map(SecretString::from);
let default_base = if api_key.is_some() {
"https://cloud-api.near.ai"
} else {
"https://private.near.ai"
};
let base_url = crate::config::helpers::env_or_override("NEARAI_BASE_URL")
.unwrap_or_else(|| default_base.to_string());
Self {
model: String::new(),
cheap_model: None,
base_url,
api_key,
fallback_model: None,
max_retries: 3,
circuit_breaker_threshold: None,
circuit_breaker_recovery_secs: 30,
response_cache_enabled: false,
response_cache_ttl_secs: 3600,
response_cache_max_entries: 1000,
failover_cooldown_secs: 300,
failover_cooldown_threshold: 3,
smart_routing_cascade: true,
}
}
}
#[derive(Debug, Clone)]
pub struct GeminiOauthConfig {
pub model: String,
pub credentials_path: PathBuf,
}
impl GeminiOauthConfig {
pub fn default_credentials_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".gemini")
.join("oauth_creds.json")
}
}