proxitor 0.17.0 → 0.19.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 +4 -0
- package/dist/cli.mjs +426 -17
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,10 @@ Common flags: `--port`, `--host`, `--config <path>`, `--openrouter-key <key>` /
|
|
|
197
197
|
|
|
198
198
|
**Anthropic returns `400` about mixed TTLs when `cacheControlTtl: 1h`.** Set `rewriteBlockTtl: auto` (or `always`) to normalize the client's block-level `cache_control` breakpoints to the same TTL — see the [configuration reference](./docs/configuration.md#prompt-caching).
|
|
199
199
|
|
|
200
|
+
**OpenRouter returns `400 invalid_prompt | Invalid Responses API request` on `/v1/responses`.** Some clients send Responses `input` items without the `type` field OpenRouter requires. `normalizeResponses: true` (the default; off only for raw passthrough) tags them, lifts `role:"system"` into `instructions`, and adds the `id`/`status` OpenRouter wants on assistant history. It acts on `/v1/responses` only.
|
|
201
|
+
|
|
202
|
+
**Strict providers reject `role:"system"` inside `/v1/messages`.** Some clients (e.g. an injected `SessionStart` hook payload) place a `role:"system"` item mid-thread in `messages`; the Anthropic Messages API allows only `user`/`assistant` there, so providers like OpenRouter → GLM return `400 ... messages[n].role: Input should be 'user' or 'assistant'`. Enable `normalizeMessages: true` (off by default; Fixes menu or per-model override) to lift each such item's text into the top-level `system` field and drop it from `messages`. It acts on `/v1/messages` only.
|
|
203
|
+
|
|
200
204
|
**The provider keeps switching between requests.** Make sure `sessionId` is not `skip` — both `auto` (default) and `always` inject a sticky session ID; without it OpenRouter only pins after the first cache hit.
|
|
201
205
|
|
|
202
206
|
**Config edits don't take effect.** They should — proxitor hot-reloads on save. If the file is invalid the proxy keeps the last valid config; `proxitor config validate` shows what was rejected.
|
package/dist/cli.mjs
CHANGED
|
@@ -14,7 +14,7 @@ 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
16
|
import { readFile, writeFile } from "node:fs/promises";
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
//#region \0rolldown/runtime.js
|
|
19
19
|
var __defProp = Object.defineProperty;
|
|
20
20
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -12217,7 +12217,7 @@ const allowsEval = /* @__PURE__*/ cached(() => {
|
|
|
12217
12217
|
return false;
|
|
12218
12218
|
}
|
|
12219
12219
|
});
|
|
12220
|
-
function isPlainObject$
|
|
12220
|
+
function isPlainObject$3(o) {
|
|
12221
12221
|
if (isObject(o) === false) return false;
|
|
12222
12222
|
const ctor = o.constructor;
|
|
12223
12223
|
if (ctor === void 0) return true;
|
|
@@ -12228,7 +12228,7 @@ function isPlainObject$2(o) {
|
|
|
12228
12228
|
return true;
|
|
12229
12229
|
}
|
|
12230
12230
|
function shallowClone(o) {
|
|
12231
|
-
if (isPlainObject$
|
|
12231
|
+
if (isPlainObject$3(o)) return { ...o };
|
|
12232
12232
|
if (Array.isArray(o)) return [...o];
|
|
12233
12233
|
if (o instanceof Map) return new Map(o);
|
|
12234
12234
|
if (o instanceof Set) return new Set(o);
|
|
@@ -12311,7 +12311,7 @@ function omit(schema, mask) {
|
|
|
12311
12311
|
}));
|
|
12312
12312
|
}
|
|
12313
12313
|
function extend(schema, shape) {
|
|
12314
|
-
if (!isPlainObject$
|
|
12314
|
+
if (!isPlainObject$3(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
12315
12315
|
const checks = schema._zod.def.checks;
|
|
12316
12316
|
if (checks && checks.length > 0) {
|
|
12317
12317
|
const existingShape = schema._zod.def.shape;
|
|
@@ -12327,7 +12327,7 @@ function extend(schema, shape) {
|
|
|
12327
12327
|
} }));
|
|
12328
12328
|
}
|
|
12329
12329
|
function safeExtend(schema, shape) {
|
|
12330
|
-
if (!isPlainObject$
|
|
12330
|
+
if (!isPlainObject$3(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
12331
12331
|
return clone(schema, mergeDefs(schema._zod.def, { get shape() {
|
|
12332
12332
|
const _shape = {
|
|
12333
12333
|
...schema._zod.def.shape,
|
|
@@ -13862,7 +13862,7 @@ function mergeValues(a, b) {
|
|
|
13862
13862
|
valid: true,
|
|
13863
13863
|
data: a
|
|
13864
13864
|
};
|
|
13865
|
-
if (isPlainObject$
|
|
13865
|
+
if (isPlainObject$3(a) && isPlainObject$3(b)) {
|
|
13866
13866
|
const bKeys = Object.keys(b);
|
|
13867
13867
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
13868
13868
|
const newObj = {
|
|
@@ -13938,7 +13938,7 @@ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
|
|
|
13938
13938
|
$ZodType.init(inst, def);
|
|
13939
13939
|
inst._zod.parse = (payload, ctx) => {
|
|
13940
13940
|
const input = payload.value;
|
|
13941
|
-
if (!isPlainObject$
|
|
13941
|
+
if (!isPlainObject$3(input)) {
|
|
13942
13942
|
payload.issues.push({
|
|
13943
13943
|
expected: "record",
|
|
13944
13944
|
code: "invalid_type",
|
|
@@ -16293,6 +16293,8 @@ const modelOverrideSchema = object({
|
|
|
16293
16293
|
cacheControlTtl: ttlSchema.optional(),
|
|
16294
16294
|
rewriteBlockTtl: triStateSchema.optional(),
|
|
16295
16295
|
sessionId: triStateSchema.optional(),
|
|
16296
|
+
normalizeResponses: boolean().optional(),
|
|
16297
|
+
normalizeMessages: boolean().optional(),
|
|
16296
16298
|
normalizeVolatileSystem: boolean().optional()
|
|
16297
16299
|
}).strict();
|
|
16298
16300
|
const proxyConfigSchema = object({
|
|
@@ -16312,6 +16314,8 @@ const proxyConfigSchema = object({
|
|
|
16312
16314
|
cacheControlTtl: ttlSchema.optional(),
|
|
16313
16315
|
rewriteBlockTtl: triStateSchema.default("skip"),
|
|
16314
16316
|
sessionId: triStateSchema.default("auto"),
|
|
16317
|
+
normalizeResponses: boolean().default(true),
|
|
16318
|
+
normalizeMessages: boolean().default(false),
|
|
16315
16319
|
normalizeVolatileSystem: boolean().default(false),
|
|
16316
16320
|
observability: observabilityConfigSchema,
|
|
16317
16321
|
modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
|
|
@@ -16488,6 +16492,8 @@ function resolveModelConfig(config, modelName) {
|
|
|
16488
16492
|
cacheControlTtl: config.cacheControlTtl,
|
|
16489
16493
|
rewriteBlockTtl: config.rewriteBlockTtl,
|
|
16490
16494
|
sessionId: config.sessionId,
|
|
16495
|
+
normalizeResponses: config.normalizeResponses,
|
|
16496
|
+
normalizeMessages: config.normalizeMessages,
|
|
16491
16497
|
normalizeVolatileSystem: config.normalizeVolatileSystem
|
|
16492
16498
|
};
|
|
16493
16499
|
if (!modelName || !config.modelOverrides) return result;
|
|
@@ -16521,6 +16527,8 @@ function applyOverride(result, override) {
|
|
|
16521
16527
|
if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
|
|
16522
16528
|
if (override.rewriteBlockTtl !== void 0) result.rewriteBlockTtl = override.rewriteBlockTtl;
|
|
16523
16529
|
if (override.sessionId !== void 0) result.sessionId = override.sessionId;
|
|
16530
|
+
if (override.normalizeResponses !== void 0) result.normalizeResponses = override.normalizeResponses;
|
|
16531
|
+
if (override.normalizeMessages !== void 0) result.normalizeMessages = override.normalizeMessages;
|
|
16524
16532
|
if (override.normalizeVolatileSystem !== void 0) result.normalizeVolatileSystem = override.normalizeVolatileSystem;
|
|
16525
16533
|
}
|
|
16526
16534
|
/** Reject base URLs ending in /v1 — paths are forwarded as-is, so /v1 suffix causes doubled paths. */
|
|
@@ -16796,7 +16804,7 @@ const LogTypes = {
|
|
|
16796
16804
|
trace: { level: LogLevels.trace },
|
|
16797
16805
|
verbose: { level: LogLevels.verbose }
|
|
16798
16806
|
};
|
|
16799
|
-
function isPlainObject$1(value) {
|
|
16807
|
+
function isPlainObject$1$1(value) {
|
|
16800
16808
|
if (value === null || typeof value !== "object") return false;
|
|
16801
16809
|
const prototype = Object.getPrototypeOf(value);
|
|
16802
16810
|
if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
|
|
@@ -16805,7 +16813,7 @@ function isPlainObject$1(value) {
|
|
|
16805
16813
|
return true;
|
|
16806
16814
|
}
|
|
16807
16815
|
function _defu(baseObject, defaults, namespace = ".", merger) {
|
|
16808
|
-
if (!isPlainObject$1(defaults)) return _defu(baseObject, {}, namespace, merger);
|
|
16816
|
+
if (!isPlainObject$1$1(defaults)) return _defu(baseObject, {}, namespace, merger);
|
|
16809
16817
|
const object = Object.assign({}, defaults);
|
|
16810
16818
|
for (const key in baseObject) {
|
|
16811
16819
|
if (key === "__proto__" || key === "constructor") continue;
|
|
@@ -16813,7 +16821,7 @@ function _defu(baseObject, defaults, namespace = ".", merger) {
|
|
|
16813
16821
|
if (value === null || value === void 0) continue;
|
|
16814
16822
|
if (merger && merger(object, key, value, namespace)) continue;
|
|
16815
16823
|
if (Array.isArray(value) && Array.isArray(object[key])) object[key] = [...value, ...object[key]];
|
|
16816
|
-
else if (isPlainObject$1(value) && isPlainObject$1(object[key])) object[key] = _defu(value, object[key], (namespace ? `${namespace}.` : "") + key.toString(), merger);
|
|
16824
|
+
else if (isPlainObject$1$1(value) && isPlainObject$1$1(object[key])) object[key] = _defu(value, object[key], (namespace ? `${namespace}.` : "") + key.toString(), merger);
|
|
16817
16825
|
else object[key] = value;
|
|
16818
16826
|
}
|
|
16819
16827
|
return object;
|
|
@@ -16822,11 +16830,11 @@ function createDefu(merger) {
|
|
|
16822
16830
|
return (...arguments_) => arguments_.reduce((p, c) => _defu(p, c, "", merger), {});
|
|
16823
16831
|
}
|
|
16824
16832
|
const defu = createDefu();
|
|
16825
|
-
function isPlainObject(obj) {
|
|
16833
|
+
function isPlainObject$2(obj) {
|
|
16826
16834
|
return Object.prototype.toString.call(obj) === "[object Object]";
|
|
16827
16835
|
}
|
|
16828
16836
|
function isLogObj(arg) {
|
|
16829
|
-
if (!isPlainObject(arg)) return false;
|
|
16837
|
+
if (!isPlainObject$2(arg)) return false;
|
|
16830
16838
|
if (!arg.message && !arg.args) return false;
|
|
16831
16839
|
if (arg.stack) return false;
|
|
16832
16840
|
return true;
|
|
@@ -18119,6 +18127,58 @@ const REWRITE_HINTS = {
|
|
|
18119
18127
|
always: "All models",
|
|
18120
18128
|
skip: "Leave client block ttl as-is (may mismatch root)"
|
|
18121
18129
|
};
|
|
18130
|
+
/** Hint texts for the normalize-responses on/off toggle — repairs /v1/responses bodies for OpenRouter. */
|
|
18131
|
+
const NORMALIZE_RESPONSES_HINTS = {
|
|
18132
|
+
on: "Repair /v1/responses bodies (type tagging, system→instructions, id/status)",
|
|
18133
|
+
off: "Raw passthrough (OpenRouter may reject malformed /v1/responses)"
|
|
18134
|
+
};
|
|
18135
|
+
async function askNormalizeResponses(message, current, opts) {
|
|
18136
|
+
const options = [{
|
|
18137
|
+
value: true,
|
|
18138
|
+
label: "On",
|
|
18139
|
+
hint: NORMALIZE_RESPONSES_HINTS.on
|
|
18140
|
+
}, {
|
|
18141
|
+
value: false,
|
|
18142
|
+
label: "Off",
|
|
18143
|
+
hint: NORMALIZE_RESPONSES_HINTS.off
|
|
18144
|
+
}];
|
|
18145
|
+
if (opts?.removable) options.push({
|
|
18146
|
+
value: "reset",
|
|
18147
|
+
label: "Reset / inherit",
|
|
18148
|
+
hint: opts.resetHint ?? "Remove override"
|
|
18149
|
+
});
|
|
18150
|
+
return await select({
|
|
18151
|
+
message,
|
|
18152
|
+
initialValue: current ?? (opts?.removable ? "reset" : false),
|
|
18153
|
+
options
|
|
18154
|
+
});
|
|
18155
|
+
}
|
|
18156
|
+
/** Hint texts for the normalize-messages on/off toggle — lifts stray role:"system" out of /v1/messages. */
|
|
18157
|
+
const NORMALIZE_MESSAGES_HINTS = {
|
|
18158
|
+
on: "Lift role:system → top-level system (/v1/messages only)",
|
|
18159
|
+
off: "Raw passthrough (strict providers may reject role:system)"
|
|
18160
|
+
};
|
|
18161
|
+
async function askNormalizeMessages(message, current, opts) {
|
|
18162
|
+
const options = [{
|
|
18163
|
+
value: true,
|
|
18164
|
+
label: "On",
|
|
18165
|
+
hint: NORMALIZE_MESSAGES_HINTS.on
|
|
18166
|
+
}, {
|
|
18167
|
+
value: false,
|
|
18168
|
+
label: "Off",
|
|
18169
|
+
hint: NORMALIZE_MESSAGES_HINTS.off
|
|
18170
|
+
}];
|
|
18171
|
+
if (opts?.removable) options.push({
|
|
18172
|
+
value: "reset",
|
|
18173
|
+
label: "Reset / inherit",
|
|
18174
|
+
hint: opts.resetHint ?? "Remove override"
|
|
18175
|
+
});
|
|
18176
|
+
return await select({
|
|
18177
|
+
message,
|
|
18178
|
+
initialValue: current ?? (opts?.removable ? "reset" : false),
|
|
18179
|
+
options
|
|
18180
|
+
});
|
|
18181
|
+
}
|
|
18122
18182
|
const NORMALIZE_HINTS = {
|
|
18123
18183
|
on: "Rewrite cch → stable prefix cache",
|
|
18124
18184
|
off: "Passthrough — rewrite nothing"
|
|
@@ -18223,6 +18283,18 @@ async function collectSessionTriState(currentSid) {
|
|
|
18223
18283
|
if (sid === "reset") return { sessionId: { remove: true } };
|
|
18224
18284
|
return { sessionId: { value: sid } };
|
|
18225
18285
|
}
|
|
18286
|
+
async function collectNormalizeResponses(currentNr) {
|
|
18287
|
+
const nr = await askNormalizeResponses("Repair /v1/responses bodies", currentNr, { removable: true });
|
|
18288
|
+
if (typeof nr === "symbol") return null;
|
|
18289
|
+
if (nr === "reset") return { normalizeResponses: { remove: true } };
|
|
18290
|
+
return { normalizeResponses: { value: nr } };
|
|
18291
|
+
}
|
|
18292
|
+
async function collectNormalizeMessages(currentNm) {
|
|
18293
|
+
const nm = await askNormalizeMessages("Lift role:system out of /v1/messages", currentNm, { removable: true });
|
|
18294
|
+
if (typeof nm === "symbol") return null;
|
|
18295
|
+
if (nm === "reset") return { normalizeMessages: { remove: true } };
|
|
18296
|
+
return { normalizeMessages: { value: nm } };
|
|
18297
|
+
}
|
|
18226
18298
|
/**
|
|
18227
18299
|
* Collects cacheControl mode, TTL, and rewriteBlockTtl.
|
|
18228
18300
|
* Mode-cancel aborts; TTL-cancel keeps existing TTL; rewrite-cancel keeps existing rewrite.
|
|
@@ -18670,6 +18742,20 @@ async function editSessionId(current) {
|
|
|
18670
18742
|
applyField(next, "sessionId", result.sessionId);
|
|
18671
18743
|
return next;
|
|
18672
18744
|
}
|
|
18745
|
+
async function editNormalizeResponses(current) {
|
|
18746
|
+
const result = await collectNormalizeResponses(current.normalizeResponses);
|
|
18747
|
+
if (result === null) return current;
|
|
18748
|
+
const next = { ...current };
|
|
18749
|
+
applyField(next, "normalizeResponses", result.normalizeResponses);
|
|
18750
|
+
return next;
|
|
18751
|
+
}
|
|
18752
|
+
async function editNormalizeMessages(current) {
|
|
18753
|
+
const result = await collectNormalizeMessages(current.normalizeMessages);
|
|
18754
|
+
if (result === null) return current;
|
|
18755
|
+
const next = { ...current };
|
|
18756
|
+
applyField(next, "normalizeMessages", result.normalizeMessages);
|
|
18757
|
+
return next;
|
|
18758
|
+
}
|
|
18673
18759
|
async function editCacheControl(current, configPath) {
|
|
18674
18760
|
const globalTtl = readGlobalTtl(configPath);
|
|
18675
18761
|
const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl, current.rewriteBlockTtl);
|
|
@@ -18777,8 +18863,110 @@ async function cachingCommand(opts) {
|
|
|
18777
18863
|
outro("Bye!");
|
|
18778
18864
|
}
|
|
18779
18865
|
//#endregion
|
|
18866
|
+
//#region src/commands/config/normalize-messages.ts
|
|
18867
|
+
async function normalizeMessagesCommand(opts) {
|
|
18868
|
+
const configPath = requireConfigPath(opts?.configPath);
|
|
18869
|
+
const raw = readConfigFileRaw(configPath).normalizeMessages;
|
|
18870
|
+
const effective = raw ?? DEFAULTS.normalizeMessages;
|
|
18871
|
+
log.info(`Current: normalizeMessages = ${raw === void 0 ? `(default -> ${effective ? "on" : "off"})` : effective}`);
|
|
18872
|
+
const choice = await askNormalizeMessages("Lift stray role:\"system\" out of /v1/messages into top-level system? Fixes 400 rejections from strict Anthropic-format providers (OpenRouter → GLM et al.). Acts on /v1/messages only.", raw, {
|
|
18873
|
+
removable: true,
|
|
18874
|
+
resetHint: `remove (default: ${DEFAULTS.normalizeMessages ? "on" : "off"})`
|
|
18875
|
+
});
|
|
18876
|
+
if (typeof choice === "symbol") return;
|
|
18877
|
+
const fields = {};
|
|
18878
|
+
fields.normalizeMessages = choice === "reset" ? void 0 : choice;
|
|
18879
|
+
setGlobalConfigFields(configPath, fields);
|
|
18880
|
+
const label = choice === "reset" ? `(default: ${DEFAULTS.normalizeMessages})` : choice;
|
|
18881
|
+
log.success(`normalizeMessages set to ${label}`);
|
|
18882
|
+
}
|
|
18883
|
+
//#endregion
|
|
18884
|
+
//#region src/commands/config/normalize-responses.ts
|
|
18885
|
+
async function normalizeResponsesCommand(opts) {
|
|
18886
|
+
const configPath = requireConfigPath(opts?.configPath);
|
|
18887
|
+
const raw = readConfigFileRaw(configPath).normalizeResponses;
|
|
18888
|
+
const effective = raw ?? DEFAULTS.normalizeResponses;
|
|
18889
|
+
log.info(`Current: normalizeResponses = ${raw === void 0 ? `(default -> ${effective ? "on" : "off"})` : effective}`);
|
|
18890
|
+
const choice = await askNormalizeResponses("Repair /v1/responses request bodies for OpenRouter (tag input types, lift role:\"system\" into instructions, synthesize assistant id/status)? Acts on /v1/responses only.", raw, {
|
|
18891
|
+
removable: true,
|
|
18892
|
+
resetHint: `remove (default: ${DEFAULTS.normalizeResponses ? "on" : "off"})`
|
|
18893
|
+
});
|
|
18894
|
+
if (typeof choice === "symbol") return;
|
|
18895
|
+
const fields = {};
|
|
18896
|
+
fields.normalizeResponses = choice === "reset" ? void 0 : choice;
|
|
18897
|
+
setGlobalConfigFields(configPath, fields);
|
|
18898
|
+
const label = choice === "reset" ? `(default: ${DEFAULTS.normalizeResponses})` : choice;
|
|
18899
|
+
log.success(`normalizeResponses set to ${label}`);
|
|
18900
|
+
}
|
|
18901
|
+
//#endregion
|
|
18902
|
+
//#region src/commands/config/fixes-menu.ts
|
|
18903
|
+
/** Compatibility fixes for request bodies OpenRouter rejects. Mirrors caching-menu's lever table. */
|
|
18904
|
+
const FIXES_LEVERS = [{
|
|
18905
|
+
value: "normalizeResponses",
|
|
18906
|
+
label: "Repair /v1/responses bodies (normalizeResponses)",
|
|
18907
|
+
global: (configPath) => normalizeResponsesCommand({ configPath }),
|
|
18908
|
+
perModel: (current) => editNormalizeResponses(current)
|
|
18909
|
+
}, {
|
|
18910
|
+
value: "normalizeMessages",
|
|
18911
|
+
label: "Lift role:system out of /v1/messages (normalizeMessages)",
|
|
18912
|
+
global: (configPath) => normalizeMessagesCommand({ configPath }),
|
|
18913
|
+
perModel: (current) => editNormalizeMessages(current)
|
|
18914
|
+
}];
|
|
18915
|
+
async function runFixesLeverMenu(opts) {
|
|
18916
|
+
for (;;) {
|
|
18917
|
+
note(opts.renderNote(), opts.noteTitle);
|
|
18918
|
+
const choice = await select({
|
|
18919
|
+
message: "Tune which fix?",
|
|
18920
|
+
options: [...FIXES_LEVERS.map((lever) => ({
|
|
18921
|
+
value: lever.value,
|
|
18922
|
+
label: lever.label
|
|
18923
|
+
})), {
|
|
18924
|
+
value: "back",
|
|
18925
|
+
label: opts.backLabel
|
|
18926
|
+
}]
|
|
18927
|
+
});
|
|
18928
|
+
if (isCancel(choice) || choice === "back") return;
|
|
18929
|
+
const chosen = FIXES_LEVERS.find((l) => l.value === choice);
|
|
18930
|
+
if (chosen) await opts.onLever(chosen);
|
|
18931
|
+
}
|
|
18932
|
+
}
|
|
18933
|
+
async function globalFixesMenu(opts) {
|
|
18934
|
+
const configPath = requireConfigPath(opts?.configPath);
|
|
18935
|
+
await runFixesLeverMenu({
|
|
18936
|
+
noteTitle: "Fixes",
|
|
18937
|
+
backLabel: "← Back",
|
|
18938
|
+
renderNote: () => {
|
|
18939
|
+
const raw = readConfigFileRaw(configPath);
|
|
18940
|
+
return [`normalizeResponses: ${raw.normalizeResponses ?? "(default → on)"}`, `normalizeMessages: ${raw.normalizeMessages ?? "(default → off)"}`].join("\n");
|
|
18941
|
+
},
|
|
18942
|
+
onLever: (lever) => lever.global(configPath)
|
|
18943
|
+
});
|
|
18944
|
+
}
|
|
18945
|
+
/** Self-persists each lever; returns the latest override. */
|
|
18946
|
+
async function perModelFixesMenu(opts) {
|
|
18947
|
+
let current = opts.current;
|
|
18948
|
+
await runFixesLeverMenu({
|
|
18949
|
+
noteTitle: `Fixes for "${opts.modelKey}"`,
|
|
18950
|
+
backLabel: "← Back to override edit",
|
|
18951
|
+
renderNote: () => [`normalizeResponses: ${current.normalizeResponses ?? "(inherit)"}`, `normalizeMessages: ${current.normalizeMessages ?? "(inherit)"}`].join("\n"),
|
|
18952
|
+
onLever: async (lever) => {
|
|
18953
|
+
const next = await lever.perModel(current);
|
|
18954
|
+
if (!overridesEqual(next, current)) {
|
|
18955
|
+
setModelOverride(opts.configPath, opts.modelKey, next);
|
|
18956
|
+
current = next;
|
|
18957
|
+
}
|
|
18958
|
+
}
|
|
18959
|
+
});
|
|
18960
|
+
return current;
|
|
18961
|
+
}
|
|
18962
|
+
async function fixesCommand(opts) {
|
|
18963
|
+
intro("Proxitor · Fixes");
|
|
18964
|
+
await globalFixesMenu(opts);
|
|
18965
|
+
outro("Bye!");
|
|
18966
|
+
}
|
|
18967
|
+
//#endregion
|
|
18780
18968
|
//#region src/commands/config/edit.ts
|
|
18781
|
-
function
|
|
18969
|
+
function toggleHint(value) {
|
|
18782
18970
|
if (value === void 0) return "(inherit)";
|
|
18783
18971
|
return value ? "on" : "off";
|
|
18784
18972
|
}
|
|
@@ -18791,12 +18979,16 @@ function formatOverrideHint(override) {
|
|
|
18791
18979
|
}
|
|
18792
18980
|
if (override.sessionId) parts.push(`session: ${override.sessionId}`);
|
|
18793
18981
|
if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
|
|
18794
|
-
if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${
|
|
18982
|
+
if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${toggleHint(override.normalizeVolatileSystem)}`);
|
|
18983
|
+
if (override.normalizeResponses !== void 0) parts.push(`fix: ${toggleHint(override.normalizeResponses)}`);
|
|
18795
18984
|
if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
|
|
18796
18985
|
return parts.join(", ") || "(empty)";
|
|
18797
18986
|
}
|
|
18798
18987
|
function formatCachingHint(current) {
|
|
18799
|
-
return `cc ${current.cacheControl ?? "inherit"} · ttl ${current.cacheControlTtl ? describeTtl(current.cacheControlTtl) : "inherit"} · sid ${current.sessionId ?? "inherit"} · nvs ${
|
|
18988
|
+
return `cc ${current.cacheControl ?? "inherit"} · ttl ${current.cacheControlTtl ? describeTtl(current.cacheControlTtl) : "inherit"} · sid ${current.sessionId ?? "inherit"} · nvs ${toggleHint(current.normalizeVolatileSystem)}`;
|
|
18989
|
+
}
|
|
18990
|
+
function formatFixesHint(current) {
|
|
18991
|
+
return `normalizeResponses: ${current.normalizeResponses ?? "inherit"}`;
|
|
18800
18992
|
}
|
|
18801
18993
|
async function editProvider(modelKey, current, client) {
|
|
18802
18994
|
const isPattern = modelKey.includes("*");
|
|
@@ -18851,6 +19043,11 @@ async function editOverrideCommand(client, configPath) {
|
|
|
18851
19043
|
label: "💾 Caching",
|
|
18852
19044
|
hint: formatCachingHint(current)
|
|
18853
19045
|
},
|
|
19046
|
+
{
|
|
19047
|
+
value: "fixes",
|
|
19048
|
+
label: "🛠 Fixes",
|
|
19049
|
+
hint: formatFixesHint(current)
|
|
19050
|
+
},
|
|
18854
19051
|
{
|
|
18855
19052
|
value: "done",
|
|
18856
19053
|
label: "✓ Done"
|
|
@@ -18866,6 +19063,14 @@ async function editOverrideCommand(client, configPath) {
|
|
|
18866
19063
|
});
|
|
18867
19064
|
continue;
|
|
18868
19065
|
}
|
|
19066
|
+
if (field === "fixes") {
|
|
19067
|
+
current = await perModelFixesMenu({
|
|
19068
|
+
modelKey,
|
|
19069
|
+
current,
|
|
19070
|
+
configPath: resolvedConfigPath
|
|
19071
|
+
});
|
|
19072
|
+
continue;
|
|
19073
|
+
}
|
|
18869
19074
|
const before = current;
|
|
18870
19075
|
current = await editProvider(modelKey, current, client);
|
|
18871
19076
|
if (!overridesEqual(current, before)) setModelOverride(resolvedConfigPath, modelKey, current);
|
|
@@ -19493,6 +19698,10 @@ async function runConfigMenu(client) {
|
|
|
19493
19698
|
value: "caching",
|
|
19494
19699
|
label: "💾 Caching"
|
|
19495
19700
|
},
|
|
19701
|
+
{
|
|
19702
|
+
value: "fixes",
|
|
19703
|
+
label: "🛠 Fixes"
|
|
19704
|
+
},
|
|
19496
19705
|
{
|
|
19497
19706
|
value: "_sep2",
|
|
19498
19707
|
label: "── Model Overrides ──",
|
|
@@ -19547,6 +19756,9 @@ async function runConfigMenu(client) {
|
|
|
19547
19756
|
case "caching":
|
|
19548
19757
|
await globalCachingMenu();
|
|
19549
19758
|
break;
|
|
19759
|
+
case "fixes":
|
|
19760
|
+
await globalFixesMenu();
|
|
19761
|
+
break;
|
|
19550
19762
|
case "add":
|
|
19551
19763
|
await addOverrideCommand({ client });
|
|
19552
19764
|
break;
|
|
@@ -19570,7 +19782,7 @@ async function runConfigMenu(client) {
|
|
|
19570
19782
|
}
|
|
19571
19783
|
//#endregion
|
|
19572
19784
|
//#region src/version.ts
|
|
19573
|
-
const version = "0.
|
|
19785
|
+
const version = "0.19.0";
|
|
19574
19786
|
//#endregion
|
|
19575
19787
|
//#region src/commands/doctor.ts
|
|
19576
19788
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
@@ -23426,7 +23638,12 @@ function rewriteBlockTtls(body, ttl, isAnthropic) {
|
|
|
23426
23638
|
visitBlocks(body.system);
|
|
23427
23639
|
visitBlocks(body.tools);
|
|
23428
23640
|
const messages = body.messages;
|
|
23429
|
-
for (const m of messages ?? [])
|
|
23641
|
+
for (const m of messages ?? []) {
|
|
23642
|
+
if (m === null || typeof m !== "object" || Array.isArray(m)) continue;
|
|
23643
|
+
rewriteNode(m);
|
|
23644
|
+
visitBlocks(m.content);
|
|
23645
|
+
visitBlocks(m.tool_calls);
|
|
23646
|
+
}
|
|
23430
23647
|
return mutated;
|
|
23431
23648
|
}
|
|
23432
23649
|
//#endregion
|
|
@@ -23527,6 +23744,188 @@ const injectSessionId = createMiddleware(async (c, next) => {
|
|
|
23527
23744
|
await next();
|
|
23528
23745
|
});
|
|
23529
23746
|
//#endregion
|
|
23747
|
+
//#region src/proxy/utils/messages-system.ts
|
|
23748
|
+
/**
|
|
23749
|
+
* Whether to lift stray `role:"system"` items out of `messages`. The lift is
|
|
23750
|
+
* only valid on `/v1/messages` (where `system` is a top-level field) — never on
|
|
23751
|
+
* chat-completions (system belongs in `messages`) or responses — so it is gated
|
|
23752
|
+
* by endpoint regardless of the on/off setting.
|
|
23753
|
+
*/
|
|
23754
|
+
function shouldNormalizeMessages(enabled, path) {
|
|
23755
|
+
return enabled && classifyEndpoint(path) === "messages";
|
|
23756
|
+
}
|
|
23757
|
+
function isPlainObject$1(value) {
|
|
23758
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23759
|
+
}
|
|
23760
|
+
/** Flatten a message content value (string or block array) to plain text. */
|
|
23761
|
+
function contentToText$1(content) {
|
|
23762
|
+
if (typeof content === "string") return content;
|
|
23763
|
+
if (!Array.isArray(content)) return void 0;
|
|
23764
|
+
const parts = [];
|
|
23765
|
+
for (const block of content) {
|
|
23766
|
+
const text = isPlainObject$1(block) ? block.text : void 0;
|
|
23767
|
+
if (typeof text === "string") parts.push(text);
|
|
23768
|
+
}
|
|
23769
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
23770
|
+
}
|
|
23771
|
+
/**
|
|
23772
|
+
* Append lifted system text to a top-level `system` value, preserving its form:
|
|
23773
|
+
* a block array stays an array (new `{type:"text"}` block), a string stays a
|
|
23774
|
+
* string, and a missing system starts as a plain string. A stray single-block
|
|
23775
|
+
* object (malformed but parseable) is wrapped as a one-element array so its
|
|
23776
|
+
* content is never silently discarded.
|
|
23777
|
+
*/
|
|
23778
|
+
function appendSystemText(current, text) {
|
|
23779
|
+
if (Array.isArray(current)) return [...current, {
|
|
23780
|
+
type: "text",
|
|
23781
|
+
text
|
|
23782
|
+
}];
|
|
23783
|
+
if (typeof current === "string") return current.length > 0 ? `${current}\n\n${text}` : text;
|
|
23784
|
+
if (isPlainObject$1(current)) return [current, {
|
|
23785
|
+
type: "text",
|
|
23786
|
+
text
|
|
23787
|
+
}];
|
|
23788
|
+
return text;
|
|
23789
|
+
}
|
|
23790
|
+
/**
|
|
23791
|
+
* Lift every `role:"system"` item in `messages` into the top-level `system`
|
|
23792
|
+
* field. The Anthropic Messages API allows only `user`/`assistant` in
|
|
23793
|
+
* `messages` — a stray `role:"system"` (e.g. injected hook output like a
|
|
23794
|
+
* SessionStart payload) is rejected by strict Anthropic-format providers
|
|
23795
|
+
* (OpenRouter → GLM et al.) with a 400 on `messages[n].role`. System content
|
|
23796
|
+
* belongs in the top-level `system`, so its text is merged there and the item
|
|
23797
|
+
* is dropped from `messages`, which also preserves user/assistant alternation.
|
|
23798
|
+
* Items with no extractable text are dropped. Idempotent; returns whether the
|
|
23799
|
+
* body changed.
|
|
23800
|
+
*/
|
|
23801
|
+
function liftSystemMessages(body) {
|
|
23802
|
+
const messages = body.messages;
|
|
23803
|
+
if (!Array.isArray(messages)) return false;
|
|
23804
|
+
let system = body.system;
|
|
23805
|
+
const next = [];
|
|
23806
|
+
let mutated = false;
|
|
23807
|
+
for (const raw of messages) {
|
|
23808
|
+
if (isPlainObject$1(raw) && raw.role === "system") {
|
|
23809
|
+
const text = contentToText$1(raw.content);
|
|
23810
|
+
if (text !== void 0) system = appendSystemText(system, text);
|
|
23811
|
+
mutated = true;
|
|
23812
|
+
continue;
|
|
23813
|
+
}
|
|
23814
|
+
next.push(raw);
|
|
23815
|
+
}
|
|
23816
|
+
if (mutated) {
|
|
23817
|
+
body.messages = next;
|
|
23818
|
+
if (system !== body.system) body.system = system;
|
|
23819
|
+
}
|
|
23820
|
+
return mutated;
|
|
23821
|
+
}
|
|
23822
|
+
//#endregion
|
|
23823
|
+
//#region src/proxy/middleware/normalize-messages.ts
|
|
23824
|
+
/** Lift stray role:"system" items from messages into top-level system (see liftSystemMessages); no-op for non-messages endpoints. */
|
|
23825
|
+
const normalizeMessagesMiddleware = createMiddleware(async (c, next) => {
|
|
23826
|
+
const parsedBody = c.var.parsedBody;
|
|
23827
|
+
if (parsedBody && shouldNormalizeMessages(c.var.resolvedConfig.normalizeMessages, c.req.path) && liftSystemMessages(parsedBody)) c.set("bodyMutated", true);
|
|
23828
|
+
await next();
|
|
23829
|
+
});
|
|
23830
|
+
//#endregion
|
|
23831
|
+
//#region src/proxy/utils/responses-input.ts
|
|
23832
|
+
/**
|
|
23833
|
+
* Whether to normalize a Responses-api body. The lift is only valid on
|
|
23834
|
+
* /v1/responses — never on messages or chat-completions — so it is gated by
|
|
23835
|
+
* endpoint regardless of the on/off setting.
|
|
23836
|
+
*/
|
|
23837
|
+
function shouldNormalizeResponses(enabled, path) {
|
|
23838
|
+
return enabled && classifyEndpoint(path) === "responses";
|
|
23839
|
+
}
|
|
23840
|
+
function isPlainObject(value) {
|
|
23841
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23842
|
+
}
|
|
23843
|
+
/** Flatten a Responses content value (string or block array) to plain text. */
|
|
23844
|
+
function contentToText(content) {
|
|
23845
|
+
if (typeof content === "string") return content;
|
|
23846
|
+
if (!Array.isArray(content)) return void 0;
|
|
23847
|
+
const parts = [];
|
|
23848
|
+
for (const block of content) {
|
|
23849
|
+
const text = isPlainObject(block) ? block.text : void 0;
|
|
23850
|
+
if (typeof text === "string") parts.push(text);
|
|
23851
|
+
}
|
|
23852
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
23853
|
+
}
|
|
23854
|
+
function appendInstructions(current, text) {
|
|
23855
|
+
return typeof current === "string" ? `${current}\n\n${text}` : text;
|
|
23856
|
+
}
|
|
23857
|
+
/** Tag a role-bearing message item and complete assistant-history metadata. */
|
|
23858
|
+
function tagMessageItem(obj) {
|
|
23859
|
+
let changed = false;
|
|
23860
|
+
if (typeof obj.role === "string" && obj.type === void 0) {
|
|
23861
|
+
obj.type = "message";
|
|
23862
|
+
changed = true;
|
|
23863
|
+
}
|
|
23864
|
+
if (obj.type === "message" && obj.role === "assistant") {
|
|
23865
|
+
if (obj.id === void 0) {
|
|
23866
|
+
obj.id = `msg_${randomUUID().slice(0, 12)}`;
|
|
23867
|
+
changed = true;
|
|
23868
|
+
}
|
|
23869
|
+
if (obj.status === void 0) {
|
|
23870
|
+
obj.status = "completed";
|
|
23871
|
+
changed = true;
|
|
23872
|
+
}
|
|
23873
|
+
}
|
|
23874
|
+
return changed;
|
|
23875
|
+
}
|
|
23876
|
+
/**
|
|
23877
|
+
* Lift a `role: "system"` item: OpenRouter Responses has no system role in
|
|
23878
|
+
* `input`, so its text moves to `instructions`. Items with no extractable text
|
|
23879
|
+
* (image-only/empty) can't be represented there and are dropped — keeping them
|
|
23880
|
+
* as `role: "system"` would reproduce the rejection this normalizer exists to fix.
|
|
23881
|
+
*/
|
|
23882
|
+
function handleSystemItem(obj, state) {
|
|
23883
|
+
const text = contentToText(obj.content);
|
|
23884
|
+
if (text !== void 0) state.instructions = appendInstructions(state.instructions, text);
|
|
23885
|
+
}
|
|
23886
|
+
/**
|
|
23887
|
+
* Make a Responses body satisfy OpenRouter's `input` schema. Each item is a
|
|
23888
|
+
* `type`-discriminated union, so items missing `type` (OpenAI infers "message")
|
|
23889
|
+
* are rejected as `invalid_prompt`. Also relocates `role: "system"` items to
|
|
23890
|
+
* `instructions` and synthesizes the `id`/`status` assistant history requires.
|
|
23891
|
+
* Idempotent; returns whether the body changed.
|
|
23892
|
+
*/
|
|
23893
|
+
function normalizeResponsesInput(body) {
|
|
23894
|
+
const input = body.input;
|
|
23895
|
+
if (!Array.isArray(input)) return false;
|
|
23896
|
+
let mutated = false;
|
|
23897
|
+
const state = {
|
|
23898
|
+
instructions: body.instructions,
|
|
23899
|
+
next: []
|
|
23900
|
+
};
|
|
23901
|
+
for (const raw of input) {
|
|
23902
|
+
if (!isPlainObject(raw)) {
|
|
23903
|
+
state.next.push(raw);
|
|
23904
|
+
continue;
|
|
23905
|
+
}
|
|
23906
|
+
if (raw.role === "system") {
|
|
23907
|
+
handleSystemItem(raw, state);
|
|
23908
|
+
mutated = true;
|
|
23909
|
+
continue;
|
|
23910
|
+
}
|
|
23911
|
+
if (tagMessageItem(raw)) mutated = true;
|
|
23912
|
+
state.next.push(raw);
|
|
23913
|
+
}
|
|
23914
|
+
if (mutated) {
|
|
23915
|
+
body.input = state.next;
|
|
23916
|
+
if (state.instructions !== void 0) body.instructions = state.instructions;
|
|
23917
|
+
}
|
|
23918
|
+
return mutated;
|
|
23919
|
+
}
|
|
23920
|
+
//#endregion
|
|
23921
|
+
//#region src/proxy/middleware/normalize-responses-input.ts
|
|
23922
|
+
/** Normalize Responses-API input for OpenRouter's schema (see normalizeResponsesInput); no-op for chat/messages. */
|
|
23923
|
+
const normalizeResponsesInputMiddleware = createMiddleware(async (c, next) => {
|
|
23924
|
+
const parsedBody = c.var.parsedBody;
|
|
23925
|
+
if (parsedBody && shouldNormalizeResponses(c.var.resolvedConfig.normalizeResponses, c.req.path) && normalizeResponsesInput(parsedBody)) c.set("bodyMutated", true);
|
|
23926
|
+
await next();
|
|
23927
|
+
});
|
|
23928
|
+
//#endregion
|
|
23530
23929
|
//#region src/proxy/middleware/normalize-volatile-system.ts
|
|
23531
23930
|
/** 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:
|
|
23532
23931
|
* - `cch=…` per-turn hash
|
|
@@ -23842,8 +24241,10 @@ const injectChain = [
|
|
|
23842
24241
|
parseBody,
|
|
23843
24242
|
resolveConfig,
|
|
23844
24243
|
injectProvider,
|
|
24244
|
+
normalizeMessagesMiddleware,
|
|
23845
24245
|
injectCacheControl,
|
|
23846
24246
|
normalizeVolatileSystemMiddleware,
|
|
24247
|
+
normalizeResponsesInputMiddleware,
|
|
23847
24248
|
injectSessionId
|
|
23848
24249
|
];
|
|
23849
24250
|
function createProxyServer(source, onReady) {
|
|
@@ -24075,6 +24476,14 @@ const configCli = (0, import_cjs.subcommands)({
|
|
|
24075
24476
|
await cachingCommand({ configPath: args.configPath });
|
|
24076
24477
|
}
|
|
24077
24478
|
}),
|
|
24479
|
+
fixes: (0, import_cjs.command)({
|
|
24480
|
+
name: "fixes",
|
|
24481
|
+
description: "Tune compatibility fixes (interactive)",
|
|
24482
|
+
args: { ...configArgs },
|
|
24483
|
+
handler: async (args) => {
|
|
24484
|
+
await fixesCommand({ configPath: args.configPath });
|
|
24485
|
+
}
|
|
24486
|
+
}),
|
|
24078
24487
|
edit: (0, import_cjs.command)({
|
|
24079
24488
|
name: "edit",
|
|
24080
24489
|
description: "Edit an existing model override (interactive)",
|