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