use std::str::FromStr;
use anyhow::{Context, Result, bail};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderKind {
OpenAi,
Anthropic,
Google,
}
impl ProviderKind {
pub fn as_str(self) -> &'static str {
match self {
ProviderKind::OpenAi => "openai",
ProviderKind::Anthropic => "anthropic",
ProviderKind::Google => "google",
}
}
}
impl FromStr for ProviderKind {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"openai" | "oai" | "gpt" => Ok(ProviderKind::OpenAi),
"anthropic" | "claude" => Ok(ProviderKind::Anthropic),
"google" | "gemini" | "googleai" => Ok(ProviderKind::Google),
other => bail!("unknown provider '{other}' (expected openai|anthropic|google)"),
}
}
}
#[derive(Debug, Clone)]
pub struct Request {
kind: ProviderKind,
raw: Value,
model_hint: Option<String>,
cache_stage_enabled: bool,
}
impl Request {
pub fn parse(kind: ProviderKind, body: &str) -> Result<Self> {
let raw: Value = serde_json::from_str(body).context("request body is not valid JSON")?;
Ok(Self {
kind,
raw,
model_hint: None,
cache_stage_enabled: false,
})
}
pub fn from_value(kind: ProviderKind, raw: Value) -> Self {
Self {
kind,
raw,
model_hint: None,
cache_stage_enabled: false,
}
}
pub fn kind(&self) -> ProviderKind {
self.kind
}
pub fn set_model_hint(&mut self, model: Option<&str>) {
self.model_hint = model.map(str::to_string);
}
pub fn set_cache_stage_enabled(&mut self, enabled: bool) {
self.cache_stage_enabled = enabled;
}
pub fn cache_stage_enabled(&self) -> bool {
self.cache_stage_enabled
}
pub fn model_id(&self) -> Option<&str> {
self.raw
.get("model")
.and_then(Value::as_str)
.or(self.model_hint.as_deref())
}
pub fn raw(&self) -> &Value {
&self.raw
}
pub fn raw_mut(&mut self) -> &mut Value {
&mut self.raw
}
pub fn to_json_string(&self) -> Result<String> {
serde_json::to_string(&self.raw).context("failed to serialize request")
}
pub fn get_str(&self, pointer: &str) -> Option<&str> {
self.raw.pointer(pointer).and_then(Value::as_str)
}
pub fn set(&mut self, pointer: &str, value: Value) -> bool {
match self.raw.pointer_mut(pointer) {
Some(slot) => {
*slot = value;
true
}
None => false,
}
}
}
#[derive(Debug, Clone)]
pub struct Response {
kind: ProviderKind,
raw: Value,
}
impl Response {
pub fn parse(kind: ProviderKind, body: &str) -> Result<Self> {
let raw: Value = serde_json::from_str(body).context("response body is not valid JSON")?;
Ok(Self { kind, raw })
}
pub fn kind(&self) -> ProviderKind {
self.kind
}
pub fn raw(&self) -> &Value {
&self.raw
}
pub fn raw_mut(&mut self) -> &mut Value {
&mut self.raw
}
pub fn to_json_string(&self) -> Result<String> {
serde_json::to_string(&self.raw).context("failed to serialize response")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_kind_parses_aliases() {
assert_eq!(
"openai".parse::<ProviderKind>().unwrap(),
ProviderKind::OpenAi
);
assert_eq!(
"Claude".parse::<ProviderKind>().unwrap(),
ProviderKind::Anthropic
);
assert!("bogus".parse::<ProviderKind>().is_err());
}
#[test]
fn request_round_trip_is_lossless() {
let body = r#"{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}],"max_tokens":7,"unknown_field":{"keep":[1,2,3]}}"#;
let req = Request::parse(ProviderKind::OpenAi, body).unwrap();
let out = req.to_json_string().unwrap();
let a: Value = serde_json::from_str(body).unwrap();
let b: Value = serde_json::from_str(&out).unwrap();
assert_eq!(a, b, "unknown fields must survive round-trip");
}
#[test]
fn model_id_prefers_body_then_hint_and_never_serializes() {
let mut req = Request::parse(ProviderKind::OpenAi, r#"{"model":"gpt-4o"}"#).unwrap();
assert_eq!(req.model_id(), Some("gpt-4o"));
req.set_model_hint(Some("ignored"));
assert_eq!(
req.model_id(),
Some("gpt-4o"),
"body model outranks the hint"
);
let mut g = Request::parse(ProviderKind::Google, r#"{"contents":[]}"#).unwrap();
assert_eq!(g.model_id(), None);
g.set_model_hint(Some("gemini-3-pro"));
assert_eq!(g.model_id(), Some("gemini-3-pro"));
assert_eq!(
g.to_json_string().unwrap(),
r#"{"contents":[]}"#,
"the hint must not leak into the forwarded body"
);
}
#[test]
fn pointer_get_and_set() {
let mut req = Request::parse(
ProviderKind::OpenAi,
r#"{"messages":[{"role":"user","content":"hi"}]}"#,
)
.unwrap();
assert_eq!(req.get_str("/messages/0/content"), Some("hi"));
assert!(req.set("/messages/0/content", Value::String("bye".into())));
assert_eq!(req.get_str("/messages/0/content"), Some("bye"));
assert!(!req.set("/nope/0", Value::Null));
}
}