Skip to content

feat(sessions): accept a per-session Claude model on POST /api/sessions - #514

Open
irisitymichaelgrundberg wants to merge 2 commits into
Ark0N:masterfrom
irisitymichaelgrundberg:feat/claude-session-model
Open

irisitymichaelgrundberg wants to merge 2 commits into
Ark0N:masterfrom
irisitymichaelgrundberg:feat/claude-session-model

Conversation

@irisitymichaelgrundberg

@irisitymichaelgrundberg irisitymichaelgrundberg commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part of #513 (the Claude half).

A caller can now start one Claude session on a chosen model without writing anything to disk. POST /api/sessions takes an optional model, and the session launches with claude --model <id>.

Why

The only per-request way to pick Claude's model has been modelOverride, which the route writes into <workingDir>/.claude/settings.local.json. That model outlives the session, and every later claude run in the directory starts on it, including runs outside Codeman. A caller that wants one session on Fable and the next on Sonnet has no way to say so without editing the user's checkout.

What it does

  • model in CreateSessionSchema. It is optional, at most 100 characters, and uses the same character set as the registry's model-claude pattern. A value the schema accepts therefore can't be refused at launch. An empty string means no per-session model, as it does for modelOverride.
  • The route prefers it. When the CLI's model source is claude-settings-file, the route now takes body.model first and falls back to the app-wide default model as before. The session hands it to the existing --model slot in Claude's launch template.
  • modelOverride is unchanged. A caller who wants the model written into the case still sends that field. If a caller sends both, the file gets modelOverride and this session runs on model.
  • Claude only. Every other CLI takes its model in its own config object, such as codexConfig.model, so the route refuses a top-level model for them with INVALID_INPUT. Otherwise it would be dropped without a word. The check keys on the registry's model-source capability, so the branch adds no CLI id check.
  • It survives recovery. SessionState now carries model, the model the session launched with. Mux recovery in server.ts and reboot restore both pass it back, so a recovered session relaunches on the same --model and not on the account default. Sessions launched on the app-wide default model keep that one across a restart too.

CLAUDE.md used to say model choice goes through settings.local.json, "NOT --model". It now describes both routes: modelOverride writes a lasting default into the case, and model sets one session's launch flag. The endpoint reference in skills/codeman/reference/endpoints.md and its plugin mirror list model among the create fields. That paragraph's two line references had gone stale, so they now name the handler and sessionCapacityMessage().

Testing

  • npm test passes, as do typecheck, lint and format.
  • The new test/routes/session-routes-claude-model.test.ts creates sessions through app.inject(), starts each one, and reads the model the session hands the mux. It covers:
    • a model the caller names;
    • that model winning over the app-wide default;
    • the default still applying when the caller names none, or names an empty string;
    • model and modelOverride sent together, where the launch gets model and the case file gets modelOverride;
    • a model with shell characters failing the request with 400;
    • a model sent with codex refused before any session is made.
  • The new test/session-model-recovery.test.ts checks that toState() carries the model and that a session rebuilt from that state launches on it. restoreMuxSessions() can't be reached under vitest, so a source check pins both recovery constructors, the way test/remote-wake.test.ts pins its wiring.
  • test/cli-registry-spawn-golden.test.ts pins how a model renders, including one that opens with a dash: it lands as --model "--dangerously-skip-permissions", and Claude's option parser, Commander, takes the word after --model as its value.
  • Each new test fails without the change it covers.

No changeset, following the repository's convention for contributors.

🤖 Generated with Claude Code

POST /api/sessions takes an optional `model`, and a Claude session
launches with `claude --model <id>`. It wins over the app-wide default
model and writes nothing to disk, unlike `modelOverride`, which stays as
it is and still writes the case's .claude/settings.local.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… claude

SessionState now carries the model a session launched with, and both
recovery constructors (mux recovery and reboot restore) pass it back, so a
recovered session relaunches on the same --model rather than the account
default. A top-level `model` sent with any other CLI is refused, since
those take their model in their own config object, and an empty string
means no per-session model, as it does for modelOverride.

CLAUDE.md now describes both routes for a Claude model. The tests pin
which of `model` and `modelOverride` reaches the launch and which the
case file, and that a model opening with a dash renders as --model's value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irisitymichaelgrundberg
irisitymichaelgrundberg marked this pull request as ready for review October 2, 2026 04:49
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.

1 participant