Skip to content

Commit 6644202

Browse files
authored
Merge pull request RooCodeInc#1451 from dtrugman/feat/add-openai-style-cost-calculation
Add openai style cost calculation
2 parents 01b822e + 03045d8 commit 6644202

File tree

7 files changed

+199
-45
lines changed

7 files changed

+199
-45
lines changed

src/api/providers/__tests__/requesty.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
2222
contextWindow: 4000,
2323
supportsPromptCache: false,
2424
supportsImages: true,
25-
inputPrice: 0,
26-
outputPrice: 0,
25+
inputPrice: 1,
26+
outputPrice: 10,
27+
cacheReadsPrice: 0.1,
28+
cacheWritesPrice: 1.5,
2729
},
2830
openAiStreamingEnabled: true,
2931
includeMaxTokens: true, // Add this to match the implementation
@@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
8385
yield {
8486
choices: [{ delta: { content: " world" } }],
8587
usage: {
86-
prompt_tokens: 10,
87-
completion_tokens: 5,
88+
prompt_tokens: 30,
89+
completion_tokens: 10,
90+
prompt_tokens_details: {
91+
cached_tokens: 15,
92+
caching_tokens: 5,
93+
},
8894
},
8995
}
9096
},
@@ -105,10 +111,11 @@ describe("RequestyHandler", () => {
105111
{ type: "text", text: " world" },
106112
{
107113
type: "usage",
108-
inputTokens: 10,
109-
outputTokens: 5,
110-
cacheWriteTokens: undefined,
111-
cacheReadTokens: undefined,
114+
inputTokens: 30,
115+
outputTokens: 10,
116+
cacheWriteTokens: 5,
117+
cacheReadTokens: 15,
118+
totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
112119
},
113120
])
114121

@@ -182,6 +189,9 @@ describe("RequestyHandler", () => {
182189
type: "usage",
183190
inputTokens: 10,
184191
outputTokens: 5,
192+
cacheWriteTokens: 0,
193+
cacheReadTokens: 0,
194+
totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
185195
},
186196
])
187197

src/api/providers/openai.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
116116
}
117117
}
118118
if (chunk.usage) {
119-
yield this.processUsageMetrics(chunk.usage)
119+
yield this.processUsageMetrics(chunk.usage, modelInfo)
120120
}
121121
}
122122
} else {
@@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
139139
type: "text",
140140
text: response.choices[0]?.message.content || "",
141141
}
142-
yield this.processUsageMetrics(response.usage)
142+
yield this.processUsageMetrics(response.usage, modelInfo)
143143
}
144144
}
145145

146-
protected processUsageMetrics(usage: any): ApiStreamUsageChunk {
146+
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
147147
return {
148148
type: "usage",
149149
inputTokens: usage?.prompt_tokens || 0,

src/api/providers/requesty.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import axios from "axios"
22

33
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
4-
import { parseApiPrice } from "../../utils/cost"
4+
import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
55
import { ApiStreamUsageChunk } from "../transform/stream"
66
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
7+
import OpenAI from "openai"
8+
9+
// Requesty usage includes an extra field for Anthropic use cases.
10+
// Safely cast the prompt token details section to the appropriate structure.
11+
interface RequestyUsage extends OpenAI.CompletionUsage {
12+
prompt_tokens_details?: {
13+
caching_tokens?: number
14+
cached_tokens?: number
15+
}
16+
total_cost?: number
17+
}
718

819
export class RequestyHandler extends OpenAiHandler {
920
constructor(options: OpenAiHandlerOptions) {
@@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler {
2738
}
2839
}
2940

30-
protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
41+
protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
42+
const requestyUsage = usage as RequestyUsage
43+
const inputTokens = requestyUsage?.prompt_tokens || 0
44+
const outputTokens = requestyUsage?.completion_tokens || 0
45+
const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
46+
const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
47+
const totalCost = modelInfo
48+
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
49+
: 0
3150
return {
3251
type: "usage",
33-
inputTokens: usage?.prompt_tokens || 0,
34-
outputTokens: usage?.completion_tokens || 0,
35-
cacheWriteTokens: usage?.cache_creation_input_tokens,
36-
cacheReadTokens: usage?.cache_read_input_tokens,
52+
inputTokens: inputTokens,
53+
outputTokens: outputTokens,
54+
cacheWriteTokens: cacheWriteTokens,
55+
cacheReadTokens: cacheReadTokens,
56+
totalCost: totalCost,
3757
}
3858
}
3959
}

src/api/providers/vscode-lm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import * as vscode from "vscode"
33

44
import { SingleCompletionHandler } from "../"
5-
import { calculateApiCost } from "../../utils/cost"
5+
import { calculateApiCostAnthropic } from "../../utils/cost"
66
import { ApiStream } from "../transform/stream"
77
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
88
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
462462
type: "usage",
463463
inputTokens: totalInputTokens,
464464
outputTokens: totalOutputTokens,
465-
totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
465+
totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
466466
}
467467
} catch (error: unknown) {
468468
this.ensureCleanState()

src/core/Cline.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
5555
import { GlobalFileNames } from "../shared/globalFileNames"
5656
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
5757
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
58-
import { calculateApiCost } from "../utils/cost"
58+
import { calculateApiCostAnthropic } from "../utils/cost"
5959
import { fileExistsAtPath } from "../utils/fs"
6060
import { arePathsEqual, getReadablePath } from "../utils/path"
6161
import { parseMentions } from "./mentions"
@@ -875,7 +875,7 @@ export class Cline {
875875
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
876876
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
877877

878-
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
878+
//const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
879879
if (didEndLoop) {
880880
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
881881
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
@@ -3173,7 +3173,7 @@ export class Cline {
31733173
cacheReads: cacheReadTokens,
31743174
cost:
31753175
totalCost ??
3176-
calculateApiCost(
3176+
calculateApiCostAnthropic(
31773177
this.api.getModel().info,
31783178
inputTokens,
31793179
outputTokens,

src/utils/__tests__/cost.test.ts

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { calculateApiCost } from "../cost"
1+
import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
22
import { ModelInfo } from "../../shared/api"
33

44
describe("Cost Utility", () => {
5-
describe("calculateApiCost", () => {
5+
describe("calculateApiCostAnthropic", () => {
66
const mockModelInfo: ModelInfo = {
77
maxTokens: 8192,
88
contextWindow: 200_000,
@@ -14,7 +14,7 @@ describe("Cost Utility", () => {
1414
}
1515

1616
it("should calculate basic input/output costs correctly", () => {
17-
const cost = calculateApiCost(mockModelInfo, 1000, 500)
17+
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
1818

1919
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
2020
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -23,7 +23,7 @@ describe("Cost Utility", () => {
2323
})
2424

2525
it("should handle cache writes cost", () => {
26-
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000)
26+
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000)
2727

2828
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
2929
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -33,7 +33,7 @@ describe("Cost Utility", () => {
3333
})
3434

3535
it("should handle cache reads cost", () => {
36-
const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000)
36+
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000)
3737

3838
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
3939
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -43,7 +43,7 @@ describe("Cost Utility", () => {
4343
})
4444

4545
it("should handle all cost components together", () => {
46-
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000)
46+
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000)
4747

4848
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
4949
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -60,17 +60,17 @@ describe("Cost Utility", () => {
6060
supportsPromptCache: true,
6161
}
6262

63-
const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
63+
const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
6464
expect(cost).toBe(0)
6565
})
6666

6767
it("should handle zero tokens", () => {
68-
const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0)
68+
const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)
6969
expect(cost).toBe(0)
7070
})
7171

7272
it("should handle undefined cache values", () => {
73-
const cost = calculateApiCost(mockModelInfo, 1000, 500)
73+
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
7474

7575
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
7676
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -85,7 +85,7 @@ describe("Cost Utility", () => {
8585
cacheReadsPrice: undefined,
8686
}
8787

88-
const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000)
88+
const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000)
8989

9090
// Should only include input and output costs
9191
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
@@ -94,4 +94,97 @@ describe("Cost Utility", () => {
9494
expect(cost).toBe(0.0105)
9595
})
9696
})
97+
98+
describe("calculateApiCostOpenAI", () => {
99+
const mockModelInfo: ModelInfo = {
100+
maxTokens: 8192,
101+
contextWindow: 200_000,
102+
supportsPromptCache: true,
103+
inputPrice: 3.0, // $3 per million tokens
104+
outputPrice: 15.0, // $15 per million tokens
105+
cacheWritesPrice: 3.75, // $3.75 per million tokens
106+
cacheReadsPrice: 0.3, // $0.30 per million tokens
107+
}
108+
109+
it("should calculate basic input/output costs correctly", () => {
110+
const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
111+
112+
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
113+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
114+
// Total: 0.003 + 0.0075 = 0.0105
115+
expect(cost).toBe(0.0105)
116+
})
117+
118+
it("should handle cache writes cost", () => {
119+
const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000)
120+
121+
// Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003
122+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
123+
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
124+
// Total: 0.003 + 0.0075 + 0.0075 = 0.018
125+
expect(cost).toBeCloseTo(0.018, 6)
126+
})
127+
128+
it("should handle cache reads cost", () => {
129+
const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000)
130+
131+
// Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003
132+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
133+
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
134+
// Total: 0.003 + 0.0075 + 0.0009 = 0.0114
135+
expect(cost).toBe(0.0114)
136+
})
137+
138+
it("should handle all cost components together", () => {
139+
const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000)
140+
141+
// Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
142+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
143+
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
144+
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
145+
// Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
146+
expect(cost).toBe(0.0189)
147+
})
148+
149+
it("should handle missing prices gracefully", () => {
150+
const modelWithoutPrices: ModelInfo = {
151+
maxTokens: 8192,
152+
contextWindow: 200_000,
153+
supportsPromptCache: true,
154+
}
155+
156+
const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000)
157+
expect(cost).toBe(0)
158+
})
159+
160+
it("should handle zero tokens", () => {
161+
const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)
162+
expect(cost).toBe(0)
163+
})
164+
165+
it("should handle undefined cache values", () => {
166+
const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
167+
168+
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
169+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
170+
// Total: 0.003 + 0.0075 = 0.0105
171+
expect(cost).toBe(0.0105)
172+
})
173+
174+
it("should handle missing cache prices", () => {
175+
const modelWithoutCachePrices: ModelInfo = {
176+
...mockModelInfo,
177+
cacheWritesPrice: undefined,
178+
cacheReadsPrice: undefined,
179+
}
180+
181+
const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000)
182+
183+
// Should only include input and output costs
184+
// Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
185+
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
186+
// Total: 0.003 + 0.0075 = 0.0105
187+
expect(cost).toBe(0.0105)
188+
})
189+
})
97190
})

0 commit comments

Comments
 (0)