mermaid_cli/providers/capabilities.rs
1//! Per-provider capability flags.
2//!
3//! Providers advertise what they support via `Capabilities`. The
4//! reducer uses this to decide (e.g.) whether to show the reasoning
5//! slider, whether an MCP tool call shape works, or whether vision
6//! inputs are accepted. Future replacements for models.dev
7//! (static-lookup) or `/api/show` (runtime probe) plug in here.
8//!
9//! `Capabilities` is a superset of the adapter-layer
10//! `ModelCapabilities`. Adapters translate via `from_legacy` so the
11//! provider layer doesn't duplicate capability logic.
12
13use crate::models::{ModelCapabilities, ReasoningCapability};
14
15/// Capabilities of a model as advertised by its provider adapter.
16#[derive(Debug, Clone)]
17pub struct Capabilities {
18 /// Native tool/function calling supported.
19 pub supports_tools: bool,
20 /// Vision (image inputs on user messages) supported.
21 pub supports_vision: bool,
22 /// Reasoning controls exposed — binary, enumerated, or numeric.
23 pub supports_reasoning: ReasoningCapability,
24 /// Maximum context window in tokens, if the adapter knows.
25 pub max_context_tokens: Option<usize>,
26 /// Does the provider emit a verifiable "thinking signature" that
27 /// must round-trip on the next request (Anthropic's extended
28 /// thinking)? When true, the reducer preserves the signature on
29 /// the assistant message via `thinking_signature`.
30 pub emits_thinking_signature: bool,
31}
32
33impl Capabilities {
34 /// Construct from the adapter-layer `ModelCapabilities`. Used by
35 /// provider wrappers so we don't duplicate adapter-side capability
36 /// logic.
37 pub fn from_legacy(caps: &ModelCapabilities) -> Self {
38 Self {
39 supports_tools: caps.supports_tools,
40 supports_vision: caps.supports_vision,
41 supports_reasoning: caps.supports_reasoning.clone(),
42 max_context_tokens: caps.max_context_tokens,
43 // Only Anthropic emits signatures; we can't tell from
44 // `ModelCapabilities` alone. Adapters set this explicitly.
45 emits_thinking_signature: false,
46 }
47 }
48
49 /// Builder: mark that this provider round-trips thinking signatures.
50 pub fn with_thinking_signature(mut self) -> Self {
51 self.emits_thinking_signature = true;
52 self
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn from_legacy_preserves_flags() {
62 let legacy = ModelCapabilities {
63 supports_tools: true,
64 supports_vision: true,
65 supports_reasoning: ReasoningCapability::Binary,
66 max_context_tokens: Some(32_000),
67 };
68 let caps = Capabilities::from_legacy(&legacy);
69 assert!(caps.supports_tools);
70 assert!(caps.supports_vision);
71 assert_eq!(caps.max_context_tokens, Some(32_000));
72 assert!(!caps.emits_thinking_signature);
73 }
74
75 #[test]
76 fn with_thinking_signature_sets_flag() {
77 let legacy = ModelCapabilities::ollama_default();
78 let caps = Capabilities::from_legacy(&legacy).with_thinking_signature();
79 assert!(caps.emits_thinking_signature);
80 }
81}