File size: 4,284 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Cost Estimator — Pre-flight cost estimation for LLM requests
 *
 * Estimates token-based costs before routing to a provider.
 * Uses pricing data from the dashboard/database.
 *
 * @module shared/utils/costEstimator
 */

import { formatCost } from "./formatting";

export { formatCost };

/**
 * Default pricing per 1M tokens (fallback when no pricing config exists).
 * Values in USD.
 */
const DEFAULT_PRICING = {
  "gpt-4o": { input: 2.5, output: 10.0 },
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
  "gpt-4.1": { input: 2.0, output: 8.0 },
  "gpt-4.1-mini": { input: 0.4, output: 1.6 },
  "gpt-4.1-nano": { input: 0.1, output: 0.4 },
  o3: { input: 2.0, output: 8.0 },
  "o4-mini": { input: 1.1, output: 4.4 },
  "claude-sonnet-4-5-20250514": { input: 3.0, output: 15.0 },
  "claude-3-5-haiku-20241022": { input: 0.8, output: 4.0 },
  "gemini-2.5-pro": { input: 1.25, output: 10.0 },
  "gemini-2.5-flash": { input: 0.15, output: 0.6 },
};

/**
 * Rough token estimation from text.
 * Uses ~4 chars per token approximation (GPT-family average).
 *
 * @param {string} text
 * @returns {number} Estimated token count
 */
export function estimateTokens(text) {
  if (!text || typeof text !== "string") return 0;
  return Math.ceil(text.length / 4);
}

/**
 * Estimate input tokens from a chat completion request body.
 *
 * @param {Object} body - Request body
 * @param {Array<{role: string, content: string|Array<{type: string, text?: string}>}>} [body.messages]
 * @param {string} [body.system]
 * @returns {number} Estimated input token count
 */
export function estimateInputTokens(body) {
  if (!body) return 0;
  let total = 0;

  if (body.system) total += estimateTokens(body.system);

  if (Array.isArray(body.messages)) {
    for (const msg of body.messages) {
      if (typeof msg.content === "string") {
        total += estimateTokens(msg.content);
      } else if (Array.isArray(msg.content)) {
        for (const part of msg.content) {
          if (part.type === "text" && typeof part.text === "string") {
            total += estimateTokens(part.text);
          }
        }
      }
      // Add ~4 tokens overhead per message (role, separators)
      total += 4;
    }
  }

  return total;
}

/**
 * Estimate the cost of a request given a model.
 *
 * @param {Object} params
 * @param {string} params.model - Model identifier
 * @param {number} params.inputTokens - Estimated input tokens
 * @param {number} [params.maxOutputTokens=1000] - Max output tokens
 * @param {Object} [params.pricingOverrides] - Custom pricing { input, output } per 1M tokens
 * @returns {{ inputCost: number, outputCost: number, totalCost: number, model: string, inputTokens: number, outputTokens: number }}
 */
export function estimateCost({ model, inputTokens, maxOutputTokens = 1000, pricingOverrides }) {
  // Find matching pricing (exact match or prefix match)
  let pricing = pricingOverrides;
  if (!pricing) {
    const key = Object.keys(DEFAULT_PRICING).find((k) => model === k || model.startsWith(k));
    pricing = key ? DEFAULT_PRICING[key] : { input: 1.0, output: 3.0 }; // conservative fallback
  }

  const inputCost = (inputTokens / 1_000_000) * pricing.input;
  const outputCost = (maxOutputTokens / 1_000_000) * pricing.output;
  const totalCost = inputCost + outputCost;

  return {
    model,
    inputTokens,
    outputTokens: maxOutputTokens,
    inputCost: Math.round(inputCost * 1_000_000) / 1_000_000,
    outputCost: Math.round(outputCost * 1_000_000) / 1_000_000,
    totalCost: Math.round(totalCost * 1_000_000) / 1_000_000,
  };
}

/**
 * Quick pre-flight estimate: given a request body and model, return estimated cost.
 *
 * @param {Object} body - Chat completion request body
 * @param {string} model - Target model
 * @param {Object} [pricingOverrides] - Optional pricing overrides
 * @returns {{ inputCost: number, outputCost: number, totalCost: number, formatted: string }}
 */
export function preflightEstimate(body, model, pricingOverrides) {
  const inputTokens = estimateInputTokens(body);
  const maxOutput = body.max_tokens || body.maxOutputTokens || 1000;
  const result = estimateCost({ model, inputTokens, maxOutputTokens: maxOutput, pricingOverrides });

  return {
    ...result,
    formatted: formatCost(result.totalCost),
  };
}