Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions src/adapters/kiro-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,45 @@ function tokenCount(eventType: string, obj: Record<string, unknown>, key: string
return value;
}

/**
* A cache counter Kiro did not report, kept as unknown rather than zero (#4546).
*
* `OcxUsage` omits cache fields it has no reading for, and `cacheHitRate` is null when
* unobserved -- the convention everywhere except here. Coercing an absent counter to 0 makes
* "the provider said nothing" indistinguishable from "nothing was cached", which is the
* difference between a routing change that preserved the prompt cache and one that destroyed
* it. A malformed value is still a malformed event; only absence is unknown.
*/
function optionalTokenCount(
eventType: string,
obj: Record<string, unknown>,
key: string,
): number | undefined {
if (obj[key] === undefined) return undefined;
return tokenCount(eventType, obj, key, true);
}

function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefined {
if (value === undefined || value === null) return undefined;
if (typeof value !== "object" || Array.isArray(value)) {
return malformed(eventType, "tokenUsage must be an object");
}
const usage = value as Record<string, unknown>;
const uncached = tokenCount(eventType, usage, "uncachedInputTokens", true);
const cacheRead = tokenCount(eventType, usage, "cacheReadInputTokens", false);
const cacheWrite = tokenCount(eventType, usage, "cacheWriteInputTokens", false);
const cacheRead = optionalTokenCount(eventType, usage, "cacheReadInputTokens");
const cacheWrite = optionalTokenCount(eventType, usage, "cacheWriteInputTokens");
Comment on lines +94 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Synchronize the owned adapter documentation

This changes src/adapters/kiro-events.ts's usage-event contract from measured zeroes to omitted cache counters, but the commit leaves every structure document mapped to src/adapters/ unchanged. That leaves the repository's architecture source of truth without the new Kiro telemetry invariant; update the applicable owned documentation and its coverage reference in the same change.

AGENTS.md reference: src/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

const outputTokens = tokenCount(eventType, usage, "outputTokens", true);
const totalTokens = tokenCount(eventType, usage, "totalTokens", true);
const inputTokens = uncached + cacheRead + cacheWrite;
// An unreported counter contributes nothing to the total, which is a different statement
// from claiming it was measured as zero.
const inputTokens = uncached + (cacheRead ?? 0) + (cacheWrite ?? 0);
if (!Number.isSafeInteger(inputTokens)) return malformed(eventType, "input token usage overflowed");
return {
inputTokens,
outputTokens,
totalTokens,
cachedInputTokens: cacheRead,
cacheReadInputTokens: cacheRead,
cacheCreationInputTokens: cacheWrite,
...(cacheRead !== undefined ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
...(cacheWrite !== undefined ? { cacheCreationInputTokens: cacheWrite } : {}),
};
}

Expand Down
32 changes: 32 additions & 0 deletions tests/providers/kiro/kiro-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,38 @@ describe("kiro adapter — parseStream", () => {
expect(contextTotalTokens).toBeGreaterThan(19);
});

test("unreported cache counters stay unknown instead of being recorded as measured zeros", async () => {
const adapter = createKiroAdapter(provider);
await adapter.buildRequest(parsedWith([{ role: "user", content: "x".repeat(700) }]));
const done = await doneUsage(
adapter,
eventFrame({ content: "answer" }),
eventFrame({
tokenUsage: {
uncachedInputTokens: 10,
outputTokens: 4,
totalTokens: 14,
},
}, "metadataEvent"),
);
// Kiro said nothing about caching on this turn. Storing 0 would make that indistinguishable
// from a measured total miss, which is the difference between routing that preserved a
// prompt cache and routing that destroyed it (#4546).
expect("cachedInputTokens" in done).toBe(false);
expect("cacheReadInputTokens" in done).toBe(false);
expect("cacheCreationInputTokens" in done).toBe(false);
expect(done.inputTokens).toBe(10);
});

test("a malformed cache counter is still a malformed event", async () => {
expect(() => parseKiroEvent(
"metadataEvent",
new TextEncoder().encode(JSON.stringify({
tokenUsage: { uncachedInputTokens: 10, cacheReadInputTokens: -1, outputTokens: 4, totalTokens: 14 },
})),
)).toThrow();
});

test("authoritative turn usage floors a smaller payload context estimate", async () => {
const adapter = createKiroAdapter(provider);
await adapter.buildRequest(parsedWith([{ role: "user", content: "hi" }]));
Expand Down
Loading