Skip to main content

agent_engine/runtime/openai/catalog/
muse.rs

1//! Meta Muse model catalog and exact-id reasoning capability.
2//!
3//! Evidence: the official Muse Code CLI (`muse 1.4.6-R6000.1`) reads its
4//! model list from `GET https://api.meta.ai/muse-code/models` (Bearer = Model
5//! API key). The live response (observed 2026-10) is an OpenAI-style list;
6//! each row's capabilities sit under `metadata["muse-code"]`: `is_hidden`,
7//! `limit.{context,output}`, `modalities.{input,output}`, client `options`
8//! (`reasoningEffort` default, `forceReasoning`, ...) and `variants` — a map
9//! of named presets each carrying a `reasoningEffort` drawn from
10//! `none|minimal|low|medium|high|xhigh|max|ultra`. Meta's public Model API
11//! example uses `muse-wonder-1.0` on `POST /v1/responses`.
12//!
13//! The catalog is server-driven, so the static table is deliberately tiny:
14//! only the publicly documented id ships as a seed, and it carries NO
15//! reasoning claim (only the provider default is expressible). Anything else
16//! — more ids, exact effort sets, default effort, limits, image input —
17//! comes from the live catalog via the capability cache.
18//!
19//! Routing follows the registry precedent for server-catalog providers
20//! (e.g. `inferx/<any published id>`): any id of the safe shape routes to
21//! the pinned `api.meta.ai` origin and Meta is authoritative for existence.
22//! Routing must NOT depend on the process-local capability cache — the
23//! daemon, a resumed session or a fresh process would otherwise refuse a
24//! model the picker (another process) had listed. Capability claims stay
25//! evidence-gated: named reasoning levels require the live effort set, and
26//! strict validators (subagent authorization, session drivers) still require
27//! a seed or an exact live row ([`muse_runtime_model`]).
28
29use agent_core::reasoning::ReasoningLevel;
30use serde::Deserialize;
31
32use super::{
33    capability_cache, CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary,
34    ReasoningSupport,
35};
36
37/// Canonical provider key / model prefix (`muse/<id>`).
38pub const MUSE_PROVIDER_KEY: &str = "muse";
39/// Human-readable provider name.
40pub const MUSE_PROVIDER_NAME: &str = "Meta Muse";
41/// Broker path for the live catalog (pinned origin `https://api.meta.ai`).
42pub const MUSE_MODELS_PATH: &str = "/muse-code/models";
43
44/// One statically known Muse model row.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct MuseModelDescriptor {
47    pub id: &'static str,
48    pub label: &'static str,
49}
50
51/// Publicly documented ids only (Meta Model API example request).
52pub const MUSE_TEXT_MODELS: &[MuseModelDescriptor] = &[MuseModelDescriptor {
53    id: "muse-wonder-1.0",
54    label: "Muse Wonder 1.0",
55}];
56
57/// Exact static lookup — never substring-based.
58pub fn muse_static_model(id: &str) -> Option<&'static MuseModelDescriptor> {
59    MUSE_TEXT_MODELS.iter().find(|model| model.id == id)
60}
61
62/// Conservative id shape for live rows: lowercase ascii alnum plus `.-_`,
63/// starting alphanumeric, bounded length. Anything else is dropped.
64fn valid_muse_id(id: &str) -> bool {
65    let bytes = id.as_bytes();
66    !bytes.is_empty()
67        && bytes.len() <= 128
68        && bytes[0].is_ascii_alphanumeric()
69        && bytes.iter().all(|b| {
70            b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'-' | b'_')
71        })
72}
73
74/// Serializes tests that replace the process-global `muse/*` cache rows
75/// (`capability_cache::replace_provider` is provider-wide, so concurrent
76/// tests would clobber each other's live rows).
77#[cfg(test)]
78pub(crate) static TEST_CACHE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
79
80#[cfg(test)]
81pub(crate) fn test_cache_guard() -> std::sync::MutexGuard<'static, ()> {
82    TEST_CACHE_LOCK
83        .lock()
84        .unwrap_or_else(|poisoned| poisoned.into_inner())
85}
86
87/// True when `id` may be routed to the pinned Muse origin: the safe id
88/// shape only. Existence is Meta's decision (a typo surfaces as an upstream
89/// error on the first turn, exactly like other server-catalog providers).
90pub fn muse_routable_id(id: &str) -> bool {
91    valid_muse_id(id)
92}
93
94/// True when `id` is KNOWN: a static seed, or an exact row the live Muse
95/// catalog put in the capability cache this process. Strict validators use
96/// this; routing uses [`muse_routable_id`].
97pub fn muse_runtime_model(id: &str) -> bool {
98    muse_static_model(id).is_some()
99        || capability_cache::get(&format!("{MUSE_PROVIDER_KEY}/{id}"))
100            .is_some_and(|row| row.provider_key == MUSE_PROVIDER_KEY && row.id == id)
101}
102
103/// Exact named effort set for a routable Muse id. `None` = no authoritative
104/// evidence (static seed, or a live row that advertised no variants): only
105/// the provider default (`adaptive`, field omitted) may be sent.
106pub fn muse_supported_levels(id: &str) -> Option<Vec<ReasoningLevel>> {
107    match capability_cache::get(&format!("{MUSE_PROVIDER_KEY}/{id}"))?.reasoning {
108        ReasoningSupport::NamedEffort { supported, .. } if !supported.is_empty() => Some(supported),
109        _ => None,
110    }
111}
112
113/// The live catalog's default effort for `id` (`options.reasoningEffort`,
114/// what the official client sends when the user picks nothing). Only
115/// returned when it is a member of the exact advertised set.
116pub fn muse_default_level(id: &str) -> Option<ReasoningLevel> {
117    match capability_cache::get(&format!("{MUSE_PROVIDER_KEY}/{id}"))?.reasoning {
118        ReasoningSupport::NamedEffort {
119            supported,
120            default_level: Some(level),
121        } if supported.contains(&level) => Some(level),
122        _ => None,
123    }
124}
125
126/// Wire label for a validated level. `Adaptive` (and levels Muse has no name
127/// for) → `None` → the `reasoning` field is omitted (provider default).
128pub fn muse_effort_label(level: ReasoningLevel) -> Option<&'static str> {
129    match level {
130        ReasoningLevel::Off => Some("none"),
131        ReasoningLevel::Low => Some("low"),
132        ReasoningLevel::Medium => Some("medium"),
133        ReasoningLevel::High => Some("high"),
134        ReasoningLevel::XHigh => Some("xhigh"),
135        ReasoningLevel::Max => Some("max"),
136        ReasoningLevel::Ultra => Some("ultra"),
137        ReasoningLevel::Adaptive | ReasoningLevel::UltraCode => None,
138    }
139}
140
141/// Inverse of [`muse_effort_label`] for catalog parsing. `minimal` has no
142/// Synaps level and is dropped rather than approximated.
143fn parse_muse_effort(effort: &str) -> Option<ReasoningLevel> {
144    match effort {
145        "none" => Some(ReasoningLevel::Off),
146        "low" => Some(ReasoningLevel::Low),
147        "medium" => Some(ReasoningLevel::Medium),
148        "high" => Some(ReasoningLevel::High),
149        "xhigh" => Some(ReasoningLevel::XHigh),
150        "max" => Some(ReasoningLevel::Max),
151        "ultra" => Some(ReasoningLevel::Ultra),
152        _ => None,
153    }
154}
155
156fn muse_row(id: &str, label: Option<String>) -> CatalogModel {
157    CatalogModel {
158        provider_key: MUSE_PROVIDER_KEY.into(),
159        provider_name: MUSE_PROVIDER_NAME.into(),
160        provider_kind: CatalogProviderKind::Generic {
161            key: MUSE_PROVIDER_KEY.into(),
162        },
163        id: id.into(),
164        label,
165        context_tokens: None,
166        max_output_tokens: None,
167        input_modalities: vec![Modality::Text],
168        pricing: PricingSummary::default(),
169        reasoning: ReasoningSupport::Unknown,
170        source: CatalogSource::Live,
171    }
172}
173
174/// Static seeds for pickers and the not-logged-in fallback.
175pub fn muse_static_catalog_models() -> Vec<CatalogModel> {
176    MUSE_TEXT_MODELS
177        .iter()
178        .map(|descriptor| {
179            let mut row = muse_row(descriptor.id, Some(descriptor.label.into()));
180            row.source = CatalogSource::StaticFallback;
181            row
182        })
183        .collect()
184}
185
186#[derive(Deserialize)]
187struct MuseCatalogBody {
188    #[serde(default)]
189    models: Option<Vec<serde_json::Value>>,
190    /// OpenAI-style `{"data":[...]}` tolerated as a fallback shape.
191    #[serde(default)]
192    data: Option<Vec<serde_json::Value>>,
193}
194
195#[derive(Deserialize)]
196struct MuseCatalogRow {
197    id: String,
198    #[serde(default)]
199    display_label: Option<String>,
200    #[serde(default)]
201    visibility: Option<String>,
202    #[serde(default)]
203    display_order: Option<i64>,
204    #[serde(default)]
205    context_limit: Option<u64>,
206    #[serde(default)]
207    output_limit: Option<u64>,
208    #[serde(default)]
209    reasoning_effort_variants: Option<Vec<MuseEffortVariant>>,
210    /// Observed wire shape: capabilities live under `metadata["muse-code"]`.
211    #[serde(default)]
212    metadata: Option<serde_json::Value>,
213}
214
215#[derive(Deserialize)]
216struct MuseEffortVariant {
217    #[serde(default)]
218    effort: Option<String>,
219}
220
221/// `metadata["muse-code"]` as served by `GET /muse-code/models` (models.dev
222/// style descriptor plus client `options` / `variants`). Every field is
223/// optional; a block that fails to deserialize is ignored as a whole (the row
224/// still routes, it just makes no capability claims).
225#[derive(Deserialize, Default)]
226struct MuseCodeMeta {
227    #[serde(default)]
228    is_hidden: Option<bool>,
229    #[serde(default)]
230    limit: Option<MuseLimit>,
231    #[serde(default)]
232    modalities: Option<MuseModalities>,
233    #[serde(default)]
234    options: Option<MuseOptions>,
235    /// `{"<name>": {"reasoningEffort": "<effort>", ...}, ...}`
236    #[serde(default)]
237    variants: Option<serde_json::Map<String, serde_json::Value>>,
238}
239
240#[derive(Deserialize, Default)]
241struct MuseLimit {
242    #[serde(default)]
243    context: Option<u64>,
244    #[serde(default)]
245    output: Option<u64>,
246}
247
248#[derive(Deserialize, Default)]
249struct MuseModalities {
250    #[serde(default)]
251    input: Option<Vec<String>>,
252    #[serde(default)]
253    output: Option<Vec<String>>,
254}
255
256#[derive(Deserialize, Default)]
257#[serde(rename_all = "camelCase")]
258struct MuseOptions {
259    #[serde(default)]
260    reasoning_effort: Option<String>,
261}
262
263/// Stable display/validation order for named efforts (the wire map's key
264/// order is not meaningful).
265fn effort_rank(level: ReasoningLevel) -> u8 {
266    match level {
267        ReasoningLevel::Off => 0,
268        ReasoningLevel::Low => 1,
269        ReasoningLevel::Medium => 2,
270        ReasoningLevel::High => 3,
271        ReasoningLevel::XHigh => 4,
272        ReasoningLevel::Max => 5,
273        ReasoningLevel::Ultra => 6,
274        ReasoningLevel::Adaptive | ReasoningLevel::UltraCode => u8::MAX,
275    }
276}
277
278fn push_unique(supported: &mut Vec<ReasoningLevel>, level: ReasoningLevel) {
279    if !supported.contains(&level) {
280        supported.push(level);
281    }
282}
283
284/// Parse `GET /muse-code/models`.
285///
286/// Observed wire shape (Meta, 2026-10): `{"object":"list","data":[{"id",
287/// "object","created","owned_by","metadata":{"muse-code":{"is_hidden",
288/// "limit":{"context","output"},"modalities":{"input","output"},
289/// "options":{"reasoningEffort",...},"variants":{"<name>":{"reasoningEffort"}},
290/// ...}}}]}`. A flat `{"models":[{display_label, reasoning_effort_variants,
291/// ...}]}` shape is also accepted. Hidden rows, rows that cannot output text
292/// (e.g. image generators) and ids outside the safe shape are dropped; rows
293/// that fail to deserialize are skipped individually so one novel row cannot
294/// blank the whole catalog. Effort variants become an exact
295/// [`ReasoningSupport::NamedEffort`] set; `options.reasoningEffort` becomes
296/// its default only when that level is in the set.
297pub fn parse_muse_catalog_models(body: &str) -> Result<Vec<CatalogModel>, String> {
298    let parsed: MuseCatalogBody =
299        serde_json::from_str(body).map_err(|e| format!("invalid Muse catalog JSON: {e}"))?;
300    let rows = parsed
301        .models
302        .or(parsed.data)
303        .ok_or_else(|| "Muse catalog has no models array".to_string())?;
304    let mut ordered: Vec<(i64, usize, CatalogModel)> = Vec::new();
305    for (index, value) in rows.into_iter().enumerate() {
306        let Ok(row) = serde_json::from_value::<MuseCatalogRow>(value) else {
307            continue;
308        };
309        let meta: MuseCodeMeta = row
310            .metadata
311            .as_ref()
312            .and_then(|m| m.get("muse-code"))
313            .and_then(|m| serde_json::from_value(m.clone()).ok())
314            .unwrap_or_default();
315        let modalities = meta.modalities.unwrap_or_default();
316        let text_output = modalities
317            .output
318            .as_ref()
319            .is_none_or(|out| out.iter().any(|m| m == "text"));
320        if row.visibility.as_deref() == Some("hidden")
321            || meta.is_hidden == Some(true)
322            || !text_output
323            || !valid_muse_id(&row.id)
324        {
325            continue;
326        }
327        if ordered.iter().any(|(_, _, existing)| existing.id == row.id) {
328            continue;
329        }
330        let label = row
331            .display_label
332            .map(|l| l.trim().to_string())
333            .filter(|l| !l.is_empty());
334        let mut model = muse_row(&row.id, label);
335        let limit = meta.limit.unwrap_or_default();
336        model.context_tokens = row.context_limit.or(limit.context).filter(|n| *n > 0);
337        model.max_output_tokens = row.output_limit.or(limit.output).filter(|n| *n > 0);
338        if let Some(input) = modalities.input.filter(|i| !i.is_empty()) {
339            let mut parsed = vec![Modality::Text];
340            for m in input.iter().map(|m| Modality::from_str(m.trim())) {
341                if !parsed.contains(&m) {
342                    parsed.push(m);
343                }
344            }
345            model.input_modalities = parsed;
346        }
347        let mut supported = Vec::new();
348        for effort in row
349            .reasoning_effort_variants
350            .unwrap_or_default()
351            .into_iter()
352            .filter_map(|v| v.effort)
353            .filter_map(|e| parse_muse_effort(e.trim()))
354        {
355            push_unique(&mut supported, effort);
356        }
357        let mut variant_levels = Vec::new();
358        // Only an explicit `reasoningEffort` counts. A preset's NAME may
359        // describe speed or a tool profile; treating `max` as an effort
360        // would authorize a wire value Meta can reject.
361        for variant in meta.variants.unwrap_or_default().into_values() {
362            let Some(effort) = variant.get("reasoningEffort").and_then(|e| e.as_str()) else {
363                continue;
364            };
365            if let Some(level) = parse_muse_effort(effort.trim()) {
366                push_unique(&mut variant_levels, level);
367            }
368        }
369        variant_levels.sort_by_key(|l| effort_rank(*l));
370        for level in variant_levels {
371            push_unique(&mut supported, level);
372        }
373        if !supported.is_empty() {
374            let default_level = meta
375                .options
376                .and_then(|o| o.reasoning_effort)
377                .and_then(|e| parse_muse_effort(e.trim()))
378                .filter(|level| supported.contains(level));
379            model.reasoning = ReasoningSupport::NamedEffort {
380                supported,
381                default_level,
382            };
383        }
384        ordered.push((row.display_order.unwrap_or(i64::MAX), index, model));
385    }
386    ordered.sort_by_key(|(order, index, _)| (*order, *index));
387    Ok(ordered.into_iter().map(|(_, _, model)| model).collect())
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    const LIVE: &str = r#"{"schema_version":1,"provider_id":"meta","source":"network","models":[
395        {"id":"muse-spark-1.3","display_label":"Muse Spark 1.3","visibility":"visible",
396         "display_order":2,"is_default":false,"context_limit":262144,"output_limit":128000,
397         "reasoning_effort_variants":[{"effort":"low","description":"l"},{"effort":"minimal"},
398           {"effort":"high"},{"effort":"high"},{"effort":"bogus"}]},
399        {"id":"muse-wonder-1.0","display_label":"Muse Wonder 1.0","visibility":"visible",
400         "display_order":1,"is_default":true,"context_limit":1000000,"output_limit":0,
401         "reasoning_effort_variants":[]},
402        {"id":"muse-internal-x","visibility":"hidden","display_order":0},
403        {"id":"Bad Id/../x","display_order":0},
404        {"display_label":"no id"},
405        {"id":"muse-novel-row","display_order":"not-a-number"}
406    ]}"#;
407
408    #[test]
409    fn effort_labels_round_trip_and_adaptive_omits() {
410        for level in [
411            ReasoningLevel::Off,
412            ReasoningLevel::Low,
413            ReasoningLevel::Medium,
414            ReasoningLevel::High,
415            ReasoningLevel::XHigh,
416            ReasoningLevel::Max,
417            ReasoningLevel::Ultra,
418        ] {
419            let label = muse_effort_label(level).unwrap();
420            assert_eq!(parse_muse_effort(label), Some(level), "{label}");
421        }
422        assert_eq!(muse_effort_label(ReasoningLevel::Adaptive), None);
423        assert_eq!(muse_effort_label(ReasoningLevel::UltraCode), None);
424        assert_eq!(parse_muse_effort("minimal"), None);
425    }
426
427    #[test]
428    fn live_catalog_parse_orders_filters_and_reads_exact_efforts() {
429        let models = parse_muse_catalog_models(LIVE).unwrap();
430        let ids: Vec<_> = models.iter().map(|m| m.id.as_str()).collect();
431        // display_order sort; hidden, malformed and id-less rows dropped; a
432        // row with a bad field type is skipped without blanking the catalog.
433        assert_eq!(ids, ["muse-wonder-1.0", "muse-spark-1.3"]);
434        let wonder = &models[0];
435        assert_eq!(wonder.runtime_id(), "muse/muse-wonder-1.0");
436        assert_eq!(wonder.context_tokens, Some(1_000_000));
437        assert_eq!(wonder.max_output_tokens, None, "0 is not a limit");
438        assert_eq!(wonder.reasoning, ReasoningSupport::Unknown);
439        let spark = &models[1];
440        assert_eq!(spark.label.as_deref(), Some("Muse Spark 1.3"));
441        assert_eq!(
442            spark.reasoning,
443            ReasoningSupport::NamedEffort {
444                supported: vec![ReasoningLevel::Low, ReasoningLevel::High],
445                default_level: None,
446            }
447        );
448        assert!(parse_muse_catalog_models(r#"{"data":[{"id":"m-1"}]}"#)
449            .unwrap()
450            .iter()
451            .any(|m| m.id == "m-1"));
452        assert!(parse_muse_catalog_models("{}").is_err());
453        assert!(parse_muse_catalog_models("nope").is_err());
454    }
455
456    /// Real `GET /muse-code/models` rows (trimmed, 2026-10): capabilities
457    /// live under `metadata["muse-code"]`. Regression for live rows parsing
458    /// as `ReasoningSupport::Unknown`, which made every named level fail
459    /// with "no advertised effort levels".
460    const WIRE: &str = r#"{"object":"list","data":[
461        {"id":"muse-wonder-1.0","object":"model","created":1,"owned_by":"meta","metadata":{"muse-code":{
462          "attachment":true,"description":null,"family":"avocado","is_hidden":false,
463          "limit":{"context":1007997,"output":128000},
464          "modalities":{"input":["text","image"],"output":["text"]},
465          "name":"muse-wonder-1.0",
466          "options":{"forceReasoning":true,"include":["reasoning.encrypted_content"],
467                     "reasoningEffort":"high","temperature":0.9,"top_p":0.9},
468          "reasoning":true,"tool_call":true,
469          "variants":{"high":{"reasoningEffort":"high"},"low":{"reasoningEffort":"low"},
470                      "max":{"reasoningEffort":"max"},"medium":{"reasoningEffort":"medium"},
471                      "minimal":{"reasoningEffort":"minimal"},
472                      "xhigh":{"reasoningEffort":"xhigh","selector":{"description":"d"}}}}}},
473        {"id":"muse-image-1.0","object":"model","owned_by":"meta","metadata":{"muse-code":{
474          "is_hidden":true,"modalities":{"input":["text","image"],"output":["image"]},
475          "options":{},"tool_call":false}}},
476        {"id":"muse-visible-imagegen-zz7","object":"model","metadata":{"muse-code":{
477          "is_hidden":false,"modalities":{"input":["text"],"output":["image"]}}}},
478        {"id":"muse-odd-default-zz8","object":"model","metadata":{"muse-code":{
479          "options":{"reasoningEffort":"ultra"},"variants":{"fast":{"reasoningEffort":"low"}}}}},
480        {"id":"muse-bad-meta-zz9","object":"model","metadata":{"muse-code":{"limit":"nope"}}}
481    ]}"#;
482
483    #[test]
484    fn wire_catalog_reads_muse_code_metadata() {
485        let models = parse_muse_catalog_models(WIRE).unwrap();
486        let ids: Vec<_> = models.iter().map(|m| m.id.as_str()).collect();
487        // Hidden and image-output rows dropped; a malformed metadata block
488        // keeps the row (routable, no claims).
489        assert_eq!(
490            ids,
491            [
492                "muse-wonder-1.0",
493                "muse-odd-default-zz8",
494                "muse-bad-meta-zz9"
495            ]
496        );
497        let wonder = &models[0];
498        assert_eq!(wonder.source, CatalogSource::Live);
499        assert_eq!(wonder.context_tokens, Some(1_007_997));
500        assert_eq!(wonder.max_output_tokens, Some(128_000));
501        assert_eq!(
502            wonder.input_modalities,
503            vec![Modality::Text, Modality::Image]
504        );
505        // Map key order is irrelevant: canonical order, `minimal` dropped,
506        // `options.reasoningEffort` is the default.
507        assert_eq!(
508            wonder.reasoning,
509            ReasoningSupport::NamedEffort {
510                supported: vec![
511                    ReasoningLevel::Low,
512                    ReasoningLevel::Medium,
513                    ReasoningLevel::High,
514                    ReasoningLevel::XHigh,
515                    ReasoningLevel::Max,
516                ],
517                default_level: Some(ReasoningLevel::High),
518            }
519        );
520        // Variant name is not the effort; a default outside the set is not
521        // claimed.
522        assert_eq!(
523            models[1].reasoning,
524            ReasoningSupport::NamedEffort {
525                supported: vec![ReasoningLevel::Low],
526                default_level: None,
527            }
528        );
529        let bad = &models[2];
530        assert_eq!(bad.reasoning, ReasoningSupport::Unknown);
531        assert_eq!(bad.input_modalities, vec![Modality::Text]);
532        assert_eq!(bad.context_tokens, None);
533    }
534
535    /// A preset's NAME is not an effort. `max` here is a speed/tool preset
536    /// with no `reasoningEffort`; counting it would authorize (and send)
537    /// `reasoning.effort:"max"`, which Meta can reject with a 400.
538    #[test]
539    fn variant_name_without_reasoning_effort_is_not_an_effort() {
540        let wire = r#"{"object":"list","data":[
541            {"id":"muse-preset-zz1","object":"model","metadata":{"muse-code":{
542              "variants":{"max":{"speed":"fast"},"high":{"tools":"minimal"},
543                          "careful":{"reasoningEffort":"medium"}}}}},
544            {"id":"muse-preset-zz2","object":"model","metadata":{"muse-code":{
545              "variants":{"high":{},"low":{"temperature":0.1}}}}}
546        ]}"#;
547        let models = parse_muse_catalog_models(wire).unwrap();
548        assert_eq!(
549            models[0].reasoning,
550            ReasoningSupport::NamedEffort {
551                supported: vec![ReasoningLevel::Medium],
552                default_level: None,
553            }
554        );
555        assert_eq!(models[1].reasoning, ReasoningSupport::Unknown);
556    }
557
558    #[test]
559    fn wire_catalog_default_drives_default_level_for_model() {
560        use crate::runtime::openai::catalog::validation::{
561            default_level_for_model, validate_reasoning_mutation,
562        };
563        let _guard = test_cache_guard();
564        capability_cache::replace_provider(MUSE_PROVIDER_KEY, &[]);
565        assert_eq!(
566            default_level_for_model("muse/muse-wonder-1.0"),
567            Some(ReasoningLevel::Adaptive),
568            "no live evidence → provider default"
569        );
570        capability_cache::replace_provider(
571            MUSE_PROVIDER_KEY,
572            &parse_muse_catalog_models(WIRE).unwrap(),
573        );
574        assert_eq!(
575            muse_default_level("muse-wonder-1.0"),
576            Some(ReasoningLevel::High)
577        );
578        assert_eq!(
579            default_level_for_model("muse/muse-wonder-1.0"),
580            Some(ReasoningLevel::High)
581        );
582        assert!(validate_reasoning_mutation("muse/muse-wonder-1.0", ReasoningLevel::XHigh).is_ok());
583        assert!(
584            validate_reasoning_mutation("muse/muse-wonder-1.0", ReasoningLevel::Ultra).is_err()
585        );
586        assert_eq!(muse_default_level("muse-odd-default-zz8"), None);
587        assert_eq!(
588            default_level_for_model("muse/muse-odd-default-zz8"),
589            Some(ReasoningLevel::Adaptive)
590        );
591        capability_cache::replace_provider(MUSE_PROVIDER_KEY, &[]);
592    }
593
594    #[test]
595    fn static_seed_is_documented_id_only_and_ids_fail_closed() {
596        assert!(muse_static_model("muse-wonder-1.0").is_some());
597        assert!(muse_static_model("muse-wonder").is_none());
598        let seeds = muse_static_catalog_models();
599        assert_eq!(seeds.len(), 1);
600        assert_eq!(seeds[0].source, CatalogSource::StaticFallback);
601        assert_eq!(seeds[0].reasoning, ReasoningSupport::Unknown);
602        assert!(muse_runtime_model("muse-wonder-1.0"));
603        // Known (strict) vs routable (shape): uncataloged ids route but are
604        // not "known" to strict validators.
605        assert!(!muse_runtime_model("never-cataloged-zz9"));
606        assert!(muse_routable_id("never-cataloged-zz9"));
607        assert!(!muse_routable_id("Bad Id"));
608        assert_eq!(muse_supported_levels("muse-wonder-1.0"), None);
609    }
610
611    #[test]
612    fn id_shape_guard() {
613        for ok in ["muse-wonder-1.0", "muse_spark.1", "a"] {
614            assert!(valid_muse_id(ok), "{ok}");
615        }
616        for bad in ["", "Muse", "-x", "a/b", "a b", "../x", &"a".repeat(129)] {
617            assert!(!valid_muse_id(bad), "{bad}");
618        }
619    }
620}