Skip to main content

llm_relay/types/
common.rs

1use serde::{Deserialize, Serialize};
2
3/// LLM provider type.
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum Provider {
7    #[default]
8    Anthropic,
9    #[serde(alias = "openai")]
10    OpenAiCompatible,
11}
12
13impl Provider {
14    pub fn as_str(&self) -> &'static str {
15        match self {
16            Self::Anthropic => "anthropic",
17            Self::OpenAiCompatible => "openai",
18        }
19    }
20
21    pub fn default_base_url(&self) -> &'static str {
22        match self {
23            Self::Anthropic => "https://api.anthropic.com",
24            Self::OpenAiCompatible => "https://api.openai.com",
25        }
26    }
27}
28
29impl std::fmt::Display for Provider {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.write_str(self.as_str())
32    }
33}
34
35impl std::str::FromStr for Provider {
36    type Err = String;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        match s {
40            "anthropic" => Ok(Self::Anthropic),
41            "openai" => Ok(Self::OpenAiCompatible),
42            _ => Err(format!("unknown provider: {s}")),
43        }
44    }
45}
46
47/// Extended thinking configuration.
48///
49/// Extended thinking lets Claude think step-by-step before responding.
50/// Two modes are available:
51/// - **Adaptive**: Claude decides when and how much to think. Controlled via effort level.
52///   Supported on Claude Opus 4.6 and Sonnet 4.6.
53/// - **Enabled**: Manual extended thinking with an explicit token budget.
54///   For older models (Sonnet 4.5, etc.) or when a specific budget is needed.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "lowercase")]
57pub enum ThinkingConfig {
58    /// Adaptive thinking — Claude decides when and how much to think.
59    Adaptive {
60        #[serde(default)]
61        effort: EffortLevel,
62    },
63    /// Manual extended thinking with an explicit token budget.
64    Enabled { budget_tokens: u32 },
65}
66
67/// Effort level for adaptive thinking.
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "lowercase")]
70pub enum EffortLevel {
71    Max,
72    #[default]
73    High,
74    Medium,
75    Low,
76}
77
78impl EffortLevel {
79    pub fn as_str(&self) -> &'static str {
80        match self {
81            Self::Max => "max",
82            Self::High => "high",
83            Self::Medium => "medium",
84            Self::Low => "low",
85        }
86    }
87
88    pub fn all() -> &'static [EffortLevel] {
89        &[Self::Max, Self::High, Self::Medium, Self::Low]
90    }
91}
92
93impl std::fmt::Display for EffortLevel {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(self.as_str())
96    }
97}
98
99impl std::str::FromStr for EffortLevel {
100    type Err = String;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s {
104            "max" => Ok(Self::Max),
105            "high" => Ok(Self::High),
106            "medium" | "med" => Ok(Self::Medium),
107            "low" | "minimal" => Ok(Self::Low),
108            _ => Err(format!("unknown effort level: {s}")),
109        }
110    }
111}
112
113/// Provider-agnostic tool definition.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ToolDefinition {
116    pub name: String,
117    pub description: String,
118    pub input_schema: serde_json::Value,
119}
120
121impl ToolDefinition {
122    pub fn new(
123        name: impl Into<String>,
124        description: impl Into<String>,
125        input_schema: serde_json::Value,
126    ) -> Self {
127        Self {
128            name: name.into(),
129            description: description.into(),
130            input_schema,
131        }
132    }
133}
134
135/// Token usage information.
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct Usage {
138    #[serde(default)]
139    pub input_tokens: u64,
140    #[serde(default)]
141    pub output_tokens: u64,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub cache_creation_input_tokens: Option<u64>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub cache_read_input_tokens: Option<u64>,
146}
147
148impl Usage {
149    pub fn total_tokens(&self) -> u64 {
150        self.input_tokens + self.output_tokens
151    }
152}
153
154/// Response format specification (for JSON mode).
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ResponseFormat {
157    #[serde(rename = "type")]
158    pub format_type: String,
159}
160
161impl ResponseFormat {
162    pub fn json_object() -> Self {
163        Self {
164            format_type: "json_object".to_string(),
165        }
166    }
167}
168
169/// Stop reason, normalized across providers.
170///
171/// Serializes/deserializes using Anthropic wire format (`"end_turn"`, `"tool_use"`, `"max_tokens"`).
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum StopReason {
174    EndTurn,
175    ToolUse,
176    MaxTokens,
177    Other(String),
178}
179
180impl StopReason {
181    pub fn from_anthropic(s: &str) -> Self {
182        match s {
183            "end_turn" => Self::EndTurn,
184            "tool_use" => Self::ToolUse,
185            "max_tokens" => Self::MaxTokens,
186            other => Self::Other(other.to_string()),
187        }
188    }
189
190    pub fn from_openai(s: &str) -> Self {
191        match s {
192            "stop" => Self::EndTurn,
193            "tool_calls" => Self::ToolUse,
194            "length" => Self::MaxTokens,
195            other => Self::Other(other.to_string()),
196        }
197    }
198
199    pub fn to_anthropic(&self) -> &str {
200        match self {
201            Self::EndTurn => "end_turn",
202            Self::ToolUse => "tool_use",
203            Self::MaxTokens => "max_tokens",
204            Self::Other(s) => s,
205        }
206    }
207
208    pub fn to_openai(&self) -> &str {
209        match self {
210            Self::EndTurn => "stop",
211            Self::ToolUse => "tool_calls",
212            Self::MaxTokens => "length",
213            Self::Other(s) => s,
214        }
215    }
216
217    pub fn is_tool_use(&self) -> bool {
218        matches!(self, Self::ToolUse)
219    }
220}
221
222impl std::fmt::Display for StopReason {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.write_str(self.to_anthropic())
225    }
226}
227
228impl Serialize for StopReason {
229    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
230        serializer.serialize_str(self.to_anthropic())
231    }
232}
233
234impl<'de> Deserialize<'de> for StopReason {
235    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
236        let s = String::deserialize(deserializer)?;
237        Ok(Self::from_anthropic(&s))
238    }
239}