fable, and every model alias the CLI actually takes
the claude tier list had three entries — opus, sonnet, haiku — and the CLI accepts nine. `--help` says
`--model` takes either an alias for the latest model in a family or a full name, and the aliases the
INSTALLED cli accepts came out of its own binary with `strings` rather than from memory: default, opus,
sonnet, haiku, fable, mythos, opusplan, and a [1m] suffix on the families with a 1M-context variant.
verified against 2.1.233.
aliases rather than full ids (claude-opus-5) so a new model in a family needs no edit here. the cost is
that what `opus` resolves to moves under you — right for a picker, wrong for a reproducible eval.
mythos is deliberately left out: claude-mythos-5 is Project Glasswing only, and a picker entry that 401s
is worse than a missing one. the comment says how to add it.
opusplan is the one entry that is a MODE rather than a model — opus plans, sonnet executes.
NOT derived from the harness, though it could be: Query.supportedModels() returns the real list with
display names and effort levels, but it is a control request and needs a live session, and this endpoint
is what the composer calls before there is one. noted as open in the file.
the test pins the alias set and the property everything else rests on: claude-manager derives what it
hands the SDK with `split('/')[1]`, so a bracketed alias only works while the id has exactly one slash and
the brackets are unencoded. tidying an id into claude-code/opus/1m would fail as the harness quietly
running the wrong model, not as an error.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { listClaudeCodeModels } from './list-models';
|
||||
|
||||
/**
|
||||
* The Claude tier list, and the one property the rest of the system depends on: the id survives the split.
|
||||
*
|
||||
* `claude-manager` derives what it hands the SDK with `params.model?.split('/')[1]`, so a bracketed alias
|
||||
* only works if the id has exactly one `/` and the brackets come through untouched. That is easy to break
|
||||
* by "tidying" an id into `claude-code/opus/1m` or url-encoding the brackets, and it would fail as the
|
||||
* harness silently running the wrong model rather than as an error — the same class of silent mismatch the
|
||||
* mid-conversation switch work existed to remove.
|
||||
*/
|
||||
const ids = () => listClaudeCodeModels().map((m) => m.id);
|
||||
|
||||
describe('claude-code tiers', () => {
|
||||
test('every alias the installed CLI accepts is offered', () => {
|
||||
// Read out of the CLI binary rather than recalled — see the note in list-models.ts. `mythos` is
|
||||
// deliberately excluded (Project Glasswing only), so its absence is part of the claim.
|
||||
expect(ids()).toEqual([
|
||||
'claude-code/fable',
|
||||
'claude-code/fable[1m]',
|
||||
'claude-code/opus',
|
||||
'claude-code/opus[1m]',
|
||||
'claude-code/opusplan',
|
||||
'claude-code/opusplan[1m]',
|
||||
'claude-code/sonnet',
|
||||
'claude-code/sonnet[1m]',
|
||||
'claude-code/haiku',
|
||||
]);
|
||||
});
|
||||
|
||||
test('the alias survives `split("/")[1]` with its brackets intact', () => {
|
||||
for (const id of ids()) {
|
||||
const parts = id.split('/');
|
||||
expect(parts).toHaveLength(2);
|
||||
expect(parts[1]).not.toContain('%');
|
||||
}
|
||||
expect('claude-code/opus[1m]'.split('/')[1]).toBe('opus[1m]');
|
||||
});
|
||||
|
||||
test('a 1M variant declares a 1M window, and nothing else does', () => {
|
||||
for (const model of listClaudeCodeModels()) {
|
||||
expect(model.contextWindow).toBe(model.id.endsWith('[1m]') ? 1_000_000 : 200_000);
|
||||
}
|
||||
});
|
||||
|
||||
test('every tier is routed to the Claude sidecar', () => {
|
||||
// `isClaudeModel` is a `startsWith` on the harness prefix; an id that misses it silently dispatches to
|
||||
// OpenCode, which is the wrong-harness bug the model-switch guard refuses at runtime.
|
||||
for (const id of ids()) expect(id.startsWith('claude-code')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +1,68 @@
|
||||
import type { ModelInfo } from './types';
|
||||
import { getOpenCodeServerUrl } from './opencode/sidecar-server';
|
||||
|
||||
// The Claude harness runs the `claude` CLI, so its tiers are a fixed set.
|
||||
const CLAUDE_CODE_MODELS: ModelInfo[] = [
|
||||
{
|
||||
id: 'claude-code/opus',
|
||||
name: 'opus',
|
||||
provider: 'claude-code',
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16000,
|
||||
reasoning: true,
|
||||
images: true,
|
||||
},
|
||||
{
|
||||
id: 'claude-code/sonnet',
|
||||
name: 'sonnet',
|
||||
provider: 'claude-code',
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16000,
|
||||
reasoning: true,
|
||||
images: true,
|
||||
},
|
||||
{
|
||||
id: 'claude-code/haiku',
|
||||
name: 'haiku',
|
||||
provider: 'claude-code',
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
reasoning: false,
|
||||
images: true,
|
||||
},
|
||||
/*
|
||||
The Claude harness runs the `claude` CLI, so its tiers are a fixed set — but the set is bigger than the
|
||||
three this listed until 2026-08-17.
|
||||
|
||||
── Where this list comes from ──
|
||||
|
||||
The CLI takes either an ALIAS for the latest model in a family or a full model name; `claude --help` says
|
||||
so on `--model`. The aliases below are the ones the installed CLI actually accepts, read out of its own
|
||||
binary (`strings`) rather than recalled: `default`, `opus`, `sonnet`, `haiku`, `fable`, `mythos`, plus
|
||||
`opusplan` and a `[1m]` suffix on the families that offer a 1M-context variant. Verified against
|
||||
2.1.233.
|
||||
|
||||
Aliases rather than full names (`claude-opus-5`) on purpose: an alias follows the latest model in its
|
||||
family, so this file does not need editing every time one ships. The cost is that what `opus` resolves to
|
||||
changes under you — which is the right trade for a picker, and the wrong one for a reproducible eval.
|
||||
|
||||
── Why it is still hardcoded ──
|
||||
|
||||
The SDK can answer this properly: `Query.supportedModels()` returns the harness's own list with display
|
||||
names, descriptions and effort levels. It is a CONTROL REQUEST, so it needs a live session — and this
|
||||
endpoint is what the composer calls BEFORE there is one. Deriving the catalogue from the harness means
|
||||
either holding a session open just to ask, or caching an answer from the last session, and neither is
|
||||
worth it while the set changes once a release. `[open]` If a `supportedModels()` cache ever lands, this
|
||||
array becomes its fallback rather than its source.
|
||||
|
||||
── mythos is deliberately absent ──
|
||||
|
||||
`claude-mythos-5` is Project Glasswing only. Listing it would offer a model most installs cannot reach,
|
||||
and a picker entry that 401s is worse than one that is missing. Add it here if this machine has access.
|
||||
*/
|
||||
type ClaudeTier = { alias: string; label: string; long?: boolean; reasoning?: boolean; maxTokens?: number };
|
||||
|
||||
const CLAUDE_CODE_TIERS: ClaudeTier[] = [
|
||||
{ alias: 'fable', label: 'fable' },
|
||||
{ alias: 'fable[1m]', label: 'fable (1M)', long: true },
|
||||
{ alias: 'opus', label: 'opus' },
|
||||
{ alias: 'opus[1m]', label: 'opus (1M)', long: true },
|
||||
// opus plans, sonnet executes. A real alias, and the one entry that is a MODE rather than a model —
|
||||
// worth listing because it is the cheapest way to get opus-quality planning on a long task.
|
||||
{ alias: 'opusplan', label: 'opus plan → sonnet' },
|
||||
{ alias: 'opusplan[1m]', label: 'opus plan → sonnet (1M)', long: true },
|
||||
{ alias: 'sonnet', label: 'sonnet' },
|
||||
{ alias: 'sonnet[1m]', label: 'sonnet (1M)', long: true },
|
||||
{ alias: 'haiku', label: 'haiku', reasoning: false, maxTokens: 8192 },
|
||||
];
|
||||
|
||||
const CLAUDE_CODE_MODELS: ModelInfo[] = CLAUDE_CODE_TIERS.map((tier) => ({
|
||||
id: `claude-code/${tier.alias}`,
|
||||
name: tier.label,
|
||||
provider: 'claude-code',
|
||||
// `[1m]` is the caller ASKING for the 1M-context variant, so it is the one case where the window is
|
||||
// known from the alias. Everything else keeps the 200K this file has always claimed — a floor, not a
|
||||
// measurement, since an alias resolves to whatever the CLI currently points it at.
|
||||
contextWindow: tier.long ? 1_000_000 : 200_000,
|
||||
maxTokens: tier.maxTokens ?? 16000,
|
||||
reasoning: tier.reasoning ?? true,
|
||||
images: true,
|
||||
}));
|
||||
|
||||
/** The Claude tiers alone, so a test can assert the alias set without reaching the OpenCode sidecar. */
|
||||
export const listClaudeCodeModels = (): ModelInfo[] => CLAUDE_CODE_MODELS;
|
||||
|
||||
// Cache the OpenCode catalog; it's stable for a session.
|
||||
let openCodeCache: ModelInfo[] | null = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user