Skip to main content

ferrin_google/
capabilities.rs

1//! Model capabilities inferred from the model id.
2
3/// Feature flags of a model id.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct ModelCapabilities {
6    /// A `gemini-*` model.
7    pub is_gemini: bool,
8    /// A `gemini-2.5*` model (Gemini API drops `frequencyPenalty` and
9    /// `presencePenalty` for these).
10    pub is_gemini_2_5: bool,
11    /// A `gemma-*` model (no `systemInstruction`).
12    pub is_gemma: bool,
13    /// Gemini 2 tools (`googleSearch`, `urlContext`, `codeExecution`,
14    /// `enterpriseWebSearch`) are accepted.
15    pub supports_gemini2_tools: bool,
16    /// The `fileSearch` tool is accepted.
17    pub supports_file_search: bool,
18    /// Gemini 3 wire features: thinking levels, thought signatures on tool
19    /// calls, mixed function and provider tools, `VALIDATED` tool mode.
20    pub uses_gemini3_features: bool,
21}
22
23fn segment_has_prefix(model_id: &str, prefix: &str) -> bool {
24    model_id.split('/').any(|segment| {
25        segment
26            .to_ascii_lowercase()
27            .strip_prefix(prefix)
28            .is_some_and(|rest| rest.is_empty() || rest.starts_with(['.', '-']))
29    })
30}
31
32fn segment_equals(model_id: &str, names: &[&str]) -> bool {
33    model_id
34        .split('/')
35        .any(|segment| names.contains(&segment.to_ascii_lowercase().as_str()))
36}
37
38fn is_known_pre_gemini2(model_id: &str) -> bool {
39    segment_has_prefix(model_id, "gemini-1")
40        || segment_equals(model_id, &["gemini-pro", "gemini-pro-vision"])
41        || segment_has_prefix(model_id, "gemini-robotics-er-1.5")
42}
43
44/// Infers the capabilities of `model_id`.
45#[must_use]
46pub fn capabilities(model_id: &str) -> ModelCapabilities {
47    let lower = model_id.to_ascii_lowercase();
48    let is_gemini = model_id
49        .split('/')
50        .any(|segment| segment.to_ascii_lowercase().starts_with("gemini-"));
51    let is_gemini_2 = segment_has_prefix(model_id, "gemini-2");
52    let pre_gemini2 = is_known_pre_gemini2(model_id);
53    let uses_gemini3_features = is_gemini && !(pre_gemini2 || is_gemini_2);
54    let is_gemini_2_5 = segment_has_prefix(model_id, "gemini-2.5");
55    ModelCapabilities {
56        is_gemini,
57        is_gemini_2_5,
58        is_gemma: lower.starts_with("gemma-"),
59        supports_gemini2_tools: (is_gemini && !pre_gemini2) || lower.contains("nano-banana"),
60        supports_file_search: is_gemini_2_5 || uses_gemini3_features,
61        uses_gemini3_features,
62    }
63}
64
65/// Maximum output tokens used when scaling a reasoning budget for Gemini 2.5.
66pub const GEMINI_2_5_MAX_OUTPUT_TOKENS: u32 = 65_536;
67
68/// Maximum thinking budget of a Gemini 2.5 model.
69#[must_use]
70pub fn max_thinking_tokens_gemini_2_5(model_id: &str) -> u32 {
71    let lower = model_id.to_ascii_lowercase();
72    if lower.contains("2.5-pro") || lower.contains("gemini-3-pro-image") {
73        32_768
74    } else {
75        24_576
76    }
77}
78
79/// Lowest thinking level a Gemini 3 model accepts: `low` for
80/// `gemini-flash-latest` and for `gemini-X.Y-flash` (not `-lite`) from 3.7
81/// on, otherwise `minimal`.
82#[must_use]
83pub fn minimum_thinking_level_gemini3(model_id: &str) -> &'static str {
84    let name = model_id
85        .rsplit('/')
86        .next()
87        .unwrap_or_default()
88        .to_ascii_lowercase();
89    if name == "gemini-flash-latest" {
90        return "low";
91    }
92    let Some(rest) = name.strip_prefix("gemini-") else {
93        return "minimal";
94    };
95    let Some((version, tail)) = rest.split_once("-flash") else {
96        return "minimal";
97    };
98    let Some((major, minor)) = version.split_once('.') else {
99        return "minimal";
100    };
101    let (Ok(major), Ok(minor)) = (major.parse::<u32>(), minor.parse::<u32>()) else {
102        return "minimal";
103    };
104    let flash_variant = match tail.strip_prefix('-') {
105        None if tail.is_empty() => true,
106        None => false,
107        Some(suffix) => !(suffix == "lite" || suffix.starts_with("lite-")),
108    };
109    if !flash_variant {
110        return "minimal";
111    }
112    if major > 3 || (major == 3 && minor >= 7) {
113        "low"
114    } else {
115        "minimal"
116    }
117}