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; }