Skip to main content

sim_lib_agent_runner_http/
provider.rs

1//! Open HTTP-provider profile records.
2//!
3//! Profiles are ordinary data owned by the HTTP runner crate. Provider identity
4//! stays outside the kernel so new providers can be described without adding a
5//! closed enum to `sim-kernel`.
6
7use sim_kernel::Symbol;
8use std::time::Duration;
9
10/// Maximum decoded provider response size used by default.
11pub const DEFAULT_MAX_OUTPUT_BYTES: usize = 1024 * 1024;
12
13/// Authentication shape used by a provider profile.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum ProviderAuth {
16    /// The provider does not need authentication by default.
17    None,
18    /// The provider expects an `Authorization: Bearer <secret>` value read from
19    /// an environment variable.
20    BearerEnv {
21        /// Default environment variable name.
22        env: String,
23    },
24    /// The provider may accept an `Authorization: Bearer <secret>` value, but
25    /// does not require one by default.
26    OptionalBearerEnv {
27        /// Suggested environment variable name.
28        env: String,
29    },
30    /// The provider expects a named header value read from an environment
31    /// variable.
32    HeaderEnv {
33        /// Header name to send.
34        header: String,
35        /// Default environment variable name.
36        env: String,
37    },
38}
39
40impl ProviderAuth {
41    /// Returns the default environment variable named by this auth profile.
42    pub fn default_env(&self) -> Option<&str> {
43        match self {
44            Self::None => None,
45            Self::BearerEnv { env } | Self::HeaderEnv { env, .. } => Some(env),
46            Self::OptionalBearerEnv { .. } => None,
47        }
48    }
49
50    /// Returns the documented environment variable for optional auth shapes.
51    pub fn env_hint(&self) -> Option<&str> {
52        match self {
53            Self::None => None,
54            Self::BearerEnv { env }
55            | Self::OptionalBearerEnv { env }
56            | Self::HeaderEnv { env, .. } => Some(env),
57        }
58    }
59}
60
61/// Data profile for a concrete HTTP model provider.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct ProviderProfile {
64    /// Open provider id such as `openai`, `anthropic`, or `ollama`.
65    pub provider: Symbol,
66    /// Default runner symbol exposed by the agent library.
67    pub runner_symbol: Symbol,
68    /// Runtime codec symbol used to encode/decode provider payloads.
69    pub codec: Symbol,
70    /// Default base endpoint. An empty value means the caller must supply one.
71    pub default_endpoint: String,
72    /// Provider path that lists models relative to the base endpoint.
73    pub models_path: &'static str,
74    /// Provider path that performs chat inference relative to the base endpoint.
75    pub chat_path: &'static str,
76    /// Provider authentication profile.
77    pub auth: ProviderAuth,
78    /// Default runner locality, either `local` or `network`.
79    pub default_locality: Symbol,
80    /// Default model name used when the option map does not provide `model`.
81    pub default_model: String,
82    /// Default HTTP timeout.
83    pub default_timeout: Duration,
84    /// Default streaming flag.
85    pub default_stream: bool,
86    /// Default tool-use flag.
87    pub default_tools: bool,
88    /// Default response byte limit.
89    pub default_max_output_bytes: usize,
90}
91
92impl ProviderProfile {
93    /// Returns the native OpenAI profile.
94    pub fn openai() -> Self {
95        provider_profiles::openai()
96    }
97
98    /// Returns the native Anthropic profile.
99    pub fn anthropic() -> Self {
100        provider_profiles::anthropic()
101    }
102
103    /// Returns the native Ollama profile.
104    pub fn ollama() -> Self {
105        provider_profiles::ollama()
106    }
107
108    /// Returns the native LM Studio profile.
109    pub fn lm_studio() -> Self {
110        provider_profiles::lm_studio()
111    }
112
113    /// Returns the native Lemonade profile.
114    pub fn lemonade() -> Self {
115        provider_profiles::lemonade()
116    }
117
118    /// Returns the generic OpenAI-compatible fallback profile.
119    pub fn openai_compatible() -> Self {
120        provider_profiles::openai_compatible()
121    }
122}
123
124/// Provider profile constructors.
125pub mod provider_profiles {
126    use super::{DEFAULT_MAX_OUTPUT_BYTES, ProviderAuth, ProviderProfile};
127    use sim_kernel::Symbol;
128    use std::time::Duration;
129
130    /// Returns every built-in provider profile.
131    pub fn all() -> Vec<ProviderProfile> {
132        vec![
133            openai(),
134            anthropic(),
135            ollama(),
136            lm_studio(),
137            lemonade(),
138            openai_compatible(),
139        ]
140    }
141
142    /// Native OpenAI provider profile.
143    pub fn openai() -> ProviderProfile {
144        ProviderProfile {
145            provider: Symbol::new("openai"),
146            runner_symbol: Symbol::qualified("runner", "openai"),
147            codec: Symbol::qualified("codec", "openai"),
148            default_endpoint: "https://api.openai.com/v1".to_owned(),
149            models_path: "/models",
150            chat_path: "/chat/completions",
151            auth: ProviderAuth::BearerEnv {
152                env: "OPENAI_API_KEY".to_owned(),
153            },
154            default_locality: Symbol::new("network"),
155            default_model: "gpt-5-mini".to_owned(),
156            default_timeout: Duration::from_secs(60),
157            default_stream: false,
158            default_tools: true,
159            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
160        }
161    }
162
163    /// Native Anthropic provider profile.
164    pub fn anthropic() -> ProviderProfile {
165        ProviderProfile {
166            provider: Symbol::new("anthropic"),
167            runner_symbol: Symbol::qualified("runner", "anthropic"),
168            codec: Symbol::qualified("codec", "anthropic"),
169            default_endpoint: "https://api.anthropic.com/v1".to_owned(),
170            models_path: "/models",
171            chat_path: "/messages",
172            auth: ProviderAuth::HeaderEnv {
173                header: "x-api-key".to_owned(),
174                env: "ANTHROPIC_API_KEY".to_owned(),
175            },
176            default_locality: Symbol::new("network"),
177            default_model: "claude-sonnet-latest".to_owned(),
178            default_timeout: Duration::from_secs(60),
179            default_stream: false,
180            default_tools: true,
181            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
182        }
183    }
184
185    /// Native Ollama provider profile.
186    pub fn ollama() -> ProviderProfile {
187        ProviderProfile {
188            provider: Symbol::new("ollama"),
189            runner_symbol: Symbol::qualified("runner", "ollama"),
190            codec: Symbol::qualified("codec", "ollama"),
191            default_endpoint: "http://127.0.0.1:11434".to_owned(),
192            models_path: "/api/tags",
193            chat_path: "/api/chat",
194            auth: ProviderAuth::None,
195            default_locality: Symbol::new("local"),
196            default_model: "qwen3.5:4b".to_owned(),
197            default_timeout: Duration::from_secs(120),
198            default_stream: true,
199            default_tools: false,
200            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
201        }
202    }
203
204    /// Native LM Studio provider profile.
205    pub fn lm_studio() -> ProviderProfile {
206        ProviderProfile {
207            provider: Symbol::new("lm-studio"),
208            runner_symbol: Symbol::qualified("runner", "lm-studio"),
209            codec: Symbol::qualified("codec", "lm-studio"),
210            default_endpoint: "http://127.0.0.1:1234/v1".to_owned(),
211            models_path: "/models",
212            chat_path: "/chat/completions",
213            auth: ProviderAuth::OptionalBearerEnv {
214                env: "LM_STUDIO_API_KEY".to_owned(),
215            },
216            default_locality: Symbol::new("local"),
217            default_model: "local/default".to_owned(),
218            default_timeout: Duration::from_secs(120),
219            default_stream: false,
220            default_tools: true,
221            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
222        }
223    }
224
225    /// Native Lemonade provider profile.
226    pub fn lemonade() -> ProviderProfile {
227        ProviderProfile {
228            provider: Symbol::new("lemonade"),
229            runner_symbol: Symbol::qualified("runner", "lemonade"),
230            codec: Symbol::qualified("codec", "lemonade"),
231            default_endpoint: "http://127.0.0.1:13305/v1".to_owned(),
232            models_path: "/models",
233            chat_path: "/chat/completions",
234            auth: ProviderAuth::None,
235            default_locality: Symbol::new("local"),
236            default_model: "Qwen3-Coder".to_owned(),
237            default_timeout: Duration::from_secs(120),
238            default_stream: false,
239            default_tools: true,
240            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
241        }
242    }
243
244    /// Generic OpenAI-compatible fallback profile.
245    pub fn openai_compatible() -> ProviderProfile {
246        ProviderProfile {
247            provider: Symbol::new("openai-compatible"),
248            runner_symbol: Symbol::qualified("runner", "openai-compatible"),
249            codec: Symbol::qualified("codec", "openai"),
250            default_endpoint: String::new(),
251            models_path: "/models",
252            chat_path: "/chat/completions",
253            auth: ProviderAuth::BearerEnv {
254                env: "OPENAI_API_KEY".to_owned(),
255            },
256            default_locality: Symbol::new("network"),
257            default_model: "gpt-4.1-mini".to_owned(),
258            default_timeout: Duration::from_secs(60),
259            default_stream: false,
260            default_tools: true,
261            default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
262        }
263    }
264}
265
266/// Native OpenAI provider profile.
267pub fn openai_profile() -> ProviderProfile {
268    provider_profiles::openai()
269}
270
271/// Native Anthropic provider profile.
272pub fn anthropic_profile() -> ProviderProfile {
273    provider_profiles::anthropic()
274}
275
276/// Native Ollama provider profile.
277pub fn ollama_profile() -> ProviderProfile {
278    provider_profiles::ollama()
279}
280
281/// Native LM Studio provider profile.
282pub fn lm_studio_profile() -> ProviderProfile {
283    provider_profiles::lm_studio()
284}
285
286/// Native Lemonade provider profile.
287pub fn lemonade_profile() -> ProviderProfile {
288    provider_profiles::lemonade()
289}
290
291/// Generic OpenAI-compatible fallback provider profile.
292pub fn openai_compatible_profile() -> ProviderProfile {
293    provider_profiles::openai_compatible()
294}
295
296#[cfg(test)]
297mod tests {
298    use super::{ProviderAuth, ProviderProfile, provider_profiles};
299    use sim_kernel::Symbol;
300
301    #[test]
302    fn profiles_cover_known_providers_and_generic_fallback() {
303        let providers = provider_profiles::all()
304            .into_iter()
305            .map(|profile| profile.provider.to_string())
306            .collect::<Vec<_>>();
307        assert_eq!(
308            providers,
309            vec![
310                "openai",
311                "anthropic",
312                "ollama",
313                "lm-studio",
314                "lemonade",
315                "openai-compatible"
316            ]
317        );
318    }
319
320    #[test]
321    fn anthropic_profile_records_header_auth_and_messages_path() {
322        let profile = ProviderProfile::anthropic();
323        assert_eq!(profile.provider, Symbol::new("anthropic"));
324        assert_eq!(
325            profile.runner_symbol,
326            Symbol::qualified("runner", "anthropic")
327        );
328        assert_eq!(profile.codec, Symbol::qualified("codec", "anthropic"));
329        assert_eq!(profile.default_endpoint, "https://api.anthropic.com/v1");
330        assert_eq!(profile.models_path, "/models");
331        assert_eq!(profile.chat_path, "/messages");
332        assert_eq!(
333            profile.auth,
334            ProviderAuth::HeaderEnv {
335                header: "x-api-key".to_owned(),
336                env: "ANTHROPIC_API_KEY".to_owned()
337            }
338        );
339        assert_eq!(profile.default_locality, Symbol::new("network"));
340    }
341
342    #[test]
343    fn local_profiles_default_to_loopback_without_auth() {
344        for profile in [ProviderProfile::ollama(), ProviderProfile::lemonade()] {
345            assert_eq!(profile.auth, ProviderAuth::None);
346            assert_eq!(profile.default_locality, Symbol::new("local"));
347            assert!(profile.default_endpoint.starts_with("http://127.0.0.1:"));
348        }
349    }
350
351    #[test]
352    fn lm_studio_profile_records_optional_bearer_auth() {
353        let profile = ProviderProfile::lm_studio();
354
355        assert_eq!(
356            profile.auth,
357            ProviderAuth::OptionalBearerEnv {
358                env: "LM_STUDIO_API_KEY".to_owned()
359            }
360        );
361        assert_eq!(profile.auth.default_env(), None);
362        assert_eq!(profile.auth.env_hint(), Some("LM_STUDIO_API_KEY"));
363        assert_eq!(profile.default_locality, Symbol::new("local"));
364        assert!(profile.default_endpoint.starts_with("http://127.0.0.1:"));
365    }
366}