Skip to main content

leviath_core/blueprint/
model.rs

1//! Which model a stage runs on, and what a user may override.
2//!
3//! Two levels: a [`ModelEntry`] names a provider and model, and [`ModelConfig`]
4//! decides whether the user's own default may stand in for it.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A single model entry within a [`ModelConfig`] models list.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct ModelEntry {
12    /// Provider name (e.g., "anthropic", "openai")
13    pub provider: String,
14
15    /// Model identifier (e.g., "claude-sonnet-4-6")
16    pub model: String,
17}
18
19impl ModelEntry {
20    /// One provider/model pair in a stage's fallback list.
21    pub fn new(provider: String, model: String) -> Self {
22        Self { provider, model }
23    }
24}
25
26/// Model configuration for a stage.
27///
28/// Models are specified as an ordered priority list in `models`. The first
29/// entry whose provider is registered at runtime is used. When
30/// `allow_user_default` is true (the default), the user's configured default
31/// model is tried as a last resort. When false, the stage fails if none of
32/// the listed models are available.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ModelConfig {
35    /// Ordered list of models to try (first available wins).
36    #[serde(default)]
37    pub models: Vec<ModelEntry>,
38
39    /// When true (default), fall back to the user's configured default model
40    /// if none of the listed models are available.
41    #[serde(default = "default_allow_user_default")]
42    pub allow_user_default: bool,
43
44    /// Optional parameters that apply to whichever model gets selected.
45    #[serde(default)]
46    pub parameters: HashMap<String, serde_json::Value>,
47
48    /// Optional per-stage cap on the wall-clock time (in seconds) one inference
49    /// for this stage may run - the whole call including retries. When set, it
50    /// overrides the default job timeout; when `None`, the default applies.
51    ///
52    /// This lets a stage with slow first-token latency (e.g. a large-prompt
53    /// analyze call) get a long cap while a quick iterative stage fails fast on
54    /// a stalled connection instead of hanging for the full default.
55    #[serde(default)]
56    pub request_timeout_secs: Option<u64>,
57}
58
59fn default_allow_user_default() -> bool {
60    true
61}
62
63impl ModelConfig {
64    /// Create a new model configuration with a single model entry.
65    pub fn new(provider: String, model: String) -> Self {
66        Self {
67            models: vec![ModelEntry::new(provider, model)],
68            allow_user_default: true,
69            parameters: HashMap::new(),
70            request_timeout_secs: None,
71        }
72    }
73
74    /// Convenience: provider of the first model entry (for backward compat).
75    pub fn provider(&self) -> &str {
76        self.models
77            .first()
78            .map(|e| e.provider.as_str())
79            .unwrap_or("anthropic")
80    }
81
82    /// Convenience: model name of the first model entry (for backward compat).
83    pub fn model(&self) -> &str {
84        self.models
85            .first()
86            .map(|e| e.model.as_str())
87            .unwrap_or("claude-sonnet-4-6")
88    }
89}