use crate::agent::AgentKind;
use crate::map::HeaderMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OverlayKind {
Copilot,
Codex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedSchema {
pub agent: AgentKind,
pub overlay: Option<OverlayKind>,
}
impl ResolvedSchema {
pub fn compose(persona_map: HeaderMap, overlay_map: Option<HeaderMap>) -> HeaderMap {
let mut out = persona_map;
if let Some(o) = overlay_map {
out.merge_replacing(o);
}
out
}
}
pub fn lookup(provider: &str, agent_id: &str) -> Option<ResolvedSchema> {
let base = AgentKind::from_agent_id(agent_id).unwrap_or(AgentKind::Opencode);
match provider {
"openai" => Some(ResolvedSchema {
agent: base,
overlay: matches!(base, AgentKind::CodexCli).then_some(OverlayKind::Codex),
}),
"copilot" | "github-copilot" => Some(ResolvedSchema {
agent: base,
overlay: Some(OverlayKind::Copilot),
}),
"deepseek" | "zai" | "zai-coding-plan" | "zhipuai" | "zhipuai-coding-plan" | "codex" => Some(ResolvedSchema {
agent: base,
overlay: matches!(provider, "codex").then_some(OverlayKind::Codex),
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys;
use crate::name::HeaderName;
use crate::value::HeaderValue;
#[test]
fn lookup_known_pairs() {
assert_eq!(
lookup("openai", "codex-cli"),
Some(ResolvedSchema {
agent: AgentKind::CodexCli,
overlay: Some(OverlayKind::Codex)
})
);
assert_eq!(
lookup("copilot", "opencode"),
Some(ResolvedSchema {
agent: AgentKind::Opencode,
overlay: Some(OverlayKind::Copilot)
})
);
assert_eq!(
lookup("deepseek", "claude-code"),
Some(ResolvedSchema {
agent: AgentKind::ClaudeCode,
overlay: None
})
);
}
#[test]
fn unknown_agent_falls_back_to_opencode() {
let r = lookup("copilot", "my-tool").unwrap();
assert_eq!(r.agent, AgentKind::Opencode);
assert_eq!(r.overlay, Some(OverlayKind::Copilot));
}
#[test]
fn unknown_provider_returns_none() {
assert!(lookup("nonesuch", "opencode").is_none());
}
#[test]
fn openai_with_non_codex_persona_has_no_overlay() {
let r = lookup("openai", "opencode").unwrap();
assert!(r.overlay.is_none());
}
#[test]
fn copilot_cli_resolves_with_copilot_overlay() {
let r = lookup("copilot", "copilot-cli").unwrap();
assert_eq!(r.agent, AgentKind::CopilotCli);
assert_eq!(r.overlay, Some(OverlayKind::Copilot));
}
#[test]
fn compose_overlay_wins_on_conflict() {
let mut persona_map = HeaderMap::new();
persona_map.insert(
HeaderName::new("X-Session-Id"),
HeaderValue::from_string("from-persona".into()),
);
persona_map.insert("X-Persona-Only", HeaderValue::from_string("p".into()));
let mut overlay_map = HeaderMap::new();
overlay_map.insert(
HeaderName::new("x-session-id"),
HeaderValue::from_string("from-overlay".into()),
);
overlay_map.insert("X-Overlay-Only", HeaderValue::from_string("o".into()));
let composed = ResolvedSchema::compose(persona_map, Some(overlay_map));
assert_eq!(composed.get(&keys::X_SESSION_ID).unwrap().as_str(), "from-overlay");
assert_eq!(composed.get("X-Persona-Only").unwrap().as_str(), "p");
assert_eq!(composed.get("X-Overlay-Only").unwrap().as_str(), "o");
}
#[test]
fn compose_without_overlay_is_identity() {
let mut persona_map = HeaderMap::new();
persona_map.insert("A", HeaderValue::from_string("1".into()));
let composed = ResolvedSchema::compose(persona_map.clone(), None);
assert_eq!(composed, persona_map);
}
}