Skip to content

Commit 0ecae9d

Browse files
fix: exclude cache tokens from context window calculation (RooCodeInc#5603)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 99448fc commit 0ecae9d

File tree

6 files changed

+40
-14
lines changed

6 files changed

+40
-14
lines changed

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export const clineMessageSchema = z.object({
155155
progressStatus: toolProgressStatusSchema.optional(),
156156
contextCondense: contextCondenseSchema.optional(),
157157
isProtected: z.boolean().optional(),
158+
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),
158159
})
159160

160161
export type ClineMessage = z.infer<typeof clineMessageSchema>

packages/types/src/provider-settings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,11 @@ export const getModelId = (settings: ProviderSettings): string | undefined => {
292292
const modelIdKey = MODEL_ID_KEYS.find((key) => settings[key])
293293
return modelIdKey ? (settings[modelIdKey] as string) : undefined
294294
}
295+
296+
// Providers that use Anthropic-style API protocol
297+
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code"]
298+
299+
// Helper function to determine API protocol for a provider
300+
export const getApiProtocol = (provider: ProviderName | undefined): "anthropic" | "openai" => {
301+
return provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider) ? "anthropic" : "openai"
302+
}

src/core/task/Task.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type HistoryItem,
2222
TelemetryEventName,
2323
TodoItem,
24+
getApiProtocol,
2425
} from "@roo-code/types"
2526
import { TelemetryService } from "@roo-code/telemetry"
2627
import { CloudService } from "@roo-code/cloud"
@@ -1207,11 +1208,16 @@ export class Task extends EventEmitter<ClineEvents> {
12071208
// top-down build file structure of project which for large projects can
12081209
// take a few seconds. For the best UX we show a placeholder api_req_started
12091210
// message with a loading spinner as this happens.
1211+
1212+
// Determine API protocol based on provider
1213+
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider)
1214+
12101215
await this.say(
12111216
"api_req_started",
12121217
JSON.stringify({
12131218
request:
12141219
userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...",
1220+
apiProtocol,
12151221
}),
12161222
)
12171223

@@ -1243,6 +1249,7 @@ export class Task extends EventEmitter<ClineEvents> {
12431249

12441250
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
12451251
request: finalUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
1252+
apiProtocol,
12461253
} satisfies ClineApiReqInfo)
12471254

12481255
await this.saveClineMessages()
@@ -1263,8 +1270,9 @@ export class Task extends EventEmitter<ClineEvents> {
12631270
// of prices in tasks from history (it's worth removing a few months
12641271
// from now).
12651272
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
1273+
const existingData = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}")
12661274
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
1267-
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
1275+
...existingData,
12681276
tokensIn: inputTokens,
12691277
tokensOut: outputTokens,
12701278
cacheWrites: cacheWriteTokens,

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export interface ClineApiReqInfo {
379379
cost?: number
380380
cancelReason?: ClineApiReqCancelReason
381381
streamingFailedMessage?: string
382+
apiProtocol?: "anthropic" | "openai"
382383
}
383384

384385
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

src/shared/__tests__/getApiMetrics.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe("getApiMetrics", () => {
6161
expect(result.totalCacheWrites).toBe(5)
6262
expect(result.totalCacheReads).toBe(10)
6363
expect(result.totalCost).toBe(0.005)
64-
expect(result.contextTokens).toBe(315) // 100 + 200 + 5 + 10
64+
expect(result.contextTokens).toBe(300) // 100 + 200 (OpenAI default, no cache tokens)
6565
})
6666

6767
it("should calculate metrics from multiple api_req_started messages", () => {
@@ -83,7 +83,7 @@ describe("getApiMetrics", () => {
8383
expect(result.totalCacheWrites).toBe(8) // 5 + 3
8484
expect(result.totalCacheReads).toBe(17) // 10 + 7
8585
expect(result.totalCost).toBe(0.008) // 0.005 + 0.003
86-
expect(result.contextTokens).toBe(210) // 50 + 150 + 3 + 7 (from the last message)
86+
expect(result.contextTokens).toBe(200) // 50 + 150 (OpenAI default, no cache tokens)
8787
})
8888

8989
it("should calculate metrics from condense_context messages", () => {
@@ -123,7 +123,7 @@ describe("getApiMetrics", () => {
123123
expect(result.totalCacheWrites).toBe(8) // 5 + 3
124124
expect(result.totalCacheReads).toBe(17) // 10 + 7
125125
expect(result.totalCost).toBe(0.01) // 0.005 + 0.002 + 0.003
126-
expect(result.contextTokens).toBe(210) // 50 + 150 + 3 + 7 (from the last api_req_started message)
126+
expect(result.contextTokens).toBe(200) // 50 + 150 (OpenAI default, no cache tokens)
127127
})
128128
})
129129

@@ -242,9 +242,9 @@ describe("getApiMetrics", () => {
242242
expect(result.totalCacheReads).toBe(10)
243243
expect(result.totalCost).toBe(0.005)
244244

245-
// The implementation will use the last message with tokens for contextTokens
246-
// In this case, it's the cacheReads message
247-
expect(result.contextTokens).toBe(10)
245+
// The implementation will use the last message that has any tokens
246+
// In this case, it's the message with tokensOut:200 (since the last few messages have no tokensIn/Out)
247+
expect(result.contextTokens).toBe(200) // 0 + 200 (from the tokensOut message)
248248
})
249249

250250
it("should handle non-number values in api_req_started message", () => {
@@ -264,8 +264,8 @@ describe("getApiMetrics", () => {
264264
expect(result.totalCacheReads).toBeUndefined()
265265
expect(result.totalCost).toBe(0)
266266

267-
// The implementation concatenates string values for contextTokens
268-
expect(result.contextTokens).toBe("not-a-numbernot-a-numbernot-a-numbernot-a-number")
267+
// The implementation concatenates all token values including cache tokens
268+
expect(result.contextTokens).toBe("not-a-numbernot-a-number") // tokensIn + tokensOut (OpenAI default)
269269
})
270270
})
271271

@@ -279,7 +279,7 @@ describe("getApiMetrics", () => {
279279
const result = getApiMetrics(messages)
280280

281281
// Should use the values from the last api_req_started message
282-
expect(result.contextTokens).toBe(210) // 50 + 150 + 3 + 7
282+
expect(result.contextTokens).toBe(200) // 50 + 150 (OpenAI default, no cache tokens)
283283
})
284284

285285
it("should calculate contextTokens from the last condense_context message", () => {
@@ -305,7 +305,7 @@ describe("getApiMetrics", () => {
305305
const result = getApiMetrics(messages)
306306

307307
// Should use the values from the last api_req_started message
308-
expect(result.contextTokens).toBe(210) // 50 + 150 + 3 + 7
308+
expect(result.contextTokens).toBe(200) // 50 + 150 (OpenAI default, no cache tokens)
309309
})
310310

311311
it("should handle missing values when calculating contextTokens", () => {
@@ -320,7 +320,7 @@ describe("getApiMetrics", () => {
320320
const result = getApiMetrics(messages)
321321

322322
// Should handle missing or invalid values
323-
expect(result.contextTokens).toBe(15) // 0 + 0 + 5 + 10
323+
expect(result.contextTokens).toBe(0) // 0 + 0 (OpenAI default, no cache tokens)
324324

325325
// Restore console.error
326326
console.error = originalConsoleError

src/shared/getApiMetrics.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type ParsedApiReqStartedTextType = {
66
cacheWrites: number
77
cacheReads: number
88
cost?: number // Only present if combineApiRequests has been called
9+
apiProtocol?: "anthropic" | "openai"
910
}
1011

1112
/**
@@ -72,8 +73,15 @@ export function getApiMetrics(messages: ClineMessage[]) {
7273
if (message.type === "say" && message.say === "api_req_started" && message.text) {
7374
try {
7475
const parsedText: ParsedApiReqStartedTextType = JSON.parse(message.text)
75-
const { tokensIn, tokensOut, cacheWrites, cacheReads } = parsedText
76-
result.contextTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
76+
const { tokensIn, tokensOut, cacheWrites, cacheReads, apiProtocol } = parsedText
77+
78+
// Calculate context tokens based on API protocol
79+
if (apiProtocol === "anthropic") {
80+
result.contextTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
81+
} else {
82+
// For OpenAI (or when protocol is not specified)
83+
result.contextTokens = (tokensIn || 0) + (tokensOut || 0)
84+
}
7785
} catch (error) {
7886
console.error("Error parsing JSON:", error)
7987
continue

0 commit comments

Comments
 (0)