fix(chat): write the resolved model to chats.model_id at provision time
Closes the api row of recoupable/chat#1956 (model provenance for headless/task runs).
What
Every chat writer now records the model explicitly on the chat row:
createSessionWithInitialChattakes a requiredmodelIdand writesmodel_idon the insert (lib/sessions/createSessionWithInitialChat.ts)provisionRunSession→handleStartChatRunthread the validated model from the request, soPOST /api/chat/runswithmodel: Xyields a chat row withmodel_id = Xfrom the moment the 202 returns- Interactive writers (
createSessionHandler,createSessionChatHandler) pass the default explicitly - One default constant for everything:
DEFAULT_MODELinlib/const.ts, valuemoonshotai/kimi-k3(review decision).DEFAULT_RUN_MODEL_ID, the localDEFAULT_MODEL_IDalias, and the interimDEFAULT_CHAT_MODEL_IDare all gone. This also fixes a pre-existing three-way disagreement: the UI'sdefaultModelId(session-chats endpoint) wasgpt-5.4-nanowhile the run default and the DB column default werehaiku-4.5
Why
Reproduced 2026-08-12 (table in the issue): four POST /api/chat/runs calls with four explicit models — all four chat rows read anthropic/claude-haiku-4.5 (the column default) while usage_events billed the real models. The interactive path reads chats.model_id as its source of truth (chat#1767), so the column being wrong for headless runs made model debugging three-sources-disagree work.
Merge sequencing
This PR first, then the database PR dropping the chats.model_id column default (hard dependency: dropping the default before every writer is explicit would leave interactive chats with NULL). No docs PR — no response schema documents the resolved model today; checked in the issue.
Tests
- TDD, all five units RED before GREEN:
createSessionWithInitialChat(writes caller's modelId),provisionRunSession(forwards),handleStartChatRun(passes validated model),createSessionHandlerpersistence +createSessionChatHandler(explicit default pinned to the literal"anthropic/claude-haiku-4.5", not the constant, so a broken import can't false-green) - Full
lib/sessions+lib/chatsuites: 642 passed tsc --noEmit: 202 pre-existing errors on clean main, 202 with this diff — zero introduced. Lint clean on touched files.
Preview verification to follow as a PR comment.
🤖 Generated with Claude Code
Summary by cubic
Writes the resolved chat model to chats.model_id at provision time and unifies the default to DEFAULT_MODEL = "moonshotai/kimi-k3". Previously headless runs used the column default (anthropic/claude-haiku-4.5) and different paths had divergent defaults; now every path persists the requested model or the unified default, and empty/whitespace model values normalize to the default.
-
Review notes
createSessionWithInitialChatnow requiresmodelIdand writes it;handleStartChatRun→provisionRunSessionforwards it to the chat insert.- Interactive writers (
createSessionHandler,createSessionChatHandler, workflow stream) setmodel_idexplicitly usingDEFAULT_MODEL. - Deleted per-path defaults (
DEFAULT_RUN_MODEL_ID, local constants); all consumers importDEFAULT_MODEL. API route docs reflect the new default. - General agent fallback and
getSessionChatsdefaultModelIdnow useDEFAULT_MODEL. - Request validation trims
modeland falls back when empty/whitespace.
-
Migration and sequencing
- Callers of
createSessionWithInitialChatmust passmodelId. - Merge this before dropping the
chats.model_idcolumn default.
- Callers of
Summary by CodeRabbit
- New Features
- New chats and chat runs now consistently record and use the selected AI model.
- Blank or unspecified model selections automatically use
moonshotai/kimi-k3.
- Bug Fixes
- Fixed cases where a requested model could be lost during chat run setup.
- Ensured initial session chats use the configured model instead of relying on an implicit default.
- Documentation
- Updated the chat runs API documentation to reflect the new default model.