1use std::collections::{BTreeMap, HashSet};
10
11use serde::Deserialize;
12
13use super::model::{
14 fill_opt, CacheBreakpointStyle, Capabilities, CapabilitiesFile, ComputerUseStyle,
15 LiveEndpointFamily, ProviderDefaults, ReasoningHistoryWireField, ReasoningRoundTripPolicy,
16 ScreenshotScaling, SystemMessagePlacement, ToolModeParitySource, 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<CacheBreakpointStyle>,
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_round_trip: Option<ReasoningRoundTripPolicy>,
273 #[serde(default)]
276 pub reasoning_history_wire_field: Option<ReasoningHistoryWireField>,
277 #[serde(default)]
281 pub server_parser: Option<String>,
282 #[serde(default)]
286 pub honors_chat_template_kwargs: Option<bool>,
287 #[serde(default)]
290 pub chat_template_options_field: Option<String>,
291 #[serde(default)]
294 pub requires_completion_tokens: Option<bool>,
295 #[serde(default)]
302 pub chat_completions_unsupported: Option<bool>,
303 #[serde(default)]
307 pub reasoning_tools_require_responses: Option<bool>,
308 #[serde(default)]
312 pub requires_streaming: Option<bool>,
313 #[serde(default)]
317 pub reasoning_effort_supported: Option<bool>,
318 #[serde(default)]
321 pub reasoning_effort_levels: Option<Vec<String>>,
322 #[serde(default)]
326 pub reasoning_none_supported: Option<bool>,
327 #[serde(default)]
335 pub max_thinking_budget: Option<i64>,
336 #[serde(default)]
339 pub reasoning_disable_supported: Option<bool>,
340 #[serde(default)]
353 pub reasoning_required_for_tools: Option<bool>,
354 #[serde(default)]
358 pub reasoning_text_promotable: Option<bool>,
359 #[serde(default)]
362 pub reasoning_wire_format: Option<String>,
363 #[serde(default)]
364 pub seed_supported: Option<bool>,
365 #[serde(default)]
366 pub top_k_supported: Option<bool>,
367 #[serde(default)]
368 pub temperature_supported: Option<bool>,
369 #[serde(default)]
370 pub top_p_supported: Option<bool>,
371 #[serde(default)]
372 pub frequency_penalty_supported: Option<bool>,
373 #[serde(default)]
374 pub presence_penalty_supported: Option<bool>,
375 #[serde(default)]
378 pub stop_supported: Option<bool>,
379 #[serde(default)]
383 pub allowed_tool_choice_modes: Option<Vec<String>>,
384 #[serde(default)]
387 pub requires_tool_result_adjacency: Option<bool>,
388 #[serde(default)]
394 pub supports_parallel_tool_calls: Option<bool>,
395 #[serde(default)]
399 pub tools_exclude_response_format: Option<bool>,
400 #[serde(default)]
404 pub recommended_endpoint: Option<String>,
405 #[serde(default)]
408 pub text_tool_wire_format_supported: Option<bool>,
409 #[serde(default)]
414 pub preferred_tool_format: Option<String>,
415 #[serde(default)]
420 pub tool_mode_parity: Option<String>,
421 #[serde(default)]
423 pub tool_mode_parity_notes: Option<String>,
424 #[serde(default)]
431 pub thinking_disable_directive: Option<String>,
432 #[serde(default)]
445 pub auto_reasoning_overrides: Option<BTreeMap<String, String>>,
446 #[serde(default)]
457 pub provider_route_denylist: Option<Vec<String>>,
458 #[serde(default)]
476 pub openrouter_provider_order: Option<Vec<String>>,
477 #[serde(default)]
491 pub serving_precision: Option<String>,
492 #[serde(default)]
501 pub computer_use_style: Option<ComputerUseStyle>,
502 #[serde(default)]
506 pub screenshot_scaling: Option<ScreenshotScaling>,
507 #[serde(default)]
512 pub safety_ack_flow: Option<bool>,
513 #[serde(default)]
518 pub system_message_placement: Option<SystemMessagePlacement>,
519}
520
521impl ProviderRule {
522 fn fill_missing_from(&mut self, other: &ProviderRule) {
532 let ProviderRule {
533 model_match: _,
536 version_min: _,
537 extends: _,
538 native_tools,
539 message_wire_format,
540 live_endpoint_family,
541 native_tool_wire_format,
542 defer_loading,
543 tool_search,
544 responses_api,
545 hosted_tools,
546 remote_mcp,
547 conversation_state,
548 compaction,
549 background_mode,
550 batch_api,
551 batch_wire_format,
552 batch_input_mode,
553 batch_discount_percent,
554 batch_turnaround_hours,
555 batch_max_requests,
556 batch_max_input_bytes,
557 batch_result_retention_days,
558 batch_result_ordering,
559 batch_partial_failure,
560 batch_cancellation,
561 batch_security_notes,
562 batch_operational_notes,
563 batch_regions,
564 tool_approval_policy,
565 max_tools,
566 prompt_caching,
567 prompt_cache_ttls,
568 prompt_cache_min_prefix_tokens,
569 cache_breakpoint_style,
570 vision,
571 audio,
572 pdf,
573 video,
574 files_api_supported,
575 file_upload_wire_format,
576 structured_output,
577 json_schema,
578 prefers_xml_scaffolding,
579 reserved_tool_call_token,
580 prefers_markdown_scaffolding,
581 structured_output_mode,
582 supports_assistant_prefill,
583 prefers_role_developer,
584 prefers_xml_tools,
585 thinking_block_style,
586 thinking_modes,
587 interleaved_thinking_supported,
588 anthropic_beta_features,
589 thinking,
590 vision_supported,
591 image_url_input_supported,
592 preserve_thinking,
593 reasoning_round_trip,
594 reasoning_history_wire_field,
595 server_parser,
596 honors_chat_template_kwargs,
597 chat_template_options_field,
598 requires_completion_tokens,
599 chat_completions_unsupported,
600 reasoning_tools_require_responses,
601 requires_streaming,
602 reasoning_effort_supported,
603 reasoning_effort_levels,
604 reasoning_none_supported,
605 max_thinking_budget,
606 reasoning_disable_supported,
607 reasoning_required_for_tools,
608 reasoning_text_promotable,
609 reasoning_wire_format,
610 seed_supported,
611 top_k_supported,
612 temperature_supported,
613 top_p_supported,
614 frequency_penalty_supported,
615 presence_penalty_supported,
616 stop_supported,
617 allowed_tool_choice_modes,
618 requires_tool_result_adjacency,
619 supports_parallel_tool_calls,
620 tools_exclude_response_format,
621 recommended_endpoint,
622 text_tool_wire_format_supported,
623 preferred_tool_format,
624 tool_mode_parity,
625 tool_mode_parity_notes,
626 thinking_disable_directive,
627 auto_reasoning_overrides,
628 provider_route_denylist,
629 openrouter_provider_order,
630 serving_precision,
631 computer_use_style,
632 screenshot_scaling,
633 safety_ack_flow,
634 system_message_placement,
635 } = other;
636 fill_opt(&mut self.native_tools, native_tools);
637 fill_opt(&mut self.message_wire_format, message_wire_format);
638 fill_opt(&mut self.live_endpoint_family, live_endpoint_family);
639 fill_opt(&mut self.native_tool_wire_format, native_tool_wire_format);
640 fill_opt(&mut self.defer_loading, defer_loading);
641 fill_opt(&mut self.tool_search, tool_search);
642 fill_opt(&mut self.responses_api, responses_api);
643 fill_opt(&mut self.hosted_tools, hosted_tools);
644 fill_opt(&mut self.remote_mcp, remote_mcp);
645 fill_opt(&mut self.conversation_state, conversation_state);
646 fill_opt(&mut self.compaction, compaction);
647 fill_opt(&mut self.background_mode, background_mode);
648 fill_opt(&mut self.batch_api, batch_api);
649 fill_opt(&mut self.batch_wire_format, batch_wire_format);
650 fill_opt(&mut self.batch_input_mode, batch_input_mode);
651 fill_opt(&mut self.batch_discount_percent, batch_discount_percent);
652 fill_opt(&mut self.batch_turnaround_hours, batch_turnaround_hours);
653 fill_opt(&mut self.batch_max_requests, batch_max_requests);
654 fill_opt(&mut self.batch_max_input_bytes, batch_max_input_bytes);
655 fill_opt(
656 &mut self.batch_result_retention_days,
657 batch_result_retention_days,
658 );
659 fill_opt(&mut self.batch_result_ordering, batch_result_ordering);
660 fill_opt(&mut self.batch_partial_failure, batch_partial_failure);
661 fill_opt(&mut self.batch_cancellation, batch_cancellation);
662 fill_opt(&mut self.batch_security_notes, batch_security_notes);
663 fill_opt(&mut self.batch_operational_notes, batch_operational_notes);
664 fill_opt(&mut self.batch_regions, batch_regions);
665 fill_opt(&mut self.tool_approval_policy, tool_approval_policy);
666 fill_opt(&mut self.max_tools, max_tools);
667 fill_opt(&mut self.prompt_caching, prompt_caching);
668 fill_opt(&mut self.prompt_cache_ttls, prompt_cache_ttls);
669 fill_opt(
670 &mut self.prompt_cache_min_prefix_tokens,
671 prompt_cache_min_prefix_tokens,
672 );
673 fill_opt(&mut self.cache_breakpoint_style, cache_breakpoint_style);
674 fill_opt(&mut self.audio, audio);
675 fill_opt(&mut self.pdf, pdf);
676 fill_opt(&mut self.video, video);
677 fill_opt(&mut self.files_api_supported, files_api_supported);
678 fill_opt(&mut self.file_upload_wire_format, file_upload_wire_format);
679 fill_opt(&mut self.prefers_xml_scaffolding, prefers_xml_scaffolding);
680 fill_opt(&mut self.reserved_tool_call_token, reserved_tool_call_token);
681 fill_opt(
682 &mut self.prefers_markdown_scaffolding,
683 prefers_markdown_scaffolding,
684 );
685 fill_opt(&mut self.structured_output_mode, structured_output_mode);
686 fill_opt(
687 &mut self.supports_assistant_prefill,
688 supports_assistant_prefill,
689 );
690 fill_opt(&mut self.prefers_role_developer, prefers_role_developer);
691 fill_opt(&mut self.prefers_xml_tools, prefers_xml_tools);
692 fill_opt(&mut self.thinking_block_style, thinking_block_style);
693 fill_opt(
694 &mut self.interleaved_thinking_supported,
695 interleaved_thinking_supported,
696 );
697 fill_opt(&mut self.anthropic_beta_features, anthropic_beta_features);
698 fill_opt(
699 &mut self.image_url_input_supported,
700 image_url_input_supported,
701 );
702 fill_opt(&mut self.preserve_thinking, preserve_thinking);
703 fill_opt(&mut self.reasoning_round_trip, reasoning_round_trip);
704 fill_opt(
705 &mut self.reasoning_history_wire_field,
706 reasoning_history_wire_field,
707 );
708 fill_opt(&mut self.server_parser, server_parser);
709 fill_opt(
710 &mut self.honors_chat_template_kwargs,
711 honors_chat_template_kwargs,
712 );
713 fill_opt(
714 &mut self.chat_template_options_field,
715 chat_template_options_field,
716 );
717 fill_opt(
718 &mut self.requires_completion_tokens,
719 requires_completion_tokens,
720 );
721 fill_opt(
722 &mut self.chat_completions_unsupported,
723 chat_completions_unsupported,
724 );
725 fill_opt(
726 &mut self.reasoning_tools_require_responses,
727 reasoning_tools_require_responses,
728 );
729 fill_opt(&mut self.requires_streaming, requires_streaming);
730 fill_opt(
731 &mut self.reasoning_effort_supported,
732 reasoning_effort_supported,
733 );
734 fill_opt(&mut self.reasoning_effort_levels, reasoning_effort_levels);
735 fill_opt(&mut self.reasoning_none_supported, reasoning_none_supported);
736 fill_opt(&mut self.max_thinking_budget, max_thinking_budget);
737 fill_opt(
738 &mut self.reasoning_disable_supported,
739 reasoning_disable_supported,
740 );
741 fill_opt(
742 &mut self.reasoning_required_for_tools,
743 reasoning_required_for_tools,
744 );
745 fill_opt(
746 &mut self.reasoning_text_promotable,
747 reasoning_text_promotable,
748 );
749 fill_opt(&mut self.reasoning_wire_format, reasoning_wire_format);
750 fill_opt(&mut self.seed_supported, seed_supported);
751 fill_opt(&mut self.top_k_supported, top_k_supported);
752 fill_opt(&mut self.temperature_supported, temperature_supported);
753 fill_opt(&mut self.top_p_supported, top_p_supported);
754 fill_opt(
755 &mut self.frequency_penalty_supported,
756 frequency_penalty_supported,
757 );
758 fill_opt(
759 &mut self.presence_penalty_supported,
760 presence_penalty_supported,
761 );
762 fill_opt(&mut self.stop_supported, stop_supported);
763 fill_opt(
764 &mut self.allowed_tool_choice_modes,
765 allowed_tool_choice_modes,
766 );
767 fill_opt(
768 &mut self.requires_tool_result_adjacency,
769 requires_tool_result_adjacency,
770 );
771 fill_opt(
772 &mut self.supports_parallel_tool_calls,
773 supports_parallel_tool_calls,
774 );
775 fill_opt(
776 &mut self.tools_exclude_response_format,
777 tools_exclude_response_format,
778 );
779 fill_opt(&mut self.recommended_endpoint, recommended_endpoint);
780 fill_opt(
781 &mut self.text_tool_wire_format_supported,
782 text_tool_wire_format_supported,
783 );
784 fill_opt(&mut self.preferred_tool_format, preferred_tool_format);
785 fill_opt(&mut self.tool_mode_parity, tool_mode_parity);
786 fill_opt(&mut self.tool_mode_parity_notes, tool_mode_parity_notes);
787 fill_opt(
788 &mut self.thinking_disable_directive,
789 thinking_disable_directive,
790 );
791 fill_opt(&mut self.auto_reasoning_overrides, auto_reasoning_overrides);
792 fill_opt(&mut self.provider_route_denylist, provider_route_denylist);
793 fill_opt(
794 &mut self.openrouter_provider_order,
795 openrouter_provider_order,
796 );
797 fill_opt(&mut self.serving_precision, serving_precision);
798 fill_opt(&mut self.computer_use_style, computer_use_style);
799 fill_opt(&mut self.screenshot_scaling, screenshot_scaling);
800 fill_opt(&mut self.safety_ack_flow, safety_ack_flow);
801 fill_opt(&mut self.system_message_placement, system_message_placement);
802 if self.structured_output.is_none() && self.json_schema.is_none() {
808 self.structured_output.clone_from(structured_output);
809 self.json_schema.clone_from(json_schema);
810 }
811 if self.thinking_modes.is_none() && self.thinking.is_none() {
812 self.thinking_modes.clone_from(thinking_modes);
813 self.thinking.clone_from(thinking);
814 }
815 if self.vision.is_none() && self.vision_supported.is_none() {
816 self.vision.clone_from(vision);
817 self.vision_supported.clone_from(vision_supported);
818 }
819 }
820}
821
822pub(super) struct MatchedCapabilityRule {
823 pub(super) provider: String,
825 pub(super) rule: ProviderRule,
828 pub(super) matched_patterns: Vec<String>,
831}
832
833#[derive(Default)]
839struct RuleResolution {
840 provider: Option<String>,
842 merged: Option<ProviderRule>,
843 matched_patterns: Vec<String>,
845}
846
847impl RuleResolution {
848 fn absorb(&mut self, layer_provider: &str, rule: &ProviderRule) -> bool {
852 if self.provider.is_none() {
853 self.provider = Some(layer_provider.to_string());
854 }
855 self.matched_patterns.push(rule.model_match.clone());
856 match &mut self.merged {
857 None => self.merged = Some(rule.clone()),
858 Some(merged) => merged.fill_missing_from(rule),
859 }
860 !rule.extends
861 }
862
863 fn into_matched(self) -> Option<MatchedCapabilityRule> {
864 Some(MatchedCapabilityRule {
865 provider: self.provider?,
866 rule: self.merged.expect("merged is set whenever provider is set"),
867 matched_patterns: self.matched_patterns,
868 })
869 }
870}
871
872fn absorb_layer_matches(
877 user: Option<&CapabilitiesFile>,
878 builtin: &CapabilitiesFile,
879 layer_provider: &str,
880 model: &str,
881 resolution: &mut RuleResolution,
882) -> bool {
883 for file in user.into_iter().chain(std::iter::once(builtin)) {
884 if let Some(rules) = file.provider.get(layer_provider) {
885 for rule in rules {
886 if rule_matches(rule, model) && resolution.absorb(layer_provider, rule) {
887 return true;
888 }
889 }
890 }
891 }
892 false
893}
894
895fn resolve_rule_chain(
903 user: Option<&CapabilitiesFile>,
904 builtin: &CapabilitiesFile,
905 provider: &str,
906 model: &str,
907) -> (RuleResolution, ProviderDefaults) {
908 let mut resolution = RuleResolution::default();
909 let mut effective_defaults = ProviderDefaults::default();
910 let mut current = provider.to_string();
911 let mut visited = HashSet::new();
912 while visited.insert(current.clone()) {
913 let layer_defaults = merged_provider_defaults(user, builtin, ¤t);
914 if effective_defaults.has_any_field() {
915 effective_defaults.fill_missing_from(&layer_defaults);
916 } else {
917 effective_defaults.overlay(&layer_defaults);
918 }
919 if absorb_layer_matches(user, builtin, ¤t, model, &mut resolution) {
920 break;
921 }
922 let next = user
923 .and_then(|file| file.provider_family.get(¤t))
924 .or_else(|| builtin.provider_family.get(¤t))
925 .cloned();
926 match next {
927 Some(parent) => current = parent,
928 None => break,
929 }
930 }
931 (resolution, effective_defaults)
932}
933
934pub(super) fn declared_portable_option_support(
940 user: Option<&CapabilitiesFile>,
941 builtin: &CapabilitiesFile,
942 provider: &str,
943 model: &str,
944 option: super::PortableOption,
945) -> (Option<bool>, Option<Vec<String>>) {
946 let model = crate::llm_config::capability_model_id(provider, model);
947 let (resolution, defaults) = resolve_rule_chain(user, builtin, provider, &model);
948 let rule = resolution.merged.as_ref();
949 let supported = match option {
950 super::PortableOption::Temperature => rule
951 .and_then(|rule| rule.temperature_supported)
952 .or(defaults.temperature_supported),
953 super::PortableOption::TopP => rule
954 .and_then(|rule| rule.top_p_supported)
955 .or(defaults.top_p_supported),
956 super::PortableOption::TopK => rule
957 .and_then(|rule| rule.top_k_supported)
958 .or(defaults.top_k_supported),
959 super::PortableOption::Seed => rule
960 .and_then(|rule| rule.seed_supported)
961 .or(defaults.seed_supported),
962 super::PortableOption::FrequencyPenalty => rule
963 .and_then(|rule| rule.frequency_penalty_supported)
964 .or(defaults.frequency_penalty_supported),
965 super::PortableOption::PresencePenalty => rule
966 .and_then(|rule| rule.presence_penalty_supported)
967 .or(defaults.presence_penalty_supported),
968 super::PortableOption::Stop => rule
969 .and_then(|rule| rule.stop_supported)
970 .or(defaults.stop_supported),
971 super::PortableOption::Cache | super::PortableOption::PromptCacheTtl => {
972 rule.and_then(|rule| rule.prompt_caching)
973 }
974 };
975 let supported_values = (option == super::PortableOption::PromptCacheTtl).then(|| {
976 rule.and_then(|rule| rule.prompt_cache_ttls.clone())
977 .or_else(|| defaults.prompt_cache_ttls.clone())
978 .unwrap_or_default()
979 });
980 (supported, supported_values)
981}
982
983pub(super) fn first_matching_rule(
984 user: Option<&CapabilitiesFile>,
985 builtin: &CapabilitiesFile,
986 provider: &str,
987 model: &str,
988) -> Option<MatchedCapabilityRule> {
989 resolve_rule_chain(user, builtin, provider, model)
990 .0
991 .into_matched()
992}
993
994pub(super) fn rule_thinking_modes(rule: &ProviderRule) -> Vec<String> {
995 rule.thinking_modes.clone().unwrap_or_else(|| {
996 if rule.thinking.unwrap_or(false) {
997 vec!["enabled".to_string()]
998 } else {
999 Vec::new()
1000 }
1001 })
1002}
1003
1004pub(super) fn rule_vision(rule: &ProviderRule) -> bool {
1005 rule.vision.or(rule.vision_supported).unwrap_or(false)
1006}
1007
1008pub(super) fn lookup_with(
1009 provider: &str,
1010 model: &str,
1011 builtin: &CapabilitiesFile,
1012 user: Option<&CapabilitiesFile>,
1013) -> Capabilities {
1014 if provider == "mock" {
1025 for family in ["anthropic", "openai", "gemini"] {
1026 let defaults = merged_provider_defaults(user, builtin, family);
1027 let mut resolution = RuleResolution::default();
1028 absorb_layer_matches(user, builtin, family, model, &mut resolution);
1029 if let Some(rule) = resolution.merged.as_ref() {
1030 let mut caps = rule_to_caps(rule, &defaults);
1031 if family == "anthropic" {
1032 caps.native_tool_wire_format = "openai".to_string();
1033 }
1034 return caps;
1035 }
1036 }
1037 return Capabilities::default();
1038 }
1039
1040 let (resolution, effective_defaults) = resolve_rule_chain(user, builtin, provider, model);
1043 if let Some(rule) = resolution.merged.as_ref() {
1044 return rule_to_caps(rule, &effective_defaults);
1045 }
1046 if effective_defaults.has_any_field() {
1047 return defaults_to_caps(&effective_defaults);
1048 }
1049 Capabilities::default()
1050}
1051
1052fn merged_provider_defaults(
1053 user: Option<&CapabilitiesFile>,
1054 builtin: &CapabilitiesFile,
1055 provider: &str,
1056) -> ProviderDefaults {
1057 let mut defaults = builtin
1058 .provider_defaults
1059 .get(provider)
1060 .cloned()
1061 .unwrap_or_default();
1062 if let Some(user_defaults) = user.and_then(|file| file.provider_defaults.get(provider)) {
1063 defaults.overlay(user_defaults);
1064 }
1065 defaults
1066}
1067
1068fn defaults_to_caps(defaults: &ProviderDefaults) -> Capabilities {
1069 let empty = ProviderRule {
1070 model_match: "*".to_string(),
1071 version_min: None,
1072 extends: false,
1073 native_tools: None,
1074 message_wire_format: None,
1075 live_endpoint_family: None,
1076 native_tool_wire_format: None,
1077 defer_loading: None,
1078 tool_search: None,
1079 responses_api: None,
1080 hosted_tools: None,
1081 remote_mcp: None,
1082 conversation_state: None,
1083 compaction: None,
1084 background_mode: None,
1085 tool_approval_policy: None,
1086 batch_api: None,
1087 batch_wire_format: None,
1088 batch_input_mode: None,
1089 batch_discount_percent: None,
1090 batch_turnaround_hours: None,
1091 batch_max_requests: None,
1092 batch_max_input_bytes: None,
1093 batch_result_retention_days: None,
1094 batch_result_ordering: None,
1095 batch_partial_failure: None,
1096 batch_cancellation: None,
1097 batch_security_notes: None,
1098 batch_operational_notes: None,
1099 batch_regions: None,
1100 max_tools: None,
1101 prompt_caching: None,
1102 prompt_cache_ttls: None,
1103 prompt_cache_min_prefix_tokens: None,
1104 cache_breakpoint_style: None,
1105 vision: None,
1106 audio: None,
1107 pdf: None,
1108 video: None,
1109 files_api_supported: None,
1110 file_upload_wire_format: None,
1111 structured_output: None,
1112 prefers_xml_scaffolding: None,
1113 reserved_tool_call_token: None,
1114 prefers_markdown_scaffolding: None,
1115 structured_output_mode: None,
1116 supports_assistant_prefill: None,
1117 prefers_role_developer: None,
1118 prefers_xml_tools: None,
1119 thinking_block_style: None,
1120 json_schema: None,
1121 thinking_modes: None,
1122 interleaved_thinking_supported: None,
1123 anthropic_beta_features: None,
1124 thinking: None,
1125 vision_supported: None,
1126 image_url_input_supported: None,
1127 preserve_thinking: None,
1128 reasoning_round_trip: None,
1129 reasoning_history_wire_field: None,
1130 server_parser: None,
1131 honors_chat_template_kwargs: None,
1132 chat_template_options_field: None,
1133 requires_completion_tokens: None,
1134 chat_completions_unsupported: None,
1135 reasoning_tools_require_responses: None,
1136 requires_streaming: None,
1137 reasoning_effort_supported: None,
1138 reasoning_effort_levels: None,
1139 reasoning_none_supported: None,
1140 max_thinking_budget: None,
1141 reasoning_disable_supported: None,
1142 reasoning_required_for_tools: None,
1143 reasoning_text_promotable: None,
1144 reasoning_wire_format: None,
1145 seed_supported: None,
1146 top_k_supported: None,
1147 temperature_supported: None,
1148 top_p_supported: None,
1149 frequency_penalty_supported: None,
1150 presence_penalty_supported: None,
1151 stop_supported: None,
1152 allowed_tool_choice_modes: None,
1153 requires_tool_result_adjacency: None,
1154 supports_parallel_tool_calls: None,
1155 tools_exclude_response_format: None,
1156 recommended_endpoint: None,
1157 text_tool_wire_format_supported: None,
1158 preferred_tool_format: None,
1159 tool_mode_parity: None,
1160 tool_mode_parity_notes: None,
1161 thinking_disable_directive: None,
1162 auto_reasoning_overrides: None,
1163 provider_route_denylist: None,
1164 openrouter_provider_order: None,
1165 serving_precision: None,
1166 computer_use_style: None,
1167 screenshot_scaling: None,
1168 safety_ack_flow: None,
1169 system_message_placement: None,
1170 };
1171 let mut caps = rule_to_caps(&empty, defaults);
1172 caps.preferred_tool_format = None;
1173 caps.tool_mode_parity = None;
1174 caps
1175}
1176
1177fn rule_to_caps(rule: &ProviderRule, defaults: &ProviderDefaults) -> Capabilities {
1178 let (parity_verdict, parity_source) = rule_tool_mode_parity(rule);
1179 let thinking_modes = rule_thinking_modes(rule);
1180 let thinking_block_style = rule_thinking_block_style(rule);
1181 let prompt_caching = rule.prompt_caching.unwrap_or(false);
1182 let emits_inline_reasoning = thinking_block_style == "inline";
1187 let message_wire_format = WireDialect::from_message_wire_format(
1188 &rule
1189 .message_wire_format
1190 .clone()
1191 .or_else(|| defaults.message_wire_format.clone())
1192 .unwrap_or_else(|| "openai".to_string()),
1193 );
1194 let live_endpoint_family = rule
1200 .live_endpoint_family
1201 .or(defaults.live_endpoint_family)
1202 .or_else(|| {
1203 (message_wire_format == WireDialect::Gemini)
1204 .then_some(LiveEndpointFamily::GeminiGenerateContent)
1205 });
1206 Capabilities {
1207 native_tools: rule.native_tools.unwrap_or(false),
1208 message_wire_format,
1209 live_endpoint_family,
1210 native_tool_wire_format: rule
1211 .native_tool_wire_format
1212 .clone()
1213 .or_else(|| defaults.native_tool_wire_format.clone())
1214 .unwrap_or_else(|| "openai".to_string()),
1215 defer_loading: rule.defer_loading.unwrap_or(false),
1216 tool_search: rule.tool_search.clone().unwrap_or_default(),
1217 responses_api: rule.responses_api.unwrap_or(false),
1218 hosted_tools: rule.hosted_tools.clone().unwrap_or_default(),
1219 remote_mcp: rule.remote_mcp.unwrap_or(false),
1220 conversation_state: rule.conversation_state.unwrap_or(false),
1221 compaction: rule.compaction.unwrap_or(false),
1222 background_mode: rule.background_mode.unwrap_or(false),
1223 batch_api: rule.batch_api.or(defaults.batch_api).unwrap_or(false),
1224 batch_wire_format: rule
1225 .batch_wire_format
1226 .clone()
1227 .or_else(|| defaults.batch_wire_format.clone()),
1228 batch_input_mode: rule
1229 .batch_input_mode
1230 .clone()
1231 .or_else(|| defaults.batch_input_mode.clone()),
1232 batch_discount_percent: rule
1233 .batch_discount_percent
1234 .or(defaults.batch_discount_percent),
1235 batch_turnaround_hours: rule
1236 .batch_turnaround_hours
1237 .or(defaults.batch_turnaround_hours),
1238 batch_max_requests: rule.batch_max_requests.or(defaults.batch_max_requests),
1239 batch_max_input_bytes: rule
1240 .batch_max_input_bytes
1241 .or(defaults.batch_max_input_bytes),
1242 batch_result_retention_days: rule
1243 .batch_result_retention_days
1244 .or(defaults.batch_result_retention_days),
1245 batch_result_ordering: rule
1246 .batch_result_ordering
1247 .clone()
1248 .or_else(|| defaults.batch_result_ordering.clone()),
1249 batch_partial_failure: rule
1250 .batch_partial_failure
1251 .clone()
1252 .or_else(|| defaults.batch_partial_failure.clone()),
1253 batch_cancellation: rule
1254 .batch_cancellation
1255 .clone()
1256 .or_else(|| defaults.batch_cancellation.clone()),
1257 batch_security_notes: rule
1258 .batch_security_notes
1259 .clone()
1260 .or_else(|| defaults.batch_security_notes.clone())
1261 .unwrap_or_default(),
1262 batch_operational_notes: rule
1263 .batch_operational_notes
1264 .clone()
1265 .or_else(|| defaults.batch_operational_notes.clone())
1266 .unwrap_or_default(),
1267 batch_regions: rule
1268 .batch_regions
1269 .clone()
1270 .or_else(|| defaults.batch_regions.clone())
1271 .unwrap_or_default(),
1272 tool_approval_policy: rule.tool_approval_policy.clone(),
1273 max_tools: rule.max_tools,
1274 prompt_caching,
1275 prompt_cache_ttls: if prompt_caching {
1276 rule.prompt_cache_ttls
1277 .clone()
1278 .or_else(|| defaults.prompt_cache_ttls.clone())
1279 .unwrap_or_default()
1280 } else {
1281 Vec::new()
1282 },
1283 prompt_cache_min_prefix_tokens: if prompt_caching {
1284 rule.prompt_cache_min_prefix_tokens
1285 .or(defaults.prompt_cache_min_prefix_tokens)
1286 } else {
1287 None
1288 },
1289 cache_breakpoint_style: rule
1290 .cache_breakpoint_style
1291 .or(defaults.cache_breakpoint_style)
1292 .unwrap_or_default(),
1293 vision: rule_vision(rule),
1294 audio: rule.audio.unwrap_or(false),
1295 pdf: rule.pdf.unwrap_or(false),
1296 video: rule.video.unwrap_or(false),
1297 files_api_supported: rule
1298 .files_api_supported
1299 .or(defaults.files_api_supported)
1300 .unwrap_or(false),
1301 file_upload_wire_format: rule
1302 .file_upload_wire_format
1303 .clone()
1304 .or_else(|| defaults.file_upload_wire_format.clone()),
1305 structured_output: rule_structured_output(rule),
1306 json_schema: rule_structured_output(rule),
1307 prefers_xml_scaffolding: rule.prefers_xml_scaffolding.unwrap_or(false),
1308 reserved_tool_call_token: rule.reserved_tool_call_token.unwrap_or(false),
1309 prefers_markdown_scaffolding: rule.prefers_markdown_scaffolding.unwrap_or(false),
1310 structured_output_mode: rule_structured_output_mode(rule),
1311 supports_assistant_prefill: rule.supports_assistant_prefill.unwrap_or(false),
1312 prefers_role_developer: rule.prefers_role_developer.unwrap_or(false),
1313 prefers_xml_tools: rule.prefers_xml_tools.unwrap_or(false),
1314 thinking_block_style,
1315 emits_inline_reasoning,
1316 thinking_modes,
1317 interleaved_thinking_supported: rule.interleaved_thinking_supported.unwrap_or(false),
1318 anthropic_beta_features: rule.anthropic_beta_features.clone().unwrap_or_default(),
1319 vision_supported: rule.vision_supported.unwrap_or(false),
1320 image_url_input_supported: rule
1321 .image_url_input_supported
1322 .or(defaults.image_url_input_supported)
1323 .unwrap_or(true),
1324 preserve_thinking: rule.preserve_thinking.unwrap_or(false),
1325 reasoning_round_trip: rule
1326 .reasoning_round_trip
1327 .or(defaults.reasoning_round_trip)
1328 .unwrap_or_default(),
1329 reasoning_history_wire_field: rule.reasoning_history_wire_field,
1330 server_parser: rule
1331 .server_parser
1332 .clone()
1333 .unwrap_or_else(|| "none".to_string()),
1334 honors_chat_template_kwargs: rule.honors_chat_template_kwargs.unwrap_or(false),
1335 chat_template_options_field: rule.chat_template_options_field.clone(),
1336 requires_completion_tokens: rule.requires_completion_tokens.unwrap_or(false),
1337 chat_completions_unsupported: rule.chat_completions_unsupported.unwrap_or(false),
1338 reasoning_tools_require_responses: rule.reasoning_tools_require_responses.unwrap_or(false),
1339 requires_streaming: rule.requires_streaming.unwrap_or(false),
1340 reasoning_effort_supported: rule.reasoning_effort_supported.unwrap_or(false),
1341 reasoning_effort_levels: rule.reasoning_effort_levels.clone().unwrap_or_default(),
1342 reasoning_none_supported: rule.reasoning_none_supported.unwrap_or(false),
1343 max_thinking_budget: rule.max_thinking_budget,
1344 reasoning_disable_supported: rule.reasoning_disable_supported.unwrap_or(true),
1345 reasoning_required_for_tools: rule.reasoning_required_for_tools.unwrap_or(false),
1346 reasoning_text_promotable: rule.reasoning_text_promotable.unwrap_or(false),
1347 reasoning_wire_format: rule
1348 .reasoning_wire_format
1349 .clone()
1350 .or_else(|| defaults.reasoning_wire_format.clone()),
1351 seed_supported: rule
1352 .seed_supported
1353 .or(defaults.seed_supported)
1354 .unwrap_or(true),
1355 top_k_supported: rule
1356 .top_k_supported
1357 .or(defaults.top_k_supported)
1358 .unwrap_or(true),
1359 temperature_supported: rule
1360 .temperature_supported
1361 .or(defaults.temperature_supported)
1362 .unwrap_or(true),
1363 top_p_supported: rule
1364 .top_p_supported
1365 .or(defaults.top_p_supported)
1366 .unwrap_or(true),
1367 frequency_penalty_supported: rule
1368 .frequency_penalty_supported
1369 .or(defaults.frequency_penalty_supported)
1370 .unwrap_or(true),
1371 presence_penalty_supported: rule
1372 .presence_penalty_supported
1373 .or(defaults.presence_penalty_supported)
1374 .unwrap_or(true),
1375 stop_supported: rule
1376 .stop_supported
1377 .or(defaults.stop_supported)
1378 .unwrap_or(true),
1379 allowed_tool_choice_modes: rule.allowed_tool_choice_modes.clone().unwrap_or_default(),
1380 requires_tool_result_adjacency: rule.requires_tool_result_adjacency.unwrap_or(false),
1381 supports_parallel_tool_calls: rule.supports_parallel_tool_calls.unwrap_or(true),
1382 tools_exclude_response_format: rule.tools_exclude_response_format.unwrap_or(false),
1383 recommended_endpoint: rule.recommended_endpoint.clone(),
1384 text_tool_wire_format_supported: rule.text_tool_wire_format_supported.unwrap_or(true),
1385 preferred_tool_format: Some(rule_preferred_tool_format(rule)),
1386 tool_mode_parity: Some(parity_verdict),
1387 tool_mode_parity_source: Some(parity_source),
1388 tool_mode_parity_notes: rule.tool_mode_parity_notes.clone(),
1389 thinking_disable_directive: rule.thinking_disable_directive.clone(),
1390 auto_reasoning_overrides: rule.auto_reasoning_overrides.clone().unwrap_or_default(),
1391 provider_route_denylist: rule.provider_route_denylist.clone().unwrap_or_default(),
1392 openrouter_provider_order: rule.openrouter_provider_order.clone().unwrap_or_default(),
1393 serving_precision: rule
1394 .serving_precision
1395 .clone()
1396 .unwrap_or_else(|| "unverified".to_string()),
1397 computer_use_style: rule.computer_use_style,
1398 screenshot_scaling: rule.screenshot_scaling,
1399 safety_ack_flow: rule.safety_ack_flow.unwrap_or(false),
1400 system_message_placement: rule.system_message_placement,
1401 }
1402}
1403
1404pub(super) fn rule_preferred_tool_format(rule: &ProviderRule) -> String {
1405 rule.preferred_tool_format.clone().unwrap_or_else(|| {
1411 if rule.native_tools.unwrap_or(false) {
1412 "native".to_string()
1413 } else {
1414 "json".to_string()
1415 }
1416 })
1417}
1418
1419pub(super) fn rule_tool_mode_parity(rule: &ProviderRule) -> (String, ToolModeParitySource) {
1425 if let Some(declared) = rule.tool_mode_parity.clone() {
1426 return (declared, ToolModeParitySource::Declared);
1427 }
1428 let derived = match (
1429 rule.native_tools.unwrap_or(false),
1430 rule.text_tool_wire_format_supported.unwrap_or(true),
1431 ) {
1432 (true, true) => "unknown",
1433 (true, false) => "native_only",
1434 (false, true) => "text_only",
1435 (false, false) => "unsupported",
1436 };
1437 (derived.to_string(), ToolModeParitySource::Derived)
1438}
1439
1440pub(super) fn rule_structured_output(rule: &ProviderRule) -> Option<String> {
1441 rule.structured_output
1442 .clone()
1443 .or_else(|| rule.json_schema.clone())
1444 .filter(|value| value != "none")
1445}
1446
1447pub(super) fn rule_structured_output_mode(rule: &ProviderRule) -> String {
1448 if let Some(mode) = &rule.structured_output_mode {
1449 return mode.clone();
1450 }
1451 match rule_structured_output(rule).as_deref() {
1452 Some("native") | Some("format_kw") => "native_json".to_string(),
1453 Some("tool_use") => "xml_tagged".to_string(),
1454 _ => "none".to_string(),
1455 }
1456}
1457
1458pub(super) fn rule_thinking_block_style(rule: &ProviderRule) -> String {
1459 rule.thinking_block_style.clone().unwrap_or_else(|| {
1460 if rule.reasoning_effort_supported.unwrap_or(false)
1461 || rule.requires_completion_tokens.unwrap_or(false)
1462 {
1463 "reasoning_summary".to_string()
1464 } else {
1465 "none".to_string()
1466 }
1467 })
1468}
1469
1470pub(crate) fn rule_matches(rule: &ProviderRule, model: &str) -> bool {
1471 let lower = model.to_lowercase();
1472 if !glob_match(&rule.model_match.to_lowercase(), &lower) {
1473 return false;
1474 }
1475 if let Some(version_min) = &rule.version_min {
1476 if version_min.len() != 2 {
1477 return false;
1478 }
1479 let want = (version_min[0], version_min[1]);
1480 let have = match extract_version(model) {
1481 Some(v) => v,
1482 None => return false,
1486 };
1487 if have < want {
1488 return false;
1489 }
1490 }
1491 true
1492}
1493
1494fn extract_version(model: &str) -> Option<(u32, u32)> {
1499 claude_generation(model).or_else(|| gpt_generation(model))
1500}