From b63bae662ac5f87bbead2a1b1167960e9956f3d4 Mon Sep 17 00:00:00 2001 From: Daniel Katulevskiy Date: Thu, 1 Oct 2026 02:52:54 -0700 Subject: [PATCH] Claude: label resolved defaults with live model names --- .../harness/examples/claude_models_probe.rs | 20 ++++ crates/harness/src/claude/catalog.rs | 61 ++++++++++-- crates/harness/tests/claude_models.rs | 96 +++++++++++++++++++ 3 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 crates/harness/examples/claude_models_probe.rs diff --git a/crates/harness/examples/claude_models_probe.rs b/crates/harness/examples/claude_models_probe.rs new file mode 100644 index 0000000000..e2c4c651c4 --- /dev/null +++ b/crates/harness/examples/claude_models_probe.rs @@ -0,0 +1,20 @@ +//! Read-only model discovery using the same resolver and protocol as the picker. +use zeron_harness::{ClaudeHarness, Harness}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let harness = ClaudeHarness::new(); + let context = harness.model_context()?.expect("Claude context"); + println!("binary: {}", context.binary_path.display()); + println!( + "version: {}", + context.binary_version.as_deref().unwrap_or("unknown") + ); + let catalog = harness.model_catalog(true).await?; + println!("source: {}", catalog.source); + for model in &catalog.models { + println!("{} => {}", model.id, model.label); + } + println!("models: {}", catalog.models.len()); + Ok(()) +} diff --git a/crates/harness/src/claude/catalog.rs b/crates/harness/src/claude/catalog.rs index 21e3fc89da..f391a08da6 100644 --- a/crates/harness/src/claude/catalog.rs +++ b/crates/harness/src/claude/catalog.rs @@ -220,6 +220,38 @@ fn settings_models_keep_manifest_and_full_ladder() { assert_eq!(models_with_settings(&path), static_models()); } +/// Claude's named rows put the versioned display name before the capability +/// text in their descriptions. Use that supplied name when the displayName is +/// a short alias; never reconstruct a model name from a catalog or model ID. +fn discovered_model_label(entry: &serde_json::Value, id: &str) -> String { + let Some(name) = model_text(entry, "displayName") else { + return id.to_owned(); + }; + let short_name = name + .strip_suffix(')') + .and_then(|name| name.rsplit_once(" (").map(|(base, _)| base)) + .unwrap_or(name); + if let Some(description) = model_text(entry, "description") { + let heading = description.split('·').next().unwrap_or(description).trim(); + if let Some(version) = heading + .strip_prefix(short_name) + .and_then(|s| s.strip_prefix(' ')) + && version.chars().next().is_some_and(|c| c.is_ascii_digit()) + { + return heading.to_owned(); + } + } + name.to_owned() +} + +fn model_text<'a>(entry: &'a serde_json::Value, key: &str) -> Option<&'a str> { + entry + .get(key) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|v| !v.is_empty()) +} + /// Overlay only new concrete model IDs; curated metadata remains authoritative. pub(super) fn with_discovered_models( mut models: Vec, @@ -236,13 +268,7 @@ pub(super) fn with_discovered_models( let mut default = None; let mut valid = false; for entry in entries { - let text = |key: &str| { - entry - .get(key) - .and_then(serde_json::Value::as_str) - .map(str::trim) - .filter(|v| !v.is_empty()) - }; + let text = |key: &str| model_text(entry, key); let Some(id) = text("resolvedModel").or_else(|| text("value")) else { continue; }; @@ -283,10 +309,27 @@ pub(super) fn with_discovered_models( if ladder.contains(&ReasoningLevel::XHigh) { ladder.extend([ReasoningLevel::Ultracode, ReasoningLevel::Ultrathink]); } + // The default row is a selection policy, not a model name. Match its + // resolved ID to the CLI's named row before deduplication, regardless + // of row order. Unnamed defaults retain the concrete ID as their label. + let named_entry = if text("value") == Some("default") { + entries.iter().find(|named| { + model_text(named, "value") != Some("default") + && model_text(named, "resolvedModel").or_else(|| model_text(named, "value")) + == Some(id) + }) + } else { + Some(entry) + }; models.push(Model { id: id.into(), - label: text("displayName").unwrap_or(id).into(), - description: text("description").map(str::to_owned), + label: named_entry + .map(|named| discovered_model_label(named, id)) + .unwrap_or_else(|| id.to_owned()), + description: named_entry + .and_then(|named| model_text(named, "description")) + .or_else(|| text("description")) + .map(str::to_owned), reasoning_levels: ladder, options: vec![], }); diff --git a/crates/harness/tests/claude_models.rs b/crates/harness/tests/claude_models.rs index f89cc39e56..3d118a5b48 100644 --- a/crates/harness/tests/claude_models.rs +++ b/crates/harness/tests/claude_models.rs @@ -82,3 +82,99 @@ async fn failed_or_logged_out_initialize_falls_back_to_curated_catalog() { assert!(harness.model_catalog(false).await.is_err()); } } + +#[tokio::test] +async fn default_uses_the_named_rows_live_model_name_in_either_order() { + for aliases in [["default", "sonnet"], ["sonnet", "default"]] { + let (_dir, harness) = fixture(json!({"subtype":"success", "response":{ + "models": aliases.map(|alias| json!({ + "value":alias, "resolvedModel":"claude-sonnet-5-5", + "displayName":if alias == "default" { "Default (recommended)" } else { "Sonnet" }, + "description":if alias == "default" { + "Use the default model (currently Sonnet 5.5) · $2/$10 per Mtok" + } else { "Sonnet 5.5 · Efficient for routine tasks · $2/$10 per Mtok" }, + "supportedEffortLevels":["low","medium","high","xhigh","max"] + })) + }})); + let catalog = harness.model_catalog(true).await.unwrap(); + let sonnet = &catalog.models[0]; + assert_eq!(sonnet.id, "claude-sonnet-5-5"); + assert_eq!(sonnet.label, "Sonnet 5.5"); + assert_eq!( + sonnet.description.as_deref(), + Some("Sonnet 5.5 · Efficient for routine tasks · $2/$10 per Mtok") + ); + assert_eq!( + catalog.models.iter().filter(|m| m.id == sonnet.id).count(), + 1 + ); + assert!(sonnet.reasoning_levels.contains(&ReasoningLevel::XHigh)); + // Discovery supplies this name; the fallback must not know the new model. + assert!( + !zeron_harness::claude::catalog::static_models() + .iter() + .any(|m| m.id == sonnet.id) + ); + } +} + +#[tokio::test] +async fn live_names_do_not_depend_on_known_families_versions_or_id_formats() { + for (id, name, description, expected) in [ + ( + "provider/opaque-deployment", + "Nebula", + "Nebula 17.3 · Provider description", + "Nebula 17.3", + ), + ( + "gateway/custom", + "Custom gateway model", + "Gateway description", + "Custom gateway model", + ), + ( + "opaque-model-without-prose", + "Nebula", + "Nebula 18.1", + "Nebula 18.1", + ), + ( + "next-generation", + "New Model Preview", + "Provider description", + "New Model Preview", + ), + ( + "claude-sonnet-6-2[1m]", + "Sonnet (1M context)", + "Sonnet 6.2 · Provider description", + "Sonnet 6.2", + ), + ] { + let (_dir, harness) = fixture(json!({"subtype":"success", "response":{ + "models":[ + {"value":"default", "resolvedModel":id, "displayName":"Default (recommended)"}, + {"value":"new-alias", "resolvedModel":id, "displayName":name, "description":description} + ] + }})); + let catalog = harness.model_catalog(true).await.unwrap(); + assert_eq!(catalog.models[0].id, id); + assert_eq!(catalog.models[0].label, expected); + assert_eq!(catalog.models[0].description.as_deref(), Some(description)); + } +} + +#[tokio::test] +async fn unnamed_default_uses_its_concrete_id_without_guessing_the_model_name() { + let (_dir, harness) = fixture(json!({"subtype":"success", "response":{ + "models":[ + {"value":"default", "resolvedModel":"gateway/custom", "displayName":"Default (recommended)"}, + {"value":"gateway/other", "displayName":"Custom gateway model"} + ] + }})); + let catalog = harness.model_catalog(true).await.unwrap(); + assert_eq!(catalog.models[0].id, "gateway/custom"); + assert_eq!(catalog.models[0].label, "gateway/custom"); + assert_eq!(catalog.models.last().unwrap().label, "Custom gateway model"); +}