@timo972/cc-router 0.10.0 → 0.11.0-rc.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/CHANGELOG.md +155 -33
- package/README.md +4 -3
- package/dist/config/manager.js +9 -0
- package/dist/providers/anthropic/rate-limit-headers.js +44 -0
- package/dist/providers/anthropic/usage.js +24 -5
- package/dist/proxy/anthropic-messages-route.js +456 -0
- package/dist/proxy/anthropic-response-capture.js +40 -0
- package/dist/proxy/event-sequence.js +18 -0
- package/dist/proxy/lease-lifecycle.js +20 -13
- package/dist/proxy/messages-cross-route.js +7 -0
- package/dist/proxy/openai-ingress.js +218 -109
- package/dist/proxy/responses-server.js +7 -0
- package/dist/proxy/server.js +100 -109
- package/dist/proxy/stats.js +19 -0
- package/dist/proxy/token-pool.js +302 -37
- package/dist/proxy/upstream-retry.js +87 -0
- package/package.json +1 -1
package/dist/proxy/server.js
CHANGED
|
@@ -4,12 +4,14 @@ import { ServerResponse } from "http";
|
|
|
4
4
|
import { timingSafeEqual } from "crypto";
|
|
5
5
|
import { TokenPool } from "./token-pool.js";
|
|
6
6
|
import { needsRefresh, refreshAccountIfCurrent, saveAccounts, startRefreshLoop } from "./token-refresher.js";
|
|
7
|
-
import { loadAccounts, loadOpenAIAccounts, saveOpenAIAccountsToPath, accountsFileExists, readAccountsFromPath, readConfig, writeConfig, getProxyRequestTimeoutMs, migrateLegacyAccountProviders, setProviderAccountsEnabled } from "../config/manager.js";
|
|
7
|
+
import { loadAccounts, loadOpenAIAccounts, saveOpenAIAccountsToPath, accountsFileExists, readAccountsFromPath, readConfig, writeConfig, getAutoFailoverEnabled, getProxyRequestTimeoutMs, migrateLegacyAccountProviders, setProviderAccountsEnabled } from "../config/manager.js";
|
|
8
8
|
import { checkForUpdate, performUpdate, restartSelf, printUpdateBanner, getCurrentVersion } from "../utils/self-update.js";
|
|
9
9
|
import { trackEvent, startHeartbeat } from "../utils/telemetry.js";
|
|
10
10
|
import { loadTelemetryState } from "../config/telemetry.js";
|
|
11
11
|
import { logRoute, logError, logStartup } from "./logger.js";
|
|
12
12
|
import { createLocalRoutingErrorLog, stats } from "./stats.js";
|
|
13
|
+
import { applyRateLimitHeaders } from "../providers/anthropic/rate-limit-headers.js";
|
|
14
|
+
import { mountAnthropicMessagesRoute, withOAuthBeta } from "./anthropic-messages-route.js";
|
|
13
15
|
import { PROXY_PORT, LITELLM_URL, ACCOUNTS_PATH } from "../config/paths.js";
|
|
14
16
|
import { writePid, removePid, managesPidFile } from "../daemon/pid.js";
|
|
15
17
|
import { applyOpenAIAccountPatch, validateAccountPatchBody } from "./account-patch.js";
|
|
@@ -26,14 +28,13 @@ import { SessionRouter } from "./session-router.js";
|
|
|
26
28
|
import { createAnthropicProxy } from "./anthropic-proxy.js";
|
|
27
29
|
import { AnthropicUsageRefresher } from "../providers/anthropic/usage-refresher.js";
|
|
28
30
|
import { OpenAIUsageRefresher } from "../providers/openai/usage-fetch.js";
|
|
29
|
-
import {
|
|
31
|
+
import { attachAnthropicResponseCapture } from "./anthropic-response-capture.js";
|
|
30
32
|
import { canUseExtraUsage } from "../providers/anthropic/usage.js";
|
|
31
33
|
import { applyUpstreamFailureRoutingDetailed, reconcileAmbiguousRateLimitCooldown, routeFailureDetails, routeReasonDetails, } from "./lease-lifecycle.js";
|
|
32
34
|
import { persistProviderEnabledState } from "./provider-routing.js";
|
|
33
35
|
import { accountDeletionStatusCode, deleteAnthropicAccountTransaction, deleteOpenAIAccountTransaction, } from "./account-deletion.js";
|
|
34
36
|
import { addOpenAIAccountTransaction } from "./account-add.js";
|
|
35
37
|
import { createAnthropicRefreshMiddleware, createAnthropicRoutingMiddleware, } from "./anthropic-routing.js";
|
|
36
|
-
import { createStreamLifecycleTracker } from "./stream-lifecycle.js";
|
|
37
38
|
const zeroRoutingMetrics = () => ({
|
|
38
39
|
inFlightRequests: 0,
|
|
39
40
|
activeSessions: 0,
|
|
@@ -139,6 +140,8 @@ function publicUsageSnapshot(usage) {
|
|
|
139
140
|
fetchStatus: usage.fetchStatus,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
143
|
+
// An unreported utilization surfaces as 0 here, which is what the dashboard
|
|
144
|
+
// has always shown for it; only release decisions need the distinction.
|
|
142
145
|
function publicWindow(window) {
|
|
143
146
|
return { utilization: publicUtilization(window.utilization), resetAt: publicTimestamp(window.resetAt) };
|
|
144
147
|
}
|
|
@@ -277,57 +280,9 @@ function providerStatus(accounts) {
|
|
|
277
280
|
enabled: accounts.filter(a => a.enabled !== false).length,
|
|
278
281
|
};
|
|
279
282
|
}
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
function applyInputUsage(entry, usage) {
|
|
284
|
-
entry.cacheReadTokens = usage["cache_read_input_tokens"] ?? 0;
|
|
285
|
-
entry.cacheCreationTokens = usage["cache_creation_input_tokens"] ?? 0;
|
|
286
|
-
entry.inputTokens = usage["input_tokens"] ?? 0;
|
|
287
|
-
stats.totalCacheReadTokens += entry.cacheReadTokens;
|
|
288
|
-
stats.totalCacheCreationTokens += entry.cacheCreationTokens;
|
|
289
|
-
stats.totalInputTokens += entry.inputTokens;
|
|
290
|
-
}
|
|
291
|
-
function applyOutputUsage(entry, usage) {
|
|
292
|
-
entry.outputTokens = usage["output_tokens"] ?? 0;
|
|
293
|
-
stats.totalOutputTokens += entry.outputTokens;
|
|
294
|
-
}
|
|
295
|
-
// ─── Rate limit header extraction ──────────────────────────────────────────
|
|
296
|
-
function inferPlan(requestsLimit) {
|
|
297
|
-
if (requestsLimit <= 0)
|
|
298
|
-
return "";
|
|
299
|
-
if (requestsLimit <= 100)
|
|
300
|
-
return "Pro";
|
|
301
|
-
if (requestsLimit <= 500)
|
|
302
|
-
return "Max 5x";
|
|
303
|
-
return "Max 20x";
|
|
304
|
-
}
|
|
305
|
-
function extractRateLimits(headers) {
|
|
306
|
-
const h = (name) => String(headers[name] ?? "");
|
|
307
|
-
const status = h("anthropic-ratelimit-unified-status");
|
|
308
|
-
if (!status)
|
|
309
|
-
return null; // No unified headers in this response
|
|
310
|
-
const requestsLimit = parseInt(h("anthropic-ratelimit-requests-limit"), 10) || 0;
|
|
311
|
-
return {
|
|
312
|
-
status: status === "rate_limited" ? "rate_limited" : "allowed",
|
|
313
|
-
fiveHourUtil: parseFloat(h("anthropic-ratelimit-unified-5h-utilization")) || 0,
|
|
314
|
-
fiveHourReset: parseInt(h("anthropic-ratelimit-unified-5h-reset"), 10) || 0,
|
|
315
|
-
sevenDayUtil: parseFloat(h("anthropic-ratelimit-unified-7d-utilization")) || 0,
|
|
316
|
-
sevenDayReset: parseInt(h("anthropic-ratelimit-unified-7d-reset"), 10) || 0,
|
|
317
|
-
claim: h("anthropic-ratelimit-unified-representative-claim"),
|
|
318
|
-
plan: inferPlan(requestsLimit),
|
|
319
|
-
requestsLimit,
|
|
320
|
-
lastUpdated: Date.now(),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
/** Apply upstream rate-limit headers without discarding the usage snapshot. */
|
|
324
|
-
export function applyRateLimitHeaders(account, headers) {
|
|
325
|
-
const rateLimits = extractRateLimits(headers);
|
|
326
|
-
if (!rateLimits)
|
|
327
|
-
return false;
|
|
328
|
-
account.rateLimits = { ...account.rateLimits, ...rateLimits };
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
283
|
+
// Re-exported so existing importers keep working; the implementation moved to
|
|
284
|
+
// providers/anthropic so both Anthropic transports share it.
|
|
285
|
+
export { applyRateLimitHeaders } from "../providers/anthropic/rate-limit-headers.js";
|
|
331
286
|
/**
|
|
332
287
|
* Build the single function through which this server writes OpenAI accounts.
|
|
333
288
|
*
|
|
@@ -442,6 +397,13 @@ export async function startServer(opts = {}) {
|
|
|
442
397
|
openAIUsageRefresher.start();
|
|
443
398
|
const app = express();
|
|
444
399
|
const proxyRequestTimeoutMs = getProxyRequestTimeoutMs();
|
|
400
|
+
// Router-side 429 failover / 5xx retry is on by default; `"autoFailover":
|
|
401
|
+
// false` in config.json opts out for anyone who cannot work with the
|
|
402
|
+
// trade-off (a committed retry abandons the original failure response).
|
|
403
|
+
// A single-attempt budget IS the off switch: both transports then relay
|
|
404
|
+
// every upstream failure unchanged, exactly as before the feature existed.
|
|
405
|
+
const autoFailover = getAutoFailoverEnabled();
|
|
406
|
+
const upstreamAttempts = autoFailover ? {} : { maxAttempts: 1 };
|
|
445
407
|
// ─── Proxy auth middleware ─────────────────────────────────────────────────
|
|
446
408
|
// If a proxySecret is configured, all requests must present it as EITHER
|
|
447
409
|
// "Authorization: Bearer <secret>" (Claude Code CLI, HTTP clients)
|
|
@@ -923,6 +885,7 @@ export async function startServer(opts = {}) {
|
|
|
923
885
|
prepareOpenAIAccount: (account) => prepareOpenAIAccountForRequest(account, openAIAccounts, persistOpenAIAccounts),
|
|
924
886
|
modelRouting,
|
|
925
887
|
onUpstreamAuthFailure: onOpenAIUpstreamAuthFailure,
|
|
888
|
+
...upstreamAttempts,
|
|
926
889
|
});
|
|
927
890
|
mountMessagesCrossProviderRoute(app, {
|
|
928
891
|
openAIRouter,
|
|
@@ -930,6 +893,58 @@ export async function startServer(opts = {}) {
|
|
|
930
893
|
prepareOpenAIAccount: (account) => prepareOpenAIAccountForRequest(account, openAIAccounts, persistOpenAIAccounts),
|
|
931
894
|
modelRouting,
|
|
932
895
|
onUpstreamAuthFailure: onOpenAIUpstreamAuthFailure,
|
|
896
|
+
...upstreamAttempts,
|
|
897
|
+
});
|
|
898
|
+
// Shared between the retrying /v1/messages route and the generic /v1 chain
|
|
899
|
+
// so a locally rejected request is reported identically on both.
|
|
900
|
+
const onAnthropicEmptyPool = (err, _req, res) => {
|
|
901
|
+
stats.totalErrors++;
|
|
902
|
+
logError("proxy", 503, err.message);
|
|
903
|
+
res.status(503).json({
|
|
904
|
+
type: "error",
|
|
905
|
+
error: { type: "no_accounts", message: err.message },
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
const onAnthropicNoEligibleAccount = (err, req) => {
|
|
909
|
+
stats.totalErrors++;
|
|
910
|
+
const entry = createLocalRoutingErrorLog(err.reason, req._ccRouteContext?.modelFamily);
|
|
911
|
+
stats.addLog(entry);
|
|
912
|
+
logError(entry.accountId, entry.statusCode ?? 0, entry.details ?? "no-eligible");
|
|
913
|
+
};
|
|
914
|
+
const onAnthropicRefreshFailure = (account) => {
|
|
915
|
+
stats.totalErrors++;
|
|
916
|
+
logError(account.id, 401, "Token refresh failed");
|
|
917
|
+
};
|
|
918
|
+
// Claude-bound POST /v1/messages goes through its own transport with
|
|
919
|
+
// router-side 429 failover and 5xx retry; every other /v1 endpoint stays on
|
|
920
|
+
// the generic byte-transparent proxy below.
|
|
921
|
+
mountAnthropicMessagesRoute(app, {
|
|
922
|
+
target,
|
|
923
|
+
timeoutMs: proxyRequestTimeoutMs,
|
|
924
|
+
pool,
|
|
925
|
+
sessionRouter,
|
|
926
|
+
...upstreamAttempts,
|
|
927
|
+
needsRefresh,
|
|
928
|
+
refresh: account => refreshAccountIfCurrent(account, pool),
|
|
929
|
+
onRefreshFailure: onAnthropicRefreshFailure,
|
|
930
|
+
onEmptyPool: onAnthropicEmptyPool,
|
|
931
|
+
onNoEligibleAccount: onAnthropicNoEligibleAccount,
|
|
932
|
+
// A relayed 401 means the token is stale — refresh in the background so
|
|
933
|
+
// the next request succeeds without making this client wait on it.
|
|
934
|
+
onUpstream401: account => {
|
|
935
|
+
void refreshAccountIfCurrent(account, pool).catch(console.error);
|
|
936
|
+
},
|
|
937
|
+
// Refresh in the background to narrow only ambiguity-owned global state
|
|
938
|
+
// when fresh usage proves a requested-model exhaustion.
|
|
939
|
+
onRateLimited: (route, ambiguousCooldownToken) => {
|
|
940
|
+
queueMicrotask(() => {
|
|
941
|
+
void usageRefresher.refreshAfterCurrent(route.account).then(result => {
|
|
942
|
+
if (result.ok) {
|
|
943
|
+
reconcileAmbiguousRateLimitCooldown(route, pool, ambiguousCooldownToken);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
},
|
|
933
948
|
});
|
|
934
949
|
// ─── Proxy middleware ──────────────────────────────────────────────────────
|
|
935
950
|
// IMPORTANT: selfHandleResponse must be false (default) for SSE streaming to
|
|
@@ -952,15 +967,9 @@ export async function startServer(opts = {}) {
|
|
|
952
967
|
// CRITICAL: api.anthropic.com requires the "oauth-2025-04-20" beta flag to
|
|
953
968
|
// accept OAuth tokens (sk-ant-oat01-*). Without it the request is rejected
|
|
954
969
|
// with "OAuth authentication is currently not supported."
|
|
955
|
-
// APPEND — do NOT replace — so existing betas (tools, computer-use, etc.)
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
? String(existingBeta).split(",").map(b => b.trim()).filter(Boolean)
|
|
959
|
-
: [];
|
|
960
|
-
if (!betas.includes("oauth-2025-04-20")) {
|
|
961
|
-
betas.push("oauth-2025-04-20");
|
|
962
|
-
proxyReq.setHeader("anthropic-beta", betas.join(","));
|
|
963
|
-
}
|
|
970
|
+
// APPEND — do NOT replace — so existing betas (tools, computer-use, etc.)
|
|
971
|
+
// are preserved. Shared with the retrying /v1/messages transport.
|
|
972
|
+
proxyReq.setHeader("anthropic-beta", withOAuthBeta(proxyReq.getHeader("anthropic-beta")));
|
|
964
973
|
// All other headers are forwarded automatically by http-proxy-middleware:
|
|
965
974
|
// anthropic-version — required by Anthropic API
|
|
966
975
|
// X-Claude-Code-Session-Id — session aggregation header sent by Claude Code
|
|
@@ -1037,39 +1046,33 @@ export async function startServer(opts = {}) {
|
|
|
1037
1046
|
: "service-overloaded";
|
|
1038
1047
|
logError(account.id, 529, "Service overloaded — cooldown 30s");
|
|
1039
1048
|
}
|
|
1049
|
+
else if (status >= 500) {
|
|
1050
|
+
// Any other upstream 5xx passes through byte-transparent — but it
|
|
1051
|
+
// must not pass through the DIAGNOSTICS silently: an overnight
|
|
1052
|
+
// Anthropic 500 stopped an unattended Claude session while this
|
|
1053
|
+
// log showed nothing and the stats reported a clean night. The
|
|
1054
|
+
// question "did the proxy or the upstream fail?" was only
|
|
1055
|
+
// answerable by cross-referencing the client's own transcript.
|
|
1056
|
+
// Unlike 429/529 this takes no cooldown: a plain 5xx says nothing
|
|
1057
|
+
// about the account's capacity and can even be request-specific,
|
|
1058
|
+
// so cooling the account down would punish it for upstream's (or
|
|
1059
|
+
// the request's) problem.
|
|
1060
|
+
stats.totalErrors++;
|
|
1061
|
+
account.errorCount++;
|
|
1062
|
+
pendingLog.type = "error";
|
|
1063
|
+
pendingLog.details = route
|
|
1064
|
+
? routeFailureDetails(route, "upstream-error")
|
|
1065
|
+
: "upstream-error";
|
|
1066
|
+
logError(account.id, status, "Upstream server error (passed through)");
|
|
1067
|
+
}
|
|
1040
1068
|
// ── Capture rate limit utilization from response headers ────────────
|
|
1041
1069
|
applyRateLimitHeaders(account, proxyRes.headers);
|
|
1042
1070
|
const entry = pendingLog;
|
|
1043
1071
|
stats.addLog(entry);
|
|
1044
1072
|
// ── Capture token usage from Anthropic response body ─────────────────
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
|
|
1048
|
-
// Non-streaming JSON carries all fields in a single usage object.
|
|
1049
|
-
// The proxy is byte-transparent and the client's accept-encoding makes
|
|
1050
|
-
// upstream compress, so the capture decompresses its own copy of the
|
|
1051
|
-
// stream (see usage-capture.ts) — previously compressed responses were
|
|
1052
|
-
// skipped, which in practice was EVERY response: no cache rate or
|
|
1053
|
-
// token counts ever appeared on Anthropic activity rows.
|
|
1054
|
-
const contentType = String(proxyRes.headers["content-type"] ?? "");
|
|
1055
|
-
const encoding = String(proxyRes.headers["content-encoding"] ?? "");
|
|
1056
|
-
const isCompressed = /gzip|br|deflate/.test(encoding);
|
|
1057
|
-
const streamTracker = createStreamLifecycleTracker(req._startTime ?? Date.now(), !isCompressed && contentType.includes("text/event-stream"));
|
|
1058
|
-
entry.streamLifecycle = streamTracker.state;
|
|
1059
|
-
streamTracker.attach(proxyRes, response);
|
|
1060
|
-
proxyRes.on("data", (chunk) => streamTracker.observeChunk(chunk));
|
|
1061
|
-
const usageCapture = createAnthropicUsageCapture({
|
|
1062
|
-
contentType,
|
|
1063
|
-
contentEncoding: encoding,
|
|
1064
|
-
// Mutates the already-logged entry in place; the dashboard picks the
|
|
1065
|
-
// values up on its next poll.
|
|
1066
|
-
onInputUsage: (usage) => applyInputUsage(entry, usage),
|
|
1067
|
-
onOutputUsage: (usage) => applyOutputUsage(entry, usage),
|
|
1068
|
-
});
|
|
1069
|
-
if (usageCapture) {
|
|
1070
|
-
proxyRes.on("data", (chunk) => usageCapture.write(chunk));
|
|
1071
|
-
proxyRes.on("end", () => usageCapture.end());
|
|
1072
|
-
}
|
|
1073
|
+
// Passive stream-lifecycle + token-usage taps, shared with the
|
|
1074
|
+
// retrying /v1/messages transport (see anthropic-response-capture.ts).
|
|
1075
|
+
attachAnthropicResponseCapture(proxyRes, response, entry, req._startTime ?? Date.now());
|
|
1073
1076
|
},
|
|
1074
1077
|
error: (err, _req, res) => {
|
|
1075
1078
|
const request = _req;
|
|
@@ -1106,27 +1109,12 @@ export async function startServer(opts = {}) {
|
|
|
1106
1109
|
// and breaks SSE streaming passthrough.
|
|
1107
1110
|
app.use("/v1", createAnthropicRoutingMiddleware({
|
|
1108
1111
|
sessionRouter,
|
|
1109
|
-
onEmptyPool:
|
|
1110
|
-
|
|
1111
|
-
logError("proxy", 503, err.message);
|
|
1112
|
-
res.status(503).json({
|
|
1113
|
-
type: "error",
|
|
1114
|
-
error: { type: "no_accounts", message: err.message },
|
|
1115
|
-
});
|
|
1116
|
-
},
|
|
1117
|
-
onNoEligibleAccount: (err, req) => {
|
|
1118
|
-
stats.totalErrors++;
|
|
1119
|
-
const entry = createLocalRoutingErrorLog(err.reason, req._ccRouteContext?.modelFamily);
|
|
1120
|
-
stats.addLog(entry);
|
|
1121
|
-
logError(entry.accountId, entry.statusCode ?? 0, entry.details ?? "no-eligible");
|
|
1122
|
-
},
|
|
1112
|
+
onEmptyPool: onAnthropicEmptyPool,
|
|
1113
|
+
onNoEligibleAccount: onAnthropicNoEligibleAccount,
|
|
1123
1114
|
}), createAnthropicRefreshMiddleware({
|
|
1124
1115
|
needsRefresh,
|
|
1125
1116
|
refresh: account => refreshAccountIfCurrent(account, pool),
|
|
1126
|
-
onRefreshFailure:
|
|
1127
|
-
stats.totalErrors++;
|
|
1128
|
-
logError(account.id, 401, "Token refresh failed");
|
|
1129
|
-
},
|
|
1117
|
+
onRefreshFailure: onAnthropicRefreshFailure,
|
|
1130
1118
|
}), (req, _res, next) => {
|
|
1131
1119
|
const route = req._ccRoute;
|
|
1132
1120
|
const account = route.account;
|
|
@@ -1239,6 +1227,9 @@ export async function startServer(opts = {}) {
|
|
|
1239
1227
|
console.log(autoUpdate
|
|
1240
1228
|
? chalk.gray(" Auto-update: enabled (patch/minor)")
|
|
1241
1229
|
: chalk.gray(" Auto-update: off (notify-only) — run 'cc-router update' to install"));
|
|
1230
|
+
console.log(autoFailover
|
|
1231
|
+
? chalk.gray(" Auto-failover: on — 429/5xx retried across accounts before the first relayed byte")
|
|
1232
|
+
: chalk.gray(" Auto-failover: off — upstream failures pass through; clients own retries"));
|
|
1242
1233
|
// Anonymous telemetry — fire-and-forget, never blocks proxy startup.
|
|
1243
1234
|
try {
|
|
1244
1235
|
const telemetryState = loadTelemetryState();
|
package/dist/proxy/stats.js
CHANGED
|
@@ -52,6 +52,25 @@ class ProxyStats {
|
|
|
52
52
|
}
|
|
53
53
|
// Singleton — shared across server and health endpoint
|
|
54
54
|
export const stats = new ProxyStats();
|
|
55
|
+
/**
|
|
56
|
+
* Record Anthropic input-side usage (message_start, or a non-streaming JSON
|
|
57
|
+
* body) on both the request's log entry and the running totals. Mutates an
|
|
58
|
+
* entry that is typically already stored — the dashboard picks the values up
|
|
59
|
+
* on its next poll.
|
|
60
|
+
*/
|
|
61
|
+
export function applyAnthropicInputUsage(entry, usage) {
|
|
62
|
+
entry.cacheReadTokens = usage["cache_read_input_tokens"] ?? 0;
|
|
63
|
+
entry.cacheCreationTokens = usage["cache_creation_input_tokens"] ?? 0;
|
|
64
|
+
entry.inputTokens = usage["input_tokens"] ?? 0;
|
|
65
|
+
stats.totalCacheReadTokens += entry.cacheReadTokens;
|
|
66
|
+
stats.totalCacheCreationTokens += entry.cacheCreationTokens;
|
|
67
|
+
stats.totalInputTokens += entry.inputTokens;
|
|
68
|
+
}
|
|
69
|
+
/** Record Anthropic output-side usage (message_delta) — see input counterpart. */
|
|
70
|
+
export function applyAnthropicOutputUsage(entry, usage) {
|
|
71
|
+
entry.outputTokens = usage["output_tokens"] ?? 0;
|
|
72
|
+
stats.totalOutputTokens += entry.outputTokens;
|
|
73
|
+
}
|
|
55
74
|
/** Record Codex token usage on both the request's log entry and the running totals. */
|
|
56
75
|
export function applyCodexUsage(entry, usage) {
|
|
57
76
|
if (!usage)
|