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}