use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::{Map, Value};
use crate::auth::OAuthConfig;
use crate::canonical::{Content, ReasoningEffort};
use crate::config::provider::{AuthId, HeaderSpec, ModelsOverride, ProtocolId};
use crate::store::{AmbientSpec, Secret};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OutMode {
Text,
Ndjson,
Raw,
}
impl OutMode {
pub fn parse(s: &str) -> Option<OutMode> {
match s {
"text" => Some(OutMode::Text),
"ndjson" => Some(OutMode::Ndjson),
"raw" => Some(OutMode::Raw),
_ => None,
}
}
}
#[derive(Default, Clone, Debug, PartialEq, serde::Serialize)]
pub struct PartialProvider {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<ProtocolId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth: Option<AuthId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_header: Option<HeaderSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub beta_headers: Option<Vec<(String, String)>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_aliases: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_prefixes: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub body_defaults: Map<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unsupported_body_keys: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub models: Option<ModelsOverride>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oauth: Option<OAuthConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ambient: Option<AmbientSpec>,
}
impl PartialProvider {
fn or(self, other: PartialProvider) -> PartialProvider {
PartialProvider {
base_url: self.base_url.or(other.base_url),
protocol: self.protocol.or(other.protocol),
auth: self.auth.or(other.auth),
api_header: self.api_header.or(other.api_header),
beta_headers: self.beta_headers.or(other.beta_headers),
model_aliases: self.model_aliases.or(other.model_aliases),
model_prefixes: self.model_prefixes.or(other.model_prefixes),
body_defaults: or_map(self.body_defaults, other.body_defaults),
unsupported_body_keys: self.unsupported_body_keys.or(other.unsupported_body_keys),
models: self.models.or(other.models),
oauth: self.oauth.or(other.oauth),
ambient: self.ambient.or(other.ambient),
}
}
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct PartialConfig {
pub provider: Option<String>,
pub default_provider: Option<String>,
pub model: Option<String>,
pub api_key: Option<Secret>,
pub output: Option<OutMode>,
pub thinking: Option<bool>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub reasoning: Option<ReasoningEffort>,
pub stream: Option<bool>,
pub timeout_connect: Option<u64>,
pub timeout_response: Option<u64>,
pub timeout_idle: Option<u64>,
pub system: Option<Vec<Content>>,
pub providers: BTreeMap<String, PartialProvider>,
pub extra: Map<String, Value>,
}
impl PartialConfig {
pub fn or(self, other: PartialConfig) -> PartialConfig {
PartialConfig {
provider: self.provider.or(other.provider),
default_provider: self.default_provider.or(other.default_provider),
model: self.model.or(other.model),
api_key: self.api_key.or(other.api_key),
output: self.output.or(other.output),
thinking: self.thinking.or(other.thinking),
max_tokens: self.max_tokens.or(other.max_tokens),
temperature: self.temperature.or(other.temperature),
top_p: self.top_p.or(other.top_p),
reasoning: self.reasoning.or(other.reasoning),
stream: self.stream.or(other.stream),
timeout_connect: self.timeout_connect.or(other.timeout_connect),
timeout_response: self.timeout_response.or(other.timeout_response),
timeout_idle: self.timeout_idle.or(other.timeout_idle),
system: self.system.or(other.system),
providers: merge_providers(self.providers, other.providers),
extra: or_map(self.extra, other.extra),
}
}
}
fn merge_providers(
mut hi: BTreeMap<String, PartialProvider>,
lo: BTreeMap<String, PartialProvider>,
) -> BTreeMap<String, PartialProvider> {
for (key, lo_row) in lo {
let merged = match hi.remove(&key) {
Some(hi_row) => hi_row.or(lo_row),
None => lo_row,
};
hi.insert(key, merged);
}
hi
}
pub(crate) fn or_map(mut hi: Map<String, Value>, lo: Map<String, Value>) -> Map<String, Value> {
for (key, value) in lo {
hi.entry(key).or_insert(value);
}
hi
}