Skip to content

fix(gateway): preserve billed costs and actual routes - #15366

Merged
marius-kilocode merged 3 commits into
v2from
gateway-usage-parity
Oct 22, 2026
Merged

marius-kilocode merged 3 commits into
v2from
gateway-usage-parity

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

What Problem This Solves

Gateway responses lost reported billing and actual-model metadata before v2 settled a step. Auto could therefore settle at its catalog price, and earlier responses could not retain different routes after restart.

This change carries successful Gateway metadata through the existing usage path, settles reported costs, and keeps the requested model separate from the actual route. It does not add an event type, table, projection, ledger, or accounting service.

Why This Change Was Made

Behavior was compared against freshly fetched main 9e4e3a43c80, from v2 981607bf544. No main code was merged into v2.

  • Main's KiloSession.providerCost prefers Kilo OpenRouter upstream inference cost, then regular OpenRouter cost, raw native upstream inference cost, Gateway market cost, and finally the selected model's token prices. A valid zero stops fallback. Finite numeric strings are accepted; malformed values are unavailable; finite negative charges clamp to zero at settlement. Kilo does not add the routing fee to upstream cost, including BYOK responses.
  • Main's Gateway middleware carries successful route attempts and native Messages/Responses metadata. The v2 success adapter now preserves those facts through each dialect's raw usage, including ordinary JSON usage responses.
  • Main's processor keeps selected identity and actual identity separate. Existing v2 Step.Ended/Step.Failed.native now carry only native.kilo.{routedModelID,vercelID,generationID,responseID} when supplied. Existing cost and token fields remain authoritative. Raw Gateway envelopes and duplicated token counts are not persisted.
  • Main's TUI usage groups actual routes. Main CLI stats still use selected identity; this PR intentionally implements the required actual-route grouping in v2 stats. Requested Auto selection and non-Kilo accounting remain unchanged.
  • Main titles consume text without recording spend. V2 already records title spend, so its existing title and native/streamed compaction paths now use the same reported-cost precedence instead of losing provider charges.

User Impact

  • Auto and BYOK responses use reported provider charges instead of a misleading zero catalog estimate.
  • Different responses, reasoning groups, and tool-continuation steps retain their own actual routes after restart and compaction.
  • Auto footers retain the requested model and append the actual route. Per-step token rows and collapsed groups use each assistant's durable metadata, not a session's latest snapshot.
  • Statistics group actual Kilo routes, with the original requested-model fallback. Existing child-session aggregation counts each session once.
  • The latest-only session.metadata RPC is unchanged and is not durable history. No Protocol or HttpApi surface changed, so client regeneration was not needed.

Privacy-safe crops from the real production TUI after reopening the same isolated session:

Earlier Auto response retains route alpha

Later Auto response retains route beta after restart

Evidence

All final checks used the repository-pinned Bun 1.4.2. Tests ran from their package directories with fresh OPENCODE_TEST_HOME, XDG_DATA_HOME, XDG_CONFIG_HOME, XDG_STATE_HOME, and XDG_CACHE_HOME.

Package Executed command Result
core bun test test/kilocode/gateway-usage.test.ts test/kilocode/session-stats.test.ts test/session-runner.test.ts test/session-runner-tool-events.test.ts test/session-title.test.ts test/session-stats.test.ts 291 passed
kilo-gateway-core bun test 77 passed
kilo-cli bun test test/gateway-usage.test.ts test/rpc.test.ts 5 passed, 140 assertions
tui bun test test/kilocode/routed-model.test.ts test/kilocode/routed-model.view.test.tsx test/util/session.test.ts test/feature-plugins/prompt-footer.test.tsx 7 passed
script/upstream bun test 87 passed, 327 assertions
affected packages bun typecheck Passed
root bun run check Passed, lint has no errors and all 28 typecheck tasks passed

The initial nested-worktree install selected an ancestor Node 22 type package. A local generated-only bun install --linker hoisted --frozen-lockfile selected locked Node 24 types. The changed branch's root check and final tests then passed. No manifest, lockfile, global install, or source workaround was changed.

The first CI run found one introduced tooling-test regression: the real-tree placement test still expected no compaction patch. The follow-up keeps its plugin-hook/service assertions and verifies the new registered billing patch instead. The complete tooling suite and root check pass locally after that correction; no failed workflow was manually rerun.

Review follow-up: partial or malformed normalized OpenRouter records no longer hide a valid raw OpenRouter cost. Regression tests reproduced the lost nonzero and zero charges before the fix, and also verify that a valid normalized zero still takes precedence. The stats test checks exact row membership/count without relying on the order of tied token totals. The two addressed review threads were resolved after the fix was pushed, without bot replies. The focused core/real-host suites and root check passed again.

Real-host tests cover Chat, Anthropic Messages, OpenAI Responses, five different routes within one session, real reasoning/read-tool continuation, provider-cost precedence, zero and malformed costs, token fallback, exact message association, title and compaction charges, parent/child totals, and full transcript equality after closing and reopening file-backed SQLite.

Real self-tests, separate from helper/renderer tests:

  • Ran bun dev run --standalone --format json against a local controlled Gateway with a fresh KILO_V2_PROFILE. First response settled upstream cost $0.125 despite a zero routing fee. A new process using the same profile settled a second route at market cost $0. SQLite retained both routes and distinct request/generation/response IDs while both requested models stayed Auto.
  • Ran real bun dev stats --standalone --models --cost: alpha and beta appeared as separate model rows at $0.125 and $0.
  • Reopened that profile in a unique headful vscode-self-test session running the real TUI through the isolated bun dev entry with --standalone. Both earlier and later reasoning/footer labels were correct. The TUI's Compact session command settled $0.0625; the session total became $0.2075, including the existing $0.02 title charge. A clean env -i restart with the same explicit isolated database retained both routes and the completed compaction. The harness and fixture processes were stopped afterward.
  • An authorized deployed kilo/deepseek/deepseek-v4.1-flash smoke test returned ACCOUNTING_OK. Its isolated stored assistant had cost $0.0017056884, the actual public model ID, and request/generation IDs. Credentials and private request IDs were not printed or published.

Limits: controlled fixtures prove precedence and durable association, not a reconciliation with a deployed billing invoice. JSON native-compaction usage is covered at the success transformer; the exercised public native-model Session path selected streamed compaction, which was tested end to end. No historical rows without response metadata are reconstructed.

Placement

Change Rung Why not lower Proof
Successful Gateway metadata propagation 1 Existing response hook already wraps the Kilo-owned transformer kilo-gateway-core/src/stream.ts; no adapter index changes
Provider cost and minimal per-step identity 4 Finish publisher and settlement are static code with no mutation hook or service override Narrow marked call-outs in core/src/session/{usage,title,compaction}.ts and runner/{publish-llm-event,step}.ts; policy in core/src/kilocode/session/usage.ts
Actual-route statistics 4 The aggregate SQL has no model-identity hook One marked SQL expression plus import; policy in the Kilo-owned usage module
Production TUI labels and context lookup 4 No plugin renderer slot exists for these transcript/footer/group rows Thin call-outs in routes/session/{index,group-view}.tsx and util/session.ts; Kilo-owned components

All nine shared files have shared-patches.json entries. Shared-file limits change from 27 to 36 files and 204 to 255 marked lines; unmarked lines stay at 39. No new core-internal adapter import or mirrored file was added. Logic-package import checks, annotation checks, and shared-patch checks pass.

Progress map

gateway: 59/75 to 61/75, with 12 existing drops unchanged. Module 30/35 to 31/35; test 17/22 to 18/22; patch 12/18 unchanged.

  • Ported two cells: gateway-metadata.ts and its regression tests. The disposition pointers identify the success transformer, actual settlement, native-dialect host tests, and restart tests.
  • Still open: cloud-session modules/tests, event-service reporting, provider quota/usage reporting, legacy server handlers, transform defaults/tests, and the legacy custom-router patch. These are separate behavior from cost/routed-response accounting and are not claimed by this PR. No custom router, FIM, dashboard, or account/policy feature is added here.
  • Existing broad provider/session/model-routing cells are not closed for this focused subset. The map was regenerated twice, checked for drift, and the remaining Gateway inventory was inspected.
  • Scope came from current main code and live callers, not archived port status or migration dispositions. No umbrella issue is closed.

Manual Test

  1. Use Auto for two responses that route to different models. Verify each footer and reasoning/tool group keeps its own actual route while the composer remains Auto.
  2. Compact the session and restart the isolated profile. Verify earlier route labels remain correct and the session cost includes the reported compaction charge.
  3. Open per-step token diagnostics and run bun dev stats --standalone --models --cost. Verify actual-route rows and zero-cost responses. A regular non-Kilo provider must retain its existing token-price accounting.

@github-actions github-actions Bot added area:cli CLI, TUI and core engine area:gateway Kilo Gateway and auth labels Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

➖ v2 migration progress: 0.9% → 0.9% (+2 done, 0 total, 0 dropped; 92/9829 cells)

Placement: 51 new marked lines in shared files (51 patch), 0 new kilo_port annotations, 0 new core-internal imports.

Ratchets: 9 core-internal imports (base 9); shared patches by rung 20 seam, 16 patch (base 20 seam, 7 patch).

Feature Before After Change
gateway 59/75 61/75 +1 module done; +1 test done
2 cells closed
  • module:packages/kilo-gateway/src/gateway-metadata.ts
  • module:packages/kilo-gateway/test/gateway-metadata.test.ts

v2 migration progress map at this commit

Full map · how cells close

@kilo-code-bot

kilo-code-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • packages/core/src/kilocode/session/usage.ts
  • packages/core/test/kilocode/gateway-usage.test.ts
  • packages/core/test/kilocode/session-stats.test.ts

Verification notes

  • providerCost now evaluates normalized OpenRouter fields first, then the raw providerMetadata.openrouter record, per field, so a plain cost is no longer lost when normalized metadata exists but carries no cost fields. Precedence still matches v1 KiloSession.providerCost: upstream inference cost, then regular cost, then raw native upstream cost, then Gateway marketCost, with a valid zero stopping fallback and non-finite values unavailable.
  • The new table test covers partial normalized metadata, malformed normalized cost with a finite raw cost, and a normalized zero beating a raw upstream value; Money.USD.make is used consistently with existing cases.
  • The stats assertion now uses expect.arrayContaining with an explicit toHaveLength(4), removing the dependency on unordered SQLite row order while still rejecting missing or duplicated model rows.
  • I did not run the test suites; verification is from reading the changed code and tests.
Previous Review Summary (commit 78db244)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 78db244)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/core/src/kilocode/session/usage.ts 18 Record-level ?? fallback discards the raw OpenRouter usage object when normalized metadata exists but lacks cost fields, so a plain openrouter.cost can be skipped before Gateway/token fallbacks.
packages/core/test/kilocode/session-stats.test.ts 88 Exact stats.models order is asserted while the two single-step rows tie on token total and the stats query has no ORDER BY, so the order depends on SQLite row order.
Files Reviewed (21 files)
  • migration-tracking/progress/README.md
  • migration-tracking/progress/dispositions.json
  • migration-tracking/progress/history.svg
  • migration-tracking/progress/progress.json
  • migration-tracking/progress/progress.svg
  • packages/core/src/kilocode/session/usage.ts - 1 suggestion
  • packages/core/src/session/compaction.ts
  • packages/core/src/session/runner/publish-llm-event.ts
  • packages/core/src/session/runner/step.ts
  • packages/core/src/session/stats.ts
  • packages/core/src/session/title.ts
  • packages/core/src/session/usage.ts
  • packages/core/test/kilocode/gateway-usage.test.ts
  • packages/core/test/kilocode/session-stats.test.ts - 1 suggestion
  • packages/kilo-cli/test/gateway-usage.test.ts
  • packages/kilo-gateway-core/src/stream.ts
  • packages/kilo-gateway-core/test/usage-metadata.test.ts
  • packages/tui/src/kilocode/routed-model.tsx
  • packages/tui/src/routes/session/group-view.tsx
  • packages/tui/src/routes/session/index.tsx
  • packages/tui/src/util/session.ts
  • packages/tui/test/kilocode/routed-model.test.ts
  • packages/tui/test/kilocode/routed-model.view.test.tsx
  • script/upstream/placement.test.ts
  • script/upstream/shared-patches.json

Verification notes

  • Cost precedence matches v1 (origin/main KiloSession.providerCost), including a valid zero stopping fallback and Number("") handling; providerCost is gated on providerID === "kilo", so non-Kilo settlement stays on catalog token pricing.
  • Both settlement paths (Step.Ended/Step.Failed via publish-llm-event.ts + step.ts, and title.ts/compaction.ts via SessionUsage.record) preserve a reported zero with ??, and failed-step usage flows through publishStepFailure.
  • native() and the stats SQL both read the projected message shape ({ id, type, ...data } with top-level native), so native.kilo.routedModelID is populated where the SQL expects it; non-Kilo rows and non-string/blank routes fall back to model.id.
  • transformResponse deletes content-length/content-encoding for the new non-SSE JSON path, and the stamped identity is a superset of what the existing onComplete consumer reads.
  • TUI use() additions are inside context.Provider; resolvePart returns elements of message.content, so the RoutedStep last-part identity check holds.
  • Shared-file changes are marked, listed in shared-patches.json (limits 27→36 files, 204→255 marked lines), and the PR body names the rung and missing seam. CI-owned ratchets and the progress map were not re-reviewed.
  • I did not run the test suites; verification is from reading the code and tests.

Reviewed by deepseek-flash-latest · Input: 88.5K · Output: 8.6K · Cached: 287.9K

Review guidance: REVIEW.md from base branch v2

@marius-kilocode
marius-kilocode merged commit f50d908 into v2 Oct 22, 2026
24 checks passed
@marius-kilocode
marius-kilocode deleted the gateway-usage-parity branch October 22, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:cli CLI, TUI and core engine area:gateway Kilo Gateway and auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants