Skip to main content

harn_vm/llm/capabilities/
tool_format.rs

1//! Tool-format decision: validate and auto-correct a requested `tool_format`
2//! against the route's declared tool-call dialect validity.
3//!
4//! The capability registry declares, per route, which channel actually returns
5//! parseable tool calls. This module is the enforcement seam: it classifies a
6//! requested format into a [`ToolFormatWire`] channel, decides whether that
7//! channel is forbidden for the route, and either passes the request through or
8//! steers it to a working channel with an explanatory [`ToolFormatDecision`].
9
10use super::lookup::lookup;
11use super::model::Capabilities;
12
13/// The wire channel a `tool_format` string flows through. `native` is the
14/// provider's structured `tool_calls` JSON channel; `text` and `json` are
15/// text-channel grammars carried in assistant content. Mirrors
16/// `llm_config::ToolFormatChannel`, kept local so the capability registry
17/// (the single source of truth for tool-call dialect validity) has no
18/// dependency on the resolver.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ToolFormatWire {
21    /// Provider-native JSON tool calling (`tool_format = "native"`).
22    Native,
23    /// A text-channel grammar (`tool_format = "text"` or `"json"`).
24    Text,
25}
26
27impl ToolFormatWire {
28    /// Classify a `tool_format` string. Returns `None` for unknown values so
29    /// callers can reject typos loudly rather than guessing a channel.
30    pub fn classify(tool_format: &str) -> Option<Self> {
31        match tool_format {
32            "native" => Some(Self::Native),
33            "text" | "json" => Some(Self::Text),
34            _ => None,
35        }
36    }
37}
38
39/// Outcome of validating a requested `(provider, model, tool_format)` combo
40/// against the capability registry's tool-call dialect validity model.
41///
42/// This is the FOOTGUN-REMOVAL contract: a harness developer can ask for any
43/// tool_format, and the registry guarantees the resolved format is one that
44/// actually yields parseable tool calls for that route — auto-correcting a
45/// known-broken combo (e.g. a `native` pin on a `native_unreliable` route that
46/// silently drops to unparsed DSML text) and explaining why.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ToolFormatDecision {
49    /// The tool_format that should actually be used on the wire. Equal to the
50    /// requested format when the combo was already valid; otherwise the
51    /// registry's `preferred_tool_format` for the route.
52    pub effective: String,
53    /// Set when the requested format was overridden. Human-readable, names the
54    /// bad combo and the working alternative — surface this to the harness
55    /// developer so vanishing tool calls are never silent.
56    pub correction: Option<String>,
57}
58
59impl ToolFormatDecision {
60    fn accepted(format: String) -> Self {
61        Self {
62            effective: format,
63            correction: None,
64        }
65    }
66}
67
68/// True when a route's `tool_mode_parity` says the native (provider JSON)
69/// channel cannot be trusted to yield parseable tool calls. `unsupported`
70/// (no working channel) is intentionally excluded: there is no better format
71/// to steer to, so the gate leaves such a route alone rather than rewriting to
72/// another broken channel under a misleading "Using X instead" message.
73fn parity_forbids_native(parity: &str) -> bool {
74    matches!(parity, "native_unreliable" | "text_only")
75}
76
77/// True when a route's `tool_mode_parity` says a text-channel grammar cannot be
78/// trusted to yield parseable tool calls. See [`parity_forbids_native`] for why
79/// `unsupported` is excluded.
80fn parity_forbids_text(parity: &str) -> bool {
81    matches!(parity, "text_unreliable" | "native_only")
82}
83
84/// True when the requested wire channel is known not to return parseable tool
85/// calls for a route. The gate auto-corrects only on *positive* evidence of
86/// breakage, never on a "we don't know" default:
87///
88/// - `tool_mode_parity` is an explicit verdict (`parity_forbids_*`).
89/// - `text_tool_wire_format_supported = false` is an explicit declaration that
90///   the text channel does not survive this route (e.g. native-only local
91///   Ollama Qwen3 rows that omit a parity string). It defaults to `true`, so an
92///   unknown route is never wrongly judged text-broken.
93///
94/// `native_tools` is deliberately NOT consulted here: it defaults to `false`
95/// for unknown providers, so treating `!native_tools` as "native is broken"
96/// would wrongly rewrite a custom proxy that does support native tools. The
97/// hard `native` + `!native_tools` capability gate in `extract_llm_options`
98/// already rejects a genuine native-on-non-native mismatch loudly.
99fn channel_forbidden(wire: ToolFormatWire, caps: &Capabilities) -> bool {
100    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
101    match wire {
102        ToolFormatWire::Native => parity_forbids_native(parity),
103        ToolFormatWire::Text => {
104            parity_forbids_text(parity) || !caps.text_tool_wire_format_supported
105        }
106    }
107}
108
109/// Validate (and, where the registry knows better, auto-correct) a requested
110/// `tool_format` for a `(provider, model)` route.
111///
112/// This is the single enforcement seam for tool-call dialect validity. The
113/// capability registry already declares, per route, which channel actually
114/// returns parseable tool calls (`tool_mode_parity`) and which format to use
115/// (`preferred_tool_format`). Before this function those fields were advisory
116/// metadata that any alias pin or explicit `--tool-format` flag could silently
117/// override — the footgun behind the DeepSeek V3.2 DSML "vanishing tool calls"
118/// dead-abstain. Now any combo whose requested channel is forbidden — by the
119/// route's `tool_mode_parity` verdict OR by an explicit
120/// `text_tool_wire_format_supported = false` declaration — is rewritten to a
121/// working channel (preferring the route's `preferred_tool_format`), with a
122/// `correction` message naming both. Unknown formats, routes with no adverse
123/// signal (`unknown`/`interchangeable`), and routes with no working channel at
124/// all pass through unchanged.
125pub fn validate_tool_format(provider: &str, model: &str, requested: &str) -> ToolFormatDecision {
126    let caps = lookup(provider, model);
127    validate_tool_format_with_caps(provider, model, requested, &caps)
128}
129
130/// `validate_tool_format` against an already-resolved [`Capabilities`], so hot
131/// callers that already hold one avoid a second matrix lookup.
132pub fn validate_tool_format_with_caps(
133    provider: &str,
134    model: &str,
135    requested: &str,
136    caps: &Capabilities,
137) -> ToolFormatDecision {
138    // Unknown / unclassifiable formats are not ours to second-guess — the
139    // exhaustive-match guard elsewhere already rejects typos loudly.
140    let Some(wire) = ToolFormatWire::classify(requested) else {
141        return ToolFormatDecision::accepted(requested.to_string());
142    };
143
144    if !channel_forbidden(wire, caps) {
145        return ToolFormatDecision::accepted(requested.to_string());
146    }
147
148    // The requested channel is known-broken for this route. Pick the opposite
149    // channel as the steer target, preferring the route's declared
150    // `preferred_tool_format` when it lands on a channel that is itself not
151    // forbidden. If BOTH channels are forbidden (a route with no working tool
152    // surface), there is nothing better to offer — pass the request through
153    // unchanged rather than rewrite to an equally-broken format under a
154    // misleading correction message.
155    let opposite = match wire {
156        ToolFormatWire::Native => ToolFormatWire::Text,
157        ToolFormatWire::Text => ToolFormatWire::Native,
158    };
159    if channel_forbidden(opposite, caps) {
160        return ToolFormatDecision::accepted(requested.to_string());
161    }
162    let preferred = caps
163        .preferred_tool_format
164        .clone()
165        .filter(|fmt| ToolFormatWire::classify(fmt) == Some(opposite))
166        .unwrap_or_else(|| match opposite {
167            ToolFormatWire::Native => "native".to_string(),
168            ToolFormatWire::Text => "json".to_string(),
169        });
170
171    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
172    let mut correction = format!(
173        "tool_format `{requested}` is not safe for {provider}/{model} \
174         (tool_mode_parity = `{parity}`): this route does not return parseable \
175         tool calls on the {} channel, so calls would silently vanish. \
176         Using `{preferred}` instead.",
177        match wire {
178            ToolFormatWire::Native => "provider-native",
179            ToolFormatWire::Text => "text",
180        }
181    );
182    if let Some(note) = caps.tool_mode_parity_notes.as_deref() {
183        if !note.is_empty() {
184            correction.push_str(" (");
185            correction.push_str(note);
186            correction.push(')');
187        }
188    }
189
190    ToolFormatDecision {
191        effective: preferred,
192        correction: Some(correction),
193    }
194}
195
196/// FOOTGUN-REMOVAL — fail fast when a `(provider, model)` route has NO viable
197/// tool channel at all: the registry forbids both the provider-native channel
198/// AND every text-channel grammar. `validate_tool_format` deliberately passes
199/// such a route through unchanged (it has no *better* format to steer to and
200/// must not rewrite to an equally-broken one under a misleading "Using X
201/// instead" message); but a tool-bearing call dispatched on a route with no
202/// working channel can only produce a silent empty tool stream. This guard lets
203/// the call seam reject that combo BEFORE dispatch with an actionable message —
204/// naming the bad `(provider, model)` and a suggested alternative provider for
205/// the same model family — instead of billing a noncommittal completion.
206///
207/// Returns `Some(message)` only when both channels are forbidden (e.g. a route
208/// flagged `native_unreliable` whose text channel is also declared unsupported,
209/// or one explicitly pinned `tool_mode_parity = "unsupported"`). Returns `None`
210/// for every route that still has at least one working channel, so it never
211/// fires on the auto-correctable DeepInfra/SambaNova gpt-oss rows (those keep a
212/// working text channel) or on any healthy route. Modeled on the same
213/// `channel_forbidden` machinery `validate_tool_format` uses, so the two stay in
214/// lock-step: the gate auto-corrects when one channel works and fails fast when
215/// neither does.
216pub fn no_viable_tool_channel(provider: &str, model: &str) -> Option<String> {
217    let caps = lookup(provider, model);
218    no_viable_tool_channel_with_caps(provider, model, &caps)
219}
220
221/// `no_viable_tool_channel` against an already-resolved [`Capabilities`], so hot
222/// callers that already hold one avoid a second matrix lookup.
223pub fn no_viable_tool_channel_with_caps(
224    provider: &str,
225    model: &str,
226    caps: &Capabilities,
227) -> Option<String> {
228    let native_forbidden = channel_forbidden(ToolFormatWire::Native, caps);
229    let text_forbidden = channel_forbidden(ToolFormatWire::Text, caps);
230    if !(native_forbidden && text_forbidden) {
231        return None;
232    }
233    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
234    let mut message = format!(
235        "no viable tool-calling channel for {provider}/{model} \
236         (tool_mode_parity = `{parity}`): the registry trusts neither the \
237         provider-native `tool_calls` channel nor a text-channel grammar to \
238         return parseable tool calls on this route, so a tool-bearing call here \
239         can only emit a silent empty tool stream. {}",
240        suggested_alternative_provider_hint(model)
241    );
242    if let Some(note) = caps.tool_mode_parity_notes.as_deref() {
243        if !note.is_empty() {
244            message.push_str(" (");
245            message.push_str(note);
246            message.push(')');
247        }
248    }
249    Some(message)
250}
251
252/// A short, actionable "try this provider instead" hint for a model whose
253/// current route has no viable tool channel. gpt-oss (Harmony) is the canonical
254/// case: its native channel is a footgun on several pay-per-token routes, so
255/// steer callers to the channels Harn has proven clean (Fireworks/DeepInfra/
256/// SambaNova on TEXT, or a native-clean route). Generic for everything else.
257fn suggested_alternative_provider_hint(model: &str) -> String {
258    if model.to_ascii_lowercase().contains("gpt-oss") {
259        "For gpt-oss (Harmony), use a TEXT-channel route (e.g. \
260         `fireworks`/`deepinfra`/`sambanova` gpt-oss, which Harn pins to \
261         `tool_format = \"text\"`) or a native-clean route; the provider-native \
262         Harmony channel drops tool calls into the reasoning channel."
263            .to_string()
264    } else {
265        "Pick a provider whose route for this model has a working native or \
266         text tool channel (see `harn provider catalog matrix`)."
267            .to_string()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::super::lookup::{clear_user_overrides, lookup_with_user_overrides};
274    use super::super::model::CapabilitiesFile;
275    use super::super::BUILTIN_PROVIDERS_TOML;
276    use super::*;
277
278    fn reset() {
279        clear_user_overrides();
280    }
281
282    #[test]
283    fn every_catalogued_alias_tool_format_pin_is_safe_for_route() {
284        // Alias pins are consumed directly by downstream catalogs and CLI
285        // routing. They must not encode a known-broken channel that the
286        // central runtime guard would have to correct later.
287        reset();
288        let catalog = crate::llm_config::parse_config_toml(BUILTIN_PROVIDERS_TOML)
289            .expect("providers.toml must parse at build time");
290        let mut unsafe_pins = Vec::new();
291        for (alias, def) in &catalog.aliases {
292            let Some(tool_format) = def.tool_format.as_deref() else {
293                continue;
294            };
295            let decision = validate_tool_format(&def.provider, &def.id, tool_format);
296            if let Some(correction) = decision.correction.as_deref() {
297                unsafe_pins.push(format!(
298                    "{alias} -> {}:{} pins {tool_format}, would be corrected to {} ({correction})",
299                    def.provider, def.id, decision.effective
300                ));
301            }
302        }
303        assert!(
304            unsafe_pins.is_empty(),
305            "aliases pin unsafe tool_format values:\n- {}",
306            unsafe_pins.join("\n- ")
307        );
308    }
309
310    #[test]
311    fn validate_tool_format_autocorrects_native_pin_on_native_unreliable_route() {
312        reset();
313        // DeepSeek V3.2 on OpenRouter: tool_mode_parity = native_unreliable,
314        // preferred_tool_format = text. A `native` request is the footgun — it
315        // drops to unparsed DSML text and gets rejected. The gate must steer it
316        // to the route's preferred text-channel format and explain why.
317        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "native");
318        assert_eq!(
319            decision.effective, "text",
320            "native must be auto-corrected to the route's preferred text format"
321        );
322        let reason = decision.correction.expect("a correction must be reported");
323        assert!(reason.contains("native"), "names the rejected format");
324        assert!(reason.contains("native_unreliable"), "names the parity");
325        assert!(reason.contains("text"), "names the working alternative");
326    }
327
328    #[test]
329    fn validate_tool_format_passes_through_safe_combos() {
330        reset();
331        // A native-capable route with no adverse parity keeps the requested
332        // native format untouched (no spurious correction).
333        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3-base", "native");
334        assert_eq!(decision.effective, "native");
335        assert!(decision.correction.is_none());
336
337        // The same native_unreliable route is fine when text is requested.
338        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "text");
339        assert_eq!(decision.effective, "text");
340        assert!(decision.correction.is_none());
341
342        // json is also a text-channel grammar and is accepted on a text route.
343        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "json");
344        assert_eq!(decision.effective, "json");
345        assert!(decision.correction.is_none());
346    }
347
348    #[test]
349    fn validate_tool_format_leaves_unknown_routes_and_formats_alone() {
350        reset();
351        // Unknown provider/model has parity = unknown -> no opinion, pass through.
352        let decision = validate_tool_format("my-proxy", "mystery-1", "native");
353        assert_eq!(decision.effective, "native");
354        assert!(decision.correction.is_none());
355
356        // An unclassifiable tool_format string is not ours to rewrite.
357        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "frobnicate");
358        assert_eq!(decision.effective, "frobnicate");
359        assert!(decision.correction.is_none());
360    }
361
362    #[test]
363    fn validate_tool_format_steers_off_text_on_native_only_route() {
364        reset();
365        // Synthesize a native_only route via a project override and confirm a
366        // text request is steered to native (the symmetric direction).
367        let overrides: CapabilitiesFile = toml::from_str(
368            "[[provider.acme]]\n\
369             model_match = \"native-only-*\"\n\
370             native_tools = true\n\
371             text_tool_wire_format_supported = false\n\
372             tool_mode_parity = \"native_only\"\n\
373             preferred_tool_format = \"native\"\n",
374        )
375        .expect("override parses");
376        let caps = lookup_with_user_overrides("acme", "native-only-1", Some(&overrides));
377        let decision = validate_tool_format_with_caps("acme", "native-only-1", "text", &caps);
378        assert_eq!(decision.effective, "native");
379        let reason = decision
380            .correction
381            .expect("text on native_only is corrected");
382        assert!(reason.contains("native_only"));
383    }
384
385    #[test]
386    fn validate_tool_format_honors_structural_text_unsupported_bit() {
387        reset();
388        // Real shipping route: ollama/qwen3* declares native_tools = true and
389        // text_tool_wire_format_supported = false with NO tool_mode_parity
390        // string. The gate's contract ("always yields parseable tool calls")
391        // must hold from the structural bit alone — a text/json request is
392        // steered to native, not passed through onto an unsupported channel.
393        let caps = lookup("ollama", "qwen3-coder:30b");
394        assert!(!caps.text_tool_wire_format_supported);
395        for requested in ["text", "json"] {
396            let decision =
397                validate_tool_format_with_caps("ollama", "qwen3-coder:30b", requested, &caps);
398            assert_eq!(
399                decision.effective, "native",
400                "{requested} must be steered to native on a text-unsupported route"
401            );
402            assert!(decision.correction.is_some());
403        }
404        // native is the route's working channel — untouched.
405        let native = validate_tool_format_with_caps("ollama", "qwen3-coder:30b", "native", &caps);
406        assert_eq!(native.effective, "native");
407        assert!(native.correction.is_none());
408    }
409
410    #[test]
411    fn tool_format_resolution_is_serving_stack_aware_for_same_weights() {
412        // The (model x serving-stack) insight: the SAME Qwen3.6 weights resolve
413        // to DIFFERENT working tool-call channels depending on who serves them.
414        // This divergence lives in the capability matrix as data (provider rows),
415        // NOT in alias pins — so an alias refactor must not be able to regress
416        // it. Locking the three live serving stacks here makes that explicit.
417        reset();
418
419        // llama.cpp (:8001) — native is probe-validated and trusted.
420        let llamacpp = validate_tool_format("llamacpp", "qwen3.6-35b-a3b-ud-q4-k-xl", "native");
421        assert_eq!(
422            llamacpp.effective, "native",
423            "llama.cpp serves qwen3.6 native"
424        );
425        assert!(llamacpp.correction.is_none());
426
427        // Ollama (/v1) — the embedded qwen tool-call parser 500s on text-mode
428        // output, so this route is served on the text/json channel: a native
429        // request must be auto-corrected to json (never silently dropped).
430        let ollama = validate_tool_format("ollama", "qwen3.6-35b-a3b", "native");
431        assert_eq!(
432            ollama.effective, "json",
433            "ollama qwen3.6 must steer native -> json (server-side parser 500 leak)"
434        );
435        assert!(
436            ollama.correction.is_some(),
437            "the native->json steer must be explained, not silent"
438        );
439
440        // A native_unreliable cloud route (deepinfra GLM-5) carries the same
441        // serving-stack verdict via tool_mode_parity + empirical notes, and is
442        // likewise steered off native.
443        let glm = validate_tool_format("deepinfra", "deepinfra/glm-5.2", "native");
444        assert_eq!(glm.effective, "json");
445        assert!(glm.correction.is_some());
446    }
447
448    #[test]
449    fn validate_tool_format_passes_through_when_no_channel_works() {
450        reset();
451        // A route with no working tool surface — text_only parity forbids the
452        // native channel, and text_tool_wire_format_supported = false forbids
453        // the text channel — so BOTH channels are forbidden. The gate has
454        // nothing better to steer to; it must NOT rewrite to an equally broken
455        // format under a misleading correction. Pass through unchanged.
456        let overrides: CapabilitiesFile = toml::from_str(
457            "[[provider.acme]]\n\
458             model_match = \"no-tools-*\"\n\
459             native_tools = false\n\
460             tool_mode_parity = \"text_only\"\n\
461             text_tool_wire_format_supported = false\n",
462        )
463        .expect("override parses");
464        let caps = lookup_with_user_overrides("acme", "no-tools-1", Some(&overrides));
465        for requested in ["native", "text", "json"] {
466            let decision = validate_tool_format_with_caps("acme", "no-tools-1", requested, &caps);
467            assert_eq!(
468                decision.effective, requested,
469                "{requested} passes through unchanged"
470            );
471            assert!(decision.correction.is_none());
472        }
473    }
474
475    /// FOOTGUN-REMOVAL — gpt-oss (Harmony) on the pay-per-token DeepInfra and
476    /// SambaNova routes drops tool calls into the reasoning channel on native, so
477    /// a `native` pin must auto-correct to the route's `text` channel with an
478    /// explanatory correction. The known-good native routes (cerebras gpt-oss,
479    /// sambanova minimax) must stay untouched.
480    #[test]
481    fn validate_tool_format_autocorrects_gpt_oss_native_pin_to_text() {
482        reset();
483        for (provider, model) in [
484            ("deepinfra", "deepinfra/openai/gpt-oss-120b"),
485            ("sambanova", "sambanova/gpt-oss-120b"),
486        ] {
487            let decision = validate_tool_format(provider, model, "native");
488            assert_eq!(
489                decision.effective, "text",
490                "{provider}/{model}: native must auto-correct to text"
491            );
492            let reason = decision
493                .correction
494                .unwrap_or_else(|| panic!("{provider}/{model}: a correction must be reported"));
495            assert!(
496                reason.contains("native_unreliable"),
497                "{provider}/{model}: names the parity"
498            );
499            assert!(
500                reason.contains("text"),
501                "{provider}/{model}: names the working alternative"
502            );
503            // text is already safe and passes through unchanged.
504            let text = validate_tool_format(provider, model, "text");
505            assert_eq!(text.effective, "text");
506            assert!(text.correction.is_none());
507        }
508    }
509
510    /// FOOTGUN-REMOVAL — the GLM-5.x native channel emits `<tool_call>` markup
511    /// instead of provider-native `tool_calls`, so the zai-direct GLM rows pin
512    /// text and a `native` pin must auto-correct, matching the Fireworks/
513    /// DeepInfra/Baseten precedents.
514    #[test]
515    fn validate_tool_format_autocorrects_zai_glm_native_pin_to_text() {
516        reset();
517        for model in ["glm-5.2", "glm-5.1", "glm-5"] {
518            let decision = validate_tool_format("zai", model, "native");
519            assert_eq!(
520                decision.effective, "text",
521                "zai/{model}: native must auto-correct to text"
522            );
523            let reason = decision
524                .correction
525                .unwrap_or_else(|| panic!("zai/{model}: a correction must be reported"));
526            assert!(
527                reason.contains("native_unreliable"),
528                "zai/{model}: names the parity"
529            );
530        }
531    }
532
533    /// The known-good native routes must NOT be touched by the gpt-oss/GLM
534    /// pins above — a native pin stays native with no spurious correction.
535    #[test]
536    fn validate_tool_format_leaves_known_good_native_routes_unchanged() {
537        reset();
538        for (provider, model) in [
539            // cerebras gpt-oss is native-clean (only throttled).
540            ("cerebras", "gpt-oss-120b"),
541            // sambanova deepseek-v3.2 is native and interchangeable; minimax is
542            // native_unreliable upstream and is not a known-good native
543            // exemplar.
544            ("sambanova", "DeepSeek-V3.2"),
545        ] {
546            let decision = validate_tool_format(provider, model, "native");
547            assert_eq!(
548                decision.effective, "native",
549                "{provider}/{model}: known-good native route must stay native"
550            );
551            assert!(
552                decision.correction.is_none(),
553                "{provider}/{model}: no spurious correction"
554            );
555        }
556    }
557
558    /// FOOTGUN-REMOVAL — the first-class no-viable-channel guard fires when BOTH
559    /// channels are forbidden (a route the registry trusts on neither native nor
560    /// text), naming the bad combo and a suggested alternative — never a silent
561    /// empty tool stream.
562    #[test]
563    fn no_viable_tool_channel_guard_fires_only_when_both_channels_forbidden() {
564        reset();
565        // Construct a gpt-oss route with NO working channel: native_unreliable
566        // forbids native, and text_tool_wire_format_supported = false forbids the
567        // text channel too.
568        let overrides: CapabilitiesFile = toml::from_str(
569            "[[provider.acme]]\n\
570             model_match = \"acme/gpt-oss-stub\"\n\
571             native_tools = false\n\
572             tool_mode_parity = \"native_unreliable\"\n\
573             text_tool_wire_format_supported = false\n",
574        )
575        .expect("override parses");
576        let caps = lookup_with_user_overrides("acme", "acme/gpt-oss-stub", Some(&overrides));
577        let message = no_viable_tool_channel_with_caps("acme", "acme/gpt-oss-stub", &caps)
578            .expect("the guard must fire when neither channel works");
579        assert!(
580            message.contains("no viable tool-calling channel"),
581            "names the failure: {message}"
582        );
583        assert!(
584            message.contains("acme/gpt-oss-stub"),
585            "names the bad combo: {message}"
586        );
587        // gpt-oss models get the Harmony-specific text-channel hint.
588        assert!(
589            message.contains("gpt-oss") && message.contains("text"),
590            "suggests an alternative: {message}"
591        );
592
593        // The DeepInfra/SambaNova gpt-oss rows keep a working text channel, so
594        // the guard must NOT fire on them (they auto-correct instead).
595        assert!(
596            no_viable_tool_channel("deepinfra", "deepinfra/openai/gpt-oss-120b").is_none(),
597            "auto-correctable route must not trip the fail-fast guard"
598        );
599        assert!(
600            no_viable_tool_channel("sambanova", "sambanova/gpt-oss-120b").is_none(),
601            "auto-correctable route must not trip the fail-fast guard"
602        );
603        // A healthy native-clean route never trips it.
604        assert!(
605            no_viable_tool_channel("cerebras", "gpt-oss-120b").is_none(),
606            "healthy native route must not trip the guard"
607        );
608        // The generic (non-gpt-oss) no-channel case still fires with a generic
609        // hint.
610        let generic: CapabilitiesFile = toml::from_str(
611            "[[provider.acme]]\n\
612             model_match = \"mystery-1\"\n\
613             native_tools = false\n\
614             tool_mode_parity = \"text_only\"\n\
615             text_tool_wire_format_supported = false\n",
616        )
617        .expect("override parses");
618        let caps = lookup_with_user_overrides("acme", "mystery-1", Some(&generic));
619        let message = no_viable_tool_channel_with_caps("acme", "mystery-1", &caps)
620            .expect("guard fires on the generic no-channel route too");
621        assert!(
622            message.contains("harn provider catalog matrix"),
623            "{message}"
624        );
625    }
626
627    // --- `extends = true` field-wise fall-through ---
628}