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
4 changes: 4 additions & 0 deletions scripts/test-layout/layout-kiro-usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"responses-combo-bridge-usage.test.ts": "responses",
"server-kiro-usage-log-e2e.test.ts": "server"
}
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"root": "tests",
"explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-server-inbound.json", "layout-providers.json", "layout-responses.json", "layout-exec-boundary.json", "layout-persistent-dashboard.json", "layout-start-ownership.json", "layout-config.json"],
"explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-server-inbound.json", "layout-providers.json", "layout-responses.json", "layout-exec-boundary.json", "layout-persistent-dashboard.json", "layout-start-ownership.json", "layout-config.json", "layout-kiro-usage.json"],
"explicit": { "service-desktop-recovery.test.ts": "service", "provider-input-modalities.test.ts": "providers", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration", "coding-agent-windows-dispatch.test.ts": "adapters", "coding-agent-native-empty-argv.test.ts": "windows",
"stop-restore-history-ownership.test.ts": "service",
"cli-stop-refusal-transport.test.ts": "cli",
Expand Down
1 change: 1 addition & 0 deletions src/adapters/kiro-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefine
totalTokens,
...(cacheRead !== undefined ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
...(cacheWrite !== undefined ? { cacheCreationInputTokens: cacheWrite } : {}),
upstreamReported: true,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/adapters/kiro/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ function mergeKiroUsage(
...(sumOptional("providerCredits") !== undefined ? { providerCredits: sumOptional("providerCredits") } : {}),
...(sumOptional("reasoningOutputTokens") !== undefined ? { reasoningOutputTokens: sumOptional("reasoningOutputTokens") } : {}),
...(first.estimated || second.estimated ? { estimated: true } : {}),
...(first.upstreamReported && second.upstreamReported ? { upstreamReported: true as const } : {}),
};
}

Expand Down
6 changes: 5 additions & 1 deletion src/server/request-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,10 @@ function finalizedUsage(
const usageFallback = !finalUsage && estimate !== undefined
? { inputTokens: estimate, outputTokens: 0, estimated: true }
: undefined;
const combinedInputTokens = finalUsage && estimate !== undefined
// The estimate is a floor under a count the adapter could only guess. A count the adapter read
// off the upstream's own usage frame stays exact: max() against a larger local estimate would
// replace it and mark it estimated.
const combinedInputTokens = finalUsage && !finalUsage.upstreamReported && estimate !== undefined
? Math.max(finalUsage.inputTokens, estimate)
: undefined;
const loggedUsage = finalUsage && combinedInputTokens !== undefined
Expand Down Expand Up @@ -1907,6 +1910,7 @@ export function aggregateAttemptUsage(
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
...(providerCredits !== undefined ? { providerCredits } : {}),
...(status === "estimated" ? { estimated: true } : {}),
...(usages.every(usage => usage.upstreamReported) ? { upstreamReported: true as const } : {}),
};
return { usage: aggregate, status, totalTokens };
}
Expand Down
19 changes: 19 additions & 0 deletions src/server/responses/core-combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,19 @@ export async function executeComboResponses(
finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
attemptRetained = true;
};
// The row is written from `logCtx`, which copies `childLog` only when the child commits. A
// bridge child binds its usage later, at the terminal frame, so the binding is forwarded here
// after that point. Left on `childLog`, the row would re-read the bridged wire instead of the
// adapter's usage, and that wire has no providerCredits and a context-derived input count.
// A binding that arrives before the commit waits for it, and a child that never commits
// forwards nothing: both flags belong to this target only.
let childCommitted = false;
let childUsageBound = false;
const forwardChildUsage = (): void => {
if (!childCommitted || !childUsageBound) return;
logCtx.usageFromBridge = true;
if (childLog.usage) logCtx.usage = childLog.usage;
};
const completedTarget = { provider: pick.target.provider, model: pick.target.model };
const writerGeneration = pick.writerGeneration;
let consumedChildFailure: ConsumedComboFailure | undefined;
Expand Down Expand Up @@ -1147,6 +1160,10 @@ export async function executeComboResponses(
comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
comboReplaySnapshot,
deferCodexResetDerivedCooldown,
onBridgeUsageBound: () => {
childUsageBound = true;
forwardChildUsage();
},
// Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
// Object.assign(logCtx, childLog) would overwrite the request-relative value).
onFirstOutput: () => {
Expand Down Expand Up @@ -1246,6 +1263,8 @@ export async function executeComboResponses(
options.onCodexAuthContextResolved?.(resolvedAuth);
options.setTerminalOutcomeRecorder?.(terminalRecorder);
callbackGate.commit();
childCommitted = true;
forwardChildUsage();
return response;
}

Expand Down
7 changes: 7 additions & 0 deletions src/server/responses/core-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ export interface HandleResponsesOptions {
callerDirectAuth?: CallerDirectAuth | null;
/** Internal recursion guard; callers outside this module must not set it. */
comboAttempt?: boolean;
/**
* Set by the combo loop for each child. A child logs into its own context, which the combo
* copies once when the child commits, and the bridge reports usage after that, at the terminal
* frame. The transport calls this when it binds that usage on the child's context, so the combo
* can carry the binding to the context that writes the row.
*/
onBridgeUsageBound?: () => void;
/** Child-owned prepaid initial send; capacity remains charged until dispatch or unsent release. */
comboInitialSend?: { permit: SingleUseDispatchPermit; producerOwned?: boolean };
/** Exact externally booked combo hop, used for its child's spend preflight and send reports. */
Expand Down
5 changes: 2 additions & 3 deletions src/server/responses/request-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,11 @@ export async function prepareResponsesTransport(
logCtx.usageFromBridge = true;
if (usesApiKeyAccount(route.provider)) {
logCtx.usage = logCtx.activeAttempt?.usage;
return;
}
if (usage) {
} else if (usage) {
logCtx.usage = usage;
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
}
options.onBridgeUsageBound?.();
};
const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
if (!anthropicAdmissionIsCurrent()) return false;
Expand Down
7 changes: 7 additions & 0 deletions src/types/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,13 @@ export interface OcxUsage {
cacheCreation1hInputTokens?: number;
reasoningOutputTokens?: number;
estimated?: boolean;
/**
* The adapter read these counts off the upstream's own usage frame. Only an adapter whose
* provider is estimated by default (usageForFinalLog) sets it, and a merge keeps it only when
* every leg had it. A figure taken off a bridged wire or guessed never carries it, so the
* provider default still marks those as estimated.
*/
upstreamReported?: true;
/**
* The raw upstream usage object for Responses-shaped upstreams (openai/codex#41980 parity):
* codex-rs preserves the complete `response.usage` object through its own pipeline, so fields
Expand Down
8 changes: 7 additions & 1 deletion src/usage/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ export function usageTotalTokens(usage: OcxUsage | undefined): number | undefine
* Providers whose adapters can only estimate usage (no authoritative per-turn frame).
* Callers should pass the route ADAPTER when available; the name-prefix match is a
* fallback for paths that only know the configured provider name (e.g. "cursor-mykey").
*
* Kiro is on the list because most of what reaches the log is a guess: a figure taken off the
* bridged Responses wire derives its input from the context checkpoint. The adapter opts a
* measured figure out with `upstreamReported`, set where it reads Kiro's own tokenUsage frame.
*/
function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
return providerOrAdapter === "kiro" || providerOrAdapter.startsWith("kiro-")
Expand All @@ -556,7 +560,9 @@ export function usageForFinalLog(
): OcxUsage | undefined {
if (!usage) return undefined;
if (locallyAnswered) return usage;
if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
if (usage.estimated || (isEstimatedUsageProvider(provider) && !usage.upstreamReported)) {
return { ...usage, estimated: true };
}
return usage;
}

Expand Down
10 changes: 8 additions & 2 deletions structure/providers/kiro.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,14 @@ finite `contextUsagePercentage` (and a `tokenUsage` block) there as a fallback,
from `metadataEvent` is legitimate rather than impossible. Both feed the same field, and any
positive value overwrites an earlier one.

Spend arrives in `meteringEvent` as **credits, not tokens**. No captured response carried
`tokenUsage` on any event, which is why Kiro token usage stays estimated. The parser preserves
Spend arrives in `meteringEvent` as **credits, not tokens**. Kiro token usage stays estimated
unless a `metadataEvent` carries a `tokenUsage` block; no captured response did. That block is the
only frame that yields a measured count: the parser marks the usage `upstreamReported`,
`mergeKiroUsage` keeps the mark only when both legs of a two-leg turn have it, and
`usageForFinalLog` (`src/usage/log.ts`) then skips the provider-wide estimated mark while
`finalizedUsage` (`src/server/request-log.ts`) skips the local input floor. A turn without
`tokenUsage`, a leg without it, and a count read off the bridged Responses wire all keep the
estimated mark. The parser preserves
`meteringEvent` unit/usage (`amount` is an alias) and optional `unitPlural`; credit readings populate
`OcxUsage.providerCredits` independently of token metadata. The latest reading within a response
is a snapshot; separate completion-fallback responses add their credits. Missing metering stays
Expand Down
9 changes: 9 additions & 0 deletions structure/transports/responses-spend.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ what those sends may cost, and it is the only bound here that survives a restart
caller is `request-spend.ts`, installed on the execution budget at genuine ingress in `core.ts`
and parked on the log context so `addFinalRequestLog` can settle it. Native Chat installs the same tracker before its independent physical-send ladder and charges it immediately before each dispatch, so that fast path cannot bypass root, identity or provider-pool ceilings.

The tracker settles with `logCtx.usage`, and so does the row. A combo copies its child's log
context into that one only when the child commits, while the bridge binds the adapter usage later,
at the terminal frame. `executeComboResponses` therefore passes each child `onBridgeUsageBound`
(`core-options.ts`), which `bindKeyUsageFromBridge` calls after it binds. After the child commits,
the combo sets `usageFromBridge` and `usage` on its own context; a binding that came earlier waits
for the commit, and a child that fails or never commits forwards nothing. Without that, the row
and the settlement would read the usage back off the bridged Responses wire, which carries no
`providerCredits` and derives its input count from the context checkpoint.

Unconfigured requests preserve the legacy counter observer: reservations increment the send
counter, refunds decrement it, and named reports reconcile prepaid sends. Enforced requests use
the seed and physical-start protocol below across Responses, native Messages and native Chat.
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
"codex-shim-overlay-readiness.test.ts": "codex-integration",
"codex-shim-overlay-candidates.test.ts": "codex-integration",
"droid-normalized-reasoning-defaults.test.ts": "clients",
"responses-combo-bridge-usage.test.ts": "responses",
"responses-combo-exhausted-error.test.ts": "responses",
"responses-combo-run-turn-classified-failure.test.ts": "responses",
"cli-config-default-show.test.ts": "cli",
Expand Down Expand Up @@ -1274,7 +1275,7 @@
"server-jev-combo-e2e.test.ts": "server", "server-kiro-completion-e2e.test.ts": "server",
"combo-requested-effort.test.ts": "server",
"server-kiro-device-login.test.ts": "server", "server-kiro-oauth-401-replay.test.ts": "server",
"server-kiro-refusal-e2e.test.ts": "server", "server-live-existing-call.test.ts": "server",
"server-kiro-refusal-e2e.test.ts": "server", "server-kiro-usage-log-e2e.test.ts": "server", "server-live-existing-call.test.ts": "server",
"server-live-close.test.ts": "server",
"server-live-cancel.test.ts": "server",
"server-live-frame-log.test.ts": "server",
Expand Down
1 change: 1 addition & 0 deletions tests/providers/kiro/kiro-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,7 @@ describe("kiro adapter — parseStream", () => {
cacheCreationInputTokens: 2,
outputTokens: 4,
totalTokens: 19,
upstreamReported: true,
});
// The whole-payload checkpoint must exceed this turn's own total, which is the point of
// reporting it separately.
Expand Down
Loading
Loading