compute-cfo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/budget.d.ts +23 -0
- package/dist/budget.js +73 -0
- package/dist/exporters.d.ts +9 -0
- package/dist/exporters.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +23 -0
- package/dist/pricing.d.ts +11 -0
- package/dist/pricing.js +84 -0
- package/dist/tracker.d.ts +21 -0
- package/dist/tracker.js +61 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +35 -0
- package/dist/wrapper.d.ts +11 -0
- package/dist/wrapper.js +134 -0
- package/package.json +37 -0
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget policy enforcement.
|
|
3
|
+
*/
|
|
4
|
+
import { BudgetWindow, CostEvent, OnExceed } from './types';
|
|
5
|
+
export interface BudgetPolicyOptions {
|
|
6
|
+
maxCost: number;
|
|
7
|
+
window?: BudgetWindow;
|
|
8
|
+
onExceed?: OnExceed;
|
|
9
|
+
onExceedCallback?: (event: CostEvent, projected: number) => void;
|
|
10
|
+
tags?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare class BudgetPolicy {
|
|
13
|
+
readonly maxCost: number;
|
|
14
|
+
readonly window: BudgetWindow;
|
|
15
|
+
readonly onExceed: OnExceed;
|
|
16
|
+
readonly onExceedCallback?: (event: CostEvent, projected: number) => void;
|
|
17
|
+
readonly tags?: Record<string, string>;
|
|
18
|
+
constructor(options: BudgetPolicyOptions);
|
|
19
|
+
private getWindowStart;
|
|
20
|
+
private matchesTags;
|
|
21
|
+
currentSpend(events: CostEvent[]): number;
|
|
22
|
+
check(events: CostEvent[], pendingEvent: CostEvent): void;
|
|
23
|
+
}
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Budget policy enforcement.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BudgetPolicy = void 0;
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
class BudgetPolicy {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.maxCost = options.maxCost;
|
|
11
|
+
this.window = options.window ?? 'total';
|
|
12
|
+
this.onExceed = options.onExceed ?? 'throw';
|
|
13
|
+
this.onExceedCallback = options.onExceedCallback;
|
|
14
|
+
this.tags = options.tags;
|
|
15
|
+
}
|
|
16
|
+
getWindowStart(now) {
|
|
17
|
+
if (this.window === 'total')
|
|
18
|
+
return null;
|
|
19
|
+
const d = new Date(now);
|
|
20
|
+
if (this.window === 'hourly') {
|
|
21
|
+
d.setMinutes(0, 0, 0);
|
|
22
|
+
return d;
|
|
23
|
+
}
|
|
24
|
+
if (this.window === 'daily') {
|
|
25
|
+
d.setHours(0, 0, 0, 0);
|
|
26
|
+
return d;
|
|
27
|
+
}
|
|
28
|
+
if (this.window === 'monthly') {
|
|
29
|
+
d.setDate(1);
|
|
30
|
+
d.setHours(0, 0, 0, 0);
|
|
31
|
+
return d;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
matchesTags(event) {
|
|
36
|
+
if (!this.tags)
|
|
37
|
+
return true;
|
|
38
|
+
return Object.entries(this.tags).every(([k, v]) => event.tags[k] === v);
|
|
39
|
+
}
|
|
40
|
+
currentSpend(events) {
|
|
41
|
+
const now = new Date();
|
|
42
|
+
const windowStart = this.getWindowStart(now);
|
|
43
|
+
let total = 0;
|
|
44
|
+
for (const e of events) {
|
|
45
|
+
if (windowStart && new Date(e.timestamp) < windowStart)
|
|
46
|
+
continue;
|
|
47
|
+
if (!this.matchesTags(e))
|
|
48
|
+
continue;
|
|
49
|
+
if (e.costUsd !== null)
|
|
50
|
+
total += e.costUsd;
|
|
51
|
+
}
|
|
52
|
+
return total;
|
|
53
|
+
}
|
|
54
|
+
check(events, pendingEvent) {
|
|
55
|
+
if (!this.matchesTags(pendingEvent))
|
|
56
|
+
return;
|
|
57
|
+
const current = this.currentSpend(events);
|
|
58
|
+
const pendingCost = pendingEvent.costUsd ?? 0;
|
|
59
|
+
const projected = current + pendingCost;
|
|
60
|
+
if (projected <= this.maxCost)
|
|
61
|
+
return;
|
|
62
|
+
if (this.onExceed === 'throw') {
|
|
63
|
+
throw new types_1.BudgetExceededError(this.maxCost, projected, this.window);
|
|
64
|
+
}
|
|
65
|
+
else if (this.onExceed === 'warn') {
|
|
66
|
+
console.warn(`[compute-cfo] Budget warning: $${projected.toFixed(4)} / $${this.maxCost.toFixed(4)} (${this.window} window)`);
|
|
67
|
+
}
|
|
68
|
+
else if (this.onExceed === 'callback' && this.onExceedCallback) {
|
|
69
|
+
this.onExceedCallback(pendingEvent, projected);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.BudgetPolicy = BudgetPolicy;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost event exporters.
|
|
3
|
+
*/
|
|
4
|
+
import { CostEvent } from './types';
|
|
5
|
+
export type Exporter = (event: CostEvent) => void;
|
|
6
|
+
export declare function consoleExporter(event: CostEvent): void;
|
|
7
|
+
export declare function jsonlExporter(path?: string): Exporter;
|
|
8
|
+
export declare function webhookExporter(url: string): Exporter;
|
|
9
|
+
export declare function getExporter(spec: string): Exporter;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cost event exporters.
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.consoleExporter = consoleExporter;
|
|
40
|
+
exports.jsonlExporter = jsonlExporter;
|
|
41
|
+
exports.webhookExporter = webhookExporter;
|
|
42
|
+
exports.getExporter = getExporter;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const types_1 = require("./types");
|
|
45
|
+
function consoleExporter(event) {
|
|
46
|
+
const costStr = event.costUsd !== null ? `$${event.costUsd.toFixed(6)}` : '$?.??????';
|
|
47
|
+
const totalTokens = event.inputTokens + event.outputTokens;
|
|
48
|
+
const parts = [
|
|
49
|
+
`[compute-cfo]`,
|
|
50
|
+
event.model,
|
|
51
|
+
`${totalTokens} tokens`,
|
|
52
|
+
costStr,
|
|
53
|
+
];
|
|
54
|
+
if (Object.keys(event.tags).length > 0) {
|
|
55
|
+
const tagStr = Object.entries(event.tags)
|
|
56
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
57
|
+
.join(' ');
|
|
58
|
+
parts.push(tagStr);
|
|
59
|
+
}
|
|
60
|
+
console.error(parts.join(' | '));
|
|
61
|
+
}
|
|
62
|
+
function jsonlExporter(path = 'compute_cfo_events.jsonl') {
|
|
63
|
+
return (event) => {
|
|
64
|
+
fs.appendFileSync(path, JSON.stringify((0, types_1.costEventToDict)(event)) + '\n');
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function webhookExporter(url) {
|
|
68
|
+
return (event) => {
|
|
69
|
+
const data = JSON.stringify((0, types_1.costEventToDict)(event));
|
|
70
|
+
fetch(url, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: data,
|
|
74
|
+
}).catch(() => { }); // fire-and-forget
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function getExporter(spec) {
|
|
78
|
+
if (spec === 'console')
|
|
79
|
+
return consoleExporter;
|
|
80
|
+
if (spec === 'jsonl')
|
|
81
|
+
return jsonlExporter();
|
|
82
|
+
if (spec.startsWith('jsonl:'))
|
|
83
|
+
return jsonlExporter(spec.slice(6));
|
|
84
|
+
if (spec.startsWith('webhook:'))
|
|
85
|
+
return webhookExporter(spec.slice(8));
|
|
86
|
+
throw new Error(`Unknown exporter: "${spec}". Use 'console', 'jsonl', 'jsonl:/path', or 'webhook:URL'.`);
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compute-cfo: Cost tracking, attribution, and budget enforcement for AI inference APIs.
|
|
3
|
+
*/
|
|
4
|
+
export { getCost, getPrice, resolveModel } from './pricing';
|
|
5
|
+
export { CostTracker } from './tracker';
|
|
6
|
+
export type { CostTrackerOptions } from './tracker';
|
|
7
|
+
export { BudgetPolicy } from './budget';
|
|
8
|
+
export type { BudgetPolicyOptions } from './budget';
|
|
9
|
+
export { BudgetExceededError, costEventToDict, } from './types';
|
|
10
|
+
export type { CostEvent, BudgetWindow, OnExceed } from './types';
|
|
11
|
+
export { wrap } from './wrapper';
|
|
12
|
+
export type { WrapOptions } from './wrapper';
|
|
13
|
+
export { consoleExporter, jsonlExporter, webhookExporter, } from './exporters';
|
|
14
|
+
export type { Exporter } from './exporters';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* compute-cfo: Cost tracking, attribution, and budget enforcement for AI inference APIs.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.webhookExporter = exports.jsonlExporter = exports.consoleExporter = exports.wrap = exports.costEventToDict = exports.BudgetExceededError = exports.BudgetPolicy = exports.CostTracker = exports.resolveModel = exports.getPrice = exports.getCost = void 0;
|
|
7
|
+
var pricing_1 = require("./pricing");
|
|
8
|
+
Object.defineProperty(exports, "getCost", { enumerable: true, get: function () { return pricing_1.getCost; } });
|
|
9
|
+
Object.defineProperty(exports, "getPrice", { enumerable: true, get: function () { return pricing_1.getPrice; } });
|
|
10
|
+
Object.defineProperty(exports, "resolveModel", { enumerable: true, get: function () { return pricing_1.resolveModel; } });
|
|
11
|
+
var tracker_1 = require("./tracker");
|
|
12
|
+
Object.defineProperty(exports, "CostTracker", { enumerable: true, get: function () { return tracker_1.CostTracker; } });
|
|
13
|
+
var budget_1 = require("./budget");
|
|
14
|
+
Object.defineProperty(exports, "BudgetPolicy", { enumerable: true, get: function () { return budget_1.BudgetPolicy; } });
|
|
15
|
+
var types_1 = require("./types");
|
|
16
|
+
Object.defineProperty(exports, "BudgetExceededError", { enumerable: true, get: function () { return types_1.BudgetExceededError; } });
|
|
17
|
+
Object.defineProperty(exports, "costEventToDict", { enumerable: true, get: function () { return types_1.costEventToDict; } });
|
|
18
|
+
var wrapper_1 = require("./wrapper");
|
|
19
|
+
Object.defineProperty(exports, "wrap", { enumerable: true, get: function () { return wrapper_1.wrap; } });
|
|
20
|
+
var exporters_1 = require("./exporters");
|
|
21
|
+
Object.defineProperty(exports, "consoleExporter", { enumerable: true, get: function () { return exporters_1.consoleExporter; } });
|
|
22
|
+
Object.defineProperty(exports, "jsonlExporter", { enumerable: true, get: function () { return exporters_1.jsonlExporter; } });
|
|
23
|
+
Object.defineProperty(exports, "webhookExporter", { enumerable: true, get: function () { return exporters_1.webhookExporter; } });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model pricing database for OpenAI and Anthropic.
|
|
3
|
+
* Prices are in USD per 1 million tokens. Updated March 2026.
|
|
4
|
+
*/
|
|
5
|
+
export interface ModelPrice {
|
|
6
|
+
inputPerMillion: number;
|
|
7
|
+
outputPerMillion: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveModel(model: string): string;
|
|
10
|
+
export declare function getPrice(model: string): ModelPrice | null;
|
|
11
|
+
export declare function getCost(model: string, inputTokens: number, outputTokens: number): number | null;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Model pricing database for OpenAI and Anthropic.
|
|
4
|
+
* Prices are in USD per 1 million tokens. Updated March 2026.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.resolveModel = resolveModel;
|
|
8
|
+
exports.getPrice = getPrice;
|
|
9
|
+
exports.getCost = getCost;
|
|
10
|
+
const MODEL_PRICES = {
|
|
11
|
+
// ── OpenAI ──────────────────────────────────────────────
|
|
12
|
+
// GPT-4.1 family
|
|
13
|
+
'gpt-4.1': { inputPerMillion: 2.0, outputPerMillion: 8.0 },
|
|
14
|
+
'gpt-4.1-mini': { inputPerMillion: 0.4, outputPerMillion: 1.6 },
|
|
15
|
+
'gpt-4.1-nano': { inputPerMillion: 0.1, outputPerMillion: 0.4 },
|
|
16
|
+
// GPT-4o family
|
|
17
|
+
'gpt-4o': { inputPerMillion: 2.5, outputPerMillion: 10.0 },
|
|
18
|
+
'gpt-4o-mini': { inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
19
|
+
'gpt-4o-audio-preview': { inputPerMillion: 2.5, outputPerMillion: 10.0 },
|
|
20
|
+
// GPT-4 legacy
|
|
21
|
+
'gpt-4-turbo': { inputPerMillion: 10.0, outputPerMillion: 30.0 },
|
|
22
|
+
'gpt-4': { inputPerMillion: 30.0, outputPerMillion: 60.0 },
|
|
23
|
+
// GPT-3.5
|
|
24
|
+
'gpt-3.5-turbo': { inputPerMillion: 0.5, outputPerMillion: 1.5 },
|
|
25
|
+
// o-series reasoning
|
|
26
|
+
'o3': { inputPerMillion: 2.0, outputPerMillion: 8.0 },
|
|
27
|
+
'o3-mini': { inputPerMillion: 1.1, outputPerMillion: 4.4 },
|
|
28
|
+
'o4-mini': { inputPerMillion: 1.1, outputPerMillion: 4.4 },
|
|
29
|
+
'o1': { inputPerMillion: 15.0, outputPerMillion: 60.0 },
|
|
30
|
+
'o1-mini': { inputPerMillion: 1.1, outputPerMillion: 4.4 },
|
|
31
|
+
'o1-preview': { inputPerMillion: 15.0, outputPerMillion: 60.0 },
|
|
32
|
+
// Embeddings
|
|
33
|
+
'text-embedding-3-small': { inputPerMillion: 0.02, outputPerMillion: 0 },
|
|
34
|
+
'text-embedding-3-large': { inputPerMillion: 0.13, outputPerMillion: 0 },
|
|
35
|
+
'text-embedding-ada-002': { inputPerMillion: 0.1, outputPerMillion: 0 },
|
|
36
|
+
// ── Anthropic ───────────────────────────────────────────
|
|
37
|
+
'claude-opus-4-20250514': { inputPerMillion: 15.0, outputPerMillion: 75.0 },
|
|
38
|
+
'claude-sonnet-4-20250514': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
39
|
+
'claude-3-7-sonnet-20250219': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
40
|
+
'claude-3-5-sonnet-20241022': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
41
|
+
'claude-3-5-sonnet-20240620': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
42
|
+
'claude-3-5-haiku-20241022': { inputPerMillion: 0.8, outputPerMillion: 4.0 },
|
|
43
|
+
'claude-3-opus-20240229': { inputPerMillion: 15.0, outputPerMillion: 75.0 },
|
|
44
|
+
'claude-3-sonnet-20240229': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
45
|
+
'claude-3-haiku-20240307': { inputPerMillion: 0.25, outputPerMillion: 1.25 },
|
|
46
|
+
// Aliases
|
|
47
|
+
'claude-opus-4-0': { inputPerMillion: 15.0, outputPerMillion: 75.0 },
|
|
48
|
+
'claude-sonnet-4-0': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
49
|
+
'claude-3.7-sonnet': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
50
|
+
'claude-3.5-sonnet': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
51
|
+
'claude-3.5-haiku': { inputPerMillion: 0.8, outputPerMillion: 4.0 },
|
|
52
|
+
'claude-3-opus': { inputPerMillion: 15.0, outputPerMillion: 75.0 },
|
|
53
|
+
'claude-3-sonnet': { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
54
|
+
'claude-3-haiku': { inputPerMillion: 0.25, outputPerMillion: 1.25 },
|
|
55
|
+
};
|
|
56
|
+
const ALIASES = {
|
|
57
|
+
'gpt-4o-2024-11-20': 'gpt-4o',
|
|
58
|
+
'gpt-4o-2024-08-06': 'gpt-4o',
|
|
59
|
+
'gpt-4o-2024-05-13': 'gpt-4o',
|
|
60
|
+
'gpt-4o-mini-2024-07-18': 'gpt-4o-mini',
|
|
61
|
+
'gpt-4-turbo-2024-04-09': 'gpt-4-turbo',
|
|
62
|
+
'gpt-4-turbo-preview': 'gpt-4-turbo',
|
|
63
|
+
'gpt-4-0125-preview': 'gpt-4-turbo',
|
|
64
|
+
'gpt-4-1106-preview': 'gpt-4-turbo',
|
|
65
|
+
'gpt-3.5-turbo-0125': 'gpt-3.5-turbo',
|
|
66
|
+
'gpt-3.5-turbo-1106': 'gpt-3.5-turbo',
|
|
67
|
+
'o3-2025-04-16': 'o3',
|
|
68
|
+
'o4-mini-2025-04-16': 'o4-mini',
|
|
69
|
+
};
|
|
70
|
+
function resolveModel(model) {
|
|
71
|
+
return ALIASES[model] ?? model;
|
|
72
|
+
}
|
|
73
|
+
function getPrice(model) {
|
|
74
|
+
const canonical = resolveModel(model);
|
|
75
|
+
return MODEL_PRICES[canonical] ?? null;
|
|
76
|
+
}
|
|
77
|
+
function getCost(model, inputTokens, outputTokens) {
|
|
78
|
+
const price = getPrice(model);
|
|
79
|
+
if (!price)
|
|
80
|
+
return null;
|
|
81
|
+
return ((inputTokens * price.inputPerMillion +
|
|
82
|
+
outputTokens * price.outputPerMillion) /
|
|
83
|
+
1000000);
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostTracker — core cost accumulation and querying.
|
|
3
|
+
*/
|
|
4
|
+
import { BudgetPolicy } from './budget';
|
|
5
|
+
import { CostEvent } from './types';
|
|
6
|
+
export interface CostTrackerOptions {
|
|
7
|
+
budget?: BudgetPolicy;
|
|
8
|
+
export?: string | null;
|
|
9
|
+
quiet?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class CostTracker {
|
|
12
|
+
private _events;
|
|
13
|
+
private _budget?;
|
|
14
|
+
private _exporters;
|
|
15
|
+
constructor(options?: CostTrackerOptions);
|
|
16
|
+
record(event: CostEvent): void;
|
|
17
|
+
get events(): CostEvent[];
|
|
18
|
+
get totalCost(): number;
|
|
19
|
+
costBy(key: string): Record<string, number>;
|
|
20
|
+
reset(): void;
|
|
21
|
+
}
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CostTracker — core cost accumulation and querying.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CostTracker = void 0;
|
|
7
|
+
const exporters_1 = require("./exporters");
|
|
8
|
+
class CostTracker {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this._events = [];
|
|
11
|
+
this._exporters = [];
|
|
12
|
+
this._budget = options.budget;
|
|
13
|
+
let exportSpec = options.export ?? 'console';
|
|
14
|
+
if (options.quiet)
|
|
15
|
+
exportSpec = null;
|
|
16
|
+
if (exportSpec) {
|
|
17
|
+
this._exporters.push((0, exporters_1.getExporter)(exportSpec));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
record(event) {
|
|
21
|
+
if (this._budget) {
|
|
22
|
+
this._budget.check(this._events, event);
|
|
23
|
+
}
|
|
24
|
+
if (this._budget && event.costUsd !== null) {
|
|
25
|
+
const spent = this._budget.currentSpend(this._events) + event.costUsd;
|
|
26
|
+
event.budgetRemainingUsd = Math.max(0, this._budget.maxCost - spent);
|
|
27
|
+
}
|
|
28
|
+
this._events.push(event);
|
|
29
|
+
for (const exporter of this._exporters) {
|
|
30
|
+
exporter(event);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
get events() {
|
|
34
|
+
return [...this._events];
|
|
35
|
+
}
|
|
36
|
+
get totalCost() {
|
|
37
|
+
return this._events.reduce((sum, e) => sum + (e.costUsd ?? 0), 0);
|
|
38
|
+
}
|
|
39
|
+
costBy(key) {
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const e of this._events) {
|
|
42
|
+
if (e.costUsd === null)
|
|
43
|
+
continue;
|
|
44
|
+
let groupKey;
|
|
45
|
+
if (key === 'model')
|
|
46
|
+
groupKey = e.model;
|
|
47
|
+
else if (key === 'provider')
|
|
48
|
+
groupKey = e.provider;
|
|
49
|
+
else
|
|
50
|
+
groupKey = e.tags[key];
|
|
51
|
+
if (groupKey !== undefined) {
|
|
52
|
+
result[groupKey] = (result[groupKey] ?? 0) + e.costUsd;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
reset() {
|
|
58
|
+
this._events = [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.CostTracker = CostTracker;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data types for compute-cfo.
|
|
3
|
+
*/
|
|
4
|
+
export interface CostEvent {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
operation: string;
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
costUsd: number | null;
|
|
12
|
+
latencyMs?: number;
|
|
13
|
+
tags: Record<string, string>;
|
|
14
|
+
budgetRemainingUsd?: number;
|
|
15
|
+
}
|
|
16
|
+
export type BudgetWindow = 'hourly' | 'daily' | 'monthly' | 'total';
|
|
17
|
+
export type OnExceed = 'throw' | 'warn' | 'callback';
|
|
18
|
+
export declare class BudgetExceededError extends Error {
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
readonly current: number;
|
|
21
|
+
readonly window: string;
|
|
22
|
+
constructor(limit: number, current: number, window: string);
|
|
23
|
+
}
|
|
24
|
+
export declare function costEventToDict(event: CostEvent): Record<string, unknown>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core data types for compute-cfo.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BudgetExceededError = void 0;
|
|
7
|
+
exports.costEventToDict = costEventToDict;
|
|
8
|
+
class BudgetExceededError extends Error {
|
|
9
|
+
constructor(limit, current, window) {
|
|
10
|
+
super(`Budget exceeded: $${current.toFixed(4)} / $${limit.toFixed(4)} (${window} window)`);
|
|
11
|
+
this.name = 'BudgetExceededError';
|
|
12
|
+
this.limit = limit;
|
|
13
|
+
this.current = current;
|
|
14
|
+
this.window = window;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.BudgetExceededError = BudgetExceededError;
|
|
18
|
+
function costEventToDict(event) {
|
|
19
|
+
const d = {
|
|
20
|
+
timestamp: event.timestamp,
|
|
21
|
+
provider: event.provider,
|
|
22
|
+
model: event.model,
|
|
23
|
+
operation: event.operation,
|
|
24
|
+
input_tokens: event.inputTokens,
|
|
25
|
+
output_tokens: event.outputTokens,
|
|
26
|
+
cost_usd: event.costUsd,
|
|
27
|
+
};
|
|
28
|
+
if (event.latencyMs !== undefined)
|
|
29
|
+
d.latency_ms = event.latencyMs;
|
|
30
|
+
if (Object.keys(event.tags).length > 0)
|
|
31
|
+
d.tags = event.tags;
|
|
32
|
+
if (event.budgetRemainingUsd !== undefined)
|
|
33
|
+
d.budget_remaining_usd = event.budgetRemainingUsd;
|
|
34
|
+
return d;
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in wrapper for OpenAI and Anthropic SDK clients.
|
|
3
|
+
*/
|
|
4
|
+
import { CostTracker } from './tracker';
|
|
5
|
+
export interface WrapOptions {
|
|
6
|
+
tracker?: CostTracker;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Wrap an OpenAI or Anthropic client with cost tracking.
|
|
10
|
+
*/
|
|
11
|
+
export declare function wrap<T extends object>(client: T, options?: WrapOptions): T;
|
package/dist/wrapper.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Drop-in wrapper for OpenAI and Anthropic SDK clients.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.wrap = wrap;
|
|
7
|
+
const pricing_1 = require("./pricing");
|
|
8
|
+
const tracker_1 = require("./tracker");
|
|
9
|
+
let defaultTracker = null;
|
|
10
|
+
function getOrCreateDefaultTracker() {
|
|
11
|
+
if (!defaultTracker) {
|
|
12
|
+
defaultTracker = new tracker_1.CostTracker({ export: 'console' });
|
|
13
|
+
}
|
|
14
|
+
return defaultTracker;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an OpenAI or Anthropic client with cost tracking.
|
|
18
|
+
*/
|
|
19
|
+
function wrap(client, options) {
|
|
20
|
+
const tracker = options?.tracker ?? getOrCreateDefaultTracker();
|
|
21
|
+
// Detect client type by checking for characteristic properties
|
|
22
|
+
if ('chat' in client && typeof client.chat?.completions?.create === 'function') {
|
|
23
|
+
return wrapOpenAI(client, tracker);
|
|
24
|
+
}
|
|
25
|
+
if ('messages' in client && typeof client.messages?.create === 'function') {
|
|
26
|
+
return wrapAnthropic(client, tracker);
|
|
27
|
+
}
|
|
28
|
+
throw new TypeError(`Unsupported client type. Supported: OpenAI, Anthropic`);
|
|
29
|
+
}
|
|
30
|
+
function wrapOpenAI(client, tracker) {
|
|
31
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
32
|
+
const trackedCreate = async (params) => {
|
|
33
|
+
const { metadata, ...rest } = params ?? {};
|
|
34
|
+
const tags = metadata && typeof metadata === 'object' ? { ...metadata } : {};
|
|
35
|
+
const model = rest.model ?? 'unknown';
|
|
36
|
+
const start = performance.now();
|
|
37
|
+
const response = await originalCreate(rest);
|
|
38
|
+
const latencyMs = Math.round((performance.now() - start) * 10) / 10;
|
|
39
|
+
const usage = response?.usage;
|
|
40
|
+
const inputTokens = usage?.prompt_tokens ?? 0;
|
|
41
|
+
const outputTokens = usage?.completion_tokens ?? 0;
|
|
42
|
+
const actualModel = response?.model ?? model;
|
|
43
|
+
const costUsd = (0, pricing_1.getCost)(actualModel, inputTokens, outputTokens);
|
|
44
|
+
const event = {
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
model: actualModel,
|
|
48
|
+
operation: 'chat.completions',
|
|
49
|
+
inputTokens,
|
|
50
|
+
outputTokens,
|
|
51
|
+
costUsd,
|
|
52
|
+
latencyMs,
|
|
53
|
+
tags,
|
|
54
|
+
};
|
|
55
|
+
tracker.record(event);
|
|
56
|
+
return response;
|
|
57
|
+
};
|
|
58
|
+
// Create a proxy that intercepts chat.completions.create
|
|
59
|
+
return new Proxy(client, {
|
|
60
|
+
get(target, prop) {
|
|
61
|
+
if (prop === 'chat') {
|
|
62
|
+
return new Proxy(target.chat, {
|
|
63
|
+
get(chatTarget, chatProp) {
|
|
64
|
+
if (chatProp === 'completions') {
|
|
65
|
+
return new Proxy(chatTarget.completions, {
|
|
66
|
+
get(compTarget, compProp) {
|
|
67
|
+
if (compProp === 'create')
|
|
68
|
+
return trackedCreate;
|
|
69
|
+
return compTarget[compProp];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return chatTarget[chatProp];
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return target[prop];
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function wrapAnthropic(client, tracker) {
|
|
82
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
83
|
+
const trackedCreate = async (params) => {
|
|
84
|
+
const { compute_cfo_tags, ...rest } = params ?? {};
|
|
85
|
+
const tags = {};
|
|
86
|
+
// Extract from Anthropic metadata
|
|
87
|
+
if (rest.metadata && typeof rest.metadata === 'object') {
|
|
88
|
+
for (const [k, v] of Object.entries(rest.metadata)) {
|
|
89
|
+
if (k !== 'user_id')
|
|
90
|
+
tags[k] = String(v);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Merge explicit tags
|
|
94
|
+
if (compute_cfo_tags && typeof compute_cfo_tags === 'object') {
|
|
95
|
+
Object.assign(tags, compute_cfo_tags);
|
|
96
|
+
}
|
|
97
|
+
const model = rest.model ?? 'unknown';
|
|
98
|
+
const start = performance.now();
|
|
99
|
+
const response = await originalCreate(rest);
|
|
100
|
+
const latencyMs = Math.round((performance.now() - start) * 10) / 10;
|
|
101
|
+
const usage = response?.usage;
|
|
102
|
+
const inputTokens = usage?.input_tokens ?? 0;
|
|
103
|
+
const outputTokens = usage?.output_tokens ?? 0;
|
|
104
|
+
const actualModel = response?.model ?? model;
|
|
105
|
+
const costUsd = (0, pricing_1.getCost)(actualModel, inputTokens, outputTokens);
|
|
106
|
+
const event = {
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
provider: 'anthropic',
|
|
109
|
+
model: actualModel,
|
|
110
|
+
operation: 'messages',
|
|
111
|
+
inputTokens,
|
|
112
|
+
outputTokens,
|
|
113
|
+
costUsd,
|
|
114
|
+
latencyMs,
|
|
115
|
+
tags,
|
|
116
|
+
};
|
|
117
|
+
tracker.record(event);
|
|
118
|
+
return response;
|
|
119
|
+
};
|
|
120
|
+
return new Proxy(client, {
|
|
121
|
+
get(target, prop) {
|
|
122
|
+
if (prop === 'messages') {
|
|
123
|
+
return new Proxy(target.messages, {
|
|
124
|
+
get(msgTarget, msgProp) {
|
|
125
|
+
if (msgProp === 'create')
|
|
126
|
+
return trackedCreate;
|
|
127
|
+
return msgTarget[msgProp];
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return target[prop];
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "compute-cfo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cost tracking, attribution, and budget enforcement for AI inference APIs",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["llm", "cost", "tracking", "openai", "anthropic", "budget", "inference", "ai"],
|
|
14
|
+
"author": "Compute CFO <hello@computecfo.com>",
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/YanLukashin/compute-cfo"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"openai": ">=4.0.0",
|
|
22
|
+
"@anthropic-ai/sdk": ">=0.20.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"openai": { "optional": true },
|
|
26
|
+
"@anthropic-ai/sdk": { "optional": true }
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.4.0",
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"jest": "^29.0.0",
|
|
32
|
+
"ts-jest": "^29.0.0",
|
|
33
|
+
"@types/jest": "^29.0.0",
|
|
34
|
+
"openai": "^4.80.0",
|
|
35
|
+
"@anthropic-ai/sdk": "^0.39.0"
|
|
36
|
+
}
|
|
37
|
+
}
|