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