proxitor 0.18.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 +3 -1
- package/dist/cli.mjs +207 -35
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,7 +197,9 @@ 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:
|
|
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.
|
|
201
203
|
|
|
202
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.
|
|
203
205
|
|
package/dist/cli.mjs
CHANGED
|
@@ -16293,7 +16293,8 @@ const modelOverrideSchema = object({
|
|
|
16293
16293
|
cacheControlTtl: ttlSchema.optional(),
|
|
16294
16294
|
rewriteBlockTtl: triStateSchema.optional(),
|
|
16295
16295
|
sessionId: triStateSchema.optional(),
|
|
16296
|
-
normalizeResponses:
|
|
16296
|
+
normalizeResponses: boolean().optional(),
|
|
16297
|
+
normalizeMessages: boolean().optional(),
|
|
16297
16298
|
normalizeVolatileSystem: boolean().optional()
|
|
16298
16299
|
}).strict();
|
|
16299
16300
|
const proxyConfigSchema = object({
|
|
@@ -16313,7 +16314,8 @@ const proxyConfigSchema = object({
|
|
|
16313
16314
|
cacheControlTtl: ttlSchema.optional(),
|
|
16314
16315
|
rewriteBlockTtl: triStateSchema.default("skip"),
|
|
16315
16316
|
sessionId: triStateSchema.default("auto"),
|
|
16316
|
-
normalizeResponses:
|
|
16317
|
+
normalizeResponses: boolean().default(true),
|
|
16318
|
+
normalizeMessages: boolean().default(false),
|
|
16317
16319
|
normalizeVolatileSystem: boolean().default(false),
|
|
16318
16320
|
observability: observabilityConfigSchema,
|
|
16319
16321
|
modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
|
|
@@ -16491,6 +16493,7 @@ function resolveModelConfig(config, modelName) {
|
|
|
16491
16493
|
rewriteBlockTtl: config.rewriteBlockTtl,
|
|
16492
16494
|
sessionId: config.sessionId,
|
|
16493
16495
|
normalizeResponses: config.normalizeResponses,
|
|
16496
|
+
normalizeMessages: config.normalizeMessages,
|
|
16494
16497
|
normalizeVolatileSystem: config.normalizeVolatileSystem
|
|
16495
16498
|
};
|
|
16496
16499
|
if (!modelName || !config.modelOverrides) return result;
|
|
@@ -16525,6 +16528,7 @@ function applyOverride(result, override) {
|
|
|
16525
16528
|
if (override.rewriteBlockTtl !== void 0) result.rewriteBlockTtl = override.rewriteBlockTtl;
|
|
16526
16529
|
if (override.sessionId !== void 0) result.sessionId = override.sessionId;
|
|
16527
16530
|
if (override.normalizeResponses !== void 0) result.normalizeResponses = override.normalizeResponses;
|
|
16531
|
+
if (override.normalizeMessages !== void 0) result.normalizeMessages = override.normalizeMessages;
|
|
16528
16532
|
if (override.normalizeVolatileSystem !== void 0) result.normalizeVolatileSystem = override.normalizeVolatileSystem;
|
|
16529
16533
|
}
|
|
16530
16534
|
/** Reject base URLs ending in /v1 — paths are forwarded as-is, so /v1 suffix causes doubled paths. */
|
|
@@ -16800,7 +16804,7 @@ const LogTypes = {
|
|
|
16800
16804
|
trace: { level: LogLevels.trace },
|
|
16801
16805
|
verbose: { level: LogLevels.verbose }
|
|
16802
16806
|
};
|
|
16803
|
-
function isPlainObject$1(value) {
|
|
16807
|
+
function isPlainObject$1$1(value) {
|
|
16804
16808
|
if (value === null || typeof value !== "object") return false;
|
|
16805
16809
|
const prototype = Object.getPrototypeOf(value);
|
|
16806
16810
|
if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
|
|
@@ -16809,7 +16813,7 @@ function isPlainObject$1(value) {
|
|
|
16809
16813
|
return true;
|
|
16810
16814
|
}
|
|
16811
16815
|
function _defu(baseObject, defaults, namespace = ".", merger) {
|
|
16812
|
-
if (!isPlainObject$1(defaults)) return _defu(baseObject, {}, namespace, merger);
|
|
16816
|
+
if (!isPlainObject$1$1(defaults)) return _defu(baseObject, {}, namespace, merger);
|
|
16813
16817
|
const object = Object.assign({}, defaults);
|
|
16814
16818
|
for (const key in baseObject) {
|
|
16815
16819
|
if (key === "__proto__" || key === "constructor") continue;
|
|
@@ -16817,7 +16821,7 @@ function _defu(baseObject, defaults, namespace = ".", merger) {
|
|
|
16817
16821
|
if (value === null || value === void 0) continue;
|
|
16818
16822
|
if (merger && merger(object, key, value, namespace)) continue;
|
|
16819
16823
|
if (Array.isArray(value) && Array.isArray(object[key])) object[key] = [...value, ...object[key]];
|
|
16820
|
-
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);
|
|
16821
16825
|
else object[key] = value;
|
|
16822
16826
|
}
|
|
16823
16827
|
return object;
|
|
@@ -18123,12 +18127,58 @@ const REWRITE_HINTS = {
|
|
|
18123
18127
|
always: "All models",
|
|
18124
18128
|
skip: "Leave client block ttl as-is (may mismatch root)"
|
|
18125
18129
|
};
|
|
18126
|
-
/**
|
|
18130
|
+
/** Hint texts for the normalize-responses on/off toggle — repairs /v1/responses bodies for OpenRouter. */
|
|
18127
18131
|
const NORMALIZE_RESPONSES_HINTS = {
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
skip: "Off — raw passthrough (OpenRouter may reject)"
|
|
18132
|
+
on: "Repair /v1/responses bodies (type tagging, system→instructions, id/status)",
|
|
18133
|
+
off: "Raw passthrough (OpenRouter may reject malformed /v1/responses)"
|
|
18131
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
|
+
}
|
|
18132
18182
|
const NORMALIZE_HINTS = {
|
|
18133
18183
|
on: "Rewrite cch → stable prefix cache",
|
|
18134
18184
|
off: "Passthrough — rewrite nothing"
|
|
@@ -18233,12 +18283,18 @@ async function collectSessionTriState(currentSid) {
|
|
|
18233
18283
|
if (sid === "reset") return { sessionId: { remove: true } };
|
|
18234
18284
|
return { sessionId: { value: sid } };
|
|
18235
18285
|
}
|
|
18236
|
-
async function
|
|
18237
|
-
const nr = await
|
|
18286
|
+
async function collectNormalizeResponses(currentNr) {
|
|
18287
|
+
const nr = await askNormalizeResponses("Repair /v1/responses bodies", currentNr, { removable: true });
|
|
18238
18288
|
if (typeof nr === "symbol") return null;
|
|
18239
18289
|
if (nr === "reset") return { normalizeResponses: { remove: true } };
|
|
18240
18290
|
return { normalizeResponses: { value: nr } };
|
|
18241
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
|
+
}
|
|
18242
18298
|
/**
|
|
18243
18299
|
* Collects cacheControl mode, TTL, and rewriteBlockTtl.
|
|
18244
18300
|
* Mode-cancel aborts; TTL-cancel keeps existing TTL; rewrite-cancel keeps existing rewrite.
|
|
@@ -18687,12 +18743,19 @@ async function editSessionId(current) {
|
|
|
18687
18743
|
return next;
|
|
18688
18744
|
}
|
|
18689
18745
|
async function editNormalizeResponses(current) {
|
|
18690
|
-
const result = await
|
|
18746
|
+
const result = await collectNormalizeResponses(current.normalizeResponses);
|
|
18691
18747
|
if (result === null) return current;
|
|
18692
18748
|
const next = { ...current };
|
|
18693
18749
|
applyField(next, "normalizeResponses", result.normalizeResponses);
|
|
18694
18750
|
return next;
|
|
18695
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
|
+
}
|
|
18696
18759
|
async function editCacheControl(current, configPath) {
|
|
18697
18760
|
const globalTtl = readGlobalTtl(configPath);
|
|
18698
18761
|
const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl, current.rewriteBlockTtl);
|
|
@@ -18800,21 +18863,40 @@ async function cachingCommand(opts) {
|
|
|
18800
18863
|
outro("Bye!");
|
|
18801
18864
|
}
|
|
18802
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
|
|
18803
18884
|
//#region src/commands/config/normalize-responses.ts
|
|
18804
18885
|
async function normalizeResponsesCommand(opts) {
|
|
18805
18886
|
const configPath = requireConfigPath(opts?.configPath);
|
|
18806
18887
|
const raw = readConfigFileRaw(configPath).normalizeResponses;
|
|
18807
18888
|
const effective = raw ?? DEFAULTS.normalizeResponses;
|
|
18808
|
-
log.info(`Current: normalizeResponses = ${raw === void 0 ? `(default -> ${effective})` : effective}`);
|
|
18809
|
-
const
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
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}`);
|
|
18818
18900
|
}
|
|
18819
18901
|
//#endregion
|
|
18820
18902
|
//#region src/commands/config/fixes-menu.ts
|
|
@@ -18824,6 +18906,11 @@ const FIXES_LEVERS = [{
|
|
|
18824
18906
|
label: "Repair /v1/responses bodies (normalizeResponses)",
|
|
18825
18907
|
global: (configPath) => normalizeResponsesCommand({ configPath }),
|
|
18826
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)
|
|
18827
18914
|
}];
|
|
18828
18915
|
async function runFixesLeverMenu(opts) {
|
|
18829
18916
|
for (;;) {
|
|
@@ -18849,7 +18936,8 @@ async function globalFixesMenu(opts) {
|
|
|
18849
18936
|
noteTitle: "Fixes",
|
|
18850
18937
|
backLabel: "← Back",
|
|
18851
18938
|
renderNote: () => {
|
|
18852
|
-
|
|
18939
|
+
const raw = readConfigFileRaw(configPath);
|
|
18940
|
+
return [`normalizeResponses: ${raw.normalizeResponses ?? "(default → on)"}`, `normalizeMessages: ${raw.normalizeMessages ?? "(default → off)"}`].join("\n");
|
|
18853
18941
|
},
|
|
18854
18942
|
onLever: (lever) => lever.global(configPath)
|
|
18855
18943
|
});
|
|
@@ -18860,7 +18948,7 @@ async function perModelFixesMenu(opts) {
|
|
|
18860
18948
|
await runFixesLeverMenu({
|
|
18861
18949
|
noteTitle: `Fixes for "${opts.modelKey}"`,
|
|
18862
18950
|
backLabel: "← Back to override edit",
|
|
18863
|
-
renderNote: () => `normalizeResponses: ${current.normalizeResponses ?? "(inherit)"}`,
|
|
18951
|
+
renderNote: () => [`normalizeResponses: ${current.normalizeResponses ?? "(inherit)"}`, `normalizeMessages: ${current.normalizeMessages ?? "(inherit)"}`].join("\n"),
|
|
18864
18952
|
onLever: async (lever) => {
|
|
18865
18953
|
const next = await lever.perModel(current);
|
|
18866
18954
|
if (!overridesEqual(next, current)) {
|
|
@@ -18878,7 +18966,7 @@ async function fixesCommand(opts) {
|
|
|
18878
18966
|
}
|
|
18879
18967
|
//#endregion
|
|
18880
18968
|
//#region src/commands/config/edit.ts
|
|
18881
|
-
function
|
|
18969
|
+
function toggleHint(value) {
|
|
18882
18970
|
if (value === void 0) return "(inherit)";
|
|
18883
18971
|
return value ? "on" : "off";
|
|
18884
18972
|
}
|
|
@@ -18891,13 +18979,13 @@ function formatOverrideHint(override) {
|
|
|
18891
18979
|
}
|
|
18892
18980
|
if (override.sessionId) parts.push(`session: ${override.sessionId}`);
|
|
18893
18981
|
if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
|
|
18894
|
-
if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${
|
|
18895
|
-
if (override.normalizeResponses) parts.push(`fix: ${override.normalizeResponses}`);
|
|
18982
|
+
if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${toggleHint(override.normalizeVolatileSystem)}`);
|
|
18983
|
+
if (override.normalizeResponses !== void 0) parts.push(`fix: ${toggleHint(override.normalizeResponses)}`);
|
|
18896
18984
|
if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
|
|
18897
18985
|
return parts.join(", ") || "(empty)";
|
|
18898
18986
|
}
|
|
18899
18987
|
function formatCachingHint(current) {
|
|
18900
|
-
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)}`;
|
|
18901
18989
|
}
|
|
18902
18990
|
function formatFixesHint(current) {
|
|
18903
18991
|
return `normalizeResponses: ${current.normalizeResponses ?? "inherit"}`;
|
|
@@ -19694,7 +19782,7 @@ async function runConfigMenu(client) {
|
|
|
19694
19782
|
}
|
|
19695
19783
|
//#endregion
|
|
19696
19784
|
//#region src/version.ts
|
|
19697
|
-
const version = "0.
|
|
19785
|
+
const version = "0.19.0";
|
|
19698
19786
|
//#endregion
|
|
19699
19787
|
//#region src/commands/doctor.ts
|
|
19700
19788
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
@@ -23656,15 +23744,98 @@ const injectSessionId = createMiddleware(async (c, next) => {
|
|
|
23656
23744
|
await next();
|
|
23657
23745
|
});
|
|
23658
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
|
|
23659
23831
|
//#region src/proxy/utils/responses-input.ts
|
|
23660
23832
|
/**
|
|
23661
|
-
* Whether to normalize a Responses-api body.
|
|
23662
|
-
*
|
|
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.
|
|
23663
23836
|
*/
|
|
23664
|
-
function shouldNormalizeResponses(
|
|
23665
|
-
|
|
23666
|
-
if (mode === "always") return true;
|
|
23667
|
-
return classifyEndpoint(path) === "responses";
|
|
23837
|
+
function shouldNormalizeResponses(enabled, path) {
|
|
23838
|
+
return enabled && classifyEndpoint(path) === "responses";
|
|
23668
23839
|
}
|
|
23669
23840
|
function isPlainObject(value) {
|
|
23670
23841
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -24070,6 +24241,7 @@ const injectChain = [
|
|
|
24070
24241
|
parseBody,
|
|
24071
24242
|
resolveConfig,
|
|
24072
24243
|
injectProvider,
|
|
24244
|
+
normalizeMessagesMiddleware,
|
|
24073
24245
|
injectCacheControl,
|
|
24074
24246
|
normalizeVolatileSystemMiddleware,
|
|
24075
24247
|
normalizeResponsesInputMiddleware,
|