Skip to main content

agent_engine/runtime/openai/catalog/
google_gemini.rs

1//! Conservative Google Gemini (Code Assist) model catalog.
2//!
3//! Provenance of the wire IDs exposed here is limited to the ones the official
4//! `google-gemini/gemini-cli` reference names as first-class defaults in
5//! `packages/core/src/config/models.ts`:
6//!
7//!   DEFAULT_GEMINI_MODEL       = "gemini-2.5-pro"
8//!   DEFAULT_GEMINI_FLASH_MODEL = "gemini-2.5-flash"
9//!
10//! Preview / experimental IDs (gemini-3-*, gemini-3.1-*, gemini-3.5-flash,
11//! auto-gemini-*) are intentionally NOT surfaced — they are gated behind
12//! experiments in the reference client and may 404 or return a
13//! "not available" error for accounts that don't have the flag flipped.
14//!
15//! Media / embedding / video / voice models are NOT surfaced either: this
16//! runtime is text + tool-call only.
17
18use super::{
19    CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary, ReasoningSupport,
20};
21
22pub const PROVIDER_KEY: &str = "google-gemini";
23pub const PROVIDER_NAME: &str = "Google Gemini (Code Assist)";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct GoogleGeminiModelDescriptor {
27    /// Wire id sent as `request.model` to `streamGenerateContent`.
28    pub id: &'static str,
29    pub label: &'static str,
30    pub context_tokens: Option<u64>,
31    /// Gemini 2.5 models support internal "thinking" — a reasoning-support
32    /// distinction that lets the UI hint at higher latency / cost.
33    pub thinking: bool,
34}
35
36/// Text/tool-capable wire IDs exposed by the official Gemini CLI. Preview and
37/// rollout models are intentionally visible; upstream account policy remains
38/// authoritative and may reject models not enabled for a particular user.
39pub const GOOGLE_GEMINI_TEXT_MODELS: &[GoogleGeminiModelDescriptor] = &[
40    GoogleGeminiModelDescriptor {
41        id: "gemini-3.1-pro-preview",
42        label: "Gemini 3.1 Pro (Preview)",
43        context_tokens: Some(1_048_576),
44        thinking: true,
45    },
46    GoogleGeminiModelDescriptor {
47        id: "gemini-3-pro-preview",
48        label: "Gemini 3 Pro (Preview)",
49        context_tokens: Some(1_048_576),
50        thinking: true,
51    },
52    GoogleGeminiModelDescriptor {
53        id: "gemini-3.5-flash",
54        label: "Gemini 3.5 Flash",
55        context_tokens: Some(1_048_576),
56        thinking: true,
57    },
58    GoogleGeminiModelDescriptor {
59        id: "gemini-3-flash-preview",
60        label: "Gemini 3 Flash (Preview)",
61        context_tokens: Some(1_048_576),
62        thinking: true,
63    },
64    GoogleGeminiModelDescriptor {
65        id: "gemini-3.1-flash-lite",
66        label: "Gemini 3.1 Flash Lite",
67        context_tokens: Some(1_048_576),
68        thinking: true,
69    },
70    GoogleGeminiModelDescriptor {
71        id: "gemini-2.5-pro",
72        label: "Gemini 2.5 Pro",
73        context_tokens: Some(1_048_576),
74        thinking: true,
75    },
76    GoogleGeminiModelDescriptor {
77        id: "gemini-2.5-flash",
78        label: "Gemini 2.5 Flash",
79        context_tokens: Some(1_048_576),
80        thinking: true,
81    },
82];
83
84pub fn google_gemini_model(id: &str) -> Option<&'static GoogleGeminiModelDescriptor> {
85    GOOGLE_GEMINI_TEXT_MODELS.iter().find(|m| m.id == id)
86}
87
88pub fn google_gemini_static_catalog_models() -> Vec<CatalogModel> {
89    GOOGLE_GEMINI_TEXT_MODELS
90        .iter()
91        .map(|d| CatalogModel {
92            provider_key: PROVIDER_KEY.into(),
93            provider_name: PROVIDER_NAME.into(),
94            provider_kind: CatalogProviderKind::Generic {
95                key: PROVIDER_KEY.into(),
96            },
97            id: d.id.into(),
98            label: Some(d.label.into()),
99            context_tokens: d.context_tokens,
100            max_output_tokens: None,
101            input_modalities: vec![Modality::Text],
102            pricing: PricingSummary::default(),
103            reasoning: if d.thinking {
104                ReasoningSupport::GenericOpenAi
105            } else {
106                ReasoningSupport::None
107            },
108            source: CatalogSource::StaticFallback,
109        })
110        .collect()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn catalog_exposes_all_official_reference_text_ids() {
119        assert_eq!(
120            GOOGLE_GEMINI_TEXT_MODELS
121                .iter()
122                .map(|m| m.id)
123                .collect::<Vec<_>>(),
124            vec![
125                "gemini-3.1-pro-preview",
126                "gemini-3-pro-preview",
127                "gemini-3.5-flash",
128                "gemini-3-flash-preview",
129                "gemini-3.1-flash-lite",
130                "gemini-2.5-pro",
131                "gemini-2.5-flash",
132            ]
133        );
134    }
135
136    /// Regression: `gemini-pro-latest` is a public Gemini API alias, NOT a
137    /// Code Assist wire ID. Live `streamGenerateContent` on
138    /// `cloudcode-pa.googleapis.com` returns `404 NOT_FOUND: Requested entity
139    /// was not found.` for it (2026-07-13, project witty-bonito-c3bk6), and
140    /// the official gemini-cli `models.ts` does not list it in
141    /// `VALID_GEMINI_MODELS`. It must never re-enter the trusted catalog.
142    #[test]
143    fn catalog_excludes_public_api_alias_gemini_pro_latest() {
144        assert!(
145            google_gemini_model("gemini-pro-latest").is_none(),
146            "gemini-pro-latest is not a Code Assist wire ID (404 upstream)"
147        );
148        assert!(!google_gemini_static_catalog_models()
149            .iter()
150            .any(|model| model.runtime_id() == "google-gemini/gemini-pro-latest"));
151    }
152
153    #[test]
154    fn catalog_is_text_only_and_excludes_non_chat_models() {
155        let models = google_gemini_static_catalog_models();
156        assert!(models
157            .iter()
158            .all(|m| m.input_modalities == vec![Modality::Text]));
159        for banned in [
160            "auto-gemini-2.5",
161            "auto-gemini-3",
162            "text-embedding-004",
163            "embedding-001",
164            "gemini-2.5-image",
165            "gemini-2.5-video",
166            "gemini-2.5-voice",
167        ] {
168            assert!(
169                !models.iter().any(|m| m.id == banned),
170                "{banned} must not be in the text/tool catalog"
171            );
172        }
173    }
174
175    #[test]
176    fn google_gemini_model_lookup_is_exact_match_only() {
177        assert!(google_gemini_model("gemini-2.5-pro").is_some());
178        assert!(google_gemini_model("gemini-2.5-flash").is_some());
179        // No fuzzy match: 'gemini-2.5' alone is NOT a wire ID.
180        assert!(google_gemini_model("gemini-2.5").is_none());
181        assert!(google_gemini_model("Gemini-2.5-Pro").is_none());
182        assert!(google_gemini_model("").is_none());
183    }
184}