harn-vm 0.9.8

Async bytecode virtual machine for the Harn programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
//! Tool-format decision: validate and auto-correct a requested `tool_format`
//! against the route's declared tool-call dialect validity.
//!
//! The capability registry declares, per route, which channel actually returns
//! parseable tool calls. This module is the enforcement seam: it classifies a
//! requested format into a [`ToolFormatWire`] channel, decides whether that
//! channel is forbidden for the route, and either passes the request through or
//! steers it to a working channel with an explanatory [`ToolFormatDecision`].

use super::lookup::lookup;
use super::model::Capabilities;

/// The wire channel a `tool_format` string flows through. `native` is the
/// provider's structured `tool_calls` JSON channel; `text` and `json` are
/// text-channel grammars carried in assistant content. Mirrors
/// `llm_config::ToolFormatChannel`, kept local so the capability registry
/// (the single source of truth for tool-call dialect validity) has no
/// dependency on the resolver.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolFormatWire {
    /// Provider-native JSON tool calling (`tool_format = "native"`).
    Native,
    /// A text-channel grammar (`tool_format = "text"` or `"json"`).
    Text,
}

impl ToolFormatWire {
    /// Classify a `tool_format` string. Returns `None` for unknown values so
    /// callers can reject typos loudly rather than guessing a channel.
    pub fn classify(tool_format: &str) -> Option<Self> {
        match tool_format {
            "native" => Some(Self::Native),
            "text" | "json" => Some(Self::Text),
            _ => None,
        }
    }
}

/// Outcome of validating a requested `(provider, model, tool_format)` combo
/// against the capability registry's tool-call dialect validity model.
///
/// This is the FOOTGUN-REMOVAL contract: a harness developer can ask for any
/// tool_format, and the registry guarantees the resolved format is one that
/// actually yields parseable tool calls for that route — auto-correcting a
/// known-broken combo (e.g. a `native` pin on a `native_unreliable` route that
/// silently drops to unparsed DSML text) and explaining why.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolFormatDecision {
    /// The tool_format that should actually be used on the wire. Equal to the
    /// requested format when the combo was already valid; otherwise the
    /// registry's `preferred_tool_format` for the route.
    pub effective: String,
    /// Set when the requested format was overridden. Human-readable, names the
    /// bad combo and the working alternative — surface this to the harness
    /// developer so vanishing tool calls are never silent.
    pub correction: Option<String>,
}

impl ToolFormatDecision {
    fn accepted(format: String) -> Self {
        Self {
            effective: format,
            correction: None,
        }
    }
}

/// True when a route's `tool_mode_parity` says the native (provider JSON)
/// channel cannot be trusted to yield parseable tool calls. `unsupported`
/// (no working channel) is intentionally excluded: there is no better format
/// to steer to, so the gate leaves such a route alone rather than rewriting to
/// another broken channel under a misleading "Using X instead" message.
fn parity_forbids_native(parity: &str) -> bool {
    matches!(parity, "native_unreliable" | "text_only")
}

/// True when a route's `tool_mode_parity` says a text-channel grammar cannot be
/// trusted to yield parseable tool calls. See [`parity_forbids_native`] for why
/// `unsupported` is excluded.
fn parity_forbids_text(parity: &str) -> bool {
    matches!(parity, "text_unreliable" | "native_only")
}

/// True when the requested wire channel is known not to return parseable tool
/// calls for a route. The gate auto-corrects only on *positive* evidence of
/// breakage, never on a "we don't know" default:
///
/// - `tool_mode_parity` is an explicit verdict (`parity_forbids_*`).
/// - `text_tool_wire_format_supported = false` is an explicit declaration that
///   the text channel does not survive this route (e.g. native-only local
///   Ollama Qwen3 rows that omit a parity string). It defaults to `true`, so an
///   unknown route is never wrongly judged text-broken.
///
/// `native_tools` is deliberately NOT consulted here: it defaults to `false`
/// for unknown providers, so treating `!native_tools` as "native is broken"
/// would wrongly rewrite a custom proxy that does support native tools. The
/// hard `native` + `!native_tools` capability gate in `extract_llm_options`
/// already rejects a genuine native-on-non-native mismatch loudly.
fn channel_forbidden(wire: ToolFormatWire, caps: &Capabilities) -> bool {
    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
    match wire {
        ToolFormatWire::Native => parity_forbids_native(parity),
        ToolFormatWire::Text => {
            parity_forbids_text(parity) || !caps.text_tool_wire_format_supported
        }
    }
}

/// Validate (and, where the registry knows better, auto-correct) a requested
/// `tool_format` for a `(provider, model)` route.
///
/// This is the single enforcement seam for tool-call dialect validity. The
/// capability registry already declares, per route, which channel actually
/// returns parseable tool calls (`tool_mode_parity`) and which format to use
/// (`preferred_tool_format`). Before this function those fields were advisory
/// metadata that any alias pin or explicit `--tool-format` flag could silently
/// override — the footgun behind the DeepSeek V3.2 DSML "vanishing tool calls"
/// dead-abstain. Now any combo whose requested channel is forbidden — by the
/// route's `tool_mode_parity` verdict OR by an explicit
/// `text_tool_wire_format_supported = false` declaration — is rewritten to a
/// working channel (preferring the route's `preferred_tool_format`), with a
/// `correction` message naming both. Unknown formats, routes with no adverse
/// signal (`unknown`/`interchangeable`), and routes with no working channel at
/// all pass through unchanged.
pub fn validate_tool_format(provider: &str, model: &str, requested: &str) -> ToolFormatDecision {
    let caps = lookup(provider, model);
    validate_tool_format_with_caps(provider, model, requested, &caps)
}

/// `validate_tool_format` against an already-resolved [`Capabilities`], so hot
/// callers that already hold one avoid a second matrix lookup.
pub fn validate_tool_format_with_caps(
    provider: &str,
    model: &str,
    requested: &str,
    caps: &Capabilities,
) -> ToolFormatDecision {
    // Unknown / unclassifiable formats are not ours to second-guess — the
    // exhaustive-match guard elsewhere already rejects typos loudly.
    let Some(wire) = ToolFormatWire::classify(requested) else {
        return ToolFormatDecision::accepted(requested.to_string());
    };

    if !channel_forbidden(wire, caps) {
        return ToolFormatDecision::accepted(requested.to_string());
    }

    // The requested channel is known-broken for this route. Pick the opposite
    // channel as the steer target, preferring the route's declared
    // `preferred_tool_format` when it lands on a channel that is itself not
    // forbidden. If BOTH channels are forbidden (a route with no working tool
    // surface), there is nothing better to offer — pass the request through
    // unchanged rather than rewrite to an equally-broken format under a
    // misleading correction message.
    let opposite = match wire {
        ToolFormatWire::Native => ToolFormatWire::Text,
        ToolFormatWire::Text => ToolFormatWire::Native,
    };
    if channel_forbidden(opposite, caps) {
        return ToolFormatDecision::accepted(requested.to_string());
    }
    let preferred = caps
        .preferred_tool_format
        .clone()
        .filter(|fmt| ToolFormatWire::classify(fmt) == Some(opposite))
        .unwrap_or_else(|| match opposite {
            ToolFormatWire::Native => "native".to_string(),
            ToolFormatWire::Text => "json".to_string(),
        });

    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
    let mut correction = format!(
        "tool_format `{requested}` is not safe for {provider}/{model} \
         (tool_mode_parity = `{parity}`): this route does not return parseable \
         tool calls on the {} channel, so calls would silently vanish. \
         Using `{preferred}` instead.",
        match wire {
            ToolFormatWire::Native => "provider-native",
            ToolFormatWire::Text => "text",
        }
    );
    if let Some(note) = caps.tool_mode_parity_notes.as_deref() {
        if !note.is_empty() {
            correction.push_str(" (");
            correction.push_str(note);
            correction.push(')');
        }
    }

    ToolFormatDecision {
        effective: preferred,
        correction: Some(correction),
    }
}

/// FOOTGUN-REMOVAL — fail fast when a `(provider, model)` route has NO viable
/// tool channel at all: the registry forbids both the provider-native channel
/// AND every text-channel grammar. `validate_tool_format` deliberately passes
/// such a route through unchanged (it has no *better* format to steer to and
/// must not rewrite to an equally-broken one under a misleading "Using X
/// instead" message); but a tool-bearing call dispatched on a route with no
/// working channel can only produce a silent empty tool stream. This guard lets
/// the call seam reject that combo BEFORE dispatch with an actionable message —
/// naming the bad `(provider, model)` and a suggested alternative provider for
/// the same model family — instead of billing a noncommittal completion.
///
/// Returns `Some(message)` only when both channels are forbidden (e.g. a route
/// flagged `native_unreliable` whose text channel is also declared unsupported,
/// or one explicitly pinned `tool_mode_parity = "unsupported"`). Returns `None`
/// for every route that still has at least one working channel, so it never
/// fires on the auto-correctable DeepInfra/SambaNova gpt-oss rows (those keep a
/// working text channel) or on any healthy route. Modeled on the same
/// `channel_forbidden` machinery `validate_tool_format` uses, so the two stay in
/// lock-step: the gate auto-corrects when one channel works and fails fast when
/// neither does.
pub fn no_viable_tool_channel(provider: &str, model: &str) -> Option<String> {
    let caps = lookup(provider, model);
    no_viable_tool_channel_with_caps(provider, model, &caps)
}

/// `no_viable_tool_channel` against an already-resolved [`Capabilities`], so hot
/// callers that already hold one avoid a second matrix lookup.
pub fn no_viable_tool_channel_with_caps(
    provider: &str,
    model: &str,
    caps: &Capabilities,
) -> Option<String> {
    let native_forbidden = channel_forbidden(ToolFormatWire::Native, caps);
    let text_forbidden = channel_forbidden(ToolFormatWire::Text, caps);
    if !(native_forbidden && text_forbidden) {
        return None;
    }
    let parity = caps.tool_mode_parity.as_deref().unwrap_or("unknown");
    let mut message = format!(
        "no viable tool-calling channel for {provider}/{model} \
         (tool_mode_parity = `{parity}`): the registry trusts neither the \
         provider-native `tool_calls` channel nor a text-channel grammar to \
         return parseable tool calls on this route, so a tool-bearing call here \
         can only emit a silent empty tool stream. {}",
        suggested_alternative_provider_hint(model)
    );
    if let Some(note) = caps.tool_mode_parity_notes.as_deref() {
        if !note.is_empty() {
            message.push_str(" (");
            message.push_str(note);
            message.push(')');
        }
    }
    Some(message)
}

/// A short, actionable "try this provider instead" hint for a model whose
/// current route has no viable tool channel. gpt-oss (Harmony) is the canonical
/// case: its native channel is a footgun on several pay-per-token routes, so
/// steer callers to the channels Harn has proven clean (Fireworks/DeepInfra/
/// SambaNova on TEXT, or a native-clean route). Generic for everything else.
fn suggested_alternative_provider_hint(model: &str) -> String {
    if model.to_ascii_lowercase().contains("gpt-oss") {
        "For gpt-oss (Harmony), use a TEXT-channel route (e.g. \
         `fireworks`/`deepinfra`/`sambanova` gpt-oss, which Harn pins to \
         `tool_format = \"text\"`) or a native-clean route; the provider-native \
         Harmony channel drops tool calls into the reasoning channel."
            .to_string()
    } else {
        "Pick a provider whose route for this model has a working native or \
         text tool channel (see `harn provider catalog matrix`)."
            .to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::super::lookup::{clear_user_overrides, lookup_with_user_overrides};
    use super::super::model::CapabilitiesFile;
    use super::super::BUILTIN_PROVIDERS_TOML;
    use super::*;

    fn reset() {
        clear_user_overrides();
    }

    #[test]
    fn every_catalogued_alias_tool_format_pin_is_safe_for_route() {
        // Alias pins are consumed directly by downstream catalogs and CLI
        // routing. They must not encode a known-broken channel that the
        // central runtime guard would have to correct later.
        reset();
        let catalog = crate::llm_config::parse_config_toml(BUILTIN_PROVIDERS_TOML)
            .expect("providers.toml must parse at build time");
        let mut unsafe_pins = Vec::new();
        for (alias, def) in &catalog.aliases {
            let Some(tool_format) = def.tool_format.as_deref() else {
                continue;
            };
            let decision = validate_tool_format(&def.provider, &def.id, tool_format);
            if let Some(correction) = decision.correction.as_deref() {
                unsafe_pins.push(format!(
                    "{alias} -> {}:{} pins {tool_format}, would be corrected to {} ({correction})",
                    def.provider, def.id, decision.effective
                ));
            }
        }
        assert!(
            unsafe_pins.is_empty(),
            "aliases pin unsafe tool_format values:\n- {}",
            unsafe_pins.join("\n- ")
        );
    }

    #[test]
    fn validate_tool_format_autocorrects_native_pin_on_native_unreliable_route() {
        reset();
        // DeepSeek V3.2 on OpenRouter: tool_mode_parity = native_unreliable,
        // preferred_tool_format = text. A `native` request is the footgun — it
        // drops to unparsed DSML text and gets rejected. The gate must steer it
        // to the route's preferred text-channel format and explain why.
        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "native");
        assert_eq!(
            decision.effective, "text",
            "native must be auto-corrected to the route's preferred text format"
        );
        let reason = decision.correction.expect("a correction must be reported");
        assert!(reason.contains("native"), "names the rejected format");
        assert!(reason.contains("native_unreliable"), "names the parity");
        assert!(reason.contains("text"), "names the working alternative");
    }

    #[test]
    fn validate_tool_format_passes_through_safe_combos() {
        reset();
        // A native-capable route with no adverse parity keeps the requested
        // native format untouched (no spurious correction).
        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3-base", "native");
        assert_eq!(decision.effective, "native");
        assert!(decision.correction.is_none());

        // The same native_unreliable route is fine when text is requested.
        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "text");
        assert_eq!(decision.effective, "text");
        assert!(decision.correction.is_none());

        // json is also a text-channel grammar and is accepted on a text route.
        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "json");
        assert_eq!(decision.effective, "json");
        assert!(decision.correction.is_none());
    }

    #[test]
    fn validate_tool_format_leaves_unknown_routes_and_formats_alone() {
        reset();
        // Unknown provider/model has parity = unknown -> no opinion, pass through.
        let decision = validate_tool_format("my-proxy", "mystery-1", "native");
        assert_eq!(decision.effective, "native");
        assert!(decision.correction.is_none());

        // An unclassifiable tool_format string is not ours to rewrite.
        let decision = validate_tool_format("openrouter", "deepseek/deepseek-v3.2", "frobnicate");
        assert_eq!(decision.effective, "frobnicate");
        assert!(decision.correction.is_none());
    }

    #[test]
    fn validate_tool_format_steers_off_text_on_native_only_route() {
        reset();
        // Synthesize a native_only route via a project override and confirm a
        // text request is steered to native (the symmetric direction).
        let overrides: CapabilitiesFile = toml::from_str(
            "[[provider.acme]]\n\
             model_match = \"native-only-*\"\n\
             native_tools = true\n\
             text_tool_wire_format_supported = false\n\
             tool_mode_parity = \"native_only\"\n\
             preferred_tool_format = \"native\"\n",
        )
        .expect("override parses");
        let caps = lookup_with_user_overrides("acme", "native-only-1", Some(&overrides));
        let decision = validate_tool_format_with_caps("acme", "native-only-1", "text", &caps);
        assert_eq!(decision.effective, "native");
        let reason = decision
            .correction
            .expect("text on native_only is corrected");
        assert!(reason.contains("native_only"));
    }

    #[test]
    fn validate_tool_format_honors_structural_text_unsupported_bit() {
        reset();
        // Real shipping route: ollama/qwen3* declares native_tools = true and
        // text_tool_wire_format_supported = false with NO tool_mode_parity
        // string. The gate's contract ("always yields parseable tool calls")
        // must hold from the structural bit alone — a text/json request is
        // steered to native, not passed through onto an unsupported channel.
        let caps = lookup("ollama", "qwen3-coder:30b");
        assert!(!caps.text_tool_wire_format_supported);
        for requested in ["text", "json"] {
            let decision =
                validate_tool_format_with_caps("ollama", "qwen3-coder:30b", requested, &caps);
            assert_eq!(
                decision.effective, "native",
                "{requested} must be steered to native on a text-unsupported route"
            );
            assert!(decision.correction.is_some());
        }
        // native is the route's working channel — untouched.
        let native = validate_tool_format_with_caps("ollama", "qwen3-coder:30b", "native", &caps);
        assert_eq!(native.effective, "native");
        assert!(native.correction.is_none());
    }

    #[test]
    fn tool_format_resolution_is_serving_stack_aware_for_same_weights() {
        // The (model x serving-stack) insight: the SAME Qwen3.6 weights resolve
        // to DIFFERENT working tool-call channels depending on who serves them.
        // This divergence lives in the capability matrix as data (provider rows),
        // NOT in alias pins — so an alias refactor must not be able to regress
        // it. Locking the three live serving stacks here makes that explicit.
        reset();

        // llama.cpp (:8001) — native is probe-validated and trusted.
        let llamacpp = validate_tool_format("llamacpp", "qwen3.6-35b-a3b-ud-q4-k-xl", "native");
        assert_eq!(
            llamacpp.effective, "native",
            "llama.cpp serves qwen3.6 native"
        );
        assert!(llamacpp.correction.is_none());

        // Ollama (/v1) — the embedded qwen tool-call parser 500s on text-mode
        // output, so this route is served on the text/json channel: a native
        // request must be auto-corrected to json (never silently dropped).
        let ollama = validate_tool_format("ollama", "qwen3.6-35b-a3b", "native");
        assert_eq!(
            ollama.effective, "json",
            "ollama qwen3.6 must steer native -> json (server-side parser 500 leak)"
        );
        assert!(
            ollama.correction.is_some(),
            "the native->json steer must be explained, not silent"
        );

        // A native_unreliable cloud route (deepinfra GLM-5) carries the same
        // serving-stack verdict via tool_mode_parity + empirical notes, and is
        // likewise steered off native.
        let glm = validate_tool_format("deepinfra", "deepinfra/glm-5.2", "native");
        assert_eq!(glm.effective, "json");
        assert!(glm.correction.is_some());
    }

    #[test]
    fn validate_tool_format_passes_through_when_no_channel_works() {
        reset();
        // A route with no working tool surface — text_only parity forbids the
        // native channel, and text_tool_wire_format_supported = false forbids
        // the text channel — so BOTH channels are forbidden. The gate has
        // nothing better to steer to; it must NOT rewrite to an equally broken
        // format under a misleading correction. Pass through unchanged.
        let overrides: CapabilitiesFile = toml::from_str(
            "[[provider.acme]]\n\
             model_match = \"no-tools-*\"\n\
             native_tools = false\n\
             tool_mode_parity = \"text_only\"\n\
             text_tool_wire_format_supported = false\n",
        )
        .expect("override parses");
        let caps = lookup_with_user_overrides("acme", "no-tools-1", Some(&overrides));
        for requested in ["native", "text", "json"] {
            let decision = validate_tool_format_with_caps("acme", "no-tools-1", requested, &caps);
            assert_eq!(
                decision.effective, requested,
                "{requested} passes through unchanged"
            );
            assert!(decision.correction.is_none());
        }
    }

    /// FOOTGUN-REMOVAL — gpt-oss (Harmony) on the pay-per-token DeepInfra and
    /// SambaNova routes drops tool calls into the reasoning channel on native, so
    /// a `native` pin must auto-correct to the route's `text` channel with an
    /// explanatory correction. The known-good native routes (cerebras gpt-oss,
    /// sambanova minimax) must stay untouched.
    #[test]
    fn validate_tool_format_autocorrects_gpt_oss_native_pin_to_text() {
        reset();
        for (provider, model) in [
            ("deepinfra", "deepinfra/openai/gpt-oss-120b"),
            ("sambanova", "sambanova/gpt-oss-120b"),
        ] {
            let decision = validate_tool_format(provider, model, "native");
            assert_eq!(
                decision.effective, "text",
                "{provider}/{model}: native must auto-correct to text"
            );
            let reason = decision
                .correction
                .unwrap_or_else(|| panic!("{provider}/{model}: a correction must be reported"));
            assert!(
                reason.contains("native_unreliable"),
                "{provider}/{model}: names the parity"
            );
            assert!(
                reason.contains("text"),
                "{provider}/{model}: names the working alternative"
            );
            // text is already safe and passes through unchanged.
            let text = validate_tool_format(provider, model, "text");
            assert_eq!(text.effective, "text");
            assert!(text.correction.is_none());
        }
    }

    /// FOOTGUN-REMOVAL — the GLM-5.x native channel emits `<tool_call>` markup
    /// instead of provider-native `tool_calls`, so the zai-direct GLM rows pin
    /// text and a `native` pin must auto-correct, matching the Fireworks/
    /// DeepInfra/Baseten precedents.
    #[test]
    fn validate_tool_format_autocorrects_zai_glm_native_pin_to_text() {
        reset();
        for model in ["glm-5.2", "glm-5.1", "glm-5"] {
            let decision = validate_tool_format("zai", model, "native");
            assert_eq!(
                decision.effective, "text",
                "zai/{model}: native must auto-correct to text"
            );
            let reason = decision
                .correction
                .unwrap_or_else(|| panic!("zai/{model}: a correction must be reported"));
            assert!(
                reason.contains("native_unreliable"),
                "zai/{model}: names the parity"
            );
        }
    }

    /// The known-good native routes must NOT be touched by the gpt-oss/GLM
    /// pins above — a native pin stays native with no spurious correction.
    #[test]
    fn validate_tool_format_leaves_known_good_native_routes_unchanged() {
        reset();
        for (provider, model) in [
            // cerebras gpt-oss is native-clean (only throttled).
            ("cerebras", "gpt-oss-120b"),
            // sambanova deepseek-v3.2 is native and interchangeable; minimax is
            // native_unreliable upstream and is not a known-good native
            // exemplar.
            ("sambanova", "DeepSeek-V3.2"),
        ] {
            let decision = validate_tool_format(provider, model, "native");
            assert_eq!(
                decision.effective, "native",
                "{provider}/{model}: known-good native route must stay native"
            );
            assert!(
                decision.correction.is_none(),
                "{provider}/{model}: no spurious correction"
            );
        }
    }

    /// FOOTGUN-REMOVAL — the first-class no-viable-channel guard fires when BOTH
    /// channels are forbidden (a route the registry trusts on neither native nor
    /// text), naming the bad combo and a suggested alternative — never a silent
    /// empty tool stream.
    #[test]
    fn no_viable_tool_channel_guard_fires_only_when_both_channels_forbidden() {
        reset();
        // Construct a gpt-oss route with NO working channel: native_unreliable
        // forbids native, and text_tool_wire_format_supported = false forbids the
        // text channel too.
        let overrides: CapabilitiesFile = toml::from_str(
            "[[provider.acme]]\n\
             model_match = \"acme/gpt-oss-stub\"\n\
             native_tools = false\n\
             tool_mode_parity = \"native_unreliable\"\n\
             text_tool_wire_format_supported = false\n",
        )
        .expect("override parses");
        let caps = lookup_with_user_overrides("acme", "acme/gpt-oss-stub", Some(&overrides));
        let message = no_viable_tool_channel_with_caps("acme", "acme/gpt-oss-stub", &caps)
            .expect("the guard must fire when neither channel works");
        assert!(
            message.contains("no viable tool-calling channel"),
            "names the failure: {message}"
        );
        assert!(
            message.contains("acme/gpt-oss-stub"),
            "names the bad combo: {message}"
        );
        // gpt-oss models get the Harmony-specific text-channel hint.
        assert!(
            message.contains("gpt-oss") && message.contains("text"),
            "suggests an alternative: {message}"
        );

        // The DeepInfra/SambaNova gpt-oss rows keep a working text channel, so
        // the guard must NOT fire on them (they auto-correct instead).
        assert!(
            no_viable_tool_channel("deepinfra", "deepinfra/openai/gpt-oss-120b").is_none(),
            "auto-correctable route must not trip the fail-fast guard"
        );
        assert!(
            no_viable_tool_channel("sambanova", "sambanova/gpt-oss-120b").is_none(),
            "auto-correctable route must not trip the fail-fast guard"
        );
        // A healthy native-clean route never trips it.
        assert!(
            no_viable_tool_channel("cerebras", "gpt-oss-120b").is_none(),
            "healthy native route must not trip the guard"
        );
        // The generic (non-gpt-oss) no-channel case still fires with a generic
        // hint.
        let generic: CapabilitiesFile = toml::from_str(
            "[[provider.acme]]\n\
             model_match = \"mystery-1\"\n\
             native_tools = false\n\
             tool_mode_parity = \"text_only\"\n\
             text_tool_wire_format_supported = false\n",
        )
        .expect("override parses");
        let caps = lookup_with_user_overrides("acme", "mystery-1", Some(&generic));
        let message = no_viable_tool_channel_with_caps("acme", "mystery-1", &caps)
            .expect("guard fires on the generic no-channel route too");
        assert!(
            message.contains("harn provider catalog matrix"),
            "{message}"
        );
    }

    // --- `extends = true` field-wise fall-through ---
}