Skip to main content

systemprompt_models/services/ai/
model.rs

1//! Per-provider AI policy and per-model descriptors.
2//!
3//! [`AiProviderConfig`] is the deployment policy layered on a registry provider
4//! (enable flag, default-model overrides, resilience). [`ModelDefinition`] and
5//! its [`ModelCapabilities`], [`ModelLimits`], and [`ModelPricing`] are the
6//! per-model descriptors shared with `profile.providers`. Connectivity itself
7//! is never modelled here — it lives in the provider registry.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use serde::{Deserialize, Serialize};
13
14use super::config::ResilienceSettings;
15
16const fn default_true() -> bool {
17    true
18}
19
20#[expect(
21    clippy::struct_excessive_bools,
22    reason = "model capability matrix: each bool is an independent provider feature flag, not \
23              state"
24)]
25#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, schemars::JsonSchema)]
26pub struct ModelCapabilities {
27    #[serde(default)]
28    pub vision: bool,
29
30    #[serde(default)]
31    pub audio_input: bool,
32
33    #[serde(default)]
34    pub video_input: bool,
35
36    #[serde(default)]
37    pub image_generation: bool,
38
39    #[serde(default)]
40    pub audio_generation: bool,
41
42    #[serde(default)]
43    pub streaming: bool,
44
45    #[serde(default)]
46    pub tools: bool,
47
48    #[serde(default)]
49    pub structured_output: bool,
50
51    #[serde(default)]
52    pub system_prompts: bool,
53
54    #[serde(default)]
55    pub image_resolution_config: bool,
56}
57
58#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, schemars::JsonSchema)]
59pub struct ModelLimits {
60    #[serde(default)]
61    pub context_window: u32,
62
63    #[serde(default)]
64    pub max_output_tokens: u32,
65
66    /// Maximum extended-thinking / reasoning budget (in tokens) the upstream
67    /// accepts for this model. `None` means the provider validates the budget
68    /// itself (Anthropic) or maps it to an effort bucket (`OpenAI`); set it for
69    /// providers that reject an out-of-range numeric budget (Gemini: 24576 for
70    /// flash, 32768 for pro). The gateway clamps the requested budget to this.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub max_thinking_budget: Option<u32>,
73}
74
75#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, schemars::JsonSchema)]
76pub struct ModelPricing {
77    #[serde(default)]
78    pub input_per_million: f64,
79
80    #[serde(default)]
81    pub output_per_million: f64,
82
83    /// Rate for tokens served from an existing prompt cache. Typically a
84    /// fraction of [`Self::input_per_million`] (0.1x on Anthropic). Left at
85    /// `0.0` a cache-heavy agent loop — where nearly every input token is a
86    /// cache read — bills as free.
87    #[serde(default)]
88    pub cache_read_per_million: f64,
89
90    /// Rate for tokens written into the prompt cache (Anthropic's 5-minute
91    /// tier, 1.25x input). The 1-hour 2x tier is not modelled separately.
92    #[serde(default)]
93    pub cache_write_per_million: f64,
94
95    #[serde(default)]
96    pub per_image_cents: Option<f64>,
97}
98
99impl ModelPricing {
100    /// Whether these rates can produce a non-zero bill. Token models need both
101    /// a read and a write side; an image model prices per image instead and is
102    /// legitimately zero on both token rates.
103    #[must_use]
104    pub fn is_billable(&self) -> bool {
105        if self.per_image_cents.is_some_and(|c| c > 0.0) {
106            return true;
107        }
108        self.input_per_million > 0.0 && self.output_per_million > 0.0
109    }
110}
111
112#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
113pub struct ModelDefinition {
114    #[serde(default)]
115    pub capabilities: ModelCapabilities,
116
117    #[serde(default)]
118    pub limits: ModelLimits,
119
120    #[serde(default)]
121    pub pricing: ModelPricing,
122}
123
124/// Per-provider AI *policy*, keyed by registry provider name.
125///
126/// Connectivity (endpoint, credential, model catalog) lives in the profile
127/// `providers` registry; this struct carries only the policy a deployment
128/// layers on top of an entry: whether the provider is enabled, its agent-side
129/// default-model override, image defaults, web-search toggle, and resilience.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct AiProviderConfig {
132    #[serde(default = "default_true")]
133    pub enabled: bool,
134
135    /// Overrides the provider client's built-in default model when non-empty.
136    #[serde(default)]
137    pub default_model: String,
138
139    #[serde(default)]
140    pub default_image_model: String,
141
142    #[serde(default)]
143    pub google_search_enabled: bool,
144
145    /// Resilience policy applied to outbound AI provider calls (timeouts,
146    /// retry, circuit breaker, bulkhead).
147    #[serde(default)]
148    pub resilience: ResilienceSettings,
149}
150
151impl Default for AiProviderConfig {
152    fn default() -> Self {
153        Self {
154            enabled: true,
155            default_model: String::new(),
156            default_image_model: String::new(),
157            google_search_enabled: false,
158            resilience: ResilienceSettings::default(),
159        }
160    }
161}