Skip to main content

everruns_core/
llm_models.rs

1// LLM Provider and Model entity types
2//
3// These types represent the database entities for LLM providers and models.
4// Note: This is separate from llm.rs which defines the LlmProvider trait.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::typed_id::{ModelId, ProviderId};
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14/// LLM provider type
15#[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 using Open Responses API (<https://www.openresponses.org/>)
21    Openai,
22    /// OpenRouter using the OpenAI-compatible Responses API
23    Openrouter,
24    /// Azure OpenAI using the Azure-hosted OpenAI v1 API
25    #[serde(rename = "azure_openai")]
26    AzureOpenai,
27    /// OpenAI using Chat Completions API (for backward compatibility)
28    #[serde(rename = "openai_completions")]
29    OpenaiCompletions,
30    Anthropic,
31    /// Google Gemini API
32    Gemini,
33    /// LLM simulator for testing
34    #[serde(rename = "llmsim")]
35    LlmSim,
36    /// AWS Bedrock Runtime (ConverseStream API)
37    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/// LLM provider status
74#[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// LLM model "healthy" status is not persisted on the model row. It is
83// derived at read time from the joined provider's state and exposed as a
84// boolean on `LlmModelWithProvider`. The per-row `enabled` flag is the only
85// persisted user-facing toggle, and it controls visibility in UI model
86// pickers.
87
88/// How the model was added to the system
89#[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    /// User-created via API or UI
95    #[default]
96    Manual,
97    /// Automatically discovered from provider's list_models API
98    Discovered,
99    /// From hardcoded seed data
100    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/// LLM Provider entity (API keys never exposed)
127/// Note: This is the entity struct, separate from the LlmProvider trait in llm.rs
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "openapi", derive(ToSchema))]
130pub struct LlmProvider {
131    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
132    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
133    pub id: ProviderId,
134    /// Human-readable provider name. Safe to render in user-facing messages.
135    pub name: String,
136    /// Provider implementation type (OpenAI, Anthropic, Gemini, etc.).
137    pub provider_type: LlmProviderType,
138    /// Custom base URL for self-hosted / proxied providers. `None` means use the provider's default endpoint.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub base_url: Option<String>,
141    /// Whether an API key is configured. The key itself is never returned.
142    pub api_key_set: bool,
143    /// Current lifecycle status of this provider.
144    pub status: LlmProviderStatus,
145    /// Timestamp of the most recent successful model sync from the provider's API (RFC 3339).
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub last_synced_at: Option<DateTime<Utc>>,
148    /// Timestamp when this provider was created (RFC 3339).
149    pub created_at: DateTime<Utc>,
150    /// Timestamp when this provider was last updated (RFC 3339).
151    pub updated_at: DateTime<Utc>,
152}
153
154/// LLM Model entity
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[cfg_attr(feature = "openapi", derive(ToSchema))]
157pub struct LlmModel {
158    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
159    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
160    pub id: ModelId,
161    /// Owning provider's prefixed public identifier.
162    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
163    pub provider_id: ProviderId,
164    /// Provider-side model identifier as sent on the wire (e.g. `gpt-4o`, `claude-sonnet-4`).
165    pub model_id: String,
166    /// Human-readable display name. Safe to render in user-facing messages.
167    pub display_name: String,
168    /// Capability tags supported by this model (e.g. `chat`, `tools`, `vision`).
169    pub capabilities: Vec<String>,
170    /// Whether this model is starred in the UI for quick access.
171    pub is_favorite: bool,
172    /// Whether this model is selectable. Controls UI visibility AND server-side resolution: `LlmResolverService` requires `enabled = true`, and org default-model validation rejects disabled models. Disabled models stay visible in raw list endpoints (so admins can re-enable them) but cannot be used in active sessions or as a session/agent default.
173    pub enabled: bool,
174    /// How this model entry was added (manually, discovered, or seeded as predefined).
175    pub source: LlmModelSource,
176    /// Timestamp when this model was created (RFC 3339).
177    pub created_at: DateTime<Utc>,
178    /// Timestamp when this model was last updated (RFC 3339).
179    pub updated_at: DateTime<Utc>,
180}
181
182/// LLM Model with provider info
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "openapi", derive(ToSchema))]
185pub struct LlmModelWithProvider {
186    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
187    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
188    pub id: ModelId,
189    /// Owning provider's prefixed public identifier.
190    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
191    pub provider_id: ProviderId,
192    /// Provider-side model identifier as sent on the wire (e.g. `gpt-4o`).
193    #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
194    pub model_id: String,
195    /// Human-readable display name.
196    #[cfg_attr(feature = "openapi", schema(example = "Claude Sonnet 4.5"))]
197    pub display_name: String,
198    /// Capability tags supported by this model.
199    #[cfg_attr(feature = "openapi", schema(example = json!(["text", "tools", "vision", "thinking"])))]
200    pub capabilities: Vec<String>,
201    /// Whether this model is starred in the UI for quick access.
202    #[cfg_attr(feature = "openapi", schema(example = true))]
203    pub is_favorite: bool,
204    /// Whether this model is selectable. Controls UI visibility AND server-side resolution: `LlmResolverService` requires `enabled = true`, and org default-model validation rejects disabled models.
205    #[cfg_attr(feature = "openapi", schema(example = true))]
206    pub enabled: bool,
207    /// How this model entry was added (manually, discovered, or seeded as predefined).
208    #[cfg_attr(feature = "openapi", schema(example = "predefined"))]
209    pub source: LlmModelSource,
210    /// Timestamp when this model was created (RFC 3339).
211    #[cfg_attr(feature = "openapi", schema(example = "2026-01-04T11:23:00Z"))]
212    pub created_at: DateTime<Utc>,
213    /// Timestamp when this model was last updated (RFC 3339).
214    #[cfg_attr(feature = "openapi", schema(example = "2026-05-27T15:24:00Z"))]
215    pub updated_at: DateTime<Utc>,
216    /// Joined provider display name.
217    #[cfg_attr(feature = "openapi", schema(example = "Anthropic"))]
218    pub provider_name: String,
219    /// Joined provider implementation type.
220    #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
221    pub provider_type: LlmProviderType,
222    /// Derived: model is configured and ready for use. Currently means the
223    /// joined provider is active and has an API key set; over time this may
224    /// also incorporate live reachability checks. Not persisted.
225    #[cfg_attr(feature = "openapi", schema(example = true))]
226    pub healthy: bool,
227    /// Readonly profile with model capabilities (limits, pricing, modalities). Not persisted.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub profile: Option<LlmModelProfile>,
230    /// Vendor/brand of the model, derived from the model registry. Drives UI
231    /// branding (icons). `None` when the model id is not in the registry. Not persisted.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub model_vendor: Option<ModelVendor>,
234}
235
236// ============================================
237// LLM Model Profile types
238// Based on models.dev structure
239// ============================================
240
241/// Cost information for the model (per million tokens)
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "openapi", derive(ToSchema))]
244pub struct LlmModelCost {
245    /// Input cost per million tokens (USD)
246    pub input: f64,
247    /// Output cost per million tokens (USD)
248    pub output: f64,
249    /// Cached read cost per million tokens (USD), if supported
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub cache_read: Option<f64>,
252    /// Tiered pricing that applies above certain context thresholds.
253    /// When present, the base cost fields apply up to the tier threshold,
254    /// and each tier's costs apply for tokens beyond that threshold.
255    #[serde(default, skip_serializing_if = "Vec::is_empty")]
256    pub cost_tiers: Vec<CostTier>,
257}
258
259/// A pricing tier that activates above a context token threshold.
260/// For example, OpenAI charges higher rates for prompts exceeding 200K tokens.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[cfg_attr(feature = "openapi", derive(ToSchema))]
263pub struct CostTier {
264    /// Context token threshold above which this tier applies
265    pub above_tokens: i32,
266    /// Input cost per million tokens (USD) for this tier
267    pub input: f64,
268    /// Output cost per million tokens (USD) for this tier
269    pub output: f64,
270    /// Cached read cost per million tokens (USD) for this tier, if supported
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub cache_read: Option<f64>,
273}
274
275/// Token limits for the model
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[cfg_attr(feature = "openapi", derive(ToSchema))]
278pub struct LlmModelLimits {
279    /// Maximum context window size in tokens
280    pub context: i32,
281    /// Maximum input tokens (if different from context - output)
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub input: Option<i32>,
284    /// Maximum output tokens
285    pub output: i32,
286    /// Maximum images or PDF pages per request
287    #[serde(skip_serializing_if = "Option::is_none", default)]
288    pub max_media: Option<i32>,
289}
290
291/// Modality type (text, image, audio, video)
292#[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/// Model modalities for input and output
304#[derive(Debug, Clone, Serialize, Deserialize)]
305#[cfg_attr(feature = "openapi", derive(ToSchema))]
306pub struct LlmModelModalities {
307    /// Supported input modalities
308    pub input: Vec<Modality>,
309    /// Supported output modalities
310    pub output: Vec<Modality>,
311}
312
313/// Reasoning effort level for models that support it
314#[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/// Named reasoning effort value for UI display
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[cfg_attr(feature = "openapi", derive(ToSchema))]
329pub struct ReasoningEffortValue {
330    /// The API value (e.g., "low", "medium")
331    pub value: ReasoningEffort,
332    /// Display name (e.g., "Low", "Medium")
333    pub name: String,
334}
335
336/// Reasoning effort configuration for a model
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[cfg_attr(feature = "openapi", derive(ToSchema))]
339pub struct ReasoningEffortConfig {
340    /// Available reasoning effort values for this model
341    pub values: Vec<ReasoningEffortValue>,
342    /// Default reasoning effort for this model
343    pub default: ReasoningEffort,
344}
345
346/// Vendor / brand that authored a model. Independent of the provider type
347/// that serves it (the same model may be offered by several providers or
348/// gateways). Primarily drives UI iconography.
349#[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/// LLM Model Profile describing model capabilities
366/// Based on models.dev structure (<https://models.dev/api.json>)
367///
368/// NOTE: Currently only includes profiles for:
369/// - OpenAI: gpt-4o, gpt-4o-mini, o1, o1-mini, o1-pro, o3-mini
370/// - Anthropic: claude-3-5-sonnet, claude-3-5-haiku, claude-3-opus, claude-3-sonnet, claude-3-haiku, claude-sonnet-4, claude-opus-4
371///
372/// Additional model profiles can be added as needed.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[cfg_attr(feature = "openapi", derive(ToSchema))]
375pub struct LlmModelProfile {
376    /// Display name of the model
377    pub name: String,
378    /// Model family (e.g., "gpt-4o", "claude-3-5-sonnet")
379    pub family: String,
380    /// Short human-readable description of the model's strengths and intended use
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub description: Option<String>,
383    /// Release date (YYYY-MM-DD format)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub release_date: Option<String>,
386    /// Last updated date (YYYY-MM-DD format)
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub last_updated: Option<String>,
389    /// Whether the model supports file/image attachments
390    pub attachment: bool,
391    /// Whether the model has reasoning/chain-of-thought capabilities
392    pub reasoning: bool,
393    /// Whether temperature control is supported
394    pub temperature: bool,
395    /// Knowledge cutoff date (YYYY-MM-DD format)
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub knowledge: Option<String>,
398    /// Whether the model supports tool/function calling
399    pub tool_call: bool,
400    /// Whether the model supports structured output (JSON mode)
401    pub structured_output: bool,
402    /// Whether the model has open weights
403    pub open_weights: bool,
404    /// Cost per million tokens
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub cost: Option<LlmModelCost>,
407    /// Token limits
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub limits: Option<LlmModelLimits>,
410    /// Supported modalities
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub modalities: Option<LlmModelModalities>,
413    /// Reasoning effort configuration (for reasoning models)
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub reasoning_effort: Option<ReasoningEffortConfig>,
416    /// Whether the model supports tool_search (deferred tool loading).
417    /// When true, the driver can use namespaces and defer_loading to reduce
418    /// token usage for large tool sets. Currently supported by GPT-5.4 and newer.
419    #[serde(default)]
420    pub tool_search: bool,
421    /// Provider-advertised request parameters supported by this model.
422    #[serde(default, skip_serializing_if = "Vec::is_empty")]
423    pub supported_parameters: Vec<String>,
424    /// Whether the model supports native execution phases ("commentary" / "final_answer").
425    /// When true, the driver sends the `phase` field on assistant messages in the wire format.
426    /// Currently supported by GPT-5.4 and newer via OpenAI Responses API.
427    #[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        // Verify all provider types serialize correctly
438        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        // Verify all provider types deserialize correctly
471        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        // Verify FromStr works correctly
504        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        // Verify Display works correctly
570        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}