1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::typed_id::{ModelId, ProviderId};
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(ToSchema))]
17#[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
18#[serde(rename_all = "snake_case")]
19pub enum LlmProviderType {
20 Openai,
22 Openrouter,
24 #[serde(rename = "azure_openai")]
26 AzureOpenai,
27 #[serde(rename = "openai_completions")]
29 OpenaiCompletions,
30 Anthropic,
31 Gemini,
33 #[serde(rename = "llmsim")]
35 LlmSim,
36 Bedrock,
38}
39
40impl std::fmt::Display for LlmProviderType {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 LlmProviderType::Openai => write!(f, "openai"),
44 LlmProviderType::Openrouter => write!(f, "openrouter"),
45 LlmProviderType::AzureOpenai => write!(f, "azure_openai"),
46 LlmProviderType::OpenaiCompletions => write!(f, "openai_completions"),
47 LlmProviderType::Anthropic => write!(f, "anthropic"),
48 LlmProviderType::Gemini => write!(f, "gemini"),
49 LlmProviderType::LlmSim => write!(f, "llmsim"),
50 LlmProviderType::Bedrock => write!(f, "bedrock"),
51 }
52 }
53}
54
55impl std::str::FromStr for LlmProviderType {
56 type Err = String;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 match s {
60 "openai" => Ok(LlmProviderType::Openai),
61 "openrouter" => Ok(LlmProviderType::Openrouter),
62 "azure_openai" => Ok(LlmProviderType::AzureOpenai),
63 "openai_completions" => Ok(LlmProviderType::OpenaiCompletions),
64 "anthropic" => Ok(LlmProviderType::Anthropic),
65 "gemini" => Ok(LlmProviderType::Gemini),
66 "llmsim" => Ok(LlmProviderType::LlmSim),
67 "bedrock" => Ok(LlmProviderType::Bedrock),
68 _ => Err(format!("Unknown provider type: {}", s)),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75#[cfg_attr(feature = "openapi", derive(ToSchema))]
76#[serde(rename_all = "snake_case")]
77pub enum LlmProviderStatus {
78 Active,
79 Disabled,
80}
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
90#[cfg_attr(feature = "openapi", derive(ToSchema))]
91#[cfg_attr(feature = "openapi", schema(example = "predefined"))]
92#[serde(rename_all = "snake_case")]
93pub enum LlmModelSource {
94 #[default]
96 Manual,
97 Discovered,
99 Predefined,
101}
102
103impl std::fmt::Display for LlmModelSource {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 LlmModelSource::Manual => write!(f, "manual"),
107 LlmModelSource::Discovered => write!(f, "discovered"),
108 LlmModelSource::Predefined => write!(f, "predefined"),
109 }
110 }
111}
112
113impl std::str::FromStr for LlmModelSource {
114 type Err = String;
115
116 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 match s {
118 "manual" => Ok(LlmModelSource::Manual),
119 "discovered" => Ok(LlmModelSource::Discovered),
120 "predefined" => Ok(LlmModelSource::Predefined),
121 _ => Err(format!("Unknown model source: {}", s)),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "openapi", derive(ToSchema))]
130pub struct LlmProvider {
131 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
133 pub id: ProviderId,
134 pub name: String,
136 pub provider_type: LlmProviderType,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub base_url: Option<String>,
141 pub api_key_set: bool,
143 pub status: LlmProviderStatus,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub last_synced_at: Option<DateTime<Utc>>,
148 pub created_at: DateTime<Utc>,
150 pub updated_at: DateTime<Utc>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156#[cfg_attr(feature = "openapi", derive(ToSchema))]
157pub struct LlmModel {
158 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
160 pub id: ModelId,
161 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
163 pub provider_id: ProviderId,
164 pub model_id: String,
166 pub display_name: String,
168 pub capabilities: Vec<String>,
170 pub is_favorite: bool,
172 pub enabled: bool,
174 pub source: LlmModelSource,
176 pub created_at: DateTime<Utc>,
178 pub updated_at: DateTime<Utc>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "openapi", derive(ToSchema))]
185pub struct LlmModelWithProvider {
186 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
188 pub id: ModelId,
189 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
191 pub provider_id: ProviderId,
192 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
194 pub model_id: String,
195 #[cfg_attr(feature = "openapi", schema(example = "Claude Sonnet 4.5"))]
197 pub display_name: String,
198 #[cfg_attr(feature = "openapi", schema(example = json!(["text", "tools", "vision", "thinking"])))]
200 pub capabilities: Vec<String>,
201 #[cfg_attr(feature = "openapi", schema(example = true))]
203 pub is_favorite: bool,
204 #[cfg_attr(feature = "openapi", schema(example = true))]
206 pub enabled: bool,
207 #[cfg_attr(feature = "openapi", schema(example = "predefined"))]
209 pub source: LlmModelSource,
210 #[cfg_attr(feature = "openapi", schema(example = "2026-01-04T11:23:00Z"))]
212 pub created_at: DateTime<Utc>,
213 #[cfg_attr(feature = "openapi", schema(example = "2026-05-27T15:24:00Z"))]
215 pub updated_at: DateTime<Utc>,
216 #[cfg_attr(feature = "openapi", schema(example = "Anthropic"))]
218 pub provider_name: String,
219 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
221 pub provider_type: LlmProviderType,
222 #[cfg_attr(feature = "openapi", schema(example = true))]
226 pub healthy: bool,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub profile: Option<LlmModelProfile>,
230 #[serde(skip_serializing_if = "Option::is_none")]
233 pub model_vendor: Option<ModelVendor>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "openapi", derive(ToSchema))]
244pub struct LlmModelCost {
245 pub input: f64,
247 pub output: f64,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub cache_read: Option<f64>,
252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 pub cost_tiers: Vec<CostTier>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
262#[cfg_attr(feature = "openapi", derive(ToSchema))]
263pub struct CostTier {
264 pub above_tokens: i32,
266 pub input: f64,
268 pub output: f64,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub cache_read: Option<f64>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277#[cfg_attr(feature = "openapi", derive(ToSchema))]
278pub struct LlmModelLimits {
279 pub context: i32,
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub input: Option<i32>,
284 pub output: i32,
286 #[serde(skip_serializing_if = "Option::is_none", default)]
288 pub max_media: Option<i32>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293#[cfg_attr(feature = "openapi", derive(ToSchema))]
294#[serde(rename_all = "snake_case")]
295pub enum Modality {
296 Text,
297 Image,
298 Audio,
299 Video,
300 Pdf,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305#[cfg_attr(feature = "openapi", derive(ToSchema))]
306pub struct LlmModelModalities {
307 pub input: Vec<Modality>,
309 pub output: Vec<Modality>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
315#[cfg_attr(feature = "openapi", derive(ToSchema))]
316#[serde(rename_all = "snake_case")]
317pub enum ReasoningEffort {
318 None,
319 Minimal,
320 Low,
321 Medium,
322 High,
323 Xhigh,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328#[cfg_attr(feature = "openapi", derive(ToSchema))]
329pub struct ReasoningEffortValue {
330 pub value: ReasoningEffort,
332 pub name: String,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338#[cfg_attr(feature = "openapi", derive(ToSchema))]
339pub struct ReasoningEffortConfig {
340 pub values: Vec<ReasoningEffortValue>,
342 pub default: ReasoningEffort,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350#[cfg_attr(feature = "openapi", derive(ToSchema))]
351#[serde(rename_all = "lowercase")]
352pub enum ModelVendor {
353 OpenAi,
354 Anthropic,
355 Google,
356 Nvidia,
357 Qwen,
358 Microsoft,
359 MiniMax,
360 Moonshot,
361 XAi,
362 LlmSim,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
374#[cfg_attr(feature = "openapi", derive(ToSchema))]
375pub struct LlmModelProfile {
376 pub name: String,
378 pub family: String,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub description: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub release_date: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub last_updated: Option<String>,
389 pub attachment: bool,
391 pub reasoning: bool,
393 pub temperature: bool,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub knowledge: Option<String>,
398 pub tool_call: bool,
400 pub structured_output: bool,
402 pub open_weights: bool,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub cost: Option<LlmModelCost>,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub limits: Option<LlmModelLimits>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub modalities: Option<LlmModelModalities>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub reasoning_effort: Option<ReasoningEffortConfig>,
416 #[serde(default)]
420 pub tool_search: bool,
421 #[serde(default, skip_serializing_if = "Vec::is_empty")]
423 pub supported_parameters: Vec<String>,
424 #[serde(default)]
428 pub supports_phases: bool,
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn test_llm_provider_type_serialization() {
437 assert_eq!(
439 serde_json::to_string(&LlmProviderType::Openai).unwrap(),
440 "\"openai\""
441 );
442 assert_eq!(
443 serde_json::to_string(&LlmProviderType::Openrouter).unwrap(),
444 "\"openrouter\""
445 );
446 assert_eq!(
447 serde_json::to_string(&LlmProviderType::OpenaiCompletions).unwrap(),
448 "\"openai_completions\""
449 );
450 assert_eq!(
451 serde_json::to_string(&LlmProviderType::AzureOpenai).unwrap(),
452 "\"azure_openai\""
453 );
454 assert_eq!(
455 serde_json::to_string(&LlmProviderType::Anthropic).unwrap(),
456 "\"anthropic\""
457 );
458 assert_eq!(
459 serde_json::to_string(&LlmProviderType::Gemini).unwrap(),
460 "\"gemini\""
461 );
462 assert_eq!(
463 serde_json::to_string(&LlmProviderType::LlmSim).unwrap(),
464 "\"llmsim\""
465 );
466 }
467
468 #[test]
469 fn test_llm_provider_type_deserialization() {
470 assert!(matches!(
472 serde_json::from_str::<LlmProviderType>("\"openai\"").unwrap(),
473 LlmProviderType::Openai
474 ));
475 assert!(matches!(
476 serde_json::from_str::<LlmProviderType>("\"openrouter\"").unwrap(),
477 LlmProviderType::Openrouter
478 ));
479 assert!(matches!(
480 serde_json::from_str::<LlmProviderType>("\"openai_completions\"").unwrap(),
481 LlmProviderType::OpenaiCompletions
482 ));
483 assert!(matches!(
484 serde_json::from_str::<LlmProviderType>("\"azure_openai\"").unwrap(),
485 LlmProviderType::AzureOpenai
486 ));
487 assert!(matches!(
488 serde_json::from_str::<LlmProviderType>("\"anthropic\"").unwrap(),
489 LlmProviderType::Anthropic
490 ));
491 assert!(matches!(
492 serde_json::from_str::<LlmProviderType>("\"gemini\"").unwrap(),
493 LlmProviderType::Gemini
494 ));
495 assert!(matches!(
496 serde_json::from_str::<LlmProviderType>("\"llmsim\"").unwrap(),
497 LlmProviderType::LlmSim
498 ));
499 }
500
501 #[test]
502 fn test_llm_provider_type_from_str() {
503 assert!(matches!(
505 "openai".parse::<LlmProviderType>().unwrap(),
506 LlmProviderType::Openai
507 ));
508 assert!(matches!(
509 "openrouter".parse::<LlmProviderType>().unwrap(),
510 LlmProviderType::Openrouter
511 ));
512 assert!(matches!(
513 "openai_completions".parse::<LlmProviderType>().unwrap(),
514 LlmProviderType::OpenaiCompletions
515 ));
516 assert!(matches!(
517 "azure_openai".parse::<LlmProviderType>().unwrap(),
518 LlmProviderType::AzureOpenai
519 ));
520 assert!(matches!(
521 "anthropic".parse::<LlmProviderType>().unwrap(),
522 LlmProviderType::Anthropic
523 ));
524 assert!(matches!(
525 "gemini".parse::<LlmProviderType>().unwrap(),
526 LlmProviderType::Gemini
527 ));
528 assert!(matches!(
529 "llmsim".parse::<LlmProviderType>().unwrap(),
530 LlmProviderType::LlmSim
531 ));
532 }
533
534 #[test]
535 fn test_llm_model_limits_input_omitted_when_none() {
536 let limits = LlmModelLimits {
537 context: 200_000,
538 input: None,
539 output: 64_000,
540 max_media: None,
541 };
542 let json = serde_json::to_value(&limits).unwrap();
543 assert!(!json.as_object().unwrap().contains_key("input"));
544 }
545
546 #[test]
547 fn test_llm_model_limits_input_included_when_some() {
548 let limits = LlmModelLimits {
549 context: 200_000,
550 input: Some(150_000),
551 output: 64_000,
552 max_media: None,
553 };
554 let json = serde_json::to_value(&limits).unwrap();
555 assert_eq!(json["input"], 150_000);
556 }
557
558 #[test]
559 fn test_llm_model_limits_deserialize_without_input() {
560 let json = r#"{"context": 200000, "output": 64000}"#;
561 let limits: LlmModelLimits = serde_json::from_str(json).unwrap();
562 assert_eq!(limits.context, 200_000);
563 assert!(limits.input.is_none());
564 assert_eq!(limits.output, 64_000);
565 }
566
567 #[test]
568 fn test_llm_provider_type_display() {
569 assert_eq!(LlmProviderType::Openai.to_string(), "openai");
571 assert_eq!(LlmProviderType::Openrouter.to_string(), "openrouter");
572 assert_eq!(LlmProviderType::AzureOpenai.to_string(), "azure_openai");
573 assert_eq!(
574 LlmProviderType::OpenaiCompletions.to_string(),
575 "openai_completions"
576 );
577 assert_eq!(LlmProviderType::Anthropic.to_string(), "anthropic");
578 assert_eq!(LlmProviderType::Gemini.to_string(), "gemini");
579 assert_eq!(LlmProviderType::LlmSim.to_string(), "llmsim");
580 }
581}