Skip to content

Kiro: forward per-turn token counts + session model for the pricing hedge#276

Merged
alexeyzimarev merged 2 commits into
mainfrom
ai-1196-kiro-token-hedge
Jul 7, 2026
Merged

Kiro: forward per-turn token counts + session model for the pricing hedge#276
alexeyzimarev merged 2 commits into
mainfrom
ai-1196-kiro-token-hedge

Conversation

@realtonyyoung

Copy link
Copy Markdown
Contributor

KiroUsage now reads Kiro's per-turn input_token_count/output_token_count (carried only when > 0) and the session model_id (rts_model_state.model_info) from {id}.json, and injects them into the _kcap_usage object on the turn's final assistant line.

Kiro persists these counts as 0 today — the Amazon-Q CLI discards the streaming API's TokenUsage (upstream aws/amazon-q-developer-cli#2397). So this is a dormant hedge: nothing changes now, but the server stamps a canonical $usage (and pricing/cost lights up) automatically the moment a future kiro-cli release starts recording tokens — no further code change. Credits/context% behaviour is unchanged.

Server side (normalizer $usage stamping + the credits chip) is in the paired kcap-server PR.

Tests: KiroUsageTests cover token+model capture, injection, and the zero-guard (dormant) case. Full CLI unit suite green (2240/2240); AOT publish clean.

Closes #275
Linear: AI-1196

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

AI-1196

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Forward Kiro per-turn token counts and session model into _kcap_usage (dormant hedge)

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Read per-turn input/output token counts and session model_id from Kiro {id}.json metadata.
• Inject token/model fields into _kcap_usage only when token counts are non-zero.
• Add unit tests covering capture, injection, and zero-count dormant behavior.
Diagram

graph TD
  A["Kiro session {id}.json"] --> B["KiroUsage.AnchorMap"] --> C[("Anchor→usage map")]
  D["Transcript line (.jsonl)"] --> E["KiroUsage.EnrichLine"] --> F["Transcript line + _kcap_usage"] --> G["Server normalizer"]
  C --> E
  subgraph Legend
    direction LR
    _file["File/Input"] ~~~ _fn["Function"] ~~~ _data[("Data")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always include token/model fields (even when 0) and let server ignore
  • ➕ Simpler client logic (no non-zero guard).
  • ➕ Server can uniformly read fields without null checks.
  • ➖ Risks treating current persisted zeros as real usage if server logic changes.
  • ➖ Violates stated goal of staying fully dormant until upstream starts recording tokens.
2. Have CLI stamp canonical $usage directly (instead of _kcap_usage)
  • ➕ Makes usage payload explicit and potentially reduces server coupling.
  • ➕ Easier to validate end-to-end behavior in CLI outputs alone.
  • ➖ Duplicates server-side normalization logic and pricing semantics in the CLI.
  • ➖ Higher risk of divergence across clients; harder to roll out model/pricing changes centrally.

Recommendation: Current approach (injecting optional token/model into _kcap_usage only when counts are >0) is the safest hedge: it prevents today’s persisted zeros from being misinterpreted, while enabling the server to light up canonical $usage automatically when upstream starts populating token counts. The alternatives either increase divergence (client-side $usage stamping) or introduce risk around zero values.

Files changed (2) +112 / -17

Enhancement (1) +54 / -17
KiroUsage.csCapture token/model from metadata and conditionally inject into _kcap_usage +54/-17

Capture token/model from metadata and conditionally inject into _kcap_usage

• Extends TurnUsage to include optional input/output token counts and session model_id. Parses session_state.rts_model_state.model_info.model_id and per-turn input_token_count/output_token_count, carrying token/model only when counts are > 0. Updates JSONL enrichment to inject these fields onto the anchor assistant line alongside credits/context%.

src/Capacitor.Cli.Core/Kiro/KiroUsage.cs

Tests (1) +58 / -0
KiroUsageTests.csAdd tests for token/model forwarding hedge and zero-count guard +58/-0

Add tests for token/model forwarding hedge and zero-count guard

• Adds unit coverage ensuring non-zero token counts and session model are captured in AnchorMap and injected by EnrichLine. Verifies that zero token counts remain dormant (no token fields injected).

test/Capacitor.Cli.Tests.Unit/KiroUsageTests.cs

@qodo-code-review

qodo-code-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. _kcap_usage uses model key ✗ Dismissed 📎 Requirement gap ≡ Correctness
Description
When forwarding the session model identifier, the code writes it to _kcap_usage.model instead of
_kcap_usage.model_id. This can prevent the server from recognizing the model identifier required
to stamp canonical $usage when non-zero token counts are present.
Code

src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[R115-117]

+            if (u.InputTokens is { } inTok)       usage["input_token_count"]        = inTok;
+            if (u.OutputTokens is { } outTok)     usage["output_token_count"]       = outTok;
+            if (u.Model is { Length: > 0 } model) usage["model"]                    = model;
Evidence
PR Compliance ID 1 explicitly requires forwarding the session model_id in _kcap_usage alongside
non-zero token counts. The added enrichment code injects the model under the key model, not
model_id, so the required field name is not present on _kcap_usage.

Forward Kiro per-turn token usage counts and session model_id in _kcap_usage (only when counts > 0)
src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[115-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The compliance requirement expects the session `model_id` to be included in `_kcap_usage` when forwarding non-zero token counts, but the implementation writes the model identifier under the key `model`.

## Issue Context
PR Compliance ID 1 requires forwarding Kiro per-turn `input_token_count`/`output_token_count` (only when > 0) and the session `model_id` in `_kcap_usage`. The current code injects `usage["model"] = model` rather than `usage["model_id"] = modelId`.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[115-117]
- src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[24-29]
- test/Capacitor.Cli.Tests.Unit/KiroUsageTests.cs[78-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Model parse aborts usage ✓ Resolved 🐞 Bug ☼ Reliability
Description
KiroUsage.AnchorMap reads session_state.rts_model_state.model_info.model_id via GetValue<string>(),
which can throw if model_id is present but not a JSON string; the outer catch then aborts parsing
and returns an empty/partial anchor map. This can silently drop credits/context% enrichment during
Kiro import for malformed/variant metadata blobs.
Code

src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[R46-50]

+            // Session-level model id (e.g. "minimax-m2.5", "auto"). Rides alongside the
+            // token counts so the server can stamp a canonical $usage with a model —
+            // AccumulateTokens drops entries with no model (HasModel guard).
+            var sessionModel = root?["session_state"]?["rts_model_state"]?["model_info"]?["model_id"]
+                ?.GetValue<string>();
Evidence
The session model is extracted with GetValue<string>() inside the same broad try/catch that
guards the entire AnchorMap parse; an exception at this point short-circuits the rest of the
per-turn usage loop. Kiro import depends on AnchorMap to enrich transcript lines with
credits/context%, so aborting AnchorMap drops that enrichment.

src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[40-86]
src/Capacitor.Cli/Commands/KiroImportSource.cs[253-268]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`KiroUsage.AnchorMap` uses `GetValue<string>()` for `model_id`. If the JSON contains `model_id` but not as a string (e.g., `null`, number, object), `GetValue<string>()` can throw, and because the method wraps the entire extraction in one broad `try/catch`, this can abort anchor-map construction and drop otherwise usable turn usage data.

## Issue Context
Usage enrichment is explicitly best-effort and should not be disabled by a single malformed optional field. Today the new `model_id` extraction is session-level, but an exception there prevents credits/context% mapping for the whole session metadata.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[40-52]
- src/Capacitor.Cli.Core/Kiro/KiroUsage.cs[84-93]

## Suggested change
- Replace `?.GetValue<string>()` with a defensive pattern:
 - check `is JsonValue` and use `TryGetValue<string>(out var s)`
 - optionally apply `string.IsNullOrWhiteSpace` normalization
- Alternatively, isolate model parsing in its own `try/catch` so a bad `model_id` cannot abort turn parsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/Kiro/KiroUsage.cs
Comment thread src/Capacitor.Cli.Core/Kiro/KiroUsage.cs Outdated
@realtonyyoung realtonyyoung force-pushed the ai-1196-kiro-token-hedge branch from 415c63f to 9f0459f Compare July 6, 2026 03:30
… pricing hedge

KiroUsage now reads input/output_token_count (carried only when > 0) and the
session model_id (rts_model_state.model_info) from {id}.json and injects them
into _kcap_usage, so the server can stamp a canonical $usage once Kiro starts
recording token counts. Kiro persists 0 today (the Amazon-Q CLI discards the
API's TokenUsage — upstream aws/amazon-q-developer-cli#2397), so this stays
dormant until then. Credits/context% behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung realtonyyoung force-pushed the ai-1196-kiro-token-hedge branch from 9f0459f to 2d4f6b3 Compare July 6, 2026 13:04
GetValue<string>() on model_id threw if it was present but not a JSON string,
and since the whole method is one best-effort try/catch a bad optional field
aborted the anchor map — silently dropping credits/context% enrichment for the
whole session. Read it defensively (is JsonValue + TryGetValue<string>). Added a
regression test with a non-string model_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit 32ada14 into main Jul 7, 2026
5 checks passed
@alexeyzimarev alexeyzimarev deleted the ai-1196-kiro-token-hedge branch July 7, 2026 13:19
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.

Kiro: forward per-turn token counts + model so server can price them when available

2 participants