From b89ddda387cf02a7b604005d49e2294e8a3a8d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BA=91=E9=BE=99?= <76432572+nankingjing@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:27:48 +0800 Subject: [PATCH 1/2] docs(rust/README): document xAI and Kimi model aliases --- rust/README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/rust/README.md b/rust/README.md index 53ebfc744e..f615c741e6 100644 --- a/rust/README.md +++ b/rust/README.md @@ -110,13 +110,27 @@ Primary artifacts: ## Model Aliases -Short names resolve to the latest model versions: - -| Alias | Resolves To | -|-------|------------| -| `opus` | `claude-opus-4-7` | -| `sonnet` | `claude-sonnet-4-6` | -| `haiku` | `claude-haiku-4-5-20251213` | +Short model aliases resolve to canonical model IDs. Because the router selects a +provider from the resolved model, an alias also determines which backend and +credentials are used — choose the alias for the provider you have an API key for. +Alias resolution is case-insensitive (`OPUS` behaves the same as `opus`). + +| Alias | Resolves To | Provider | Auth env var | +|-------|-------------|----------|--------------| +| `opus` | `claude-opus-4-7` | Anthropic | `ANTHROPIC_API_KEY` | +| `sonnet` | `claude-sonnet-4-6` | Anthropic | `ANTHROPIC_API_KEY` | +| `haiku` | `claude-haiku-4-5-20251213` | Anthropic | `ANTHROPIC_API_KEY` | +| `grok`, `grok-3` | `grok-3` | xAI | `XAI_API_KEY` | +| `grok-mini`, `grok-3-mini` | `grok-3-mini` | xAI | `XAI_API_KEY` | +| `grok-2` | `grok-2` | xAI | `XAI_API_KEY` | +| `kimi` | `kimi-k2.5` | DashScope (OpenAI-compatible) | `DASHSCOPE_API_KEY` | + +Anthropic aliases track the latest published model versions. xAI models honor +`XAI_BASE_URL` and Kimi honors `DASHSCOPE_BASE_URL` for base-URL overrides. Alias +resolution is implemented in `resolve_model_alias` +([`crates/api/src/providers/mod.rs`](./crates/api/src/providers/mod.rs)); for +provider-specific request handling see +[`../docs/MODEL_COMPATIBILITY.md`](../docs/MODEL_COMPATIBILITY.md). ## CLI Flags and Commands From 072594dfee4252c6f6943f8431b6d767bb7b6eea Mon Sep 17 00:00:00 2001 From: nankingjing <76432572+nankingjing@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:19:39 +0800 Subject: [PATCH 2/2] test(api): lock the README model-alias table to the resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `## Model Aliases` table and `MODEL_REGISTRY` had drifted: the table listed only the three Anthropic aliases while the resolver had grown the xAI and Kimi entries, so an alias could resolve without being documented. Assert the two agree in both directions — every registry alias appears in the table with the canonical id and auth env var it actually resolves to, and the table introduces no alias the resolver does not know. Each row is also checked case-insensitively, since the README claims `OPUS` behaves the same as `opus`. Also pin that a provider-prefixed form such as `anthropic/opus` is not an alias: it is passed through as-is rather than expanded. The README now says so, which is what made the distinction worth a test rather than a comment. --- rust/README.md | 5 +- rust/crates/api/src/providers/mod.rs | 129 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/rust/README.md b/rust/README.md index f615c741e6..8fa9882a83 100644 --- a/rust/README.md +++ b/rust/README.md @@ -113,7 +113,10 @@ Primary artifacts: Short model aliases resolve to canonical model IDs. Because the router selects a provider from the resolved model, an alias also determines which backend and credentials are used — choose the alias for the provider you have an API key for. -Alias resolution is case-insensitive (`OPUS` behaves the same as `opus`). +Alias resolution is case-insensitive (`OPUS` behaves the same as `opus`). Aliases +are the bare short names listed below: a prefixed form such as `anthropic/opus` +is not an alias, and is passed through as-is rather than expanded to +`claude-opus-4-7`. | Alias | Resolves To | Provider | Auth env var | |-------|-------------|----------|--------------| diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index 2524e5520a..b571f9ee0c 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -902,6 +902,135 @@ mod tests { assert_eq!(resolve_model_alias("grok-2"), "grok-2"); } + /// Collects every non-empty backtick-delimited span in a markdown cell. + fn backticked(cell: &str) -> Vec<&str> { + let mut spans = Vec::new(); + let mut rest = cell; + while let Some(start) = rest.find('`') { + let after_open = &rest[start + 1..]; + let Some(end) = after_open.find('`') else { + break; + }; + spans.push(&after_open[..end]); + rest = &after_open[end + 1..]; + } + spans + } + + // Locks the `## Model Aliases` table in `rust/README.md` to the resolver. + // + // The table and the alias set must agree in *both* directions: no alias may + // resolve without being documented, and none may be documented without + // resolving. The table previously listed only the three Anthropic aliases + // while the resolver had grown xAI and Kimi entries, so an alias could work + // undocumented — exactly the drift a parity test catches and a hand-written + // `assert_eq!` per alias does not. + #[test] + fn readme_alias_table_matches_resolver() { + // `rust/crates/api/src/providers/mod.rs` -> `rust/README.md`. + const README: &str = include_str!("../../../../README.md"); + + let section = README + .split("## Model Aliases") + .nth(1) + .expect("rust/README.md must keep a `## Model Aliases` section"); + let section = section.split("\n## ").next().unwrap_or(section); + + // (alias, documented canonical id, documented auth env var) + let mut documented: Vec<(String, String, String)> = Vec::new(); + for line in section.lines() { + let line = line.trim(); + let Some(row) = line.strip_prefix('|') else { + continue; + }; + let cells: Vec<&str> = row + .trim_end_matches('|') + .split('|') + .map(str::trim) + .collect(); + if cells.len() < 4 { + continue; + } + let aliases = backticked(cells[0]); + if aliases.is_empty() { + // Header and `|---|---|` separator rows carry no aliases. + continue; + } + let canonicals = backticked(cells[1]); + assert_eq!( + canonicals.len(), + 1, + "`{line}` must document exactly one canonical model id" + ); + let auth_envs = backticked(cells[3]); + assert_eq!( + auth_envs.len(), + 1, + "`{line}` must document exactly one auth env var" + ); + for alias in aliases { + documented.push(( + alias.to_string(), + canonicals[0].to_string(), + auth_envs[0].to_string(), + )); + } + } + + assert!( + !documented.is_empty(), + "parsed no alias rows out of the `## Model Aliases` table in rust/README.md" + ); + + for (alias, canonical, auth_env) in &documented { + let resolved = resolve_model_alias(alias); + assert_eq!( + resolved.as_str(), + canonical.as_str(), + "rust/README.md documents `{alias}` -> `{canonical}`, but resolve_model_alias returns `{resolved}`" + ); + assert_eq!( + resolve_model_alias(&alias.to_ascii_uppercase()).as_str(), + resolved.as_str(), + "rust/README.md claims alias resolution is case-insensitive, but `{}` disagrees with `{alias}`", + alias.to_ascii_uppercase() + ); + let metadata = super::metadata_for_model(&resolved).unwrap_or_else(|| { + panic!("no provider metadata for documented model `{canonical}`") + }); + assert_eq!( + metadata.auth_env, + auth_env.as_str(), + "rust/README.md documents `{alias}` as needing `{auth_env}`, but `{canonical}` resolves to provider metadata with auth env `{}`", + metadata.auth_env + ); + } + + let documented_aliases: std::collections::BTreeSet<&str> = documented + .iter() + .map(|(alias, _, _)| alias.as_str()) + .collect(); + let resolver_aliases: std::collections::BTreeSet<&str> = super::MODEL_REGISTRY + .iter() + .map(|(alias, _)| *alias) + .collect(); + assert_eq!( + documented_aliases, resolver_aliases, + "rust/README.md's `## Model Aliases` table and MODEL_REGISTRY have drifted apart" + ); + } + + // The table documents bare short names only, so the provider-prefixed form + // is deliberately *not* an alias. This pins that distinction, since the + // prefix is handled later (at request-build time) and a future refactor + // could easily fold it into alias resolution. + #[test] + fn provider_prefixed_alias_is_not_an_alias() { + assert_eq!(resolve_model_alias("opus"), "claude-opus-4-7"); + assert_eq!(resolve_model_alias("anthropic/opus"), "anthropic/opus"); + assert_eq!(resolve_model_alias("xai/grok"), "xai/grok"); + } + #[test] fn detects_provider_from_model_name_first() { assert_eq!(detect_provider_kind("grok"), ProviderKind::Xai);