Skip to main content

rpi_cli/
provider.rs

1//! File, trust and native-extension adapters for the shared model runtime.
2use crate::{config, settings};
3use rpi_ai::{Model, ThinkingLevel};
4use rpi_runtime::provider::RuntimeFactories;
5pub use rpi_runtime::provider::{
6    authenticated_catalog, available_catalog, ResolveError, ResolvedModel, ANTHROPIC_API_KEY_ENV,
7    ANTHROPIC_AUTH_TOKEN_ENV, ANTHROPIC_BASE_URL_ENV, DEFAULT_MODEL_ID, DEFAULT_THINKING_LEVEL,
8    NO_API_KEY_HINT, OPENAI_API_KEY_ENV,
9};
10use std::sync::Arc;
11
12pub(crate) struct CliValues;
13impl rpi_runtime::ValueSource for CliValues {
14    fn env(&self, name: &str) -> Option<String> {
15        std::env::var(name).ok()
16    }
17    fn command(&self, command: &str, cached: bool) -> Option<String> {
18        if cached {
19            config::resolve_command(command)
20        } else {
21            config::resolve_command_uncached(command)
22        }
23    }
24}
25pub(crate) fn declaration(
26    manifest: &rpi_extensions::ProviderManifest,
27) -> rpi_runtime::ProviderDeclaration {
28    rpi_runtime::ProviderDeclaration {
29        id: manifest.id.clone(),
30        models: manifest.models.clone(),
31        api_key_env: manifest.api_key_env.clone(),
32        requires_auth: manifest.requires_auth,
33    }
34}
35fn runtime_settings(settings: settings::Settings) -> rpi_runtime::RuntimeSettings {
36    let timeout = settings.http_idle_timeout_ms();
37    rpi_runtime::RuntimeSettings {
38        default_provider: settings.default_provider,
39        default_model: settings.default_model,
40        default_thinking_level: settings.default_thinking_level,
41        theme: settings.theme,
42        http_idle_timeout_ms: timeout,
43    }
44}
45fn resolve_with_factories(
46    provider: Option<&str>,
47    model: Option<&str>,
48    thinking: Option<ThinkingLevel>,
49    api_key: Option<&str>,
50    base_url: Option<&str>,
51    settings: settings::Settings,
52    factories: &RuntimeFactories,
53) -> Result<ResolvedModel, ResolveError> {
54    rpi_runtime::RuntimeConfig {
55        models: config::load_models_config()?,
56        credentials: config::read_auth()?,
57        settings: runtime_settings(settings),
58    }
59    .resolve(
60        &rpi_runtime::ModelRequest {
61            provider,
62            model,
63            thinking,
64            api_key,
65            base_url,
66            ..Default::default()
67        },
68        factories,
69        &CliValues,
70    )
71}
72pub fn resolve(
73    cli_provider: Option<&str>,
74    cli_model: Option<&str>,
75    cli_thinking: Option<ThinkingLevel>,
76    cli_api_key: Option<&str>,
77    cli_base_url: Option<&str>,
78) -> Result<ResolvedModel, ResolveError> {
79    let settings = settings::load_settings().unwrap_or_default();
80    resolve_with_settings(
81        cli_provider,
82        cli_model,
83        cli_thinking,
84        cli_api_key,
85        cli_base_url,
86        settings,
87    )
88}
89
90/// Resolve using upstream's global -> trusted-project settings precedence.
91pub fn resolve_for_cwd(
92    cli_provider: Option<&str>,
93    cli_model: Option<&str>,
94    cli_thinking: Option<ThinkingLevel>,
95    cli_api_key: Option<&str>,
96    cli_base_url: Option<&str>,
97    cwd: &std::path::Path,
98    project_trusted: bool,
99) -> Result<ResolvedModel, ResolveError> {
100    let settings = settings::load_effective_model_settings(cwd, project_trusted)?;
101    resolve_with_settings(
102        cli_provider,
103        cli_model,
104        cli_thinking,
105        cli_api_key,
106        cli_base_url,
107        settings,
108    )
109}
110
111fn resolve_with_settings(
112    cli_provider: Option<&str>,
113    cli_model: Option<&str>,
114    cli_thinking: Option<ThinkingLevel>,
115    cli_api_key: Option<&str>,
116    cli_base_url: Option<&str>,
117    settings: settings::Settings,
118) -> Result<ResolvedModel, ResolveError> {
119    resolve_with_factories(
120        cli_provider,
121        cli_model,
122        cli_thinking,
123        cli_api_key,
124        cli_base_url,
125        settings,
126        &RuntimeFactories::default(),
127    )
128}
129
130pub fn resolve_with_extensions(
131    args: &crate::args::Args,
132    cwd: &std::path::Path,
133    trusted: bool,
134    session: &rpi_extensions::ExtensionSession,
135) -> Result<ResolvedModel, ResolveError> {
136    let settings = settings::load_effective_model_settings(cwd, trusted)?;
137    resolve_with_factories(
138        args.provider.as_deref(),
139        args.model.as_deref(),
140        args.thinking,
141        args.api_key.as_deref(),
142        args.base_url.as_deref(),
143        settings,
144        &factories_from_session(session),
145    )
146}
147
148fn factories_from_session(session: &rpi_extensions::ExtensionSession) -> RuntimeFactories {
149    let mut factories = RuntimeFactories::default();
150    if let Some(refresher) = crate::auth::PackageOAuthRefresher::new(session) {
151        factories.set_oauth_refresher(Arc::new(refresher));
152    }
153    if let Some(snapshot) = session.snapshot() {
154        for manifest in manifests_from_session(session) {
155            factories.declare(declaration(&manifest));
156        }
157        for provider in rpi_extensions::PluggableProvider::from_session(
158            session,
159            tokio::runtime::Handle::current(),
160        ) {
161            if let Some(record) = snapshot
162                .providers()
163                .iter()
164                .find(|record| record.provider_id == provider.id())
165            {
166                let api =
167                    serde_json::from_value(serde_json::Value::String(record.api_style.clone()))
168                        .expect("API strings include custom protocols");
169                factories.register(provider, api);
170            }
171        }
172    }
173    factories
174}
175pub fn catalog_all() -> Result<Vec<Model>, config::ConfigError> {
176    catalog_with_manifests(&[])
177}
178pub fn manifests_from_session(
179    session: &rpi_extensions::ExtensionSession,
180) -> Vec<rpi_extensions::ProviderManifest> {
181    session.snapshot().map_or_else(Vec::new, |snapshot| {
182        snapshot
183            .providers()
184            .iter()
185            .filter_map(|provider| snapshot.provider_manifest(&provider.provider_id).cloned())
186            .collect()
187    })
188}
189
190pub fn catalog_with_manifests(
191    manifests: &[rpi_extensions::ProviderManifest],
192) -> Result<Vec<Model>, config::ConfigError> {
193    catalog_with_diagnostics(manifests).map(|(models, _)| models)
194}
195pub(crate) fn catalog_with_diagnostics(
196    manifests: &[rpi_extensions::ProviderManifest],
197) -> Result<(Vec<Model>, Vec<String>), config::ConfigError> {
198    let declarations: Vec<_> = manifests.iter().map(declaration).collect();
199    Ok(rpi_runtime::provider::catalog_with_diagnostics(
200        config::load_models_config()?,
201        &declarations,
202        &CliValues,
203    ))
204}
205
206#[cfg(test)]
207mod tests;