fest-gateway 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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/cli/claude.js +326 -0
- package/dist/cli/claude.js.map +1 -0
- package/dist/cli/config.js +59 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/login.js +130 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.js +20 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/whoami.js +34 -0
- package/dist/cli/whoami.js.map +1 -0
- package/dist/server/adapters/anthropic.js +74 -0
- package/dist/server/adapters/anthropic.js.map +1 -0
- package/dist/server/adapters/fireworks.js +54 -0
- package/dist/server/adapters/fireworks.js.map +1 -0
- package/dist/server/adapters/index.js +16 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/registry.js +38 -0
- package/dist/server/adapters/registry.js.map +1 -0
- package/dist/server/adapters/rewrite.js +152 -0
- package/dist/server/adapters/rewrite.js.map +1 -0
- package/dist/server/adapters/url.js +24 -0
- package/dist/server/adapters/url.js.map +1 -0
- package/dist/server/api/auth.js +213 -0
- package/dist/server/api/auth.js.map +1 -0
- package/dist/server/api/live.js +108 -0
- package/dist/server/api/live.js.map +1 -0
- package/dist/server/api/models.js +129 -0
- package/dist/server/api/models.js.map +1 -0
- package/dist/server/api/oauth.js +300 -0
- package/dist/server/api/oauth.js.map +1 -0
- package/dist/server/api/routes.js +164 -0
- package/dist/server/api/routes.js.map +1 -0
- package/dist/server/api/routing-view.js +47 -0
- package/dist/server/api/routing-view.js.map +1 -0
- package/dist/server/auth/accounts.js +111 -0
- package/dist/server/auth/accounts.js.map +1 -0
- package/dist/server/auth/gateway-401.js +85 -0
- package/dist/server/auth/gateway-401.js.map +1 -0
- package/dist/server/auth/guard.js +74 -0
- package/dist/server/auth/guard.js.map +1 -0
- package/dist/server/auth/oauth.js +43 -0
- package/dist/server/auth/oauth.js.map +1 -0
- package/dist/server/auth/password.js +104 -0
- package/dist/server/auth/password.js.map +1 -0
- package/dist/server/auth/posture.js +145 -0
- package/dist/server/auth/posture.js.map +1 -0
- package/dist/server/auth/session.js +161 -0
- package/dist/server/auth/session.js.map +1 -0
- package/dist/server/bin/fest.js +589 -0
- package/dist/server/bin/fest.js.map +1 -0
- package/dist/server/config.js +109 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/credentials/provider.js +70 -0
- package/dist/server/credentials/provider.js.map +1 -0
- package/dist/server/credentials/resolve.js +89 -0
- package/dist/server/credentials/resolve.js.map +1 -0
- package/dist/server/http/body.js +66 -0
- package/dist/server/http/body.js.map +1 -0
- package/dist/server/http/errors.js +144 -0
- package/dist/server/http/errors.js.map +1 -0
- package/dist/server/http/headers.js +189 -0
- package/dist/server/http/headers.js.map +1 -0
- package/dist/server/http/pipe.js +97 -0
- package/dist/server/http/pipe.js.map +1 -0
- package/dist/server/http/server.js +226 -0
- package/dist/server/http/server.js.map +1 -0
- package/dist/server/http/sse.js +167 -0
- package/dist/server/http/sse.js.map +1 -0
- package/dist/server/http/static.js +138 -0
- package/dist/server/http/static.js.map +1 -0
- package/dist/server/ingest/live-bus.js +67 -0
- package/dist/server/ingest/live-bus.js.map +1 -0
- package/dist/server/ingest/sink.js +141 -0
- package/dist/server/ingest/sink.js.map +1 -0
- package/dist/server/log.js +31 -0
- package/dist/server/log.js.map +1 -0
- package/dist/server/pipeline/count-tokens.js +48 -0
- package/dist/server/pipeline/count-tokens.js.map +1 -0
- package/dist/server/pipeline/dispatch.js +138 -0
- package/dist/server/pipeline/dispatch.js.map +1 -0
- package/dist/server/pipeline/passthrough.js +392 -0
- package/dist/server/pipeline/passthrough.js.map +1 -0
- package/dist/server/pipeline/preflight.js +66 -0
- package/dist/server/pipeline/preflight.js.map +1 -0
- package/dist/server/pipeline/record.js +49 -0
- package/dist/server/pipeline/record.js.map +1 -0
- package/dist/server/pipeline/substitute.js +287 -0
- package/dist/server/pipeline/substitute.js.map +1 -0
- package/dist/server/routes/resolve.js +176 -0
- package/dist/server/routes/resolve.js.map +1 -0
- package/dist/server/routes/table.js +247 -0
- package/dist/server/routes/table.js.map +1 -0
- package/dist/server/routes/watch.js +101 -0
- package/dist/server/routes/watch.js.map +1 -0
- package/dist/server/secret/fingerprint.js +107 -0
- package/dist/server/secret/fingerprint.js.map +1 -0
- package/dist/server/secret/non-persistable.js +69 -0
- package/dist/server/secret/non-persistable.js.map +1 -0
- package/dist/server/store/audit.js +53 -0
- package/dist/server/store/audit.js.map +1 -0
- package/dist/server/store/bootstrap.js +53 -0
- package/dist/server/store/bootstrap.js.map +1 -0
- package/dist/server/store/db.js +157 -0
- package/dist/server/store/db.js.map +1 -0
- package/dist/server/store/ids.js +14 -0
- package/dist/server/store/ids.js.map +1 -0
- package/dist/server/store/migrations/001-init.sql +213 -0
- package/dist/server/store/migrations/002-feed-and-error-indexes.sql +26 -0
- package/dist/server/store/migrations/003-routing.sql +42 -0
- package/dist/server/store/migrations/004-admin-auth.sql +72 -0
- package/dist/server/store/migrations/005-notional-cost.sql +28 -0
- package/dist/server/store/queries.js +904 -0
- package/dist/server/store/queries.js.map +1 -0
- package/dist/server/store/retention.js +79 -0
- package/dist/server/store/retention.js.map +1 -0
- package/dist/server/store/seed.js +203 -0
- package/dist/server/store/seed.js.map +1 -0
- package/dist/server/store/tokens.js +133 -0
- package/dist/server/store/tokens.js.map +1 -0
- package/dist/server/store/write.js +330 -0
- package/dist/server/store/write.js.map +1 -0
- package/dist/server/usage/accumulator.js +209 -0
- package/dist/server/usage/accumulator.js.map +1 -0
- package/dist/server/usage/cost.js +57 -0
- package/dist/server/usage/cost.js.map +1 -0
- package/dist/server/usage/prices/catalog.js +136 -0
- package/dist/server/usage/prices/catalog.js.map +1 -0
- package/dist/server/usage/prices/refresh.js +125 -0
- package/dist/server/usage/prices/refresh.js.map +1 -0
- package/dist/server/usage/prices/snapshot.json.gz +0 -0
- package/dist/server/usage/prices/table.js +102 -0
- package/dist/server/usage/prices/table.js.map +1 -0
- package/dist/server/usage/pricing.js +103 -0
- package/dist/server/usage/pricing.js.map +1 -0
- package/dist/shared/api.js +19 -0
- package/dist/shared/api.js.map +1 -0
- package/dist/shared/base-models.js +28 -0
- package/dist/shared/base-models.js.map +1 -0
- package/dist/shared/demotion-vars.js +17 -0
- package/dist/shared/demotion-vars.js.map +1 -0
- package/dist/shared/series.js +50 -0
- package/dist/shared/series.js.map +1 -0
- package/dist/shared/types.js +40 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/web/dist/assets/index-8HlKsJqt.css +2 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js +56 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js.map +1 -0
- package/dist/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist/web/dist/index.html +44 -0
- package/dist/web/dist/nearform-icon.svg +4 -0
- package/package.json +82 -0
- package/routes.example.json +22 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The metering write path.
|
|
3
|
+
*
|
|
4
|
+
* Called from the flush side of the usage queue, never from a proxied request
|
|
5
|
+
* (see the constraint comment in db.ts — `DatabaseSync` blocks the event loop).
|
|
6
|
+
* One batch is one transaction, and inside it each record writes both its raw
|
|
7
|
+
* `requests` row and its `usage_hourly` rollups, so the rollups can never
|
|
8
|
+
* disagree with the rows they summarise.
|
|
9
|
+
*
|
|
10
|
+
* Two principles run through this file:
|
|
11
|
+
*
|
|
12
|
+
* 1. METERING MUST DEGRADE, NEVER TAKE DOWN THE PROXY. A malformed record is
|
|
13
|
+
* logged and skipped; it never fails its batch and never escapes as an
|
|
14
|
+
* exception into the caller. Losing one row of observability is a nuisance;
|
|
15
|
+
* killing the gateway that developers are actively coding against is an
|
|
16
|
+
* outage.
|
|
17
|
+
* 2. COST IS REPORTED HONESTLY. `costUsd === null` means "no dollar figure
|
|
18
|
+
* applies", not zero, and the rollup keeps three separate facts so that
|
|
19
|
+
* distinction survives aggregation.
|
|
20
|
+
*/
|
|
21
|
+
import { isErrorStatus } from "../../shared/types.js";
|
|
22
|
+
import { log } from "../log.js";
|
|
23
|
+
const HOUR_MS = 3_600_000;
|
|
24
|
+
/** Epoch ms truncated to the start of its UTC hour. */
|
|
25
|
+
export function hourStart(epochMs) {
|
|
26
|
+
return Math.floor(epochMs / HOUR_MS) * HOUR_MS;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fixed latency histogram buckets: <1s, <3s, <10s, <30s, <60s, >=60s.
|
|
30
|
+
*
|
|
31
|
+
* Buckets rather than percentiles because bucket counts ADD across rollup rows
|
|
32
|
+
* and percentiles do not: p95-of-p95s is not a number that means anything.
|
|
33
|
+
*/
|
|
34
|
+
export function latencyBucket(durationMs) {
|
|
35
|
+
if (durationMs < 1_000)
|
|
36
|
+
return 0;
|
|
37
|
+
if (durationMs < 3_000)
|
|
38
|
+
return 1;
|
|
39
|
+
if (durationMs < 10_000)
|
|
40
|
+
return 2;
|
|
41
|
+
if (durationMs < 30_000)
|
|
42
|
+
return 3;
|
|
43
|
+
if (durationMs < 60_000)
|
|
44
|
+
return 4;
|
|
45
|
+
return 5;
|
|
46
|
+
}
|
|
47
|
+
/** '' is the "all" bucket sentinel for a rollup dimension (see 001-init.sql). */
|
|
48
|
+
const ALL = "";
|
|
49
|
+
const INSERT_REQUEST = `
|
|
50
|
+
INSERT INTO requests (
|
|
51
|
+
id, org_id, user_id, token_id,
|
|
52
|
+
started_at, ended_at,
|
|
53
|
+
posture, identity_carrier, caller_fingerprint, credential_fingerprint, credential_origin,
|
|
54
|
+
session_id,
|
|
55
|
+
requested_model, served_model, upstream,
|
|
56
|
+
stream, status, http_status, error_type, error_message, partial,
|
|
57
|
+
input_tokens, cache_read_tokens, cache_write_5m_tokens, cache_write_1h_tokens,
|
|
58
|
+
output_tokens, web_searches, service_tier,
|
|
59
|
+
cost_usd, cost_basis, notional_cost_usd,
|
|
60
|
+
ttfb_ms, duration_ms, bytes_in, bytes_out,
|
|
61
|
+
upstream_request_id,
|
|
62
|
+
rl_status, rl_5h_utilization, rl_5h_status, rl_5h_reset_at,
|
|
63
|
+
rl_7d_utilization, rl_7d_status, rl_7d_reset_at,
|
|
64
|
+
rl_claim, rl_overage_status, rl_overage_reason,
|
|
65
|
+
client_version,
|
|
66
|
+
pipeline, route_id, credentials_considered
|
|
67
|
+
) VALUES (
|
|
68
|
+
:id, :org_id, :user_id, :token_id,
|
|
69
|
+
:started_at, :ended_at,
|
|
70
|
+
:posture, :identity_carrier, :caller_fingerprint, :credential_fingerprint, :credential_origin,
|
|
71
|
+
:session_id,
|
|
72
|
+
:requested_model, :served_model, :upstream,
|
|
73
|
+
:stream, :status, :http_status, :error_type, :error_message, :partial,
|
|
74
|
+
:input_tokens, :cache_read_tokens, :cache_write_5m_tokens, :cache_write_1h_tokens,
|
|
75
|
+
:output_tokens, :web_searches, :service_tier,
|
|
76
|
+
:cost_usd, :cost_basis, :notional_cost_usd,
|
|
77
|
+
:ttfb_ms, :duration_ms, :bytes_in, :bytes_out,
|
|
78
|
+
:upstream_request_id,
|
|
79
|
+
:rl_status, :rl_5h_utilization, :rl_5h_status, :rl_5h_reset_at,
|
|
80
|
+
:rl_7d_utilization, :rl_7d_status, :rl_7d_reset_at,
|
|
81
|
+
:rl_claim, :rl_overage_status, :rl_overage_reason,
|
|
82
|
+
:client_version,
|
|
83
|
+
:pipeline, :route_id, :credentials_considered
|
|
84
|
+
)`;
|
|
85
|
+
const ADDITIVE_COLUMNS = [
|
|
86
|
+
"requests",
|
|
87
|
+
"errors",
|
|
88
|
+
"input_tokens",
|
|
89
|
+
"cache_read_tokens",
|
|
90
|
+
"cache_write_5m_tokens",
|
|
91
|
+
"cache_write_1h_tokens",
|
|
92
|
+
"output_tokens",
|
|
93
|
+
"web_searches",
|
|
94
|
+
"cost_usd",
|
|
95
|
+
"unpriced_requests",
|
|
96
|
+
"subscription_requests",
|
|
97
|
+
"notional_cost_usd",
|
|
98
|
+
"notional_unpriced_requests",
|
|
99
|
+
"duration_ms_sum",
|
|
100
|
+
"ttfb_ms_sum",
|
|
101
|
+
"ttfb_count",
|
|
102
|
+
"lat_b0",
|
|
103
|
+
"lat_b1",
|
|
104
|
+
"lat_b2",
|
|
105
|
+
"lat_b3",
|
|
106
|
+
"lat_b4",
|
|
107
|
+
"lat_b5",
|
|
108
|
+
];
|
|
109
|
+
const KEY_COLUMNS = [
|
|
110
|
+
"org_id",
|
|
111
|
+
"hour_start",
|
|
112
|
+
"user_id",
|
|
113
|
+
"served_model",
|
|
114
|
+
"credential_origin",
|
|
115
|
+
"cost_basis",
|
|
116
|
+
];
|
|
117
|
+
const ROLLUP_COLUMNS = [...KEY_COLUMNS, ...ADDITIVE_COLUMNS, "duration_ms_max"];
|
|
118
|
+
const UPSERT_ROLLUP = `
|
|
119
|
+
INSERT INTO usage_hourly (${ROLLUP_COLUMNS.join(", ")})
|
|
120
|
+
VALUES (${ROLLUP_COLUMNS.map((c) => ":" + c).join(", ")})
|
|
121
|
+
ON CONFLICT(${KEY_COLUMNS.join(", ")}) DO UPDATE SET
|
|
122
|
+
${ADDITIVE_COLUMNS.map((c) => `${c} = ${c} + excluded.${c}`).join(",\n ")},
|
|
123
|
+
duration_ms_max = MAX(duration_ms_max, excluded.duration_ms_max)`;
|
|
124
|
+
/** Booleans are stored as INTEGER 0/1 per the schema's convention. */
|
|
125
|
+
function bit(value) {
|
|
126
|
+
return value ? 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
/** `undefined` and missing values collapse to SQL NULL; 0 and '' are preserved. */
|
|
129
|
+
function nullable(value) {
|
|
130
|
+
return value === undefined ? null : value;
|
|
131
|
+
}
|
|
132
|
+
function requestParams(orgId, record) {
|
|
133
|
+
const usage = record.usage;
|
|
134
|
+
const rl = record.rateLimit;
|
|
135
|
+
const attributed = record;
|
|
136
|
+
// Read every field eagerly so a malformed record (missing `usage`, say)
|
|
137
|
+
// throws HERE, before any statement has run, and the skip is clean.
|
|
138
|
+
return {
|
|
139
|
+
id: record.id,
|
|
140
|
+
org_id: orgId,
|
|
141
|
+
user_id: nullable(attributed.userId),
|
|
142
|
+
token_id: nullable(attributed.tokenId),
|
|
143
|
+
started_at: record.startedAt,
|
|
144
|
+
ended_at: record.endedAt,
|
|
145
|
+
posture: record.posture,
|
|
146
|
+
identity_carrier: record.identityCarrier,
|
|
147
|
+
caller_fingerprint: nullable(record.callerFingerprint),
|
|
148
|
+
credential_fingerprint: nullable(record.credentialFingerprint),
|
|
149
|
+
credential_origin: record.credentialOrigin,
|
|
150
|
+
session_id: nullable(record.sessionId),
|
|
151
|
+
requested_model: nullable(record.requestedModel),
|
|
152
|
+
served_model: nullable(record.servedModel),
|
|
153
|
+
upstream: record.upstream,
|
|
154
|
+
stream: bit(record.stream),
|
|
155
|
+
status: record.status,
|
|
156
|
+
http_status: nullable(record.httpStatus),
|
|
157
|
+
error_type: nullable(record.errorType),
|
|
158
|
+
error_message: nullable(record.errorMessage),
|
|
159
|
+
partial: bit(record.partial),
|
|
160
|
+
input_tokens: usage.inputTokens,
|
|
161
|
+
cache_read_tokens: usage.cacheReadTokens,
|
|
162
|
+
cache_write_5m_tokens: usage.cacheWrite5mTokens,
|
|
163
|
+
cache_write_1h_tokens: usage.cacheWrite1hTokens,
|
|
164
|
+
output_tokens: usage.outputTokens,
|
|
165
|
+
web_searches: usage.webSearches,
|
|
166
|
+
service_tier: nullable(usage.serviceTier),
|
|
167
|
+
cost_usd: nullable(record.costUsd),
|
|
168
|
+
cost_basis: record.costBasis,
|
|
169
|
+
notional_cost_usd: nullable(record.notionalCostUsd),
|
|
170
|
+
ttfb_ms: nullable(record.ttfbMs),
|
|
171
|
+
duration_ms: record.durationMs,
|
|
172
|
+
bytes_in: record.bytesIn,
|
|
173
|
+
bytes_out: record.bytesOut,
|
|
174
|
+
upstream_request_id: nullable(record.upstreamRequestId),
|
|
175
|
+
rl_status: nullable(rl?.status),
|
|
176
|
+
rl_5h_utilization: nullable(rl?.fiveHourUtilization),
|
|
177
|
+
rl_5h_status: nullable(rl?.fiveHourStatus),
|
|
178
|
+
rl_5h_reset_at: nullable(rl?.fiveHourResetAt),
|
|
179
|
+
rl_7d_utilization: nullable(rl?.sevenDayUtilization),
|
|
180
|
+
rl_7d_status: nullable(rl?.sevenDayStatus),
|
|
181
|
+
rl_7d_reset_at: nullable(rl?.sevenDayResetAt),
|
|
182
|
+
rl_claim: nullable(rl?.representativeClaim),
|
|
183
|
+
rl_overage_status: nullable(rl?.overageStatus),
|
|
184
|
+
rl_overage_reason: nullable(rl?.overageDisabledReason),
|
|
185
|
+
client_version: nullable(record.clientVersion),
|
|
186
|
+
pipeline: record.pipeline,
|
|
187
|
+
route_id: nullable(record.routeId),
|
|
188
|
+
// Serialised here rather than in the request path: the array is small, but
|
|
189
|
+
// pricing and persistence both belong in the flush, and this is both.
|
|
190
|
+
credentials_considered: JSON.stringify(record.credentialsConsidered ?? []),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function rollupParams(orgId, record) {
|
|
194
|
+
const usage = record.usage;
|
|
195
|
+
const attributed = record;
|
|
196
|
+
const userId = attributed.userId ?? ALL;
|
|
197
|
+
// served_model is NOT NULL in the rollup, so an unresolved model lands in the
|
|
198
|
+
// same '' bucket as "all models". That is a deliberate collapse, and it is
|
|
199
|
+
// why the four keys below are de-duplicated before writing.
|
|
200
|
+
const servedModel = record.servedModel ?? ALL;
|
|
201
|
+
// ── Cost honesty ──────────────────────────────────────────────────────────
|
|
202
|
+
// Three mutually exclusive facts, because one number cannot carry them:
|
|
203
|
+
// cost_usd dollars the org actually owes, priced rows only
|
|
204
|
+
// subscription_requests real usage the developer's own plan absorbed
|
|
205
|
+
// unpriced_requests usage we could not price (unknown model)
|
|
206
|
+
// Collapsing either counter into cost_usd as 0 would make a dashboard read
|
|
207
|
+
// "$0.00" for a month of heavy subscription traffic, which is a lie of the
|
|
208
|
+
// most convincing kind. SUM() over a nullable cost column skips NULLs
|
|
209
|
+
// silently — that is exactly the understatement this split prevents.
|
|
210
|
+
const priced = typeof record.costUsd === "number" && Number.isFinite(record.costUsd);
|
|
211
|
+
const isSubscription = record.costBasis === "subscription";
|
|
212
|
+
const costUsd = priced ? record.costUsd : 0;
|
|
213
|
+
const subscriptionRequests = isSubscription ? 1 : 0;
|
|
214
|
+
// Keyed off `!priced` rather than `costUsd === null` so a non-finite cost
|
|
215
|
+
// (NaN/Infinity from a bad rate table) is still counted as unpriced. With the
|
|
216
|
+
// narrower `=== null` test such a record would be excluded from the dollar
|
|
217
|
+
// sum AND from the n/a count — vanishing from the books entirely, which is
|
|
218
|
+
// precisely the silent understatement this split exists to prevent.
|
|
219
|
+
const unpricedRequests = !priced && !isSubscription ? 1 : 0;
|
|
220
|
+
// Notional value runs on its own books, and the asymmetry is the whole point:
|
|
221
|
+
// subscription rows are EXCLUDED from cost_usd and INCLUDED here. A model
|
|
222
|
+
// with no published rate is unknown on both, so it gets the same
|
|
223
|
+
// lower-bound counter treatment, keyed off `!notionalPriced` for the same
|
|
224
|
+
// NaN reason as above.
|
|
225
|
+
const notionalPriced = typeof record.notionalCostUsd === "number" && Number.isFinite(record.notionalCostUsd);
|
|
226
|
+
const notionalCostUsd = notionalPriced ? (record.notionalCostUsd ?? 0) : 0;
|
|
227
|
+
const notionalUnpricedRequests = notionalPriced ? 0 : 1;
|
|
228
|
+
const bucket = latencyBucket(record.durationMs);
|
|
229
|
+
// ttfb is only summed when it exists, so `ttfb_ms_sum / ttfb_count` is an
|
|
230
|
+
// average over requests that actually produced a first byte rather than one
|
|
231
|
+
// diluted by aborts and non-streaming calls.
|
|
232
|
+
const hasTtfb = typeof record.ttfbMs === "number";
|
|
233
|
+
const shared = {
|
|
234
|
+
org_id: orgId,
|
|
235
|
+
hour_start: hourStart(record.startedAt),
|
|
236
|
+
credential_origin: record.credentialOrigin,
|
|
237
|
+
cost_basis: record.costBasis,
|
|
238
|
+
requests: 1,
|
|
239
|
+
// Shared with the raw-range predicate in `queries.ts`; see NON_ERROR_STATUSES.
|
|
240
|
+
errors: isErrorStatus(record.status) ? 1 : 0,
|
|
241
|
+
input_tokens: usage.inputTokens,
|
|
242
|
+
cache_read_tokens: usage.cacheReadTokens,
|
|
243
|
+
cache_write_5m_tokens: usage.cacheWrite5mTokens,
|
|
244
|
+
cache_write_1h_tokens: usage.cacheWrite1hTokens,
|
|
245
|
+
output_tokens: usage.outputTokens,
|
|
246
|
+
web_searches: usage.webSearches,
|
|
247
|
+
cost_usd: costUsd ?? 0,
|
|
248
|
+
unpriced_requests: unpricedRequests,
|
|
249
|
+
subscription_requests: subscriptionRequests,
|
|
250
|
+
notional_cost_usd: notionalCostUsd,
|
|
251
|
+
notional_unpriced_requests: notionalUnpricedRequests,
|
|
252
|
+
duration_ms_sum: record.durationMs,
|
|
253
|
+
duration_ms_max: record.durationMs,
|
|
254
|
+
ttfb_ms_sum: hasTtfb ? (record.ttfbMs ?? 0) : 0,
|
|
255
|
+
ttfb_count: hasTtfb ? 1 : 0,
|
|
256
|
+
lat_b0: bucket === 0 ? 1 : 0,
|
|
257
|
+
lat_b1: bucket === 1 ? 1 : 0,
|
|
258
|
+
lat_b2: bucket === 2 ? 1 : 0,
|
|
259
|
+
lat_b3: bucket === 3 ? 1 : 0,
|
|
260
|
+
lat_b4: bucket === 4 ? 1 : 0,
|
|
261
|
+
lat_b5: bucket === 5 ? 1 : 0,
|
|
262
|
+
};
|
|
263
|
+
// Four buckets per record, so every dashboard query is a point lookup or a
|
|
264
|
+
// narrow range scan instead of a GROUP BY over the raw hot table:
|
|
265
|
+
// (user, model) the specific cell
|
|
266
|
+
// (user, '') one developer, all models
|
|
267
|
+
// ('', model) all developers, one model
|
|
268
|
+
// ('', '') org totals
|
|
269
|
+
const keys = [
|
|
270
|
+
[userId, servedModel],
|
|
271
|
+
[userId, ALL],
|
|
272
|
+
[ALL, servedModel],
|
|
273
|
+
[ALL, ALL],
|
|
274
|
+
];
|
|
275
|
+
// De-duplicate: when the record is unattributed (userId '') or its model is
|
|
276
|
+
// unknown (servedModel ''), two or more of the four keys are the SAME primary
|
|
277
|
+
// key, and writing both would double-count that record via the upsert's
|
|
278
|
+
// `col = col + excluded.col`. Do not remove this.
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
const rows = [];
|
|
281
|
+
for (const [u, m] of keys) {
|
|
282
|
+
const key = `${u}\u0000${m}`;
|
|
283
|
+
if (seen.has(key))
|
|
284
|
+
continue;
|
|
285
|
+
seen.add(key);
|
|
286
|
+
rows.push({ ...shared, user_id: u, served_model: m });
|
|
287
|
+
}
|
|
288
|
+
return rows;
|
|
289
|
+
}
|
|
290
|
+
export function createRequestWriter(store) {
|
|
291
|
+
// Prepared ONCE and reused for the process lifetime. Re-preparing per row is
|
|
292
|
+
// the difference between roughly 1ms and 50ms for a 200-row batch: parsing
|
|
293
|
+
// and planning a 46-column INSERT dominates actually executing it.
|
|
294
|
+
const insertRequest = store.db.prepare(INSERT_REQUEST);
|
|
295
|
+
const upsertRollup = store.db.prepare(UPSERT_ROLLUP);
|
|
296
|
+
insertRequest.setAllowBareNamedParameters(true);
|
|
297
|
+
upsertRollup.setAllowBareNamedParameters(true);
|
|
298
|
+
return {
|
|
299
|
+
writeBatch(orgId, records) {
|
|
300
|
+
if (records.length === 0)
|
|
301
|
+
return 0;
|
|
302
|
+
let written = 0;
|
|
303
|
+
store.transaction(() => {
|
|
304
|
+
for (const record of records) {
|
|
305
|
+
try {
|
|
306
|
+
// Build all parameters for this record BEFORE executing anything.
|
|
307
|
+
// A malformed record then fails with nothing written, which keeps
|
|
308
|
+
// the row/rollup invariant without needing a SAVEPOINT per row.
|
|
309
|
+
const reqParams = requestParams(orgId, record);
|
|
310
|
+
const rollups = rollupParams(orgId, record);
|
|
311
|
+
insertRequest.run(reqParams);
|
|
312
|
+
for (const rollup of rollups)
|
|
313
|
+
upsertRollup.run(rollup);
|
|
314
|
+
written += 1;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
// Per-record, inside the transaction: one bad record must not cost
|
|
318
|
+
// us the other 199. The batch still commits.
|
|
319
|
+
log.warn("usage record skipped", {
|
|
320
|
+
id: typeof record?.id === "string" ? record.id : "<unknown>",
|
|
321
|
+
err: String(err),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
return written;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../../server/store/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtD,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AA2BhC,MAAM,OAAO,GAAG,SAAS,CAAC;AAE1B,uDAAuD;AACvD,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,IAAI,UAAU,GAAG,KAAK;QAAE,OAAO,CAAC,CAAC;IACjC,IAAI,UAAU,GAAG,KAAK;QAAE,OAAO,CAAC,CAAC;IACjC,IAAI,UAAU,GAAG,MAAM;QAAE,OAAO,CAAC,CAAC;IAClC,IAAI,UAAU,GAAG,MAAM;QAAE,OAAO,CAAC,CAAC;IAClC,IAAI,UAAU,GAAG,MAAM;QAAE,OAAO,CAAC,CAAC;IAClC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,iFAAiF;AACjF,MAAM,GAAG,GAAG,EAAE,CAAC;AAEf,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCrB,CAAC;AAEH,MAAM,gBAAgB,GAAG;IACvB,UAAU;IACV,QAAQ;IACR,cAAc;IACd,mBAAmB;IACnB,uBAAuB;IACvB,uBAAuB;IACvB,eAAe;IACf,cAAc;IACd,UAAU;IACV,mBAAmB;IACnB,uBAAuB;IACvB,mBAAmB;IACnB,4BAA4B;IAC5B,iBAAiB;IACjB,aAAa;IACb,YAAY;IACZ,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;CACA,CAAC;AAEX,MAAM,WAAW,GAAG;IAClB,QAAQ;IACR,YAAY;IACZ,SAAS;IACT,cAAc;IACd,mBAAmB;IACnB,YAAY;CACJ,CAAC;AAEX,MAAM,cAAc,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;AAEhF,MAAM,aAAa,GAAG;4BACM,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;UAC3C,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;cACzC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;IAChC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;mEACT,CAAC;AAEpE,sEAAsE;AACtE,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED,mFAAmF;AACnF,SAAS,QAAQ,CAAI,KAA2B;IAC9C,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5C,CAAC;AAID,SAAS,aAAa,CAAC,KAAa,EAAE,MAAmB;IACvD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC;IAC5B,MAAM,UAAU,GAAG,MAAwC,CAAC;IAE5D,wEAAwE;IACxE,oEAAoE;IACpE,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC;QACpC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;QACtC,UAAU,EAAE,MAAM,CAAC,SAAS;QAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,gBAAgB,EAAE,MAAM,CAAC,eAAe;QACxC,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACtD,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,qBAAqB,CAAC;QAC9D,iBAAiB,EAAE,MAAM,CAAC,gBAAgB;QAC1C,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;QACtC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC;QAChD,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC;QAC1C,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC;QACxC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;QACtC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;QAC5C,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;QAC5B,YAAY,EAAE,KAAK,CAAC,WAAW;QAC/B,iBAAiB,EAAE,KAAK,CAAC,eAAe;QACxC,qBAAqB,EAAE,KAAK,CAAC,kBAAkB;QAC/C,qBAAqB,EAAE,KAAK,CAAC,kBAAkB;QAC/C,aAAa,EAAE,KAAK,CAAC,YAAY;QACjC,YAAY,EAAE,KAAK,CAAC,WAAW;QAC/B,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC;QACzC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;QAClC,UAAU,EAAE,MAAM,CAAC,SAAS;QAC5B,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC;QACnD,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAChC,WAAW,EAAE,MAAM,CAAC,UAAU;QAC9B,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACvD,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAC/B,iBAAiB,EAAE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;QACpD,YAAY,EAAE,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;QAC1C,cAAc,EAAE,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;QAC7C,iBAAiB,EAAE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;QACpD,YAAY,EAAE,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;QAC1C,cAAc,EAAE,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;QAC7C,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;QAC3C,iBAAiB,EAAE,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;QAC9C,iBAAiB,EAAE,QAAQ,CAAC,EAAE,EAAE,qBAAqB,CAAC;QACtD,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC;QAC9C,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC;QAClC,2EAA2E;QAC3E,sEAAsE;QACtE,sBAAsB,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,qBAAqB,IAAI,EAAE,CAAC;KAC3E,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,MAAmB;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,MAAM,UAAU,GAAG,MAAwC,CAAC;IAC5D,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,IAAI,GAAG,CAAC;IACxC,8EAA8E;IAC9E,2EAA2E;IAC3E,4DAA4D;IAC5D,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,GAAG,CAAC;IAE9C,6EAA6E;IAC7E,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,qEAAqE;IACrE,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrF,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,KAAK,cAAc,CAAC;IAC3D,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,oBAAoB,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,0EAA0E;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,gBAAgB,GAAG,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5D,8EAA8E;IAC9E,0EAA0E;IAC1E,iEAAiE;IACjE,0EAA0E;IAC1E,uBAAuB;IACvB,MAAM,cAAc,GAClB,OAAO,MAAM,CAAC,eAAe,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACxF,MAAM,eAAe,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,wBAAwB,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAExD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAChD,0EAA0E;IAC1E,4EAA4E;IAC5E,6CAA6C;IAC7C,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC;IAElD,MAAM,MAAM,GAAW;QACrB,MAAM,EAAE,KAAK;QACb,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC;QACvC,iBAAiB,EAAE,MAAM,CAAC,gBAAgB;QAC1C,UAAU,EAAE,MAAM,CAAC,SAAS;QAC5B,QAAQ,EAAE,CAAC;QACX,+EAA+E;QAC/E,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,YAAY,EAAE,KAAK,CAAC,WAAW;QAC/B,iBAAiB,EAAE,KAAK,CAAC,eAAe;QACxC,qBAAqB,EAAE,KAAK,CAAC,kBAAkB;QAC/C,qBAAqB,EAAE,KAAK,CAAC,kBAAkB;QAC/C,aAAa,EAAE,KAAK,CAAC,YAAY;QACjC,YAAY,EAAE,KAAK,CAAC,WAAW;QAC/B,QAAQ,EAAE,OAAO,IAAI,CAAC;QACtB,iBAAiB,EAAE,gBAAgB;QACnC,qBAAqB,EAAE,oBAAoB;QAC3C,iBAAiB,EAAE,eAAe;QAClC,0BAA0B,EAAE,wBAAwB;QACpD,eAAe,EAAE,MAAM,CAAC,UAAU;QAClC,eAAe,EAAE,MAAM,CAAC,UAAU;QAClC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC7B,CAAC;IAEF,2EAA2E;IAC3E,kEAAkE;IAClE,qCAAqC;IACrC,6CAA6C;IAC7C,6CAA6C;IAC7C,8BAA8B;IAC9B,MAAM,IAAI,GAA6C;QACrD,CAAC,MAAM,EAAE,WAAW,CAAC;QACrB,CAAC,MAAM,EAAE,GAAG,CAAC;QACb,CAAC,GAAG,EAAE,WAAW,CAAC;QAClB,CAAC,GAAG,EAAE,GAAG,CAAC;KACX,CAAC;IAEF,4EAA4E;IAC5E,8EAA8E;IAC9E,wEAAwE;IACxE,kDAAkD;IAClD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAY;IAC9C,6EAA6E;IAC7E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,aAAa,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,aAAa,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC;IAChD,YAAY,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC;IAE/C,OAAO;QACL,UAAU,CAAC,KAAa,EAAE,OAA+B;YACvD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,CAAC,CAAC;YAEnC,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;gBACrB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;oBAC7B,IAAI,CAAC;wBACH,kEAAkE;wBAClE,kEAAkE;wBAClE,gEAAgE;wBAChE,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;wBAC/C,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;wBAE5C,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;wBAC7B,KAAK,MAAM,MAAM,IAAI,OAAO;4BAAE,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;wBACvD,OAAO,IAAI,CAAC,CAAC;oBACf,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,mEAAmE;wBACnE,6CAA6C;wBAC7C,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE;4BAC/B,EAAE,EAAE,OAAO,MAAM,EAAE,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW;4BAC5D,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;yBACjB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folds Anthropic SSE events into a single UsagePayload.
|
|
3
|
+
*
|
|
4
|
+
* The whole point of this module is one rule, so it is stated up front:
|
|
5
|
+
*
|
|
6
|
+
* USAGE FIELDS ARE LAST-WINS. THEY ARE NEVER SUMMED.
|
|
7
|
+
*
|
|
8
|
+
* `message_delta.usage.output_tokens` is the FINAL CUMULATIVE count for the
|
|
9
|
+
* message, re-sent (and revised upward) on each delta. Adding deltas together
|
|
10
|
+
* over-counts output by roughly the number of deltas — the classic 10x
|
|
11
|
+
* over-billing bug. If you are here because totals look low, the fix is
|
|
12
|
+
* somewhere else; do not turn these assignments into `+=`.
|
|
13
|
+
*
|
|
14
|
+
* Secondly, this code runs on a proxied request that has already been answered.
|
|
15
|
+
* `apply()` must never throw, whatever the upstream sends: malformed JSON, a
|
|
16
|
+
* JSON `null`, a string where a number belongs, NaN, Infinity, negatives.
|
|
17
|
+
* Anything unusable is treated as *absent*, which leaves the previous value in
|
|
18
|
+
* place.
|
|
19
|
+
*/
|
|
20
|
+
import { EMPTY_USAGE } from "../../shared/types.js";
|
|
21
|
+
/**
|
|
22
|
+
* Accept only a real, finite, non-negative number.
|
|
23
|
+
*
|
|
24
|
+
* Numeric strings are rejected rather than coerced on purpose: a `"1234"` from
|
|
25
|
+
* Anthropic would mean the wire format changed, and silently accepting it would
|
|
26
|
+
* hide that. `Number("")` is 0 and `Number("abc")` is NaN, so string coercion
|
|
27
|
+
* also turns junk into plausible-looking zeroes.
|
|
28
|
+
*/
|
|
29
|
+
function num(value) {
|
|
30
|
+
if (typeof value !== "number")
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!Number.isFinite(value))
|
|
33
|
+
return undefined; // NaN, Infinity, -Infinity
|
|
34
|
+
if (value < 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function asRecord(value) {
|
|
39
|
+
// typeof null === "object", and arrays are objects too. Neither is a usage
|
|
40
|
+
// block, and treating them as one yields undefined lookups rather than throws.
|
|
41
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
42
|
+
return undefined;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Merge one `usage` object into a draft, last-wins per field.
|
|
47
|
+
* Returns true if at least one field was usable — that, not the mere presence
|
|
48
|
+
* of a `usage` key, is what "we observed usage" means.
|
|
49
|
+
*/
|
|
50
|
+
function mergeUsage(draft, usage) {
|
|
51
|
+
let seen = false;
|
|
52
|
+
const input = num(usage["input_tokens"]);
|
|
53
|
+
if (input !== undefined) {
|
|
54
|
+
// Never add cache reads in here. The buckets are disjoint billing buckets;
|
|
55
|
+
// see the UsagePayload doc comment in shared/types.ts.
|
|
56
|
+
draft.inputTokens = input;
|
|
57
|
+
seen = true;
|
|
58
|
+
}
|
|
59
|
+
const cacheRead = num(usage["cache_read_input_tokens"]);
|
|
60
|
+
if (cacheRead !== undefined) {
|
|
61
|
+
draft.cacheReadTokens = cacheRead;
|
|
62
|
+
seen = true;
|
|
63
|
+
}
|
|
64
|
+
// Cache writes: prefer the per-TTL breakdown when Anthropic sends it.
|
|
65
|
+
const creation = asRecord(usage["cache_creation"]);
|
|
66
|
+
const fiveM = creation ? num(creation["ephemeral_5m_input_tokens"]) : undefined;
|
|
67
|
+
const oneH = creation ? num(creation["ephemeral_1h_input_tokens"]) : undefined;
|
|
68
|
+
if (fiveM !== undefined || oneH !== undefined) {
|
|
69
|
+
if (fiveM !== undefined)
|
|
70
|
+
draft.cacheWrite5mTokens = fiveM;
|
|
71
|
+
if (oneH !== undefined)
|
|
72
|
+
draft.cacheWrite1hTokens = oneH;
|
|
73
|
+
draft.cacheBreakdownKnown = true;
|
|
74
|
+
seen = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const flat = num(usage["cache_creation_input_tokens"]);
|
|
78
|
+
if (flat !== undefined && draft.cacheBreakdownKnown) {
|
|
79
|
+
// We already have the real per-TTL split from an earlier event; the flat
|
|
80
|
+
// total is the same tokens, just less precisely reported. Count it as
|
|
81
|
+
// observed but do not overwrite.
|
|
82
|
+
seen = true;
|
|
83
|
+
}
|
|
84
|
+
else if (flat !== undefined) {
|
|
85
|
+
// APPROXIMATION, and only when the breakdown is absent: the flat total is
|
|
86
|
+
// attributed entirely to the 5m bucket because 5m is the default TTL and
|
|
87
|
+
// the overwhelmingly common case. If `cache_creation` is present it always
|
|
88
|
+
// wins, so this never overrides real per-TTL data. 1h is left untouched
|
|
89
|
+
// rather than zeroed, to stay last-wins-per-field.
|
|
90
|
+
draft.cacheWrite5mTokens = flat;
|
|
91
|
+
seen = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const output = num(usage["output_tokens"]);
|
|
95
|
+
if (output !== undefined) {
|
|
96
|
+
// Assignment, not accumulation. See the module header.
|
|
97
|
+
draft.outputTokens = output;
|
|
98
|
+
seen = true;
|
|
99
|
+
}
|
|
100
|
+
const tier = usage["service_tier"];
|
|
101
|
+
if (typeof tier === "string" && tier.length > 0) {
|
|
102
|
+
draft.serviceTier = tier;
|
|
103
|
+
seen = true;
|
|
104
|
+
}
|
|
105
|
+
const toolUse = asRecord(usage["server_tool_use"]);
|
|
106
|
+
if (toolUse) {
|
|
107
|
+
const searches = num(toolUse["web_search_requests"]);
|
|
108
|
+
if (searches !== undefined) {
|
|
109
|
+
draft.webSearches = searches;
|
|
110
|
+
seen = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return seen;
|
|
114
|
+
}
|
|
115
|
+
function parse(data) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(data);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Truncated or hostile payload. Metering loses one event; the request is
|
|
121
|
+
// already served.
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function createUsageAccumulator() {
|
|
126
|
+
const draft = { ...EMPTY_USAGE, serviceTier: undefined, cacheBreakdownKnown: false };
|
|
127
|
+
let observed = false;
|
|
128
|
+
let error = null;
|
|
129
|
+
return {
|
|
130
|
+
apply(event) {
|
|
131
|
+
const parsed = parse(event.data);
|
|
132
|
+
const root = asRecord(parsed);
|
|
133
|
+
if (!root)
|
|
134
|
+
return;
|
|
135
|
+
if (event.type === "message_start") {
|
|
136
|
+
// Usage lives one level deeper here, under the partial message.
|
|
137
|
+
const message = asRecord(root["message"]);
|
|
138
|
+
const usage = message ? asRecord(message["usage"]) : undefined;
|
|
139
|
+
if (usage && mergeUsage(draft, usage))
|
|
140
|
+
observed = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (event.type === "message_delta") {
|
|
144
|
+
// Older API versions send only output_tokens; newer ones re-echo the
|
|
145
|
+
// input and cache fields. Both go through the same last-wins merge, so
|
|
146
|
+
// a re-echo is a no-op rather than a double count.
|
|
147
|
+
const usage = asRecord(root["usage"]);
|
|
148
|
+
if (usage && mergeUsage(draft, usage))
|
|
149
|
+
observed = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (event.type === "error") {
|
|
153
|
+
const err = asRecord(root["error"]);
|
|
154
|
+
if (err) {
|
|
155
|
+
const type = typeof err["type"] === "string" ? err["type"] : "unknown";
|
|
156
|
+
const message = typeof err["message"] === "string" ? err["message"] : "";
|
|
157
|
+
error = { type, message };
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Any other event type carries no usage. Nothing to do.
|
|
162
|
+
},
|
|
163
|
+
snapshot() {
|
|
164
|
+
return {
|
|
165
|
+
inputTokens: draft.inputTokens,
|
|
166
|
+
cacheReadTokens: draft.cacheReadTokens,
|
|
167
|
+
cacheWrite5mTokens: draft.cacheWrite5mTokens,
|
|
168
|
+
cacheWrite1hTokens: draft.cacheWrite1hTokens,
|
|
169
|
+
outputTokens: draft.outputTokens,
|
|
170
|
+
webSearches: draft.webSearches,
|
|
171
|
+
serviceTier: draft.serviceTier,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
sawUsage() {
|
|
175
|
+
return observed;
|
|
176
|
+
},
|
|
177
|
+
streamError() {
|
|
178
|
+
return error;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Parse usage out of a non-streaming JSON response body.
|
|
184
|
+
*
|
|
185
|
+
* Returns null when there is nothing usable, so a caller can tell "no usage
|
|
186
|
+
* reported" from "usage reported as zero" — the same distinction `sawUsage()`
|
|
187
|
+
* draws on the streaming path.
|
|
188
|
+
*/
|
|
189
|
+
export function usageFromJson(body) {
|
|
190
|
+
const root = asRecord(body);
|
|
191
|
+
if (!root)
|
|
192
|
+
return null;
|
|
193
|
+
const usage = asRecord(root["usage"]);
|
|
194
|
+
if (!usage)
|
|
195
|
+
return null;
|
|
196
|
+
const draft = { ...EMPTY_USAGE, serviceTier: undefined, cacheBreakdownKnown: false };
|
|
197
|
+
if (!mergeUsage(draft, usage))
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
inputTokens: draft.inputTokens,
|
|
201
|
+
cacheReadTokens: draft.cacheReadTokens,
|
|
202
|
+
cacheWrite5mTokens: draft.cacheWrite5mTokens,
|
|
203
|
+
cacheWrite1hTokens: draft.cacheWrite1hTokens,
|
|
204
|
+
outputTokens: draft.outputTokens,
|
|
205
|
+
webSearches: draft.webSearches,
|
|
206
|
+
serviceTier: draft.serviceTier,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=accumulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accumulator.js","sourceRoot":"","sources":["../../../server/usage/accumulator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nullable cost algebra.
|
|
3
|
+
*
|
|
4
|
+
* `null` means "no dollar figure applies or is available". It does NOT mean
|
|
5
|
+
* zero, and the distinction is load-bearing in two different ways:
|
|
6
|
+
*
|
|
7
|
+
* - A model with no rate in our table is *unpriced*, not free. Reporting it as
|
|
8
|
+
* $0 understates spend, which is the failure mode this module exists to
|
|
9
|
+
* prevent.
|
|
10
|
+
* - A request served by a developer's own subscription has no marginal dollar
|
|
11
|
+
* cost to the org at all. Its notional list price is not spend and must
|
|
12
|
+
* never be added to a spend total.
|
|
13
|
+
*
|
|
14
|
+
* So addition propagates null: any unpriceable component poisons the total, and
|
|
15
|
+
* the UI is expected to render "$X (+N n/a)" rather than a confidently wrong
|
|
16
|
+
* number. Callers that want a partial sum must count the unpriced rows
|
|
17
|
+
* separately — see `sumCosts`.
|
|
18
|
+
*
|
|
19
|
+
* Adapted from the cost accounting in fw-ai/fireconnect
|
|
20
|
+
* (packages/setup-cli/lib/harnesses/claude/usage/cost.mjs), which arrived at
|
|
21
|
+
* the same null-propagating rule the hard way.
|
|
22
|
+
*/
|
|
23
|
+
/** Add two costs. Null propagates: an unknown component makes the sum unknown. */
|
|
24
|
+
export function addCost(a, b) {
|
|
25
|
+
if (a === null || b === null)
|
|
26
|
+
return null;
|
|
27
|
+
return a + b;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sum a list of costs, keeping the unpriced count so a dashboard can be honest
|
|
31
|
+
* ("$12.3456 (+3 n/a)") instead of silently dropping rows the way SQL `SUM`
|
|
32
|
+
* does with NULLs.
|
|
33
|
+
*/
|
|
34
|
+
export function sumCosts(costs) {
|
|
35
|
+
let priced = 0;
|
|
36
|
+
let unpricedCount = 0;
|
|
37
|
+
for (const c of costs) {
|
|
38
|
+
if (c === null)
|
|
39
|
+
unpricedCount += 1;
|
|
40
|
+
else
|
|
41
|
+
priced += c;
|
|
42
|
+
}
|
|
43
|
+
return { priced, unpricedCount, total: unpricedCount > 0 ? null : priced };
|
|
44
|
+
}
|
|
45
|
+
/** Four decimals: a single cheap call can be worth well under a cent. */
|
|
46
|
+
export function formatCost(cost) {
|
|
47
|
+
if (cost === null)
|
|
48
|
+
return "n/a";
|
|
49
|
+
return `$${cost.toFixed(4)}`;
|
|
50
|
+
}
|
|
51
|
+
/** Render a sum honestly, flagging that it is a lower bound when it is one. */
|
|
52
|
+
export function formatCostSum(sum) {
|
|
53
|
+
if (sum.unpricedCount === 0)
|
|
54
|
+
return formatCost(sum.priced);
|
|
55
|
+
return `≥ $${sum.priced.toFixed(4)} (+${sum.unpricedCount} n/a)`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=cost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../../../server/usage/cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,kFAAkF;AAClF,MAAM,UAAU,OAAO,CAAC,CAAO,EAAE,CAAO;IACtC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAsB;IAC7C,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,IAAI;YAAE,aAAa,IAAI,CAAC,CAAC;;YAC9B,MAAM,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;AAC7E,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,UAAU,CAAC,IAAU;IACnC,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,GAAG,CAAC,aAAa,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3D,OAAO,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,aAAa,OAAO,CAAC;AACnE,CAAC"}
|