Skip to content

Commit ae6eb66

Browse files
authored
fix(kiro): preserve cache usage fields (#116)
1 parent 38e86b0 commit ae6eb66

11 files changed

Lines changed: 539 additions & 111 deletions

internal/runtime/executor/kiro_executor.go

Lines changed: 156 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ func (e *KiroExecutor) executeWithRetry(ctx context.Context, auth *cliproxyauth.
10061006
}
10071007

10081008
// 3. Update TotalTokens
1009-
usageInfo.TotalTokens = usageInfo.InputTokens + usageInfo.OutputTokens
1009+
finalizeKiroUsageTotal(&usageInfo)
10101010

10111011
appendAPIResponseChunk(ctx, e.cfg, []byte(content))
10121012
reporter.publish(ctx, usageInfo)
@@ -1638,6 +1638,140 @@ type eventStreamMessage struct {
16381638
// NOTE: Request building functions moved to internal/translator/kiro/claude/kiro_claude_request.go
16391639
// The executor now uses kiroclaude.BuildKiroPayload() instead
16401640

1641+
func applyKiroTokenUsage(detail *usage.Detail, tokenUsage map[string]interface{}) bool {
1642+
if detail == nil || tokenUsage == nil {
1643+
return false
1644+
}
1645+
updated := false
1646+
if outputTokens, ok := kiroTokenUsageInt64(tokenUsage, "outputTokens"); ok {
1647+
detail.OutputTokens = outputTokens
1648+
updated = true
1649+
}
1650+
if totalTokens, ok := kiroTokenUsageInt64(tokenUsage, "totalTokens"); ok {
1651+
detail.TotalTokens = totalTokens
1652+
updated = true
1653+
}
1654+
if uncachedInputTokens, ok := kiroTokenUsageInt64(tokenUsage, "uncachedInputTokens"); ok {
1655+
detail.InputTokens = uncachedInputTokens
1656+
updated = true
1657+
}
1658+
if cacheReadTokens, ok := kiroTokenUsageInt64(tokenUsage, "cacheReadInputTokens"); ok {
1659+
detail.CacheReadTokens = cacheReadTokens
1660+
detail.CachedTokens = cacheReadTokens
1661+
updated = true
1662+
}
1663+
if cacheCreationTokens, ok := kiroTokenUsageInt64(tokenUsage, "cacheWriteInputTokens"); ok {
1664+
detail.CacheCreationTokens = cacheCreationTokens
1665+
if detail.CachedTokens == 0 {
1666+
detail.CachedTokens = cacheCreationTokens
1667+
}
1668+
updated = true
1669+
}
1670+
return updated
1671+
}
1672+
1673+
func finalizeKiroUsageTotal(detail *usage.Detail) {
1674+
if detail == nil || detail.TotalTokens != 0 {
1675+
return
1676+
}
1677+
detail.TotalTokens = detail.InputTokens +
1678+
detail.OutputTokens +
1679+
detail.ReasoningTokens +
1680+
detail.CacheReadTokens +
1681+
detail.CacheCreationTokens
1682+
}
1683+
1684+
func applyKiroContextUsageFallback(detail *usage.Detail, contextUsagePercentage float64, hasPreciseTokenUsage bool) (int64, bool) {
1685+
if detail == nil || contextUsagePercentage <= 0 || hasPreciseTokenUsage {
1686+
return 0, false
1687+
}
1688+
calculatedInputTokens := int64(contextUsagePercentage * 200000 / 100)
1689+
if calculatedInputTokens <= 0 {
1690+
return 0, false
1691+
}
1692+
detail.InputTokens = calculatedInputTokens
1693+
finalizeKiroUsageTotal(detail)
1694+
return calculatedInputTokens, true
1695+
}
1696+
1697+
func kiroTokenUsageInt64(tokenUsage map[string]interface{}, key string) (int64, bool) {
1698+
raw, ok := tokenUsage[key]
1699+
if !ok || raw == nil {
1700+
return 0, false
1701+
}
1702+
switch value := raw.(type) {
1703+
case int:
1704+
return int64(value), true
1705+
case int8:
1706+
return int64(value), true
1707+
case int16:
1708+
return int64(value), true
1709+
case int32:
1710+
return int64(value), true
1711+
case int64:
1712+
return value, true
1713+
case uint:
1714+
return int64(value), true
1715+
case uint8:
1716+
return int64(value), true
1717+
case uint16:
1718+
return int64(value), true
1719+
case uint32:
1720+
return int64(value), true
1721+
case uint64:
1722+
return int64(value), true
1723+
case float32:
1724+
return int64(value), true
1725+
case float64:
1726+
return int64(value), true
1727+
case json.Number:
1728+
if n, err := value.Int64(); err == nil {
1729+
return n, true
1730+
}
1731+
if n, err := value.Float64(); err == nil {
1732+
return int64(n), true
1733+
}
1734+
}
1735+
return 0, false
1736+
}
1737+
1738+
func kiroTokenUsageFloat64(tokenUsage map[string]interface{}, key string) (float64, bool) {
1739+
raw, ok := tokenUsage[key]
1740+
if !ok || raw == nil {
1741+
return 0, false
1742+
}
1743+
switch value := raw.(type) {
1744+
case int:
1745+
return float64(value), true
1746+
case int8:
1747+
return float64(value), true
1748+
case int16:
1749+
return float64(value), true
1750+
case int32:
1751+
return float64(value), true
1752+
case int64:
1753+
return float64(value), true
1754+
case uint:
1755+
return float64(value), true
1756+
case uint8:
1757+
return float64(value), true
1758+
case uint16:
1759+
return float64(value), true
1760+
case uint32:
1761+
return float64(value), true
1762+
case uint64:
1763+
return float64(value), true
1764+
case float32:
1765+
return float64(value), true
1766+
case float64:
1767+
return value, true
1768+
case json.Number:
1769+
n, err := value.Float64()
1770+
return n, err == nil
1771+
}
1772+
return 0, false
1773+
}
1774+
16411775
// parseEventStream parses AWS Event Stream binary format.
16421776
// Extracts text content, tool uses, and stop_reason from the response.
16431777
// Supports embedded [Called ...] tool calls and input buffering for toolUseEvent.
@@ -1655,6 +1789,7 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroclaude.Ki
16551789

16561790
// Upstream usage tracking - Kiro API returns credit usage and context percentage
16571791
var upstreamContextPercentage float64 // Context usage percentage from upstream (e.g., 78.56)
1792+
var hasPreciseTokenUsage bool // Whether official tokenUsage supplied precise counts
16581793

16591794
for {
16601795
msg, eventErr := e.readEventStreamMessage(reader)
@@ -1827,33 +1962,13 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroclaude.Ki
18271962

18281963
// Check for nested tokenUsage object (official format)
18291964
if tokenUsage, ok := metadata["tokenUsage"].(map[string]interface{}); ok {
1830-
// outputTokens - precise output token count
1831-
if outputTokens, ok := tokenUsage["outputTokens"].(float64); ok {
1832-
usageInfo.OutputTokens = int64(outputTokens)
1833-
log.Infof("kiro: parseEventStream found precise outputTokens in tokenUsage: %d", usageInfo.OutputTokens)
1834-
}
1835-
// totalTokens - precise total token count
1836-
if totalTokens, ok := tokenUsage["totalTokens"].(float64); ok {
1837-
usageInfo.TotalTokens = int64(totalTokens)
1838-
log.Infof("kiro: parseEventStream found precise totalTokens in tokenUsage: %d", usageInfo.TotalTokens)
1839-
}
1840-
// uncachedInputTokens - input tokens not from cache
1841-
if uncachedInputTokens, ok := tokenUsage["uncachedInputTokens"].(float64); ok {
1842-
usageInfo.InputTokens = int64(uncachedInputTokens)
1843-
log.Infof("kiro: parseEventStream found uncachedInputTokens in tokenUsage: %d", usageInfo.InputTokens)
1844-
}
1845-
// cacheReadInputTokens - tokens read from cache
1846-
if cacheReadTokens, ok := tokenUsage["cacheReadInputTokens"].(float64); ok {
1847-
// Add to input tokens if we have uncached tokens, otherwise use as input
1848-
if usageInfo.InputTokens > 0 {
1849-
usageInfo.InputTokens += int64(cacheReadTokens)
1850-
} else {
1851-
usageInfo.InputTokens = int64(cacheReadTokens)
1852-
}
1853-
log.Debugf("kiro: parseEventStream found cacheReadInputTokens in tokenUsage: %d", int64(cacheReadTokens))
1965+
if applyKiroTokenUsage(&usageInfo, tokenUsage) {
1966+
hasPreciseTokenUsage = true
1967+
log.Infof("kiro: parseEventStream found tokenUsage input=%d cache_read=%d cache_creation=%d output=%d total=%d",
1968+
usageInfo.InputTokens, usageInfo.CacheReadTokens, usageInfo.CacheCreationTokens, usageInfo.OutputTokens, usageInfo.TotalTokens)
18541969
}
18551970
// contextUsagePercentage - can be used as fallback for input token estimation
1856-
if ctxPct, ok := tokenUsage["contextUsagePercentage"].(float64); ok {
1971+
if ctxPct, ok := kiroTokenUsageFloat64(tokenUsage, "contextUsagePercentage"); ok {
18571972
upstreamContextPercentage = ctxPct
18581973
log.Debugf("kiro: parseEventStream found contextUsagePercentage in tokenUsage: %.2f%%", ctxPct)
18591974
}
@@ -2103,15 +2218,10 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroclaude.Ki
21032218
// Use contextUsagePercentage to calculate more accurate input tokens
21042219
// Kiro model has 200k max context, contextUsagePercentage represents the percentage used
21052220
// Formula: input_tokens = contextUsagePercentage * 200000 / 100
2106-
if upstreamContextPercentage > 0 {
2107-
calculatedInputTokens := int64(upstreamContextPercentage * 200000 / 100)
2108-
if calculatedInputTokens > 0 {
2109-
localEstimate := usageInfo.InputTokens
2110-
usageInfo.InputTokens = calculatedInputTokens
2111-
usageInfo.TotalTokens = usageInfo.InputTokens + usageInfo.OutputTokens
2112-
log.Infof("kiro: parseEventStream using contextUsagePercentage (%.2f%%) to calculate input tokens: %d (local estimate was: %d)",
2113-
upstreamContextPercentage, calculatedInputTokens, localEstimate)
2114-
}
2221+
localEstimate := usageInfo.InputTokens
2222+
if calculatedInputTokens, ok := applyKiroContextUsageFallback(&usageInfo, upstreamContextPercentage, hasPreciseTokenUsage); ok {
2223+
log.Infof("kiro: parseEventStream using contextUsagePercentage (%.2f%%) to calculate input tokens: %d (local estimate was: %d)",
2224+
upstreamContextPercentage, calculatedInputTokens, localEstimate)
21152225
}
21162226

21172227
return cleanedContent, toolUses, usageInfo, stopReason, nil
@@ -2344,6 +2454,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
23442454
var upstreamCreditUsage float64 // Credit usage from upstream (e.g., 1.458)
23452455
var upstreamContextPercentage float64 // Context usage percentage from upstream (e.g., 78.56)
23462456
var hasUpstreamUsage bool // Whether we received usage from upstream
2457+
var hasPreciseTokenUsage bool // Whether official tokenUsage supplied precise counts
23472458

23482459
// Translator param for maintaining tool call state across streaming events
23492460
// IMPORTANT: This must persist across all TranslateStream calls
@@ -3165,36 +3276,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
31653276

31663277
// Check for nested tokenUsage object (official format)
31673278
if tokenUsage, ok := metadata["tokenUsage"].(map[string]interface{}); ok {
3168-
// outputTokens - precise output token count
3169-
if outputTokens, ok := tokenUsage["outputTokens"].(float64); ok {
3170-
totalUsage.OutputTokens = int64(outputTokens)
3171-
hasUpstreamUsage = true
3172-
log.Infof("kiro: streamToChannel found precise outputTokens in tokenUsage: %d", totalUsage.OutputTokens)
3173-
}
3174-
// totalTokens - precise total token count
3175-
if totalTokens, ok := tokenUsage["totalTokens"].(float64); ok {
3176-
totalUsage.TotalTokens = int64(totalTokens)
3177-
log.Infof("kiro: streamToChannel found precise totalTokens in tokenUsage: %d", totalUsage.TotalTokens)
3178-
}
3179-
// uncachedInputTokens - input tokens not from cache
3180-
if uncachedInputTokens, ok := tokenUsage["uncachedInputTokens"].(float64); ok {
3181-
totalUsage.InputTokens = int64(uncachedInputTokens)
3279+
if applyKiroTokenUsage(&totalUsage, tokenUsage) {
3280+
hasPreciseTokenUsage = true
31823281
hasUpstreamUsage = true
3183-
log.Infof("kiro: streamToChannel found uncachedInputTokens in tokenUsage: %d", totalUsage.InputTokens)
3184-
}
3185-
// cacheReadInputTokens - tokens read from cache
3186-
if cacheReadTokens, ok := tokenUsage["cacheReadInputTokens"].(float64); ok {
3187-
// Add to input tokens if we have uncached tokens, otherwise use as input
3188-
if totalUsage.InputTokens > 0 {
3189-
totalUsage.InputTokens += int64(cacheReadTokens)
3190-
} else {
3191-
totalUsage.InputTokens = int64(cacheReadTokens)
3192-
}
3193-
hasUpstreamUsage = true
3194-
log.Debugf("kiro: streamToChannel found cacheReadInputTokens in tokenUsage: %d", int64(cacheReadTokens))
3282+
log.Infof("kiro: streamToChannel found tokenUsage input=%d cache_read=%d cache_creation=%d output=%d total=%d",
3283+
totalUsage.InputTokens, totalUsage.CacheReadTokens, totalUsage.CacheCreationTokens, totalUsage.OutputTokens, totalUsage.TotalTokens)
31953284
}
31963285
// contextUsagePercentage - can be used as fallback for input token estimation
3197-
if ctxPct, ok := tokenUsage["contextUsagePercentage"].(float64); ok {
3286+
if ctxPct, ok := kiroTokenUsageFloat64(tokenUsage, "contextUsagePercentage"); ok {
31983287
upstreamContextPercentage = ctxPct
31993288
log.Debugf("kiro: streamToChannel found contextUsagePercentage in tokenUsage: %.2f%%", ctxPct)
32003289
}
@@ -3407,22 +3496,13 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
34073496
// Kiro model has 200k max context, contextUsagePercentage represents the percentage used
34083497
// Formula: input_tokens = contextUsagePercentage * 200000 / 100
34093498
// Note: The effective input context is ~170k (200k - 30k reserved for output)
3410-
if upstreamContextPercentage > 0 {
3411-
// Calculate input tokens from context percentage
3412-
// Using 200k as the base since that's what Kiro reports against
3413-
calculatedInputTokens := int64(upstreamContextPercentage * 200000 / 100)
3414-
3415-
// Only use calculated value if it's significantly different from local estimate
3416-
// This provides more accurate token counts based on upstream data
3417-
if calculatedInputTokens > 0 {
3418-
localEstimate := totalUsage.InputTokens
3419-
totalUsage.InputTokens = calculatedInputTokens
3420-
log.Debugf("kiro: using contextUsagePercentage (%.2f%%) to calculate input tokens: %d (local estimate was: %d)",
3421-
upstreamContextPercentage, calculatedInputTokens, localEstimate)
3422-
}
3499+
localEstimate := totalUsage.InputTokens
3500+
if calculatedInputTokens, ok := applyKiroContextUsageFallback(&totalUsage, upstreamContextPercentage, hasPreciseTokenUsage); ok {
3501+
log.Debugf("kiro: using contextUsagePercentage (%.2f%%) to calculate input tokens: %d (local estimate was: %d)",
3502+
upstreamContextPercentage, calculatedInputTokens, localEstimate)
34233503
}
34243504

3425-
totalUsage.TotalTokens = totalUsage.InputTokens + totalUsage.OutputTokens
3505+
finalizeKiroUsageTotal(&totalUsage)
34263506

34273507
// Log upstream usage information if received
34283508
if hasUpstreamUsage {

internal/runtime/executor/kiro_executor_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
kiroauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/kiro"
88
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
9+
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
910
)
1011

1112
func TestBuildKiroEndpointConfigs(t *testing.T) {
@@ -494,3 +495,89 @@ func TestMapModelToKiro_MapsClaudeOpus47Variants(t *testing.T) {
494495
})
495496
}
496497
}
498+
499+
func TestApplyKiroTokenUsagePreservesCacheTokenBreakdown(t *testing.T) {
500+
detail := usage.Detail{}
501+
ok := applyKiroTokenUsage(&detail, map[string]interface{}{
502+
"uncachedInputTokens": float64(290),
503+
"outputTokens": float64(1),
504+
"totalTokens": float64(4130),
505+
"cacheReadInputTokens": float64(3822),
506+
"cacheWriteInputTokens": float64(17),
507+
})
508+
if !ok {
509+
t.Fatal("applyKiroTokenUsage() = false, want true")
510+
}
511+
if detail.InputTokens != 290 {
512+
t.Fatalf("InputTokens = %d, want 290", detail.InputTokens)
513+
}
514+
if detail.OutputTokens != 1 {
515+
t.Fatalf("OutputTokens = %d, want 1", detail.OutputTokens)
516+
}
517+
if detail.TotalTokens != 4130 {
518+
t.Fatalf("TotalTokens = %d, want 4130", detail.TotalTokens)
519+
}
520+
if detail.CacheReadTokens != 3822 {
521+
t.Fatalf("CacheReadTokens = %d, want 3822", detail.CacheReadTokens)
522+
}
523+
if detail.CacheCreationTokens != 17 {
524+
t.Fatalf("CacheCreationTokens = %d, want 17", detail.CacheCreationTokens)
525+
}
526+
if detail.CachedTokens != 3822 {
527+
t.Fatalf("CachedTokens = %d, want 3822", detail.CachedTokens)
528+
}
529+
}
530+
531+
func TestFinalizeKiroUsageTotalIncludesCacheTokensWhenUpstreamTotalMissing(t *testing.T) {
532+
detail := usage.Detail{
533+
InputTokens: 290,
534+
OutputTokens: 1,
535+
CacheReadTokens: 3822,
536+
CacheCreationTokens: 17,
537+
}
538+
finalizeKiroUsageTotal(&detail)
539+
if detail.TotalTokens != 4130 {
540+
t.Fatalf("TotalTokens = %d, want 4130", detail.TotalTokens)
541+
}
542+
}
543+
544+
func TestKiroContextUsageFallbackDoesNotOverwritePreciseTokenUsage(t *testing.T) {
545+
detail := usage.Detail{}
546+
hasPreciseTokenUsage := applyKiroTokenUsage(&detail, map[string]interface{}{
547+
"uncachedInputTokens": float64(290),
548+
"outputTokens": float64(1),
549+
"totalTokens": float64(4130),
550+
"cacheReadInputTokens": float64(3822),
551+
"cacheWriteInputTokens": float64(17),
552+
"contextUsagePercentage": float64(50),
553+
})
554+
if !hasPreciseTokenUsage {
555+
t.Fatal("applyKiroTokenUsage() = false, want true")
556+
}
557+
if _, applied := applyKiroContextUsageFallback(&detail, 50, hasPreciseTokenUsage); applied {
558+
t.Fatal("applyKiroContextUsageFallback() applied despite precise token usage")
559+
}
560+
if detail.InputTokens != 290 {
561+
t.Fatalf("InputTokens = %d, want precise uncached value 290", detail.InputTokens)
562+
}
563+
if detail.TotalTokens != 4130 {
564+
t.Fatalf("TotalTokens = %d, want upstream total 4130", detail.TotalTokens)
565+
}
566+
}
567+
568+
func TestKiroContextUsageFallbackAppliesWhenPreciseTokenUsageMissing(t *testing.T) {
569+
detail := usage.Detail{OutputTokens: 3}
570+
calculated, applied := applyKiroContextUsageFallback(&detail, 50, false)
571+
if !applied {
572+
t.Fatal("applyKiroContextUsageFallback() applied = false, want true")
573+
}
574+
if calculated != 100000 {
575+
t.Fatalf("calculated input tokens = %d, want 100000", calculated)
576+
}
577+
if detail.InputTokens != 100000 {
578+
t.Fatalf("InputTokens = %d, want 100000", detail.InputTokens)
579+
}
580+
if detail.TotalTokens != 100003 {
581+
t.Fatalf("TotalTokens = %d, want 100003", detail.TotalTokens)
582+
}
583+
}

0 commit comments

Comments
 (0)