1use agent_core::reasoning::ReasoningLevel;
30use serde::Deserialize;
31
32use super::{
33 capability_cache, CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary,
34 ReasoningSupport,
35};
36
37pub const MUSE_PROVIDER_KEY: &str = "muse";
39pub const MUSE_PROVIDER_NAME: &str = "Meta Muse";
41pub const MUSE_MODELS_PATH: &str = "/muse-code/models";
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct MuseModelDescriptor {
47 pub id: &'static str,
48 pub label: &'static str,
49}
50
51pub const MUSE_TEXT_MODELS: &[MuseModelDescriptor] = &[MuseModelDescriptor {
53 id: "muse-wonder-1.0",
54 label: "Muse Wonder 1.0",
55}];
56
57pub fn muse_static_model(id: &str) -> Option<&'static MuseModelDescriptor> {
59 MUSE_TEXT_MODELS.iter().find(|model| model.id == id)
60}
61
62fn 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#[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
87pub fn muse_routable_id(id: &str) -> bool {
91 valid_muse_id(id)
92}
93
94pub 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
103pub 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
113pub 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
126pub 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
141fn 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
174pub 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 #[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 #[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#[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 #[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
263fn 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
284pub 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 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 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 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 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 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 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 #[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 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}