Skip to main content

vtcode_llm/
config_adapter.rs

1//! Provider configuration traits decoupled from VT Code's dot-config storage.
2//!
3//! Consumers can implement [`ProviderConfig`] for their own types and use the
4//! conversion helpers to build `vtcode_core` provider factories without
5//! depending on VT Code's internal configuration structs.
6
7use std::borrow::Cow;
8
9use std::path::PathBuf;
10
11use anyhow::Error;
12use vtcode_commons::cgp::HasComponent;
13use vtcode_commons::{ErrorFormatter, ErrorReporter, PathScope, TelemetrySink, WorkspacePaths};
14use vtcode_config::TimeoutsConfig;
15use vtcode_config::core::{AnthropicConfig, OpenAIConfig};
16use vtcode_config::core::{ModelConfig, PromptCachingConfig};
17
18/// Trait describing the configuration required to instantiate an LLM provider.
19///
20/// The trait intentionally returns owned-friendly values so that consumers can
21/// back the configuration with environment variables, secret managers, or
22/// custom structs. The [`as_factory_config`] helper converts a trait object into
23/// the concrete configuration type expected by `vtcode_core`'s provider
24/// factory.
25pub trait ProviderConfig {
26    /// API key or bearer token used to authenticate with the provider.
27    fn api_key(&self) -> Option<Cow<'_, str>>;
28
29    /// Optional override for the provider's base URL.
30    fn base_url(&self) -> Option<Cow<'_, str>> {
31        None
32    }
33
34    /// Preferred model identifier for the provider.
35    fn model(&self) -> Option<Cow<'_, str>> {
36        None
37    }
38
39    /// Optional prompt cache configuration forwarded to providers that support
40    /// caching.
41    fn prompt_cache(&self) -> Option<Cow<'_, PromptCachingConfig>> {
42        None
43    }
44
45    /// Optional request timeout configuration forwarded to the provider factory.
46    fn timeouts(&self) -> Option<Cow<'_, TimeoutsConfig>> {
47        None
48    }
49
50    /// Optional OpenAI-specific configuration forwarded to the provider factory.
51    fn openai(&self) -> Option<Cow<'_, OpenAIConfig>> {
52        None
53    }
54
55    /// Optional Anthropic-specific configuration forwarded to the provider factory.
56    fn anthropic(&self) -> Option<Cow<'_, AnthropicConfig>> {
57        None
58    }
59
60    /// Optional model behavior configuration (loop detection, capability overrides).
61    fn model_behavior(&self) -> Option<Cow<'_, ModelConfig>> {
62        None
63    }
64}
65
66/// Marker component for projecting a borrowed provider config into the owned
67/// config bag consumed by `vtcode-core`.
68enum FactoryConfigProjectionComponent {}
69
70trait FactoryConfigProjectionProvider<Ctx> {
71    fn project(ctx: &Ctx) -> crate::factory_types::ProviderConfig;
72}
73
74trait CanProjectFactoryConfig {
75    fn project_factory_config(&self) -> crate::factory_types::ProviderConfig;
76}
77
78impl<Ctx> CanProjectFactoryConfig for Ctx
79where
80    Ctx: HasComponent<FactoryConfigProjectionComponent>,
81    <Ctx as HasComponent<FactoryConfigProjectionComponent>>::Provider: FactoryConfigProjectionProvider<Ctx>,
82{
83    fn project_factory_config(&self) -> crate::factory_types::ProviderConfig {
84        <<Ctx as HasComponent<FactoryConfigProjectionComponent>>::Provider as FactoryConfigProjectionProvider<Ctx>>::project(self)
85    }
86}
87
88struct BorrowedConfigProjectionCtx<'a> {
89    source: &'a dyn ProviderConfig,
90}
91
92struct BorrowedConfigProjection;
93
94impl HasComponent<FactoryConfigProjectionComponent> for BorrowedConfigProjectionCtx<'_> {
95    type Provider = BorrowedConfigProjection;
96}
97
98impl FactoryConfigProjectionProvider<BorrowedConfigProjectionCtx<'_>> for BorrowedConfigProjection {
99    fn project(ctx: &BorrowedConfigProjectionCtx<'_>) -> crate::factory_types::ProviderConfig {
100        project_provider_config(ctx.source)
101    }
102}
103
104/// Convert an implementor of [`ProviderConfig`] into the configuration used by
105/// the `vtcode_core` provider factory.
106pub fn as_factory_config(source: &dyn ProviderConfig) -> crate::factory_types::ProviderConfig {
107    BorrowedConfigProjectionCtx { source }.project_factory_config()
108}
109
110/// Telemetry event emitted when adapter hooks adjust provider configuration.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum AdapterEvent {
113    /// Records the resolved prompt cache directory and its associated scope.
114    PromptCacheResolved { scope: PathScope, cache_dir: PathBuf },
115    /// Emitted when the telemetry sink itself fails so callers can log a
116    /// fallback message.
117    TelemetryFailure { message: String },
118    /// Raised when the adapter reports a validation or hook failure.
119    AdapterError { message: String },
120}
121
122/// Trait that bundles the adapter hook dependencies into a single interface.
123/// This replaces the previous four-parameter generic bound
124/// (`Paths`, `Telemetry`, `Reporter`, `Formatter`) with a single trait,
125/// following the **Config Trait pattern** from the Rust Patterns guide
126/// (Ch 3 — Config Trait Pattern).
127///
128/// Associated types keep the trait dyn-incompatible (which is intentional —
129/// these are compile-time wiring, not runtime polymorphism).
130pub trait AdapterHooksProvider: Send + Sync {
131    type Paths: WorkspacePaths + ?Sized;
132    type Telemetry: TelemetrySink<AdapterEvent> + ?Sized;
133    type Reporter: ErrorReporter + ?Sized;
134    type Formatter: ErrorFormatter + ?Sized;
135
136    fn workspace_paths(&self) -> &Self::Paths;
137    fn telemetry(&self) -> &Self::Telemetry;
138    fn error_reporter(&self) -> &Self::Reporter;
139    fn error_formatter(&self) -> &Self::Formatter;
140}
141
142/// Shared adapter that enriches [`ProviderConfig`] conversions with
143/// [`WorkspacePaths`], telemetry, and error-reporting hooks from
144/// `vtcode-commons`.
145///
146/// Uses the **Config Trait pattern** — one generic parameter (`Hooks`) instead
147/// of the previous four (`Paths`, `Telemetry`, `Reporter`, `Formatter`).
148pub struct AdapterHooks<'a, Hooks: AdapterHooksProvider> {
149    hooks: &'a Hooks,
150}
151
152impl<'a, Hooks: AdapterHooksProvider> AdapterHooks<'a, Hooks> {
153    /// Create a new adapter that enriches provider configuration conversions.
154    fn new(hooks: &'a Hooks) -> Self {
155        Self { hooks }
156    }
157
158    /// Convert a [`ProviderConfig`] into the factory configuration while
159    /// applying workspace-aware prompt cache resolution and telemetry hooks.
160    pub fn apply_to(&self, source: &dyn ProviderConfig) -> crate::factory_types::ProviderConfig {
161        HookedConfigProjectionCtx { source, hooks: self }.project_factory_config()
162    }
163
164    fn enrich_prompt_cache(&self, prompt_cache: &mut PromptCachingConfig) {
165        let resolved = prompt_cache.resolve_cache_dir(Some(self.hooks.workspace_paths().workspace_root()));
166        let scope = self.hooks.workspace_paths().scope_for_path(&resolved);
167        // Check absoluteness before moving `resolved` so we can report a meaningful error.
168        let is_abs = resolved.is_absolute();
169        let resolved_display = resolved.display().to_string();
170
171        // Set the prompt cache dir (borrows from `resolved`) then move `resolved` into the event
172        prompt_cache.cache_dir = resolved.to_string_lossy().into_owned();
173        self.record_event(AdapterEvent::PromptCacheResolved { scope, cache_dir: resolved });
174
175        if !is_abs {
176            let error = Error::msg(format!(
177                "Prompt cache directory `{resolved_display}` could not be resolved to an absolute path"
178            ));
179            self.report_error(error);
180        }
181    }
182
183    fn record_event(&self, event: AdapterEvent) {
184        if let Err(err) = self.hooks.telemetry().record(&event) {
185            self.handle_error(err.context("failed to record LLM adapter telemetry event"));
186        }
187    }
188
189    fn capture_error_message(&self, error: &Error) -> String {
190        let message = self.hooks.error_formatter().format_error(error).into_owned();
191        let _ = self.hooks.error_reporter().capture(error);
192        message
193    }
194
195    fn report_error(&self, error: Error) {
196        let message = self.capture_error_message(&error);
197        // Best-effort recording of the formatted message; ignore additional
198        // failures to avoid recursive error handling loops.
199        let _ = self.hooks.telemetry().record(&AdapterEvent::AdapterError { message });
200    }
201
202    fn handle_error(&self, error: Error) {
203        let message = self.capture_error_message(&error);
204        let _ = self.hooks.telemetry().record(&AdapterEvent::TelemetryFailure { message });
205    }
206}
207
208struct HookedConfigProjectionCtx<'source, 'hooks, Hooks: AdapterHooksProvider> {
209    source: &'source dyn ProviderConfig,
210    hooks: &'hooks AdapterHooks<'hooks, Hooks>,
211}
212
213struct HookedConfigProjection;
214
215impl<'source, 'hooks, Hooks: AdapterHooksProvider> HasComponent<FactoryConfigProjectionComponent>
216    for HookedConfigProjectionCtx<'source, 'hooks, Hooks>
217{
218    type Provider = HookedConfigProjection;
219}
220
221impl<'source, 'hooks, Hooks: AdapterHooksProvider>
222    FactoryConfigProjectionProvider<HookedConfigProjectionCtx<'source, 'hooks, Hooks>> for HookedConfigProjection
223{
224    fn project(ctx: &HookedConfigProjectionCtx<'source, 'hooks, Hooks>) -> crate::factory_types::ProviderConfig {
225        let mut config = BorrowedConfigProjectionCtx { source: ctx.source }.project_factory_config();
226        if let Some(prompt_cache) = config.prompt_cache.as_mut() {
227            ctx.hooks.enrich_prompt_cache(prompt_cache);
228        }
229        config
230    }
231}
232
233/// Convert a [`ProviderConfig`] into the factory configuration using the
234/// supplied adapter hooks for workspace, telemetry, and error integration.
235pub fn as_factory_config_with_hooks<'a, Hooks: AdapterHooksProvider>(
236    source: &dyn ProviderConfig,
237    hooks: &AdapterHooks<'a, Hooks>,
238) -> crate::factory_types::ProviderConfig {
239    HookedConfigProjectionCtx { source, hooks }.project_factory_config()
240}
241
242fn project_provider_config(source: &dyn ProviderConfig) -> crate::factory_types::ProviderConfig {
243    crate::factory_types::ProviderConfig {
244        api_key: source.api_key().map(Cow::into_owned),
245        openai_chatgpt_auth: None,
246        copilot_auth: None,
247        base_url: source.base_url().map(Cow::into_owned),
248        model: source.model().map(Cow::into_owned),
249        prompt_cache: source.prompt_cache().map(Cow::into_owned),
250        timeouts: source.timeouts().map(Cow::into_owned),
251        openai: source.openai().map(Cow::into_owned),
252        anthropic: source.anthropic().map(Cow::into_owned),
253        model_behavior: source.model_behavior().map(Cow::into_owned),
254        workspace_root: None,
255    }
256}
257
258fn borrowed_optional_str(value: &Option<String>) -> Option<Cow<'_, str>> {
259    value.as_deref().map(Cow::Borrowed)
260}
261
262fn borrowed_optional<T: Clone>(value: &Option<T>) -> Option<Cow<'_, T>> {
263    value.as_ref().map(Cow::Borrowed)
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use anyhow::{Error, Result, anyhow};
270    use assert_fs::TempDir;
271    use std::borrow::Cow;
272    use std::path::Path;
273    use std::sync::{Arc, Mutex};
274
275    #[derive(Clone)]
276    struct TestPaths {
277        root: PathBuf,
278        config: PathBuf,
279        cache: PathBuf,
280    }
281
282    impl WorkspacePaths for TestPaths {
283        fn workspace_root(&self) -> &Path {
284            &self.root
285        }
286
287        fn config_dir(&self) -> PathBuf {
288            self.config.clone()
289        }
290
291        fn cache_dir(&self) -> Option<PathBuf> {
292            Some(self.cache.clone())
293        }
294    }
295
296    #[derive(Default)]
297    struct RecordingTelemetry {
298        events: Arc<Mutex<Vec<AdapterEvent>>>,
299    }
300
301    impl TelemetrySink<AdapterEvent> for RecordingTelemetry {
302        fn record(&self, event: &AdapterEvent) -> Result<()> {
303            self.events.lock().unwrap().push(event.clone());
304            Ok(())
305        }
306    }
307
308    #[derive(Default)]
309    struct FailingTelemetry {
310        events: Arc<Mutex<Vec<AdapterEvent>>>,
311        fail_next: Arc<Mutex<bool>>,
312    }
313
314    impl TelemetrySink<AdapterEvent> for FailingTelemetry {
315        fn record(&self, event: &AdapterEvent) -> Result<()> {
316            let mut fail = self.fail_next.lock().unwrap();
317            if std::mem::take(&mut *fail) {
318                Err(anyhow!("telemetry unavailable"))
319            } else {
320                self.events.lock().unwrap().push(event.clone());
321                Ok(())
322            }
323        }
324    }
325
326    #[derive(Default)]
327    struct RecordingReporter {
328        errors: Arc<Mutex<Vec<String>>>,
329    }
330
331    impl ErrorReporter for RecordingReporter {
332        fn capture(&self, error: &Error) -> Result<()> {
333            self.errors.lock().unwrap().push(error.to_string());
334            Ok(())
335        }
336    }
337
338    #[derive(Default)]
339    struct RecordingFormatter {
340        messages: Arc<Mutex<Vec<String>>>,
341    }
342
343    impl ErrorFormatter for RecordingFormatter {
344        fn format_error(&self, error: &Error) -> Cow<'_, str> {
345            let message = error.to_string();
346            self.messages.lock().unwrap().push(message.clone());
347            Cow::Owned(message)
348        }
349    }
350
351    /// Test harness that bundles all adapter hook dependencies into a single
352    /// struct, demonstrating the Config Trait pattern.
353    struct TestHooks {
354        paths: TestPaths,
355        telemetry: RecordingTelemetry,
356        reporter: RecordingReporter,
357        formatter: RecordingFormatter,
358    }
359
360    impl AdapterHooksProvider for TestHooks {
361        type Paths = TestPaths;
362        type Telemetry = RecordingTelemetry;
363        type Reporter = RecordingReporter;
364        type Formatter = RecordingFormatter;
365
366        fn workspace_paths(&self) -> &TestPaths {
367            &self.paths
368        }
369        fn telemetry(&self) -> &RecordingTelemetry {
370            &self.telemetry
371        }
372        fn error_reporter(&self) -> &RecordingReporter {
373            &self.reporter
374        }
375        fn error_formatter(&self) -> &RecordingFormatter {
376            &self.formatter
377        }
378    }
379
380    /// Test harness that uses a failing telemetry sink.
381    struct FailingHooks {
382        paths: TestPaths,
383        telemetry: FailingTelemetry,
384        reporter: RecordingReporter,
385        formatter: RecordingFormatter,
386    }
387
388    impl AdapterHooksProvider for FailingHooks {
389        type Paths = TestPaths;
390        type Telemetry = FailingTelemetry;
391        type Reporter = RecordingReporter;
392        type Formatter = RecordingFormatter;
393
394        fn workspace_paths(&self) -> &TestPaths {
395            &self.paths
396        }
397        fn telemetry(&self) -> &FailingTelemetry {
398            &self.telemetry
399        }
400        fn error_reporter(&self) -> &RecordingReporter {
401            &self.reporter
402        }
403        fn error_formatter(&self) -> &RecordingFormatter {
404            &self.formatter
405        }
406    }
407
408    #[test]
409    fn applies_workspace_paths_to_prompt_cache() {
410        let temp_dir = TempDir::new().unwrap();
411        let root = temp_dir.path().join("workspace");
412        let config = root.join("config");
413        let cache = root.join("cache");
414        std::fs::create_dir_all(&config).unwrap();
415        std::fs::create_dir_all(&cache).unwrap();
416
417        let hooks = TestHooks {
418            paths: TestPaths { root, config, cache },
419            telemetry: RecordingTelemetry::default(),
420            reporter: RecordingReporter::default(),
421            formatter: RecordingFormatter::default(),
422        };
423        let adapter = AdapterHooks::new(&hooks);
424
425        let prompt_cache = PromptCachingConfig {
426            cache_dir: "relative/cache".to_string(),
427            ..PromptCachingConfig::default()
428        };
429
430        let config = OwnedProviderConfig::new().with_prompt_cache(prompt_cache);
431        let adapted = as_factory_config_with_hooks(&config, &adapter);
432
433        let prompt_cache = adapted.prompt_cache.expect("prompt cache present");
434        assert!(prompt_cache.cache_dir.ends_with("relative/cache"));
435
436        let events = hooks.telemetry.events.lock().unwrap();
437        assert_eq!(events.len(), 1);
438        match &events[0] {
439            AdapterEvent::PromptCacheResolved { scope, cache_dir } => {
440                assert_eq!(*scope, PathScope::Workspace);
441                assert!(cache_dir.ends_with("relative/cache"));
442            }
443            other => panic!("unexpected event: {other:?}"),
444        }
445    }
446
447    #[test]
448    fn reports_errors_when_telemetry_fails() {
449        let temp_dir = TempDir::new().unwrap();
450        let root = temp_dir.path().join("workspace");
451        let config = root.join("config");
452        let cache = root.join("cache");
453        std::fs::create_dir_all(&config).unwrap();
454        std::fs::create_dir_all(&cache).unwrap();
455
456        let hooks = FailingHooks {
457            paths: TestPaths { root, config, cache },
458            telemetry: {
459                let t = FailingTelemetry::default();
460                *t.fail_next.lock().unwrap() = true;
461                t
462            },
463            reporter: RecordingReporter::default(),
464            formatter: RecordingFormatter::default(),
465        };
466        let adapter = AdapterHooks::new(&hooks);
467
468        let prompt_cache = PromptCachingConfig {
469            cache_dir: "relative/cache".to_string(),
470            ..PromptCachingConfig::default()
471        };
472        let config = OwnedProviderConfig::new().with_prompt_cache(prompt_cache);
473        let _ = as_factory_config_with_hooks(&config, &adapter);
474
475        // Ensure the error reporter observed the telemetry failure and the
476        // formatter was invoked to produce a fallback message.
477        assert_eq!(hooks.reporter.errors.lock().unwrap().len(), 1);
478        assert_eq!(hooks.formatter.messages.lock().unwrap().len(), 1);
479
480        // The fallback telemetry event should succeed after the initial
481        // failure flag is cleared.
482        let events = hooks.telemetry.events.lock().unwrap();
483        assert_eq!(events.len(), 1);
484        assert!(matches!(events[0], AdapterEvent::TelemetryFailure { .. }));
485    }
486
487    #[test]
488    fn core_provider_config_exposes_borrowed_nested_values() {
489        let config = crate::factory_types::ProviderConfig {
490            api_key: None,
491            openai_chatgpt_auth: None,
492            copilot_auth: None,
493            base_url: None,
494            model: None,
495            prompt_cache: Some(PromptCachingConfig::default()),
496            timeouts: Some(TimeoutsConfig::default()),
497            openai: Some(OpenAIConfig::default()),
498            anthropic: Some(AnthropicConfig::default()),
499            model_behavior: Some(ModelConfig::default()),
500            workspace_root: None,
501        };
502
503        assert!(matches!(
504            <crate::factory_types::ProviderConfig as ProviderConfig>::prompt_cache(&config),
505            Some(Cow::Borrowed(_))
506        ));
507        assert!(matches!(
508            <crate::factory_types::ProviderConfig as ProviderConfig>::timeouts(&config),
509            Some(Cow::Borrowed(_))
510        ));
511        assert!(matches!(
512            <crate::factory_types::ProviderConfig as ProviderConfig>::openai(&config),
513            Some(Cow::Borrowed(_))
514        ));
515        assert!(matches!(
516            <crate::factory_types::ProviderConfig as ProviderConfig>::anthropic(&config),
517            Some(Cow::Borrowed(_))
518        ));
519        assert!(matches!(
520            <crate::factory_types::ProviderConfig as ProviderConfig>::model_behavior(&config),
521            Some(Cow::Borrowed(_))
522        ));
523    }
524
525    #[test]
526    fn owned_provider_config_exposes_borrowed_nested_values() {
527        let config = OwnedProviderConfig::new()
528            .with_prompt_cache(PromptCachingConfig::default())
529            .with_timeouts(TimeoutsConfig::default())
530            .with_openai(OpenAIConfig::default())
531            .with_anthropic(AnthropicConfig::default())
532            .with_model_behavior(ModelConfig::default());
533
534        assert!(matches!(<OwnedProviderConfig as ProviderConfig>::prompt_cache(&config), Some(Cow::Borrowed(_))));
535        assert!(matches!(<OwnedProviderConfig as ProviderConfig>::timeouts(&config), Some(Cow::Borrowed(_))));
536        assert!(matches!(<OwnedProviderConfig as ProviderConfig>::openai(&config), Some(Cow::Borrowed(_))));
537        assert!(matches!(<OwnedProviderConfig as ProviderConfig>::anthropic(&config), Some(Cow::Borrowed(_))));
538        assert!(matches!(<OwnedProviderConfig as ProviderConfig>::model_behavior(&config), Some(Cow::Borrowed(_))));
539    }
540
541    #[test]
542    fn preserves_provider_specific_fields_from_core_config() {
543        let source = crate::factory_types::ProviderConfig {
544            api_key: Some("secret".to_string()),
545            openai_chatgpt_auth: None,
546            copilot_auth: None,
547            base_url: Some("https://api.example.com".to_string()),
548            model: Some("gpt-5".to_string()),
549            prompt_cache: Some(PromptCachingConfig::default()),
550            timeouts: Some(TimeoutsConfig::default()),
551            openai: Some(OpenAIConfig { websocket_mode: true, ..OpenAIConfig::default() }),
552            anthropic: Some(AnthropicConfig {
553                count_tokens_enabled: true,
554                ..AnthropicConfig::default()
555            }),
556            model_behavior: Some(ModelConfig::default()),
557            workspace_root: None,
558        };
559
560        let adapted = as_factory_config(&source);
561
562        assert_eq!(adapted.api_key, source.api_key);
563        assert_eq!(adapted.base_url, source.base_url);
564        assert_eq!(adapted.model, source.model);
565        assert!(adapted.prompt_cache.is_some());
566        assert_eq!(adapted.timeouts.as_ref().unwrap().pty_ceiling_seconds, 300);
567        assert!(adapted.openai.as_ref().unwrap().websocket_mode);
568        assert!(adapted.anthropic.as_ref().unwrap().count_tokens_enabled);
569        assert!(adapted.model_behavior.is_some());
570    }
571
572    #[test]
573    fn owned_provider_config_keeps_provider_specific_fields() {
574        let config = OwnedProviderConfig::new()
575            .with_timeouts(TimeoutsConfig::default())
576            .with_openai(OpenAIConfig { websocket_mode: true, ..OpenAIConfig::default() })
577            .with_anthropic(AnthropicConfig {
578                count_tokens_enabled: true,
579                ..AnthropicConfig::default()
580            });
581
582        let adapted = as_factory_config(&config);
583
584        assert_eq!(adapted.timeouts.as_ref().unwrap().streaming_ceiling_seconds, 600);
585        assert!(adapted.openai.as_ref().unwrap().websocket_mode);
586        assert!(adapted.anthropic.as_ref().unwrap().count_tokens_enabled);
587    }
588}
589
590/// [`ProviderConfig`] implementation for the concrete factory configuration.
591impl ProviderConfig for crate::factory_types::ProviderConfig {
592    fn api_key(&self) -> Option<Cow<'_, str>> {
593        borrowed_optional_str(&self.api_key)
594    }
595
596    fn base_url(&self) -> Option<Cow<'_, str>> {
597        borrowed_optional_str(&self.base_url)
598    }
599
600    fn model(&self) -> Option<Cow<'_, str>> {
601        borrowed_optional_str(&self.model)
602    }
603
604    fn prompt_cache(&self) -> Option<Cow<'_, PromptCachingConfig>> {
605        borrowed_optional(&self.prompt_cache)
606    }
607
608    fn timeouts(&self) -> Option<Cow<'_, TimeoutsConfig>> {
609        borrowed_optional(&self.timeouts)
610    }
611
612    fn openai(&self) -> Option<Cow<'_, OpenAIConfig>> {
613        borrowed_optional(&self.openai)
614    }
615
616    fn anthropic(&self) -> Option<Cow<'_, AnthropicConfig>> {
617        borrowed_optional(&self.anthropic)
618    }
619
620    fn model_behavior(&self) -> Option<Cow<'_, ModelConfig>> {
621        borrowed_optional(&self.model_behavior)
622    }
623}
624
625/// Simple builder-friendly provider configuration backed by owned values.
626#[derive(Clone, Debug, Default)]
627#[must_use = "builders do nothing unless consumed"]
628pub struct OwnedProviderConfig {
629    api_key: Option<String>,
630    base_url: Option<String>,
631    model: Option<String>,
632    prompt_cache: Option<PromptCachingConfig>,
633    timeouts: Option<TimeoutsConfig>,
634    openai: Option<OpenAIConfig>,
635    anthropic: Option<AnthropicConfig>,
636    model_behavior: Option<ModelConfig>,
637}
638
639impl OwnedProviderConfig {
640    fn new() -> Self {
641        Self::default()
642    }
643
644    pub fn with_api_key(mut self, value: impl Into<String>) -> Self {
645        self.api_key = Some(value.into());
646        self
647    }
648
649    pub fn with_base_url(mut self, value: impl Into<String>) -> Self {
650        self.base_url = Some(value.into());
651        self
652    }
653
654    pub fn with_model(mut self, value: impl Into<String>) -> Self {
655        self.model = Some(value.into());
656        self
657    }
658
659    fn with_prompt_cache(mut self, value: PromptCachingConfig) -> Self {
660        self.prompt_cache = Some(value);
661        self
662    }
663
664    fn with_timeouts(mut self, value: TimeoutsConfig) -> Self {
665        self.timeouts = Some(value);
666        self
667    }
668
669    fn with_openai(mut self, value: OpenAIConfig) -> Self {
670        self.openai = Some(value);
671        self
672    }
673
674    fn with_anthropic(mut self, value: AnthropicConfig) -> Self {
675        self.anthropic = Some(value);
676        self
677    }
678
679    fn with_model_behavior(mut self, value: ModelConfig) -> Self {
680        self.model_behavior = Some(value);
681        self
682    }
683}
684
685impl ProviderConfig for OwnedProviderConfig {
686    fn api_key(&self) -> Option<Cow<'_, str>> {
687        borrowed_optional_str(&self.api_key)
688    }
689
690    fn base_url(&self) -> Option<Cow<'_, str>> {
691        borrowed_optional_str(&self.base_url)
692    }
693
694    fn model(&self) -> Option<Cow<'_, str>> {
695        borrowed_optional_str(&self.model)
696    }
697
698    fn prompt_cache(&self) -> Option<Cow<'_, PromptCachingConfig>> {
699        borrowed_optional(&self.prompt_cache)
700    }
701
702    fn timeouts(&self) -> Option<Cow<'_, TimeoutsConfig>> {
703        borrowed_optional(&self.timeouts)
704    }
705
706    fn openai(&self) -> Option<Cow<'_, OpenAIConfig>> {
707        borrowed_optional(&self.openai)
708    }
709
710    fn anthropic(&self) -> Option<Cow<'_, AnthropicConfig>> {
711        borrowed_optional(&self.anthropic)
712    }
713
714    fn model_behavior(&self) -> Option<Cow<'_, ModelConfig>> {
715        borrowed_optional(&self.model_behavior)
716    }
717}