gglib_core/domain/runtime_capabilities.rs
1//! What the *engine* underneath gglib can do — the mirror of
2//! [`GgufCapabilities`](super::gguf::GgufCapabilities).
3//!
4//! gglib has always had rich detection for what a **model** can do: GGUF
5//! metadata drives capability flags, which drive launch flags and request
6//! shaping. It has had none for what **llama-server** can do. The version was
7//! read at install time, printed to the console, and discarded.
8//!
9//! That gap is why compensation accumulates. Every behaviour gglib applies to
10//! work around a llama.cpp limitation — dialect normalization, grammar
11//! origination, reasoning-tag stripping — is hardcoded as unconditionally on,
12//! because there has never been a way to ask *is this still needed?*. Upstream
13//! ships the fix, gglib keeps compensating, and nobody finds out.
14//!
15//! [`RuntimeCapabilities`] closes that loop: probe the binary once, record what
16//! it is, and let a capability be a *question with an answer* rather than an
17//! assumption baked into a call site.
18//!
19//! # Unknown means gglib compensates
20//!
21//! A version string this module cannot parse yields
22//! [`RuntimeCapabilities::unknown`] — no build number, no flags — and every
23//! compensation stays on. This is the same discipline
24//! [`ModelContext::catalog_resolved`] applies to models: an empty capability
25//! set means *nobody knows*, not *the feature is absent*, and the safe
26//! response to not knowing is to keep doing the work ourselves. Deferring to
27//! native behaviour on a runtime we failed to identify would trade a known
28//! cost for an unknown risk.
29//!
30//! # Resolved once, held for the run
31//!
32//! A probe result is taken when a server is launched and held for that
33//! process's lifetime. Nothing re-probes mid-request, and nothing switches
34//! strategy mid-stream: a request that starts under one set of capabilities
35//! finishes under the same set.
36//!
37//! This is deliberate. Hot-swapping parsing or constraint strategy partway
38//! through a response — on the evidence of a residue hit, say — would make
39//! failures depend on *when* within a stream the evidence arrived, which is
40//! precisely the class of bug that cannot be reproduced from a recording. The
41//! observation layer's job is to *log* divergence between what a runtime
42//! claimed and what it delivered. Acting on that log is a deliberate change
43//! to a threshold in this module, made between runs with the evidence in hand
44//! — not an automatic reaction inside one.
45//!
46//! # Adding a capability
47//!
48//! 1. Add a `const MIN_BUILD_*` with a doc comment citing the upstream
49//! release, PR, or issue that establishes the build.
50//! 2. Add the matching [`RuntimeFlags`] bit.
51//! 3. Set it in [`RuntimeCapabilities::from_build`].
52//! 4. Add a test pinning both sides of the threshold.
53//!
54//! Note what step 4 buys: the threshold is the claim, and the test is what
55//! stops it drifting into folklore.
56//!
57//! [`ModelContext::catalog_resolved`]: crate::request_pipeline::ModelContext::catalog_resolved
58
59use bitflags::bitflags;
60use serde::{Deserialize, Serialize};
61
62/// First llama.cpp build whose PEG-native chat parser handles delimited
63/// ("constructed") tool-call dialects — the XML-style envelopes gglib's own
64/// [`DelimitedToolCallParser`] was written for — natively.
65///
66/// Established by the b9656 release, which threaded OpenAI-wrapper leniency
67/// through the PEG-native generator. Builds at or above this one *have* the
68/// machinery.
69///
70/// **Having it is not the same as being able to rely on it.** Upstream issues
71/// filed against exactly this path — a duplicate `</parameter>` dropping a
72/// whole tool call ([#24807]), a thinking model emitting prose before
73/// `<tool_call>` ([#20260]) — are failure modes gglib's parser already
74/// handles. So this flag answers "does the runtime attempt this itself?", not
75/// "should gglib stop attempting it?". The second question is settled by
76/// measurement, and until it is, nothing gates on this flag.
77///
78/// [`DelimitedToolCallParser`]: crate::normalize::parsers::delimited::DelimitedToolCallParser
79/// [#24807]: https://github.com/ggml-org/llama.cpp/issues/24807
80/// [#20260]: https://github.com/ggml-org/llama.cpp/issues/20260
81pub(crate) const MIN_BUILD_PEG_NATIVE_TOOL_CALLS: u32 = 9656;
82
83bitflags! {
84 /// Capabilities of the llama-server binary gglib is running against.
85 ///
86 /// Derived from the build number by [`RuntimeCapabilities::from_build`].
87 /// Empty means *unknown runtime*, never *featureless runtime* — see the
88 /// module docs.
89 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
90 pub struct RuntimeFlags: u32 {
91 /// The runtime parses delimited tool-call dialects natively via its
92 /// PEG-native chat parser. See [`MIN_BUILD_PEG_NATIVE_TOOL_CALLS`].
93 const PEG_NATIVE_TOOL_CALLS = 0b0000_0001;
94 }
95}
96
97/// Smallest build number treated as a real llama.cpp release.
98///
99/// llama.cpp's `CMake` derives `LLAMA_BUILD_NUMBER` from `git describe`. A clone
100/// without release tags fetched — the ordinary state of a source build, and of
101/// gglib's own `.llama/llama.cpp` checkout — cannot derive one and falls back
102/// to `1`. The banner then reads `version: 1 (69bf643)`: a sentinel meaning
103/// *unnumbered*, not a build that predates every threshold in this module.
104///
105/// Taking it literally is worse than not parsing it at all. A source build of
106/// current llama.cpp would be classified as ancient, silently losing every
107/// native capability it actually has, and no amount of upstream progress would
108/// ever change the answer.
109///
110/// Real release numbers have been in the thousands since 2023, so anything
111/// under this floor is an unnumbered build. Its identity is the commit sha,
112/// which [`RuntimeCapabilities::commit`] records instead.
113pub(crate) const MIN_PLAUSIBLE_BUILD: u32 = 1_000;
114
115/// What the llama-server binary underneath gglib is, and what it can do.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct RuntimeCapabilities {
118 /// llama.cpp build number (the `9656` of tag `b9656`), when it could be
119 /// determined.
120 ///
121 /// `None` for a binary whose version output this module does not
122 /// recognise — a source build with a modified banner, a wrapper script, a
123 /// future format — and for an unnumbered source build, whose sentinel `1`
124 /// is rejected by [`MIN_PLAUSIBLE_BUILD`]. Recorded rather than guessed.
125 pub build: Option<u32>,
126
127 /// The commit sha the banner reported, when it carried one.
128 ///
129 /// For a numbered release this is redundant with [`Self::build`]. For an
130 /// unnumbered source build it is the *only* identity available, which is
131 /// why it is captured separately rather than left inside
132 /// [`Self::version_line`]: a stored request record naming a sha can be
133 /// resolved against upstream history later, and one naming `version: 1`
134 /// cannot.
135 pub commit: Option<String>,
136
137 /// The raw version line as the binary reported it.
138 ///
139 /// Kept verbatim even when [`Self::build`] parsed cleanly, because it is
140 /// the only artifact that lets a stored request record be re-interpreted
141 /// after this module learns to read a format it could not read before.
142 pub version_line: String,
143
144 /// Capabilities derived from [`Self::build`].
145 pub flags: RuntimeFlags,
146}
147
148impl RuntimeCapabilities {
149 /// An unidentified runtime: no build, no flags, every compensation on.
150 #[must_use]
151 pub fn unknown(version_line: impl Into<String>) -> Self {
152 let version_line = version_line.into();
153 let commit = parse_commit(&version_line);
154 Self {
155 build: None,
156 commit,
157 version_line,
158 flags: RuntimeFlags::empty(),
159 }
160 }
161
162 /// Derive capabilities from a build number.
163 ///
164 /// The single place a build number becomes a capability set, so a
165 /// threshold is stated once and every caller agrees about it.
166 ///
167 /// A build under [`MIN_PLAUSIBLE_BUILD`] is an unnumbered source build and
168 /// yields [`Self::unknown`] — the conservative answer, and the only honest
169 /// one, since such a binary could be any commit at all.
170 #[must_use]
171 pub fn from_build(build: u32, version_line: impl Into<String>) -> Self {
172 if build < MIN_PLAUSIBLE_BUILD {
173 return Self::unknown(version_line);
174 }
175
176 let mut flags = RuntimeFlags::empty();
177
178 if build >= MIN_BUILD_PEG_NATIVE_TOOL_CALLS {
179 flags |= RuntimeFlags::PEG_NATIVE_TOOL_CALLS;
180 }
181
182 let version_line = version_line.into();
183 let commit = parse_commit(&version_line);
184 Self {
185 build: Some(build),
186 commit,
187 version_line,
188 flags,
189 }
190 }
191
192 /// Parse a llama-server version banner into capabilities, falling back to
193 /// [`Self::unknown`] when no build number can be read.
194 #[must_use]
195 pub fn from_version_output(output: &str) -> Self {
196 let line = version_line(output);
197 parse_build_number(output).map_or_else(
198 || Self::unknown(line.clone()),
199 |build| Self::from_build(build, line.clone()),
200 )
201 }
202
203 /// Whether this runtime has `flag`.
204 ///
205 /// Always `false` for an unidentified runtime, which is what keeps
206 /// "unknown" and "absent" behaving identically at call sites without every
207 /// call site having to remember the distinction.
208 #[must_use]
209 pub const fn has(&self, flag: RuntimeFlags) -> bool {
210 self.flags.contains(flag)
211 }
212
213 /// Whether the runtime was identified at all.
214 #[must_use]
215 pub const fn is_identified(&self) -> bool {
216 self.build.is_some()
217 }
218}
219
220/// Extract the llama.cpp build number from a `--version` banner.
221///
222/// Reads two shapes, in order:
223///
224/// | Shape | Example | Source |
225/// |---|---|---|
226/// | `version: <n>` | `version: 9656 (a1b2c3d)` | llama-server's own banner |
227/// | `b<n>` token | `llama-b10327-bin-linux` | release tag in a path or banner |
228///
229/// Returns `None` rather than a guess when neither appears — see the module
230/// docs on why an unparsed runtime must not look like a featureless one.
231#[must_use]
232pub(crate) fn parse_build_number(output: &str) -> Option<u32> {
233 if let Some(build) = output
234 .split("version:")
235 .skip(1)
236 .find_map(|rest| leading_number(rest.trim_start()))
237 {
238 return Some(build);
239 }
240
241 output
242 .split(|c: char| !c.is_ascii_alphanumeric())
243 .filter_map(|token| token.strip_prefix('b'))
244 .find_map(|rest| {
245 (!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
246 .then(|| rest.parse().ok())
247 .flatten()
248 })
249}
250
251/// Extract the commit sha from a `--version` banner.
252///
253/// llama-server renders it parenthesised after the build number —
254/// `version: 1 (69bf643)`. Read as the first parenthesised run of hex digits
255/// so a banner carrying other parenthesised text does not yield a false sha.
256#[must_use]
257pub(crate) fn parse_commit(output: &str) -> Option<String> {
258 let (_, after) = output.split_once('(')?;
259 let (inner, _) = after.split_once(')')?;
260 let sha = inner.trim();
261
262 (sha.len() >= 7 && sha.chars().all(|c| c.is_ascii_hexdigit())).then(|| sha.to_owned())
263}
264
265/// The leading run of ASCII digits in `s`, parsed.
266fn leading_number(s: &str) -> Option<u32> {
267 let digits: String = s.chars().take_while(char::is_ascii_digit).collect();
268 digits.parse().ok()
269}
270
271/// The most informative single line of a version banner.
272///
273/// llama-server prints the version first and build details after; the first
274/// non-empty line is the one worth keeping. An entirely blank banner is
275/// recorded as `"unknown"` so the field is never an empty string that reads
276/// as "not recorded".
277fn version_line(output: &str) -> String {
278 output
279 .lines()
280 .map(str::trim)
281 .find(|line| !line.is_empty())
282 .unwrap_or("unknown")
283 .to_owned()
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn reads_the_llama_server_banner_shape() {
292 assert_eq!(parse_build_number("version: 9656 (a1b2c3d)"), Some(9656));
293 }
294
295 #[test]
296 fn reads_a_release_tag_shape() {
297 assert_eq!(
298 parse_build_number("llama-b10327-bin-linux-x64"),
299 Some(10327)
300 );
301 }
302
303 /// The banner shape wins: a path containing a stale tag must not override
304 /// the version the binary reports about itself.
305 #[test]
306 fn the_banner_shape_takes_precedence_over_a_tag_token() {
307 let output = "version: 9656 (a1b2c3d)\nbuilt from /opt/llama-b1234-src";
308 assert_eq!(parse_build_number(output), Some(9656));
309 }
310
311 #[test]
312 fn an_unreadable_banner_yields_no_build() {
313 assert_eq!(
314 parse_build_number("some custom fork, no version here"),
315 None
316 );
317 assert_eq!(parse_build_number(""), None);
318 }
319
320 /// A `b` token that is not all digits is not a build tag.
321 #[test]
322 fn a_non_numeric_b_token_is_not_a_build() {
323 assert_eq!(parse_build_number("built with backend=vulkan"), None);
324 }
325
326 /// The case that motivated [`MIN_PLAUSIBLE_BUILD`], observed on a real
327 /// machine: gglib's own `.llama/llama.cpp` checkout, built from source at
328 /// a current commit, reports `version: 1` because no release tags were
329 /// fetched. Taken literally it would read as a pre-historic build.
330 #[test]
331 fn an_unnumbered_source_build_is_unidentified_not_ancient() {
332 let caps = RuntimeCapabilities::from_version_output("version: 1 (69bf643)");
333
334 assert!(
335 !caps.is_identified(),
336 "a source-build sentinel must not read as build 1"
337 );
338 assert_eq!(
339 caps.commit.as_deref(),
340 Some("69bf643"),
341 "the sha is the only identity such a build has"
342 );
343 }
344
345 /// The floor must not reject a real release.
346 #[test]
347 fn a_real_release_build_is_still_identified() {
348 let caps = RuntimeCapabilities::from_version_output("version: 10327 (69bf643)");
349
350 assert_eq!(caps.build, Some(10327));
351 assert!(caps.has(RuntimeFlags::PEG_NATIVE_TOOL_CALLS));
352 assert_eq!(caps.commit.as_deref(), Some("69bf643"));
353 }
354
355 #[test]
356 fn a_banner_without_a_sha_records_no_commit() {
357 assert_eq!(
358 RuntimeCapabilities::from_version_output("version: 10327").commit,
359 None
360 );
361 }
362
363 /// Parenthesised prose is not a sha.
364 #[test]
365 fn non_hex_parenthesised_text_is_not_a_commit() {
366 assert_eq!(parse_commit("version: 10327 (debug build)"), None);
367 }
368
369 /// The load-bearing default: a runtime we cannot identify claims nothing,
370 /// so every compensation stays on.
371 #[test]
372 fn an_unidentified_runtime_claims_no_capabilities() {
373 let caps = RuntimeCapabilities::from_version_output("mystery build");
374
375 assert!(!caps.is_identified());
376 assert!(!caps.has(RuntimeFlags::PEG_NATIVE_TOOL_CALLS));
377 assert_eq!(caps.version_line, "mystery build");
378 }
379
380 #[test]
381 fn a_build_below_the_threshold_lacks_peg_native() {
382 let caps = RuntimeCapabilities::from_build(MIN_BUILD_PEG_NATIVE_TOOL_CALLS - 1, "v");
383 assert!(!caps.has(RuntimeFlags::PEG_NATIVE_TOOL_CALLS));
384 }
385
386 #[test]
387 fn the_threshold_build_itself_has_peg_native() {
388 let caps = RuntimeCapabilities::from_build(MIN_BUILD_PEG_NATIVE_TOOL_CALLS, "v");
389 assert!(caps.has(RuntimeFlags::PEG_NATIVE_TOOL_CALLS));
390 assert!(caps.is_identified());
391 }
392
393 #[test]
394 fn the_raw_version_line_survives_a_successful_parse() {
395 let caps =
396 RuntimeCapabilities::from_version_output("version: 9700 (deadbee)\nbuilt with cc");
397 assert_eq!(caps.build, Some(9700));
398 assert_eq!(caps.version_line, "version: 9700 (deadbee)");
399 }
400
401 #[test]
402 fn a_blank_banner_records_a_placeholder_rather_than_an_empty_line() {
403 assert_eq!(
404 RuntimeCapabilities::from_version_output("\n \n").version_line,
405 "unknown"
406 );
407 }
408}