Skip to content

fix(chat): write the resolved model to chats.model_id at provision time - #830

Merged
sweetmantech merged 4 commits into
mainfrom
fix/chat-model-provenance
Aug 13, 2026
Merged

fix(chat): write the resolved model to chats.model_id at provision time#830
sweetmantech merged 4 commits into
mainfrom
fix/chat-model-provenance

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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:

  • createSessionWithInitialChat takes a required modelId and writes model_id on the insert (lib/sessions/createSessionWithInitialChat.ts)
  • provisionRunSessionhandleStartChatRun thread the validated model from the request, so POST /api/chat/runs with model: X yields a chat row with model_id = X from the moment the 202 returns
  • Interactive writers (createSessionHandler, createSessionChatHandler) pass the default explicitly
  • One default constant for everything: DEFAULT_MODEL in lib/const.ts, value moonshotai/kimi-k3 (review decision). DEFAULT_RUN_MODEL_ID, the local DEFAULT_MODEL_ID alias, and the interim DEFAULT_CHAT_MODEL_ID are all gone. This also fixes a pre-existing three-way disagreement: the UI's defaultModelId (session-chats endpoint) was gpt-5.4-nano while the run default and the DB column default were haiku-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), createSessionHandler persistence + 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/chat suites: 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

    • createSessionWithInitialChat now requires modelId and writes it; handleStartChatRunprovisionRunSession forwards it to the chat insert.
    • Interactive writers (createSessionHandler, createSessionChatHandler, workflow stream) set model_id explicitly using DEFAULT_MODEL.
    • Deleted per-path defaults (DEFAULT_RUN_MODEL_ID, local constants); all consumers import DEFAULT_MODEL. API route docs reflect the new default.
    • General agent fallback and getSessionChats defaultModelId now use DEFAULT_MODEL.
    • Request validation trims model and falls back when empty/whitespace.
  • Migration and sequencing

    • Callers of createSessionWithInitialChat must pass modelId.
    • Merge this before dropping the chats.model_id column default.

Written for commit bb879e6. Summary will update on new commits.

Review in cubic

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.

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Aug 13, 2026 2:19am

Request Review

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e8ee4aa6-8667-4458-af14-b12f5a16eb61

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb063b and bb879e6.

⛔ Files ignored due to path filters (2)
  • lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sessions/chats/__tests__/getSessionChatsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/chat/handleChatWorkflowStream.ts
  • lib/chat/runs/validateChatRunRequest.ts
  • lib/const.ts
  • lib/sessions/chats/createSessionChatHandler.ts
  • lib/sessions/createSessionHandler.ts
📝 Walkthrough

Walkthrough

The change centralizes the chat model default as moonshotai/kimi-k3, trims model input during run validation, propagates modelId through run provisioning, and records the model on newly created chat rows.

Changes

Chat model default and validation

Layer / File(s) Summary
Shared default and fallback handling
lib/const.ts, lib/chat/runs/validateChatRunRequest.ts, lib/chat/handleChatWorkflowStream.ts, app/api/chat/runs/route.ts
DEFAULT_CHAT_MODEL_ID now defines moonshotai/kimi-k3. Run validation trims model values and applies this default when the value is missing or blank. Workflow fallback logic and API documentation use the shared constant.
Run session model propagation
lib/chat/runs/handleStartChatRun.ts, lib/chat/runs/provisionRunSession.ts
Chat run startup passes modelId through session provisioning to initial chat creation.
Session chat model persistence
lib/sessions/createSessionHandler.ts, lib/sessions/createSessionWithInitialChat.ts, lib/sessions/chats/createSessionChatHandler.ts
Session creation supplies the default model, and new chat inserts store the resolved value in model_id.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: 🔵 Low · up to 9bb06

The change correctly persists the resolved model for new chats, but chats with an empty or whitespace-only stored model ID could still start with an invalid model value instead of the default. This is a bounded follow-up risk and does not otherwise prevent merging.

Possibly related issues

  • recoupable/chat#1956: The change threads the resolved model into chats.model_id during headless and interactive session creation.

Possibly related PRs

  • recoupable/api#809: Both changes modify headless chat-run initialization in lib/chat/runs/handleStartChatRun.ts.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant validateChatRunRequest
  participant handleStartChatRun
  participant provisionRunSession
  participant createSessionWithInitialChat
  participant ChatDatabase

  Client->>validateChatRunRequest: submit model value
  validateChatRunRequest->>handleStartChatRun: return trimmed modelId or default
  handleStartChatRun->>provisionRunSession: pass modelId
  provisionRunSession->>createSessionWithInitialChat: pass modelId
  createSessionWithInitialChat->>ChatDatabase: insert chat with model_id
Loading

Poem

A shared model lights the way,
Blank requests find kimi-k3 today.
Through every run, the value flows,
Into each chat row, it goes.
One clear constant, one steady stream.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed The PR adds no functions, keeps existing file/function ownership, and centralizes the chat default in DEFAULT_CHAT_MODEL_ID while threading modelId through focused session writers.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-model-provenance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread lib/const.ts Outdated
* `chats.model_id` at provision time (chat#1956). Every chat writer persists
* a model explicitly; nothing relies on a database column default.
*/
export const DEFAULT_CHAT_MODEL_ID = "anthropic/claude-haiku-4.5";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI / DRY - Why is this needed when we already have DEFAULT_MODEL?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept, but the comment now answers this inline: DEFAULT_MODEL (gpt-5.4-nano) powers internal utility LLM calls — evals, catalog batch analysis, general agent — while DEFAULT_CHAT_MODEL_ID is the user-facing chat/run default written to chats.model_id. Different domains, different values, both real. Also updated to kimi-k3 per the other threads. Fixed in 9bb063b.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved per your call in the follow-up discussion: DEFAULT_CHAT_MODEL_ID is removed entirely. DEFAULT_MODEL is now the single constant, value moonshotai/kimi-k3, used by every consumer (chat writers, UI defaultModelId via getSessionChatsHandler, generalAgent fallback, evals, catalog batch). Your one-constant instinct also surfaced a pre-existing three-way disagreement — the UI's defaultModelId was nano while the run default and column default were haiku — which this collapse eliminates. Full lib suite 4,383 tests green. Commit: latest on branch.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 13 files

Confidence score: 3/5

  • In lib/sessions/createSessionWithInitialChat.ts, allowing model: "" to persist as an empty model_id can break model provenance and may cause the wrong model behavior downstream when the run is provisioned—normalize empty model IDs to a valid default or reject them before insert/provisioning.
  • In lib/sessions/createSessionWithInitialChat.ts, the new invariant depends on every insertChat caller setting model_id, but scripts/backfill/migrat... is called out as another path; once the DB default is removed, that path could fail or write inconsistent rows—update that caller (or guard the insert contract) before the follow-up schema change.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/sessions/createSessionWithInitialChat.ts">

<violation number="1" location="lib/sessions/createSessionWithInitialChat.ts:60">
P3: This PR centralizes the invariant that "every chat writer persists a model explicitly" and the follow-up DB PR drops the `chats.model_id` column default. There is another `insertChat` caller — `scripts/backfill/migrateRoom.ts` — that constructs a chat row without `model_id`. If that idempotent backfill is re-run after the column default is dropped, it will insert chats with a NULL `model_id`, which breaks the same provenance invariant this change is meant to guarantee. Consider setting `model_id` explicitly in `migrateRoom.ts` (or confirming the DB migration backfills existing and skips new NULL inserts) before dropping the default.</violation>

<violation number="2" location="lib/sessions/createSessionWithInitialChat.ts:60">
P2: When a run request supplies `model: ""`, this line persists an empty `model_id` instead of a usable default. Reject or normalize empty model IDs before provisioning so the persisted provenance and the model sent to the workflow are valid.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Client
    participant API as API Routes
    participant Handler as Session/Chat Handlers
    participant Run as Run Services
    participant DB as Database
    participant Const as lib/const.ts

    Note over Client,DB: Chat Session Creation Flows

    Client->>API: POST /api/chat/sessions
    API->>Handler: createSessionHandler()
    Handler->>Const: DEFAULT_CHAT_MODEL_ID
    Handler->>DB: insertSession() + insertChat() with model_id = default
    DB-->>Handler: Session + Chat row
    Handler-->>API: Session created
    API-->>Client: 200 + session

    Client->>API: POST /api/chat/sessions/:id/chats
    API->>Handler: createSessionChatHandler()
    Handler->>Const: DEFAULT_CHAT_MODEL_ID
    Handler->>DB: insertChat() with model_id = default
    DB-->>Handler: Chat row
    Handler-->>API: Chat created
    API-->>Client: 200 + chat

    Note over Client,DB: Headless Run Flow (POST /api/chat/runs)

    Client->>API: POST /api/chat/runs with model: X
    API->>Run: handleStartChatRun()
    Run->>Run: validateChatRunRequest() - validates model (default = DEFAULT_RUN_MODEL_ID)
    Run->>Run: provisionRunSession() with modelId
    Run->>Run: createSessionWithInitialChat() with modelId
    Run->>DB: insertSession() + insertChat() with model_id = X
    DB-->>Run: Session + Chat row (model_id = X)
    Run-->>API: 202 Accepted
    API-->>Client: 202 + run started

    Note over Run,DB: Model Provenance Guarantee

    Run->>DB: usage_events records actual billed model
    Run->>DB: chats.model_id records resolved model
    DB-->>Run: Both written atomically
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

id: generateUUID(),
session_id: session.id,
title: chatTitle,
model_id: modelId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a run request supplies model: "", this line persists an empty model_id instead of a usable default. Reject or normalize empty model IDs before provisioning so the persisted provenance and the model sent to the workflow are valid.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sessions/createSessionWithInitialChat.ts, line 60:

<comment>When a run request supplies `model: ""`, this line persists an empty `model_id` instead of a usable default. Reject or normalize empty model IDs before provisioning so the persisted provenance and the model sent to the workflow are valid.</comment>

<file context>
@@ -50,7 +53,12 @@ export async function createSessionWithInitialChat({
+    id: generateUUID(),
+    session_id: session.id,
+    title: chatTitle,
+    model_id: modelId,
+  });
   if (!chat) {
</file context>

id: generateUUID(),
session_id: session.id,
title: chatTitle,
model_id: modelId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This PR centralizes the invariant that "every chat writer persists a model explicitly" and the follow-up DB PR drops the chats.model_id column default. There is another insertChat caller — scripts/backfill/migrateRoom.ts — that constructs a chat row without model_id. If that idempotent backfill is re-run after the column default is dropped, it will insert chats with a NULL model_id, which breaks the same provenance invariant this change is meant to guarantee. Consider setting model_id explicitly in migrateRoom.ts (or confirming the DB migration backfills existing and skips new NULL inserts) before dropping the default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sessions/createSessionWithInitialChat.ts, line 60:

<comment>This PR centralizes the invariant that "every chat writer persists a model explicitly" and the follow-up DB PR drops the `chats.model_id` column default. There is another `insertChat` caller — `scripts/backfill/migrateRoom.ts` — that constructs a chat row without `model_id`. If that idempotent backfill is re-run after the column default is dropped, it will insert chats with a NULL `model_id`, which breaks the same provenance invariant this change is meant to guarantee. Consider setting `model_id` explicitly in `migrateRoom.ts` (or confirming the DB migration backfills existing and skips new NULL inserts) before dropping the default.</comment>

<file context>
@@ -50,7 +53,12 @@ export async function createSessionWithInitialChat({
+    id: generateUUID(),
+    session_id: session.id,
+    title: chatTitle,
+    model_id: modelId,
+  });
   if (!chat) {
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern, deliberate non-fix: migrateRoom.ts backfills legacy rooms whose actual model is unknown. After recoupable/database#56, an omitted model_id inserts NULL — which for a legacy room is the honest value ("not recorded"), exactly the semantics the migration's column comment defines. Writing DEFAULT_CHAT_MODEL_ID there would fabricate provenance for historical rows, the precise failure chat#1956 exists to end. The invariant is better stated as: every writer of NEW chats records the model that will run; the backfill records what it knows, which is nothing.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — all Done-when criteria pass

Preview: api-7kc9ueyyz-recoup.vercel.app, confirmed built from this branch's head 39db0eaa (deployment fetched by SHA). Auth via a preview-peppered temp key row (created for the test, deleted after; prod keys 401 on previews by design).

# Case Call Expected chats.model_id Actual (queried in DB immediately after response)
1 The bug case POST /api/chat/runs with model: "xai/grok-4.6" → 202, chat abf8fbce xai/grok-4.6 xai/grok-4.6
2 Omitted model POST /api/chat/runs, no model → 202, chat 80ae0529 anthropic/claude-haiku-4.5 (explicit DEFAULT_RUN_MODEL_ID) anthropic/claude-haiku-4.5
3 Interactive POST /api/sessions → 200, chat 57df0b68 anthropic/claude-haiku-4.5 (explicit DEFAULT_CHAT_MODEL_ID) anthropic/claude-haiku-4.5
4 Auth POST /api/chat/runs, no key 401 401

Case 1 is the discriminator: this morning's reproduction (recoupable/chat#1956) ran four different explicit models and got four haiku-4.5 rows; the same call now records the requested model at provision time. Cases 2–3 read the same value the old column default produced — the unit tests are what pin that these are now explicit writes (insert row asserted to carry model_id), and recoupable/database#56 makes the distinction observable by dropping the default.

Contract check: request/response bodies unchanged (202 {runId, chatId, sessionId}, Location header present) — matches the documented ChatGenerateAcceptedResponse; no docs drift introduced.

Note: cases 1–2 started real (tiny-prompt) workflow runs on the test account; they complete on their own and cost a few credits.

🤖 Generated with Claude Code

sweetmantech and others added 2 commits August 12, 2026 17:52
Headless runs (POST /api/chat/runs) resolved the model correctly and
threaded it into the workflow, but the chat insert dropped it one call
before the row was written — the chats.model_id column default then
fabricated 'anthropic/claude-haiku-4.5' for every headless run
(recoupable/chat#1956).

- createSessionWithInitialChat now requires modelId and writes it on the
  chat insert; provisionRunSession threads it; handleStartChatRun passes
  the validated modelId.
- Interactive writers (createSessionHandler, createSessionChatHandler)
  set the default explicitly, so no writer relies on the column default
  and the database default can be dropped (recoupable/database follow-up).
- Single source for the default: DEFAULT_CHAT_MODEL_ID in lib/const;
  DEFAULT_RUN_MODEL_ID and handleChatWorkflowStream's local constant now
  alias it instead of repeating the string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread lib/chat/runs/validateChatRunRequest.ts Outdated
/** Default model for headless generation when the caller omits `model`. */
export const DEFAULT_RUN_MODEL_ID = "anthropic/claude-haiku-4.5";
/** Default model for headless generation when the caller omits `model` (alias of the shared default). */
export const DEFAULT_RUN_MODEL_ID = DEFAULT_CHAT_MODEL_ID;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY

  • actual: wrapper DEFAULT_RUN_MODEL_ID alias
  • required: delete DEFAULT_RUN_MODEL_ID and replace with DEFAULT_CHAT_MODEL_ID.
  • make sure DEFAULT_CHAT_MODEL_ID is kimi k3.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9bb063b: DEFAULT_RUN_MODEL_ID deleted (it had no importers outside this file), validateChatRunRequest uses DEFAULT_CHAT_MODEL_ID directly, and the value is moonshotai/kimi-k3. Bonus from the same pass: model: "" or whitespace now normalizes to the default (|| not ??) instead of persisting an empty model_id — cubic's P2.

Comment thread lib/chat/handleChatWorkflowStream.ts Outdated
import generateUUID from "@/lib/uuid/generateUUID";

const DEFAULT_MODEL_ID = "anthropic/claude-haiku-4.5";
const DEFAULT_MODEL_ID = DEFAULT_CHAT_MODEL_ID;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY

  • actual: wrapper DEFAULT_RUN_MODEL_ID alias
  • required: delete DEFAULT_RUN_MODEL_ID and replace with DEFAULT_CHAT_MODEL_ID.
  • make sure DEFAULT_CHAT_MODEL_ID is kimi k3.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9bb063b: the local DEFAULT_MODEL_ID alias is deleted; the null-fallback at the use site reads chat.model_id ?? DEFAULT_CHAT_MODEL_ID, and the constant is moonshotai/kimi-k3. The pinned test ("falls back to the default model when chat.model_id is null") was flipped to kimi-k3 first and confirmed RED before the change.

Comment thread lib/const.ts Outdated
@@ -10,6 +10,13 @@ export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Addr
export const PAYMASTER_URL = `https://api.developer.coinbase.com/rpc/v1/base/${process.env.PAYMASTER_KEY}`;
export const IMAGE_GENERATE_PRICE = "0.15";
export const DEFAULT_MODEL = "openai/gpt-5.4-nano";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update to use kimi k3

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9bb063b — moonshotai/kimi-k3, with the A/B rationale in the doc comment. Note this comment sits on DEFAULT_MODEL (gpt-5.4-nano, internal utility calls); I read the intent as the chat default and changed DEFAULT_CHAT_MODEL_ID. If you also want the utility-call model changed, say so — left untouched deliberately.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Re-verified on the current head after rebase

The earlier verification ran against pre-rebase head 39db0eaa. Re-ran the discriminating case against the current head 24d5188d (rebase onto bd58873e + a formatting-only commit), preview api-2e3l57bzi-recoup.vercel.app confirmed built from that SHA:

POST /api/chat/runs with model: "xai/grok-4.6" → 202, chat 69a16cdachats.model_id = 'xai/grok-4.6' ✅ (temp preview credential deleted after the test)

All checks green on this head (format/lint/test/Vercel). Ready to merge; recoupable/database#56 follows after this is live.

🤖 Generated with Claude Code

…; normalize empty model

Review feedback on #830:
- DEFAULT_RUN_MODEL_ID and the local DEFAULT_MODEL_ID alias are deleted;
  every consumer imports DEFAULT_CHAT_MODEL_ID directly (DRY).
- DEFAULT_CHAT_MODEL_ID is moonshotai/kimi-k3 — best recall and lowest
  cost in the 2026-08-12 4-model A/B on a production roster-brief task.
  Distinct from DEFAULT_MODEL (internal utility LLM calls).
- cubic P2: model: "" / whitespace now normalizes to the default instead
  of persisting "" as provenance and sending "" to the workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Verification on the review-fix head 9bb063bb

All review feedback is in this head (aliases deleted, default = kimi-k3, empty-model normalization). Live results against preview api-ggi2l1pls-recoup.vercel.app, confirmed built from 9bb063bb; temp credential deleted after:

Case POST /api/chat/runs body chats.model_id observed
Explicit model: "xai/grok-4.6" xai/grok-4.6
Omitted (no model) moonshotai/kimi-k3the new default
Empty string model: "" moonshotai/kimi-k3 — normalized, not "" (cubic P2)

CI green on this head. Merge order unchanged: this PR, then recoupable/database#56.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/chat/runs/provisionRunSession.ts (1)

56-61: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Set model_id explicitly in the backfill writer. scripts/backfill/migrateRoom.ts inserts chats without model_id. Since chats.model_id is nullable, removing the default creates migrated chats without model provenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/chat/runs/provisionRunSession.ts` around lines 56 - 61, Update the chat
insert logic in scripts/backfill/migrateRoom.ts to set model_id explicitly for
every migrated chat, using the appropriate model identifier available in the
backfill data or established default. Preserve the existing migration behavior
while ensuring migrated records retain model provenance.
🧹 Nitpick comments (2)
lib/chat/runs/validateChatRunRequest.ts (1)

79-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split the request validator into focused steps.

validateChatRunRequest spans 47 lines. It parses JSON, validates the schema, checks prompt/message exclusivity, authenticates, and resolves the model. Extract these responsibilities into focused helpers.

As per coding guidelines, “Flag functions longer than 20 lines or classes with >200 lines.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/chat/runs/validateChatRunRequest.ts` around lines 79 - 88, Refactor
validateChatRunRequest into focused helpers for JSON parsing, schema validation,
prompt/message exclusivity checks, authentication, and model resolution. Keep
validateChatRunRequest as the orchestration entry point, preserve the existing
validation behavior, and retain trimmedModel’s fallback to DEFAULT_CHAT_MODEL_ID
for empty or whitespace-only models.

Source: Coding guidelines

lib/chat/runs/provisionRunSession.ts (1)

40-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce oversized orchestration functions across the changed flow.

The changed functions combine multiple responsibilities and exceed the repository’s function-length guidance. Extract focused helpers while preserving the explicit model propagation contract.

  • lib/chat/runs/provisionRunSession.ts#L40-L53: separate repository, session, sandbox, and skill provisioning.
  • lib/chat/handleChatWorkflowStream.ts#L92-L92: separate ownership, stream-slot, sandbox, workflow, and response phases.
  • lib/chat/runs/handleStartChatRun.ts#L45-L50: separate resource, key, workflow, and response handling.
  • lib/chat/runs/validateChatRunRequest.ts#L79-L88: separate parsing, schema validation, authentication, and model resolution.
  • lib/sessions/createSessionWithInitialChat.ts#L38-L46: separate repository, session, chat, and rollback logic.
  • lib/sessions/createSessionHandler.ts#L47-L53: separate title resolution, persistence, error mapping, and response construction.
  • lib/sessions/chats/createSessionChatHandler.ts#L53-L58: separate idempotent lookup, conflict handling, insertion, and response mapping.

As per coding guidelines, “Flag functions longer than 20 lines or classes with >200 lines.”
As per path instructions, lib/**/*.ts functions must follow single responsibility and stay under 50 lines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/chat/runs/provisionRunSession.ts` around lines 40 - 53, Refactor the
oversized orchestration functions into focused helpers while preserving behavior
and explicit modelId propagation. In lib/chat/runs/provisionRunSession.ts lines
40-53, extract repository, session, sandbox, and skill provisioning; in
lib/chat/handleChatWorkflowStream.ts line 92, extract ownership, stream-slot,
sandbox, workflow, and response phases; in lib/chat/runs/handleStartChatRun.ts
lines 45-50, extract resource, key, workflow, and response handling; in
lib/chat/runs/validateChatRunRequest.ts lines 79-88, extract parsing, schema
validation, authentication, and model resolution; in
lib/sessions/createSessionWithInitialChat.ts lines 38-46, extract repository,
session, chat, and rollback logic; in lib/sessions/createSessionHandler.ts lines
47-53, extract title resolution, persistence, error mapping, and response
construction; and in lib/sessions/chats/createSessionChatHandler.ts lines 53-58,
extract idempotent lookup, conflict handling, insertion, and response mapping.
Keep each function single-purpose and within the repository’s length guidance.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/chat/handleChatWorkflowStream.ts`:
- Line 92: Update the modelId assignment near buildRunAgentInput to trim
persisted chat.model_id values and fall back to DEFAULT_CHAT_MODEL_ID when the
result is empty or whitespace-only, while preserving valid IDs. Add regression
coverage for both blank and whitespace-only model IDs.

---

Outside diff comments:
In `@lib/chat/runs/provisionRunSession.ts`:
- Around line 56-61: Update the chat insert logic in
scripts/backfill/migrateRoom.ts to set model_id explicitly for every migrated
chat, using the appropriate model identifier available in the backfill data or
established default. Preserve the existing migration behavior while ensuring
migrated records retain model provenance.

---

Nitpick comments:
In `@lib/chat/runs/provisionRunSession.ts`:
- Around line 40-53: Refactor the oversized orchestration functions into focused
helpers while preserving behavior and explicit modelId propagation. In
lib/chat/runs/provisionRunSession.ts lines 40-53, extract repository, session,
sandbox, and skill provisioning; in lib/chat/handleChatWorkflowStream.ts line
92, extract ownership, stream-slot, sandbox, workflow, and response phases; in
lib/chat/runs/handleStartChatRun.ts lines 45-50, extract resource, key,
workflow, and response handling; in lib/chat/runs/validateChatRunRequest.ts
lines 79-88, extract parsing, schema validation, authentication, and model
resolution; in lib/sessions/createSessionWithInitialChat.ts lines 38-46, extract
repository, session, chat, and rollback logic; in
lib/sessions/createSessionHandler.ts lines 47-53, extract title resolution,
persistence, error mapping, and response construction; and in
lib/sessions/chats/createSessionChatHandler.ts lines 53-58, extract idempotent
lookup, conflict handling, insertion, and response mapping. Keep each function
single-purpose and within the repository’s length guidance.

In `@lib/chat/runs/validateChatRunRequest.ts`:
- Around line 79-88: Refactor validateChatRunRequest into focused helpers for
JSON parsing, schema validation, prompt/message exclusivity checks,
authentication, and model resolution. Keep validateChatRunRequest as the
orchestration entry point, preserve the existing validation behavior, and retain
trimmedModel’s fallback to DEFAULT_CHAT_MODEL_ID for empty or whitespace-only
models.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f5cfd9f0-7aef-4b31-b652-d363fb8b60fc

📥 Commits

Reviewing files that changed from the base of the PR and between bd58873 and 9bb063b.

⛔ Files ignored due to path filters (7)
  • lib/chat/__tests__/handleChatWorkflowStream.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/runs/__tests__/handleStartChatRun.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/runs/__tests__/provisionRunSession.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/runs/__tests__/validateChatRunRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sessions/__tests__/createSessionHandler.persistence.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sessions/__tests__/createSessionWithInitialChat.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sessions/chats/__tests__/createSessionChatHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (9)
  • app/api/chat/runs/route.ts
  • lib/chat/handleChatWorkflowStream.ts
  • lib/chat/runs/handleStartChatRun.ts
  • lib/chat/runs/provisionRunSession.ts
  • lib/chat/runs/validateChatRunRequest.ts
  • lib/const.ts
  • lib/sessions/chats/createSessionChatHandler.ts
  • lib/sessions/createSessionHandler.ts
  • lib/sessions/createSessionWithInitialChat.ts

Comment thread lib/chat/handleChatWorkflowStream.ts Outdated
void persistLatestUserMessage(validated.chatId, validated.messages as never);

const modelId = chat.model_id ?? DEFAULT_MODEL_ID;
const modelId = chat.model_id ?? DEFAULT_CHAT_MODEL_ID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target outline ---'
ast-grep outline lib/chat/handleChatWorkflowStream.ts || true
printf '%s\n' '--- target lines ---'
sed -n '1,180p' lib/chat/handleChatWorkflowStream.ts
printf '%s\n' '--- relevant symbols ---'
rg -n --glob '!node_modules' --glob '!dist' 'buildRunAgentInput|model_id|DEFAULT_CHAT_MODEL_ID' .

Repository: recoupable/api

Length of output: 15824


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- relevant outlines ---'
for f in \
  lib/chat/buildRunAgentInput.ts \
  lib/chat/runs/validateChatRunRequest.ts \
  lib/sessions/chats/patchSessionChatHandler.ts \
  lib/sessions/chats/createSessionChatHandler.ts \
  lib/sessions/createSessionWithInitialChat.ts \
  lib/supabase/chats/updateChat.ts \
  lib/chat/__tests__/handleChatWorkflowStream.test.ts
do
  if [ -f "$f" ]; then
    ast-grep outline "$f" || true
  fi
done

printf '%s\n' '--- builder ---'
sed -n '1,180p' lib/chat/buildRunAgentInput.ts
printf '%s\n' '--- run validation ---'
sed -n '1,150p' lib/chat/runs/validateChatRunRequest.ts
printf '%s\n' '--- patch handler and updater ---'
sed -n '1,120p' lib/sessions/chats/patchSessionChatHandler.ts
sed -n '1,140p' lib/supabase/chats/updateChat.ts
printf '%s\n' '--- chat creation paths ---'
sed -n '1,120p' lib/sessions/chats/createSessionChatHandler.ts
sed -n '1,120p' lib/sessions/createSessionWithInitialChat.ts
printf '%s\n' '--- focused workflow tests ---'
sed -n '240,330p' lib/chat/__tests__/handleChatWorkflowStream.test.ts
printf '%s\n' '--- modelId validation/write references ---'
rg -n --glob '!node_modules' --glob '!dist' 'modelId|model_id|CREATE TABLE.*chats|CHECK.*model' lib supabase migrations db . 2>/dev/null | head -n 300

Repository: recoupable/api

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- patch validation ---'
sed -n '1,100p' lib/sessions/chats/validatePatchSessionChatRequest.ts
printf '%s\n' '--- insertChat/updateChat call sites ---'
rg -n --glob '*.ts' --glob '*.tsx' 'insertChat\(|updateChat\(' lib app/api
printf '%s\n' '--- schema and migration files ---'
fd -t f -i '(migration|schema|sql)$' . | head -n 200
rg -n -i --glob '*.sql' 'model_id|create table.*chats|check' . | head -n 200 || true
printf '%s\n' '--- runtime fallback probe ---'
node - <<'JS'
const values = [null, undefined, "", "   ", "  openai/gpt-5-mini  "];
for (const value of values) {
  const nullish = value ?? "moonshotai/kimi-k3";
  const normalized = value?.trim() || "moonshotai/kimi-k3";
  console.log(JSON.stringify(value), JSON.stringify(nullish), JSON.stringify(normalized));
}
JS

Repository: recoupable/api

Length of output: 5082


Normalize blank persisted model IDs before starting the workflow.

?? leaves empty and whitespace-only IDs unchanged. buildRunAgentInput forwards them to the workflow. Use chat.model_id?.trim() || DEFAULT_CHAT_MODEL_ID and add regression coverage for both cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/chat/handleChatWorkflowStream.ts` at line 92, Update the modelId
assignment near buildRunAgentInput to trim persisted chat.model_id values and
fall back to DEFAULT_CHAT_MODEL_ID when the result is empty or whitespace-only,
while preserving valid IDs. Add regression coverage for both blank and
whitespace-only model IDs.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Confidence score: 5/5

  • In lib/chat/handleChatWorkflowStream.ts, using ?? for the model fallback can let an empty chat.model_id pass through, creating inconsistent behavior with validateChatRunRequest and risking workflow calls with an invalid model ID; align this path to the same empty/whitespace normalization (|| or equivalent trim check) to de-risk regressions.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/chat/handleChatWorkflowStream.ts">

<violation number="1" location="lib/chat/handleChatWorkflowStream.ts:92">
P3: This fallback uses `??`, so an empty-string `chat.model_id` is kept and sent to the workflow, while the headless path this PR touches (validateChatRunRequest) deliberately uses `||` to normalize empty/whitespace models to the default. For interactive chats where `chat.model_id` is already an empty string (as opposed to null), the empty id reaches the workflow and is not replaced.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread lib/chat/handleChatWorkflowStream.ts Outdated
void persistLatestUserMessage(validated.chatId, validated.messages as never);

const modelId = chat.model_id ?? DEFAULT_MODEL_ID;
const modelId = chat.model_id ?? DEFAULT_CHAT_MODEL_ID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This fallback uses ??, so an empty-string chat.model_id is kept and sent to the workflow, while the headless path this PR touches (validateChatRunRequest) deliberately uses || to normalize empty/whitespace models to the default. For interactive chats where chat.model_id is already an empty string (as opposed to null), the empty id reaches the workflow and is not replaced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/handleChatWorkflowStream.ts, line 92:

<comment>This fallback uses `??`, so an empty-string `chat.model_id` is kept and sent to the workflow, while the headless path this PR touches (validateChatRunRequest) deliberately uses `||` to normalize empty/whitespace models to the default. For interactive chats where `chat.model_id` is already an empty string (as opposed to null), the empty id reaches the workflow and is not replaced.</comment>

<file context>
@@ -91,7 +89,7 @@ export async function handleChatWorkflowStream(request: NextRequest): Promise<Re
   void persistLatestUserMessage(validated.chatId, validated.messages as never);
 
-  const modelId = chat.model_id ?? DEFAULT_MODEL_ID;
+  const modelId = chat.model_id ?? DEFAULT_CHAT_MODEL_ID;
 
   // Connect the sandbox up-front so we can (a) read the real working
</file context>

Review decision on #830: no separate DEFAULT_CHAT_MODEL_ID. DEFAULT_MODEL
(now moonshotai/kimi-k3) is the single default everywhere a caller picks
none: interactive chats, headless runs (persisted to chats.model_id),
the session-chats endpoint's defaultModelId exposed to the UI, the
general agent's fallback (incl. inbound email replies), evals, and
catalog batch analysis.

This closes a previously unnoticed three-way disagreement: the UI's
defaultModelId was gpt-5.4-nano (DEFAULT_MODEL), the chats column
default was haiku-4.5, and the run default was haiku-4.5 — three
different answers to "what model do I get by default".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 7 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — head bb879e63 (single DEFAULT_MODEL = kimi-k3)

Preview: api-hpxloddcd-recoup.vercel.app, confirmed built from bb879e63 (deployment fetched by SHA). Auth: fresh Privy JWT (Bearer), expired credential not retained.

# Case Call Expected Observed (DB-verified)
1 Explicit model POST /api/chat/runs model: "xai/grok-4.6" → chat 6ad77729 xai/grok-4.6 xai/grok-4.6
2 Omitted model POST /api/chat/runs, no model → chat 26ab66c4 moonshotai/kimi-k3 moonshotai/kimi-k3
3 Empty model POST /api/chat/runs model: "" → chat 616dfb98 normalized to moonshotai/kimi-k3, never "" moonshotai/kimi-k3
4 Interactive create POST /api/sessions → chat be507dd1 moonshotai/kimi-k3 written explicitly moonshotai/kimi-k3
5 UI default surface GET /api/sessions/{id}/chats defaultModelId: "moonshotai/kimi-k3" moonshotai/kimi-k3 (was gpt-5.4-nano before this head)
6 Auth POST /api/chat/runs, no credential 401 401

Case 5 is the new surface from the one-constant collapse: the model default the web app is told now agrees with what runs and what's persisted — previously three different values (nano / haiku / haiku). Rows verified directly in the shared database immediately after each response.

CI green on this head. Merge order unchanged: this PR → recoupable/database#56.

🤖 Generated with Claude Code

@sweetmantech
sweetmantech merged commit fd18f18 into main Aug 13, 2026
6 checks passed
sweetmantech added a commit to recoupable/database that referenced this pull request Aug 13, 2026
…956) (#56)

The default fabricated provenance: writers that omitted model_id got a
plausible real model id instead of an honest NULL. After recoupable/api#830
every writer sets the field explicitly, so the default has zero remaining
writers. No backfill — pre-fix rows can't be classified retroactively;
fence by date, usage_events stays the historical record.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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