Skip to main content

tokn_headers/
agent_id.rs

1//! Logical identifier for the *agent* whose behavior an upstream call should
2//! impersonate. Carried end-to-end through the pipeline so downstream stages
3//! (header shaping, provider selection) can branch on it without reparsing
4//! inbound traffic.
5
6use serde::{Deserialize, Serialize};
7use smol_str::SmolStr;
8use std::convert::Infallible;
9use std::fmt;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(from = "SmolStr", into = "SmolStr")]
14pub enum AgentId {
15  #[default]
16  Opencode,
17  CodexCli,
18  ClaudeCode,
19  Cline,
20  CopilotCli,
21  Other(SmolStr),
22}
23
24impl AgentId {
25  pub fn as_str(&self) -> &str {
26    match self {
27      Self::Opencode => "opencode",
28      Self::CodexCli => "codex-cli",
29      Self::ClaudeCode => "claude-code",
30      Self::Cline => "cline",
31      Self::CopilotCli => "copilot-cli",
32      Self::Other(value) => value.as_str(),
33    }
34  }
35
36  pub fn from_slug(slug: &str) -> Option<Self> {
37    match slug {
38      "opencode" => Some(Self::Opencode),
39      "codex_exec" | "codex-tui" | "codex" | "codex-cli" => Some(Self::CodexCli),
40      "claude-cli" | "claude-code" => Some(Self::ClaudeCode),
41      "cline" => Some(Self::Cline),
42      "copilot" | "copilot-cli" => Some(Self::CopilotCli),
43      _ => None,
44    }
45  }
46
47  pub fn provider_default(provider_id: &str) -> Option<Self> {
48    match provider_id {
49      "openai" | "deepseek" | "llama-cpp" | "zai" | "zai-coding-plan" | "zhipuai" | "zhipuai-coding-plan" => {
50        Some(Self::Opencode)
51      }
52      "codex" => Some(Self::CodexCli),
53      "copilot" | "github-copilot" => Some(Self::CopilotCli),
54      _ => None,
55    }
56  }
57}
58
59impl From<&str> for AgentId {
60  fn from(s: &str) -> Self {
61    Self::from_slug(s).unwrap_or_else(|| Self::Other(SmolStr::new(s)))
62  }
63}
64
65impl From<String> for AgentId {
66  fn from(s: String) -> Self {
67    Self::from(s.as_str())
68  }
69}
70
71impl From<SmolStr> for AgentId {
72  fn from(s: SmolStr) -> Self {
73    Self::from(s.as_str())
74  }
75}
76
77impl From<AgentId> for SmolStr {
78  fn from(value: AgentId) -> Self {
79    match value {
80      AgentId::Other(value) => value,
81      other => SmolStr::new(other.as_str()),
82    }
83  }
84}
85
86impl FromStr for AgentId {
87  type Err = Infallible;
88
89  fn from_str(s: &str) -> Result<Self, Self::Err> {
90    Ok(Self::from(s))
91  }
92}
93
94impl fmt::Display for AgentId {
95  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96    f.write_str(self.as_str())
97  }
98}
99
100#[cfg(test)]
101mod tests {
102  use super::*;
103
104  #[test]
105  fn known_ids_round_trip() {
106    for (slug, expected) in [
107      ("opencode", AgentId::Opencode),
108      ("codex-cli", AgentId::CodexCli),
109      ("claude-code", AgentId::ClaudeCode),
110      ("cline", AgentId::Cline),
111      ("copilot-cli", AgentId::CopilotCli),
112    ] {
113      assert_eq!(AgentId::from_slug(slug), Some(expected.clone()));
114      assert_eq!(expected.as_str(), slug);
115      assert_eq!(expected.to_string(), slug);
116    }
117  }
118
119  #[test]
120  fn aliases_normalize() {
121    assert_eq!(AgentId::from_slug("codex"), Some(AgentId::CodexCli));
122    assert_eq!(AgentId::from_slug("codex_exec"), Some(AgentId::CodexCli));
123    assert_eq!(AgentId::from_slug("claude-cli"), Some(AgentId::ClaudeCode));
124    assert_eq!(AgentId::from_slug("copilot"), Some(AgentId::CopilotCli));
125  }
126
127  #[test]
128  fn unknown_slug_falls_back_to_other() {
129    let agent_id = AgentId::from("my-bespoke-tool");
130    assert_eq!(agent_id, AgentId::Other(SmolStr::new("my-bespoke-tool")));
131    assert_eq!(agent_id.as_str(), "my-bespoke-tool");
132  }
133
134  #[test]
135  fn serde_round_trip_other() {
136    let agent_id = AgentId::Other(SmolStr::new("custom-tool"));
137    let encoded = serde_json::to_string(&agent_id).unwrap();
138    assert_eq!(encoded, "\"custom-tool\"");
139    let decoded: AgentId = serde_json::from_str(&encoded).unwrap();
140    assert_eq!(decoded, agent_id);
141  }
142
143  #[test]
144  fn provider_defaults_cover_known_providers() {
145    assert_eq!(AgentId::provider_default("openai"), Some(AgentId::Opencode));
146    assert_eq!(AgentId::provider_default("llama-cpp"), Some(AgentId::Opencode));
147    assert_eq!(AgentId::provider_default("codex"), Some(AgentId::CodexCli));
148    assert_eq!(AgentId::provider_default("github-copilot"), Some(AgentId::CopilotCli));
149    assert_eq!(AgentId::provider_default("nonesuch"), None);
150  }
151}