proxitor 0.15.0 → 0.17.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/README.md +75 -9
- package/dist/cli.mjs +632 -149
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7,12 +7,13 @@ import tty, { ReadStream } from "node:tty";
|
|
|
7
7
|
import { formatWithOptions, isDeepStrictEqual, styleText } from "node:util";
|
|
8
8
|
import * as l$1 from "node:readline";
|
|
9
9
|
import l__default from "node:readline";
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, join, resolve, sep } from "node:path";
|
|
12
12
|
import { createServer } from "node:net";
|
|
13
13
|
import { STATUS_CODES, createServer as createServer$1 } from "node:http";
|
|
14
14
|
import { Http2ServerRequest, constants } from "node:http2";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
18
|
//#region \0rolldown/runtime.js
|
|
18
19
|
var __defProp = Object.defineProperty;
|
|
@@ -16271,6 +16272,20 @@ const ttlSchema = _enum([
|
|
|
16271
16272
|
"omit",
|
|
16272
16273
|
"skip"
|
|
16273
16274
|
]);
|
|
16275
|
+
const OBSERVABILITY_DEFAULTS = {
|
|
16276
|
+
routerMetadata: true,
|
|
16277
|
+
hitThreshold: 80,
|
|
16278
|
+
sideMaxTokens: 4096,
|
|
16279
|
+
sessionMaxEntries: 4096,
|
|
16280
|
+
sessionTtlMs: 6e5
|
|
16281
|
+
};
|
|
16282
|
+
const observabilityConfigSchema = object({
|
|
16283
|
+
routerMetadata: boolean().default(OBSERVABILITY_DEFAULTS.routerMetadata),
|
|
16284
|
+
hitThreshold: number$1().int().min(0).max(100).default(OBSERVABILITY_DEFAULTS.hitThreshold),
|
|
16285
|
+
sideMaxTokens: number$1().int().positive().default(OBSERVABILITY_DEFAULTS.sideMaxTokens),
|
|
16286
|
+
sessionMaxEntries: number$1().int().positive().default(OBSERVABILITY_DEFAULTS.sessionMaxEntries),
|
|
16287
|
+
sessionTtlMs: number$1().int().positive().default(OBSERVABILITY_DEFAULTS.sessionTtlMs)
|
|
16288
|
+
}).default(OBSERVABILITY_DEFAULTS);
|
|
16274
16289
|
const modelOverrideSchema = object({
|
|
16275
16290
|
provider: providerConfigSchema.optional(),
|
|
16276
16291
|
headers: record(string$1(), string$1()).optional(),
|
|
@@ -16298,6 +16313,7 @@ const proxyConfigSchema = object({
|
|
|
16298
16313
|
rewriteBlockTtl: triStateSchema.default("skip"),
|
|
16299
16314
|
sessionId: triStateSchema.default("auto"),
|
|
16300
16315
|
normalizeVolatileSystem: boolean().default(false),
|
|
16316
|
+
observability: observabilityConfigSchema,
|
|
16301
16317
|
modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
|
|
16302
16318
|
}).strict();
|
|
16303
16319
|
const DEFAULTS = proxyConfigSchema.parse({});
|
|
@@ -16329,6 +16345,16 @@ var MissingConfigError = class extends Error {
|
|
|
16329
16345
|
}
|
|
16330
16346
|
};
|
|
16331
16347
|
//#endregion
|
|
16348
|
+
//#region src/model-id.ts
|
|
16349
|
+
/** `"anthropic/claude-sonnet-4"` → `"anthropic"`; a bare `"gpt-4o"` → `"gpt-4o"`. */
|
|
16350
|
+
function parseModelAuthor(modelId) {
|
|
16351
|
+
return modelId.split("/")[0] ?? "";
|
|
16352
|
+
}
|
|
16353
|
+
/** Text after the first `/`, or `""` for a bare id. `"openai/gpt-4o"` → `"gpt-4o"`. */
|
|
16354
|
+
function parseModelSlug(modelId) {
|
|
16355
|
+
return modelId.split("/").slice(1).join("/");
|
|
16356
|
+
}
|
|
16357
|
+
//#endregion
|
|
16332
16358
|
//#region src/utils.ts
|
|
16333
16359
|
/** Format an API key with the appropriate auth prefix based on authType. Defaults to Bearer. */
|
|
16334
16360
|
function formatAuthHeader(key, authType) {
|
|
@@ -16411,14 +16437,49 @@ function buildProviderRouting(provider) {
|
|
|
16411
16437
|
if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
|
|
16412
16438
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
16413
16439
|
}
|
|
16440
|
+
/** Slug after the vendor prefix; bare ids keep their text. Delegates to parseModelSlug. */
|
|
16441
|
+
function modelSlug(s) {
|
|
16442
|
+
return parseModelSlug(s) || s;
|
|
16443
|
+
}
|
|
16414
16444
|
function matchScore(pattern, modelName) {
|
|
16415
|
-
if (pattern === modelName) return
|
|
16416
|
-
if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
|
|
16445
|
+
if (pattern === modelName) return 3e3 + pattern.length;
|
|
16446
|
+
if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return 2e3 + pattern.length;
|
|
16447
|
+
if (pattern.includes("/") && modelName.includes("/")) return -1;
|
|
16448
|
+
const sp = modelSlug(pattern);
|
|
16449
|
+
const sm = modelSlug(modelName);
|
|
16450
|
+
if (sp === sm) return 1e3 + sp.length;
|
|
16451
|
+
if (sp.endsWith("*") && sm.startsWith(sp.slice(0, -1))) return sp.length;
|
|
16417
16452
|
return -1;
|
|
16418
16453
|
}
|
|
16419
16454
|
function matchesPattern(pattern, modelName) {
|
|
16420
16455
|
return matchScore(pattern, modelName) >= 0;
|
|
16421
16456
|
}
|
|
16457
|
+
/** Override keys sharing a slug. `winner` is the key a bare name resolves to — not always `keys[0]`, since a bare key wins on the full-exact tier. Pure; callers log it. */
|
|
16458
|
+
function detectSlugCollisions(overrides) {
|
|
16459
|
+
if (!overrides) return [];
|
|
16460
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16461
|
+
for (const key of Object.keys(overrides)) {
|
|
16462
|
+
const slug = modelSlug(key);
|
|
16463
|
+
const keys = groups.get(slug);
|
|
16464
|
+
if (keys) keys.push(key);
|
|
16465
|
+
else groups.set(slug, [key]);
|
|
16466
|
+
}
|
|
16467
|
+
const collisions = [];
|
|
16468
|
+
for (const [slug, keys] of groups) {
|
|
16469
|
+
if (keys.length <= 1) continue;
|
|
16470
|
+
const winner = findBestMatch(keys, slug);
|
|
16471
|
+
if (winner) collisions.push({
|
|
16472
|
+
slug,
|
|
16473
|
+
keys,
|
|
16474
|
+
winner
|
|
16475
|
+
});
|
|
16476
|
+
}
|
|
16477
|
+
return collisions;
|
|
16478
|
+
}
|
|
16479
|
+
/** Warning text for a same-slug collision. */
|
|
16480
|
+
function formatSlugCollisionWarning(c) {
|
|
16481
|
+
return `Overrides ${c.keys.map((k) => `"${k}"`).join(" and ")} share model slug "${c.slug}"; a bare name resolves to "${c.winner}". Use the vendor-prefixed name to pick a specific one.`;
|
|
16482
|
+
}
|
|
16422
16483
|
function resolveModelConfig(config, modelName) {
|
|
16423
16484
|
const result = {
|
|
16424
16485
|
provider: config.provider,
|
|
@@ -16431,7 +16492,10 @@ function resolveModelConfig(config, modelName) {
|
|
|
16431
16492
|
};
|
|
16432
16493
|
if (!modelName || !config.modelOverrides) return result;
|
|
16433
16494
|
const bestPattern = findBestMatch(Object.keys(config.modelOverrides), modelName);
|
|
16434
|
-
if (bestPattern)
|
|
16495
|
+
if (bestPattern) {
|
|
16496
|
+
applyOverride(result, config.modelOverrides[bestPattern]);
|
|
16497
|
+
result.matchedOverride = bestPattern;
|
|
16498
|
+
}
|
|
16435
16499
|
return result;
|
|
16436
16500
|
}
|
|
16437
16501
|
function findBestMatch(patterns, modelName) {
|
|
@@ -16677,14 +16741,6 @@ async function fetchModels(client) {
|
|
|
16677
16741
|
writeCache(CACHE_KEY$1, models);
|
|
16678
16742
|
return models;
|
|
16679
16743
|
}
|
|
16680
|
-
/** `"anthropic/claude-sonnet-4"` → `"anthropic"` */
|
|
16681
|
-
function parseModelAuthor(modelId) {
|
|
16682
|
-
return modelId.split("/")[0] ?? "";
|
|
16683
|
-
}
|
|
16684
|
-
/** `"anthropic/claude-sonnet-4"` → `"claude-sonnet-4"` */
|
|
16685
|
-
function parseModelSlug(modelId) {
|
|
16686
|
-
return modelId.split("/").slice(1).join("/");
|
|
16687
|
-
}
|
|
16688
16744
|
/** `"0.000003"` → `"$3.00"`, `"0"` → `"free"` */
|
|
16689
16745
|
function formatPrice(pricePerToken) {
|
|
16690
16746
|
const per1M = Number.parseFloat(pricePerToken) * 1e6;
|
|
@@ -19514,7 +19570,7 @@ async function runConfigMenu(client) {
|
|
|
19514
19570
|
}
|
|
19515
19571
|
//#endregion
|
|
19516
19572
|
//#region src/version.ts
|
|
19517
|
-
const version = "0.
|
|
19573
|
+
const version = "0.17.0";
|
|
19518
19574
|
//#endregion
|
|
19519
19575
|
//#region src/commands/doctor.ts
|
|
19520
19576
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
@@ -19601,6 +19657,24 @@ function checkConfigValidity(path) {
|
|
|
19601
19657
|
};
|
|
19602
19658
|
}
|
|
19603
19659
|
}
|
|
19660
|
+
function checkSlugCollisions(cfg) {
|
|
19661
|
+
if (!cfg?.modelOverrides) return {
|
|
19662
|
+
name: "override-collisions",
|
|
19663
|
+
status: "ok",
|
|
19664
|
+
message: "no model overrides"
|
|
19665
|
+
};
|
|
19666
|
+
const collisions = detectSlugCollisions(cfg.modelOverrides);
|
|
19667
|
+
if (collisions.length === 0) return {
|
|
19668
|
+
name: "override-collisions",
|
|
19669
|
+
status: "ok",
|
|
19670
|
+
message: "no slug collisions"
|
|
19671
|
+
};
|
|
19672
|
+
return {
|
|
19673
|
+
name: "override-collisions",
|
|
19674
|
+
status: "warn",
|
|
19675
|
+
message: collisions.map(formatSlugCollisionWarning).join(" | ")
|
|
19676
|
+
};
|
|
19677
|
+
}
|
|
19604
19678
|
function checkApiKey(cfg) {
|
|
19605
19679
|
const fromEnv = process.env.OPENROUTER_API_KEY ? "set" : "not set";
|
|
19606
19680
|
const fromFile = cfg?.openrouterKey ? "set" : "not set";
|
|
@@ -19709,7 +19783,11 @@ async function doctorCommand(opts = {}) {
|
|
|
19709
19783
|
];
|
|
19710
19784
|
const configPath = tryFindConfigFile();
|
|
19711
19785
|
const { check: validityCheck, config: cfg } = checkConfigValidity(configPath);
|
|
19712
|
-
const configChecks = [
|
|
19786
|
+
const configChecks = [
|
|
19787
|
+
checkConfigDiscovery(),
|
|
19788
|
+
validityCheck,
|
|
19789
|
+
checkSlugCollisions(cfg)
|
|
19790
|
+
];
|
|
19713
19791
|
const apiKeyCheck = checkApiKey(cfg);
|
|
19714
19792
|
const upstreamCheck = await checkUpstream(cfg, timeoutMs, offline);
|
|
19715
19793
|
const portCheck = cfg ? await checkPort(cfg.host, cfg.port) : {
|
|
@@ -19835,8 +19913,11 @@ var FileWatchingConfigSource = class {
|
|
|
19835
19913
|
loading = false;
|
|
19836
19914
|
pending = false;
|
|
19837
19915
|
watching = false;
|
|
19916
|
+
lastCollisionSig = "";
|
|
19917
|
+
listeners = [];
|
|
19838
19918
|
constructor(options) {
|
|
19839
19919
|
this.current = options.initial;
|
|
19920
|
+
this.warnSlugCollisions(options.initial.modelOverrides);
|
|
19840
19921
|
this.loadOptions = options.loadOptions;
|
|
19841
19922
|
this.load = options.load ?? loadConfig;
|
|
19842
19923
|
this.pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
@@ -19848,6 +19929,32 @@ var FileWatchingConfigSource = class {
|
|
|
19848
19929
|
get() {
|
|
19849
19930
|
return this.current;
|
|
19850
19931
|
}
|
|
19932
|
+
subscribe(listener) {
|
|
19933
|
+
this.listeners.push(listener);
|
|
19934
|
+
return () => {
|
|
19935
|
+
const i = this.listeners.indexOf(listener);
|
|
19936
|
+
if (i !== -1) this.listeners.splice(i, 1);
|
|
19937
|
+
};
|
|
19938
|
+
}
|
|
19939
|
+
/** Notify subscribers after a successful reload. A throwing subscriber
|
|
19940
|
+
* must not abort the reload or skip the remaining subscribers. */
|
|
19941
|
+
notify(config) {
|
|
19942
|
+
for (const listener of this.listeners) try {
|
|
19943
|
+
listener(config);
|
|
19944
|
+
} catch {}
|
|
19945
|
+
}
|
|
19946
|
+
/** Warn only when the collision set changes, so reloading an unchanged config doesn't re-log. */
|
|
19947
|
+
warnSlugCollisions(overrides) {
|
|
19948
|
+
const collisions = detectSlugCollisions(overrides ?? void 0);
|
|
19949
|
+
const sig = collisions.map((c) => [
|
|
19950
|
+
c.slug,
|
|
19951
|
+
c.winner,
|
|
19952
|
+
[...c.keys].sort((a, b) => a.localeCompare(b)).join(",")
|
|
19953
|
+
].join("|")).sort((a, b) => a.localeCompare(b)).join("||");
|
|
19954
|
+
if (sig === this.lastCollisionSig) return;
|
|
19955
|
+
this.lastCollisionSig = sig;
|
|
19956
|
+
for (const collision of collisions) logger.warn(formatSlugCollisionWarning(collision));
|
|
19957
|
+
}
|
|
19851
19958
|
async reload() {
|
|
19852
19959
|
if (this.loading) {
|
|
19853
19960
|
this.pending = true;
|
|
@@ -19864,8 +19971,10 @@ var FileWatchingConfigSource = class {
|
|
|
19864
19971
|
diff = "";
|
|
19865
19972
|
}
|
|
19866
19973
|
this.current = next;
|
|
19974
|
+
this.warnSlugCollisions(next.modelOverrides);
|
|
19867
19975
|
if (restartNeeded) logger.warn("host/port changed — restart proxitor to apply (live reload does not re-bind the socket)");
|
|
19868
19976
|
logger.info(`Config reloaded${diff ? ` — ${diff}` : " (no material changes)"}`);
|
|
19977
|
+
this.notify(next);
|
|
19869
19978
|
return { ok: true };
|
|
19870
19979
|
} catch (error) {
|
|
19871
19980
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -22702,6 +22811,12 @@ function withJsonContentType(headers) {
|
|
|
22702
22811
|
"content-type": "application/json"
|
|
22703
22812
|
};
|
|
22704
22813
|
}
|
|
22814
|
+
function withRouterMetadata(headers, enabled) {
|
|
22815
|
+
return enabled ? {
|
|
22816
|
+
...headers,
|
|
22817
|
+
"x-openrouter-metadata": "enabled"
|
|
22818
|
+
} : headers;
|
|
22819
|
+
}
|
|
22705
22820
|
const buildUpstreamReq = createMiddleware(async (c, next) => {
|
|
22706
22821
|
c.set("forwardBody", resolveForwardBody({
|
|
22707
22822
|
reqId: c.var.reqId,
|
|
@@ -22716,6 +22831,7 @@ const buildUpstreamReq = createMiddleware(async (c, next) => {
|
|
|
22716
22831
|
});
|
|
22717
22832
|
if (c.var.effectiveSessionId !== void 0) headers = withSessionId(headers, c.var.effectiveSessionId);
|
|
22718
22833
|
if (c.var.bodyMutated) headers = withJsonContentType(headers);
|
|
22834
|
+
headers = withRouterMetadata(headers, c.var.config.observability.routerMetadata);
|
|
22719
22835
|
c.set("upstreamHeaders", headers);
|
|
22720
22836
|
await next();
|
|
22721
22837
|
});
|
|
@@ -22761,9 +22877,10 @@ function parseForwardBody(body) {
|
|
|
22761
22877
|
return null;
|
|
22762
22878
|
}
|
|
22763
22879
|
}
|
|
22764
|
-
/** `response` is filled later by dumpResponse
|
|
22880
|
+
/** Writes the request half; `response` is filled later by dumpResponse. Returns
|
|
22881
|
+
* the file path so the response half can find it without a reqId→path lookup. */
|
|
22765
22882
|
function dumpRequest(meta) {
|
|
22766
|
-
if (!dumpEnabled()) return;
|
|
22883
|
+
if (!dumpEnabled()) return void 0;
|
|
22767
22884
|
ensureDir(dumpDir());
|
|
22768
22885
|
const record = {
|
|
22769
22886
|
reqId: meta.reqId,
|
|
@@ -22774,156 +22891,313 @@ function dumpRequest(meta) {
|
|
|
22774
22891
|
request: parseForwardBody(meta.forwardBody),
|
|
22775
22892
|
response: null
|
|
22776
22893
|
};
|
|
22777
|
-
|
|
22894
|
+
const path = filePath(meta.reqId, meta.model);
|
|
22895
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`);
|
|
22896
|
+
return path;
|
|
22778
22897
|
}
|
|
22779
|
-
function dumpResponse(
|
|
22898
|
+
async function dumpResponse(obs) {
|
|
22780
22899
|
if (!dumpEnabled()) return;
|
|
22781
|
-
const
|
|
22782
|
-
if (
|
|
22783
|
-
const name = readdirSync(dir).find((f) => f.endsWith(`${reqId}.json`));
|
|
22784
|
-
if (!name) return;
|
|
22785
|
-
const path = join(dir, name);
|
|
22900
|
+
const path = obs.dumpPath;
|
|
22901
|
+
if (path === void 0) return;
|
|
22786
22902
|
try {
|
|
22787
|
-
const record = JSON.parse(
|
|
22788
|
-
const
|
|
22903
|
+
const record = JSON.parse(await readFile(path, "utf-8"));
|
|
22904
|
+
const r = obs.routing;
|
|
22789
22905
|
record.response = {
|
|
22790
|
-
status,
|
|
22791
|
-
|
|
22792
|
-
|
|
22793
|
-
|
|
22794
|
-
|
|
22906
|
+
status: obs.status,
|
|
22907
|
+
label: obs.outcome.label,
|
|
22908
|
+
requestType: obs.requestType,
|
|
22909
|
+
model: obs.model,
|
|
22910
|
+
sessionId: obs.sessionId ?? null,
|
|
22911
|
+
toolsCount: obs.toolsCount,
|
|
22912
|
+
inputTokens: obs.usage.inputTokens,
|
|
22913
|
+
cacheRead: obs.usage.cacheRead,
|
|
22914
|
+
cacheCreate: obs.usage.cacheCreate,
|
|
22915
|
+
hitPct: obs.outcome.hitPct,
|
|
22916
|
+
...r ? {
|
|
22917
|
+
provider: r.provider,
|
|
22918
|
+
strategy: r.strategy,
|
|
22919
|
+
region: r.region ?? null,
|
|
22920
|
+
attempt: r.attempt,
|
|
22921
|
+
fallback: r.fallback,
|
|
22922
|
+
generationId: r.generationId ?? null
|
|
22923
|
+
} : {
|
|
22924
|
+
provider: null,
|
|
22925
|
+
strategy: null,
|
|
22926
|
+
region: null,
|
|
22927
|
+
attempt: null,
|
|
22928
|
+
fallback: false,
|
|
22929
|
+
generationId: null
|
|
22930
|
+
}
|
|
22795
22931
|
};
|
|
22796
|
-
|
|
22932
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`);
|
|
22797
22933
|
} catch {}
|
|
22798
22934
|
}
|
|
22799
22935
|
//#endregion
|
|
22800
|
-
//#region src/proxy/
|
|
22801
|
-
function
|
|
22802
|
-
|
|
22803
|
-
|
|
22804
|
-
|
|
22805
|
-
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
if (typeof
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
22826
|
-
|
|
22827
|
-
|
|
22828
|
-
|
|
22829
|
-
|
|
22830
|
-
|
|
22936
|
+
//#region src/proxy/observability/extract.ts
|
|
22937
|
+
function num(v) {
|
|
22938
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
22939
|
+
}
|
|
22940
|
+
/** First non-empty candidate — the generation id may live at the root or under message/response. */
|
|
22941
|
+
function firstStringId(vals) {
|
|
22942
|
+
for (const v of vals) if (typeof v === "string" && v.length > 0) return v;
|
|
22943
|
+
}
|
|
22944
|
+
function applyAnthropic(usage, r, cr, cc) {
|
|
22945
|
+
if (cr !== void 0) r.cacheRead = cr;
|
|
22946
|
+
if (cc !== void 0) r.cacheCreate = cc;
|
|
22947
|
+
const inp = num(usage.input_tokens);
|
|
22948
|
+
if (inp !== void 0) r.inputTokens = inp + r.cacheRead + r.cacheCreate;
|
|
22949
|
+
}
|
|
22950
|
+
function applyOpenAI(usage, r) {
|
|
22951
|
+
const details = usage.prompt_tokens_details ?? usage.input_tokens_details;
|
|
22952
|
+
if (details && typeof details === "object") {
|
|
22953
|
+
const cached = num(details.cached_tokens);
|
|
22954
|
+
if (cached !== void 0) r.cacheRead = cached;
|
|
22955
|
+
const write = num(details.cache_write_tokens);
|
|
22956
|
+
if (write !== void 0) r.cacheCreate = write;
|
|
22957
|
+
}
|
|
22958
|
+
const prompt = num(usage.prompt_tokens) ?? num(usage.input_tokens);
|
|
22959
|
+
if (prompt !== void 0) r.inputTokens = prompt;
|
|
22960
|
+
}
|
|
22961
|
+
/** Find the usage object, unwrapping Anthropic `message`/`response` SSE containers. */
|
|
22962
|
+
function usageObject(parsed) {
|
|
22963
|
+
for (const c of [
|
|
22964
|
+
parsed.message,
|
|
22965
|
+
parsed.response,
|
|
22966
|
+
parsed
|
|
22967
|
+
]) if (c && typeof c === "object" && "usage" in c) {
|
|
22968
|
+
const u = c.usage;
|
|
22969
|
+
if (u && typeof u === "object") return u;
|
|
22970
|
+
}
|
|
22971
|
+
}
|
|
22972
|
+
function parseUsage(parsed) {
|
|
22973
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
22974
|
+
const usage = usageObject(parsed);
|
|
22975
|
+
if (usage === void 0) return void 0;
|
|
22976
|
+
const r = {
|
|
22977
|
+
present: true,
|
|
22978
|
+
inputTokens: 0,
|
|
22979
|
+
cacheRead: 0,
|
|
22980
|
+
cacheCreate: 0
|
|
22981
|
+
};
|
|
22982
|
+
const cr = num(usage.cache_read_input_tokens);
|
|
22983
|
+
const cc = num(usage.cache_creation_input_tokens);
|
|
22984
|
+
if (cr !== void 0 || cc !== void 0) applyAnthropic(usage, r, cr, cc);
|
|
22985
|
+
else applyOpenAI(usage, r);
|
|
22986
|
+
return r;
|
|
22987
|
+
}
|
|
22988
|
+
/** Provider from endpoints[].available (selected first), then the last attempt; '' is absent. */
|
|
22989
|
+
function resolveProvider(meta) {
|
|
22990
|
+
const avail = meta.endpoints?.available;
|
|
22991
|
+
if (Array.isArray(avail)) {
|
|
22992
|
+
const selected = avail.find((e) => e.selected === true);
|
|
22993
|
+
const selProvider = selected ? selected.provider : avail[0]?.provider;
|
|
22994
|
+
if (selProvider) return selProvider;
|
|
22995
|
+
}
|
|
22996
|
+
const attempts = meta.attempts;
|
|
22997
|
+
if (Array.isArray(attempts)) {
|
|
22998
|
+
const last = attempts.at(-1)?.provider;
|
|
22999
|
+
if (last) return last;
|
|
23000
|
+
}
|
|
23001
|
+
}
|
|
23002
|
+
function parseRouting(parsed) {
|
|
23003
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
23004
|
+
const root = parsed;
|
|
23005
|
+
const meta = root.openrouter_metadata ?? root.message?.openrouter_metadata ?? root.response?.openrouter_metadata;
|
|
23006
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
23007
|
+
const provider = resolveProvider(meta);
|
|
23008
|
+
if (!provider) return void 0;
|
|
23009
|
+
const attempt = num(meta.attempt) ?? 1;
|
|
23010
|
+
const generationId = firstStringId([
|
|
23011
|
+
root.id,
|
|
23012
|
+
root.message?.id,
|
|
23013
|
+
root.response?.id
|
|
23014
|
+
]);
|
|
23015
|
+
return {
|
|
23016
|
+
provider,
|
|
23017
|
+
strategy: typeof meta.strategy === "string" ? meta.strategy : "unknown",
|
|
23018
|
+
region: typeof meta.region === "string" ? meta.region : void 0,
|
|
23019
|
+
attempt,
|
|
23020
|
+
fallback: attempt > 1,
|
|
23021
|
+
generationId
|
|
23022
|
+
};
|
|
22831
23023
|
}
|
|
22832
|
-
|
|
22833
|
-
|
|
23024
|
+
function fold(parsed, acc) {
|
|
23025
|
+
const u = parseUsage(parsed);
|
|
23026
|
+
if (u) if (!acc.usage) acc.usage = { ...u };
|
|
23027
|
+
else {
|
|
23028
|
+
const a = acc.usage;
|
|
23029
|
+
if (u.cacheRead > 0) a.cacheRead = u.cacheRead;
|
|
23030
|
+
if (u.cacheCreate > 0) a.cacheCreate = u.cacheCreate;
|
|
23031
|
+
if (u.inputTokens > 0) a.inputTokens = u.inputTokens;
|
|
23032
|
+
}
|
|
23033
|
+
const rt = parseRouting(parsed);
|
|
23034
|
+
if (rt) acc.routing = rt;
|
|
23035
|
+
}
|
|
23036
|
+
/** Strip a leading UTF-8 BOM (U+FEFF) — trim() no longer removes it (ES2019). */
|
|
23037
|
+
function stripBom(s) {
|
|
23038
|
+
return s.charCodeAt(0) === 65279 ? s.slice(1) : s;
|
|
23039
|
+
}
|
|
23040
|
+
/** Parse one SSE `data:` line into the accumulator. Shared by the stateless and streaming folds. */
|
|
23041
|
+
function parseDataLine(line, acc) {
|
|
23042
|
+
const trimmed = stripBom(line).trim();
|
|
23043
|
+
if (!trimmed.startsWith("data:")) return;
|
|
23044
|
+
const payload = trimmed.slice(5).trim();
|
|
23045
|
+
if (payload === "" || payload === "[DONE]") return;
|
|
22834
23046
|
try {
|
|
22835
|
-
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
|
|
23047
|
+
fold(JSON.parse(payload), acc);
|
|
23048
|
+
} catch {}
|
|
23049
|
+
}
|
|
23050
|
+
function extractFromFullText(text, isSSE) {
|
|
23051
|
+
if (!isSSE) try {
|
|
23052
|
+
const parsed = JSON.parse(stripBom(text));
|
|
23053
|
+
return {
|
|
23054
|
+
usage: parseUsage(parsed),
|
|
23055
|
+
routing: parseRouting(parsed)
|
|
22843
23056
|
};
|
|
22844
|
-
extractFromUsage(usage, result);
|
|
22845
|
-
return result;
|
|
22846
23057
|
} catch {
|
|
22847
|
-
return;
|
|
23058
|
+
return {};
|
|
23059
|
+
}
|
|
23060
|
+
const acc = {};
|
|
23061
|
+
for (const line of text.split("\n")) parseDataLine(line, acc);
|
|
23062
|
+
return acc;
|
|
23063
|
+
}
|
|
23064
|
+
/** Stateful fold over an SSE byte stream — O(1) memory, fragmentation-safe. */
|
|
23065
|
+
var SseUsageAccumulator = class {
|
|
23066
|
+
buffer = "";
|
|
23067
|
+
offset = 0;
|
|
23068
|
+
done = false;
|
|
23069
|
+
decoder = new TextDecoder();
|
|
23070
|
+
acc = {};
|
|
23071
|
+
feed(chunk) {
|
|
23072
|
+
if (this.done) return;
|
|
23073
|
+
this.buffer += this.decoder.decode(chunk, { stream: true });
|
|
23074
|
+
let nl = this.buffer.indexOf("\n", this.offset);
|
|
23075
|
+
while (nl !== -1) {
|
|
23076
|
+
this.process(this.buffer.slice(this.offset, nl));
|
|
23077
|
+
this.offset = nl + 1;
|
|
23078
|
+
nl = this.buffer.indexOf("\n", this.offset);
|
|
23079
|
+
}
|
|
23080
|
+
if (this.offset > 0) {
|
|
23081
|
+
this.buffer = this.buffer.slice(this.offset);
|
|
23082
|
+
this.offset = 0;
|
|
23083
|
+
}
|
|
22848
23084
|
}
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
22854
|
-
|
|
22855
|
-
|
|
22856
|
-
|
|
22857
|
-
return result.cacheRead + result.cacheCreate > before;
|
|
22858
|
-
}
|
|
22859
|
-
/** @internal */
|
|
22860
|
-
function extractCacheUsageFromSSE(fullText) {
|
|
22861
|
-
const result = {
|
|
22862
|
-
cacheRead: 0,
|
|
22863
|
-
cacheCreate: 0,
|
|
22864
|
-
inputTokens: 0
|
|
22865
|
-
};
|
|
22866
|
-
let found = false;
|
|
22867
|
-
for (const line of fullText.split("\n")) {
|
|
22868
|
-
if (!line.startsWith("data:")) continue;
|
|
22869
|
-
const payload = line.slice(5).trim();
|
|
22870
|
-
if (payload === "[DONE]") continue;
|
|
22871
|
-
try {
|
|
22872
|
-
if (extractFromEvent(JSON.parse(payload), result)) found = true;
|
|
22873
|
-
} catch {}
|
|
23085
|
+
result() {
|
|
23086
|
+
if (this.done) return this.acc;
|
|
23087
|
+
this.done = true;
|
|
23088
|
+
this.buffer += this.decoder.decode();
|
|
23089
|
+
if (this.buffer.length > 0) this.process(this.buffer.slice(this.offset));
|
|
23090
|
+
this.buffer = "";
|
|
23091
|
+
this.offset = 0;
|
|
23092
|
+
return this.acc;
|
|
22874
23093
|
}
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22878
|
-
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
const
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
23094
|
+
process(line) {
|
|
23095
|
+
parseDataLine(line, this.acc);
|
|
23096
|
+
}
|
|
23097
|
+
};
|
|
23098
|
+
//#endregion
|
|
23099
|
+
//#region src/proxy/cache-logging.ts
|
|
23100
|
+
/**
|
|
23101
|
+
* Wraps the upstream body so usage/routing is observed once on ANY termination
|
|
23102
|
+
* path (clean close, client cancel, upstream error) via finalize() — a bare
|
|
23103
|
+
* TransformStream skips flush() on upstream errors, orphaning the dump.
|
|
23104
|
+
*/
|
|
23105
|
+
function createLoggingStream(source, contentType, status, ctx) {
|
|
23106
|
+
const accumulator = contentType.toLowerCase().includes("text/event-stream") ? new SseUsageAccumulator() : void 0;
|
|
23107
|
+
const decoder = new TextDecoder();
|
|
23108
|
+
const chunks = [];
|
|
23109
|
+
let finalized = false;
|
|
23110
|
+
const finalize = () => {
|
|
23111
|
+
if (finalized) return;
|
|
23112
|
+
finalized = true;
|
|
23113
|
+
try {
|
|
23114
|
+
let extracted;
|
|
23115
|
+
if (accumulator) extracted = accumulator.result();
|
|
22893
23116
|
else {
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
23117
|
+
let text = "";
|
|
23118
|
+
for (const c of chunks) text += decoder.decode(c, { stream: true });
|
|
23119
|
+
text += decoder.decode();
|
|
23120
|
+
extracted = extractFromFullText(text, false);
|
|
22897
23121
|
}
|
|
22898
|
-
|
|
22899
|
-
|
|
23122
|
+
ctx.observability.observe(ctx.reqCtx, extracted, status);
|
|
23123
|
+
} catch (err) {
|
|
23124
|
+
logger.debug(withReq(ctx.reqCtx.reqId, `Cache observability failed: ${err instanceof Error ? err.message : err}`));
|
|
23125
|
+
}
|
|
23126
|
+
};
|
|
23127
|
+
const reader = source.getReader();
|
|
23128
|
+
return new ReadableStream({
|
|
23129
|
+
async pull(controller) {
|
|
22900
23130
|
try {
|
|
22901
|
-
const
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
23131
|
+
const { done, value } = await reader.read();
|
|
23132
|
+
if (done) {
|
|
23133
|
+
finalize();
|
|
23134
|
+
controller.close();
|
|
23135
|
+
return;
|
|
23136
|
+
}
|
|
23137
|
+
if (value) {
|
|
23138
|
+
if (accumulator) accumulator.feed(value);
|
|
23139
|
+
else chunks.push(value);
|
|
23140
|
+
controller.enqueue(value);
|
|
23141
|
+
}
|
|
22906
23142
|
} catch (err) {
|
|
22907
|
-
|
|
23143
|
+
finalize();
|
|
23144
|
+
controller.error(err);
|
|
22908
23145
|
}
|
|
23146
|
+
},
|
|
23147
|
+
cancel() {
|
|
23148
|
+
finalize();
|
|
23149
|
+
reader.cancel().catch(() => {});
|
|
22909
23150
|
}
|
|
22910
23151
|
});
|
|
22911
23152
|
}
|
|
22912
|
-
function buildUpstreamResponseWithLogging(upstream, method,
|
|
23153
|
+
function buildUpstreamResponseWithLogging(upstream, method, ctx) {
|
|
22913
23154
|
const headers = buildResponseHeaders(upstream.headers);
|
|
22914
|
-
if (method === "HEAD" || !upstream.body)
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
23155
|
+
if (method === "HEAD" || !upstream.body) {
|
|
23156
|
+
ctx.observability.observe(ctx.reqCtx, {}, upstream.status);
|
|
23157
|
+
return new Response(null, {
|
|
23158
|
+
status: upstream.status,
|
|
23159
|
+
headers
|
|
23160
|
+
});
|
|
23161
|
+
}
|
|
22918
23162
|
const contentType = upstream.headers.get("content-type") ?? "";
|
|
22919
23163
|
const lower = contentType.toLowerCase();
|
|
22920
|
-
|
|
23164
|
+
if (!(lower.includes("application/json") || lower.includes("text/event-stream"))) {
|
|
23165
|
+
ctx.observability.observe(ctx.reqCtx, {}, upstream.status);
|
|
23166
|
+
return new Response(upstream.body, {
|
|
23167
|
+
status: upstream.status,
|
|
23168
|
+
headers
|
|
23169
|
+
});
|
|
23170
|
+
}
|
|
23171
|
+
const body = createLoggingStream(upstream.body, contentType, upstream.status, ctx);
|
|
22921
23172
|
return new Response(body, {
|
|
22922
23173
|
status: upstream.status,
|
|
22923
23174
|
headers
|
|
22924
23175
|
});
|
|
22925
23176
|
}
|
|
22926
23177
|
//#endregion
|
|
23178
|
+
//#region src/proxy/observability/classify.ts
|
|
23179
|
+
function classifyRequestType(ctx, opts) {
|
|
23180
|
+
const budget = ctx.maxTokens === void 0 || ctx.maxTokens <= 0 ? Number.POSITIVE_INFINITY : ctx.maxTokens;
|
|
23181
|
+
return ctx.toolsCount === 0 && budget <= opts.sideMaxTokens ? "side" : "main";
|
|
23182
|
+
}
|
|
23183
|
+
function classifyCacheOutcome(usage, ctx, opts) {
|
|
23184
|
+
if (!usage.present) return {
|
|
23185
|
+
label: "NOUSAGE",
|
|
23186
|
+
type: ctx.requestType,
|
|
23187
|
+
hitPct: 0
|
|
23188
|
+
};
|
|
23189
|
+
const raw = usage.inputTokens > 0 ? usage.cacheRead / usage.inputTokens * 100 : 0;
|
|
23190
|
+
const hitPct = Math.min(100, raw);
|
|
23191
|
+
let label;
|
|
23192
|
+
if (usage.cacheRead === 0) label = ctx.isFirstForSession ? "COLD" : "MISS";
|
|
23193
|
+
else label = hitPct >= opts.hitThresholdPct ? "HIT" : "PARTIAL";
|
|
23194
|
+
return {
|
|
23195
|
+
label,
|
|
23196
|
+
type: ctx.requestType,
|
|
23197
|
+
hitPct: Number(hitPct.toFixed(1))
|
|
23198
|
+
};
|
|
23199
|
+
}
|
|
23200
|
+
//#endregion
|
|
22927
23201
|
//#region src/proxy/utils/error.ts
|
|
22928
23202
|
function formatMetadata(meta) {
|
|
22929
23203
|
const parts = [];
|
|
@@ -22954,7 +23228,7 @@ function extractErrorDetail(bodyText) {
|
|
|
22954
23228
|
//#endregion
|
|
22955
23229
|
//#region src/proxy/middleware/forward-request.ts
|
|
22956
23230
|
const DUPLEX_HALF = { duplex: "half" };
|
|
22957
|
-
function buildErrorResponse(err, ctx) {
|
|
23231
|
+
function buildErrorResponse(err, ctx, observeUnhandled) {
|
|
22958
23232
|
if (err instanceof TypeError) {
|
|
22959
23233
|
logger.error(withReq(ctx.reqId, "Upstream fetch error:"), err);
|
|
22960
23234
|
return Response.json({ error: {
|
|
@@ -22966,6 +23240,7 @@ function buildErrorResponse(err, ctx) {
|
|
|
22966
23240
|
logger.warn(withReq(ctx.reqId, `Aborted: ${ctx.method} ${ctx.path}`));
|
|
22967
23241
|
return new Response(null, { status: 499 });
|
|
22968
23242
|
}
|
|
23243
|
+
observeUnhandled?.();
|
|
22969
23244
|
throw err;
|
|
22970
23245
|
}
|
|
22971
23246
|
async function buildUpstreamErrorResponse(upstream, ctx) {
|
|
@@ -22974,14 +23249,16 @@ async function buildUpstreamErrorResponse(upstream, ctx) {
|
|
|
22974
23249
|
const truncated = detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
|
|
22975
23250
|
(upstream.status >= 500 ? logger.error : logger.warn)(withReq(ctx.reqId, `${ctx.method} ${ctx.path} ← ${upstream.status} (${Date.now() - ctx.startedAt}ms): ${truncated}`));
|
|
22976
23251
|
const responseHeaders = buildResponseHeaders(upstream.headers);
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
23252
|
+
return {
|
|
23253
|
+
response: ctx.method === "HEAD" ? new Response(null, {
|
|
23254
|
+
status: upstream.status,
|
|
23255
|
+
headers: responseHeaders
|
|
23256
|
+
}) : new Response(bodyText, {
|
|
23257
|
+
status: upstream.status,
|
|
23258
|
+
headers: responseHeaders
|
|
23259
|
+
}),
|
|
23260
|
+
extracted: extractFromFullText(bodyText, false)
|
|
23261
|
+
};
|
|
22985
23262
|
}
|
|
22986
23263
|
const forwardRequest = createMiddleware(async (c) => {
|
|
22987
23264
|
const { upstreamUrl, forwardBody, upstreamHeaders, reqId, path, startedAt, method } = c.var;
|
|
@@ -22998,13 +23275,30 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
22998
23275
|
const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
|
|
22999
23276
|
const modelLog = c.var.modelName ? ` model=${c.var.modelName}` : "";
|
|
23000
23277
|
logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${modelLog}`));
|
|
23001
|
-
|
|
23278
|
+
const dumpPath = dumpEnabled() ? dumpRequest({
|
|
23002
23279
|
reqId,
|
|
23003
23280
|
method,
|
|
23004
23281
|
path,
|
|
23005
23282
|
model: c.var.modelName,
|
|
23006
23283
|
forwardBody
|
|
23007
|
-
});
|
|
23284
|
+
}) : void 0;
|
|
23285
|
+
const parsedBody = c.var.parsedBody;
|
|
23286
|
+
const toolsCount = Array.isArray(parsedBody?.tools) ? parsedBody.tools.length : 0;
|
|
23287
|
+
const maxTokens = parsedBody?.max_tokens ?? parsedBody?.max_completion_tokens ?? parsedBody?.max_output_tokens;
|
|
23288
|
+
const requestType = classifyRequestType({
|
|
23289
|
+
toolsCount,
|
|
23290
|
+
maxTokens
|
|
23291
|
+
}, { sideMaxTokens: c.var.config.observability.sideMaxTokens });
|
|
23292
|
+
const reqCtx = {
|
|
23293
|
+
reqId,
|
|
23294
|
+
model: c.var.modelName ?? "",
|
|
23295
|
+
sessionId: c.var.effectiveSessionId,
|
|
23296
|
+
toolsCount,
|
|
23297
|
+
maxTokens,
|
|
23298
|
+
requestType,
|
|
23299
|
+
dumpPath
|
|
23300
|
+
};
|
|
23301
|
+
const observability = c.var.observability;
|
|
23008
23302
|
let upstream;
|
|
23009
23303
|
try {
|
|
23010
23304
|
upstream = await fetch(upstreamUrl, {
|
|
@@ -23015,13 +23309,32 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
23015
23309
|
...forwardBody ? DUPLEX_HALF : {}
|
|
23016
23310
|
});
|
|
23017
23311
|
} catch (err) {
|
|
23018
|
-
|
|
23312
|
+
const response = buildErrorResponse(err, ctx, () => observability.observe(reqCtx, {}, 500));
|
|
23313
|
+
observability.observe(reqCtx, {}, response.status);
|
|
23314
|
+
return response;
|
|
23019
23315
|
} finally {
|
|
23020
23316
|
c.req.raw.signal.removeEventListener("abort", onClientAbort);
|
|
23021
23317
|
}
|
|
23022
|
-
if (upstream.status >= 400)
|
|
23318
|
+
if (upstream.status >= 400) {
|
|
23319
|
+
let extracted = {};
|
|
23320
|
+
let response;
|
|
23321
|
+
try {
|
|
23322
|
+
({response, extracted} = await buildUpstreamErrorResponse(upstream, ctx));
|
|
23323
|
+
} catch {
|
|
23324
|
+
logger.debug(withReq(reqId, `upstream ${upstream.status} error body unreadable; observing without detail`));
|
|
23325
|
+
response = new Response(null, {
|
|
23326
|
+
status: upstream.status,
|
|
23327
|
+
headers: buildResponseHeaders(upstream.headers)
|
|
23328
|
+
});
|
|
23329
|
+
}
|
|
23330
|
+
observability.observe(reqCtx, extracted, upstream.status);
|
|
23331
|
+
return response;
|
|
23332
|
+
}
|
|
23023
23333
|
logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
|
|
23024
|
-
return buildUpstreamResponseWithLogging(upstream, method,
|
|
23334
|
+
return buildUpstreamResponseWithLogging(upstream, method, {
|
|
23335
|
+
reqCtx,
|
|
23336
|
+
observability
|
|
23337
|
+
});
|
|
23025
23338
|
});
|
|
23026
23339
|
//#endregion
|
|
23027
23340
|
//#region src/proxy/paths.ts
|
|
@@ -23340,6 +23653,7 @@ const readBody = createMiddleware(async (c, next) => {
|
|
|
23340
23653
|
const resolveConfig = createMiddleware(async (c, next) => {
|
|
23341
23654
|
const resolved = resolveModelConfig(c.var.config, c.var.modelName);
|
|
23342
23655
|
c.set("resolvedConfig", resolved);
|
|
23656
|
+
if (c.var.config.verbose && c.var.modelName) logger.info(resolved.matchedOverride ? `override "${resolved.matchedOverride}" matched incoming "${c.var.modelName}"` : `no override matched for "${c.var.modelName}"`);
|
|
23343
23657
|
await next();
|
|
23344
23658
|
});
|
|
23345
23659
|
//#endregion
|
|
@@ -23357,6 +23671,172 @@ const setupRequest = createMiddleware(async (c, next) => {
|
|
|
23357
23671
|
await next();
|
|
23358
23672
|
});
|
|
23359
23673
|
//#endregion
|
|
23674
|
+
//#region src/proxy/observability/session-tracker.ts
|
|
23675
|
+
var SessionTracker = class {
|
|
23676
|
+
seen = /* @__PURE__ */ new Map();
|
|
23677
|
+
constructor(opts, now = Date.now) {
|
|
23678
|
+
this.opts = opts;
|
|
23679
|
+
this.now = now;
|
|
23680
|
+
}
|
|
23681
|
+
opts;
|
|
23682
|
+
now;
|
|
23683
|
+
isFirstAndRemember(sessionId) {
|
|
23684
|
+
if (sessionId === void 0) return true;
|
|
23685
|
+
const t = this.now();
|
|
23686
|
+
const prev = this.seen.get(sessionId);
|
|
23687
|
+
const fresh = prev !== void 0 && t - prev < this.opts.ttlMs;
|
|
23688
|
+
if (prev !== void 0) this.seen.delete(sessionId);
|
|
23689
|
+
else if (this.seen.size >= this.opts.maxEntries) this.evictOne();
|
|
23690
|
+
this.seen.set(sessionId, t);
|
|
23691
|
+
return !fresh;
|
|
23692
|
+
}
|
|
23693
|
+
/** Drop the LRU entry (first by Map order). A refresh moves the key to the
|
|
23694
|
+
* end, so iteration order is lastSeen order — the first entry is most stale. */
|
|
23695
|
+
evictOne() {
|
|
23696
|
+
const oldest = this.seen.keys().next().value;
|
|
23697
|
+
if (oldest !== void 0) this.seen.delete(oldest);
|
|
23698
|
+
}
|
|
23699
|
+
/** Apply a reloaded capacity/TTL without discarding remembered sessions —
|
|
23700
|
+
* shrinking evicts LRU entries, growing is a no-op, TTL applies on the next
|
|
23701
|
+
* freshness check. Rebuilding would wipe the map and misclassify as COLD. */
|
|
23702
|
+
applyConfig(opts) {
|
|
23703
|
+
this.opts = opts;
|
|
23704
|
+
while (this.seen.size > opts.maxEntries) this.evictOne();
|
|
23705
|
+
}
|
|
23706
|
+
};
|
|
23707
|
+
//#endregion
|
|
23708
|
+
//#region src/proxy/observability/sinks.ts
|
|
23709
|
+
const wrap = (code) => (s) => `\x1b[${code}m${s}\x1b[0m`;
|
|
23710
|
+
const PAINT = {
|
|
23711
|
+
HIT: wrap("32"),
|
|
23712
|
+
PARTIAL: wrap("33"),
|
|
23713
|
+
MISS: wrap("31"),
|
|
23714
|
+
COLD: wrap("2"),
|
|
23715
|
+
NOUSAGE: wrap("90")
|
|
23716
|
+
};
|
|
23717
|
+
function colorizeLabel(label, useColor) {
|
|
23718
|
+
return useColor ? PAINT[label](label) : label;
|
|
23719
|
+
}
|
|
23720
|
+
function formatLine(obs, useColor = false) {
|
|
23721
|
+
const { label, hitPct } = obs.outcome;
|
|
23722
|
+
const parts = [colorizeLabel(label, useColor)];
|
|
23723
|
+
if (label === "HIT" || label === "PARTIAL") parts.push(`${hitPct.toFixed(0)}%`);
|
|
23724
|
+
if (obs.usage.cacheRead > 0) parts.push(`read ${obs.usage.cacheRead}`);
|
|
23725
|
+
if (obs.usage.cacheCreate > 0) parts.push(`write ${obs.usage.cacheCreate}`);
|
|
23726
|
+
if (obs.usage.present) parts.push(`in ${obs.usage.inputTokens}`);
|
|
23727
|
+
if (obs.routing) parts.push(`provider=${obs.routing.provider}`);
|
|
23728
|
+
if (obs.model) parts.push(obs.model);
|
|
23729
|
+
parts.push(`[${obs.requestType}]`);
|
|
23730
|
+
return withReq(obs.reqId, parts.join(" "));
|
|
23731
|
+
}
|
|
23732
|
+
var LiveLineSink = class {
|
|
23733
|
+
useColor;
|
|
23734
|
+
constructor(useColor = () => process.stdout.isTTY === true) {
|
|
23735
|
+
this.useColor = useColor;
|
|
23736
|
+
}
|
|
23737
|
+
emit(obs) {
|
|
23738
|
+
logger.info(formatLine(obs, this.useColor()));
|
|
23739
|
+
}
|
|
23740
|
+
};
|
|
23741
|
+
var DumpSink = class {
|
|
23742
|
+
inflight = 0;
|
|
23743
|
+
waiters = [];
|
|
23744
|
+
maxConcurrent;
|
|
23745
|
+
maxWaiters;
|
|
23746
|
+
dump;
|
|
23747
|
+
enabled;
|
|
23748
|
+
constructor(deps = {}) {
|
|
23749
|
+
this.maxConcurrent = deps.maxConcurrent ?? 16;
|
|
23750
|
+
this.maxWaiters = deps.maxWaiters ?? 256;
|
|
23751
|
+
this.dump = deps.dump ?? dumpResponse;
|
|
23752
|
+
this.enabled = deps.enabled ?? dumpEnabled;
|
|
23753
|
+
}
|
|
23754
|
+
emit(obs) {
|
|
23755
|
+
if (!this.enabled()) return;
|
|
23756
|
+
const run = () => {
|
|
23757
|
+
this.inflight += 1;
|
|
23758
|
+
this.dump(obs).catch((err) => {
|
|
23759
|
+
logger.debug(withReq(obs.reqId, `DumpSink failed: ${err instanceof Error ? err.message : err}`));
|
|
23760
|
+
}).finally(() => {
|
|
23761
|
+
this.inflight -= 1;
|
|
23762
|
+
const next = this.waiters.shift();
|
|
23763
|
+
if (next) next();
|
|
23764
|
+
});
|
|
23765
|
+
};
|
|
23766
|
+
if (this.inflight < this.maxConcurrent) run();
|
|
23767
|
+
else if (this.waiters.length < this.maxWaiters) this.waiters.push(run);
|
|
23768
|
+
else logger.debug(withReq(obs.reqId, "DumpSink queue full — dump dropped"));
|
|
23769
|
+
}
|
|
23770
|
+
};
|
|
23771
|
+
//#endregion
|
|
23772
|
+
//#region src/proxy/observability/observability.ts
|
|
23773
|
+
var Observability = class {
|
|
23774
|
+
tracker;
|
|
23775
|
+
sinks;
|
|
23776
|
+
hitThresholdPct;
|
|
23777
|
+
constructor(tracker, sinks, hitThresholdPct) {
|
|
23778
|
+
this.tracker = tracker;
|
|
23779
|
+
this.sinks = sinks;
|
|
23780
|
+
this.hitThresholdPct = hitThresholdPct;
|
|
23781
|
+
}
|
|
23782
|
+
/** Classify and dispatch one observation to every sink. Never throws: the
|
|
23783
|
+
* response pipeline calls this on many termination paths, so a failure must
|
|
23784
|
+
* degrade to a debug log, not escape to the client. */
|
|
23785
|
+
observe(req, extracted, status) {
|
|
23786
|
+
try {
|
|
23787
|
+
const usage = extracted.usage ?? {
|
|
23788
|
+
present: false,
|
|
23789
|
+
inputTokens: 0,
|
|
23790
|
+
cacheRead: 0,
|
|
23791
|
+
cacheCreate: 0
|
|
23792
|
+
};
|
|
23793
|
+
const isFirst = this.tracker.isFirstAndRemember(req.sessionId);
|
|
23794
|
+
const outcome = classifyCacheOutcome(usage, {
|
|
23795
|
+
requestType: req.requestType,
|
|
23796
|
+
isFirstForSession: isFirst
|
|
23797
|
+
}, { hitThresholdPct: this.hitThresholdPct });
|
|
23798
|
+
const obs = {
|
|
23799
|
+
dumpPath: req.dumpPath,
|
|
23800
|
+
reqId: req.reqId,
|
|
23801
|
+
status,
|
|
23802
|
+
model: req.model,
|
|
23803
|
+
sessionId: req.sessionId,
|
|
23804
|
+
requestType: req.requestType,
|
|
23805
|
+
toolsCount: req.toolsCount,
|
|
23806
|
+
usage,
|
|
23807
|
+
outcome,
|
|
23808
|
+
routing: extracted.routing
|
|
23809
|
+
};
|
|
23810
|
+
for (const sink of this.sinks) try {
|
|
23811
|
+
sink.emit(obs);
|
|
23812
|
+
} catch (err) {
|
|
23813
|
+
logger.debug(withReq(obs.reqId, `Observability sink failed: ${err instanceof Error ? err.message : err}`));
|
|
23814
|
+
}
|
|
23815
|
+
} catch (err) {
|
|
23816
|
+
logger.debug(withReq(req.reqId, `Observability observe failed: ${err instanceof Error ? err.message : err}`));
|
|
23817
|
+
}
|
|
23818
|
+
}
|
|
23819
|
+
/** Apply a hot-reloaded config in place — must not discard remembered
|
|
23820
|
+
* sessions, or an unrelated reload misclassifies the next request as COLD. */
|
|
23821
|
+
reconfigure(config) {
|
|
23822
|
+
const o = config.observability;
|
|
23823
|
+
this.hitThresholdPct = o.hitThreshold;
|
|
23824
|
+
this.tracker.applyConfig({
|
|
23825
|
+
maxEntries: o.sessionMaxEntries,
|
|
23826
|
+
ttlMs: o.sessionTtlMs
|
|
23827
|
+
});
|
|
23828
|
+
}
|
|
23829
|
+
};
|
|
23830
|
+
function createObservability(config, sinks) {
|
|
23831
|
+
const o = config.observability;
|
|
23832
|
+
const tracker = new SessionTracker({
|
|
23833
|
+
maxEntries: o.sessionMaxEntries,
|
|
23834
|
+
ttlMs: o.sessionTtlMs
|
|
23835
|
+
});
|
|
23836
|
+
const built = [new LiveLineSink(), new DumpSink()];
|
|
23837
|
+
return new Observability(tracker, sinks ?? built, o.hitThreshold);
|
|
23838
|
+
}
|
|
23839
|
+
//#endregion
|
|
23360
23840
|
//#region src/proxy.ts
|
|
23361
23841
|
const injectChain = [
|
|
23362
23842
|
parseBody,
|
|
@@ -23368,8 +23848,11 @@ const injectChain = [
|
|
|
23368
23848
|
];
|
|
23369
23849
|
function createProxyServer(source, onReady) {
|
|
23370
23850
|
const app = new Hono();
|
|
23851
|
+
const observability = createObservability(source.get());
|
|
23852
|
+
source.subscribe((config) => observability.reconfigure(config));
|
|
23371
23853
|
app.use("*", async (c, next) => {
|
|
23372
23854
|
c.set("config", source.get());
|
|
23855
|
+
c.set("observability", observability);
|
|
23373
23856
|
await next();
|
|
23374
23857
|
});
|
|
23375
23858
|
app.get("/health", (c) => {
|