behest_core/capabilities.rs
1//! Capability flags advertised by provider implementations.
2//!
3//! Used by the runtime for feature negotiation and routing decisions.
4
5use serde::{Deserialize, Serialize};
6
7use crate::cache::CacheTtl;
8
9/// Static capability advertisement returned by provider implementations.
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ProviderCapabilities {
12 /// Provider can return a complete chat response.
13 pub chat: bool,
14 /// Provider can stream chat events.
15 pub chat_stream: bool,
16 /// Provider accepts tool definitions in chat requests.
17 pub tool_calling: bool,
18 /// Provider can execute or request multiple tool calls concurrently.
19 pub parallel_tool_calls: bool,
20 /// Provider can constrain output with a JSON schema.
21 pub json_schema_output: bool,
22 /// Provider can consume image content.
23 pub vision: bool,
24 /// Provider can produce embeddings.
25 pub embeddings: bool,
26 /// Maximum supported input tokens, when known.
27 pub max_input_tokens: Option<u32>,
28 /// Maximum supported output tokens, when known.
29 pub max_output_tokens: Option<u32>,
30 /// Provider supports prompt caching.
31 ///
32 /// `false` for providers with no caching (e.g. local models without a
33 /// cache layer). `true` for providers that either accept explicit
34 /// cache markers (Anthropic) or perform automatic prefix caching
35 /// (OpenAI, DeepSeek).
36 pub prompt_caching: bool,
37 /// Maximum number of cache breakpoints the provider accepts.
38 ///
39 /// `Some(4)` for Anthropic (which permits up to four `cache_control`
40 /// markers per request). `None` for providers that perform automatic
41 /// caching without an explicit breakpoint concept (OpenAI, DeepSeek).
42 pub max_cache_breakpoints: Option<u8>,
43 /// Cache TTLs the provider supports.
44 ///
45 /// Empty for providers with automatic caching. Non-empty for Anthropic
46 /// (`[FiveMinutes, OneHour]`).
47 pub cache_ttl_options: Vec<CacheTtl>,
48}
49
50impl ProviderCapabilities {
51 /// Returns a capability set for a basic non-streaming chat provider.
52 #[must_use]
53 pub fn chat() -> Self {
54 Self {
55 chat: true,
56 ..Self::empty()
57 }
58 }
59
60 /// Returns a capability set for an embedding-only provider.
61 #[must_use]
62 pub fn embeddings() -> Self {
63 Self {
64 embeddings: true,
65 ..Self::empty()
66 }
67 }
68
69 /// Returns an empty capability set.
70 #[must_use]
71 pub const fn empty() -> Self {
72 Self {
73 chat: false,
74 chat_stream: false,
75 tool_calling: false,
76 parallel_tool_calls: false,
77 json_schema_output: false,
78 vision: false,
79 embeddings: false,
80 max_input_tokens: None,
81 max_output_tokens: None,
82 prompt_caching: false,
83 max_cache_breakpoints: None,
84 cache_ttl_options: Vec::new(),
85 }
86 }
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn default_capabilities_disables_all_flags() {
96 let caps = ProviderCapabilities::default();
97 assert!(!caps.chat);
98 assert!(!caps.chat_stream);
99 assert!(!caps.tool_calling);
100 assert!(!caps.prompt_caching);
101 assert_eq!(caps.max_cache_breakpoints, None);
102 assert!(caps.cache_ttl_options.is_empty());
103 }
104
105 #[test]
106 fn chat_helper_enables_chat_only() {
107 let caps = ProviderCapabilities::chat();
108 assert!(caps.chat);
109 assert!(!caps.embeddings);
110 assert!(!caps.prompt_caching);
111 }
112
113 #[test]
114 fn empty_helper_is_all_false() {
115 let caps = ProviderCapabilities::empty();
116 assert!(!caps.chat);
117 assert!(!caps.embeddings);
118 assert!(!caps.prompt_caching);
119 assert!(caps.cache_ttl_options.is_empty());
120 }
121}