Devin's model picker shows the slug twice: every family label falls back to its id
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 21m
- Merged PRs (30d)
- 41
Description
Every entry in Devin's model picker prints its slug twice — once in bold where the human-readable name should be, once in grey underneath. `devinFamilies()` never finds a label, so all 48 fall back to the id.
This was written blind on purpose: #114 was built while `server.codeium.com` refused every request on the dev machine (that turned out to be a WSL DNS fault, not Devin), so `parseFamilies` guesses field names. The id guess was right. The label guess was not.
## The evidence
`src/daemon/devin-models.ts:102` looks for the label under three names:
```ts
const label = firstString(entry, ["label", "name", "title"]) ?? id;
```
The real payload has none of them. `devin models list --format json` (CLI 3000.10.23, commit deb81600) gives each family exactly these keys:
```
family keys: ['aliases', 'family_label', 'family_uid', 'slug', 'variants']
variant keys: ['cost_summary', 'cost_tier', 'is_beta', 'is_new', 'label',
'max_context_tokens', 'max_output_tokens', 'model_uid']
```
Note `label` exists on the *variant*, not the family. The family's name is `family_label`.
Running the shipped function against the live command:
```
$ npx tsx -e 'import {devinFamilies} from "./src/daemon/devin-models.ts"; ...'
count: 48
id=claude-opus-5 label=claude-opus-5
id=claude-fable-5.1 label=claude-fable-5.1
id=claude-sonnet-5 label=claude-sonnet-5
id=gemini-3.8-flash label=gemini-3.8-flash
id=gpt-5.6-sol label=gpt-5.6-sol
labels identical to their id: 48 / 48
```
`src/client/components/ModelDialog.tsx:480-481` renders both fields:
```tsx
{family.label}
{family.id}
```
so each of the 48 buttons currently reads `claude-opus-5` / `claude-opus-5`. Expected: `Claude Opus 5` / `claude-opus-5`.
## The id is already correct — do not "fix" it
`src/daemon/devin-models.ts:100` picks `["family", "slug", "id", "name"]`, which lands on `slug`. That is right, and it matters because `slug` and `family_uid` disagree for any family with a dot in its version:
| slug | family_uid |
|---|---|
| `claude-opus-5` | `claude-opus-5` |
| `claude-fable-5.1` | `claude-fable-5-1` |
| `gemini-3.8-flash` | `gemini-3-8-flash` |
The CLI's own human output names families by slug — `Claude Fable 5.1 (claude-fable-5.1)` — so slug is the identifier a person and the CLI both recognise. Changing the id to `family_uid` would silently change every stored `devin:` model id on existing sessions.
## Acceptance criteria
- [ ] `devinFamilies()` returns `label: "Claude Opus 5"` for `id: "claude-opus-5"` against the real payload shape.
- [ ] A unit test covers a fixture with `family_label`/`family_uid`/`slug`/`aliases`/`variants` keys, taken from the shape above, asserting both id and label.
- [ ] The existing tolerant fallbacks still work: an entry with only `label`, or with only an id and no label at all, behaves as it does today (label falls back to id).
- [ ] The id stays `slug`. A test pins `claude-fable-5.1`, not `claude-fable-5-1`.
- [ ] The picker shows a distinct name and slug on each of the 48 buttons. Screenshot on the issue.
## Out of scope
- **The roster/chip label.** `src/shared/models.ts:133` renders `Devin: claude-opus-5` from the id alone. It is in `shared/`, has no access to the fetched family list, and fixing it means deciding where that list lives client-side. File it separately if you think it is worth doing; do not fold it in here.
- Variant/effort selection. The picker offers families only; that was #114's decision.
- The empty-list fallback path — that is #133, and it touches the same file. Coordinate rather than collide.
- Anything about `server.codeium.com` reachability. That was a WSL DNS fault and is not a code problem.
## Verification
```
pnpm typecheck
pnpm test
pnpm build
```
Then, with a daemon running on the new build:
```
curl -H "x-bench-token: $(cat ~/.bench/token)" http://127.0.0.1:7420/api/devin/models \
| python3 -c "import sys,json; [print(f['id'],'->',f['label']) for f in json.load(sys.stdin)['families'][:5]]"
```
Expected: `claude-opus-5 -> Claude Opus 5`. A green build will not catch this — the current tests pass with the bug in, because no fixture carries the real field names.
## Related
- #114 — the picker this came from; the ticket explicitly flagged that the schema was unobserved.
- #133 — same file, same function's failure path.
Contributor guide
Research direction
Start in src/daemon/devin-models.ts at devinFamilies() and compare its parsing with the documented real payload shape; inspect src/client/components/ModelDialog.tsx:480-481 for the rendered fields. Add a fixture-based unit test for family_label, slug, and fallback cases, then run pnpm typecheck, pnpm test, and pnpm build; done means the API returns distinct human labels and slug ids, including claude-fable-5.1.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100