use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCapabilities {
pub tool_calling: bool,
pub strict_schema: bool,
pub parallel_tool_calls: bool,
pub streaming_tool_calls: bool,
pub prompt_caching: bool,
pub structured_output: bool,
pub max_context_tokens: u32,
}
impl ProviderCapabilities {
pub fn text_only(max_context_tokens: u32) -> Self {
Self {
tool_calling: false,
strict_schema: false,
parallel_tool_calls: false,
streaming_tool_calls: false,
prompt_caching: false,
structured_output: false,
max_context_tokens,
}
}
pub fn satisfies(&self, required: &ProviderCapabilityMask) -> bool {
(!required.tool_calling || self.tool_calling)
&& (!required.strict_schema || self.strict_schema)
&& (!required.parallel_tool_calls || self.parallel_tool_calls)
&& (!required.streaming_tool_calls || self.streaming_tool_calls)
&& (!required.prompt_caching || self.prompt_caching)
&& (!required.structured_output || self.structured_output)
&& self.max_context_tokens >= required.min_context_tokens
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ProviderCapabilityMask {
pub tool_calling: bool,
pub strict_schema: bool,
pub parallel_tool_calls: bool,
pub streaming_tool_calls: bool,
pub prompt_caching: bool,
pub structured_output: bool,
pub min_context_tokens: u32,
}
impl ProviderCapabilityMask {
pub fn tool_loop() -> Self {
Self {
tool_calling: true,
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "missing")]
pub enum CapabilityDegradation {
StrictSchema,
ParallelToolCalls,
StreamingToolCalls,
PromptCaching,
ToolCalling,
ContextWindow { needed: u32, available: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
fn full() -> ProviderCapabilities {
ProviderCapabilities {
tool_calling: true,
strict_schema: true,
parallel_tool_calls: true,
streaming_tool_calls: true,
prompt_caching: true,
structured_output: true,
max_context_tokens: 200_000,
}
}
#[test]
fn a_full_record_satisfies_the_tool_loop_mask() {
assert!(full().satisfies(&ProviderCapabilityMask::tool_loop()));
}
#[test]
fn text_only_fails_the_tool_loop_mask() {
assert!(
!ProviderCapabilities::text_only(8192).satisfies(&ProviderCapabilityMask::tool_loop())
);
}
#[test]
fn context_window_is_a_hard_requirement() {
let mask = ProviderCapabilityMask {
min_context_tokens: 1_000_000,
..ProviderCapabilityMask::default()
};
assert!(!full().satisfies(&mask));
}
}