aiopt 0.0.1
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/README.md +74 -0
- package/dist/cli.js +316 -0
- package/dist/cli.js.map +1 -0
- package/package.json +31 -0
- package/rates/rate_table.json +30 -0
- package/samples/sample_usage.jsonl +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# npx aiopt scan → your LLM cost & savings in 5 seconds
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npx aiopt init
|
|
5
|
+
npx aiopt scan
|
|
6
|
+
cat ./aiopt-output/report.txt
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This is a **serverless local CLI MVP**.
|
|
10
|
+
- No signup, no upload, no dashboard, no server deployment.
|
|
11
|
+
- Reads local JSONL/CSV → writes local outputs.
|
|
12
|
+
- **No LLM calls** (math + deterministic rules only).
|
|
13
|
+
|
|
14
|
+
## What you get
|
|
15
|
+
After `scan`, you will have:
|
|
16
|
+
1) `./aiopt-output/analysis.json` (top cost by feature/model)
|
|
17
|
+
2) `./aiopt-output/report.txt` (total cost + estimated savings)
|
|
18
|
+
3) `./aiopt-output/cost-policy.json` (policy file)
|
|
19
|
+
|
|
20
|
+
### Sample `report.txt`
|
|
21
|
+
```
|
|
22
|
+
총비용: $0.23
|
|
23
|
+
절감 가능 금액(Estimated): $0.21
|
|
24
|
+
절감 근거 3줄:
|
|
25
|
+
a) 모델 라우팅 절감(추정): $0.07
|
|
26
|
+
b) 컨텍스트 감축(추정): $0.02 (상위 20% input에 25% 감축 가정)
|
|
27
|
+
c) 재시도/오류 낭비: $0.13 (retries 기반)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Input (default)
|
|
31
|
+
- Default path: `./aiopt-input/usage.jsonl`
|
|
32
|
+
- Change: `npx aiopt scan --input <path>`
|
|
33
|
+
- Supported: JSONL (1 event per line), CSV
|
|
34
|
+
|
|
35
|
+
### Required fields (fixed)
|
|
36
|
+
`ts, provider, model, input_tokens, output_tokens, feature_tag, retries, status`
|
|
37
|
+
|
|
38
|
+
`billed_cost` is optional.
|
|
39
|
+
|
|
40
|
+
### JSONL example (5 lines)
|
|
41
|
+
```jsonl
|
|
42
|
+
{"ts":"2026-02-07T00:00:01Z","provider":"openai","model":"gpt-5-mini","input_tokens":12000,"output_tokens":1800,"feature_tag":"summarize","retries":0,"status":"ok"}
|
|
43
|
+
{"ts":"2026-02-07T00:00:02Z","provider":"openai","model":"gpt-5.2","input_tokens":35000,"output_tokens":3000,"feature_tag":"coding","retries":1,"status":"ok"}
|
|
44
|
+
{"ts":"2026-02-07T00:00:03Z","provider":"anthropic","model":"claude-sonnet","input_tokens":22000,"output_tokens":2500,"feature_tag":"classify","retries":0,"status":"ok"}
|
|
45
|
+
{"ts":"2026-02-07T00:00:04Z","provider":"gemini","model":"gemini-1.5-flash","input_tokens":9000,"output_tokens":1200,"feature_tag":"translate","retries":0,"status":"ok"}
|
|
46
|
+
{"ts":"2026-02-07T00:00:05Z","provider":"openai","model":"unknown-model-x","input_tokens":8000,"output_tokens":1000,"feature_tag":"summarize","retries":2,"status":"error"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Outputs (fixed)
|
|
50
|
+
- `analysis.json`
|
|
51
|
+
- `total_cost`
|
|
52
|
+
- `by_model_top` (top 10)
|
|
53
|
+
- `by_feature_top` (top 10)
|
|
54
|
+
- `unknown_models`
|
|
55
|
+
- `rate_table_version`, `rate_table_date`
|
|
56
|
+
- `report.txt`
|
|
57
|
+
- total cost
|
|
58
|
+
- estimated savings
|
|
59
|
+
- 3-line rationale
|
|
60
|
+
- `cost-policy.json`
|
|
61
|
+
- `version, default_provider, rules, budgets, generated_from`
|
|
62
|
+
|
|
63
|
+
## Rate table
|
|
64
|
+
- `./rates/rate_table.json`
|
|
65
|
+
- Unknown models are marked as `Estimated` and listed in `unknown_models`.
|
|
66
|
+
|
|
67
|
+
## Local dev
|
|
68
|
+
```bash
|
|
69
|
+
cd aiopt
|
|
70
|
+
npm i
|
|
71
|
+
npm run build
|
|
72
|
+
node dist/cli.js init
|
|
73
|
+
node dist/cli.js scan --input ./aiopt-input/usage.jsonl
|
|
74
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_fs3 = __toESM(require("fs"));
|
|
28
|
+
var import_path3 = __toESM(require("path"));
|
|
29
|
+
var import_commander = require("commander");
|
|
30
|
+
|
|
31
|
+
// src/io.ts
|
|
32
|
+
var import_fs = __toESM(require("fs"));
|
|
33
|
+
var import_path = __toESM(require("path"));
|
|
34
|
+
var import_sync = require("csv-parse/sync");
|
|
35
|
+
function ensureDir(p) {
|
|
36
|
+
import_fs.default.mkdirSync(p, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
function readJsonl(filePath) {
|
|
39
|
+
const raw = import_fs.default.readFileSync(filePath, "utf8");
|
|
40
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const obj = JSON.parse(line);
|
|
44
|
+
out.push(normalizeEvent(obj));
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function readCsv(filePath) {
|
|
49
|
+
const raw = import_fs.default.readFileSync(filePath, "utf8");
|
|
50
|
+
const records = (0, import_sync.parse)(raw, { columns: true, skip_empty_lines: true, trim: true });
|
|
51
|
+
return records.map((r) => normalizeEvent(r));
|
|
52
|
+
}
|
|
53
|
+
function toNum(x, def = 0) {
|
|
54
|
+
const n = Number(x);
|
|
55
|
+
return Number.isFinite(n) ? n : def;
|
|
56
|
+
}
|
|
57
|
+
function normalizeEvent(x) {
|
|
58
|
+
return {
|
|
59
|
+
ts: String(x.ts ?? ""),
|
|
60
|
+
provider: String(x.provider ?? "").toLowerCase(),
|
|
61
|
+
model: String(x.model ?? ""),
|
|
62
|
+
input_tokens: toNum(x.input_tokens),
|
|
63
|
+
output_tokens: toNum(x.output_tokens),
|
|
64
|
+
feature_tag: String(x.feature_tag ?? ""),
|
|
65
|
+
retries: toNum(x.retries),
|
|
66
|
+
status: String(x.status ?? ""),
|
|
67
|
+
billed_cost: x.billed_cost === void 0 || x.billed_cost === "" ? void 0 : toNum(x.billed_cost)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isCsvPath(p) {
|
|
71
|
+
return import_path.default.extname(p).toLowerCase() === ".csv";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/scan.ts
|
|
75
|
+
var import_fs2 = __toESM(require("fs"));
|
|
76
|
+
var import_path2 = __toESM(require("path"));
|
|
77
|
+
|
|
78
|
+
// src/cost.ts
|
|
79
|
+
function getRates(rt, provider, model) {
|
|
80
|
+
const p = rt.providers[provider];
|
|
81
|
+
if (!p) return null;
|
|
82
|
+
const m = p.models[model];
|
|
83
|
+
if (m) return { kind: "official", input: m.input, output: m.output };
|
|
84
|
+
return { kind: "estimated", input: p.default_estimated.input, output: p.default_estimated.output };
|
|
85
|
+
}
|
|
86
|
+
function costOfEvent(rt, ev) {
|
|
87
|
+
if (typeof ev.billed_cost === "number" && Number.isFinite(ev.billed_cost)) {
|
|
88
|
+
return {
|
|
89
|
+
cost: ev.billed_cost,
|
|
90
|
+
used_rate: {
|
|
91
|
+
kind: "billed_cost",
|
|
92
|
+
provider: ev.provider,
|
|
93
|
+
model: ev.model,
|
|
94
|
+
input_per_m: 0,
|
|
95
|
+
output_per_m: 0
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const r = getRates(rt, ev.provider, ev.model);
|
|
100
|
+
if (!r) {
|
|
101
|
+
const input_per_m = 1;
|
|
102
|
+
const output_per_m = 4;
|
|
103
|
+
const cost2 = ev.input_tokens / 1e6 * input_per_m + ev.output_tokens / 1e6 * output_per_m;
|
|
104
|
+
return {
|
|
105
|
+
cost: cost2,
|
|
106
|
+
used_rate: { kind: "estimated", provider: ev.provider, model: ev.model, input_per_m, output_per_m }
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const cost = ev.input_tokens / 1e6 * r.input + ev.output_tokens / 1e6 * r.output;
|
|
110
|
+
return {
|
|
111
|
+
cost,
|
|
112
|
+
used_rate: {
|
|
113
|
+
kind: r.kind,
|
|
114
|
+
provider: ev.provider,
|
|
115
|
+
model: ev.model,
|
|
116
|
+
input_per_m: r.input,
|
|
117
|
+
output_per_m: r.output
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/scan.ts
|
|
123
|
+
var ROUTE_TO_CHEAP_FEATURES = /* @__PURE__ */ new Set(["summarize", "classify", "translate"]);
|
|
124
|
+
function topN(map, n) {
|
|
125
|
+
return [...map.entries()].map(([key, v]) => ({ key, cost: v.cost, events: v.events })).sort((a, b) => b.cost - a.cost).slice(0, n);
|
|
126
|
+
}
|
|
127
|
+
function analyze(rt, events) {
|
|
128
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
129
|
+
const byFeature = /* @__PURE__ */ new Map();
|
|
130
|
+
const unknownModels = [];
|
|
131
|
+
const perEventCosts = [];
|
|
132
|
+
let total = 0;
|
|
133
|
+
for (const ev of events) {
|
|
134
|
+
const cr = costOfEvent(rt, ev);
|
|
135
|
+
total += cr.cost;
|
|
136
|
+
perEventCosts.push({ ev, cost: cr.cost });
|
|
137
|
+
const mk = `${ev.provider}:${ev.model}`;
|
|
138
|
+
const fk = ev.feature_tag || "(none)";
|
|
139
|
+
const mv = byModel.get(mk) || { cost: 0, events: 0 };
|
|
140
|
+
mv.cost += cr.cost;
|
|
141
|
+
mv.events += 1;
|
|
142
|
+
byModel.set(mk, mv);
|
|
143
|
+
const fv = byFeature.get(fk) || { cost: 0, events: 0 };
|
|
144
|
+
fv.cost += cr.cost;
|
|
145
|
+
fv.events += 1;
|
|
146
|
+
byFeature.set(fk, fv);
|
|
147
|
+
const rr = getRates(rt, ev.provider, ev.model);
|
|
148
|
+
if (!rr) {
|
|
149
|
+
unknownModels.push({ provider: ev.provider, model: ev.model, reason: "unknown provider (estimated)" });
|
|
150
|
+
} else if (rr.kind === "estimated") {
|
|
151
|
+
unknownModels.push({ provider: ev.provider, model: ev.model, reason: "unknown model (estimated)" });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let routingSavings = 0;
|
|
155
|
+
for (const { ev } of perEventCosts) {
|
|
156
|
+
if (!ROUTE_TO_CHEAP_FEATURES.has(String(ev.feature_tag || "").toLowerCase())) continue;
|
|
157
|
+
const provider = ev.provider;
|
|
158
|
+
const p = rt.providers[provider];
|
|
159
|
+
if (!p) continue;
|
|
160
|
+
const entries = Object.entries(p.models);
|
|
161
|
+
if (entries.length === 0) continue;
|
|
162
|
+
const cheapest = entries.map(([name, r]) => ({ name, score: (r.input + r.output) / 2, r })).sort((a, b) => a.score - b.score)[0];
|
|
163
|
+
const currentRate = getRates(rt, provider, ev.model);
|
|
164
|
+
if (!currentRate) continue;
|
|
165
|
+
if (currentRate.kind === "estimated") continue;
|
|
166
|
+
const currentCost = ev.input_tokens / 1e6 * currentRate.input + ev.output_tokens / 1e6 * currentRate.output;
|
|
167
|
+
const cheapCost = ev.input_tokens / 1e6 * cheapest.r.input + ev.output_tokens / 1e6 * cheapest.r.output;
|
|
168
|
+
const diff = currentCost - cheapCost;
|
|
169
|
+
if (diff > 0) routingSavings += diff;
|
|
170
|
+
}
|
|
171
|
+
const sortedByInput = [...events].sort((a, b) => (b.input_tokens || 0) - (a.input_tokens || 0));
|
|
172
|
+
const k = Math.max(1, Math.floor(sortedByInput.length * 0.2));
|
|
173
|
+
const contextTargets = sortedByInput.slice(0, k);
|
|
174
|
+
let contextSavings = 0;
|
|
175
|
+
for (const ev of contextTargets) {
|
|
176
|
+
const r = getRates(rt, ev.provider, ev.model);
|
|
177
|
+
if (!r) continue;
|
|
178
|
+
const inputPerM = r.input;
|
|
179
|
+
const saveTokens = (ev.input_tokens || 0) * 0.25;
|
|
180
|
+
contextSavings += saveTokens / 1e6 * inputPerM;
|
|
181
|
+
}
|
|
182
|
+
let retryWaste = 0;
|
|
183
|
+
for (const ev of events) {
|
|
184
|
+
const retries = Number(ev.retries || 0);
|
|
185
|
+
if (retries <= 0) continue;
|
|
186
|
+
const base = costOfEvent(rt, { ...ev, retries: 0 }).cost;
|
|
187
|
+
retryWaste += base * retries;
|
|
188
|
+
}
|
|
189
|
+
const estimatedSavingsTotal = routingSavings + contextSavings + retryWaste;
|
|
190
|
+
const analysis = {
|
|
191
|
+
total_cost: round2(total),
|
|
192
|
+
by_model_top: topN(byModel, 10).map((x) => ({ ...x, cost: round2(x.cost) })),
|
|
193
|
+
by_feature_top: topN(byFeature, 10).map((x) => ({ ...x, cost: round2(x.cost) })),
|
|
194
|
+
unknown_models: uniqUnknown(unknownModels),
|
|
195
|
+
rate_table_version: rt.version,
|
|
196
|
+
rate_table_date: rt.date
|
|
197
|
+
};
|
|
198
|
+
const savings = {
|
|
199
|
+
estimated_savings_total: round2(estimatedSavingsTotal),
|
|
200
|
+
routing_savings: round2(routingSavings),
|
|
201
|
+
context_savings: round2(contextSavings),
|
|
202
|
+
retry_waste: round2(retryWaste),
|
|
203
|
+
notes: [
|
|
204
|
+
`a) \uBAA8\uB378 \uB77C\uC6B0\uD305 \uC808\uAC10(\uCD94\uC815): $${round2(routingSavings)}`,
|
|
205
|
+
`b) \uCEE8\uD14D\uC2A4\uD2B8 \uAC10\uCD95(\uCD94\uC815): $${round2(contextSavings)} (\uC0C1\uC704 20% input\uC5D0 25% \uAC10\uCD95 \uAC00\uC815)`,
|
|
206
|
+
`c) \uC7AC\uC2DC\uB3C4/\uC624\uB958 \uB0AD\uBE44: $${round2(retryWaste)} (retries \uAE30\uBC18)`
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
const policy = buildPolicy(rt, events);
|
|
210
|
+
return { analysis, savings, policy };
|
|
211
|
+
}
|
|
212
|
+
function buildPolicy(rt, events) {
|
|
213
|
+
const freq = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const ev of events) freq.set(ev.provider, (freq.get(ev.provider) || 0) + 1);
|
|
215
|
+
const defaultProvider = [...freq.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || "openai";
|
|
216
|
+
const rules = [];
|
|
217
|
+
for (const provider of Object.keys(rt.providers)) {
|
|
218
|
+
const p = rt.providers[provider];
|
|
219
|
+
const entries = Object.entries(p.models);
|
|
220
|
+
if (entries.length === 0) continue;
|
|
221
|
+
const cheapest = entries.map(([name, r]) => ({ name, score: (r.input + r.output) / 2, r })).sort((a, b) => a.score - b.score)[0];
|
|
222
|
+
rules.push({
|
|
223
|
+
match: { provider, feature_tag_in: ["summarize", "classify", "translate"] },
|
|
224
|
+
action: { recommend_model: cheapest.name, reason: "cheap-feature routing" }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
rules.push({ match: { model_unknown: true }, action: { keep: true, reason: "unknown model -> no policy applied" } });
|
|
228
|
+
return {
|
|
229
|
+
version: 1,
|
|
230
|
+
default_provider: defaultProvider,
|
|
231
|
+
rules,
|
|
232
|
+
budgets: { currency: rt.currency, notes: "MVP: budgets not enforced" },
|
|
233
|
+
generated_from: { rate_table_version: rt.version, input: "./aiopt-input/usage.jsonl" }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function uniqUnknown(list) {
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const x of list) {
|
|
240
|
+
const k = `${x.provider}:${x.model}:${x.reason}`;
|
|
241
|
+
if (seen.has(k)) continue;
|
|
242
|
+
seen.add(k);
|
|
243
|
+
out.push(x);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
function round2(n) {
|
|
248
|
+
return Math.round(n * 100) / 100;
|
|
249
|
+
}
|
|
250
|
+
function writeOutputs(outDir, analysis, savings, policy) {
|
|
251
|
+
import_fs2.default.mkdirSync(outDir, { recursive: true });
|
|
252
|
+
import_fs2.default.writeFileSync(import_path2.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
|
|
253
|
+
const report = [
|
|
254
|
+
`\uCD1D\uBE44\uC6A9: $${analysis.total_cost}`,
|
|
255
|
+
`\uC808\uAC10 \uAC00\uB2A5 \uAE08\uC561(Estimated): $${savings.estimated_savings_total}`,
|
|
256
|
+
`\uC808\uAC10 \uADFC\uAC70 3\uC904:`,
|
|
257
|
+
savings.notes[0],
|
|
258
|
+
savings.notes[1],
|
|
259
|
+
savings.notes[2],
|
|
260
|
+
""
|
|
261
|
+
].join("\n");
|
|
262
|
+
import_fs2.default.writeFileSync(import_path2.default.join(outDir, "report.txt"), report);
|
|
263
|
+
import_fs2.default.writeFileSync(import_path2.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/cli.ts
|
|
267
|
+
var program = new import_commander.Command();
|
|
268
|
+
var DEFAULT_INPUT = "./aiopt-input/usage.jsonl";
|
|
269
|
+
var DEFAULT_OUTPUT_DIR = "./aiopt-output";
|
|
270
|
+
function loadRateTable() {
|
|
271
|
+
const p = import_path3.default.join(__dirname, "..", "rates", "rate_table.json");
|
|
272
|
+
return JSON.parse(import_fs3.default.readFileSync(p, "utf8"));
|
|
273
|
+
}
|
|
274
|
+
program.name("aiopt").description("AI \uBE44\uC6A9 \uC790\uB3D9 \uC808\uAC10 \uC778\uD504\uB77C \u2014 \uC11C\uBC84 \uC5C6\uB294 \uB85C\uCEEC CLI MVP").version("0.0.1");
|
|
275
|
+
program.command("init").description("aiopt-input/ \uBC0F \uC0D8\uD50C usage.jsonl, aiopt-output/ \uC0DD\uC131").action(() => {
|
|
276
|
+
ensureDir("./aiopt-input");
|
|
277
|
+
ensureDir("./aiopt-output");
|
|
278
|
+
const sampleSrc = import_path3.default.join(__dirname, "..", "samples", "sample_usage.jsonl");
|
|
279
|
+
const dst = import_path3.default.join("./aiopt-input", "usage.jsonl");
|
|
280
|
+
if (!import_fs3.default.existsSync(dst)) {
|
|
281
|
+
import_fs3.default.copyFileSync(sampleSrc, dst);
|
|
282
|
+
console.log("Created ./aiopt-input/usage.jsonl (sample)");
|
|
283
|
+
} else {
|
|
284
|
+
console.log("Exists ./aiopt-input/usage.jsonl (skip)");
|
|
285
|
+
}
|
|
286
|
+
console.log("Ready: ./aiopt-output/");
|
|
287
|
+
});
|
|
288
|
+
program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C \uBD84\uC11D\uD558\uACE0 3\uAC1C \uC0B0\uCD9C\uBB3C \uC0DD\uC131").option("--input <path>", "input file path (default: ./aiopt-input/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action((opts) => {
|
|
289
|
+
const inputPath = String(opts.input);
|
|
290
|
+
const outDir = String(opts.out);
|
|
291
|
+
if (!import_fs3.default.existsSync(inputPath)) {
|
|
292
|
+
console.error(`Input not found: ${inputPath}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const rt = loadRateTable();
|
|
296
|
+
const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);
|
|
297
|
+
const { analysis, savings, policy } = analyze(rt, events);
|
|
298
|
+
policy.generated_from.input = inputPath;
|
|
299
|
+
writeOutputs(outDir, analysis, savings, policy);
|
|
300
|
+
console.log(`OK: ${outDir}/analysis.json`);
|
|
301
|
+
console.log(`OK: ${outDir}/report.txt`);
|
|
302
|
+
console.log(`OK: ${outDir}/cost-policy.json`);
|
|
303
|
+
});
|
|
304
|
+
program.command("policy").description("\uB9C8\uC9C0\uB9C9 scan \uACB0\uACFC \uAE30\uBC18\uC73C\uB85C cost-policy.json\uB9CC \uC7AC\uC0DD\uC131 (MVP: scan\uACFC \uB3D9\uC77C \uB85C\uC9C1)").option("--input <path>", "input file path (default: ./aiopt-input/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action((opts) => {
|
|
305
|
+
const inputPath = String(opts.input);
|
|
306
|
+
const outDir = String(opts.out);
|
|
307
|
+
const rt = loadRateTable();
|
|
308
|
+
const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);
|
|
309
|
+
const { policy } = analyze(rt, events);
|
|
310
|
+
policy.generated_from.input = inputPath;
|
|
311
|
+
ensureDir(outDir);
|
|
312
|
+
import_fs3.default.writeFileSync(import_path3.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
|
|
313
|
+
console.log(`OK: ${outDir}/cost-policy.json`);
|
|
314
|
+
});
|
|
315
|
+
program.parse(process.argv);
|
|
316
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/io.ts","../src/scan.ts","../src/cost.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport fs from 'fs';\nimport path from 'path';\nimport { Command } from 'commander';\nimport { ensureDir, isCsvPath, readCsv, readJsonl } from './io';\nimport { RateTable } from './types';\nimport { analyze, writeOutputs } from './scan';\n\nconst program = new Command();\n\nconst DEFAULT_INPUT = './aiopt-input/usage.jsonl';\nconst DEFAULT_OUTPUT_DIR = './aiopt-output';\n\nfunction loadRateTable(): RateTable {\n const p = path.join(__dirname, '..', 'rates', 'rate_table.json');\n return JSON.parse(fs.readFileSync(p, 'utf8'));\n}\n\nprogram\n .name('aiopt')\n .description('AI 비용 자동 절감 인프라 — 서버 없는 로컬 CLI MVP')\n .version('0.0.1');\n\nprogram\n .command('init')\n .description('aiopt-input/ 및 샘플 usage.jsonl, aiopt-output/ 생성')\n .action(() => {\n ensureDir('./aiopt-input');\n ensureDir('./aiopt-output');\n\n const sampleSrc = path.join(__dirname, '..', 'samples', 'sample_usage.jsonl');\n const dst = path.join('./aiopt-input', 'usage.jsonl');\n\n if (!fs.existsSync(dst)) {\n fs.copyFileSync(sampleSrc, dst);\n console.log('Created ./aiopt-input/usage.jsonl (sample)');\n } else {\n console.log('Exists ./aiopt-input/usage.jsonl (skip)');\n }\n\n console.log('Ready: ./aiopt-output/');\n });\n\nprogram\n .command('scan')\n .description('입력 로그(JSONL/CSV)를 분석하고 3개 산출물 생성')\n .option('--input <path>', 'input file path (default: ./aiopt-input/usage.jsonl)', DEFAULT_INPUT)\n .option('--out <dir>', 'output dir (default: ./aiopt-output)', DEFAULT_OUTPUT_DIR)\n .action((opts) => {\n const inputPath = String(opts.input);\n const outDir = String(opts.out);\n\n if (!fs.existsSync(inputPath)) {\n console.error(`Input not found: ${inputPath}`);\n process.exit(1);\n }\n\n const rt = loadRateTable();\n const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);\n\n const { analysis, savings, policy } = analyze(rt, events);\n // For reproducibility: embed input path & rate table meta\n policy.generated_from.input = inputPath;\n\n writeOutputs(outDir, analysis, savings, policy);\n\n console.log(`OK: ${outDir}/analysis.json`);\n console.log(`OK: ${outDir}/report.txt`);\n console.log(`OK: ${outDir}/cost-policy.json`);\n });\n\nprogram\n .command('policy')\n .description('마지막 scan 결과 기반으로 cost-policy.json만 재생성 (MVP: scan과 동일 로직)')\n .option('--input <path>', 'input file path (default: ./aiopt-input/usage.jsonl)', DEFAULT_INPUT)\n .option('--out <dir>', 'output dir (default: ./aiopt-output)', DEFAULT_OUTPUT_DIR)\n .action((opts) => {\n const inputPath = String(opts.input);\n const outDir = String(opts.out);\n const rt = loadRateTable();\n const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);\n const { policy } = analyze(rt, events);\n policy.generated_from.input = inputPath;\n\n ensureDir(outDir);\n fs.writeFileSync(path.join(outDir, 'cost-policy.json'), JSON.stringify(policy, null, 2));\n console.log(`OK: ${outDir}/cost-policy.json`);\n });\n\nprogram.parse(process.argv);\n","import fs from 'fs';\nimport path from 'path';\nimport { parse as parseCsv } from 'csv-parse/sync';\nimport { UsageEvent } from './types';\n\nexport function ensureDir(p: string) {\n fs.mkdirSync(p, { recursive: true });\n}\n\nexport function readJsonl(filePath: string): UsageEvent[] {\n const raw = fs.readFileSync(filePath, 'utf8');\n const lines = raw.split(/\\r?\\n/).filter(l => l.trim().length > 0);\n const out: UsageEvent[] = [];\n for (const line of lines) {\n const obj = JSON.parse(line);\n out.push(normalizeEvent(obj));\n }\n return out;\n}\n\nexport function readCsv(filePath: string): UsageEvent[] {\n const raw = fs.readFileSync(filePath, 'utf8');\n const records = parseCsv(raw, { columns: true, skip_empty_lines: true, trim: true });\n return records.map((r: any) => normalizeEvent(r));\n}\n\nfunction toNum(x: any, def = 0): number {\n const n = Number(x);\n return Number.isFinite(n) ? n : def;\n}\n\nfunction normalizeEvent(x: any): UsageEvent {\n return {\n ts: String(x.ts ?? ''),\n provider: String(x.provider ?? '').toLowerCase(),\n model: String(x.model ?? ''),\n input_tokens: toNum(x.input_tokens),\n output_tokens: toNum(x.output_tokens),\n feature_tag: String(x.feature_tag ?? ''),\n retries: toNum(x.retries),\n status: String(x.status ?? ''),\n billed_cost: x.billed_cost === undefined || x.billed_cost === '' ? undefined : toNum(x.billed_cost)\n };\n}\n\nexport function isCsvPath(p: string) {\n return path.extname(p).toLowerCase() === '.csv';\n}\n","import fs from 'fs';\nimport path from 'path';\nimport { RateTable, UsageEvent } from './types';\nimport { costOfEvent, getRates } from './cost';\n\nexport type AnalysisJson = {\n total_cost: number;\n by_model_top: Array<{ key: string; cost: number; events: number }>;\n by_feature_top: Array<{ key: string; cost: number; events: number }>;\n unknown_models: Array<{ provider: string; model: string; reason: string }>;\n rate_table_version: string;\n rate_table_date: string;\n};\n\nexport type Savings = {\n estimated_savings_total: number;\n routing_savings: number;\n context_savings: number;\n retry_waste: number;\n notes: [string, string, string];\n};\n\nexport type PolicyJson = {\n version: number;\n default_provider: string;\n rules: Array<any>;\n budgets: { currency: string; notes?: string };\n generated_from: { rate_table_version: string; input: string };\n};\n\nconst ROUTE_TO_CHEAP_FEATURES = new Set(['summarize', 'classify', 'translate']);\n\nfunction topN(map: Map<string, { cost: number; events: number }>, n: number) {\n return [...map.entries()]\n .map(([key, v]) => ({ key, cost: v.cost, events: v.events }))\n .sort((a, b) => b.cost - a.cost)\n .slice(0, n);\n}\n\nexport function analyze(rt: RateTable, events: UsageEvent[]): { analysis: AnalysisJson; savings: Savings; policy: PolicyJson } {\n const byModel = new Map<string, { cost: number; events: number }>();\n const byFeature = new Map<string, { cost: number; events: number }>();\n const unknownModels: AnalysisJson['unknown_models'] = [];\n\n const perEventCosts: Array<{ ev: UsageEvent; cost: number }> = [];\n\n let total = 0;\n for (const ev of events) {\n const cr = costOfEvent(rt, ev);\n total += cr.cost;\n perEventCosts.push({ ev, cost: cr.cost });\n\n const mk = `${ev.provider}:${ev.model}`;\n const fk = ev.feature_tag || '(none)';\n\n const mv = byModel.get(mk) || { cost: 0, events: 0 };\n mv.cost += cr.cost; mv.events += 1;\n byModel.set(mk, mv);\n\n const fv = byFeature.get(fk) || { cost: 0, events: 0 };\n fv.cost += cr.cost; fv.events += 1;\n byFeature.set(fk, fv);\n\n const rr = getRates(rt, ev.provider, ev.model);\n if (!rr) {\n unknownModels.push({ provider: ev.provider, model: ev.model, reason: 'unknown provider (estimated)' });\n } else if (rr.kind === 'estimated') {\n unknownModels.push({ provider: ev.provider, model: ev.model, reason: 'unknown model (estimated)' });\n }\n }\n\n // --- Savings lever 1: routing (cheap features -> cheapest known model per provider)\n let routingSavings = 0;\n for (const { ev } of perEventCosts) {\n if (!ROUTE_TO_CHEAP_FEATURES.has(String(ev.feature_tag || '').toLowerCase())) continue;\n\n const provider = ev.provider;\n const p = rt.providers[provider];\n if (!p) continue;\n\n // Choose cheapest model by (input+output) average.\n const entries = Object.entries(p.models);\n if (entries.length === 0) continue;\n const cheapest = entries\n .map(([name, r]) => ({ name, score: (r.input + r.output) / 2, r }))\n .sort((a, b) => a.score - b.score)[0];\n\n // current cost vs cheapest cost\n const currentRate = getRates(rt, provider, ev.model);\n if (!currentRate) continue;\n if (currentRate.kind === 'estimated') continue; // unknown model: policy not applied\n\n const currentCost = (ev.input_tokens / 1e6) * currentRate.input + (ev.output_tokens / 1e6) * currentRate.output;\n const cheapCost = (ev.input_tokens / 1e6) * cheapest.r.input + (ev.output_tokens / 1e6) * cheapest.r.output;\n const diff = currentCost - cheapCost;\n if (diff > 0) routingSavings += diff;\n }\n\n // --- Savings lever 2: context reduction estimate\n // Deterministic: top 20% by input_tokens -> reduce input_tokens by 25%\n const sortedByInput = [...events].sort((a, b) => (b.input_tokens || 0) - (a.input_tokens || 0));\n const k = Math.max(1, Math.floor(sortedByInput.length * 0.2));\n const contextTargets = sortedByInput.slice(0, k);\n let contextSavings = 0;\n for (const ev of contextTargets) {\n const r = getRates(rt, ev.provider, ev.model);\n if (!r) continue;\n const inputPerM = r.input;\n const saveTokens = (ev.input_tokens || 0) * 0.25;\n contextSavings += (saveTokens / 1e6) * inputPerM;\n }\n\n // --- Savings lever 3: retry waste\n // retries>=1 -> wasted cost = base cost per call * retries\n let retryWaste = 0;\n for (const ev of events) {\n const retries = Number(ev.retries || 0);\n if (retries <= 0) continue;\n const base = costOfEvent(rt, { ...ev, retries: 0 }).cost;\n retryWaste += base * retries;\n }\n\n const estimatedSavingsTotal = routingSavings + contextSavings + retryWaste;\n\n const analysis: AnalysisJson = {\n total_cost: round2(total),\n by_model_top: topN(byModel, 10).map(x => ({ ...x, cost: round2(x.cost) })),\n by_feature_top: topN(byFeature, 10).map(x => ({ ...x, cost: round2(x.cost) })),\n unknown_models: uniqUnknown(unknownModels),\n rate_table_version: rt.version,\n rate_table_date: rt.date\n };\n\n const savings: Savings = {\n estimated_savings_total: round2(estimatedSavingsTotal),\n routing_savings: round2(routingSavings),\n context_savings: round2(contextSavings),\n retry_waste: round2(retryWaste),\n notes: [\n `a) 모델 라우팅 절감(추정): $${round2(routingSavings)}`,\n `b) 컨텍스트 감축(추정): $${round2(contextSavings)} (상위 20% input에 25% 감축 가정)`,\n `c) 재시도/오류 낭비: $${round2(retryWaste)} (retries 기반)`\n ]\n };\n\n const policy: PolicyJson = buildPolicy(rt, events);\n\n return { analysis, savings, policy };\n}\n\nfunction buildPolicy(rt: RateTable, events: UsageEvent[]): PolicyJson {\n // Default provider: most frequent\n const freq = new Map<string, number>();\n for (const ev of events) freq.set(ev.provider, (freq.get(ev.provider) || 0) + 1);\n const defaultProvider = [...freq.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || 'openai';\n\n // For cheap features: recommend cheapest known model per provider.\n const rules: any[] = [];\n for (const provider of Object.keys(rt.providers)) {\n const p = rt.providers[provider];\n const entries = Object.entries(p.models);\n if (entries.length === 0) continue;\n const cheapest = entries\n .map(([name, r]) => ({ name, score: (r.input + r.output) / 2, r }))\n .sort((a, b) => a.score - b.score)[0];\n\n rules.push({\n match: { provider, feature_tag_in: ['summarize', 'classify', 'translate'] },\n action: { recommend_model: cheapest.name, reason: 'cheap-feature routing' }\n });\n }\n\n // Unknown models: keep (no policy)\n rules.push({ match: { model_unknown: true }, action: { keep: true, reason: 'unknown model -> no policy applied' } });\n\n return {\n version: 1,\n default_provider: defaultProvider,\n rules,\n budgets: { currency: rt.currency, notes: 'MVP: budgets not enforced' },\n generated_from: { rate_table_version: rt.version, input: './aiopt-input/usage.jsonl' }\n };\n}\n\nfunction uniqUnknown(list: AnalysisJson['unknown_models']) {\n const seen = new Set<string>();\n const out: AnalysisJson['unknown_models'] = [];\n for (const x of list) {\n const k = `${x.provider}:${x.model}:${x.reason}`;\n if (seen.has(k)) continue;\n seen.add(k);\n out.push(x);\n }\n return out;\n}\n\nfunction round2(n: number) {\n return Math.round(n * 100) / 100;\n}\n\nexport function writeOutputs(outDir: string, analysis: AnalysisJson, savings: Savings, policy: PolicyJson) {\n fs.mkdirSync(outDir, { recursive: true });\n fs.writeFileSync(path.join(outDir, 'analysis.json'), JSON.stringify(analysis, null, 2));\n\n const report = [\n `총비용: $${analysis.total_cost}`,\n `절감 가능 금액(Estimated): $${savings.estimated_savings_total}`,\n `절감 근거 3줄:`,\n savings.notes[0],\n savings.notes[1],\n savings.notes[2],\n ''\n ].join('\\n');\n fs.writeFileSync(path.join(outDir, 'report.txt'), report);\n\n fs.writeFileSync(path.join(outDir, 'cost-policy.json'), JSON.stringify(policy, null, 2));\n}\n","import { RateTable, UsageEvent } from './types';\n\nexport type CostResult = {\n cost: number;\n used_rate: {\n kind: 'billed_cost' | 'official' | 'estimated';\n provider: string;\n model: string;\n input_per_m: number;\n output_per_m: number;\n };\n};\n\nexport function getRates(rt: RateTable, provider: string, model: string) {\n const p = rt.providers[provider];\n if (!p) return null;\n const m = p.models[model];\n if (m) return { kind: 'official' as const, input: m.input, output: m.output };\n return { kind: 'estimated' as const, input: p.default_estimated.input, output: p.default_estimated.output };\n}\n\nexport function costOfEvent(rt: RateTable, ev: UsageEvent): CostResult {\n if (typeof ev.billed_cost === 'number' && Number.isFinite(ev.billed_cost)) {\n return {\n cost: ev.billed_cost,\n used_rate: {\n kind: 'billed_cost',\n provider: ev.provider,\n model: ev.model,\n input_per_m: 0,\n output_per_m: 0\n }\n };\n }\n\n const r = getRates(rt, ev.provider, ev.model);\n if (!r) {\n // Unknown provider: deterministic fallback estimate\n const input_per_m = 1.0;\n const output_per_m = 4.0;\n const cost = (ev.input_tokens / 1e6) * input_per_m + (ev.output_tokens / 1e6) * output_per_m;\n return {\n cost,\n used_rate: { kind: 'estimated', provider: ev.provider, model: ev.model, input_per_m, output_per_m }\n };\n }\n\n const cost = (ev.input_tokens / 1e6) * r.input + (ev.output_tokens / 1e6) * r.output;\n return {\n cost,\n used_rate: {\n kind: r.kind,\n provider: ev.provider,\n model: ev.model,\n input_per_m: r.input,\n output_per_m: r.output\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,IAAAA,aAAe;AACf,IAAAC,eAAiB;AACjB,uBAAwB;;;ACJxB,gBAAe;AACf,kBAAiB;AACjB,kBAAkC;AAG3B,SAAS,UAAU,GAAW;AACnC,YAAAC,QAAG,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACrC;AAEO,SAAS,UAAU,UAAgC;AACxD,QAAM,MAAM,UAAAA,QAAG,aAAa,UAAU,MAAM;AAC5C,QAAM,QAAQ,IAAI,MAAM,OAAO,EAAE,OAAO,OAAK,EAAE,KAAK,EAAE,SAAS,CAAC;AAChE,QAAM,MAAoB,CAAC;AAC3B,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,QAAI,KAAK,eAAe,GAAG,CAAC;AAAA,EAC9B;AACA,SAAO;AACT;AAEO,SAAS,QAAQ,UAAgC;AACtD,QAAM,MAAM,UAAAA,QAAG,aAAa,UAAU,MAAM;AAC5C,QAAM,cAAU,YAAAC,OAAS,KAAK,EAAE,SAAS,MAAM,kBAAkB,MAAM,MAAM,KAAK,CAAC;AACnF,SAAO,QAAQ,IAAI,CAAC,MAAW,eAAe,CAAC,CAAC;AAClD;AAEA,SAAS,MAAM,GAAQ,MAAM,GAAW;AACtC,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,SAAS,eAAe,GAAoB;AAC1C,SAAO;AAAA,IACL,IAAI,OAAO,EAAE,MAAM,EAAE;AAAA,IACrB,UAAU,OAAO,EAAE,YAAY,EAAE,EAAE,YAAY;AAAA,IAC/C,OAAO,OAAO,EAAE,SAAS,EAAE;AAAA,IAC3B,cAAc,MAAM,EAAE,YAAY;AAAA,IAClC,eAAe,MAAM,EAAE,aAAa;AAAA,IACpC,aAAa,OAAO,EAAE,eAAe,EAAE;AAAA,IACvC,SAAS,MAAM,EAAE,OAAO;AAAA,IACxB,QAAQ,OAAO,EAAE,UAAU,EAAE;AAAA,IAC7B,aAAa,EAAE,gBAAgB,UAAa,EAAE,gBAAgB,KAAK,SAAY,MAAM,EAAE,WAAW;AAAA,EACpG;AACF;AAEO,SAAS,UAAU,GAAW;AACnC,SAAO,YAAAC,QAAK,QAAQ,CAAC,EAAE,YAAY,MAAM;AAC3C;;;AC/CA,IAAAC,aAAe;AACf,IAAAC,eAAiB;;;ACYV,SAAS,SAAS,IAAe,UAAkB,OAAe;AACvE,QAAM,IAAI,GAAG,UAAU,QAAQ;AAC/B,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,EAAE,OAAO,KAAK;AACxB,MAAI,EAAG,QAAO,EAAE,MAAM,YAAqB,OAAO,EAAE,OAAO,QAAQ,EAAE,OAAO;AAC5E,SAAO,EAAE,MAAM,aAAsB,OAAO,EAAE,kBAAkB,OAAO,QAAQ,EAAE,kBAAkB,OAAO;AAC5G;AAEO,SAAS,YAAY,IAAe,IAA4B;AACrE,MAAI,OAAO,GAAG,gBAAgB,YAAY,OAAO,SAAS,GAAG,WAAW,GAAG;AACzE,WAAO;AAAA,MACL,MAAM,GAAG;AAAA,MACT,WAAW;AAAA,QACT,MAAM;AAAA,QACN,UAAU,GAAG;AAAA,QACb,OAAO,GAAG;AAAA,QACV,aAAa;AAAA,QACb,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,IAAI,SAAS,IAAI,GAAG,UAAU,GAAG,KAAK;AAC5C,MAAI,CAAC,GAAG;AAEN,UAAM,cAAc;AACpB,UAAM,eAAe;AACrB,UAAMC,QAAQ,GAAG,eAAe,MAAO,cAAe,GAAG,gBAAgB,MAAO;AAChF,WAAO;AAAA,MACL,MAAAA;AAAA,MACA,WAAW,EAAE,MAAM,aAAa,UAAU,GAAG,UAAU,OAAO,GAAG,OAAO,aAAa,aAAa;AAAA,IACpG;AAAA,EACF;AAEA,QAAM,OAAQ,GAAG,eAAe,MAAO,EAAE,QAAS,GAAG,gBAAgB,MAAO,EAAE;AAC9E,SAAO;AAAA,IACL;AAAA,IACA,WAAW;AAAA,MACT,MAAM,EAAE;AAAA,MACR,UAAU,GAAG;AAAA,MACb,OAAO,GAAG;AAAA,MACV,aAAa,EAAE;AAAA,MACf,cAAc,EAAE;AAAA,IAClB;AAAA,EACF;AACF;;;AD5BA,IAAM,0BAA0B,oBAAI,IAAI,CAAC,aAAa,YAAY,WAAW,CAAC;AAE9E,SAAS,KAAK,KAAoD,GAAW;AAC3E,SAAO,CAAC,GAAG,IAAI,QAAQ,CAAC,EACrB,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,EAAE,OAAO,EAAE,EAC3D,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAC9B,MAAM,GAAG,CAAC;AACf;AAEO,SAAS,QAAQ,IAAe,QAAwF;AAC7H,QAAM,UAAU,oBAAI,IAA8C;AAClE,QAAM,YAAY,oBAAI,IAA8C;AACpE,QAAM,gBAAgD,CAAC;AAEvD,QAAM,gBAAyD,CAAC;AAEhE,MAAI,QAAQ;AACZ,aAAW,MAAM,QAAQ;AACvB,UAAM,KAAK,YAAY,IAAI,EAAE;AAC7B,aAAS,GAAG;AACZ,kBAAc,KAAK,EAAE,IAAI,MAAM,GAAG,KAAK,CAAC;AAExC,UAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,GAAG,KAAK;AACrC,UAAM,KAAK,GAAG,eAAe;AAE7B,UAAM,KAAK,QAAQ,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE;AACnD,OAAG,QAAQ,GAAG;AAAM,OAAG,UAAU;AACjC,YAAQ,IAAI,IAAI,EAAE;AAElB,UAAM,KAAK,UAAU,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE;AACrD,OAAG,QAAQ,GAAG;AAAM,OAAG,UAAU;AACjC,cAAU,IAAI,IAAI,EAAE;AAEpB,UAAM,KAAK,SAAS,IAAI,GAAG,UAAU,GAAG,KAAK;AAC7C,QAAI,CAAC,IAAI;AACP,oBAAc,KAAK,EAAE,UAAU,GAAG,UAAU,OAAO,GAAG,OAAO,QAAQ,+BAA+B,CAAC;AAAA,IACvG,WAAW,GAAG,SAAS,aAAa;AAClC,oBAAc,KAAK,EAAE,UAAU,GAAG,UAAU,OAAO,GAAG,OAAO,QAAQ,4BAA4B,CAAC;AAAA,IACpG;AAAA,EACF;AAGA,MAAI,iBAAiB;AACrB,aAAW,EAAE,GAAG,KAAK,eAAe;AAClC,QAAI,CAAC,wBAAwB,IAAI,OAAO,GAAG,eAAe,EAAE,EAAE,YAAY,CAAC,EAAG;AAE9E,UAAM,WAAW,GAAG;AACpB,UAAM,IAAI,GAAG,UAAU,QAAQ;AAC/B,QAAI,CAAC,EAAG;AAGR,UAAM,UAAU,OAAO,QAAQ,EAAE,MAAM;AACvC,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,WAAW,QACd,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,QAAQ,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE,EAAE,EACjE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAGtC,UAAM,cAAc,SAAS,IAAI,UAAU,GAAG,KAAK;AACnD,QAAI,CAAC,YAAa;AAClB,QAAI,YAAY,SAAS,YAAa;AAEtC,UAAM,cAAe,GAAG,eAAe,MAAO,YAAY,QAAS,GAAG,gBAAgB,MAAO,YAAY;AACzG,UAAM,YAAa,GAAG,eAAe,MAAO,SAAS,EAAE,QAAS,GAAG,gBAAgB,MAAO,SAAS,EAAE;AACrG,UAAM,OAAO,cAAc;AAC3B,QAAI,OAAO,EAAG,mBAAkB;AAAA,EAClC;AAIA,QAAM,gBAAgB,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,gBAAgB,MAAM,EAAE,gBAAgB,EAAE;AAC9F,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,cAAc,SAAS,GAAG,CAAC;AAC5D,QAAM,iBAAiB,cAAc,MAAM,GAAG,CAAC;AAC/C,MAAI,iBAAiB;AACrB,aAAW,MAAM,gBAAgB;AAC/B,UAAM,IAAI,SAAS,IAAI,GAAG,UAAU,GAAG,KAAK;AAC5C,QAAI,CAAC,EAAG;AACR,UAAM,YAAY,EAAE;AACpB,UAAM,cAAc,GAAG,gBAAgB,KAAK;AAC5C,sBAAmB,aAAa,MAAO;AAAA,EACzC;AAIA,MAAI,aAAa;AACjB,aAAW,MAAM,QAAQ;AACvB,UAAM,UAAU,OAAO,GAAG,WAAW,CAAC;AACtC,QAAI,WAAW,EAAG;AAClB,UAAM,OAAO,YAAY,IAAI,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC,EAAE;AACpD,kBAAc,OAAO;AAAA,EACvB;AAEA,QAAM,wBAAwB,iBAAiB,iBAAiB;AAEhE,QAAM,WAAyB;AAAA,IAC7B,YAAY,OAAO,KAAK;AAAA,IACxB,cAAc,KAAK,SAAS,EAAE,EAAE,IAAI,QAAM,EAAE,GAAG,GAAG,MAAM,OAAO,EAAE,IAAI,EAAE,EAAE;AAAA,IACzE,gBAAgB,KAAK,WAAW,EAAE,EAAE,IAAI,QAAM,EAAE,GAAG,GAAG,MAAM,OAAO,EAAE,IAAI,EAAE,EAAE;AAAA,IAC7E,gBAAgB,YAAY,aAAa;AAAA,IACzC,oBAAoB,GAAG;AAAA,IACvB,iBAAiB,GAAG;AAAA,EACtB;AAEA,QAAM,UAAmB;AAAA,IACvB,yBAAyB,OAAO,qBAAqB;AAAA,IACrD,iBAAiB,OAAO,cAAc;AAAA,IACtC,iBAAiB,OAAO,cAAc;AAAA,IACtC,aAAa,OAAO,UAAU;AAAA,IAC9B,OAAO;AAAA,MACL,mEAAsB,OAAO,cAAc,CAAC;AAAA,MAC5C,4DAAoB,OAAO,cAAc,CAAC;AAAA,MAC1C,qDAAkB,OAAO,UAAU,CAAC;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,SAAqB,YAAY,IAAI,MAAM;AAEjD,SAAO,EAAE,UAAU,SAAS,OAAO;AACrC;AAEA,SAAS,YAAY,IAAe,QAAkC;AAEpE,QAAM,OAAO,oBAAI,IAAoB;AACrC,aAAW,MAAM,OAAQ,MAAK,IAAI,GAAG,WAAW,KAAK,IAAI,GAAG,QAAQ,KAAK,KAAK,CAAC;AAC/E,QAAM,kBAAkB,CAAC,GAAG,KAAK,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK;AAGnF,QAAM,QAAe,CAAC;AACtB,aAAW,YAAY,OAAO,KAAK,GAAG,SAAS,GAAG;AAChD,UAAM,IAAI,GAAG,UAAU,QAAQ;AAC/B,UAAM,UAAU,OAAO,QAAQ,EAAE,MAAM;AACvC,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,WAAW,QACd,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,QAAQ,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE,EAAE,EACjE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAEtC,UAAM,KAAK;AAAA,MACT,OAAO,EAAE,UAAU,gBAAgB,CAAC,aAAa,YAAY,WAAW,EAAE;AAAA,MAC1E,QAAQ,EAAE,iBAAiB,SAAS,MAAM,QAAQ,wBAAwB;AAAA,IAC5E,CAAC;AAAA,EACH;AAGA,QAAM,KAAK,EAAE,OAAO,EAAE,eAAe,KAAK,GAAG,QAAQ,EAAE,MAAM,MAAM,QAAQ,qCAAqC,EAAE,CAAC;AAEnH,SAAO;AAAA,IACL,SAAS;AAAA,IACT,kBAAkB;AAAA,IAClB;AAAA,IACA,SAAS,EAAE,UAAU,GAAG,UAAU,OAAO,4BAA4B;AAAA,IACrE,gBAAgB,EAAE,oBAAoB,GAAG,SAAS,OAAO,4BAA4B;AAAA,EACvF;AACF;AAEA,SAAS,YAAY,MAAsC;AACzD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAsC,CAAC;AAC7C,aAAW,KAAK,MAAM;AACpB,UAAM,IAAI,GAAG,EAAE,QAAQ,IAAI,EAAE,KAAK,IAAI,EAAE,MAAM;AAC9C,QAAI,KAAK,IAAI,CAAC,EAAG;AACjB,SAAK,IAAI,CAAC;AACV,QAAI,KAAK,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAEA,SAAS,OAAO,GAAW;AACzB,SAAO,KAAK,MAAM,IAAI,GAAG,IAAI;AAC/B;AAEO,SAAS,aAAa,QAAgB,UAAwB,SAAkB,QAAoB;AACzG,aAAAC,QAAG,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACxC,aAAAA,QAAG,cAAc,aAAAC,QAAK,KAAK,QAAQ,eAAe,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAEtF,QAAM,SAAS;AAAA,IACb,wBAAS,SAAS,UAAU;AAAA,IAC5B,uDAAyB,QAAQ,uBAAuB;AAAA,IACxD;AAAA,IACA,QAAQ,MAAM,CAAC;AAAA,IACf,QAAQ,MAAM,CAAC;AAAA,IACf,QAAQ,MAAM,CAAC;AAAA,IACf;AAAA,EACF,EAAE,KAAK,IAAI;AACX,aAAAD,QAAG,cAAc,aAAAC,QAAK,KAAK,QAAQ,YAAY,GAAG,MAAM;AAExD,aAAAD,QAAG,cAAc,aAAAC,QAAK,KAAK,QAAQ,kBAAkB,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AACzF;;;AF/MA,IAAM,UAAU,IAAI,yBAAQ;AAE5B,IAAM,gBAAgB;AACtB,IAAM,qBAAqB;AAE3B,SAAS,gBAA2B;AAClC,QAAM,IAAI,aAAAC,QAAK,KAAK,WAAW,MAAM,SAAS,iBAAiB;AAC/D,SAAO,KAAK,MAAM,WAAAC,QAAG,aAAa,GAAG,MAAM,CAAC;AAC9C;AAEA,QACG,KAAK,OAAO,EACZ,YAAY,oHAAoC,EAChD,QAAQ,OAAO;AAElB,QACG,QAAQ,MAAM,EACd,YAAY,0EAAiD,EAC7D,OAAO,MAAM;AACZ,YAAU,eAAe;AACzB,YAAU,gBAAgB;AAE1B,QAAM,YAAY,aAAAD,QAAK,KAAK,WAAW,MAAM,WAAW,oBAAoB;AAC5E,QAAM,MAAM,aAAAA,QAAK,KAAK,iBAAiB,aAAa;AAEpD,MAAI,CAAC,WAAAC,QAAG,WAAW,GAAG,GAAG;AACvB,eAAAA,QAAG,aAAa,WAAW,GAAG;AAC9B,YAAQ,IAAI,4CAA4C;AAAA,EAC1D,OAAO;AACL,YAAQ,IAAI,yCAAyC;AAAA,EACvD;AAEA,UAAQ,IAAI,wBAAwB;AACtC,CAAC;AAEH,QACG,QAAQ,MAAM,EACd,YAAY,6GAAkC,EAC9C,OAAO,kBAAkB,wDAAwD,aAAa,EAC9F,OAAO,eAAe,wCAAwC,kBAAkB,EAChF,OAAO,CAAC,SAAS;AAChB,QAAM,YAAY,OAAO,KAAK,KAAK;AACnC,QAAM,SAAS,OAAO,KAAK,GAAG;AAE9B,MAAI,CAAC,WAAAA,QAAG,WAAW,SAAS,GAAG;AAC7B,YAAQ,MAAM,oBAAoB,SAAS,EAAE;AAC7C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,KAAK,cAAc;AACzB,QAAM,SAAS,UAAU,SAAS,IAAI,QAAQ,SAAS,IAAI,UAAU,SAAS;AAE9E,QAAM,EAAE,UAAU,SAAS,OAAO,IAAI,QAAQ,IAAI,MAAM;AAExD,SAAO,eAAe,QAAQ;AAE9B,eAAa,QAAQ,UAAU,SAAS,MAAM;AAE9C,UAAQ,IAAI,OAAO,MAAM,gBAAgB;AACzC,UAAQ,IAAI,OAAO,MAAM,aAAa;AACtC,UAAQ,IAAI,OAAO,MAAM,mBAAmB;AAC9C,CAAC;AAEH,QACG,QAAQ,QAAQ,EAChB,YAAY,qJAA2D,EACvE,OAAO,kBAAkB,wDAAwD,aAAa,EAC9F,OAAO,eAAe,wCAAwC,kBAAkB,EAChF,OAAO,CAAC,SAAS;AAChB,QAAM,YAAY,OAAO,KAAK,KAAK;AACnC,QAAM,SAAS,OAAO,KAAK,GAAG;AAC9B,QAAM,KAAK,cAAc;AACzB,QAAM,SAAS,UAAU,SAAS,IAAI,QAAQ,SAAS,IAAI,UAAU,SAAS;AAC9E,QAAM,EAAE,OAAO,IAAI,QAAQ,IAAI,MAAM;AACrC,SAAO,eAAe,QAAQ;AAE9B,YAAU,MAAM;AAChB,aAAAA,QAAG,cAAc,aAAAD,QAAK,KAAK,QAAQ,kBAAkB,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AACvF,UAAQ,IAAI,OAAO,MAAM,mBAAmB;AAC9C,CAAC;AAEH,QAAQ,MAAM,QAAQ,IAAI;","names":["import_fs","import_path","fs","parseCsv","path","import_fs","import_path","cost","fs","path","path","fs"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aiopt",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Serverless local CLI MVP for AI API cost analysis & cost-policy generation",
|
|
5
|
+
"bin": {
|
|
6
|
+
"aiopt": "dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"rates",
|
|
13
|
+
"samples",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "node --enable-source-maps dist/cli.js",
|
|
19
|
+
"prepack": "npm run build",
|
|
20
|
+
"test:npx": "npm pack --silent && node -e \"const fs=require('fs');const p=fs.readdirSync('.').find(f=>/^aiopt-.*\\.tgz$/.test(f)); if(!p) throw new Error('tgz not found'); console.log('tgz',p);\" && npx --yes ./$(ls -1 aiopt-*.tgz | tail -n 1) init && npx --yes ./$(ls -1 aiopt-*.tgz | tail -n 1) scan --input ./aiopt-input/usage.jsonl && test -f ./aiopt-output/report.txt && echo OK"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^14.0.0",
|
|
24
|
+
"csv-parse": "^6.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.0.0",
|
|
28
|
+
"tsup": "^8.5.0",
|
|
29
|
+
"typescript": "^5.9.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2026-02-07-a",
|
|
3
|
+
"date": "2026-02-07",
|
|
4
|
+
"currency": "USD",
|
|
5
|
+
"units": "per_1M_tokens",
|
|
6
|
+
"notes": "MVP subset. Unknown models are treated as estimated.",
|
|
7
|
+
"providers": {
|
|
8
|
+
"openai": {
|
|
9
|
+
"default_estimated": { "input": 1.0, "output": 4.0, "cached_input": 0.1 },
|
|
10
|
+
"models": {
|
|
11
|
+
"gpt-5-mini": { "input": 0.25, "output": 2.0, "cached_input": 0.025 },
|
|
12
|
+
"gpt-5.2": { "input": 1.75, "output": 14.0, "cached_input": 0.175 }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"anthropic": {
|
|
16
|
+
"default_estimated": { "input": 3.0, "output": 15.0, "cached_input": 0.0 },
|
|
17
|
+
"models": {
|
|
18
|
+
"claude-sonnet": { "input": 3.0, "output": 15.0, "cached_input": 0.0 },
|
|
19
|
+
"claude-haiku": { "input": 1.0, "output": 5.0, "cached_input": 0.0 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"gemini": {
|
|
23
|
+
"default_estimated": { "input": 0.5, "output": 1.5, "cached_input": 0.0 },
|
|
24
|
+
"models": {
|
|
25
|
+
"gemini-1.5-flash": { "input": 0.35, "output": 1.05, "cached_input": 0.0 },
|
|
26
|
+
"gemini-1.5-pro": { "input": 1.25, "output": 5.0, "cached_input": 0.0 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"ts":"2026-02-07T00:00:01Z","provider":"openai","model":"gpt-5-mini","input_tokens":12000,"output_tokens":1800,"feature_tag":"summarize","retries":0,"status":"ok"}
|
|
2
|
+
{"ts":"2026-02-07T00:00:02Z","provider":"openai","model":"gpt-5.2","input_tokens":35000,"output_tokens":3000,"feature_tag":"coding","retries":1,"status":"ok"}
|
|
3
|
+
{"ts":"2026-02-07T00:00:03Z","provider":"anthropic","model":"claude-sonnet","input_tokens":22000,"output_tokens":2500,"feature_tag":"classify","retries":0,"status":"ok"}
|
|
4
|
+
{"ts":"2026-02-07T00:00:04Z","provider":"gemini","model":"gemini-1.5-flash","input_tokens":9000,"output_tokens":1200,"feature_tag":"translate","retries":0,"status":"ok"}
|
|
5
|
+
{"ts":"2026-02-07T00:00:05Z","provider":"openai","model":"unknown-model-x","input_tokens":8000,"output_tokens":1000,"feature_tag":"summarize","retries":2,"status":"error"}
|