proxitor 0.16.0 → 0.18.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 +6 -2
- package/dist/cli.mjs +789 -147
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7,13 +7,14 @@ 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 {
|
|
16
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
18
|
//#region \0rolldown/runtime.js
|
|
18
19
|
var __defProp = Object.defineProperty;
|
|
19
20
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -12216,7 +12217,7 @@ const allowsEval = /* @__PURE__*/ cached(() => {
|
|
|
12216
12217
|
return false;
|
|
12217
12218
|
}
|
|
12218
12219
|
});
|
|
12219
|
-
function isPlainObject$
|
|
12220
|
+
function isPlainObject$3(o) {
|
|
12220
12221
|
if (isObject(o) === false) return false;
|
|
12221
12222
|
const ctor = o.constructor;
|
|
12222
12223
|
if (ctor === void 0) return true;
|
|
@@ -12227,7 +12228,7 @@ function isPlainObject$2(o) {
|
|
|
12227
12228
|
return true;
|
|
12228
12229
|
}
|
|
12229
12230
|
function shallowClone(o) {
|
|
12230
|
-
if (isPlainObject$
|
|
12231
|
+
if (isPlainObject$3(o)) return { ...o };
|
|
12231
12232
|
if (Array.isArray(o)) return [...o];
|
|
12232
12233
|
if (o instanceof Map) return new Map(o);
|
|
12233
12234
|
if (o instanceof Set) return new Set(o);
|
|
@@ -12310,7 +12311,7 @@ function omit(schema, mask) {
|
|
|
12310
12311
|
}));
|
|
12311
12312
|
}
|
|
12312
12313
|
function extend(schema, shape) {
|
|
12313
|
-
if (!isPlainObject$
|
|
12314
|
+
if (!isPlainObject$3(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
12314
12315
|
const checks = schema._zod.def.checks;
|
|
12315
12316
|
if (checks && checks.length > 0) {
|
|
12316
12317
|
const existingShape = schema._zod.def.shape;
|
|
@@ -12326,7 +12327,7 @@ function extend(schema, shape) {
|
|
|
12326
12327
|
} }));
|
|
12327
12328
|
}
|
|
12328
12329
|
function safeExtend(schema, shape) {
|
|
12329
|
-
if (!isPlainObject$
|
|
12330
|
+
if (!isPlainObject$3(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
12330
12331
|
return clone(schema, mergeDefs(schema._zod.def, { get shape() {
|
|
12331
12332
|
const _shape = {
|
|
12332
12333
|
...schema._zod.def.shape,
|
|
@@ -13861,7 +13862,7 @@ function mergeValues(a, b) {
|
|
|
13861
13862
|
valid: true,
|
|
13862
13863
|
data: a
|
|
13863
13864
|
};
|
|
13864
|
-
if (isPlainObject$
|
|
13865
|
+
if (isPlainObject$3(a) && isPlainObject$3(b)) {
|
|
13865
13866
|
const bKeys = Object.keys(b);
|
|
13866
13867
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
13867
13868
|
const newObj = {
|
|
@@ -13937,7 +13938,7 @@ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
|
|
|
13937
13938
|
$ZodType.init(inst, def);
|
|
13938
13939
|
inst._zod.parse = (payload, ctx) => {
|
|
13939
13940
|
const input = payload.value;
|
|
13940
|
-
if (!isPlainObject$
|
|
13941
|
+
if (!isPlainObject$3(input)) {
|
|
13941
13942
|
payload.issues.push({
|
|
13942
13943
|
expected: "record",
|
|
13943
13944
|
code: "invalid_type",
|
|
@@ -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(),
|
|
@@ -16278,6 +16293,7 @@ const modelOverrideSchema = object({
|
|
|
16278
16293
|
cacheControlTtl: ttlSchema.optional(),
|
|
16279
16294
|
rewriteBlockTtl: triStateSchema.optional(),
|
|
16280
16295
|
sessionId: triStateSchema.optional(),
|
|
16296
|
+
normalizeResponses: triStateSchema.optional(),
|
|
16281
16297
|
normalizeVolatileSystem: boolean().optional()
|
|
16282
16298
|
}).strict();
|
|
16283
16299
|
const proxyConfigSchema = object({
|
|
@@ -16297,7 +16313,9 @@ const proxyConfigSchema = object({
|
|
|
16297
16313
|
cacheControlTtl: ttlSchema.optional(),
|
|
16298
16314
|
rewriteBlockTtl: triStateSchema.default("skip"),
|
|
16299
16315
|
sessionId: triStateSchema.default("auto"),
|
|
16316
|
+
normalizeResponses: triStateSchema.default("auto"),
|
|
16300
16317
|
normalizeVolatileSystem: boolean().default(false),
|
|
16318
|
+
observability: observabilityConfigSchema,
|
|
16301
16319
|
modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
|
|
16302
16320
|
}).strict();
|
|
16303
16321
|
const DEFAULTS = proxyConfigSchema.parse({});
|
|
@@ -16472,6 +16490,7 @@ function resolveModelConfig(config, modelName) {
|
|
|
16472
16490
|
cacheControlTtl: config.cacheControlTtl,
|
|
16473
16491
|
rewriteBlockTtl: config.rewriteBlockTtl,
|
|
16474
16492
|
sessionId: config.sessionId,
|
|
16493
|
+
normalizeResponses: config.normalizeResponses,
|
|
16475
16494
|
normalizeVolatileSystem: config.normalizeVolatileSystem
|
|
16476
16495
|
};
|
|
16477
16496
|
if (!modelName || !config.modelOverrides) return result;
|
|
@@ -16505,6 +16524,7 @@ function applyOverride(result, override) {
|
|
|
16505
16524
|
if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
|
|
16506
16525
|
if (override.rewriteBlockTtl !== void 0) result.rewriteBlockTtl = override.rewriteBlockTtl;
|
|
16507
16526
|
if (override.sessionId !== void 0) result.sessionId = override.sessionId;
|
|
16527
|
+
if (override.normalizeResponses !== void 0) result.normalizeResponses = override.normalizeResponses;
|
|
16508
16528
|
if (override.normalizeVolatileSystem !== void 0) result.normalizeVolatileSystem = override.normalizeVolatileSystem;
|
|
16509
16529
|
}
|
|
16510
16530
|
/** Reject base URLs ending in /v1 — paths are forwarded as-is, so /v1 suffix causes doubled paths. */
|
|
@@ -16806,11 +16826,11 @@ function createDefu(merger) {
|
|
|
16806
16826
|
return (...arguments_) => arguments_.reduce((p, c) => _defu(p, c, "", merger), {});
|
|
16807
16827
|
}
|
|
16808
16828
|
const defu = createDefu();
|
|
16809
|
-
function isPlainObject(obj) {
|
|
16829
|
+
function isPlainObject$2(obj) {
|
|
16810
16830
|
return Object.prototype.toString.call(obj) === "[object Object]";
|
|
16811
16831
|
}
|
|
16812
16832
|
function isLogObj(arg) {
|
|
16813
|
-
if (!isPlainObject(arg)) return false;
|
|
16833
|
+
if (!isPlainObject$2(arg)) return false;
|
|
16814
16834
|
if (!arg.message && !arg.args) return false;
|
|
16815
16835
|
if (arg.stack) return false;
|
|
16816
16836
|
return true;
|
|
@@ -18103,6 +18123,12 @@ const REWRITE_HINTS = {
|
|
|
18103
18123
|
always: "All models",
|
|
18104
18124
|
skip: "Leave client block ttl as-is (may mismatch root)"
|
|
18105
18125
|
};
|
|
18126
|
+
/** Shared hint texts for the normalize-responses tri-state — used in the Fixes menu + override editor. */
|
|
18127
|
+
const NORMALIZE_RESPONSES_HINTS = {
|
|
18128
|
+
auto: "/v1/responses only (default)",
|
|
18129
|
+
always: "All endpoints",
|
|
18130
|
+
skip: "Off — raw passthrough (OpenRouter may reject)"
|
|
18131
|
+
};
|
|
18106
18132
|
const NORMALIZE_HINTS = {
|
|
18107
18133
|
on: "Rewrite cch → stable prefix cache",
|
|
18108
18134
|
off: "Passthrough — rewrite nothing"
|
|
@@ -18207,6 +18233,12 @@ async function collectSessionTriState(currentSid) {
|
|
|
18207
18233
|
if (sid === "reset") return { sessionId: { remove: true } };
|
|
18208
18234
|
return { sessionId: { value: sid } };
|
|
18209
18235
|
}
|
|
18236
|
+
async function collectNormalizeResponsesTriState(currentNr) {
|
|
18237
|
+
const nr = await askTriState("normalizeResponses mode", currentNr, NORMALIZE_RESPONSES_HINTS, { removable: true });
|
|
18238
|
+
if (typeof nr === "symbol") return null;
|
|
18239
|
+
if (nr === "reset") return { normalizeResponses: { remove: true } };
|
|
18240
|
+
return { normalizeResponses: { value: nr } };
|
|
18241
|
+
}
|
|
18210
18242
|
/**
|
|
18211
18243
|
* Collects cacheControl mode, TTL, and rewriteBlockTtl.
|
|
18212
18244
|
* Mode-cancel aborts; TTL-cancel keeps existing TTL; rewrite-cancel keeps existing rewrite.
|
|
@@ -18654,6 +18686,13 @@ async function editSessionId(current) {
|
|
|
18654
18686
|
applyField(next, "sessionId", result.sessionId);
|
|
18655
18687
|
return next;
|
|
18656
18688
|
}
|
|
18689
|
+
async function editNormalizeResponses(current) {
|
|
18690
|
+
const result = await collectNormalizeResponsesTriState(current.normalizeResponses);
|
|
18691
|
+
if (result === null) return current;
|
|
18692
|
+
const next = { ...current };
|
|
18693
|
+
applyField(next, "normalizeResponses", result.normalizeResponses);
|
|
18694
|
+
return next;
|
|
18695
|
+
}
|
|
18657
18696
|
async function editCacheControl(current, configPath) {
|
|
18658
18697
|
const globalTtl = readGlobalTtl(configPath);
|
|
18659
18698
|
const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl, current.rewriteBlockTtl);
|
|
@@ -18761,6 +18800,83 @@ async function cachingCommand(opts) {
|
|
|
18761
18800
|
outro("Bye!");
|
|
18762
18801
|
}
|
|
18763
18802
|
//#endregion
|
|
18803
|
+
//#region src/commands/config/normalize-responses.ts
|
|
18804
|
+
async function normalizeResponsesCommand(opts) {
|
|
18805
|
+
const configPath = requireConfigPath(opts?.configPath);
|
|
18806
|
+
const raw = readConfigFileRaw(configPath).normalizeResponses;
|
|
18807
|
+
const effective = raw ?? DEFAULTS.normalizeResponses;
|
|
18808
|
+
log.info(`Current: normalizeResponses = ${raw === void 0 ? `(default -> ${effective})` : effective}`);
|
|
18809
|
+
const result = await askTriState("normalizeResponses mode", raw, NORMALIZE_RESPONSES_HINTS, { removable: true });
|
|
18810
|
+
if (typeof result === "symbol") return;
|
|
18811
|
+
if (result === "reset") {
|
|
18812
|
+
setGlobalConfigField(configPath, "normalizeResponses", void 0);
|
|
18813
|
+
log.success("normalizeResponses reset to default (auto)");
|
|
18814
|
+
return;
|
|
18815
|
+
}
|
|
18816
|
+
setGlobalConfigField(configPath, "normalizeResponses", result);
|
|
18817
|
+
log.success(`normalizeResponses set to ${result}`);
|
|
18818
|
+
}
|
|
18819
|
+
//#endregion
|
|
18820
|
+
//#region src/commands/config/fixes-menu.ts
|
|
18821
|
+
/** Compatibility fixes for request bodies OpenRouter rejects. Mirrors caching-menu's lever table. */
|
|
18822
|
+
const FIXES_LEVERS = [{
|
|
18823
|
+
value: "normalizeResponses",
|
|
18824
|
+
label: "Repair /v1/responses bodies (normalizeResponses)",
|
|
18825
|
+
global: (configPath) => normalizeResponsesCommand({ configPath }),
|
|
18826
|
+
perModel: (current) => editNormalizeResponses(current)
|
|
18827
|
+
}];
|
|
18828
|
+
async function runFixesLeverMenu(opts) {
|
|
18829
|
+
for (;;) {
|
|
18830
|
+
note(opts.renderNote(), opts.noteTitle);
|
|
18831
|
+
const choice = await select({
|
|
18832
|
+
message: "Tune which fix?",
|
|
18833
|
+
options: [...FIXES_LEVERS.map((lever) => ({
|
|
18834
|
+
value: lever.value,
|
|
18835
|
+
label: lever.label
|
|
18836
|
+
})), {
|
|
18837
|
+
value: "back",
|
|
18838
|
+
label: opts.backLabel
|
|
18839
|
+
}]
|
|
18840
|
+
});
|
|
18841
|
+
if (isCancel(choice) || choice === "back") return;
|
|
18842
|
+
const chosen = FIXES_LEVERS.find((l) => l.value === choice);
|
|
18843
|
+
if (chosen) await opts.onLever(chosen);
|
|
18844
|
+
}
|
|
18845
|
+
}
|
|
18846
|
+
async function globalFixesMenu(opts) {
|
|
18847
|
+
const configPath = requireConfigPath(opts?.configPath);
|
|
18848
|
+
await runFixesLeverMenu({
|
|
18849
|
+
noteTitle: "Fixes",
|
|
18850
|
+
backLabel: "← Back",
|
|
18851
|
+
renderNote: () => {
|
|
18852
|
+
return `normalizeResponses: ${readConfigFileRaw(configPath).normalizeResponses ?? "(default → auto)"}`;
|
|
18853
|
+
},
|
|
18854
|
+
onLever: (lever) => lever.global(configPath)
|
|
18855
|
+
});
|
|
18856
|
+
}
|
|
18857
|
+
/** Self-persists each lever; returns the latest override. */
|
|
18858
|
+
async function perModelFixesMenu(opts) {
|
|
18859
|
+
let current = opts.current;
|
|
18860
|
+
await runFixesLeverMenu({
|
|
18861
|
+
noteTitle: `Fixes for "${opts.modelKey}"`,
|
|
18862
|
+
backLabel: "← Back to override edit",
|
|
18863
|
+
renderNote: () => `normalizeResponses: ${current.normalizeResponses ?? "(inherit)"}`,
|
|
18864
|
+
onLever: async (lever) => {
|
|
18865
|
+
const next = await lever.perModel(current);
|
|
18866
|
+
if (!overridesEqual(next, current)) {
|
|
18867
|
+
setModelOverride(opts.configPath, opts.modelKey, next);
|
|
18868
|
+
current = next;
|
|
18869
|
+
}
|
|
18870
|
+
}
|
|
18871
|
+
});
|
|
18872
|
+
return current;
|
|
18873
|
+
}
|
|
18874
|
+
async function fixesCommand(opts) {
|
|
18875
|
+
intro("Proxitor · Fixes");
|
|
18876
|
+
await globalFixesMenu(opts);
|
|
18877
|
+
outro("Bye!");
|
|
18878
|
+
}
|
|
18879
|
+
//#endregion
|
|
18764
18880
|
//#region src/commands/config/edit.ts
|
|
18765
18881
|
function nvsHint(value) {
|
|
18766
18882
|
if (value === void 0) return "(inherit)";
|
|
@@ -18776,12 +18892,16 @@ function formatOverrideHint(override) {
|
|
|
18776
18892
|
if (override.sessionId) parts.push(`session: ${override.sessionId}`);
|
|
18777
18893
|
if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
|
|
18778
18894
|
if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${nvsHint(override.normalizeVolatileSystem)}`);
|
|
18895
|
+
if (override.normalizeResponses) parts.push(`fix: ${override.normalizeResponses}`);
|
|
18779
18896
|
if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
|
|
18780
18897
|
return parts.join(", ") || "(empty)";
|
|
18781
18898
|
}
|
|
18782
18899
|
function formatCachingHint(current) {
|
|
18783
18900
|
return `cc ${current.cacheControl ?? "inherit"} · ttl ${current.cacheControlTtl ? describeTtl(current.cacheControlTtl) : "inherit"} · sid ${current.sessionId ?? "inherit"} · nvs ${nvsHint(current.normalizeVolatileSystem)}`;
|
|
18784
18901
|
}
|
|
18902
|
+
function formatFixesHint(current) {
|
|
18903
|
+
return `normalizeResponses: ${current.normalizeResponses ?? "inherit"}`;
|
|
18904
|
+
}
|
|
18785
18905
|
async function editProvider(modelKey, current, client) {
|
|
18786
18906
|
const isPattern = modelKey.includes("*");
|
|
18787
18907
|
const mode = await selectRoutingMode("Routing mode");
|
|
@@ -18835,6 +18955,11 @@ async function editOverrideCommand(client, configPath) {
|
|
|
18835
18955
|
label: "💾 Caching",
|
|
18836
18956
|
hint: formatCachingHint(current)
|
|
18837
18957
|
},
|
|
18958
|
+
{
|
|
18959
|
+
value: "fixes",
|
|
18960
|
+
label: "🛠 Fixes",
|
|
18961
|
+
hint: formatFixesHint(current)
|
|
18962
|
+
},
|
|
18838
18963
|
{
|
|
18839
18964
|
value: "done",
|
|
18840
18965
|
label: "✓ Done"
|
|
@@ -18850,6 +18975,14 @@ async function editOverrideCommand(client, configPath) {
|
|
|
18850
18975
|
});
|
|
18851
18976
|
continue;
|
|
18852
18977
|
}
|
|
18978
|
+
if (field === "fixes") {
|
|
18979
|
+
current = await perModelFixesMenu({
|
|
18980
|
+
modelKey,
|
|
18981
|
+
current,
|
|
18982
|
+
configPath: resolvedConfigPath
|
|
18983
|
+
});
|
|
18984
|
+
continue;
|
|
18985
|
+
}
|
|
18853
18986
|
const before = current;
|
|
18854
18987
|
current = await editProvider(modelKey, current, client);
|
|
18855
18988
|
if (!overridesEqual(current, before)) setModelOverride(resolvedConfigPath, modelKey, current);
|
|
@@ -19477,6 +19610,10 @@ async function runConfigMenu(client) {
|
|
|
19477
19610
|
value: "caching",
|
|
19478
19611
|
label: "💾 Caching"
|
|
19479
19612
|
},
|
|
19613
|
+
{
|
|
19614
|
+
value: "fixes",
|
|
19615
|
+
label: "🛠 Fixes"
|
|
19616
|
+
},
|
|
19480
19617
|
{
|
|
19481
19618
|
value: "_sep2",
|
|
19482
19619
|
label: "── Model Overrides ──",
|
|
@@ -19531,6 +19668,9 @@ async function runConfigMenu(client) {
|
|
|
19531
19668
|
case "caching":
|
|
19532
19669
|
await globalCachingMenu();
|
|
19533
19670
|
break;
|
|
19671
|
+
case "fixes":
|
|
19672
|
+
await globalFixesMenu();
|
|
19673
|
+
break;
|
|
19534
19674
|
case "add":
|
|
19535
19675
|
await addOverrideCommand({ client });
|
|
19536
19676
|
break;
|
|
@@ -19554,7 +19694,7 @@ async function runConfigMenu(client) {
|
|
|
19554
19694
|
}
|
|
19555
19695
|
//#endregion
|
|
19556
19696
|
//#region src/version.ts
|
|
19557
|
-
const version = "0.
|
|
19697
|
+
const version = "0.18.0";
|
|
19558
19698
|
//#endregion
|
|
19559
19699
|
//#region src/commands/doctor.ts
|
|
19560
19700
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
@@ -19898,6 +20038,7 @@ var FileWatchingConfigSource = class {
|
|
|
19898
20038
|
pending = false;
|
|
19899
20039
|
watching = false;
|
|
19900
20040
|
lastCollisionSig = "";
|
|
20041
|
+
listeners = [];
|
|
19901
20042
|
constructor(options) {
|
|
19902
20043
|
this.current = options.initial;
|
|
19903
20044
|
this.warnSlugCollisions(options.initial.modelOverrides);
|
|
@@ -19912,6 +20053,20 @@ var FileWatchingConfigSource = class {
|
|
|
19912
20053
|
get() {
|
|
19913
20054
|
return this.current;
|
|
19914
20055
|
}
|
|
20056
|
+
subscribe(listener) {
|
|
20057
|
+
this.listeners.push(listener);
|
|
20058
|
+
return () => {
|
|
20059
|
+
const i = this.listeners.indexOf(listener);
|
|
20060
|
+
if (i !== -1) this.listeners.splice(i, 1);
|
|
20061
|
+
};
|
|
20062
|
+
}
|
|
20063
|
+
/** Notify subscribers after a successful reload. A throwing subscriber
|
|
20064
|
+
* must not abort the reload or skip the remaining subscribers. */
|
|
20065
|
+
notify(config) {
|
|
20066
|
+
for (const listener of this.listeners) try {
|
|
20067
|
+
listener(config);
|
|
20068
|
+
} catch {}
|
|
20069
|
+
}
|
|
19915
20070
|
/** Warn only when the collision set changes, so reloading an unchanged config doesn't re-log. */
|
|
19916
20071
|
warnSlugCollisions(overrides) {
|
|
19917
20072
|
const collisions = detectSlugCollisions(overrides ?? void 0);
|
|
@@ -19943,6 +20098,7 @@ var FileWatchingConfigSource = class {
|
|
|
19943
20098
|
this.warnSlugCollisions(next.modelOverrides);
|
|
19944
20099
|
if (restartNeeded) logger.warn("host/port changed — restart proxitor to apply (live reload does not re-bind the socket)");
|
|
19945
20100
|
logger.info(`Config reloaded${diff ? ` — ${diff}` : " (no material changes)"}`);
|
|
20101
|
+
this.notify(next);
|
|
19946
20102
|
return { ok: true };
|
|
19947
20103
|
} catch (error) {
|
|
19948
20104
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -22779,6 +22935,12 @@ function withJsonContentType(headers) {
|
|
|
22779
22935
|
"content-type": "application/json"
|
|
22780
22936
|
};
|
|
22781
22937
|
}
|
|
22938
|
+
function withRouterMetadata(headers, enabled) {
|
|
22939
|
+
return enabled ? {
|
|
22940
|
+
...headers,
|
|
22941
|
+
"x-openrouter-metadata": "enabled"
|
|
22942
|
+
} : headers;
|
|
22943
|
+
}
|
|
22782
22944
|
const buildUpstreamReq = createMiddleware(async (c, next) => {
|
|
22783
22945
|
c.set("forwardBody", resolveForwardBody({
|
|
22784
22946
|
reqId: c.var.reqId,
|
|
@@ -22793,6 +22955,7 @@ const buildUpstreamReq = createMiddleware(async (c, next) => {
|
|
|
22793
22955
|
});
|
|
22794
22956
|
if (c.var.effectiveSessionId !== void 0) headers = withSessionId(headers, c.var.effectiveSessionId);
|
|
22795
22957
|
if (c.var.bodyMutated) headers = withJsonContentType(headers);
|
|
22958
|
+
headers = withRouterMetadata(headers, c.var.config.observability.routerMetadata);
|
|
22796
22959
|
c.set("upstreamHeaders", headers);
|
|
22797
22960
|
await next();
|
|
22798
22961
|
});
|
|
@@ -22838,9 +23001,10 @@ function parseForwardBody(body) {
|
|
|
22838
23001
|
return null;
|
|
22839
23002
|
}
|
|
22840
23003
|
}
|
|
22841
|
-
/** `response` is filled later by dumpResponse
|
|
23004
|
+
/** Writes the request half; `response` is filled later by dumpResponse. Returns
|
|
23005
|
+
* the file path so the response half can find it without a reqId→path lookup. */
|
|
22842
23006
|
function dumpRequest(meta) {
|
|
22843
|
-
if (!dumpEnabled()) return;
|
|
23007
|
+
if (!dumpEnabled()) return void 0;
|
|
22844
23008
|
ensureDir(dumpDir());
|
|
22845
23009
|
const record = {
|
|
22846
23010
|
reqId: meta.reqId,
|
|
@@ -22851,156 +23015,313 @@ function dumpRequest(meta) {
|
|
|
22851
23015
|
request: parseForwardBody(meta.forwardBody),
|
|
22852
23016
|
response: null
|
|
22853
23017
|
};
|
|
22854
|
-
|
|
23018
|
+
const path = filePath(meta.reqId, meta.model);
|
|
23019
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`);
|
|
23020
|
+
return path;
|
|
22855
23021
|
}
|
|
22856
|
-
function dumpResponse(
|
|
23022
|
+
async function dumpResponse(obs) {
|
|
22857
23023
|
if (!dumpEnabled()) return;
|
|
22858
|
-
const
|
|
22859
|
-
if (
|
|
22860
|
-
const name = readdirSync(dir).find((f) => f.endsWith(`${reqId}.json`));
|
|
22861
|
-
if (!name) return;
|
|
22862
|
-
const path = join(dir, name);
|
|
23024
|
+
const path = obs.dumpPath;
|
|
23025
|
+
if (path === void 0) return;
|
|
22863
23026
|
try {
|
|
22864
|
-
const record = JSON.parse(
|
|
22865
|
-
const
|
|
23027
|
+
const record = JSON.parse(await readFile(path, "utf-8"));
|
|
23028
|
+
const r = obs.routing;
|
|
22866
23029
|
record.response = {
|
|
22867
|
-
status,
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
|
|
22871
|
-
|
|
23030
|
+
status: obs.status,
|
|
23031
|
+
label: obs.outcome.label,
|
|
23032
|
+
requestType: obs.requestType,
|
|
23033
|
+
model: obs.model,
|
|
23034
|
+
sessionId: obs.sessionId ?? null,
|
|
23035
|
+
toolsCount: obs.toolsCount,
|
|
23036
|
+
inputTokens: obs.usage.inputTokens,
|
|
23037
|
+
cacheRead: obs.usage.cacheRead,
|
|
23038
|
+
cacheCreate: obs.usage.cacheCreate,
|
|
23039
|
+
hitPct: obs.outcome.hitPct,
|
|
23040
|
+
...r ? {
|
|
23041
|
+
provider: r.provider,
|
|
23042
|
+
strategy: r.strategy,
|
|
23043
|
+
region: r.region ?? null,
|
|
23044
|
+
attempt: r.attempt,
|
|
23045
|
+
fallback: r.fallback,
|
|
23046
|
+
generationId: r.generationId ?? null
|
|
23047
|
+
} : {
|
|
23048
|
+
provider: null,
|
|
23049
|
+
strategy: null,
|
|
23050
|
+
region: null,
|
|
23051
|
+
attempt: null,
|
|
23052
|
+
fallback: false,
|
|
23053
|
+
generationId: null
|
|
23054
|
+
}
|
|
22872
23055
|
};
|
|
22873
|
-
|
|
23056
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`);
|
|
22874
23057
|
} catch {}
|
|
22875
23058
|
}
|
|
22876
23059
|
//#endregion
|
|
22877
|
-
//#region src/proxy/
|
|
22878
|
-
function
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
if (typeof
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
23060
|
+
//#region src/proxy/observability/extract.ts
|
|
23061
|
+
function num(v) {
|
|
23062
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
23063
|
+
}
|
|
23064
|
+
/** First non-empty candidate — the generation id may live at the root or under message/response. */
|
|
23065
|
+
function firstStringId(vals) {
|
|
23066
|
+
for (const v of vals) if (typeof v === "string" && v.length > 0) return v;
|
|
23067
|
+
}
|
|
23068
|
+
function applyAnthropic(usage, r, cr, cc) {
|
|
23069
|
+
if (cr !== void 0) r.cacheRead = cr;
|
|
23070
|
+
if (cc !== void 0) r.cacheCreate = cc;
|
|
23071
|
+
const inp = num(usage.input_tokens);
|
|
23072
|
+
if (inp !== void 0) r.inputTokens = inp + r.cacheRead + r.cacheCreate;
|
|
23073
|
+
}
|
|
23074
|
+
function applyOpenAI(usage, r) {
|
|
23075
|
+
const details = usage.prompt_tokens_details ?? usage.input_tokens_details;
|
|
23076
|
+
if (details && typeof details === "object") {
|
|
23077
|
+
const cached = num(details.cached_tokens);
|
|
23078
|
+
if (cached !== void 0) r.cacheRead = cached;
|
|
23079
|
+
const write = num(details.cache_write_tokens);
|
|
23080
|
+
if (write !== void 0) r.cacheCreate = write;
|
|
23081
|
+
}
|
|
23082
|
+
const prompt = num(usage.prompt_tokens) ?? num(usage.input_tokens);
|
|
23083
|
+
if (prompt !== void 0) r.inputTokens = prompt;
|
|
23084
|
+
}
|
|
23085
|
+
/** Find the usage object, unwrapping Anthropic `message`/`response` SSE containers. */
|
|
23086
|
+
function usageObject(parsed) {
|
|
23087
|
+
for (const c of [
|
|
23088
|
+
parsed.message,
|
|
23089
|
+
parsed.response,
|
|
23090
|
+
parsed
|
|
23091
|
+
]) if (c && typeof c === "object" && "usage" in c) {
|
|
23092
|
+
const u = c.usage;
|
|
23093
|
+
if (u && typeof u === "object") return u;
|
|
23094
|
+
}
|
|
23095
|
+
}
|
|
23096
|
+
function parseUsage(parsed) {
|
|
23097
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
23098
|
+
const usage = usageObject(parsed);
|
|
23099
|
+
if (usage === void 0) return void 0;
|
|
23100
|
+
const r = {
|
|
23101
|
+
present: true,
|
|
23102
|
+
inputTokens: 0,
|
|
23103
|
+
cacheRead: 0,
|
|
23104
|
+
cacheCreate: 0
|
|
23105
|
+
};
|
|
23106
|
+
const cr = num(usage.cache_read_input_tokens);
|
|
23107
|
+
const cc = num(usage.cache_creation_input_tokens);
|
|
23108
|
+
if (cr !== void 0 || cc !== void 0) applyAnthropic(usage, r, cr, cc);
|
|
23109
|
+
else applyOpenAI(usage, r);
|
|
23110
|
+
return r;
|
|
23111
|
+
}
|
|
23112
|
+
/** Provider from endpoints[].available (selected first), then the last attempt; '' is absent. */
|
|
23113
|
+
function resolveProvider(meta) {
|
|
23114
|
+
const avail = meta.endpoints?.available;
|
|
23115
|
+
if (Array.isArray(avail)) {
|
|
23116
|
+
const selected = avail.find((e) => e.selected === true);
|
|
23117
|
+
const selProvider = selected ? selected.provider : avail[0]?.provider;
|
|
23118
|
+
if (selProvider) return selProvider;
|
|
23119
|
+
}
|
|
23120
|
+
const attempts = meta.attempts;
|
|
23121
|
+
if (Array.isArray(attempts)) {
|
|
23122
|
+
const last = attempts.at(-1)?.provider;
|
|
23123
|
+
if (last) return last;
|
|
23124
|
+
}
|
|
23125
|
+
}
|
|
23126
|
+
function parseRouting(parsed) {
|
|
23127
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
23128
|
+
const root = parsed;
|
|
23129
|
+
const meta = root.openrouter_metadata ?? root.message?.openrouter_metadata ?? root.response?.openrouter_metadata;
|
|
23130
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
23131
|
+
const provider = resolveProvider(meta);
|
|
23132
|
+
if (!provider) return void 0;
|
|
23133
|
+
const attempt = num(meta.attempt) ?? 1;
|
|
23134
|
+
const generationId = firstStringId([
|
|
23135
|
+
root.id,
|
|
23136
|
+
root.message?.id,
|
|
23137
|
+
root.response?.id
|
|
23138
|
+
]);
|
|
23139
|
+
return {
|
|
23140
|
+
provider,
|
|
23141
|
+
strategy: typeof meta.strategy === "string" ? meta.strategy : "unknown",
|
|
23142
|
+
region: typeof meta.region === "string" ? meta.region : void 0,
|
|
23143
|
+
attempt,
|
|
23144
|
+
fallback: attempt > 1,
|
|
23145
|
+
generationId
|
|
23146
|
+
};
|
|
22908
23147
|
}
|
|
22909
|
-
|
|
22910
|
-
|
|
23148
|
+
function fold(parsed, acc) {
|
|
23149
|
+
const u = parseUsage(parsed);
|
|
23150
|
+
if (u) if (!acc.usage) acc.usage = { ...u };
|
|
23151
|
+
else {
|
|
23152
|
+
const a = acc.usage;
|
|
23153
|
+
if (u.cacheRead > 0) a.cacheRead = u.cacheRead;
|
|
23154
|
+
if (u.cacheCreate > 0) a.cacheCreate = u.cacheCreate;
|
|
23155
|
+
if (u.inputTokens > 0) a.inputTokens = u.inputTokens;
|
|
23156
|
+
}
|
|
23157
|
+
const rt = parseRouting(parsed);
|
|
23158
|
+
if (rt) acc.routing = rt;
|
|
23159
|
+
}
|
|
23160
|
+
/** Strip a leading UTF-8 BOM (U+FEFF) — trim() no longer removes it (ES2019). */
|
|
23161
|
+
function stripBom(s) {
|
|
23162
|
+
return s.charCodeAt(0) === 65279 ? s.slice(1) : s;
|
|
23163
|
+
}
|
|
23164
|
+
/** Parse one SSE `data:` line into the accumulator. Shared by the stateless and streaming folds. */
|
|
23165
|
+
function parseDataLine(line, acc) {
|
|
23166
|
+
const trimmed = stripBom(line).trim();
|
|
23167
|
+
if (!trimmed.startsWith("data:")) return;
|
|
23168
|
+
const payload = trimmed.slice(5).trim();
|
|
23169
|
+
if (payload === "" || payload === "[DONE]") return;
|
|
22911
23170
|
try {
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22919
|
-
|
|
23171
|
+
fold(JSON.parse(payload), acc);
|
|
23172
|
+
} catch {}
|
|
23173
|
+
}
|
|
23174
|
+
function extractFromFullText(text, isSSE) {
|
|
23175
|
+
if (!isSSE) try {
|
|
23176
|
+
const parsed = JSON.parse(stripBom(text));
|
|
23177
|
+
return {
|
|
23178
|
+
usage: parseUsage(parsed),
|
|
23179
|
+
routing: parseRouting(parsed)
|
|
22920
23180
|
};
|
|
22921
|
-
extractFromUsage(usage, result);
|
|
22922
|
-
return result;
|
|
22923
23181
|
} catch {
|
|
22924
|
-
return;
|
|
23182
|
+
return {};
|
|
23183
|
+
}
|
|
23184
|
+
const acc = {};
|
|
23185
|
+
for (const line of text.split("\n")) parseDataLine(line, acc);
|
|
23186
|
+
return acc;
|
|
23187
|
+
}
|
|
23188
|
+
/** Stateful fold over an SSE byte stream — O(1) memory, fragmentation-safe. */
|
|
23189
|
+
var SseUsageAccumulator = class {
|
|
23190
|
+
buffer = "";
|
|
23191
|
+
offset = 0;
|
|
23192
|
+
done = false;
|
|
23193
|
+
decoder = new TextDecoder();
|
|
23194
|
+
acc = {};
|
|
23195
|
+
feed(chunk) {
|
|
23196
|
+
if (this.done) return;
|
|
23197
|
+
this.buffer += this.decoder.decode(chunk, { stream: true });
|
|
23198
|
+
let nl = this.buffer.indexOf("\n", this.offset);
|
|
23199
|
+
while (nl !== -1) {
|
|
23200
|
+
this.process(this.buffer.slice(this.offset, nl));
|
|
23201
|
+
this.offset = nl + 1;
|
|
23202
|
+
nl = this.buffer.indexOf("\n", this.offset);
|
|
23203
|
+
}
|
|
23204
|
+
if (this.offset > 0) {
|
|
23205
|
+
this.buffer = this.buffer.slice(this.offset);
|
|
23206
|
+
this.offset = 0;
|
|
23207
|
+
}
|
|
22925
23208
|
}
|
|
22926
|
-
|
|
22927
|
-
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
return result.cacheRead + result.cacheCreate > before;
|
|
22935
|
-
}
|
|
22936
|
-
/** @internal */
|
|
22937
|
-
function extractCacheUsageFromSSE(fullText) {
|
|
22938
|
-
const result = {
|
|
22939
|
-
cacheRead: 0,
|
|
22940
|
-
cacheCreate: 0,
|
|
22941
|
-
inputTokens: 0
|
|
22942
|
-
};
|
|
22943
|
-
let found = false;
|
|
22944
|
-
for (const line of fullText.split("\n")) {
|
|
22945
|
-
if (!line.startsWith("data:")) continue;
|
|
22946
|
-
const payload = line.slice(5).trim();
|
|
22947
|
-
if (payload === "[DONE]") continue;
|
|
22948
|
-
try {
|
|
22949
|
-
if (extractFromEvent(JSON.parse(payload), result)) found = true;
|
|
22950
|
-
} catch {}
|
|
23209
|
+
result() {
|
|
23210
|
+
if (this.done) return this.acc;
|
|
23211
|
+
this.done = true;
|
|
23212
|
+
this.buffer += this.decoder.decode();
|
|
23213
|
+
if (this.buffer.length > 0) this.process(this.buffer.slice(this.offset));
|
|
23214
|
+
this.buffer = "";
|
|
23215
|
+
this.offset = 0;
|
|
23216
|
+
return this.acc;
|
|
22951
23217
|
}
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
const
|
|
22965
|
-
|
|
22966
|
-
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
23218
|
+
process(line) {
|
|
23219
|
+
parseDataLine(line, this.acc);
|
|
23220
|
+
}
|
|
23221
|
+
};
|
|
23222
|
+
//#endregion
|
|
23223
|
+
//#region src/proxy/cache-logging.ts
|
|
23224
|
+
/**
|
|
23225
|
+
* Wraps the upstream body so usage/routing is observed once on ANY termination
|
|
23226
|
+
* path (clean close, client cancel, upstream error) via finalize() — a bare
|
|
23227
|
+
* TransformStream skips flush() on upstream errors, orphaning the dump.
|
|
23228
|
+
*/
|
|
23229
|
+
function createLoggingStream(source, contentType, status, ctx) {
|
|
23230
|
+
const accumulator = contentType.toLowerCase().includes("text/event-stream") ? new SseUsageAccumulator() : void 0;
|
|
23231
|
+
const decoder = new TextDecoder();
|
|
23232
|
+
const chunks = [];
|
|
23233
|
+
let finalized = false;
|
|
23234
|
+
const finalize = () => {
|
|
23235
|
+
if (finalized) return;
|
|
23236
|
+
finalized = true;
|
|
23237
|
+
try {
|
|
23238
|
+
let extracted;
|
|
23239
|
+
if (accumulator) extracted = accumulator.result();
|
|
22970
23240
|
else {
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
23241
|
+
let text = "";
|
|
23242
|
+
for (const c of chunks) text += decoder.decode(c, { stream: true });
|
|
23243
|
+
text += decoder.decode();
|
|
23244
|
+
extracted = extractFromFullText(text, false);
|
|
22974
23245
|
}
|
|
22975
|
-
|
|
22976
|
-
|
|
23246
|
+
ctx.observability.observe(ctx.reqCtx, extracted, status);
|
|
23247
|
+
} catch (err) {
|
|
23248
|
+
logger.debug(withReq(ctx.reqCtx.reqId, `Cache observability failed: ${err instanceof Error ? err.message : err}`));
|
|
23249
|
+
}
|
|
23250
|
+
};
|
|
23251
|
+
const reader = source.getReader();
|
|
23252
|
+
return new ReadableStream({
|
|
23253
|
+
async pull(controller) {
|
|
22977
23254
|
try {
|
|
22978
|
-
const
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
23255
|
+
const { done, value } = await reader.read();
|
|
23256
|
+
if (done) {
|
|
23257
|
+
finalize();
|
|
23258
|
+
controller.close();
|
|
23259
|
+
return;
|
|
23260
|
+
}
|
|
23261
|
+
if (value) {
|
|
23262
|
+
if (accumulator) accumulator.feed(value);
|
|
23263
|
+
else chunks.push(value);
|
|
23264
|
+
controller.enqueue(value);
|
|
23265
|
+
}
|
|
22983
23266
|
} catch (err) {
|
|
22984
|
-
|
|
23267
|
+
finalize();
|
|
23268
|
+
controller.error(err);
|
|
22985
23269
|
}
|
|
23270
|
+
},
|
|
23271
|
+
cancel() {
|
|
23272
|
+
finalize();
|
|
23273
|
+
reader.cancel().catch(() => {});
|
|
22986
23274
|
}
|
|
22987
23275
|
});
|
|
22988
23276
|
}
|
|
22989
|
-
function buildUpstreamResponseWithLogging(upstream, method,
|
|
23277
|
+
function buildUpstreamResponseWithLogging(upstream, method, ctx) {
|
|
22990
23278
|
const headers = buildResponseHeaders(upstream.headers);
|
|
22991
|
-
if (method === "HEAD" || !upstream.body)
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
23279
|
+
if (method === "HEAD" || !upstream.body) {
|
|
23280
|
+
ctx.observability.observe(ctx.reqCtx, {}, upstream.status);
|
|
23281
|
+
return new Response(null, {
|
|
23282
|
+
status: upstream.status,
|
|
23283
|
+
headers
|
|
23284
|
+
});
|
|
23285
|
+
}
|
|
22995
23286
|
const contentType = upstream.headers.get("content-type") ?? "";
|
|
22996
23287
|
const lower = contentType.toLowerCase();
|
|
22997
|
-
|
|
23288
|
+
if (!(lower.includes("application/json") || lower.includes("text/event-stream"))) {
|
|
23289
|
+
ctx.observability.observe(ctx.reqCtx, {}, upstream.status);
|
|
23290
|
+
return new Response(upstream.body, {
|
|
23291
|
+
status: upstream.status,
|
|
23292
|
+
headers
|
|
23293
|
+
});
|
|
23294
|
+
}
|
|
23295
|
+
const body = createLoggingStream(upstream.body, contentType, upstream.status, ctx);
|
|
22998
23296
|
return new Response(body, {
|
|
22999
23297
|
status: upstream.status,
|
|
23000
23298
|
headers
|
|
23001
23299
|
});
|
|
23002
23300
|
}
|
|
23003
23301
|
//#endregion
|
|
23302
|
+
//#region src/proxy/observability/classify.ts
|
|
23303
|
+
function classifyRequestType(ctx, opts) {
|
|
23304
|
+
const budget = ctx.maxTokens === void 0 || ctx.maxTokens <= 0 ? Number.POSITIVE_INFINITY : ctx.maxTokens;
|
|
23305
|
+
return ctx.toolsCount === 0 && budget <= opts.sideMaxTokens ? "side" : "main";
|
|
23306
|
+
}
|
|
23307
|
+
function classifyCacheOutcome(usage, ctx, opts) {
|
|
23308
|
+
if (!usage.present) return {
|
|
23309
|
+
label: "NOUSAGE",
|
|
23310
|
+
type: ctx.requestType,
|
|
23311
|
+
hitPct: 0
|
|
23312
|
+
};
|
|
23313
|
+
const raw = usage.inputTokens > 0 ? usage.cacheRead / usage.inputTokens * 100 : 0;
|
|
23314
|
+
const hitPct = Math.min(100, raw);
|
|
23315
|
+
let label;
|
|
23316
|
+
if (usage.cacheRead === 0) label = ctx.isFirstForSession ? "COLD" : "MISS";
|
|
23317
|
+
else label = hitPct >= opts.hitThresholdPct ? "HIT" : "PARTIAL";
|
|
23318
|
+
return {
|
|
23319
|
+
label,
|
|
23320
|
+
type: ctx.requestType,
|
|
23321
|
+
hitPct: Number(hitPct.toFixed(1))
|
|
23322
|
+
};
|
|
23323
|
+
}
|
|
23324
|
+
//#endregion
|
|
23004
23325
|
//#region src/proxy/utils/error.ts
|
|
23005
23326
|
function formatMetadata(meta) {
|
|
23006
23327
|
const parts = [];
|
|
@@ -23031,7 +23352,7 @@ function extractErrorDetail(bodyText) {
|
|
|
23031
23352
|
//#endregion
|
|
23032
23353
|
//#region src/proxy/middleware/forward-request.ts
|
|
23033
23354
|
const DUPLEX_HALF = { duplex: "half" };
|
|
23034
|
-
function buildErrorResponse(err, ctx) {
|
|
23355
|
+
function buildErrorResponse(err, ctx, observeUnhandled) {
|
|
23035
23356
|
if (err instanceof TypeError) {
|
|
23036
23357
|
logger.error(withReq(ctx.reqId, "Upstream fetch error:"), err);
|
|
23037
23358
|
return Response.json({ error: {
|
|
@@ -23043,6 +23364,7 @@ function buildErrorResponse(err, ctx) {
|
|
|
23043
23364
|
logger.warn(withReq(ctx.reqId, `Aborted: ${ctx.method} ${ctx.path}`));
|
|
23044
23365
|
return new Response(null, { status: 499 });
|
|
23045
23366
|
}
|
|
23367
|
+
observeUnhandled?.();
|
|
23046
23368
|
throw err;
|
|
23047
23369
|
}
|
|
23048
23370
|
async function buildUpstreamErrorResponse(upstream, ctx) {
|
|
@@ -23051,14 +23373,16 @@ async function buildUpstreamErrorResponse(upstream, ctx) {
|
|
|
23051
23373
|
const truncated = detail.length > 300 ? `${detail.slice(0, 300)}…` : detail;
|
|
23052
23374
|
(upstream.status >= 500 ? logger.error : logger.warn)(withReq(ctx.reqId, `${ctx.method} ${ctx.path} ← ${upstream.status} (${Date.now() - ctx.startedAt}ms): ${truncated}`));
|
|
23053
23375
|
const responseHeaders = buildResponseHeaders(upstream.headers);
|
|
23054
|
-
|
|
23055
|
-
|
|
23056
|
-
|
|
23057
|
-
|
|
23058
|
-
|
|
23059
|
-
|
|
23060
|
-
|
|
23061
|
-
|
|
23376
|
+
return {
|
|
23377
|
+
response: ctx.method === "HEAD" ? new Response(null, {
|
|
23378
|
+
status: upstream.status,
|
|
23379
|
+
headers: responseHeaders
|
|
23380
|
+
}) : new Response(bodyText, {
|
|
23381
|
+
status: upstream.status,
|
|
23382
|
+
headers: responseHeaders
|
|
23383
|
+
}),
|
|
23384
|
+
extracted: extractFromFullText(bodyText, false)
|
|
23385
|
+
};
|
|
23062
23386
|
}
|
|
23063
23387
|
const forwardRequest = createMiddleware(async (c) => {
|
|
23064
23388
|
const { upstreamUrl, forwardBody, upstreamHeaders, reqId, path, startedAt, method } = c.var;
|
|
@@ -23075,13 +23399,30 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
23075
23399
|
const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
|
|
23076
23400
|
const modelLog = c.var.modelName ? ` model=${c.var.modelName}` : "";
|
|
23077
23401
|
logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${ctx.bodyMutated ? " [inject]" : ""}${modelLog}`));
|
|
23078
|
-
|
|
23402
|
+
const dumpPath = dumpEnabled() ? dumpRequest({
|
|
23079
23403
|
reqId,
|
|
23080
23404
|
method,
|
|
23081
23405
|
path,
|
|
23082
23406
|
model: c.var.modelName,
|
|
23083
23407
|
forwardBody
|
|
23084
|
-
});
|
|
23408
|
+
}) : void 0;
|
|
23409
|
+
const parsedBody = c.var.parsedBody;
|
|
23410
|
+
const toolsCount = Array.isArray(parsedBody?.tools) ? parsedBody.tools.length : 0;
|
|
23411
|
+
const maxTokens = parsedBody?.max_tokens ?? parsedBody?.max_completion_tokens ?? parsedBody?.max_output_tokens;
|
|
23412
|
+
const requestType = classifyRequestType({
|
|
23413
|
+
toolsCount,
|
|
23414
|
+
maxTokens
|
|
23415
|
+
}, { sideMaxTokens: c.var.config.observability.sideMaxTokens });
|
|
23416
|
+
const reqCtx = {
|
|
23417
|
+
reqId,
|
|
23418
|
+
model: c.var.modelName ?? "",
|
|
23419
|
+
sessionId: c.var.effectiveSessionId,
|
|
23420
|
+
toolsCount,
|
|
23421
|
+
maxTokens,
|
|
23422
|
+
requestType,
|
|
23423
|
+
dumpPath
|
|
23424
|
+
};
|
|
23425
|
+
const observability = c.var.observability;
|
|
23085
23426
|
let upstream;
|
|
23086
23427
|
try {
|
|
23087
23428
|
upstream = await fetch(upstreamUrl, {
|
|
@@ -23092,13 +23433,32 @@ const forwardRequest = createMiddleware(async (c) => {
|
|
|
23092
23433
|
...forwardBody ? DUPLEX_HALF : {}
|
|
23093
23434
|
});
|
|
23094
23435
|
} catch (err) {
|
|
23095
|
-
|
|
23436
|
+
const response = buildErrorResponse(err, ctx, () => observability.observe(reqCtx, {}, 500));
|
|
23437
|
+
observability.observe(reqCtx, {}, response.status);
|
|
23438
|
+
return response;
|
|
23096
23439
|
} finally {
|
|
23097
23440
|
c.req.raw.signal.removeEventListener("abort", onClientAbort);
|
|
23098
23441
|
}
|
|
23099
|
-
if (upstream.status >= 400)
|
|
23442
|
+
if (upstream.status >= 400) {
|
|
23443
|
+
let extracted = {};
|
|
23444
|
+
let response;
|
|
23445
|
+
try {
|
|
23446
|
+
({response, extracted} = await buildUpstreamErrorResponse(upstream, ctx));
|
|
23447
|
+
} catch {
|
|
23448
|
+
logger.debug(withReq(reqId, `upstream ${upstream.status} error body unreadable; observing without detail`));
|
|
23449
|
+
response = new Response(null, {
|
|
23450
|
+
status: upstream.status,
|
|
23451
|
+
headers: buildResponseHeaders(upstream.headers)
|
|
23452
|
+
});
|
|
23453
|
+
}
|
|
23454
|
+
observability.observe(reqCtx, extracted, upstream.status);
|
|
23455
|
+
return response;
|
|
23456
|
+
}
|
|
23100
23457
|
logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
|
|
23101
|
-
return buildUpstreamResponseWithLogging(upstream, method,
|
|
23458
|
+
return buildUpstreamResponseWithLogging(upstream, method, {
|
|
23459
|
+
reqCtx,
|
|
23460
|
+
observability
|
|
23461
|
+
});
|
|
23102
23462
|
});
|
|
23103
23463
|
//#endregion
|
|
23104
23464
|
//#region src/proxy/paths.ts
|
|
@@ -23190,7 +23550,12 @@ function rewriteBlockTtls(body, ttl, isAnthropic) {
|
|
|
23190
23550
|
visitBlocks(body.system);
|
|
23191
23551
|
visitBlocks(body.tools);
|
|
23192
23552
|
const messages = body.messages;
|
|
23193
|
-
for (const m of messages ?? [])
|
|
23553
|
+
for (const m of messages ?? []) {
|
|
23554
|
+
if (m === null || typeof m !== "object" || Array.isArray(m)) continue;
|
|
23555
|
+
rewriteNode(m);
|
|
23556
|
+
visitBlocks(m.content);
|
|
23557
|
+
visitBlocks(m.tool_calls);
|
|
23558
|
+
}
|
|
23194
23559
|
return mutated;
|
|
23195
23560
|
}
|
|
23196
23561
|
//#endregion
|
|
@@ -23291,6 +23656,105 @@ const injectSessionId = createMiddleware(async (c, next) => {
|
|
|
23291
23656
|
await next();
|
|
23292
23657
|
});
|
|
23293
23658
|
//#endregion
|
|
23659
|
+
//#region src/proxy/utils/responses-input.ts
|
|
23660
|
+
/**
|
|
23661
|
+
* Whether to normalize a Responses-api body. Mirrors the other tri-states:
|
|
23662
|
+
* `auto` acts only on /v1/responses, `always` acts everywhere, `skip` never.
|
|
23663
|
+
*/
|
|
23664
|
+
function shouldNormalizeResponses(mode, path) {
|
|
23665
|
+
if (mode === "skip") return false;
|
|
23666
|
+
if (mode === "always") return true;
|
|
23667
|
+
return classifyEndpoint(path) === "responses";
|
|
23668
|
+
}
|
|
23669
|
+
function isPlainObject(value) {
|
|
23670
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23671
|
+
}
|
|
23672
|
+
/** Flatten a Responses content value (string or block array) to plain text. */
|
|
23673
|
+
function contentToText(content) {
|
|
23674
|
+
if (typeof content === "string") return content;
|
|
23675
|
+
if (!Array.isArray(content)) return void 0;
|
|
23676
|
+
const parts = [];
|
|
23677
|
+
for (const block of content) {
|
|
23678
|
+
const text = isPlainObject(block) ? block.text : void 0;
|
|
23679
|
+
if (typeof text === "string") parts.push(text);
|
|
23680
|
+
}
|
|
23681
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
23682
|
+
}
|
|
23683
|
+
function appendInstructions(current, text) {
|
|
23684
|
+
return typeof current === "string" ? `${current}\n\n${text}` : text;
|
|
23685
|
+
}
|
|
23686
|
+
/** Tag a role-bearing message item and complete assistant-history metadata. */
|
|
23687
|
+
function tagMessageItem(obj) {
|
|
23688
|
+
let changed = false;
|
|
23689
|
+
if (typeof obj.role === "string" && obj.type === void 0) {
|
|
23690
|
+
obj.type = "message";
|
|
23691
|
+
changed = true;
|
|
23692
|
+
}
|
|
23693
|
+
if (obj.type === "message" && obj.role === "assistant") {
|
|
23694
|
+
if (obj.id === void 0) {
|
|
23695
|
+
obj.id = `msg_${randomUUID().slice(0, 12)}`;
|
|
23696
|
+
changed = true;
|
|
23697
|
+
}
|
|
23698
|
+
if (obj.status === void 0) {
|
|
23699
|
+
obj.status = "completed";
|
|
23700
|
+
changed = true;
|
|
23701
|
+
}
|
|
23702
|
+
}
|
|
23703
|
+
return changed;
|
|
23704
|
+
}
|
|
23705
|
+
/**
|
|
23706
|
+
* Lift a `role: "system"` item: OpenRouter Responses has no system role in
|
|
23707
|
+
* `input`, so its text moves to `instructions`. Items with no extractable text
|
|
23708
|
+
* (image-only/empty) can't be represented there and are dropped — keeping them
|
|
23709
|
+
* as `role: "system"` would reproduce the rejection this normalizer exists to fix.
|
|
23710
|
+
*/
|
|
23711
|
+
function handleSystemItem(obj, state) {
|
|
23712
|
+
const text = contentToText(obj.content);
|
|
23713
|
+
if (text !== void 0) state.instructions = appendInstructions(state.instructions, text);
|
|
23714
|
+
}
|
|
23715
|
+
/**
|
|
23716
|
+
* Make a Responses body satisfy OpenRouter's `input` schema. Each item is a
|
|
23717
|
+
* `type`-discriminated union, so items missing `type` (OpenAI infers "message")
|
|
23718
|
+
* are rejected as `invalid_prompt`. Also relocates `role: "system"` items to
|
|
23719
|
+
* `instructions` and synthesizes the `id`/`status` assistant history requires.
|
|
23720
|
+
* Idempotent; returns whether the body changed.
|
|
23721
|
+
*/
|
|
23722
|
+
function normalizeResponsesInput(body) {
|
|
23723
|
+
const input = body.input;
|
|
23724
|
+
if (!Array.isArray(input)) return false;
|
|
23725
|
+
let mutated = false;
|
|
23726
|
+
const state = {
|
|
23727
|
+
instructions: body.instructions,
|
|
23728
|
+
next: []
|
|
23729
|
+
};
|
|
23730
|
+
for (const raw of input) {
|
|
23731
|
+
if (!isPlainObject(raw)) {
|
|
23732
|
+
state.next.push(raw);
|
|
23733
|
+
continue;
|
|
23734
|
+
}
|
|
23735
|
+
if (raw.role === "system") {
|
|
23736
|
+
handleSystemItem(raw, state);
|
|
23737
|
+
mutated = true;
|
|
23738
|
+
continue;
|
|
23739
|
+
}
|
|
23740
|
+
if (tagMessageItem(raw)) mutated = true;
|
|
23741
|
+
state.next.push(raw);
|
|
23742
|
+
}
|
|
23743
|
+
if (mutated) {
|
|
23744
|
+
body.input = state.next;
|
|
23745
|
+
if (state.instructions !== void 0) body.instructions = state.instructions;
|
|
23746
|
+
}
|
|
23747
|
+
return mutated;
|
|
23748
|
+
}
|
|
23749
|
+
//#endregion
|
|
23750
|
+
//#region src/proxy/middleware/normalize-responses-input.ts
|
|
23751
|
+
/** Normalize Responses-API input for OpenRouter's schema (see normalizeResponsesInput); no-op for chat/messages. */
|
|
23752
|
+
const normalizeResponsesInputMiddleware = createMiddleware(async (c, next) => {
|
|
23753
|
+
const parsedBody = c.var.parsedBody;
|
|
23754
|
+
if (parsedBody && shouldNormalizeResponses(c.var.resolvedConfig.normalizeResponses, c.req.path) && normalizeResponsesInput(parsedBody)) c.set("bodyMutated", true);
|
|
23755
|
+
await next();
|
|
23756
|
+
});
|
|
23757
|
+
//#endregion
|
|
23294
23758
|
//#region src/proxy/middleware/normalize-volatile-system.ts
|
|
23295
23759
|
/** Normalize Claude Code's per-request volatile hashes in system[0] to constants; they drift every turn and break prefix-cache for non-Anthropic providers:
|
|
23296
23760
|
* - `cch=…` per-turn hash
|
|
@@ -23435,6 +23899,172 @@ const setupRequest = createMiddleware(async (c, next) => {
|
|
|
23435
23899
|
await next();
|
|
23436
23900
|
});
|
|
23437
23901
|
//#endregion
|
|
23902
|
+
//#region src/proxy/observability/session-tracker.ts
|
|
23903
|
+
var SessionTracker = class {
|
|
23904
|
+
seen = /* @__PURE__ */ new Map();
|
|
23905
|
+
constructor(opts, now = Date.now) {
|
|
23906
|
+
this.opts = opts;
|
|
23907
|
+
this.now = now;
|
|
23908
|
+
}
|
|
23909
|
+
opts;
|
|
23910
|
+
now;
|
|
23911
|
+
isFirstAndRemember(sessionId) {
|
|
23912
|
+
if (sessionId === void 0) return true;
|
|
23913
|
+
const t = this.now();
|
|
23914
|
+
const prev = this.seen.get(sessionId);
|
|
23915
|
+
const fresh = prev !== void 0 && t - prev < this.opts.ttlMs;
|
|
23916
|
+
if (prev !== void 0) this.seen.delete(sessionId);
|
|
23917
|
+
else if (this.seen.size >= this.opts.maxEntries) this.evictOne();
|
|
23918
|
+
this.seen.set(sessionId, t);
|
|
23919
|
+
return !fresh;
|
|
23920
|
+
}
|
|
23921
|
+
/** Drop the LRU entry (first by Map order). A refresh moves the key to the
|
|
23922
|
+
* end, so iteration order is lastSeen order — the first entry is most stale. */
|
|
23923
|
+
evictOne() {
|
|
23924
|
+
const oldest = this.seen.keys().next().value;
|
|
23925
|
+
if (oldest !== void 0) this.seen.delete(oldest);
|
|
23926
|
+
}
|
|
23927
|
+
/** Apply a reloaded capacity/TTL without discarding remembered sessions —
|
|
23928
|
+
* shrinking evicts LRU entries, growing is a no-op, TTL applies on the next
|
|
23929
|
+
* freshness check. Rebuilding would wipe the map and misclassify as COLD. */
|
|
23930
|
+
applyConfig(opts) {
|
|
23931
|
+
this.opts = opts;
|
|
23932
|
+
while (this.seen.size > opts.maxEntries) this.evictOne();
|
|
23933
|
+
}
|
|
23934
|
+
};
|
|
23935
|
+
//#endregion
|
|
23936
|
+
//#region src/proxy/observability/sinks.ts
|
|
23937
|
+
const wrap = (code) => (s) => `\x1b[${code}m${s}\x1b[0m`;
|
|
23938
|
+
const PAINT = {
|
|
23939
|
+
HIT: wrap("32"),
|
|
23940
|
+
PARTIAL: wrap("33"),
|
|
23941
|
+
MISS: wrap("31"),
|
|
23942
|
+
COLD: wrap("2"),
|
|
23943
|
+
NOUSAGE: wrap("90")
|
|
23944
|
+
};
|
|
23945
|
+
function colorizeLabel(label, useColor) {
|
|
23946
|
+
return useColor ? PAINT[label](label) : label;
|
|
23947
|
+
}
|
|
23948
|
+
function formatLine(obs, useColor = false) {
|
|
23949
|
+
const { label, hitPct } = obs.outcome;
|
|
23950
|
+
const parts = [colorizeLabel(label, useColor)];
|
|
23951
|
+
if (label === "HIT" || label === "PARTIAL") parts.push(`${hitPct.toFixed(0)}%`);
|
|
23952
|
+
if (obs.usage.cacheRead > 0) parts.push(`read ${obs.usage.cacheRead}`);
|
|
23953
|
+
if (obs.usage.cacheCreate > 0) parts.push(`write ${obs.usage.cacheCreate}`);
|
|
23954
|
+
if (obs.usage.present) parts.push(`in ${obs.usage.inputTokens}`);
|
|
23955
|
+
if (obs.routing) parts.push(`provider=${obs.routing.provider}`);
|
|
23956
|
+
if (obs.model) parts.push(obs.model);
|
|
23957
|
+
parts.push(`[${obs.requestType}]`);
|
|
23958
|
+
return withReq(obs.reqId, parts.join(" "));
|
|
23959
|
+
}
|
|
23960
|
+
var LiveLineSink = class {
|
|
23961
|
+
useColor;
|
|
23962
|
+
constructor(useColor = () => process.stdout.isTTY === true) {
|
|
23963
|
+
this.useColor = useColor;
|
|
23964
|
+
}
|
|
23965
|
+
emit(obs) {
|
|
23966
|
+
logger.info(formatLine(obs, this.useColor()));
|
|
23967
|
+
}
|
|
23968
|
+
};
|
|
23969
|
+
var DumpSink = class {
|
|
23970
|
+
inflight = 0;
|
|
23971
|
+
waiters = [];
|
|
23972
|
+
maxConcurrent;
|
|
23973
|
+
maxWaiters;
|
|
23974
|
+
dump;
|
|
23975
|
+
enabled;
|
|
23976
|
+
constructor(deps = {}) {
|
|
23977
|
+
this.maxConcurrent = deps.maxConcurrent ?? 16;
|
|
23978
|
+
this.maxWaiters = deps.maxWaiters ?? 256;
|
|
23979
|
+
this.dump = deps.dump ?? dumpResponse;
|
|
23980
|
+
this.enabled = deps.enabled ?? dumpEnabled;
|
|
23981
|
+
}
|
|
23982
|
+
emit(obs) {
|
|
23983
|
+
if (!this.enabled()) return;
|
|
23984
|
+
const run = () => {
|
|
23985
|
+
this.inflight += 1;
|
|
23986
|
+
this.dump(obs).catch((err) => {
|
|
23987
|
+
logger.debug(withReq(obs.reqId, `DumpSink failed: ${err instanceof Error ? err.message : err}`));
|
|
23988
|
+
}).finally(() => {
|
|
23989
|
+
this.inflight -= 1;
|
|
23990
|
+
const next = this.waiters.shift();
|
|
23991
|
+
if (next) next();
|
|
23992
|
+
});
|
|
23993
|
+
};
|
|
23994
|
+
if (this.inflight < this.maxConcurrent) run();
|
|
23995
|
+
else if (this.waiters.length < this.maxWaiters) this.waiters.push(run);
|
|
23996
|
+
else logger.debug(withReq(obs.reqId, "DumpSink queue full — dump dropped"));
|
|
23997
|
+
}
|
|
23998
|
+
};
|
|
23999
|
+
//#endregion
|
|
24000
|
+
//#region src/proxy/observability/observability.ts
|
|
24001
|
+
var Observability = class {
|
|
24002
|
+
tracker;
|
|
24003
|
+
sinks;
|
|
24004
|
+
hitThresholdPct;
|
|
24005
|
+
constructor(tracker, sinks, hitThresholdPct) {
|
|
24006
|
+
this.tracker = tracker;
|
|
24007
|
+
this.sinks = sinks;
|
|
24008
|
+
this.hitThresholdPct = hitThresholdPct;
|
|
24009
|
+
}
|
|
24010
|
+
/** Classify and dispatch one observation to every sink. Never throws: the
|
|
24011
|
+
* response pipeline calls this on many termination paths, so a failure must
|
|
24012
|
+
* degrade to a debug log, not escape to the client. */
|
|
24013
|
+
observe(req, extracted, status) {
|
|
24014
|
+
try {
|
|
24015
|
+
const usage = extracted.usage ?? {
|
|
24016
|
+
present: false,
|
|
24017
|
+
inputTokens: 0,
|
|
24018
|
+
cacheRead: 0,
|
|
24019
|
+
cacheCreate: 0
|
|
24020
|
+
};
|
|
24021
|
+
const isFirst = this.tracker.isFirstAndRemember(req.sessionId);
|
|
24022
|
+
const outcome = classifyCacheOutcome(usage, {
|
|
24023
|
+
requestType: req.requestType,
|
|
24024
|
+
isFirstForSession: isFirst
|
|
24025
|
+
}, { hitThresholdPct: this.hitThresholdPct });
|
|
24026
|
+
const obs = {
|
|
24027
|
+
dumpPath: req.dumpPath,
|
|
24028
|
+
reqId: req.reqId,
|
|
24029
|
+
status,
|
|
24030
|
+
model: req.model,
|
|
24031
|
+
sessionId: req.sessionId,
|
|
24032
|
+
requestType: req.requestType,
|
|
24033
|
+
toolsCount: req.toolsCount,
|
|
24034
|
+
usage,
|
|
24035
|
+
outcome,
|
|
24036
|
+
routing: extracted.routing
|
|
24037
|
+
};
|
|
24038
|
+
for (const sink of this.sinks) try {
|
|
24039
|
+
sink.emit(obs);
|
|
24040
|
+
} catch (err) {
|
|
24041
|
+
logger.debug(withReq(obs.reqId, `Observability sink failed: ${err instanceof Error ? err.message : err}`));
|
|
24042
|
+
}
|
|
24043
|
+
} catch (err) {
|
|
24044
|
+
logger.debug(withReq(req.reqId, `Observability observe failed: ${err instanceof Error ? err.message : err}`));
|
|
24045
|
+
}
|
|
24046
|
+
}
|
|
24047
|
+
/** Apply a hot-reloaded config in place — must not discard remembered
|
|
24048
|
+
* sessions, or an unrelated reload misclassifies the next request as COLD. */
|
|
24049
|
+
reconfigure(config) {
|
|
24050
|
+
const o = config.observability;
|
|
24051
|
+
this.hitThresholdPct = o.hitThreshold;
|
|
24052
|
+
this.tracker.applyConfig({
|
|
24053
|
+
maxEntries: o.sessionMaxEntries,
|
|
24054
|
+
ttlMs: o.sessionTtlMs
|
|
24055
|
+
});
|
|
24056
|
+
}
|
|
24057
|
+
};
|
|
24058
|
+
function createObservability(config, sinks) {
|
|
24059
|
+
const o = config.observability;
|
|
24060
|
+
const tracker = new SessionTracker({
|
|
24061
|
+
maxEntries: o.sessionMaxEntries,
|
|
24062
|
+
ttlMs: o.sessionTtlMs
|
|
24063
|
+
});
|
|
24064
|
+
const built = [new LiveLineSink(), new DumpSink()];
|
|
24065
|
+
return new Observability(tracker, sinks ?? built, o.hitThreshold);
|
|
24066
|
+
}
|
|
24067
|
+
//#endregion
|
|
23438
24068
|
//#region src/proxy.ts
|
|
23439
24069
|
const injectChain = [
|
|
23440
24070
|
parseBody,
|
|
@@ -23442,12 +24072,16 @@ const injectChain = [
|
|
|
23442
24072
|
injectProvider,
|
|
23443
24073
|
injectCacheControl,
|
|
23444
24074
|
normalizeVolatileSystemMiddleware,
|
|
24075
|
+
normalizeResponsesInputMiddleware,
|
|
23445
24076
|
injectSessionId
|
|
23446
24077
|
];
|
|
23447
24078
|
function createProxyServer(source, onReady) {
|
|
23448
24079
|
const app = new Hono();
|
|
24080
|
+
const observability = createObservability(source.get());
|
|
24081
|
+
source.subscribe((config) => observability.reconfigure(config));
|
|
23449
24082
|
app.use("*", async (c, next) => {
|
|
23450
24083
|
c.set("config", source.get());
|
|
24084
|
+
c.set("observability", observability);
|
|
23451
24085
|
await next();
|
|
23452
24086
|
});
|
|
23453
24087
|
app.get("/health", (c) => {
|
|
@@ -23670,6 +24304,14 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
23670
24304
|
await cachingCommand({ configPath: args.configPath });
|
|
23671
24305
|
}
|
|
23672
24306
|
}),
|
|
24307
|
+
fixes: (0, import_cjs.command)({
|
|
24308
|
+
name: "fixes",
|
|
24309
|
+
description: "Tune compatibility fixes (interactive)",
|
|
24310
|
+
args: { ...configArgs },
|
|
24311
|
+
handler: async (args) => {
|
|
24312
|
+
await fixesCommand({ configPath: args.configPath });
|
|
24313
|
+
}
|
|
24314
|
+
}),
|
|
23673
24315
|
edit: (0, import_cjs.command)({
|
|
23674
24316
|
name: "edit",
|
|
23675
24317
|
description: "Edit an existing model override (interactive)",
|