Skip to content

Commit c4b14c5

Browse files
committed
fix: address PR review feedback for underreported token usage
- Add error handling in drainStreamInBackgroundToFindAllUsage with try-catch - Add 30-second timeout to prevent potential memory leaks from hanging streams - Include model ID in console warning for better debugging - Fix race condition by moving updateApiReqMsg() call inside background task - Ensure saveClineMessages() is called after usage data is updated
1 parent 60f0a03 commit c4b14c5

File tree

1 file changed

+71
-35
lines changed

1 file changed

+71
-35
lines changed

src/core/task/Task.ts

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,43 +1435,78 @@ export class Task extends EventEmitter<ClineEvents> {
14351435
}
14361436

14371437
const drainStreamInBackgroundToFindAllUsage = async () => {
1438-
let usageFound = false
1439-
while (!item.done) {
1440-
const chunk = item.value
1441-
item = await iterator.next()
1442-
if (chunk && chunk.type === "usage") {
1443-
usageFound = true
1444-
inputTokens += chunk.inputTokens
1445-
outputTokens += chunk.outputTokens
1446-
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
1447-
cacheReadTokens += chunk.cacheReadTokens ?? 0
1448-
totalCost = chunk.totalCost
1438+
const timeoutMs = 30000 // 30 second timeout
1439+
const startTime = Date.now()
1440+
1441+
try {
1442+
let usageFound = false
1443+
while (!item.done) {
1444+
// Check for timeout
1445+
if (Date.now() - startTime > timeoutMs) {
1446+
console.warn(`Background usage collection timed out after ${timeoutMs}ms`)
1447+
break
1448+
}
1449+
1450+
const chunk = item.value
1451+
item = await iterator.next()
1452+
if (chunk && chunk.type === "usage") {
1453+
usageFound = true
1454+
inputTokens += chunk.inputTokens
1455+
outputTokens += chunk.outputTokens
1456+
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
1457+
cacheReadTokens += chunk.cacheReadTokens ?? 0
1458+
totalCost = chunk.totalCost
1459+
}
1460+
}
1461+
if (usageFound) {
1462+
updateApiReqMsg()
1463+
await this.saveClineMessages()
1464+
}
1465+
if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
1466+
TelemetryService.instance.captureLlmCompletion(this.taskId, {
1467+
inputTokens,
1468+
outputTokens,
1469+
cacheWriteTokens,
1470+
cacheReadTokens,
1471+
cost:
1472+
totalCost ??
1473+
calculateApiCostAnthropic(
1474+
this.api.getModel().info,
1475+
inputTokens,
1476+
outputTokens,
1477+
cacheWriteTokens,
1478+
cacheReadTokens,
1479+
),
1480+
})
1481+
} else {
1482+
const modelId = getModelId(this.apiConfiguration)
1483+
console.warn(
1484+
`Suspicious: request ${lastApiReqIndex} is complete, but no usage info was found. Model: ${modelId}`,
1485+
)
1486+
}
1487+
} catch (error) {
1488+
console.error("Error draining stream for usage data:", error)
1489+
// Still try to capture whatever usage data we have collected so far
1490+
if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
1491+
TelemetryService.instance.captureLlmCompletion(this.taskId, {
1492+
inputTokens,
1493+
outputTokens,
1494+
cacheWriteTokens,
1495+
cacheReadTokens,
1496+
cost:
1497+
totalCost ??
1498+
calculateApiCostAnthropic(
1499+
this.api.getModel().info,
1500+
inputTokens,
1501+
outputTokens,
1502+
cacheWriteTokens,
1503+
cacheReadTokens,
1504+
),
1505+
})
14491506
}
1450-
}
1451-
if (usageFound) {
1452-
updateApiReqMsg()
1453-
}
1454-
if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
1455-
TelemetryService.instance.captureLlmCompletion(this.taskId, {
1456-
inputTokens,
1457-
outputTokens,
1458-
cacheWriteTokens,
1459-
cacheReadTokens,
1460-
cost:
1461-
totalCost ??
1462-
calculateApiCostAnthropic(
1463-
this.api.getModel().info,
1464-
inputTokens,
1465-
outputTokens,
1466-
cacheWriteTokens,
1467-
cacheReadTokens,
1468-
),
1469-
})
1470-
} else {
1471-
console.warn(`Suspicious: request ${lastApiReqIndex} is complete, but no usage info was found.`)
14721507
}
14731508
}
1474-
drainStreamInBackgroundToFindAllUsage() // no await so it runs in the background
1509+
const backgroundDrainPromise = drainStreamInBackgroundToFindAllUsage() // Store promise reference
14751510
} catch (error) {
14761511
// Abandoned happens when extension is no longer waiting for the
14771512
// Cline instance to finish aborting (error is thrown here when
@@ -1534,7 +1569,8 @@ export class Task extends EventEmitter<ClineEvents> {
15341569
presentAssistantMessage(this)
15351570
}
15361571

1537-
updateApiReqMsg()
1572+
// Note: updateApiReqMsg() is now called from within drainStreamInBackgroundToFindAllUsage
1573+
// to avoid race conditions with the background task
15381574
await this.saveClineMessages()
15391575
await this.providerRef.deref()?.postStateToWebview()
15401576

0 commit comments

Comments
 (0)