the picker stopped overruling the default

a new chat offered fable. not displayed it — SELECTED it, sent it, and the
server honoured the choice it was given. the composer's fallback was
providerModels[0], the first entry in the catalogue, which is not a default but
an ordering accident. the file already carried a comment about the day fable was
added above opus and every resumed session came back claiming fable; the same
accident then quietly defeated the whole server-side default.

so the fallback prefers the platform's default and uses catalogue order only
when that model is not in the provider's list at all — which is every opencode
model, where there is no officer-wide default to prefer.

and a test, because a comment cannot hold a constant mirrored across two
packages. it asserts the client's copy equals the server's, and that the value
names a tier rather than the bare harness. both of the ways this default has
been defeated are now failures a test catches: a drifted mirror, and a stored
harness read as a choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 18:33:38 +00:00
co-authored by Claude Opus 5
parent 2ed47fa65e
commit 4adf96efa4
3 changed files with 82 additions and 1 deletions
@@ -0,0 +1,54 @@
import { describe, expect, test } from 'bun:test';
import { CHAT_DEFAULT_MODEL, DEFAULT_MODEL, isTierlessModel } from './websocket';
import { CHAT_DEFAULT_MODEL as CLIENT_CHAT_DEFAULT_MODEL } from 'officerdev';
/**
* The default model, and the two ways it has already been defeated.
*
* Both failures were silent and both cost real money: every unconfigured surface ran Opus while the code
* said Sonnet. Neither was visible in a diff — one was a constant in another package, the other was a
* value stored in somebody's settings.
*/
describe('the client and server agree on the default', () => {
/*
A comment cannot hold this. `apps/Chat/types.ts` mirrors the server's constant because the picker has
to SHOW what the server would choose — and if it shows something else it also sends it, at which point
the server honours the client and the "default" never applies. That is not hypothetical: the client
fell back to the catalogue's first entry, which was `fable`, and did exactly that.
*/
test('the mirrored constant matches', () => {
expect(CLIENT_CHAT_DEFAULT_MODEL).toBe(CHAT_DEFAULT_MODEL);
});
test('it names a tier rather than the bare harness', () => {
expect(CHAT_DEFAULT_MODEL).toContain('/');
expect(isTierlessModel(CHAT_DEFAULT_MODEL)).toBe(false);
});
});
describe('isTierlessModel', () => {
/*
`claude-code` is a harness, and it is a legitimate value — `decideResume` assumes it when neither side
knows which sidecar a session belongs to. As an answer to "which model", it means "whatever the CLI
picks", which is Opus. Accounts have it stored from before that distinction existed, which is why the
default appeared to do nothing on a machine where it was set.
*/
test('the bare harness is not a model choice', () => {
expect(isTierlessModel(DEFAULT_MODEL)).toBe(true);
expect(isTierlessModel('claude-code')).toBe(true);
});
test('nothing chosen is not a model choice either', () => {
expect(isTierlessModel(null)).toBe(true);
expect(isTierlessModel(undefined)).toBe(true);
expect(isTierlessModel('')).toBe(true);
});
test('a real tier is', () => {
expect(isTierlessModel('claude-code/sonnet')).toBe(false);
expect(isTierlessModel('claude-code/opus[1m]')).toBe(false);
// OpenCode ids carry a provider prefix of their own, so they are tiered by the same test.
expect(isTierlessModel('opencode/some-model')).toBe(false);
});
});
@@ -3,6 +3,7 @@ import { CirclePlay, ClipboardList, FolderOpen } from 'lucide-react';
import { toneText } from '@/components/Data';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { ModelOption } from 'state/useModels';
import { CHAT_DEFAULT_MODEL } from '../types';
import { getProviderDisplayName } from 'state/useModels';
import type { ChatMessage } from '../types';
import { UsagePopover } from './UsagePopover';
@@ -88,7 +89,18 @@ export function ModelSelector({
const activeProvider = availableModels.find((m) => m.id === displayModel)?.provider ?? providers[0];
const providerModels = availableModels.filter((m) => m.provider === activeProvider);
const fallbackModelId = providerModels[0]?.id ?? null;
/*
The platform's default first, and the catalogue's order only as a last resort.
`providerModels[0]` is not a default — it is whichever entry happens to be listed first, and the
comment below records what that cost once already. It cost it again on 2026-08-25: with the server
defaulting to Sonnet, this line selected `fable`, sent it, and the server honoured the choice it was
given. A default the client can silently overrule is not a default.
Falls back to the first entry only when the platform default is not in this provider's list at all —
which is every OpenCode model, where there is no Officer-wide default to prefer.
*/
const fallbackModelId = providerModels.find((m) => m.id === CHAT_DEFAULT_MODEL)?.id ?? providerModels[0]?.id ?? null;
/*
Pick a default only for a chat that has not started.
@@ -1,3 +1,18 @@
/**
* What an unstarted chat offers when nothing else has been chosen.
*
* **Mirrors `CHAT_DEFAULT_MODEL` in `src/servers/api/chat/websocket.ts` and must not drift from it.** Same
* arrangement as `COMMANDER_DEFAULT_MODEL` in `apps/CommandHQ/types.ts`, and for the same reason: the
* server decides, but the picker has to show the same answer or it silently sends a different one.
*
* The composer used to fall back to `providerModels[0]` — the first entry in the catalogue. That is not a
* default, it is an ordering accident, and the file already carried a comment about the day `fable` was
* added above `opus` and every resumed session came back claiming fable. The same accident then defeated
* the server-side default entirely: the client selected fable, SENT fable, and the server honoured what it
* was told.
*/
export const CHAT_DEFAULT_MODEL = 'claude-code/sonnet';
export type Attachment =
| { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean }
| { type: 'image'; filename: string; dataUrl: string; attachmentId: string; loading?: boolean }