-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcost.go
More file actions
442 lines (368 loc) · 14.6 KB
/
cost.go
File metadata and controls
442 lines (368 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
package ai
import (
"fmt"
"sync"
)
// ═══════════════════════════════════════════════════════════════════════════
// Cost Tracking
// ═══════════════════════════════════════════════════════════════════════════
// ModelPricing contains pricing per 1M tokens for a model.
type ModelPricing struct {
InputPerMillion float64 // USD per 1M input tokens
OutputPerMillion float64 // USD per 1M output tokens
}
// ModelPricingMap maps models to their pricing.
var ModelPricingMap = map[Model]ModelPricing{
// OpenAI Models
// Prices per 1M tokens (input/output) from OpenAI pricing table provided by user.
// Note: OpenAI has separate pricing for cached input tokens, audio tokens, image tokens,
// and per-image / per-second endpoints. This library currently tracks only standard
// text token usage from model responses, so costs are "text token" estimates.
ModelGPT52: {1.75, 14.00},
ModelGPT51: {1.25, 10.00},
ModelGPT5Base: {1.25, 10.00},
ModelGPT5Mini: {0.25, 2.00},
ModelGPT5Nano: {0.05, 0.40},
ModelGPT52Pro: {21.00, 168.00},
ModelGPT5Pro: {15.00, 120.00},
// Codex (priced like gpt-5.1 in the table)
ModelGPT51CodexMax: {1.25, 10.00},
ModelGPT51Codex: {1.25, 10.00},
ModelGPT5CodexBase: {1.25, 10.00},
ModelGPT51CodexMini: {0.25, 2.00},
ModelCodexMiniLatest: {1.50, 6.00},
// Search + agent tools
ModelGPT5SearchAPI: {1.25, 10.00},
ModelComputerUsePreview: {3.00, 12.00},
// GPT-5 chat-latest variants
ModelGPT52ChatLatest: {1.75, 14.00},
ModelGPT51ChatLatest: {1.25, 10.00},
ModelGPT5ChatLatest: {1.25, 10.00},
// GPT-4.1 family
ModelGPT41: {2.00, 8.00},
ModelGPT41Mini: {0.40, 1.60},
ModelGPT41Nano: {0.10, 0.40},
// GPT-4o family
ModelGPT4o: {2.50, 10.00},
ModelGPT4o20240513: {5.00, 15.00},
ModelGPT4oMini: {0.15, 0.60},
// Realtime / audio models (text token pricing)
ModelGPTRealtime: {4.00, 16.00},
ModelGPTRealtimeMini: {0.60, 2.40},
ModelGPT4oRealtimePreview: {5.00, 20.00},
ModelGPT4oMiniRealtimePreview: {0.60, 2.40},
ModelGPTAudio: {2.50, 10.00},
ModelGPTAudioMini: {0.60, 2.40},
ModelGPT4oAudioPreview: {2.50, 10.00},
ModelGPT4oMiniAudioPreview: {0.15, 0.60},
// Search previews
ModelGPT4oMiniSearchPreview: {0.15, 0.60},
ModelGPT4oSearchPreview: {2.50, 10.00},
// ChatGPT aliases / special models
ModelChatGPT4oLatest: {5.00, 15.00}, // legacy table
// o-series reasoning
ModelO1: {15.00, 60.00},
ModelO1Mini: {1.10, 4.40},
ModelO1Pro: {150.00, 600.00},
ModelO3: {2.00, 8.00},
ModelO3Mini: {1.10, 4.40},
ModelO3Pro: {20.00, 80.00},
ModelO3DeepResearch: {10.00, 40.00},
ModelO4Mini: {1.10, 4.40},
ModelO4MiniDeepResearch: {2.00, 8.00},
// Image generation models (text token pricing table)
ModelGPTImage15: {5.00, 10.00},
ModelChatGPTImageLatest: {5.00, 10.00},
ModelGPTImage1: {5.00, 0.00}, // output "-" in table (image outputs are priced differently)
ModelGPTImage1Mini: {2.00, 0.00}, // output "-" in table
// Anthropic Models
// Latest (Claude 4.5)
ModelClaudeOpus: {5.00, 25.00},
ModelClaudeSonnet: {3.00, 15.00},
ModelClaudeHaiku: {1.00, 5.00},
// Legacy / still available
ModelClaudeOpus41: {15.00, 75.00},
ModelClaudeOpus4: {15.00, 75.00},
ModelClaudeSonnet4: {3.00, 15.00},
// Deprecated / legacy snapshots
ModelClaudeSonnet37: {3.00, 15.00},
ModelClaudeHaiku35: {0.80, 4.00},
ModelClaudeHaiku3: {0.25, 1.25},
ModelClaudeOpus3: {15.00, 75.00},
ModelClaudeSonnet3: {3.00, 15.00},
// Google Models
ModelGemini3Pro: {2.00, 12.00}, // Gemini 3 Pro (preview)
ModelGemini3Flash: {0.50, 3.00}, // Gemini 3 Flash (preview)
ModelGemini25Pro: {1.25, 10.00},
ModelGemini25Flash: {0.30, 2.50},
ModelGemini25FlashLite: {0.10, 0.40},
ModelGemini2Flash: {0.10, 0.40}, // Gemini 2.0 Flash (-001 on OpenRouter)
ModelGemini2FlashLite: {0.075, 0.30}, // Gemini 2.0 Flash Lite (-001 on OpenRouter)
// xAI Models
ModelGrok3: {3.00, 12.00},
ModelGrok3Mini: {0.50, 2.00},
ModelGrok41Fast: {2.00, 8.00},
// Other Models
ModelQwen3Next: {1.00, 4.00},
ModelQwen3: {2.00, 8.00},
ModelLlama4: {1.00, 4.00},
ModelMistralLarge: {2.00, 6.00},
}
// EmbeddingPricingMap maps embedding models to their pricing (per 1M tokens).
var EmbeddingPricingMap = map[EmbeddingModel]float64{
EmbedTextSmall3: 0.02,
EmbedTextLarge3: 0.13,
EmbedTextAda002: 0.10,
EmbedGecko: 0.0001, // Google pricing is very low
EmbedGeckoLatest: 0.0001,
}
// AudioPricingMap maps audio models to their pricing (per minute or per 1M chars).
var AudioPricingMap = map[string]float64{
// TTS pricing per 1M characters
string(TTSTTS1): 15.00,
string(TTSTTS1HD): 30.00,
// STT pricing per minute
string(STTWhisper1): 0.006,
}
// ═══════════════════════════════════════════════════════════════════════════
// Cost Calculation
// ═══════════════════════════════════════════════════════════════════════════
// CalculateCost calculates the estimated cost for a request in USD.
func CalculateCost(model Model, promptTokens, completionTokens int) float64 {
pricing, ok := ModelPricingMap[model]
if !ok {
// Use default pricing if model not found
pricing = ModelPricing{2.50, 10.00}
}
inputCost := float64(promptTokens) / 1_000_000 * pricing.InputPerMillion
outputCost := float64(completionTokens) / 1_000_000 * pricing.OutputPerMillion
return inputCost + outputCost
}
// CalculateEmbeddingCost calculates estimated embedding cost in USD.
func CalculateEmbeddingCost(model EmbeddingModel, tokens int) float64 {
pricing, ok := EmbeddingPricingMap[model]
if !ok {
pricing = 0.02 // default
}
return float64(tokens) / 1_000_000 * pricing
}
// CalculateTTSCost calculates estimated text-to-speech cost in USD (per character).
func CalculateTTSCost(model TTSModel, characters int) float64 {
pricing, ok := AudioPricingMap[string(model)]
if !ok {
pricing = 15.00 // default
}
return float64(characters) / 1_000_000 * pricing
}
// CalculateSTTCost calculates estimated speech-to-text cost in USD (per minute).
func CalculateSTTCost(model STTModel, durationSeconds float64) float64 {
pricing, ok := AudioPricingMap[string(model)]
if !ok {
pricing = 0.006 // default
}
return (durationSeconds / 60) * pricing
}
// ═══════════════════════════════════════════════════════════════════════════
// Cost in ResponseMeta
// ═══════════════════════════════════════════════════════════════════════════
// Cost returns the estimated cost for this response in USD.
func (m *ResponseMeta) Cost() float64 {
return CalculateCost(m.Model, m.PromptTokens, m.CompletionTokens)
}
// CostString returns a formatted USD cost string.
func (m *ResponseMeta) CostString() string {
cost := m.Cost()
if cost < 0.01 {
return fmt.Sprintf("$%.6f", cost)
}
return fmt.Sprintf("$%.4f", cost)
}
// ═══════════════════════════════════════════════════════════════════════════
// Cost Tracker - Accumulated Costs
// ═══════════════════════════════════════════════════════════════════════════
// CostTracker tracks accumulated costs across many responses.
type CostTracker struct {
mu sync.Mutex
TotalCost float64
RequestCount int
TokensUsed int
CostByModel map[Model]float64
}
// NewCostTracker creates a new CostTracker.
func NewCostTracker() *CostTracker {
return &CostTracker{
CostByModel: make(map[Model]float64),
}
}
// Track records a response's cost.
func (ct *CostTracker) Track(meta *ResponseMeta) {
ct.mu.Lock()
defer ct.mu.Unlock()
cost := meta.Cost()
ct.TotalCost += cost
ct.RequestCount++
ct.TokensUsed += meta.Tokens
ct.CostByModel[meta.Model] += cost
}
// Reset clears all tracked costs.
func (ct *CostTracker) Reset() {
ct.mu.Lock()
defer ct.mu.Unlock()
ct.TotalCost = 0
ct.RequestCount = 0
ct.TokensUsed = 0
ct.CostByModel = make(map[Model]float64)
}
// Summary returns a formatted cost summary.
func (ct *CostTracker) Summary() string {
ct.mu.Lock()
defer ct.mu.Unlock()
if ct.RequestCount == 0 {
return "No requests tracked"
}
avgCost := ct.TotalCost / float64(ct.RequestCount)
summary := fmt.Sprintf(`
Cost Summary:
Total Cost: $%.4f
Request Count: %d
Tokens Used: %d
Avg Cost/Req: $%.6f
Cost by Model:`,
ct.TotalCost, ct.RequestCount, ct.TokensUsed, avgCost)
for model, cost := range ct.CostByModel {
summary += fmt.Sprintf("\n - %s: $%.4f", model, cost)
}
return summary
}
// Print prints the cost summary to stdout.
func (ct *CostTracker) Print() {
fmt.Println(ct.Summary())
}
// ═══════════════════════════════════════════════════════════════════════════
// Global Cost Tracking (opt-in)
// ═══════════════════════════════════════════════════════════════════════════
var (
globalCostTracker *CostTracker
globalCostTrackerLock sync.Mutex
)
// EnableCostTracking enables package-level cost tracking (opt-in).
func EnableCostTracking() {
globalCostTrackerLock.Lock()
defer globalCostTrackerLock.Unlock()
if globalCostTracker == nil {
globalCostTracker = NewCostTracker()
}
}
// GetCostTracker returns the package-level cost tracker (creating one if needed).
func GetCostTracker() *CostTracker {
globalCostTrackerLock.Lock()
defer globalCostTrackerLock.Unlock()
if globalCostTracker == nil {
globalCostTracker = NewCostTracker()
}
return globalCostTracker
}
// TotalCost returns the total cost tracked by the package-level tracker.
func TotalCost() float64 {
return GetCostTracker().TotalCost
}
// PrintCostSummary prints the package-level cost summary.
func PrintCostSummary() {
GetCostTracker().Print()
}
// ═══════════════════════════════════════════════════════════════════════════
// Cost-Aware Helpers
// ═══════════════════════════════════════════════════════════════════════════
// EstimatePromptCost estimates prompt cost in USD before sending, using a rough token heuristic.
func EstimatePromptCost(model Model, promptChars int) float64 {
// Rough token estimate: 1 token ≈ 4 characters
estimatedTokens := promptChars / 4
inputPerMillion := 2.50
if pricing, ok := ModelPricingMap[model]; ok {
inputPerMillion = pricing.InputPerMillion
}
return float64(estimatedTokens) / 1_000_000 * inputPerMillion
}
// CheapestModel returns the cheapest model (by average in/out pricing) from a list.
func CheapestModel(models ...Model) Model {
if len(models) == 0 {
return ModelGPT4oMini // default cheap model
}
cheapest := models[0]
cheapestCost := float64(999999)
for _, m := range models {
pricing, ok := ModelPricingMap[m]
if !ok {
continue
}
avgCost := (pricing.InputPerMillion + pricing.OutputPerMillion) / 2
if avgCost < cheapestCost {
cheapestCost = avgCost
cheapest = m
}
}
return cheapest
}
// MostExpensiveModel returns the most expensive model (by average in/out pricing) from a list.
func MostExpensiveModel(models ...Model) Model {
if len(models) == 0 {
return ModelClaudeOpus // default high-end model
}
expensive := models[0]
highestCost := float64(0)
for _, m := range models {
pricing, ok := ModelPricingMap[m]
if !ok {
continue
}
avgCost := (pricing.InputPerMillion + pricing.OutputPerMillion) / 2
if avgCost > highestCost {
highestCost = avgCost
expensive = m
}
}
return expensive
}
// ═══════════════════════════════════════════════════════════════════════════
// Budget Controls
// ═══════════════════════════════════════════════════════════════════════════
// BudgetExceededError is returned when budget is exceeded.
type BudgetExceededError struct {
Budget float64
Current float64
}
// Error implements the error interface.
func (e *BudgetExceededError) Error() string {
return fmt.Sprintf("budget exceeded: current $%.4f >= budget $%.4f", e.Current, e.Budget)
}
// WithBudget creates a BudgetTracker with a budget limit.
func WithBudget(maxCost float64) *BudgetTracker {
return &BudgetTracker{
CostTracker: NewCostTracker(),
Budget: maxCost,
}
}
// BudgetTracker is a CostTracker with a budget limit.
type BudgetTracker struct {
*CostTracker
Budget float64
}
// CheckBudget returns an error if the budget is exceeded.
func (bt *BudgetTracker) CheckBudget() error {
if bt.TotalCost >= bt.Budget {
return &BudgetExceededError{Budget: bt.Budget, Current: bt.TotalCost}
}
return nil
}
// Remaining returns remaining budget in USD.
func (bt *BudgetTracker) Remaining() float64 {
remaining := bt.Budget - bt.TotalCost
if remaining < 0 {
return 0
}
return remaining
}
// RemainingString returns a formatted remaining budget string.
func (bt *BudgetTracker) RemainingString() string {
return fmt.Sprintf("$%.4f remaining of $%.4f budget", bt.Remaining(), bt.Budget)
}