feat(models): user-defined model aliases with cross-provider targets#781
Conversation
Compat test: ✅ PASSEDRan Output |
Wire drift: ✅ CLEANRan Report{
"drift": false,
"checkedAt": "2026-07-17T02:45:11.564Z",
"ccVersion": "2.1.211",
"captured": {
"claude-opus-4-8": {
"beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24",
"cchEmitted": false,
"billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
},
"claude-sonnet-5": {
"beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24",
"cchEmitted": false,
"billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
},
"claude-haiku-4-5": {
"beta": "interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219,advisor-tool-2026-03-01",
"cchEmitted": false,
"billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
},
"claude-fable-5": {
"beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24,fallback-credit-2026-06-01",
"cchEmitted": false,
"billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
}
},
"findings": []
} |
d72de92 to
6c6adbe
Compare
…_ALIASES / config modelAliases) Client-visible name → target model, resolved at request time BEFORE provider-prefix parsing — so a target may carry a prefix and retarget the backend (my-fast → openai:gpt-4o-mini). Complements the built-in family shorthands, which keep tracking the catalog; user aliases win when both match, which is also how you remap every opus call from a client whose model picker you don't control. One step, never recursive: a target naming another alias forwards as-is; self-mappings resolve to null so a typo can't loop. Names match case-insensitively, targets forward verbatim. Merge order per-key: config < env < repeatable flag. Aliases are advertised on /v1/models (after the real ids, deduped) so client pickers can offer them. Server-wide --model still wins downstream — it overrides the resolved model like it always has.
6c6adbe to
9798246
Compare
sprayberry-reviewer
left a comment
There was a problem hiding this comment.
Automated review from the Sprayberry Labs fleet code reviewer. This is a verdict-bearing review from the fleet bot (sprayberry-reviewer), not a human sign-off.
Verdict: no blocking issues found — approving.
What I checked
src/proxy.ts—parseModelAliasSpecs,applyModelAlias,/v1/modelsdedup logic, and the per-request alias resolution blocksrc/cli.ts— merge order (config < env < flags) via object spreadsrc/config-file.ts—sanitize()path formodelAliases, key lowercasing, target trimming, and default valuetest/model-aliases.mjs— 22 test cases covering spec parsing, lookup, provider-prefix composition, and config round-tripdocs/commands.md— new row and help text
Fetched diff with gh pr diff 781 --repo askalf/dario and shallow-cloned head for surrounding context.
Correctness — clean
Merge-order spread (cli.ts:561-568): config ← env ← flags is correctly expressed as three successive object spreads; later spreads win per-key. ✓
parseModelAliasSpecs (proxy.ts) split-on-first-=: spec.indexOf('=') + spec.slice(0, idx) / spec.slice(idx + 1) preserves = signs in targets (e.g. weird=target=with=equals). Verified against test case "split on FIRST '=' only". ✓
applyModelAlias mutability: parsed.model = aliasTarget mutates the already-shared parsed object, so the immediately following const rawModel = (parsed.model as string | undefined) ?? '' picks up the resolved alias before provider-prefix parsing — no double-parse needed. ✓
/v1/models dedup (proxy.ts:2209): aliasNames = Object.keys(modelAliases).filter(n => !advertised.includes(n)) — alias keys are lowercased; catalog entries from withLongContextVariants(catalog.bases) are also lowercase Claude IDs, so the includes comparison is apples-to-apples. An alias shadowing a real model ID is still applied at request time (the advertised-list filter only prevents a duplicate entry, not the lookup). ✓
Empty / missing model guard: applyModelAlias returns null immediately on !model (falsy), and the CLI block uses ?? '' before the call, so a missing model field in the body skips alias resolution cleanly. ✓
Minor (non-blocking)
Self-mapping guard edge case (proxy.ts:~270):
if (target === undefined || target === model) return null;The target === model comparison is against the raw model argument (not trimmed/lowercased), while the map lookup uses model.trim().toLowerCase(). So applyModelAlias("OPUS", {"opus":"opus"}) resolves to "opus" (target) rather than null — the self-alias guard doesn't fire for case-mismatched input. In practice the end routing is still correct ("opus" and "OPUS" both reach resolveClaudeAlias), and a self-alias is a misconfiguration to begin with. Not blocking; document or normalize the comparison if you ever tighten the guard:
// suggestion
if (target === undefined || target === model.trim().toLowerCase()) return null;What's good
The layered merge logic (config < env < flag), "one step, never recursive" semantics, and the deliberate placement before provider-prefix parsing are all well-reasoned and clearly documented. The test file is thorough — invalid shapes, duplicate keys last-wins, whitespace trimming, targets-with-equals, case-insensitive lookup, self-mapping, composition with parseProviderPrefix, and config-file sanitize round-trip are all exercised. Clean addition.
What
User-defined model aliases:
--model-alias=name=target(repeatable),DARIO_MODEL_ALIASES=name=target,…, configmodelAliases. Resolved per request before provider-prefix parsing, so a target may carry a prefix and retarget the backend (my-fast→openai:gpt-4o-mini,cheap→claude:haiku). Advertised on/v1/modelsafter the real ids (deduped) so client model pickers offer them.Semantics worth reviewing
--model-alias=opus=claude-haiku-4-5remaps everyopuscall — deliberate: it's the only lever when you don't control a client's picker.resolveClaudeAlias(catalog shorthands) is untouched and still runs after, on the claude-prefix path.--modelstill wins. Aliases rewrite the client's request model; the server-wide override applies downstream as before.Tests
test/model-aliases.mjs— 22 checks: spec parsing (invalid shapes, dupes last-win, split on first=), lookup behavior, composition withparseProviderPrefix, config sanitize round-trip. Full suite green locally.