Skip to main content

rpi_runtime/
runtime.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use crate::{
5    credentials::AuthState, ModelRequest, ProviderRegistry, ResolveError, ResolvedModel,
6    RuntimeConfig, ValueSource,
7};
8
9/// Full routing identity; display labels are never parsed to identify a model.
10#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
11pub struct ModelIdentity {
12    pub provider: String,
13    pub api: rpi_ai::Api,
14    pub model: String,
15}
16
17#[derive(Clone, Debug, serde::Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ModelSelection {
20    pub identity: ModelIdentity,
21    pub requested_thinking: rpi_ai::ThinkingLevel,
22    /// Model-supported preference. Hosts pass this level into stream options;
23    /// the provider maps it to its protocol's controls. Non-reasoning models use Off.
24    pub effective_thinking: rpi_ai::ThinkingLevel,
25}
26
27impl ResolvedModel {
28    pub fn selection(&self) -> ModelSelection {
29        ModelSelection {
30            identity: ModelIdentity {
31                provider: self.model.provider.clone(),
32                api: self.model.api.clone(),
33                model: self.model.id.clone(),
34            },
35            requested_thinking: self.thinking_level,
36            effective_thinking: if self.model.reasoning {
37                self.model.clamp_thinking_level(self.thinking_level)
38            } else {
39                rpi_ai::ThinkingLevel::Off
40            },
41        }
42    }
43}
44
45/// Credential-free state suitable for serving to a UI or observing over watch.
46#[derive(Clone, Debug, serde::Serialize)]
47pub struct RuntimeSnapshot {
48    pub revision: u64,
49    pub selection: ModelSelection,
50    pub models: Vec<rpi_ai::Model>,
51    pub auth: BTreeMap<String, AuthState>,
52    pub warnings: Vec<String>,
53}
54
55/// An optional atomic publication boundary for embedded applications. Hosts
56/// continue to own configuration storage, plugin loading and request cancellation.
57pub struct Runtime {
58    current: Arc<ResolvedModel>,
59    changes: tokio::sync::watch::Sender<Arc<RuntimeSnapshot>>,
60}
61
62impl Runtime {
63    pub fn new(
64        config: &RuntimeConfig,
65        providers: &ProviderRegistry,
66        values: &dyn ValueSource,
67        request: &ModelRequest<'_>,
68    ) -> Result<Self, ResolveError> {
69        let (current, snapshot) = prepare(config, providers, values, request, 0)?;
70        let (changes, _) = tokio::sync::watch::channel(Arc::new(snapshot));
71        Ok(Self {
72            current: Arc::new(current),
73            changes,
74        })
75    }
76
77    /// Cloned providers retain the original loader lease and private bindings.
78    pub fn current(&self) -> Arc<ResolvedModel> {
79        self.current.clone()
80    }
81
82    pub fn snapshot(&self) -> Arc<RuntimeSnapshot> {
83        self.changes.borrow().clone()
84    }
85
86    pub fn subscribe(&self) -> tokio::sync::watch::Receiver<Arc<RuntimeSnapshot>> {
87        self.changes.subscribe()
88    }
89
90    /// Prepare everything before publishing. Failure leaves both the active
91    /// providers and observable snapshot intact, without emitting a change.
92    pub fn reload(
93        &mut self,
94        config: &RuntimeConfig,
95        providers: &ProviderRegistry,
96        values: &dyn ValueSource,
97        request: &ModelRequest<'_>,
98    ) -> Result<(), ResolveError> {
99        let revision = self.snapshot().revision.saturating_add(1);
100        let (current, snapshot) = prepare(config, providers, values, request, revision)?;
101        self.current = Arc::new(current);
102        self.changes.send_replace(Arc::new(snapshot));
103        Ok(())
104    }
105}
106
107fn prepare(
108    config: &RuntimeConfig,
109    providers: &ProviderRegistry,
110    values: &dyn ValueSource,
111    request: &ModelRequest<'_>,
112    revision: u64,
113) -> Result<(ResolvedModel, RuntimeSnapshot), ResolveError> {
114    let current = config.resolve(request, providers, values)?;
115    let snapshot = RuntimeSnapshot {
116        revision,
117        selection: current.selection(),
118        models: current.snapshot_catalog.clone(),
119        auth: current.snapshot_auth.clone(),
120        warnings: current.compat_warnings.clone(),
121    };
122    Ok((current, snapshot))
123}