use serde_json::{Map, Value};
use crate::auth::AuthCtx;
use crate::canonical::{CanonicalRequest, Content, ReasoningEffort};
use crate::config::partial::OutMode;
use crate::config::provider::Provider;
use crate::protocol::{Envelope, ExecSpec, ProviderCtx, WireRequest};
use crate::store::Secret;
use crate::transport::Timeouts;
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedConfig {
pub provider: Provider,
pub model: String,
pub model_from_cache: bool,
pub output: OutMode,
pub thinking: bool,
pub inline_key: Option<Secret>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub reasoning: Option<ReasoningEffort>,
pub stream: Option<bool>,
pub timeout: Option<u64>,
pub system: Option<Vec<Content>>,
pub extra: Map<String, Value>,
}
impl ResolvedConfig {
pub fn timeouts(&self) -> Timeouts {
Timeouts {
connect: self.timeout,
response: self.timeout,
idle: self.timeout,
}
}
pub fn stamp_transport(&self, wire: &mut WireRequest) {
wire.timeouts = self.timeouts();
if let Some(spec) = &self.provider.transport {
wire.exec = Some(ExecSpec {
program: spec.program.clone(),
args: spec.args.clone(),
envelope: Envelope::Http,
});
}
}
pub fn provider_ctx<'a>(&'a self, beta: &'a [(&'a str, &'a str)]) -> ProviderCtx<'a> {
ProviderCtx {
base_url: &self.provider.base_url,
model: &self.model,
beta_headers: beta,
exec: self.provider.exec.as_deref(),
}
}
pub fn auth_ctx(&self) -> AuthCtx<'_> {
AuthCtx {
store_key: &self.provider.name,
inline_key: self.inline_key.as_ref(),
api_header: self.provider.api_header.as_ref(),
oauth: self.provider.oauth.as_ref(),
ambient: self.provider.ambient.as_ref(),
}
}
}
pub fn fill_absent(req: &mut CanonicalRequest, cfg: &ResolvedConfig) {
if req.model.is_empty() {
req.model = cfg.model.clone();
}
req.max_tokens = req.max_tokens.or(cfg.max_tokens);
req.temperature = req.temperature.or(cfg.temperature);
req.top_p = req.top_p.or(cfg.top_p);
req.reasoning = req.reasoning.or(cfg.reasoning);
req.stream = req.stream.or(cfg.stream).or(Some(true));
req.system = req.system.take().or_else(|| cfg.system.clone());
for (k, v) in &cfg.extra {
req.extra.entry(k.clone()).or_insert_with(|| v.clone());
}
}
pub fn strip_unsupported(req: &mut CanonicalRequest, cfg: &ResolvedConfig) {
for key in &cfg.provider.unsupported_body_keys {
match key.as_str() {
"max_tokens" => req.max_tokens = None,
"temperature" => req.temperature = None,
"top_p" => req.top_p = None,
"reasoning" => req.reasoning = None,
"output" => req.output = None,
other => {
req.extra.remove(other);
}
}
}
}
pub fn lead_with_preamble(req: &mut CanonicalRequest, cfg: &ResolvedConfig) {
let Some(text) = cfg
.provider
.oauth
.as_ref()
.and_then(|o| o.system_preamble.as_deref())
else {
return;
};
let lead = Content::Text(text.to_owned());
let system = req.system.get_or_insert_with(Vec::new);
if system.first() != Some(&lead) {
system.insert(0, lead);
}
}