1use std::collections::{BTreeMap, HashSet};
10
11use serde::Deserialize;
12
13use super::model::{
14 fill_opt, Capabilities, CapabilitiesFile, ComputerUseStyle, LiveEndpointFamily,
15 ProviderDefaults, ReasoningHistoryWireField, ScreenshotScaling, SystemMessagePlacement,
16 WireDialect,
17};
18use crate::llm::providers::anthropic::claude_generation;
19use crate::llm::providers::openai_compat::gpt_generation;
20
21use harn_glob::match_name as glob_match;
25
26#[derive(Debug, Clone, Deserialize)]
28pub struct ProviderRule {
29 pub model_match: String,
32 #[serde(default)]
37 pub version_min: Option<Vec<u32>>,
38 #[serde(default)]
47 pub extends: bool,
48 #[serde(default)]
49 pub native_tools: Option<bool>,
50 #[serde(default)]
53 pub message_wire_format: Option<String>,
54 #[serde(default)]
60 pub live_endpoint_family: Option<LiveEndpointFamily>,
61 #[serde(default)]
64 pub native_tool_wire_format: Option<String>,
65 #[serde(default)]
66 pub defer_loading: Option<bool>,
67 #[serde(default)]
68 pub tool_search: Option<Vec<String>>,
69 #[serde(default)]
72 pub responses_api: Option<bool>,
73 #[serde(default)]
75 pub hosted_tools: Option<Vec<String>>,
76 #[serde(default)]
79 pub remote_mcp: Option<bool>,
80 #[serde(default)]
83 pub conversation_state: Option<bool>,
84 #[serde(default)]
86 pub compaction: Option<bool>,
87 #[serde(default)]
89 pub background_mode: Option<bool>,
90 #[serde(default)]
93 pub batch_api: Option<bool>,
94 #[serde(default)]
98 pub batch_wire_format: Option<String>,
99 #[serde(default)]
102 pub batch_input_mode: Option<String>,
103 #[serde(default)]
106 pub batch_discount_percent: Option<u32>,
107 #[serde(default)]
110 pub batch_turnaround_hours: Option<u32>,
111 #[serde(default)]
113 pub batch_max_requests: Option<u64>,
114 #[serde(default)]
117 pub batch_max_input_bytes: Option<u64>,
118 #[serde(default)]
121 pub batch_result_retention_days: Option<u32>,
122 #[serde(default)]
125 pub batch_result_ordering: Option<String>,
126 #[serde(default)]
129 pub batch_partial_failure: Option<String>,
130 #[serde(default)]
133 pub batch_cancellation: Option<String>,
134 #[serde(default)]
137 pub batch_security_notes: Option<Vec<String>>,
138 #[serde(default)]
142 pub batch_operational_notes: Option<Vec<String>>,
143 #[serde(default)]
145 pub batch_regions: Option<Vec<String>>,
146 #[serde(default)]
148 pub tool_approval_policy: Option<String>,
149 #[serde(default)]
150 pub max_tools: Option<u32>,
151 #[serde(default)]
152 pub prompt_caching: Option<bool>,
153 #[serde(default)]
157 pub prompt_cache_ttls: Option<Vec<String>>,
158 #[serde(default)]
168 pub prompt_cache_min_prefix_tokens: Option<u32>,
169 #[serde(default)]
173 pub cache_breakpoint_style: Option<String>,
174 #[serde(default)]
177 pub vision: Option<bool>,
178 #[serde(default, alias = "audio_supported")]
181 pub audio: Option<bool>,
182 #[serde(default, alias = "pdf_supported")]
185 pub pdf: Option<bool>,
186 #[serde(default, alias = "video_supported")]
189 pub video: Option<bool>,
190 #[serde(default)]
192 pub files_api_supported: Option<bool>,
193 #[serde(default)]
196 pub file_upload_wire_format: Option<String>,
197 #[serde(default)]
200 pub structured_output: Option<String>,
201 #[serde(default)]
204 pub json_schema: Option<String>,
205 #[serde(default)]
208 pub prefers_xml_scaffolding: Option<bool>,
209 #[serde(default)]
214 pub reserved_tool_call_token: Option<bool>,
215 #[serde(default)]
218 pub prefers_markdown_scaffolding: Option<bool>,
219 #[serde(default)]
223 pub structured_output_mode: Option<String>,
224 #[serde(default)]
226 pub supports_assistant_prefill: Option<bool>,
227 #[serde(default)]
230 pub prefers_role_developer: Option<bool>,
231 #[serde(default)]
234 pub prefers_xml_tools: Option<bool>,
235 #[serde(default)]
239 pub thinking_block_style: Option<String>,
240 #[serde(default)]
243 pub thinking_modes: Option<Vec<String>>,
244 #[serde(default)]
247 pub interleaved_thinking_supported: Option<bool>,
248 #[serde(default)]
250 pub anthropic_beta_features: Option<Vec<String>>,
251 #[serde(default)]
254 pub thinking: Option<bool>,
255 #[serde(default)]
257 pub vision_supported: Option<bool>,
258 #[serde(default)]
260 pub image_url_input_supported: Option<bool>,
261 #[serde(default)]
268 pub preserve_thinking: Option<bool>,
269 #[serde(default)]
272 pub reasoning_history_wire_field: Option<ReasoningHistoryWireField>,
273 #[serde(default)]
277 pub server_parser: Option<String>,
278 #[serde(default)]
282 pub honors_chat_template_kwargs: Option<bool>,
283 #[serde(default)]
286 pub chat_template_options_field: Option<String>,
287 #[serde(default)]
290 pub requires_completion_tokens: Option<bool>,
291 #[serde(default)]
298 pub chat_completions_unsupported: Option<bool>,
299 #[serde(default)]
303 pub reasoning_tools_require_responses: Option<bool>,
304 #[serde(default)]
308 pub requires_streaming: Option<bool>,
309 #[serde(default)]
313 pub reasoning_effort_supported: Option<bool>,
314 #[serde(default)]
317 pub reasoning_effort_levels: Option<Vec<String>>,
318 #[serde(default)]
322 pub reasoning_none_supported: Option<bool>,
323 #[serde(default)]
331 pub max_thinking_budget: Option<i64>,
332 #[serde(default)]
335 pub reasoning_disable_supported: Option<bool>,
336 #[serde(default)]
349 pub reasoning_required_for_tools: Option<bool>,
350 #[serde(default)]
354 pub reasoning_text_promotable: Option<bool>,
355 #[serde(default)]
358 pub reasoning_wire_format: Option<String>,
359 #[serde(default)]
360 pub seed_supported: Option<bool>,
361 #[serde(default)]
362 pub top_k_supported: Option<bool>,
363 #[serde(default)]
364 pub temperature_supported: Option<bool>,
365 #[serde(default)]
366 pub top_p_supported: Option<bool>,
367 #[serde(default)]
368 pub frequency_penalty_supported: Option<bool>,
369 #[serde(default)]
370 pub presence_penalty_supported: Option<bool>,
371 #[serde(default)]
374 pub stop_supported: Option<bool>,
375 #[serde(default)]
379 pub allowed_tool_choice_modes: Option<Vec<String>>,
380 #[serde(default)]
383 pub requires_tool_result_adjacency: Option<bool>,
384 #[serde(default)]
390 pub supports_parallel_tool_calls: Option<bool>,
391 #[serde(default)]
395 pub tools_exclude_response_format: Option<bool>,
396 #[serde(default)]
400 pub recommended_endpoint: Option<String>,
401 #[serde(default)]
404 pub text_tool_wire_format_supported: Option<bool>,
405 #[serde(default)]
410 pub preferred_tool_format: Option<String>,
411 #[serde(default)]
416 pub tool_mode_parity: Option<String>,
417 #[serde(default)]
419 pub tool_mode_parity_notes: Option<String>,
420 #[serde(default)]
427 pub thinking_disable_directive: Option<String>,
428 #[serde(default)]
441 pub auto_reasoning_overrides: Option<BTreeMap<String, String>>,
442 #[serde(default)]
453 pub provider_route_denylist: Option<Vec<String>>,
454 #[serde(default)]
472 pub openrouter_provider_order: Option<Vec<String>>,
473 #[serde(default)]
487 pub serving_precision: Option<String>,
488 #[serde(default)]
497 pub computer_use_style: Option<ComputerUseStyle>,
498 #[serde(default)]
502 pub screenshot_scaling: Option<ScreenshotScaling>,
503 #[serde(default)]
508 pub safety_ack_flow: Option<bool>,
509 #[serde(default)]
514 pub system_message_placement: Option<SystemMessagePlacement>,
515}
516
517impl ProviderRule {
518 fn fill_missing_from(&mut self, other: &ProviderRule) {
528 let ProviderRule {
529 model_match: _,
532 version_min: _,
533 extends: _,
534 native_tools,
535 message_wire_format,
536 live_endpoint_family,
537 native_tool_wire_format,
538 defer_loading,
539 tool_search,
540 responses_api,
541 hosted_tools,
542 remote_mcp,
543 conversation_state,
544 compaction,
545 background_mode,
546 batch_api,
547 batch_wire_format,
548 batch_input_mode,
549 batch_discount_percent,
550 batch_turnaround_hours,
551 batch_max_requests,
552 batch_max_input_bytes,
553 batch_result_retention_days,
554 batch_result_ordering,
555 batch_partial_failure,
556 batch_cancellation,
557 batch_security_notes,
558 batch_operational_notes,
559 batch_regions,
560 tool_approval_policy,
561 max_tools,
562 prompt_caching,
563 prompt_cache_ttls,
564 prompt_cache_min_prefix_tokens,
565 cache_breakpoint_style,
566 vision,
567 audio,
568 pdf,
569 video,
570 files_api_supported,
571 file_upload_wire_format,
572 structured_output,
573 json_schema,
574 prefers_xml_scaffolding,
575 reserved_tool_call_token,
576 prefers_markdown_scaffolding,
577 structured_output_mode,
578 supports_assistant_prefill,
579 prefers_role_developer,
580 prefers_xml_tools,
581 thinking_block_style,
582 thinking_modes,
583 interleaved_thinking_supported,
584 anthropic_beta_features,
585 thinking,
586 vision_supported,
587 image_url_input_supported,
588 preserve_thinking,
589 reasoning_history_wire_field,
590 server_parser,
591 honors_chat_template_kwargs,
592 chat_template_options_field,
593 requires_completion_tokens,
594 chat_completions_unsupported,
595 reasoning_tools_require_responses,
596 requires_streaming,
597 reasoning_effort_supported,
598 reasoning_effort_levels,
599 reasoning_none_supported,
600 max_thinking_budget,
601 reasoning_disable_supported,
602 reasoning_required_for_tools,
603 reasoning_text_promotable,
604 reasoning_wire_format,
605 seed_supported,
606 top_k_supported,
607 temperature_supported,
608 top_p_supported,
609 frequency_penalty_supported,
610 presence_penalty_supported,
611 stop_supported,
612 allowed_tool_choice_modes,
613 requires_tool_result_adjacency,
614 supports_parallel_tool_calls,
615 tools_exclude_response_format,
616 recommended_endpoint,
617 text_tool_wire_format_supported,
618 preferred_tool_format,
619 tool_mode_parity,
620 tool_mode_parity_notes,
621 thinking_disable_directive,
622 auto_reasoning_overrides,
623 provider_route_denylist,
624 openrouter_provider_order,
625 serving_precision,
626 computer_use_style,
627 screenshot_scaling,
628 safety_ack_flow,
629 system_message_placement,
630 } = other;
631 fill_opt(&mut self.native_tools, native_tools);
632 fill_opt(&mut self.message_wire_format, message_wire_format);
633 fill_opt(&mut self.live_endpoint_family, live_endpoint_family);
634 fill_opt(&mut self.native_tool_wire_format, native_tool_wire_format);
635 fill_opt(&mut self.defer_loading, defer_loading);
636 fill_opt(&mut self.tool_search, tool_search);
637 fill_opt(&mut self.responses_api, responses_api);
638 fill_opt(&mut self.hosted_tools, hosted_tools);
639 fill_opt(&mut self.remote_mcp, remote_mcp);
640 fill_opt(&mut self.conversation_state, conversation_state);
641 fill_opt(&mut self.compaction, compaction);
642 fill_opt(&mut self.background_mode, background_mode);
643 fill_opt(&mut self.batch_api, batch_api);
644 fill_opt(&mut self.batch_wire_format, batch_wire_format);
645 fill_opt(&mut self.batch_input_mode, batch_input_mode);
646 fill_opt(&mut self.batch_discount_percent, batch_discount_percent);
647 fill_opt(&mut self.batch_turnaround_hours, batch_turnaround_hours);
648 fill_opt(&mut self.batch_max_requests, batch_max_requests);
649 fill_opt(&mut self.batch_max_input_bytes, batch_max_input_bytes);
650 fill_opt(
651 &mut self.batch_result_retention_days,
652 batch_result_retention_days,
653 );
654 fill_opt(&mut self.batch_result_ordering, batch_result_ordering);
655 fill_opt(&mut self.batch_partial_failure, batch_partial_failure);
656 fill_opt(&mut self.batch_cancellation, batch_cancellation);
657 fill_opt(&mut self.batch_security_notes, batch_security_notes);
658 fill_opt(&mut self.batch_operational_notes, batch_operational_notes);
659 fill_opt(&mut self.batch_regions, batch_regions);
660 fill_opt(&mut self.tool_approval_policy, tool_approval_policy);
661 fill_opt(&mut self.max_tools, max_tools);
662 fill_opt(&mut self.prompt_caching, prompt_caching);
663 fill_opt(&mut self.prompt_cache_ttls, prompt_cache_ttls);
664 fill_opt(
665 &mut self.prompt_cache_min_prefix_tokens,
666 prompt_cache_min_prefix_tokens,
667 );
668 fill_opt(&mut self.cache_breakpoint_style, cache_breakpoint_style);
669 fill_opt(&mut self.audio, audio);
670 fill_opt(&mut self.pdf, pdf);
671 fill_opt(&mut self.video, video);
672 fill_opt(&mut self.files_api_supported, files_api_supported);
673 fill_opt(&mut self.file_upload_wire_format, file_upload_wire_format);
674 fill_opt(&mut self.prefers_xml_scaffolding, prefers_xml_scaffolding);
675 fill_opt(&mut self.reserved_tool_call_token, reserved_tool_call_token);
676 fill_opt(
677 &mut self.prefers_markdown_scaffolding,
678 prefers_markdown_scaffolding,
679 );
680 fill_opt(&mut self.structured_output_mode, structured_output_mode);
681 fill_opt(
682 &mut self.supports_assistant_prefill,
683 supports_assistant_prefill,
684 );
685 fill_opt(&mut self.prefers_role_developer, prefers_role_developer);
686 fill_opt(&mut self.prefers_xml_tools, prefers_xml_tools);
687 fill_opt(&mut self.thinking_block_style, thinking_block_style);
688 fill_opt(
689 &mut self.interleaved_thinking_supported,
690 interleaved_thinking_supported,
691 );
692 fill_opt(&mut self.anthropic_beta_features, anthropic_beta_features);
693 fill_opt(
694 &mut self.image_url_input_supported,
695 image_url_input_supported,
696 );
697 fill_opt(&mut self.preserve_thinking, preserve_thinking);
698 fill_opt(
699 &mut self.reasoning_history_wire_field,
700 reasoning_history_wire_field,
701 );
702 fill_opt(&mut self.server_parser, server_parser);
703 fill_opt(
704 &mut self.honors_chat_template_kwargs,
705 honors_chat_template_kwargs,
706 );
707 fill_opt(
708 &mut self.chat_template_options_field,
709 chat_template_options_field,
710 );
711 fill_opt(
712 &mut self.requires_completion_tokens,
713 requires_completion_tokens,
714 );
715 fill_opt(
716 &mut self.chat_completions_unsupported,
717 chat_completions_unsupported,
718 );
719 fill_opt(
720 &mut self.reasoning_tools_require_responses,
721 reasoning_tools_require_responses,
722 );
723 fill_opt(&mut self.requires_streaming, requires_streaming);
724 fill_opt(
725 &mut self.reasoning_effort_supported,
726 reasoning_effort_supported,
727 );
728 fill_opt(&mut self.reasoning_effort_levels, reasoning_effort_levels);
729 fill_opt(&mut self.reasoning_none_supported, reasoning_none_supported);
730 fill_opt(&mut self.max_thinking_budget, max_thinking_budget);
731 fill_opt(
732 &mut self.reasoning_disable_supported,
733 reasoning_disable_supported,
734 );
735 fill_opt(
736 &mut self.reasoning_required_for_tools,
737 reasoning_required_for_tools,
738 );
739 fill_opt(
740 &mut self.reasoning_text_promotable,
741 reasoning_text_promotable,
742 );
743 fill_opt(&mut self.reasoning_wire_format, reasoning_wire_format);
744 fill_opt(&mut self.seed_supported, seed_supported);
745 fill_opt(&mut self.top_k_supported, top_k_supported);
746 fill_opt(&mut self.temperature_supported, temperature_supported);
747 fill_opt(&mut self.top_p_supported, top_p_supported);
748 fill_opt(
749 &mut self.frequency_penalty_supported,
750 frequency_penalty_supported,
751 );
752 fill_opt(
753 &mut self.presence_penalty_supported,
754 presence_penalty_supported,
755 );
756 fill_opt(&mut self.stop_supported, stop_supported);
757 fill_opt(
758 &mut self.allowed_tool_choice_modes,
759 allowed_tool_choice_modes,
760 );
761 fill_opt(
762 &mut self.requires_tool_result_adjacency,
763 requires_tool_result_adjacency,
764 );
765 fill_opt(
766 &mut self.supports_parallel_tool_calls,
767 supports_parallel_tool_calls,
768 );
769 fill_opt(
770 &mut self.tools_exclude_response_format,
771 tools_exclude_response_format,
772 );
773 fill_opt(&mut self.recommended_endpoint, recommended_endpoint);
774 fill_opt(
775 &mut self.text_tool_wire_format_supported,
776 text_tool_wire_format_supported,
777 );
778 fill_opt(&mut self.preferred_tool_format, preferred_tool_format);
779 fill_opt(&mut self.tool_mode_parity, tool_mode_parity);
780 fill_opt(&mut self.tool_mode_parity_notes, tool_mode_parity_notes);
781 fill_opt(
782 &mut self.thinking_disable_directive,
783 thinking_disable_directive,
784 );
785 fill_opt(&mut self.auto_reasoning_overrides, auto_reasoning_overrides);
786 fill_opt(&mut self.provider_route_denylist, provider_route_denylist);
787 fill_opt(
788 &mut self.openrouter_provider_order,
789 openrouter_provider_order,
790 );
791 fill_opt(&mut self.serving_precision, serving_precision);
792 fill_opt(&mut self.computer_use_style, computer_use_style);
793 fill_opt(&mut self.screenshot_scaling, screenshot_scaling);
794 fill_opt(&mut self.safety_ack_flow, safety_ack_flow);
795 fill_opt(&mut self.system_message_placement, system_message_placement);
796 if self.structured_output.is_none() && self.json_schema.is_none() {
802 self.structured_output.clone_from(structured_output);
803 self.json_schema.clone_from(json_schema);
804 }
805 if self.thinking_modes.is_none() && self.thinking.is_none() {
806 self.thinking_modes.clone_from(thinking_modes);
807 self.thinking.clone_from(thinking);
808 }
809 if self.vision.is_none() && self.vision_supported.is_none() {
810 self.vision.clone_from(vision);
811 self.vision_supported.clone_from(vision_supported);
812 }
813 }
814}
815
816pub(super) struct MatchedCapabilityRule {
817 pub(super) provider: String,
819 pub(super) rule: ProviderRule,
822 pub(super) matched_patterns: Vec<String>,
825}
826
827#[derive(Default)]
833struct RuleResolution {
834 provider: Option<String>,
836 merged: Option<ProviderRule>,
837 matched_patterns: Vec<String>,
839}
840
841impl RuleResolution {
842 fn absorb(&mut self, layer_provider: &str, rule: &ProviderRule) -> bool {
846 if self.provider.is_none() {
847 self.provider = Some(layer_provider.to_string());
848 }
849 self.matched_patterns.push(rule.model_match.clone());
850 match &mut self.merged {
851 None => self.merged = Some(rule.clone()),
852 Some(merged) => merged.fill_missing_from(rule),
853 }
854 !rule.extends
855 }
856
857 fn into_matched(self) -> Option<MatchedCapabilityRule> {
858 Some(MatchedCapabilityRule {
859 provider: self.provider?,
860 rule: self.merged.expect("merged is set whenever provider is set"),
861 matched_patterns: self.matched_patterns,
862 })
863 }
864}
865
866fn absorb_layer_matches(
871 user: Option<&CapabilitiesFile>,
872 builtin: &CapabilitiesFile,
873 layer_provider: &str,
874 model: &str,
875 resolution: &mut RuleResolution,
876) -> bool {
877 for file in user.into_iter().chain(std::iter::once(builtin)) {
878 if let Some(rules) = file.provider.get(layer_provider) {
879 for rule in rules {
880 if rule_matches(rule, model) && resolution.absorb(layer_provider, rule) {
881 return true;
882 }
883 }
884 }
885 }
886 false
887}
888
889fn resolve_rule_chain(
897 user: Option<&CapabilitiesFile>,
898 builtin: &CapabilitiesFile,
899 provider: &str,
900 model: &str,
901) -> (RuleResolution, ProviderDefaults) {
902 let mut resolution = RuleResolution::default();
903 let mut effective_defaults = ProviderDefaults::default();
904 let mut current = provider.to_string();
905 let mut visited = HashSet::new();
906 while visited.insert(current.clone()) {
907 let layer_defaults = merged_provider_defaults(user, builtin, ¤t);
908 if effective_defaults.has_any_field() {
909 effective_defaults.fill_missing_from(&layer_defaults);
910 } else {
911 effective_defaults.overlay(&layer_defaults);
912 }
913 if absorb_layer_matches(user, builtin, ¤t, model, &mut resolution) {
914 break;
915 }
916 let next = user
917 .and_then(|file| file.provider_family.get(¤t))
918 .or_else(|| builtin.provider_family.get(¤t))
919 .cloned();
920 match next {
921 Some(parent) => current = parent,
922 None => break,
923 }
924 }
925 (resolution, effective_defaults)
926}
927
928pub(super) fn first_matching_rule(
929 user: Option<&CapabilitiesFile>,
930 builtin: &CapabilitiesFile,
931 provider: &str,
932 model: &str,
933) -> Option<MatchedCapabilityRule> {
934 resolve_rule_chain(user, builtin, provider, model)
935 .0
936 .into_matched()
937}
938
939pub(super) fn rule_thinking_modes(rule: &ProviderRule) -> Vec<String> {
940 rule.thinking_modes.clone().unwrap_or_else(|| {
941 if rule.thinking.unwrap_or(false) {
942 vec!["enabled".to_string()]
943 } else {
944 Vec::new()
945 }
946 })
947}
948
949pub(super) fn rule_vision(rule: &ProviderRule) -> bool {
950 rule.vision.or(rule.vision_supported).unwrap_or(false)
951}
952
953pub(super) fn lookup_with(
954 provider: &str,
955 model: &str,
956 builtin: &CapabilitiesFile,
957 user: Option<&CapabilitiesFile>,
958) -> Capabilities {
959 if provider == "mock" {
970 for family in ["anthropic", "openai", "gemini"] {
971 let defaults = merged_provider_defaults(user, builtin, family);
972 let mut resolution = RuleResolution::default();
973 absorb_layer_matches(user, builtin, family, model, &mut resolution);
974 if let Some(rule) = resolution.merged.as_ref() {
975 let mut caps = rule_to_caps(rule, &defaults);
976 if family == "anthropic" {
977 caps.native_tool_wire_format = "openai".to_string();
978 }
979 return caps;
980 }
981 }
982 return Capabilities::default();
983 }
984
985 let (resolution, effective_defaults) = resolve_rule_chain(user, builtin, provider, model);
988 if let Some(rule) = resolution.merged.as_ref() {
989 return rule_to_caps(rule, &effective_defaults);
990 }
991 if effective_defaults.has_any_field() {
992 return defaults_to_caps(&effective_defaults);
993 }
994 Capabilities::default()
995}
996
997fn merged_provider_defaults(
998 user: Option<&CapabilitiesFile>,
999 builtin: &CapabilitiesFile,
1000 provider: &str,
1001) -> ProviderDefaults {
1002 let mut defaults = builtin
1003 .provider_defaults
1004 .get(provider)
1005 .cloned()
1006 .unwrap_or_default();
1007 if let Some(user_defaults) = user.and_then(|file| file.provider_defaults.get(provider)) {
1008 defaults.overlay(user_defaults);
1009 }
1010 defaults
1011}
1012
1013fn defaults_to_caps(defaults: &ProviderDefaults) -> Capabilities {
1014 let empty = ProviderRule {
1015 model_match: "*".to_string(),
1016 version_min: None,
1017 extends: false,
1018 native_tools: None,
1019 message_wire_format: None,
1020 live_endpoint_family: None,
1021 native_tool_wire_format: None,
1022 defer_loading: None,
1023 tool_search: None,
1024 responses_api: None,
1025 hosted_tools: None,
1026 remote_mcp: None,
1027 conversation_state: None,
1028 compaction: None,
1029 background_mode: None,
1030 tool_approval_policy: None,
1031 batch_api: None,
1032 batch_wire_format: None,
1033 batch_input_mode: None,
1034 batch_discount_percent: None,
1035 batch_turnaround_hours: None,
1036 batch_max_requests: None,
1037 batch_max_input_bytes: None,
1038 batch_result_retention_days: None,
1039 batch_result_ordering: None,
1040 batch_partial_failure: None,
1041 batch_cancellation: None,
1042 batch_security_notes: None,
1043 batch_operational_notes: None,
1044 batch_regions: None,
1045 max_tools: None,
1046 prompt_caching: None,
1047 prompt_cache_ttls: None,
1048 prompt_cache_min_prefix_tokens: None,
1049 cache_breakpoint_style: None,
1050 vision: None,
1051 audio: None,
1052 pdf: None,
1053 video: None,
1054 files_api_supported: None,
1055 file_upload_wire_format: None,
1056 structured_output: None,
1057 prefers_xml_scaffolding: None,
1058 reserved_tool_call_token: None,
1059 prefers_markdown_scaffolding: None,
1060 structured_output_mode: None,
1061 supports_assistant_prefill: None,
1062 prefers_role_developer: None,
1063 prefers_xml_tools: None,
1064 thinking_block_style: None,
1065 json_schema: None,
1066 thinking_modes: None,
1067 interleaved_thinking_supported: None,
1068 anthropic_beta_features: None,
1069 thinking: None,
1070 vision_supported: None,
1071 image_url_input_supported: None,
1072 preserve_thinking: None,
1073 reasoning_history_wire_field: None,
1074 server_parser: None,
1075 honors_chat_template_kwargs: None,
1076 chat_template_options_field: None,
1077 requires_completion_tokens: None,
1078 chat_completions_unsupported: None,
1079 reasoning_tools_require_responses: None,
1080 requires_streaming: None,
1081 reasoning_effort_supported: None,
1082 reasoning_effort_levels: None,
1083 reasoning_none_supported: None,
1084 max_thinking_budget: None,
1085 reasoning_disable_supported: None,
1086 reasoning_required_for_tools: None,
1087 reasoning_text_promotable: None,
1088 reasoning_wire_format: None,
1089 seed_supported: None,
1090 top_k_supported: None,
1091 temperature_supported: None,
1092 top_p_supported: None,
1093 frequency_penalty_supported: None,
1094 presence_penalty_supported: None,
1095 stop_supported: None,
1096 allowed_tool_choice_modes: None,
1097 requires_tool_result_adjacency: None,
1098 supports_parallel_tool_calls: None,
1099 tools_exclude_response_format: None,
1100 recommended_endpoint: None,
1101 text_tool_wire_format_supported: None,
1102 preferred_tool_format: None,
1103 tool_mode_parity: None,
1104 tool_mode_parity_notes: None,
1105 thinking_disable_directive: None,
1106 auto_reasoning_overrides: None,
1107 provider_route_denylist: None,
1108 openrouter_provider_order: None,
1109 serving_precision: None,
1110 computer_use_style: None,
1111 screenshot_scaling: None,
1112 safety_ack_flow: None,
1113 system_message_placement: None,
1114 };
1115 let mut caps = rule_to_caps(&empty, defaults);
1116 caps.preferred_tool_format = None;
1117 caps.tool_mode_parity = None;
1118 caps
1119}
1120
1121fn rule_to_caps(rule: &ProviderRule, defaults: &ProviderDefaults) -> Capabilities {
1122 let thinking_modes = rule_thinking_modes(rule);
1123 let thinking_block_style = rule_thinking_block_style(rule);
1124 let prompt_caching = rule.prompt_caching.unwrap_or(false);
1125 let emits_inline_reasoning = thinking_block_style == "inline";
1130 let message_wire_format = WireDialect::from_message_wire_format(
1131 &rule
1132 .message_wire_format
1133 .clone()
1134 .or_else(|| defaults.message_wire_format.clone())
1135 .unwrap_or_else(|| "openai".to_string()),
1136 );
1137 let live_endpoint_family = rule
1143 .live_endpoint_family
1144 .or(defaults.live_endpoint_family)
1145 .or_else(|| {
1146 (message_wire_format == WireDialect::Gemini)
1147 .then_some(LiveEndpointFamily::GeminiGenerateContent)
1148 });
1149 Capabilities {
1150 native_tools: rule.native_tools.unwrap_or(false),
1151 message_wire_format,
1152 live_endpoint_family,
1153 native_tool_wire_format: rule
1154 .native_tool_wire_format
1155 .clone()
1156 .or_else(|| defaults.native_tool_wire_format.clone())
1157 .unwrap_or_else(|| "openai".to_string()),
1158 defer_loading: rule.defer_loading.unwrap_or(false),
1159 tool_search: rule.tool_search.clone().unwrap_or_default(),
1160 responses_api: rule.responses_api.unwrap_or(false),
1161 hosted_tools: rule.hosted_tools.clone().unwrap_or_default(),
1162 remote_mcp: rule.remote_mcp.unwrap_or(false),
1163 conversation_state: rule.conversation_state.unwrap_or(false),
1164 compaction: rule.compaction.unwrap_or(false),
1165 background_mode: rule.background_mode.unwrap_or(false),
1166 batch_api: rule.batch_api.or(defaults.batch_api).unwrap_or(false),
1167 batch_wire_format: rule
1168 .batch_wire_format
1169 .clone()
1170 .or_else(|| defaults.batch_wire_format.clone()),
1171 batch_input_mode: rule
1172 .batch_input_mode
1173 .clone()
1174 .or_else(|| defaults.batch_input_mode.clone()),
1175 batch_discount_percent: rule
1176 .batch_discount_percent
1177 .or(defaults.batch_discount_percent),
1178 batch_turnaround_hours: rule
1179 .batch_turnaround_hours
1180 .or(defaults.batch_turnaround_hours),
1181 batch_max_requests: rule.batch_max_requests.or(defaults.batch_max_requests),
1182 batch_max_input_bytes: rule
1183 .batch_max_input_bytes
1184 .or(defaults.batch_max_input_bytes),
1185 batch_result_retention_days: rule
1186 .batch_result_retention_days
1187 .or(defaults.batch_result_retention_days),
1188 batch_result_ordering: rule
1189 .batch_result_ordering
1190 .clone()
1191 .or_else(|| defaults.batch_result_ordering.clone()),
1192 batch_partial_failure: rule
1193 .batch_partial_failure
1194 .clone()
1195 .or_else(|| defaults.batch_partial_failure.clone()),
1196 batch_cancellation: rule
1197 .batch_cancellation
1198 .clone()
1199 .or_else(|| defaults.batch_cancellation.clone()),
1200 batch_security_notes: rule
1201 .batch_security_notes
1202 .clone()
1203 .or_else(|| defaults.batch_security_notes.clone())
1204 .unwrap_or_default(),
1205 batch_operational_notes: rule
1206 .batch_operational_notes
1207 .clone()
1208 .or_else(|| defaults.batch_operational_notes.clone())
1209 .unwrap_or_default(),
1210 batch_regions: rule
1211 .batch_regions
1212 .clone()
1213 .or_else(|| defaults.batch_regions.clone())
1214 .unwrap_or_default(),
1215 tool_approval_policy: rule.tool_approval_policy.clone(),
1216 max_tools: rule.max_tools,
1217 prompt_caching,
1218 prompt_cache_ttls: if prompt_caching {
1219 rule.prompt_cache_ttls
1220 .clone()
1221 .or_else(|| defaults.prompt_cache_ttls.clone())
1222 .unwrap_or_default()
1223 } else {
1224 Vec::new()
1225 },
1226 prompt_cache_min_prefix_tokens: if prompt_caching {
1227 rule.prompt_cache_min_prefix_tokens
1228 .or(defaults.prompt_cache_min_prefix_tokens)
1229 } else {
1230 None
1231 },
1232 cache_breakpoint_style: rule
1233 .cache_breakpoint_style
1234 .clone()
1235 .unwrap_or_else(|| "none".to_string()),
1236 vision: rule_vision(rule),
1237 audio: rule.audio.unwrap_or(false),
1238 pdf: rule.pdf.unwrap_or(false),
1239 video: rule.video.unwrap_or(false),
1240 files_api_supported: rule
1241 .files_api_supported
1242 .or(defaults.files_api_supported)
1243 .unwrap_or(false),
1244 file_upload_wire_format: rule
1245 .file_upload_wire_format
1246 .clone()
1247 .or_else(|| defaults.file_upload_wire_format.clone()),
1248 structured_output: rule_structured_output(rule),
1249 json_schema: rule_structured_output(rule),
1250 prefers_xml_scaffolding: rule.prefers_xml_scaffolding.unwrap_or(false),
1251 reserved_tool_call_token: rule.reserved_tool_call_token.unwrap_or(false),
1252 prefers_markdown_scaffolding: rule.prefers_markdown_scaffolding.unwrap_or(false),
1253 structured_output_mode: rule_structured_output_mode(rule),
1254 supports_assistant_prefill: rule.supports_assistant_prefill.unwrap_or(false),
1255 prefers_role_developer: rule.prefers_role_developer.unwrap_or(false),
1256 prefers_xml_tools: rule.prefers_xml_tools.unwrap_or(false),
1257 thinking_block_style,
1258 emits_inline_reasoning,
1259 thinking_modes,
1260 interleaved_thinking_supported: rule.interleaved_thinking_supported.unwrap_or(false),
1261 anthropic_beta_features: rule.anthropic_beta_features.clone().unwrap_or_default(),
1262 vision_supported: rule.vision_supported.unwrap_or(false),
1263 image_url_input_supported: rule
1264 .image_url_input_supported
1265 .or(defaults.image_url_input_supported)
1266 .unwrap_or(true),
1267 preserve_thinking: rule.preserve_thinking.unwrap_or(false),
1268 reasoning_history_wire_field: rule.reasoning_history_wire_field,
1269 server_parser: rule
1270 .server_parser
1271 .clone()
1272 .unwrap_or_else(|| "none".to_string()),
1273 honors_chat_template_kwargs: rule.honors_chat_template_kwargs.unwrap_or(false),
1274 chat_template_options_field: rule.chat_template_options_field.clone(),
1275 requires_completion_tokens: rule.requires_completion_tokens.unwrap_or(false),
1276 chat_completions_unsupported: rule.chat_completions_unsupported.unwrap_or(false),
1277 reasoning_tools_require_responses: rule.reasoning_tools_require_responses.unwrap_or(false),
1278 requires_streaming: rule.requires_streaming.unwrap_or(false),
1279 reasoning_effort_supported: rule.reasoning_effort_supported.unwrap_or(false),
1280 reasoning_effort_levels: rule.reasoning_effort_levels.clone().unwrap_or_default(),
1281 reasoning_none_supported: rule.reasoning_none_supported.unwrap_or(false),
1282 max_thinking_budget: rule.max_thinking_budget,
1283 reasoning_disable_supported: rule.reasoning_disable_supported.unwrap_or(true),
1284 reasoning_required_for_tools: rule.reasoning_required_for_tools.unwrap_or(false),
1285 reasoning_text_promotable: rule.reasoning_text_promotable.unwrap_or(false),
1286 reasoning_wire_format: rule
1287 .reasoning_wire_format
1288 .clone()
1289 .or_else(|| defaults.reasoning_wire_format.clone()),
1290 seed_supported: rule
1291 .seed_supported
1292 .or(defaults.seed_supported)
1293 .unwrap_or(true),
1294 top_k_supported: rule
1295 .top_k_supported
1296 .or(defaults.top_k_supported)
1297 .unwrap_or(true),
1298 temperature_supported: rule
1299 .temperature_supported
1300 .or(defaults.temperature_supported)
1301 .unwrap_or(true),
1302 top_p_supported: rule
1303 .top_p_supported
1304 .or(defaults.top_p_supported)
1305 .unwrap_or(true),
1306 frequency_penalty_supported: rule
1307 .frequency_penalty_supported
1308 .or(defaults.frequency_penalty_supported)
1309 .unwrap_or(true),
1310 presence_penalty_supported: rule
1311 .presence_penalty_supported
1312 .or(defaults.presence_penalty_supported)
1313 .unwrap_or(true),
1314 stop_supported: rule
1315 .stop_supported
1316 .or(defaults.stop_supported)
1317 .unwrap_or(true),
1318 allowed_tool_choice_modes: rule.allowed_tool_choice_modes.clone().unwrap_or_default(),
1319 requires_tool_result_adjacency: rule.requires_tool_result_adjacency.unwrap_or(false),
1320 supports_parallel_tool_calls: rule.supports_parallel_tool_calls.unwrap_or(true),
1321 tools_exclude_response_format: rule.tools_exclude_response_format.unwrap_or(false),
1322 recommended_endpoint: rule.recommended_endpoint.clone(),
1323 text_tool_wire_format_supported: rule.text_tool_wire_format_supported.unwrap_or(true),
1324 preferred_tool_format: Some(rule_preferred_tool_format(rule)),
1325 tool_mode_parity: Some(rule_tool_mode_parity(rule)),
1326 tool_mode_parity_notes: rule.tool_mode_parity_notes.clone(),
1327 thinking_disable_directive: rule.thinking_disable_directive.clone(),
1328 auto_reasoning_overrides: rule.auto_reasoning_overrides.clone().unwrap_or_default(),
1329 provider_route_denylist: rule.provider_route_denylist.clone().unwrap_or_default(),
1330 openrouter_provider_order: rule.openrouter_provider_order.clone().unwrap_or_default(),
1331 serving_precision: rule
1332 .serving_precision
1333 .clone()
1334 .unwrap_or_else(|| "unverified".to_string()),
1335 computer_use_style: rule.computer_use_style,
1336 screenshot_scaling: rule.screenshot_scaling,
1337 safety_ack_flow: rule.safety_ack_flow.unwrap_or(false),
1338 system_message_placement: rule.system_message_placement,
1339 }
1340}
1341
1342pub(super) fn rule_preferred_tool_format(rule: &ProviderRule) -> String {
1343 rule.preferred_tool_format.clone().unwrap_or_else(|| {
1349 if rule.native_tools.unwrap_or(false) {
1350 "native".to_string()
1351 } else {
1352 "json".to_string()
1353 }
1354 })
1355}
1356
1357pub(super) fn rule_tool_mode_parity(rule: &ProviderRule) -> String {
1358 rule.tool_mode_parity.clone().unwrap_or_else(|| {
1359 match (
1360 rule.native_tools.unwrap_or(false),
1361 rule.text_tool_wire_format_supported.unwrap_or(true),
1362 ) {
1363 (true, true) => "unknown".to_string(),
1364 (true, false) => "native_only".to_string(),
1365 (false, true) => "text_only".to_string(),
1366 (false, false) => "unsupported".to_string(),
1367 }
1368 })
1369}
1370
1371pub(super) fn rule_structured_output(rule: &ProviderRule) -> Option<String> {
1372 rule.structured_output
1373 .clone()
1374 .or_else(|| rule.json_schema.clone())
1375 .filter(|value| value != "none")
1376}
1377
1378pub(super) fn rule_structured_output_mode(rule: &ProviderRule) -> String {
1379 if let Some(mode) = &rule.structured_output_mode {
1380 return mode.clone();
1381 }
1382 match rule_structured_output(rule).as_deref() {
1383 Some("native") | Some("format_kw") => "native_json".to_string(),
1384 Some("tool_use") => "xml_tagged".to_string(),
1385 _ => "none".to_string(),
1386 }
1387}
1388
1389pub(super) fn rule_thinking_block_style(rule: &ProviderRule) -> String {
1390 rule.thinking_block_style.clone().unwrap_or_else(|| {
1391 if rule.reasoning_effort_supported.unwrap_or(false)
1392 || rule.requires_completion_tokens.unwrap_or(false)
1393 {
1394 "reasoning_summary".to_string()
1395 } else {
1396 "none".to_string()
1397 }
1398 })
1399}
1400
1401pub(crate) fn rule_matches(rule: &ProviderRule, model: &str) -> bool {
1402 let lower = model.to_lowercase();
1403 if !glob_match(&rule.model_match.to_lowercase(), &lower) {
1404 return false;
1405 }
1406 if let Some(version_min) = &rule.version_min {
1407 if version_min.len() != 2 {
1408 return false;
1409 }
1410 let want = (version_min[0], version_min[1]);
1411 let have = match extract_version(model) {
1412 Some(v) => v,
1413 None => return false,
1417 };
1418 if have < want {
1419 return false;
1420 }
1421 }
1422 true
1423}
1424
1425fn extract_version(model: &str) -> Option<(u32, u32)> {
1430 claude_generation(model).or_else(|| gpt_generation(model))
1431}