1pub mod error;
2
3pub use crate::error::TypeError;
4use pyo3::prelude::*;
5use schemars::JsonSchema;
6use serde::de::Error;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::any::type_name;
10use std::fmt;
11use std::fmt::Display;
12use std::path::{Path, PathBuf};
13use tracing::error;
14pub mod anthropic;
15pub mod common;
16pub mod google;
17pub mod openai;
18pub mod prompt;
19pub mod spec;
20pub mod tools;
21pub mod traits;
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24#[pyclass(from_py_object)]
25pub enum Model {
26 Undefined,
27}
28
29impl Model {
30 pub fn as_str(&self) -> &str {
31 match self {
32 Model::Undefined => "undefined",
33 }
34 }
35
36 pub fn from_string(s: &str) -> Result<Self, TypeError> {
37 match s.to_lowercase().as_str() {
38 "undefined" => Ok(Model::Undefined),
39 _ => Err(TypeError::UnknownModelError(s.to_string())),
40 }
41 }
42}
43
44pub enum Common {
45 Undefined,
46}
47
48impl Common {
49 pub fn as_str(&self) -> &str {
50 match self {
51 Common::Undefined => "undefined",
52 }
53 }
54}
55
56impl Display for Common {
57 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58 write!(f, "{}", self.as_str())
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
63#[pyclass(from_py_object, eq, eq_int)]
64pub enum Provider {
65 OpenAI,
66 Gemini,
67 Google,
68 Vertex,
69 Anthropic,
70 GoogleAdk,
71 Undefined, }
73
74impl Provider {
75 pub fn from_string(s: &str) -> Result<Self, TypeError> {
76 match s.to_lowercase().as_str() {
77 "openai" => Ok(Provider::OpenAI),
78 "gemini" => Ok(Provider::Gemini),
79 "google" => Ok(Provider::Google),
80 "vertex" => Ok(Provider::Vertex),
81 "anthropic" => Ok(Provider::Anthropic),
82 "google_adk" => Ok(Provider::GoogleAdk),
83 "undefined" => Ok(Provider::Undefined), _ => Err(TypeError::UnknownProviderError(s.to_string())),
85 }
86 }
87
88 pub const DEFAULT_ENV_VAR: &'static str = "POTATO_HEAD_DEFAULT_PROVIDER";
89
90 pub fn from_env_default() -> Result<Option<Provider>, TypeError> {
96 match std::env::var(Self::DEFAULT_ENV_VAR) {
97 Ok(s) => {
98 let trimmed = s.trim();
99 if trimmed.is_empty() {
100 Ok(None)
101 } else {
102 Provider::from_string(trimmed).map(Some)
103 }
104 }
105 Err(_) => Ok(None),
106 }
107 }
108
109 pub fn resolve(provided: Option<&str>) -> Result<Provider, TypeError> {
112 if let Some(s) = provided.map(str::trim).filter(|s| !s.is_empty()) {
113 return Provider::from_string(s);
114 }
115
116 Self::from_env_default()?.ok_or(TypeError::MissingProviderError(Self::DEFAULT_ENV_VAR))
117 }
118
119 pub fn resolve_from_py(provider: Option<&Bound<'_, PyAny>>) -> Result<Provider, TypeError> {
122 match provider {
123 Some(p) if !p.is_none() => Self::extract_provider(p),
124 _ => Self::from_env_default()?
125 .ok_or(TypeError::MissingProviderError(Self::DEFAULT_ENV_VAR)),
126 }
127 }
128
129 pub fn extract_provider(provider: &Bound<'_, PyAny>) -> Result<Provider, TypeError> {
140 match provider.is_instance_of::<Provider>() {
141 true => Ok(provider.extract::<Provider>().inspect_err(|e| {
142 error!("Failed to extract provider: {}", e);
143 })?),
144 false => {
145 let provider = provider.extract::<String>().unwrap();
146 Ok(Provider::from_string(&provider).inspect_err(|e| {
147 error!("Failed to convert string to provider: {}", e);
148 })?)
149 }
150 }
151 }
152
153 pub fn as_str(&self) -> &str {
154 match self {
155 Provider::OpenAI => "openai",
156 Provider::Gemini => "gemini",
157 Provider::Vertex => "vertex",
158 Provider::Google => "google",
159 Provider::Anthropic => "anthropic",
160 Provider::GoogleAdk => "google_adk",
161 Provider::Undefined => "undefined", }
163 }
164}
165
166impl Display for Provider {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 write!(f, "{}", self.as_str())
169 }
170}
171
172#[pyclass(from_py_object, eq, eq_int)]
173#[derive(Debug, PartialEq, Clone)]
174pub enum SaveName {
175 Prompt,
176}
177
178#[pymethods]
179impl SaveName {
180 #[staticmethod]
181 pub fn from_string(s: &str) -> Option<Self> {
182 match s {
183 "prompt" => Some(SaveName::Prompt),
184
185 _ => None,
186 }
187 }
188
189 pub fn as_string(&self) -> &str {
190 match self {
191 SaveName::Prompt => "prompt",
192 }
193 }
194
195 pub fn __str__(&self) -> String {
196 self.to_string()
197 }
198}
199
200impl Display for SaveName {
201 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
202 write!(f, "{}", self.as_string())
203 }
204}
205
206impl AsRef<Path> for SaveName {
207 fn as_ref(&self) -> &Path {
208 match self {
209 SaveName::Prompt => Path::new("prompt"),
210 }
211 }
212}
213
214impl From<SaveName> for PathBuf {
216 fn from(save_name: SaveName) -> Self {
217 PathBuf::from(save_name.as_ref())
218 }
219}
220
221pub trait StructuredOutput: for<'de> serde::Deserialize<'de> + JsonSchema {
240 fn type_name() -> &'static str {
241 type_name::<Self>().rsplit("::").next().unwrap_or("Unknown")
242 }
243
244 fn model_validate_json_value(value: &Value) -> Result<Self, serde_json::Error> {
252 match &value {
253 Value::String(json_str) => Self::model_validate_json_str(json_str),
254 Value::Object(_) => {
255 serde_json::from_value(value.clone())
257 }
258 _ => {
259 Err(Error::custom("Expected a JSON string or object"))
261 }
262 }
263 }
264
265 fn model_validate_json_str(value: &str) -> Result<Self, serde_json::Error> {
266 serde_json::from_str(value)
267 }
268
269 fn get_structured_output_schema() -> Value {
274 let schema = ::schemars::schema_for!(Self);
275 schema.into()
276 }
277 }
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
281#[pyclass(from_py_object)]
282pub enum SettingsType {
283 GoogleChat,
284 OpenAIChat,
285 ModelSettings,
286 Anthropic,
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::sync::Mutex;
293
294 static ENV_LOCK: Mutex<()> = Mutex::new(());
295
296 fn with_env_var<F: FnOnce()>(value: Option<&str>, f: F) {
297 let _guard = ENV_LOCK.lock().unwrap();
298 let prev = std::env::var(Provider::DEFAULT_ENV_VAR).ok();
299 match value {
300 Some(v) => std::env::set_var(Provider::DEFAULT_ENV_VAR, v),
301 None => std::env::remove_var(Provider::DEFAULT_ENV_VAR),
302 }
303 f();
304 match prev {
305 Some(v) => std::env::set_var(Provider::DEFAULT_ENV_VAR, v),
306 None => std::env::remove_var(Provider::DEFAULT_ENV_VAR),
307 }
308 }
309
310 #[test]
311 fn test_provider_google_adk_round_trip() {
312 let p = Provider::from_string("google_adk").unwrap();
313 assert_eq!(p, Provider::GoogleAdk);
314 assert_eq!(p.as_str(), "google_adk");
315 }
316
317 #[test]
318 fn test_provider_all_variants_round_trip() {
319 for (s, variant) in [
320 ("openai", Provider::OpenAI),
321 ("gemini", Provider::Gemini),
322 ("google", Provider::Google),
323 ("vertex", Provider::Vertex),
324 ("anthropic", Provider::Anthropic),
325 ("google_adk", Provider::GoogleAdk),
326 ("undefined", Provider::Undefined),
327 ] {
328 let parsed = Provider::from_string(s).unwrap();
329 assert_eq!(parsed, variant);
330 assert_eq!(parsed.as_str(), s);
331 }
332 }
333
334 #[test]
335 fn test_provider_unknown_string_errors() {
336 assert!(Provider::from_string("not_a_provider").is_err());
337 }
338
339 #[test]
340 fn resolve_explicit_wins_over_env() {
341 with_env_var(Some("anthropic"), || {
342 let p = Provider::resolve(Some("openai")).unwrap();
343 assert_eq!(p, Provider::OpenAI);
344 });
345 }
346
347 #[test]
348 fn resolve_uses_env_when_explicit_missing() {
349 with_env_var(Some("openai"), || {
350 let p = Provider::resolve(None).unwrap();
351 assert_eq!(p, Provider::OpenAI);
352 });
353 }
354
355 #[test]
356 fn resolve_uses_env_when_explicit_empty_string() {
357 with_env_var(Some("openai"), || {
358 let p = Provider::resolve(Some(" ")).unwrap();
359 assert_eq!(p, Provider::OpenAI);
360 });
361 }
362
363 #[test]
364 fn resolve_empty_env_treated_as_unset() {
365 with_env_var(Some(" "), || {
366 let err = Provider::resolve(None).unwrap_err();
367 assert!(matches!(err, TypeError::MissingProviderError(_)));
368 });
369 }
370
371 #[test]
372 fn resolve_invalid_env_is_hard_error() {
373 with_env_var(Some("not_a_provider"), || {
374 let err = Provider::resolve(None).unwrap_err();
375 assert!(matches!(err, TypeError::UnknownProviderError(_)));
376 });
377 }
378
379 #[test]
380 fn resolve_no_explicit_no_env_returns_missing() {
381 with_env_var(None, || {
382 let err = Provider::resolve(None).unwrap_err();
383 match err {
384 TypeError::MissingProviderError(name) => {
385 assert_eq!(name, "POTATO_HEAD_DEFAULT_PROVIDER");
386 }
387 other => panic!("expected MissingProviderError, got {:?}", other),
388 }
389 });
390 }
391
392 #[test]
393 fn from_env_default_unset_returns_none() {
394 with_env_var(None, || {
395 assert!(Provider::from_env_default().unwrap().is_none());
396 });
397 }
398
399 #[test]
400 fn from_env_default_set_and_parseable() {
401 with_env_var(Some("gemini"), || {
402 assert_eq!(
403 Provider::from_env_default().unwrap(),
404 Some(Provider::Gemini)
405 );
406 });
407 }
408
409 #[test]
410 fn from_env_default_set_but_invalid_errors() {
411 with_env_var(Some("garbage"), || {
412 assert!(Provider::from_env_default().is_err());
413 });
414 }
415}