@ychris12138/dsh-usage-stats 0.2.10 → 0.3.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 +97 -20
- package/SECURITY.md +3 -1
- package/docs/release-checklist.md +111 -0
- package/docs/release-notes-v0.3.0.md +25 -0
- package/docs/release-notes-v0.3.1.md +40 -0
- package/lib/accounts.js +421 -172
- package/lib/balance.js +116 -15
- package/lib/billing.js +319 -0
- package/lib/client.js +481 -86
- package/lib/export.js +227 -0
- package/lib/index.js +506 -64
- package/lib/network.js +65 -0
- package/lib/orcarouter.js +79 -0
- package/lib/pricing.js +391 -0
- package/lib/provider-identity.js +129 -0
- package/lib/usage.js +190 -13
- package/package.json +17 -5
package/lib/export.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-free export projections for the usage-stats read model.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately accepts only already-normalized usage/account
|
|
5
|
+
* snapshots and copies a fixed allow-list. Never pass configuration objects,
|
|
6
|
+
* credentials, request headers, or raw upstream responses to an export.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const EXPORT_SCHEMA_VERSION = "1.0.0";
|
|
10
|
+
|
|
11
|
+
const CSV_FORMULA_PREFIX = /^[\t\r\n ]*[=+\-@]/;
|
|
12
|
+
|
|
13
|
+
function finiteOrNull(value) {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stringOrNull(value) {
|
|
18
|
+
return typeof value === "string" ? value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function booleanOrFalse(value) {
|
|
22
|
+
return value === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tokenFields(value = {}) {
|
|
26
|
+
return {
|
|
27
|
+
inputTokens: finiteOrNull(value.inputTokens) ?? 0,
|
|
28
|
+
cacheReadTokens: finiteOrNull(value.cacheReadTokens) ?? 0,
|
|
29
|
+
cacheWriteTokens: finiteOrNull(value.cacheWriteTokens) ?? 0,
|
|
30
|
+
outputTokens: finiteOrNull(value.outputTokens) ?? 0,
|
|
31
|
+
tokens: finiteOrNull(value.tokens) ?? 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pricingProjection(value) {
|
|
36
|
+
const pricing = value !== null && typeof value === "object" ? value : {};
|
|
37
|
+
const source = pricing.source !== null && typeof pricing.source === "object"
|
|
38
|
+
? {
|
|
39
|
+
kind: stringOrNull(pricing.source.kind),
|
|
40
|
+
provider: stringOrNull(pricing.source.provider),
|
|
41
|
+
url: stringOrNull(pricing.source.url)
|
|
42
|
+
}
|
|
43
|
+
: null;
|
|
44
|
+
return {
|
|
45
|
+
ruleIds: Array.isArray(pricing.ruleIds) ? pricing.ruleIds.filter((entry) => typeof entry === "string") : [],
|
|
46
|
+
source,
|
|
47
|
+
updatedAt: stringOrNull(pricing.updatedAt)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function costFields(value = {}) {
|
|
52
|
+
const complete = booleanOrFalse(value.costComplete);
|
|
53
|
+
return {
|
|
54
|
+
estimatedCost: complete ? finiteOrNull(value.estimatedCost) : null,
|
|
55
|
+
currency: complete ? stringOrNull(value.currency) : null,
|
|
56
|
+
costComplete: complete,
|
|
57
|
+
pricing: pricingProjection(value.pricing)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function splitModelKey(value) {
|
|
62
|
+
if (typeof value !== "string") return { provider: "unknown", model: "unknown" };
|
|
63
|
+
const slash = value.indexOf("/");
|
|
64
|
+
if (slash <= 0 || slash === value.length - 1) return { provider: "unknown", model: value || "unknown" };
|
|
65
|
+
return { provider: value.slice(0, slash), model: value.slice(slash + 1) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function csvCell(value, text = false) {
|
|
69
|
+
if (value === null || value === void 0) return "\"\"";
|
|
70
|
+
let rendered = String(value);
|
|
71
|
+
if (text && CSV_FORMULA_PREFIX.test(rendered)) rendered = `'${rendered}`;
|
|
72
|
+
return `"${rendered.replaceAll('"', '""')}"`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function csv(rows) {
|
|
76
|
+
return `\uFEFF${rows.map((row) => row.map((cell) => csvCell(cell.value, cell.text)).join(",")).join("\r\n")}\r\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Daily provider/model rows, one row per rendered day/model bucket. */
|
|
80
|
+
export function dailyCsv(usage) {
|
|
81
|
+
const rows = [[
|
|
82
|
+
{ value: "date" },
|
|
83
|
+
{ value: "provider" },
|
|
84
|
+
{ value: "model" },
|
|
85
|
+
{ value: "input_tokens" },
|
|
86
|
+
{ value: "cache_read_tokens" },
|
|
87
|
+
{ value: "cache_write_tokens" },
|
|
88
|
+
{ value: "output_tokens" },
|
|
89
|
+
{ value: "total_tokens" },
|
|
90
|
+
{ value: "estimated_cost" },
|
|
91
|
+
{ value: "currency" }
|
|
92
|
+
]];
|
|
93
|
+
for (const day of Array.isArray(usage?.days) ? usage.days : []) {
|
|
94
|
+
if (day === null || typeof day !== "object") continue;
|
|
95
|
+
for (const entry of Array.isArray(day.models) ? day.models : []) {
|
|
96
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
97
|
+
const route = splitModelKey(entry.model);
|
|
98
|
+
const tokens = tokenFields(entry);
|
|
99
|
+
rows.push([
|
|
100
|
+
{ value: stringOrNull(day.date), text: true },
|
|
101
|
+
{ value: route.provider, text: true },
|
|
102
|
+
{ value: route.model, text: true },
|
|
103
|
+
{ value: tokens.inputTokens },
|
|
104
|
+
{ value: tokens.cacheReadTokens },
|
|
105
|
+
{ value: tokens.cacheWriteTokens },
|
|
106
|
+
{ value: tokens.outputTokens },
|
|
107
|
+
{ value: tokens.tokens },
|
|
108
|
+
{ value: entry.costComplete === true ? finiteOrNull(entry.estimatedCost) : null },
|
|
109
|
+
{ value: entry.costComplete === true ? stringOrNull(entry.currency) : null, text: true }
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return csv(rows);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Session rows. Provider/model sets stay in one row to avoid duplicating totals. */
|
|
117
|
+
export function sessionsCsv(usage) {
|
|
118
|
+
const rows = [[
|
|
119
|
+
{ value: "session_id" },
|
|
120
|
+
{ value: "title" },
|
|
121
|
+
{ value: "provider" },
|
|
122
|
+
{ value: "model" },
|
|
123
|
+
{ value: "tokens" },
|
|
124
|
+
{ value: "estimated_cost" },
|
|
125
|
+
{ value: "currency" },
|
|
126
|
+
{ value: "last_active" }
|
|
127
|
+
]];
|
|
128
|
+
for (const session of Array.isArray(usage?.sessions) ? usage.sessions : []) {
|
|
129
|
+
if (session === null || typeof session !== "object") continue;
|
|
130
|
+
rows.push([
|
|
131
|
+
{ value: stringOrNull(session.sessionId), text: true },
|
|
132
|
+
{ value: stringOrNull(session.title), text: true },
|
|
133
|
+
{ value: Array.isArray(session.providers) ? session.providers.filter((entry) => typeof entry === "string").join(" | ") : "", text: true },
|
|
134
|
+
{ value: Array.isArray(session.models) ? session.models.filter((entry) => typeof entry === "string").join(" | ") : "", text: true },
|
|
135
|
+
{ value: finiteOrNull(session.tokens) ?? 0 },
|
|
136
|
+
{ value: session.costComplete === true ? finiteOrNull(session.estimatedCost) : null },
|
|
137
|
+
{ value: session.costComplete === true ? stringOrNull(session.currency) : null, text: true },
|
|
138
|
+
{ value: stringOrNull(session.lastAt), text: true }
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
return csv(rows);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usageEntryProjection(value) {
|
|
145
|
+
return {
|
|
146
|
+
...tokenFields(value),
|
|
147
|
+
cacheHitRate: finiteOrNull(value?.cacheHitRate),
|
|
148
|
+
...costFields(value)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function budgetWindowProjection(value) {
|
|
153
|
+
return {
|
|
154
|
+
limit: finiteOrNull(value?.limit),
|
|
155
|
+
currency: stringOrNull(value?.currency),
|
|
156
|
+
estimatedSpend: finiteOrNull(value?.estimatedSpend),
|
|
157
|
+
percent: finiteOrNull(value?.percent),
|
|
158
|
+
costComplete: booleanOrFalse(value?.costComplete),
|
|
159
|
+
level: stringOrNull(value?.level)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function alertProjection(value) {
|
|
164
|
+
if (value === null || typeof value !== "object") return null;
|
|
165
|
+
return {
|
|
166
|
+
level: stringOrNull(value.level),
|
|
167
|
+
metric: stringOrNull(value.metric),
|
|
168
|
+
value: finiteOrNull(value.value),
|
|
169
|
+
threshold: finiteOrNull(value.threshold)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function accountProjection(value) {
|
|
174
|
+
return {
|
|
175
|
+
id: stringOrNull(value?.id),
|
|
176
|
+
displayName: stringOrNull(value?.displayName),
|
|
177
|
+
accountMode: stringOrNull(value?.accountMode),
|
|
178
|
+
adapter: stringOrNull(value?.adapter),
|
|
179
|
+
configured: booleanOrFalse(value?.configured),
|
|
180
|
+
status: stringOrNull(value?.status),
|
|
181
|
+
fetchedAt: finiteOrNull(value?.fetchedAt),
|
|
182
|
+
stale: booleanOrFalse(value?.stale),
|
|
183
|
+
lastAttemptAt: finiteOrNull(value?.lastAttemptAt),
|
|
184
|
+
lastSuccessAt: finiteOrNull(value?.lastSuccessAt),
|
|
185
|
+
ageMs: finiteOrNull(value?.ageMs),
|
|
186
|
+
provenance: stringOrNull(value?.provenance),
|
|
187
|
+
reason: stringOrNull(value?.reason),
|
|
188
|
+
alert: alertProjection(value?.alert)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Versioned full export with only explicitly approved fields. */
|
|
193
|
+
export function jsonExport(usage, accounts = [], exportedAt = Date.now()) {
|
|
194
|
+
const days = (Array.isArray(usage?.days) ? usage.days : []).filter((entry) => entry !== null && typeof entry === "object").map((day) => ({
|
|
195
|
+
date: stringOrNull(day.date),
|
|
196
|
+
...usageEntryProjection(day),
|
|
197
|
+
models: (Array.isArray(day.models) ? day.models : []).filter((entry) => entry !== null && typeof entry === "object").map((entry) => ({
|
|
198
|
+
model: stringOrNull(entry.model),
|
|
199
|
+
...usageEntryProjection(entry)
|
|
200
|
+
}))
|
|
201
|
+
}));
|
|
202
|
+
const sessions = (Array.isArray(usage?.sessions) ? usage.sessions : []).filter((entry) => entry !== null && typeof entry === "object").map((session) => ({
|
|
203
|
+
sessionId: stringOrNull(session.sessionId),
|
|
204
|
+
title: stringOrNull(session.title),
|
|
205
|
+
providers: Array.isArray(session.providers) ? session.providers.filter((entry) => typeof entry === "string") : [],
|
|
206
|
+
models: Array.isArray(session.models) ? session.models.filter((entry) => typeof entry === "string") : [],
|
|
207
|
+
...usageEntryProjection(session),
|
|
208
|
+
firstAt: stringOrNull(session.firstAt),
|
|
209
|
+
lastAt: stringOrNull(session.lastAt)
|
|
210
|
+
}));
|
|
211
|
+
return {
|
|
212
|
+
schemaVersion: EXPORT_SCHEMA_VERSION,
|
|
213
|
+
exportedAt: new Date(exportedAt).toISOString(),
|
|
214
|
+
usage: {
|
|
215
|
+
updatedAt: finiteOrNull(usage?.updatedAt),
|
|
216
|
+
total: usageEntryProjection(usage?.total),
|
|
217
|
+
days,
|
|
218
|
+
sessions,
|
|
219
|
+
budgets: {
|
|
220
|
+
currency: stringOrNull(usage?.budgets?.currency),
|
|
221
|
+
daily: budgetWindowProjection(usage?.budgets?.daily),
|
|
222
|
+
monthly: budgetWindowProjection(usage?.budgets?.monthly)
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
accounts: (Array.isArray(accounts) ? accounts : []).map(accountProjection)
|
|
226
|
+
};
|
|
227
|
+
}
|