File size: 2,773 Bytes
f22a059
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { config, ROUGH_PRICE_PER_1K } from '../config.js';

// A process-wide spend counter. Good enough for a single-machine fleet;
// swap for a shared store (redis/file) if you run true multi-process.
let spentUsd = 0;

function estimateCost(model, promptTokensApprox, completionTokensApprox) {
  const rate = ROUGH_PRICE_PER_1K[model] ?? ROUGH_PRICE_PER_1K.default;
  const totalTokens = promptTokensApprox + completionTokensApprox;
  return (totalTokens / 1000) * rate;
}

function approxTokens(text) {
  // ~4 chars per token is a fine rough estimate for budgeting purposes
  return Math.ceil((text || '').length / 4);
}

/**
 * Calls a model through OpenRouter's OpenAI-compatible chat completions API.
 * @param {string} model - OpenRouter model slug, e.g. "google/gemini-flash-1.5"
 * @param {string} systemPrompt
 * @param {string} userPrompt
 * @param {object} opts - { temperature, maxTokens, jsonMode }
 */
export async function callModel(model, systemPrompt, userPrompt, opts = {}) {
  const { temperature = 0.9, maxTokens = 800, jsonMode = false } = opts;

  const promptApprox = approxTokens(systemPrompt) + approxTokens(userPrompt);
  const projectedCost = estimateCost(model, promptApprox, maxTokens);

  if (spentUsd + projectedCost > config.maxSpendUsd) {
    throw new Error(
      `Spend guard tripped: estimated $${(spentUsd + projectedCost).toFixed(4)} ` +
      `would exceed MAX_SPEND_USD=$${config.maxSpendUsd}. Stopping before the call.`
    );
  }

  if (!config.openRouterKey) {
    throw new Error('OPENROUTER_API_KEY is not set. Copy .env.example to .env and fill it in.');
  }

  const body = {
    model,
    temperature,
    max_tokens: maxTokens,
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userPrompt },
    ],
  };
  if (jsonMode) {
    body.response_format = { type: 'json_object' };
  }

  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${config.openRouterKey}`,
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const errText = await res.text();
    throw new Error(`OpenRouter error ${res.status}: ${errText}`);
  }

  const data = await res.json();
  const content = data?.choices?.[0]?.message?.content ?? '';

  // Update running spend with actual usage if OpenRouter returns it, else fall back to estimate
  const usage = data?.usage;
  const actualCost = usage
    ? estimateCost(model, usage.prompt_tokens ?? promptApprox, usage.completion_tokens ?? maxTokens)
    : projectedCost;
  spentUsd += actualCost;

  return { content, spentUsd, callCostUsd: actualCost };
}

export function getSpentUsd() {
  return spentUsd;
}