Skip to content

feat(models): user-defined model aliases with cross-provider targets#781

Merged
askalf merged 1 commit into
masterfrom
feat/model-aliases
Jul 17, 2026
Merged

feat(models): user-defined model aliases with cross-provider targets#781
askalf merged 1 commit into
masterfrom
feat/model-aliases

Conversation

@askalf

@askalf askalf commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What

User-defined model aliases: --model-alias=name=target (repeatable), DARIO_MODEL_ALIASES=name=target,…, config modelAliases. Resolved per request before provider-prefix parsing, so a target may carry a prefix and retarget the backend (my-fastopenai:gpt-4o-mini, cheapclaude:haiku). Advertised on /v1/models after the real ids (deduped) so client model pickers offer them.

Semantics worth reviewing

  • User aliases win over built-in shorthands. --model-alias=opus=claude-haiku-4-5 remaps every opus call — 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.
  • One step, never recursive. A target naming another alias forwards as-is; self-mappings return null. No cycle detection needed because no cycles are constructible.
  • Names lowercase + trim on both parse and lookup; targets forward verbatim (OpenAI-compat backends can be case-sensitive).
  • --model still wins. Aliases rewrite the client's request model; the server-wide override applies downstream as before.
  • TUI: not editable there (the Config tab has no map editor); the key round-trips through save/load untouched. Noting rather than hiding it.

Tests

test/model-aliases.mjs — 22 checks: spec parsing (invalid shapes, dupes last-win, split on first =), lookup behavior, composition with parseProviderPrefix, config sanitize round-trip. Full suite green locally.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Compat test: ✅ PASSED

Ran node test/compat.mjs against dario proxy --passthrough on the self-hosted runner for commit 9798246e438dbd318d85e4510f90d79cfbcd60a7.

Output
============================================================
  dario Compatibility Validation (--passthrough via dario)
  2026-07-17T02:41:11.260Z
============================================================

--- Anthropic Messages API (Hermes) ---
✅ #1 Anthropic non-stream: "COMPAT OK" | thinking_injected=false | service_tier=-
✅ #2 Anthropic stream: 8 events | order=correct | "**STREAM COMPAT**"
✅ #3 SSE framing: event:/data: pairs correctly paired

--- Passthrough Verification ---
✅ #4 No thinking injection: No thinking block in response (passthrough clean)
✅ #5 Client betas preserved: Client-requested thinking honored (status=200)

--- Tool Use (OpenClaw) ---
✅ #6 Tool use: tool=get_weather | input={"location":"Tokyo"}
✅ #7 Tool use stream: 13 events | tool_use=true | input_json_delta=true

--- OpenAI Compat ---
✅ #8 OpenAI non-stream: "OPENAI COMPAT"
✅ #9 OpenAI stream: 3 chunks | [DONE]=true | "**STREAM OPENAI**"

--- Header Visibility ---
✅ #10 Header visibility: request-id=true | ratelimit=true (12 headers)

============================================================
  RESULTS: 10 passed, 0 failed, 0 warnings
============================================================

Full workflow run

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Wire drift: ✅ CLEAN

Ran node scripts/check-wire-drift.mjs against the runner's claude for commit 9798246e438dbd318d85e4510f90d79cfbcd60a7 — per-model anthropic-beta (betaForModel) + billing-block cch gate vs the installed claude.

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": []
}

Full workflow run

@askalf
askalf enabled auto-merge (squash) July 17, 2026 00:31
@askalf
askalf force-pushed the feat/model-aliases branch from d72de92 to 6c6adbe Compare July 17, 2026 00:56
…_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.
@askalf
askalf force-pushed the feat/model-aliases branch from 6c6adbe to 9798246 Compare July 17, 2026 02:40

@sprayberry-reviewer sprayberry-reviewer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.tsparseModelAliasSpecs, applyModelAlias, /v1/models dedup logic, and the per-request alias resolution block
  • src/cli.ts — merge order (config < env < flags) via object spread
  • src/config-file.tssanitize() path for modelAliases, key lowercasing, target trimming, and default value
  • test/model-aliases.mjs — 22 test cases covering spec parsing, lookup, provider-prefix composition, and config round-trip
  • docs/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.

@askalf
askalf merged commit 24d22b3 into master Jul 17, 2026
12 checks passed
@askalf
askalf deleted the feat/model-aliases branch July 17, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants