Files
AriWeb-Suite/modules/webmaker/lib/costcal.js

65 lines
3.7 KiB
JavaScript

'use strict';
// ============================================================================
// 비용 보정 (v1.1.6)
//
// 작업 중에는 실시간 토큰 수에 가격표(settings.MODELS 또는 제공자 단가)를 곱해 비용을 "추정"하고,
// 작업이 끝나면 엔진(Claude Code 등)이 알려 주는 "실제" 비용으로 청구한다.
// 가격표가 실제 단가와 다르면 추정이 크게 빗나가(예: 2.5배 과소) 캐시 소진 중지가 늦어진다.
// 그래서 완료된 작업마다 (실제 ÷ 추정) 비율을 모델별로 학습해 다음 작업의 실시간 추정에 곱한다.
//
// 저장: data/cost-calibration.json { models: { "<제공자:모델>": { factor, n, updatedAt } } }
// - factor : 실제/추정 비율의 지수이동평균(새 관측 30% 반영). 처음 관측은 그대로 사용.
// - n : 관측 횟수
// 비율은 0.3~10 으로 제한해 이상치(아주 짧은 작업 등)에 휘둘리지 않게 한다.
// ============================================================================
const fs = require('fs');
const path = require('path');
const DATA_ROOT = process.env.DATA_ROOT || path.join(__dirname, '..', 'data');
const FILE = path.join(DATA_ROOT, 'cost-calibration.json');
const MIN_EST_USD = 0.002; // 이보다 작은 추정은 비율이 불안정하므로 학습하지 않음
let cache = null;
/*
* v1.4.5 보정값 다시 시작 (VERSION 2): 1.4.4 까지는 "실제 비용" 으로 Claude 세션 누적 비용을 써서(lib/claude.js 설명)
* 이어 가는 작업일수록 실제/추정 비율이 커졌고(최대 10배), 그만큼 실시간 추정이 부풀어 캐시 소진 중지가 너무 일찍 걸렸다.
* 가격표 계산도 1.4.5 에서 CLI 비용과 일치하게 고쳤으므로, 버전이 다른 옛 보정값은 버리고 1(보정 없음)에서 다시 배운다.
*/
const VERSION = 2;
function load() {
if (cache) return cache; try { cache = JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (_) { cache = null; }
if (!cache || cache.version !== VERSION) cache = { version: VERSION, models: {}, resetAt: new Date().toISOString(), previous: cache && cache.models ? cache.models : undefined };
cache.models = cache.models || {}; return cache;
}
function save() { try { fs.mkdirSync(DATA_ROOT, { recursive: true }); const tmp = FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(cache, null, 1)); fs.renameSync(tmp, FILE); } catch (_) {} }
/** 모델 참조의 보정 배율 (관측이 없으면 1) */
function factor(ref) { const m = load().models[ref]; return m && Number.isFinite(m.factor) ? m.factor : 1; }
/**
* 완료된 작업 한 건 관측.
* @param {string} ref 모델 참조 (예: anthropic:claude-fable-5-1)
* @param {number} estUsd 가격표로 계산한 추정 비용 (보정 전)
* @param {number} actualUsd 엔진이 알려 준 실제 비용
*/
function observe(ref, estUsd, actualUsd, persist = true) {
if (!ref || !(estUsd >= MIN_EST_USD) || !(actualUsd > 0)) return;
const r = Math.min(10, Math.max(0.3, actualUsd / estUsd));
const d = load(); const m = d.models[ref] || { factor: 1, n: 0 };
m.factor = m.n ? m.factor * 0.7 + r * 0.3 : r; m.n += 1; m.updatedAt = new Date().toISOString();
d.models[ref] = m; if (persist) save();
}
/**
* 기동 시 기존 작업 기록으로 보정값을 한 번 채운다 (보정 파일이 없을 때만).
* @param {Array<{ref, estUsd, actualUsd}>} samples
*/
function seed(samples) {
if (fs.existsSync(FILE) && load().version === VERSION && Object.keys(load().models).length) return 0; let n = 0;
for (const s of samples) { observe(s.ref, s.estUsd, s.actualUsd, false); n += 1; }
save(); return n;
}
function all() { return load().models; }
module.exports = { factor, observe, seed, all };