1use 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
19pub trait ProviderConfig {
27 fn api_key(&self) -> Option<Cow<'_, str>>;
29
30 fn base_url(&self) -> Option<Cow<'_, str>> {
32 None
33 }
34
35 fn model(&self) -> Option<Cow<'_, str>> {
37 None
38 }
39
40 fn prompt_cache(&self) -> Option<Cow<'_, PromptCachingConfig>> {
43 None
44 }
45
46 fn timeouts(&self) -> Option<Cow<'_, TimeoutsConfig>> {
48 None
49 }
50
51 fn openai(&self) -> Option<Cow<'_, OpenAIConfig>> {
53 None
54 }
55
56 fn anthropic(&self) -> Option<Cow<'_, AnthropicConfig>> {
58 None
59 }
60
61 fn openrouter(&self) -> Option<Cow<'_, OpenRouterConfig>> {
63 None
64 }
65
66 fn model_behavior(&self) -> Option<Cow<'_, ModelConfig>> {
68 None
69 }
70}
71
72enum 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
110pub fn as_factory_config(source: &dyn ProviderConfig) -> crate::factory_types::ProviderConfig {
113 BorrowedConfigProjectionCtx { source }.project_factory_config()
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum AdapterEvent {
119 PromptCacheResolved { scope: PathScope, cache_dir: PathBuf },
121 TelemetryFailure { message: String },
124 AdapterError { message: String },
126}
127
128pub 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
148pub struct AdapterHooks<'a, Hooks: AdapterHooksProvider> {
155 hooks: &'a Hooks,
156}
157
158impl<'a, Hooks: AdapterHooksProvider> AdapterHooks<'a, Hooks> {
159 fn new(hooks: &'a Hooks) -> Self {
161 Self { hooks }
162 }
163
164 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 let is_abs = resolved.is_absolute();
175 let resolved_display = resolved.display().to_string();
176
177 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 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
239pub 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 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 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 assert_eq!(hooks.reporter.errors.lock().unwrap().len(), 1);
485 assert_eq!(hooks.formatter.messages.lock().unwrap().len(), 1);
486
487 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
606impl 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#[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}