Skip to main content

polyc_llm/
model_info.rs

1//! Bundled model catalog: per-model context-window facts and the longest-prefix
2//! lookup that drives compaction.
3//!
4//! The catalog is a compile-checked Rust table (no JSON file, no runtime
5//! parsing). It is the single home for "how big is this model's window". The
6//! control plane (compaction trigger, via `model_select`) is the consumer today;
7//! it lives in the shared `polyc-llm` crate so a future harness-side reader gets
8//! the same numbers rather than a divergent copy. An unknown slug never refuses —
9//! it falls back to a conservative window with a single `tracing::warn`, taking
10//! a "warn, don't fail" stance.
11
12/// Per-model context facts, looked up by slug.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ModelInfo {
15    /// Maximum total tokens (prompt + generation) the model accepts.
16    pub context_window: u32,
17    /// Whole-percent fraction of `context_window` that is considered usable
18    /// (default 95). usable = `context_window` * pct / 100.
19    pub effective_context_window_percent: u16,
20}
21
22impl ModelInfo {
23    /// Default usable fraction of a model's window — reserved headroom so the
24    /// prompt never runs flush against the hard limit.
25    pub const DEFAULT_EFFECTIVE_PERCENT: u16 = 95;
26
27    /// A catalog entry with the default effective percentage.
28    #[must_use]
29    pub const fn new(context_window: u32) -> Self {
30        Self {
31            context_window,
32            effective_context_window_percent: Self::DEFAULT_EFFECTIVE_PERCENT,
33        }
34    }
35
36    /// floor(`context_window` * `effective_context_window_percent` / 100): the
37    /// usable token budget after reserving headroom.
38    #[must_use]
39    pub const fn effective_window(self) -> u32 {
40        let usable = (self.context_window as u64 * self.effective_context_window_percent as u64)
41            / Self::PERCENT_DENOM;
42        // `usable <= context_window` (the percent is a fraction of 100), so it
43        // always fits a u32; clamp defensively rather than wrap on the cast.
44        // `u32::try_from` is not yet const-stable, so the checked cast is
45        // expressed manually.
46        if usable > u32::MAX as u64 {
47            u32::MAX
48        } else {
49            // SAFETY (value): guarded above to fit in u32, so this never truncates.
50            #[allow(clippy::cast_possible_truncation)]
51            {
52                usable as u32
53            }
54        }
55    }
56
57    /// Whole-percent denominator for [`effective_window`](Self::effective_window).
58    const PERCENT_DENOM: u64 = 100;
59}
60
61/// Conservative fallback window for an unknown slug. `128_000` chosen so an
62/// unknown model compacts early rather than overflowing a real (possibly
63/// smaller) window.
64pub const FALLBACK_CONTEXT_WINDOW: u32 = 128_000;
65
66/// The bundled catalog. Keys are matched by longest prefix (see
67/// [`lookup_model`]), so dated/preview suffixes resolve to their base entry and
68/// more-specific slugs (`gpt-4o`) beat shorter ones (`gpt-4`).
69static CATALOG: &[(&str, ModelInfo)] = &[
70    // Gemini 3+ only — pre-3 (2.5 and earlier) is deprecated and unsupported.
71    // The `gemini-3` catch-all covers 3 / 3.1 / flash / pro / flash-lite (all 1M).
72    ("gemini-3-flash", ModelInfo::new(1_048_576)),
73    ("gemini-3.1-pro", ModelInfo::new(1_048_576)),
74    ("gemini-3-pro", ModelInfo::new(1_048_576)),
75    ("gemini-3", ModelInfo::new(1_048_576)),
76    ("gpt-4o", ModelInfo::new(128_000)),
77    ("gpt-4-turbo", ModelInfo::new(128_000)),
78    ("gpt-4", ModelInfo::new(8_192)),
79    ("llama3.2", ModelInfo::new(131_072)),
80    ("llama3", ModelInfo::new(131_072)),
81];
82
83/// Longest-prefix match.
84///
85/// Iterate the catalog and keep the entry whose key is a prefix of `slug`
86/// (`slug.starts_with(key)`) with the GREATEST `key.len()`. An exact match is
87/// naturally the longest possible prefix. Returns `None` if no key is a prefix.
88#[must_use]
89pub fn lookup_model(slug: &str) -> Option<ModelInfo> {
90    CATALOG
91        .iter()
92        .filter(|(k, _)| slug.starts_with(k))
93        .max_by_key(|(k, _)| k.len())
94        .map(|(_, info)| *info)
95}
96
97/// [`lookup_model`] with the fallback applied plus ONE `tracing::warn` naming
98/// the slug and the fallback window. NEVER refuses.
99#[must_use]
100pub fn lookup_model_or_fallback(slug: &str) -> ModelInfo {
101    lookup_model(slug).unwrap_or_else(|| {
102        tracing::warn!(
103            model = %slug,
104            fallback_context_window = FALLBACK_CONTEXT_WINDOW,
105            "unknown model slug; using conservative fallback context window"
106        );
107        ModelInfo::new(FALLBACK_CONTEXT_WINDOW)
108    })
109}
110
111#[cfg(test)]
112mod tests {
113    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
114
115    use super::*;
116
117    #[test]
118    fn exact_match_resolves() {
119        assert_eq!(lookup_model("gpt-4").unwrap().context_window, 8_192);
120        assert_eq!(lookup_model("gpt-4o").unwrap().context_window, 128_000);
121    }
122
123    #[test]
124    fn longest_prefix_beats_shorter_keys() {
125        // A dated gpt-4o slug must resolve to gpt-4o (128k), NOT gpt-4 (8k).
126        assert_eq!(
127            lookup_model("gpt-4o-2024-08-06").unwrap().context_window,
128            128_000
129        );
130        // gpt-4-turbo beats gpt-4.
131        assert_eq!(
132            lookup_model("gpt-4-turbo-2024").unwrap().context_window,
133            128_000
134        );
135    }
136
137    #[test]
138    fn preview_and_tag_suffixes_resolve_to_base() {
139        assert_eq!(
140            lookup_model("gemini-3.1-pro-preview")
141                .unwrap()
142                .context_window,
143            1_048_576
144        );
145        assert_eq!(
146            lookup_model("gemini-3-flash-preview-09-2025")
147                .unwrap()
148                .context_window,
149            1_048_576
150        );
151        assert_eq!(
152            lookup_model("llama3.2:latest").unwrap().context_window,
153            131_072
154        );
155    }
156
157    #[test]
158    fn unknown_slug_has_no_catalog_entry_but_falls_back() {
159        assert!(lookup_model("foo-model").is_none());
160        assert_eq!(
161            lookup_model_or_fallback("foo-model").context_window,
162            FALLBACK_CONTEXT_WINDOW
163        );
164    }
165
166    #[test]
167    fn effective_window_math() {
168        assert_eq!(ModelInfo::new(1_048_576).effective_window(), 996_147);
169        assert_eq!(ModelInfo::new(128_000).effective_window(), 121_600);
170    }
171}