Skip to main content

deepseek/
types.rs

1use serde::{Deserialize, Serialize};
2
3// ── DeepSeek Models ───────────────────────────────────────────────────────
4
5/// DeepSeek model identifiers — parity with Anthropic's multi-model routing
6#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
7pub enum DeepSeekModel {
8    /// DeepSeek V4-Pro — flagship with thinking mode + reasoning_effort.
9    #[serde(rename = "deepseek-v4-pro")]
10    #[default]
11    V4Pro,
12    /// DeepSeek-R1 — kept for back-compat. New code should prefer V4Pro.
13    #[serde(rename = "deepseek-reasoner")]
14    Reasoner,
15    /// DeepSeek-V3 — kept for back-compat.
16    #[serde(rename = "deepseek-chat")]
17    Chat,
18}
19
20impl DeepSeekModel {
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            Self::V4Pro => "deepseek-v4-pro",
24            Self::Reasoner => "deepseek-reasoner",
25            Self::Chat => "deepseek-chat",
26        }
27    }
28
29    pub fn from_alias(alias: &str) -> Self {
30        match alias {
31            "v4" | "v4-pro" | "pro" | "latest" => Self::V4Pro,
32            "reasoner" | "r1" | "deep" | "opus" => Self::Reasoner,
33            "chat" | "v3" | "fast" | "sonnet" | "haiku" => Self::Chat,
34            _ => Self::V4Pro,
35        }
36    }
37}
38
39// ── DeepSeek API Types (OpenAI-compatible) ────────────────────────────────
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ChatMessage {
43    pub role: String,
44    pub content: ChatContent,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub reasoning_content: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub tool_calls: Option<Vec<ToolCall>>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub tool_call_id: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub name: Option<String>,
53}
54
55#[derive(Debug, Clone)]
56pub enum ChatContent {
57    Text(String),
58    Null,
59}
60
61impl ChatContent {
62    pub fn as_str(&self) -> &str {
63        match self {
64            Self::Text(s) => s,
65            Self::Null => "",
66        }
67    }
68}
69
70impl Serialize for ChatContent {
71    fn serialize<S: serde::Serializer>(
72        &self,
73        serializer: S,
74    ) -> std::result::Result<S::Ok, S::Error> {
75        match self {
76            Self::Text(s) => serializer.serialize_str(s),
77            Self::Null => serializer.serialize_none(),
78        }
79    }
80}
81
82impl<'de> Deserialize<'de> for ChatContent {
83    fn deserialize<D: serde::Deserializer<'de>>(
84        deserializer: D,
85    ) -> std::result::Result<Self, D::Error> {
86        let value = serde_json::Value::deserialize(deserializer)?;
87        match value {
88            serde_json::Value::String(s) => Ok(Self::Text(s)),
89            serde_json::Value::Null => Ok(Self::Null),
90            other => Ok(Self::Text(other.to_string())),
91        }
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ToolCall {
97    pub id: String,
98    pub r#type: String,
99    pub function: FunctionCall,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct FunctionCall {
104    pub name: String,
105    pub arguments: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ChatRequest {
110    pub model: String,
111    pub messages: Vec<ChatMessage>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub tools: Option<Vec<ToolSchema>>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub tool_choice: Option<serde_json::Value>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub temperature: Option<f64>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub max_tokens: Option<u32>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub stream: Option<bool>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub reasoning_effort: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub thinking: Option<serde_json::Value>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ToolSchema {
130    pub r#type: String,
131    pub function: FunctionSchema,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FunctionSchema {
136    pub name: String,
137    pub description: String,
138    pub parameters: serde_json::Value,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ChatResponse {
143    pub id: String,
144    pub choices: Vec<Choice>,
145    pub usage: Option<UsageInfo>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct Choice {
150    pub index: u32,
151    pub message: ChatMessage,
152    pub finish_reason: Option<String>,
153}
154
155#[derive(Debug, Clone, Default, Deserialize, Serialize)]
156pub struct UsageInfo {
157    pub prompt_tokens: u32,
158    pub completion_tokens: u32,
159    pub total_tokens: u32,
160    /// Prompt tokens served from DeepSeek's context cache. Charged at the
161    /// cache-hit input rate (typically ~25% of cache-miss). Returned by the
162    /// API in the `usage` object; absent for models/responses that don't
163    /// report cache stats.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub prompt_cache_hit_tokens: Option<u32>,
166    /// Prompt tokens that missed the context cache. Charged at the full input
167    /// rate. `prompt_cache_hit_tokens + prompt_cache_miss_tokens` should equal
168    /// `prompt_tokens` when both are reported.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub prompt_cache_miss_tokens: Option<u32>,
171}
172
173// ── Agent Result (parity with Anthropic AgentResult) ──────────────────────
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct AgentResult {
177    pub success: bool,
178    pub result: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub error: Option<String>,
181    pub turns: u32,
182    pub usage: UsageInfo,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub tool_calls_made: Option<Vec<String>>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub session_id: Option<String>,
187}
188
189// ── Agent Definition (parity with Anthropic AgentDefinition) ──────────────
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct AgentDefinition {
193    pub name: String,
194    pub description: String,
195    pub prompt: String,
196    #[serde(default)]
197    pub model: DeepSeekModel,
198    #[serde(default)]
199    pub tools: Vec<String>,
200    #[serde(default)]
201    pub disallowed_tools: Vec<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub max_turns: Option<u32>,
204}
205
206// ── Effort Level (parity with Anthropic adaptive thinking) ────────────────
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub enum EffortLevel {
210    Low,
211    Medium,
212    #[default]
213    High,
214    Max,
215}
216
217impl EffortLevel {
218    /// Map effort to DeepSeek temperature (lower = more focused)
219    pub fn temperature(&self) -> f64 {
220        match self {
221            Self::Low => 0.1,
222            Self::Medium => 0.5,
223            Self::High => 0.7,
224            Self::Max => 1.0,
225        }
226    }
227
228    /// Map effort to max tokens
229    pub fn max_tokens(&self) -> u32 {
230        match self {
231            Self::Low => 2048,
232            Self::Medium => 4096,
233            Self::High => 8192,
234            Self::Max => 16384,
235        }
236    }
237}
238
239// ── Message constructors ──────────────────────────────────────────────────
240
241pub fn system_msg(content: &str) -> ChatMessage {
242    ChatMessage {
243        role: "system".into(),
244        content: ChatContent::Text(content.into()),
245        reasoning_content: None,
246        tool_calls: None,
247        tool_call_id: None,
248        name: None,
249    }
250}
251
252pub fn user_msg(content: &str) -> ChatMessage {
253    ChatMessage {
254        role: "user".into(),
255        content: ChatContent::Text(content.into()),
256        reasoning_content: None,
257        tool_calls: None,
258        tool_call_id: None,
259        name: None,
260    }
261}
262
263pub fn assistant_msg(content: &str) -> ChatMessage {
264    ChatMessage {
265        role: "assistant".into(),
266        content: ChatContent::Text(content.into()),
267        reasoning_content: None,
268        tool_calls: None,
269        tool_call_id: None,
270        name: None,
271    }
272}
273
274// ── Reasoner output ───────────────────────────────────────────────────────
275
276pub struct ReasonerOutput {
277    /// R1 chain-of-thought — logged at DEBUG, not passed to next agent.
278    pub reasoning: String,
279    /// Final answer — passed forward in the pipeline.
280    pub content: String,
281}
282
283pub fn tool_result_msg(tool_call_id: &str, content: &str) -> ChatMessage {
284    ChatMessage {
285        role: "tool".into(),
286        content: ChatContent::Text(content.into()),
287        reasoning_content: None,
288        tool_calls: None,
289        tool_call_id: Some(tool_call_id.into()),
290        name: None,
291    }
292}