1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Arc;
13
14use crate::utils::event_stream::AssistantMessageEventStream;
15
16pub type Api = String;
17pub type ProviderId = String;
18pub type ImagesApi = String;
19pub type ImagesProviderId = String;
20pub type ProviderEnv = HashMap<String, String>;
21pub type ProviderHeaders = HashMap<String, Option<String>>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum ThinkingLevel {
26 Minimal,
27 Low,
28 Medium,
29 High,
30 Xhigh,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ModelThinkingLevel {
35 Off,
36 Level(ThinkingLevel),
37}
38
39pub type ThinkingLevelMap = HashMap<String, Option<String>>;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum CacheRetention {
44 None,
45 Short,
46 Long,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51pub enum Transport {
52 Sse,
53 Websocket,
54 #[serde(rename = "websocket-cached")]
55 WebsocketCached,
56 Auto,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ThinkingBudgets {
61 pub minimal: Option<u32>,
62 pub low: Option<u32>,
63 pub medium: Option<u32>,
64 pub high: Option<u32>,
65}
66
67#[derive(Debug, Clone)]
68pub struct ProviderResponse {
69 pub status: u16,
70 pub headers: HashMap<String, String>,
71}
72
73#[derive(Clone, Default)]
74pub struct StreamOptions {
75 pub temperature: Option<f64>,
76 pub max_tokens: Option<u32>,
77 pub api_key: Option<String>,
78 pub transport: Option<Transport>,
79 pub cache_retention: Option<CacheRetention>,
80 pub session_id: Option<String>,
81 pub headers: Option<ProviderHeaders>,
82 pub timeout_ms: Option<u64>,
83 pub websocket_connect_timeout_ms: Option<u64>,
84 pub max_retries: Option<u32>,
85 pub max_retry_delay_ms: Option<u64>,
86 pub metadata: Option<HashMap<String, Value>>,
87 pub env: Option<ProviderEnv>,
88 pub on_payload: Option<OnPayloadCallback>,
89 pub on_response: Option<OnResponseCallback>,
90 pub signal: Option<tokio_util::sync::CancellationToken>,
91}
92
93pub type OnPayloadCallback =
94 Arc<dyn Fn(Value, Model) -> Pin<Box<dyn Future<Output = Option<Value>> + Send>> + Send + Sync>;
95pub type OnResponseCallback =
96 Arc<dyn Fn(ProviderResponse, Model) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
97
98#[derive(Clone)]
99pub struct SimpleStreamOptions {
100 pub base: StreamOptions,
101 pub reasoning: Option<ThinkingLevel>,
102 pub thinking_budgets: Option<ThinkingBudgets>,
103}
104
105impl SimpleStreamOptions {
106 pub fn from_stream(options: StreamOptions) -> Self {
107 Self {
108 base: options,
109 reasoning: None,
110 thinking_budgets: None,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
116pub struct TextContent {
117 #[serde(rename = "type")]
118 pub kind: String,
119 pub text: String,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub text_signature: Option<String>,
122}
123
124impl TextContent {
125 pub fn new(text: impl Into<String>) -> Self {
126 Self {
127 kind: "text".to_string(),
128 text: text.into(),
129 text_signature: None,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
135pub struct ThinkingContent {
136 #[serde(rename = "type")]
137 pub kind: String,
138 pub thinking: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub thinking_signature: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub redacted: Option<bool>,
143}
144
145impl ThinkingContent {
146 pub fn new(thinking: impl Into<String>) -> Self {
147 Self {
148 kind: "thinking".to_string(),
149 thinking: thinking.into(),
150 thinking_signature: None,
151 redacted: None,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
157pub struct ImageContent {
158 #[serde(rename = "type")]
159 pub kind: String,
160 pub data: String,
161 pub mime_type: String,
162}
163
164impl ImageContent {
165 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
166 Self {
167 kind: "image".to_string(),
168 data: data.into(),
169 mime_type: mime_type.into(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
175pub struct ToolCall {
176 #[serde(rename = "type")]
177 pub kind: String,
178 pub id: String,
179 pub name: String,
180 pub arguments: Value,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub thought_signature: Option<String>,
183}
184
185impl ToolCall {
186 pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
187 Self {
188 kind: "toolCall".to_string(),
189 id: id.into(),
190 name: name.into(),
191 arguments,
192 thought_signature: None,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Default, Serialize, Deserialize)]
198pub struct UsageCost {
199 pub input: f64,
200 pub output: f64,
201 pub cache_read: f64,
202 pub cache_write: f64,
203 pub total: f64,
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize)]
207pub struct Usage {
208 pub input: u64,
209 pub output: u64,
210 pub cache_read: u64,
211 pub cache_write: u64,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub cache_write_1h: Option<u64>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub reasoning: Option<u64>,
216 pub total_tokens: u64,
217 pub cost: UsageCost,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub enum StopReason {
223 Stop,
224 Length,
225 #[serde(rename = "toolUse")]
226 ToolUse,
227 Error,
228 Aborted,
229}
230
231#[allow(clippy::large_enum_variant)]
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(tag = "role", rename_all = "camelCase")]
234pub enum Message {
235 User {
236 content: UserContent,
237 timestamp: i64,
238 },
239 Assistant(AssistantMessage),
240 ToolResult {
241 tool_call_id: String,
242 tool_name: String,
243 content: Vec<ContentBlock>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 details: Option<Value>,
246 is_error: bool,
247 timestamp: i64,
248 },
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[serde(untagged)]
253pub enum UserContent {
254 Text(String),
255 Blocks(Vec<ContentBlock>),
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259#[serde(tag = "type", rename_all = "camelCase")]
260pub enum ContentBlock {
261 #[serde(rename = "text")]
262 Text { text: String },
263 #[serde(rename = "image")]
264 Image { data: String, mime_type: String },
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct AssistantMessage {
269 pub role: String,
270 pub content: Vec<AssistantContentBlock>,
271 pub api: Api,
272 pub provider: ProviderId,
273 pub model: String,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub response_model: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub response_id: Option<String>,
278 pub usage: Usage,
279 pub stop_reason: StopReason,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub error_message: Option<String>,
282 pub timestamp: i64,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(untagged)]
287pub enum AssistantContentBlock {
288 Text(TextContent),
289 Thinking(ThinkingContent),
290 ToolCall(ToolCall),
291}
292
293impl AssistantMessage {
294 pub fn empty(model: &Model) -> Self {
295 Self {
296 role: "assistant".to_string(),
297 content: vec![],
298 api: model.api.clone(),
299 provider: model.provider.clone(),
300 model: model.id.clone(),
301 response_model: None,
302 response_id: None,
303 usage: Usage::default(),
304 stop_reason: StopReason::Stop,
305 error_message: None,
306 timestamp: chrono::Utc::now().timestamp_millis(),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
312pub struct Tool {
313 pub name: String,
314 pub description: String,
315 pub parameters: Value,
316}
317
318#[derive(Debug, Clone)]
319pub struct Context {
320 pub system_prompt: Option<String>,
321 pub messages: Vec<Message>,
322 pub tools: Option<Vec<Tool>>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct TextSignatureV1 {
327 pub v: u8,
328 pub id: String,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub phase: Option<String>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[serde(tag = "type", rename_all = "snake_case")]
335pub enum AssistantMessageEvent {
336 Start {
337 partial: AssistantMessage,
338 },
339 TextStart {
340 content_index: usize,
341 partial: AssistantMessage,
342 },
343 TextDelta {
344 content_index: usize,
345 delta: String,
346 partial: AssistantMessage,
347 },
348 TextEnd {
349 content_index: usize,
350 content: String,
351 partial: AssistantMessage,
352 },
353 ThinkingStart {
354 content_index: usize,
355 partial: AssistantMessage,
356 },
357 ThinkingDelta {
358 content_index: usize,
359 delta: String,
360 partial: AssistantMessage,
361 },
362 ThinkingEnd {
363 content_index: usize,
364 content: String,
365 partial: AssistantMessage,
366 },
367 ToolcallStart {
368 content_index: usize,
369 partial: AssistantMessage,
370 },
371 ToolcallDelta {
372 content_index: usize,
373 delta: String,
374 partial: AssistantMessage,
375 },
376 ToolcallEnd {
377 content_index: usize,
378 tool_call: ToolCall,
379 partial: AssistantMessage,
380 },
381 Done {
382 reason: StopReason,
383 message: AssistantMessage,
384 },
385 Error {
386 reason: StopReason,
387 error: AssistantMessage,
388 },
389}
390
391#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct OpenAICompletionsCompat {
394 pub supports_store: Option<bool>,
395 pub supports_developer_role: Option<bool>,
396 pub supports_reasoning_effort: Option<bool>,
397 pub supports_usage_in_streaming: Option<bool>,
398 pub max_tokens_field: Option<String>,
399 pub requires_tool_result_name: Option<bool>,
400 pub requires_assistant_after_tool_result: Option<bool>,
401 pub requires_thinking_as_text: Option<bool>,
402 pub requires_reasoning_content_on_assistant_messages: Option<bool>,
403 pub thinking_format: Option<String>,
404 pub zai_tool_stream: Option<bool>,
405 pub supports_strict_mode: Option<bool>,
406 pub cache_control_format: Option<String>,
407 pub send_session_affinity_headers: Option<bool>,
408 pub supports_long_cache_retention: Option<bool>,
409}
410
411#[derive(Debug, Clone, Default, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct OpenAIResponsesCompat {
414 pub supports_developer_role: Option<bool>,
415 pub send_session_id_header: Option<bool>,
416 pub supports_long_cache_retention: Option<bool>,
417}
418
419#[derive(Debug, Clone, Default, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub struct AnthropicMessagesCompat {
422 pub supports_eager_tool_input_streaming: Option<bool>,
423 pub supports_long_cache_retention: Option<bool>,
424 pub send_session_affinity_headers: Option<bool>,
425 pub supports_cache_control_on_tools: Option<bool>,
426 pub supports_temperature: Option<bool>,
427 pub force_adaptive_thinking: Option<bool>,
428 pub allow_empty_signature: Option<bool>,
429}
430
431#[derive(Debug, Clone)]
432pub struct Model {
433 pub id: String,
434 pub name: String,
435 pub api: Api,
436 pub provider: ProviderId,
437 pub base_url: String,
438 pub reasoning: bool,
439 pub thinking_level_map: Option<ThinkingLevelMap>,
440 pub input: Vec<String>,
441 pub cost: ModelCost,
442 pub context_window: u32,
443 pub max_tokens: u32,
444 pub headers: Option<HashMap<String, String>>,
445 pub openai_completions_compat: Option<OpenAICompletionsCompat>,
446 pub openai_responses_compat: Option<OpenAIResponsesCompat>,
447 pub anthropic_compat: Option<AnthropicMessagesCompat>,
448}
449
450#[derive(Debug, Clone, Copy)]
451pub struct ModelCost {
452 pub input: f64,
453 pub output: f64,
454 pub cache_read: f64,
455 pub cache_write: f64,
456}
457
458#[derive(Debug, Clone)]
459pub struct ImagesModel {
460 pub id: String,
461 pub name: String,
462 pub api: ImagesApi,
463 pub provider: ImagesProviderId,
464 pub base_url: String,
465 pub input: Vec<String>,
466 pub output: Vec<String>,
467 pub cost: ModelCost,
468 pub headers: Option<HashMap<String, String>>,
469}
470
471#[derive(Debug, Clone)]
472pub struct ImagesContext {
473 pub input: Vec<ContentBlock>,
474}
475
476#[derive(Clone)]
477pub struct ImagesOptions {
478 pub api_key: Option<String>,
479 pub signal: Option<tokio_util::sync::CancellationToken>,
480 pub env: Option<ProviderEnv>,
481 pub headers: Option<ProviderHeaders>,
482 pub timeout_ms: Option<u64>,
483 pub max_retries: Option<u32>,
484 pub on_payload: Option<OnPayloadCallback>,
485 pub on_response: Option<OnResponseCallback>,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct AssistantImages {
490 pub api: ImagesApi,
491 pub provider: ImagesProviderId,
492 pub model: String,
493 pub output: Vec<ContentBlock>,
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub response_id: Option<String>,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub usage: Option<Usage>,
498 pub stop_reason: StopReason,
499 #[serde(skip_serializing_if = "Option::is_none")]
500 pub error_message: Option<String>,
501 pub timestamp: i64,
502}
503
504pub trait ProviderStreams: Send + Sync {
506 fn stream(&self, model: &Model, context: &Context, options: Option<StreamOptions>) -> AssistantMessageEventStream;
507
508 fn stream_simple(
509 &self,
510 model: &Model,
511 context: &Context,
512 options: Option<SimpleStreamOptions>,
513 ) -> AssistantMessageEventStream;
514}
515
516pub trait ProviderImages: Send + Sync {
517 fn generate_images(
518 &self,
519 model: &ImagesModel,
520 context: &ImagesContext,
521 options: Option<ImagesOptions>,
522 ) -> Pin<Box<dyn Future<Output = AssistantImages> + Send>>;
523}
524
525impl Message {
527 pub fn role(&self) -> &'static str {
528 match self {
529 Message::User { .. } => "user",
530 Message::Assistant(_) => "assistant",
531 Message::ToolResult { .. } => "toolResult",
532 }
533 }
534
535 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
536 match self {
537 Message::Assistant(m) => Some(m),
538 _ => None,
539 }
540 }
541}
542
543impl AssistantContentBlock {
544 pub fn is_text(&self) -> bool {
545 matches!(self, AssistantContentBlock::Text(_))
546 }
547
548 pub fn is_thinking(&self) -> bool {
549 matches!(self, AssistantContentBlock::Thinking(_))
550 }
551
552 pub fn is_tool_call(&self) -> bool {
553 matches!(self, AssistantContentBlock::ToolCall(_))
554 }
555}