glmproxy 2.6.0 → 2.7.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 +8 -2
- package/anthropic.js +22 -0
- package/bin/cli.js +67 -1
- package/lib/core.js +101 -18
- package/lib/fallback-models.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,8 +158,8 @@ AutoClaw handles authentication automatically. When the cloud path fails, reques
|
|
|
158
158
|
|
|
159
159
|
| ID | Name | Context | Max Output | Notes |
|
|
160
160
|
|----|------|---------|------------|-------|
|
|
161
|
-
| `zai_auto` | Auto | 1M |
|
|
162
|
-
| `zaicoding_glm-5.3` | GLM-5.3 | 1M |
|
|
161
|
+
| `zai_auto` | Auto | 1M | 131K | Routes to AutoClaw's optimal model (GLM-5.3-Flash today) |
|
|
162
|
+
| `zaicoding_glm-5.3` | GLM-5.3 | 1M | 131K | Latest GLM coding model |
|
|
163
163
|
| `zai_glm-5-turbo` | GLM-5-Turbo | 200K | 131K | Zhipu AI GLM-5 Turbo |
|
|
164
164
|
| `zai_glm-5.3-flash` | GLM-5.3-Flash ("OX-alpha") | 1M | 131K | Now a regular catalog model, served straight through the cloud path |
|
|
165
165
|
| `tdpsk_deepseek-v4-flash-202605` | Deepseek-V4-Flash | 1M | 393K | Fast DeepSeek model |
|
|
@@ -250,6 +250,9 @@ glmproxy --test-models
|
|
|
250
250
|
| `PREFER_LOCAL` | off | Set to `1` to use the local AutoClaw gateway first, skipping cloud attempts |
|
|
251
251
|
| `TRUSTED_PROXIES` | empty | Comma-separated IPs whose `X-Forwarded-For` header is trusted for rate limiting |
|
|
252
252
|
| `MAX_BODY_BYTES` | `52428800` | Max request body (50 MB) |
|
|
253
|
+
| `MAX_MESSAGE_TEXT_BYTES` | `262144` | Max per-message TEXT size (256 KB; base64 image data is not counted as text) |
|
|
254
|
+
| `MAX_TOTAL_MESSAGE_TEXT_BYTES` | `1048576` | Max combined message text (1 MB) |
|
|
255
|
+
| `MAX_IMAGE_BYTES` | `20971520` | Max decoded size per image attachment (20 MB) |
|
|
253
256
|
| `JSONL_LOG` | off | Write structured JSONL request log when `true` (also on with `LOG_LEVEL=debug`) |
|
|
254
257
|
| `JSONL_FILE` | `proxy_requests.jsonl` (Anthropic: `proxy_requests_anthropic.jsonl`) | JSONL output path |
|
|
255
258
|
| `JSONL_SYNC` | off | Write JSONL lines synchronously when `true` (flush every line) |
|
|
@@ -264,6 +267,7 @@ glmproxy --test-models
|
|
|
264
267
|
| `--limit [n]` | — | Set or clear the max message/entity limit (e.g. `--limit 256`; bare `--limit` prints the current value) |
|
|
265
268
|
| `--doctor` | — | Scan AutoClaw's current runtime model catalog and show Anthropic routing |
|
|
266
269
|
| `--test-models` / `--test` | — | Live health check: test every catalog model through the full pipeline |
|
|
270
|
+
| `--stop` | — | Kill any other running glmproxy instances (npm-global or `bin/cli.js` starts) and exit |
|
|
267
271
|
| `--help`, `-h` | — | Show CLI help |
|
|
268
272
|
|
|
269
273
|
**JSONL request logging.** Set `JSONL_LOG=true` (or `LOG_LEVEL=debug`) to write one JSON line per request:
|
|
@@ -386,6 +390,8 @@ TRUSTED_PROXIES=127.0.0.1 glmproxy --host 0.0.0.0
|
|
|
386
390
|
- On a 401, the proxy invalidates its cached token and you can retry immediately
|
|
387
391
|
- Upstream 400 `"invalid request"` gets one retry after a 2s delay (a known upstream hiccup). Quota/plan errors are never retried
|
|
388
392
|
- The cloud upstream requires AutoClaw's app system-prompt banner in every request. The proxy injects it automatically and never duplicates it. In practice it doesn't change much since your harness's own system prompt overrides it anyway
|
|
393
|
+
- Max output is clamped to each model's real upstream cap (131K for every GLM model, 393K for DeepSeek). AutoClaw's runtime catalog overstates GLM-5.3's cap (307K), and asking the cloud for more than a model's real cap makes it **silently run a DeepSeek model instead and bill DeepSeek credits** — the proxy clamps so your `zai_glm-5.3` stays GLM-5.3
|
|
394
|
+
- Images pass through natively: pasted/attached images (`image_url` data URLs, Anthropic `image` blocks) reach vision-capable models (e.g. `zai_glm-5.3-flash`) as real image parts — the model sees them directly, no OCR bridge. Text-only models reject them upstream and the request falls back to the desktop agent, mirroring AutoClaw's own gating
|
|
389
395
|
- The token file is watched for changes, so AutoClaw can rotate auth mid-session without a restart
|
|
390
396
|
- AutoClaw's client identity (app version, platform, channel) loads dynamically from its runtime file, same as the model catalog, so an AutoClaw app update is picked up without editing or restarting the proxy
|
|
391
397
|
- The fallback model catalog lives in `lib/fallback-models.json` (override with `FALLBACK_MODELS_PATH`). The built-in list is only a last resort when AutoClaw's runtime file is unreadable
|
package/anthropic.js
CHANGED
|
@@ -113,11 +113,26 @@ function anthropicToOpenAI(body, modelId) {
|
|
|
113
113
|
const toolResults = [];
|
|
114
114
|
const toolUses = [];
|
|
115
115
|
const textParts = [];
|
|
116
|
+
const imageParts = [];
|
|
117
|
+
|
|
118
|
+
// Anthropic image block → OpenAI image_url part (AutoClaw's cloud accepts
|
|
119
|
+
// native vision parts on the chat/completions wire; probe-verified 2026-09-04).
|
|
120
|
+
const toOpenAIImage = (block) => {
|
|
121
|
+
const src = block?.source;
|
|
122
|
+
if (src?.type === "base64" && typeof src.data === "string") {
|
|
123
|
+
return { type: "image_url", image_url: { url: `data:${src.media_type || "image/png"};base64,${src.data}` } };
|
|
124
|
+
}
|
|
125
|
+
if (src?.type === "url" && typeof src.url === "string") {
|
|
126
|
+
return { type: "image_url", image_url: { url: src.url } };
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
116
130
|
|
|
117
131
|
for (const block of content) {
|
|
118
132
|
if (block.type === "tool_result") toolResults.push(block);
|
|
119
133
|
else if (block.type === "tool_use") toolUses.push(block);
|
|
120
134
|
else if (block.type === "text") textParts.push(block.text);
|
|
135
|
+
else if (block.type === "image") { const p = toOpenAIImage(block); if (p) imageParts.push(p); }
|
|
121
136
|
else if (block.type === "thinking") { /* skip */ }
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -149,6 +164,13 @@ function anthropicToOpenAI(body, modelId) {
|
|
|
149
164
|
};
|
|
150
165
|
if (textParts.length > 0) msgObj.content = textParts.join("\n");
|
|
151
166
|
messages.push(msgObj);
|
|
167
|
+
} else if (imageParts.length > 0 && msg.role !== "assistant") {
|
|
168
|
+
// Multimodal user turn: OpenAI array content with text + image parts.
|
|
169
|
+
const parts = [
|
|
170
|
+
...textParts.map((t) => ({ type: "text", text: t })),
|
|
171
|
+
...imageParts,
|
|
172
|
+
];
|
|
173
|
+
messages.push({ role: msg.role, content: parts });
|
|
152
174
|
} else if (textParts.length > 0) {
|
|
153
175
|
messages.push({ role: msg.role, content: textParts.join("\n") });
|
|
154
176
|
} else if (toolResults.length === 0 && toolUses.length === 0) {
|
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs";
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
6
|
import { promptSelect, promptInput, promptNumber } from "../lib/prompts.js";
|
|
7
7
|
import http from "http";
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
8
9
|
import {
|
|
9
10
|
getModelCatalog, loadConfig, createTokenLayer,
|
|
10
11
|
fetchRemoteModelConfig, annotateCreditTiers, resolveTierTargets,
|
|
@@ -15,7 +16,7 @@ import { DEFAULT_PORTS, DEFAULT_HOST, DEFAULT_PROXY_KEY, TEST_PROXY_PORT } from
|
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const args = process.argv.slice(2);
|
|
17
18
|
|
|
18
|
-
const FLAGS = ["--anthropic", "--openai", "--port", "--host", "--key", "--rate-limit", "--max-messages", "--doctor", "--test-models", "--test", "--limit", "--help", "-h"];
|
|
19
|
+
const FLAGS = ["--anthropic", "--openai", "--port", "--host", "--key", "--rate-limit", "--max-messages", "--doctor", "--test-models", "--test", "--limit", "--stop", "--help", "-h"];
|
|
19
20
|
|
|
20
21
|
// Current effective MAX_MESSAGES env value as a finite number, or Infinity.
|
|
21
22
|
function effectiveMaxMessages() {
|
|
@@ -49,6 +50,7 @@ function showHelp() {
|
|
|
49
50
|
If you have a compression system, leaving this unlimited is preferred.
|
|
50
51
|
--doctor Live credit-tier scan of AutoClaw's catalog + routing map
|
|
51
52
|
--test-models Test all configured models against upstream and show live health
|
|
53
|
+
--stop Kill any other running glmproxy instances and exit
|
|
52
54
|
--limit Set or clear the max entity / messages limit (own menu item)
|
|
53
55
|
--help, -h Show this help message
|
|
54
56
|
|
|
@@ -288,6 +290,70 @@ if (args.includes("--doctor")) {
|
|
|
288
290
|
process.exit(0);
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
// ─── --stop / stop: kill other running glmproxy instances ───────────────────
|
|
294
|
+
// Matches processes whose command line mentions glmproxy AND cli.js (covers
|
|
295
|
+
// npm-global installs and repo checkouts started via bin/cli.js) while never
|
|
296
|
+
// matching this very process. Dev entrypoints launched as `node openai.js`
|
|
297
|
+
// carry no glmproxy marker — stop those by hand.
|
|
298
|
+
function findGlmproxyInstances() {
|
|
299
|
+
const selfPid = process.pid;
|
|
300
|
+
const hits = [];
|
|
301
|
+
if (process.platform === "win32") {
|
|
302
|
+
const res = spawnSync("powershell", ["-NoProfile", "-Command",
|
|
303
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node' -and $_.CommandLine -match 'glmproxy' -and $_.CommandLine -match 'cli\\.js' } | ForEach-Object { "$($_.ProcessId)|$($_.CommandLine)" }`,
|
|
304
|
+
], { encoding: "utf8", windowsHide: true });
|
|
305
|
+
for (const line of (res.stdout || "").split(/\r?\n/)) {
|
|
306
|
+
const idx = line.indexOf("|");
|
|
307
|
+
if (idx <= 0) continue;
|
|
308
|
+
const pid = Number.parseInt(line.slice(0, idx), 10);
|
|
309
|
+
const cmd = line.slice(idx + 1);
|
|
310
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== selfPid) hits.push({ pid, cmd });
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
const res = spawnSync("ps", ["-eo", "pid=,command="], { encoding: "utf8" });
|
|
314
|
+
for (const line of (res.stdout || "").split("\n")) {
|
|
315
|
+
const m = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
316
|
+
if (!m) continue;
|
|
317
|
+
const pid = Number.parseInt(m[1], 10);
|
|
318
|
+
const cmd = m[2];
|
|
319
|
+
if (cmd.includes("glmproxy") && cmd.includes("cli.js") && pid !== selfPid) hits.push({ pid, cmd });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return hits;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function runStop() {
|
|
326
|
+
console.log(`\n Looking for other glmproxy instances…`);
|
|
327
|
+
let hits = [];
|
|
328
|
+
try { hits = findGlmproxyInstances(); }
|
|
329
|
+
catch (e) { console.log(` ${COLORS.RED}✗ scan failed: ${e.message}${COLORS.RESET}\n`); return; }
|
|
330
|
+
|
|
331
|
+
if (hits.length === 0) {
|
|
332
|
+
console.log(` ${COLORS.GRAY}No other instances running.${COLORS.RESET}\n`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let killed = 0, failed = 0;
|
|
337
|
+
for (const { pid, cmd } of hits) {
|
|
338
|
+
const short = cmd.length > 96 ? `${cmd.slice(0, 96)}…` : cmd;
|
|
339
|
+
try {
|
|
340
|
+
process.kill(pid, "SIGTERM");
|
|
341
|
+
console.log(` ${COLORS.GREEN}✓${COLORS.RESET} stopped PID ${pid} — ${short}`);
|
|
342
|
+
killed++;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.log(` ${COLORS.RED}✗${COLORS.RESET} PID ${pid} — ${e.code || e.message} — ${short}`);
|
|
345
|
+
failed++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
console.log(`\n ${killed} instance(s) stopped${failed ? `, ${failed} failed` : ""}.\n`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --stop / stop: kill other running glmproxy instances, then exit.
|
|
352
|
+
if (args.includes("--stop") || args[0] === "stop") {
|
|
353
|
+
runStop();
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
291
357
|
// --limit: set or clear the max entity / messages limit, then exit.
|
|
292
358
|
if (args.includes("--limit")) {
|
|
293
359
|
const limitIdx = args.indexOf("--limit");
|
package/lib/core.js
CHANGED
|
@@ -192,7 +192,9 @@ export function readRuntimeModels(config) {
|
|
|
192
192
|
id: m.id,
|
|
193
193
|
name: m.name || m.id,
|
|
194
194
|
contextWindow: m.contextWindow || 1_048_576,
|
|
195
|
-
|
|
195
|
+
// The runtime file overstates GLM-5.3's output cap (307200 vs the
|
|
196
|
+
// cloud's real 131072); never advertise more than the verified cap.
|
|
197
|
+
maxTokens: Math.min(m.maxTokens || 131_072, OUTPUT_CAPS[m.id] ?? Infinity),
|
|
196
198
|
}));
|
|
197
199
|
|
|
198
200
|
if (models.length > 0) return { models, source: candidate };
|
|
@@ -227,6 +229,32 @@ export function loadModelCatalog(config) {
|
|
|
227
229
|
return { MODELS: loadModelsFromRuntime(config) };
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Real upstream output caps (probe-verified 2026-09-02)
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// AutoClaw's runtime catalog OVERSTATES GLM-5.3's max output (307200) — the
|
|
236
|
+
// cloud's real cap for every GLM model is 131072. Sending max_completion_tokens
|
|
237
|
+
// above the cap makes the upstream silently substitute a deepseek model
|
|
238
|
+
// (glm-5.3 → deepseek-v4-pro, flash/turbo/auto → deepseek-v4-flash) and bill
|
|
239
|
+
// deepseek credits. Verified to the exact token: 131072 ok, 131073 flips.
|
|
240
|
+
export const OUTPUT_CAPS = Object.freeze({
|
|
241
|
+
"zaicoding_glm-5.3": 131_072,
|
|
242
|
+
"zai_glm-5.3-flash": 131_072,
|
|
243
|
+
"zai_glm-5-turbo": 131_072,
|
|
244
|
+
"zai_auto": 131_072,
|
|
245
|
+
"tdpsk_deepseek-v4-flash-202605": 393_216,
|
|
246
|
+
"tdpsk_deepseek-v4-pro-202606": 393_216,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Clamp a requested max output to the model's real upstream cap. Unknown
|
|
250
|
+
// models (not in OUTPUT_CAPS) pass through untouched.
|
|
251
|
+
export function clampMaxOutput(modelId, value) {
|
|
252
|
+
if (!Number.isFinite(value)) return value;
|
|
253
|
+
const cap = OUTPUT_CAPS[modelId];
|
|
254
|
+
if (!cap) return value;
|
|
255
|
+
return Math.min(value, cap);
|
|
256
|
+
}
|
|
257
|
+
|
|
230
258
|
// ============================================================================
|
|
231
259
|
// Logger
|
|
232
260
|
// ============================================================================
|
|
@@ -609,9 +637,19 @@ export function isAuthorized(req, proxyKey) {
|
|
|
609
637
|
}
|
|
610
638
|
|
|
611
639
|
export function validateChatPayload(body, maxMessages = Infinity) {
|
|
640
|
+
const envInt = (name, fallback) => {
|
|
641
|
+
const raw = process.env[name];
|
|
642
|
+
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
|
643
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
644
|
+
};
|
|
612
645
|
const MAX_MESSAGES = (maxMessages && Number.isFinite(maxMessages)) ? maxMessages : Infinity;
|
|
613
|
-
|
|
614
|
-
|
|
646
|
+
// Text caps guard against runaway harness spam. Image payloads (base64
|
|
647
|
+
// data URLs) are sized SEPARATELY — the cloud accepts native image_url
|
|
648
|
+
// parts and screenshots are legitimately large; counting base64 as text
|
|
649
|
+
// caused the 413 "an individual message is too large" bug (2026-09-04).
|
|
650
|
+
const MAX_MESSAGE_TEXT_BYTES = envInt("MAX_MESSAGE_TEXT_BYTES", 256 * 1024);
|
|
651
|
+
const MAX_TOTAL_MESSAGE_TEXT_BYTES = envInt("MAX_TOTAL_MESSAGE_TEXT_BYTES", 1024 * 1024);
|
|
652
|
+
const MAX_IMAGE_BYTES = envInt("MAX_IMAGE_BYTES", 20 * 1024 * 1024);
|
|
615
653
|
const MAX_TOOLS = 64;
|
|
616
654
|
const MAX_TOOL_BYTES = 128 * 1024;
|
|
617
655
|
const MAX_TOTAL_TOOL_BYTES = 512 * 1024;
|
|
@@ -623,15 +661,39 @@ export function validateChatPayload(body, maxMessages = Infinity) {
|
|
|
623
661
|
return { message: `messages must contain at most ${MAX_MESSAGES} entries`, statusCode: 413 };
|
|
624
662
|
}
|
|
625
663
|
|
|
664
|
+
// Measure one message's content: text bytes (strings + text parts) and
|
|
665
|
+
// image bytes (decoded size of data: URLs in image_url parts).
|
|
666
|
+
const measure = (content) => {
|
|
667
|
+
if (typeof content === "string") return { textBytes: Buffer.byteLength(content), imageBytes: 0 };
|
|
668
|
+
let textBytes = 0, imageBytes = 0;
|
|
669
|
+
if (Array.isArray(content)) {
|
|
670
|
+
for (const part of content) {
|
|
671
|
+
if (typeof part === "string") { textBytes += Buffer.byteLength(part); continue; }
|
|
672
|
+
if (part?.type === "text") { textBytes += Buffer.byteLength(typeof part.text === "string" ? part.text : JSON.stringify(part.text ?? "")); continue; }
|
|
673
|
+
if (part?.type === "image_url") {
|
|
674
|
+
const url = part?.image_url?.url;
|
|
675
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
676
|
+
const comma = url.indexOf(",");
|
|
677
|
+
const b64Chars = comma >= 0 ? url.length - comma - 1 : 0;
|
|
678
|
+
imageBytes += Math.floor((b64Chars * 3) / 4); // base64 → bytes
|
|
679
|
+
}
|
|
680
|
+
// remote http(s) image references cost nothing locally
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { textBytes, imageBytes };
|
|
685
|
+
};
|
|
686
|
+
|
|
626
687
|
let totalMessageBytes = 0;
|
|
627
688
|
for (const message of body.messages) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
689
|
+
const { textBytes, imageBytes } = measure(message?.content);
|
|
690
|
+
if (imageBytes > MAX_IMAGE_BYTES) {
|
|
691
|
+
return { message: `an image attachment is too large (max ${Math.floor(MAX_IMAGE_BYTES / 1024 / 1024)}MB per image)`, statusCode: 413 };
|
|
692
|
+
}
|
|
693
|
+
if (textBytes > MAX_MESSAGE_TEXT_BYTES) {
|
|
632
694
|
return { message: "an individual message is too large", statusCode: 413 };
|
|
633
695
|
}
|
|
634
|
-
totalMessageBytes +=
|
|
696
|
+
totalMessageBytes += textBytes;
|
|
635
697
|
if (totalMessageBytes > MAX_TOTAL_MESSAGE_TEXT_BYTES) {
|
|
636
698
|
return { message: "combined message content is too large", statusCode: 413 };
|
|
637
699
|
}
|
|
@@ -1157,11 +1219,24 @@ function injectSystemBanner(messages) {
|
|
|
1157
1219
|
return list;
|
|
1158
1220
|
}
|
|
1159
1221
|
const sys = list[idx];
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1222
|
+
if (typeof sys.content === "string") {
|
|
1223
|
+
if (!sys.content.includes(AUTOCLAW_SYSTEM_BANNER)) {
|
|
1224
|
+
list[idx] = { ...sys, content: AUTOCLAW_SYSTEM_BANNER + "\n\n" + sys.content };
|
|
1225
|
+
}
|
|
1226
|
+
return list;
|
|
1227
|
+
}
|
|
1228
|
+
if (Array.isArray(sys.content)) {
|
|
1229
|
+
// Multimodal system message — prepend the banner as a text part instead of
|
|
1230
|
+
// flattening, so image parts survive.
|
|
1231
|
+
const hasBanner = sys.content.some((p) =>
|
|
1232
|
+
typeof p === "string" ? p.includes(AUTOCLAW_SYSTEM_BANNER)
|
|
1233
|
+
: p?.type === "text" && typeof p?.text === "string" && p.text.includes(AUTOCLAW_SYSTEM_BANNER));
|
|
1234
|
+
if (!hasBanner) {
|
|
1235
|
+
list[idx] = { ...sys, content: [{ type: "text", text: AUTOCLAW_SYSTEM_BANNER }, ...sys.content] };
|
|
1236
|
+
}
|
|
1237
|
+
return list;
|
|
1238
|
+
}
|
|
1239
|
+
const text = String(sys.content ?? "");
|
|
1165
1240
|
if (!text.includes(AUTOCLAW_SYSTEM_BANNER)) {
|
|
1166
1241
|
list[idx] = { ...sys, content: AUTOCLAW_SYSTEM_BANNER + "\n\n" + text };
|
|
1167
1242
|
}
|
|
@@ -1177,8 +1252,12 @@ function buildSanitizedBody(openAIBody, upstreamModelId) {
|
|
|
1177
1252
|
};
|
|
1178
1253
|
if (typeof openAIBody.temperature === "number") sanitized.temperature = openAIBody.temperature;
|
|
1179
1254
|
if (typeof openAIBody.top_p === "number") sanitized.top_p = openAIBody.top_p;
|
|
1180
|
-
|
|
1181
|
-
|
|
1255
|
+
// Clamp max output to the model's REAL upstream cap. Exceeding it makes the
|
|
1256
|
+
// cloud silently swap in a deepseek model (probe-verified 2026-09-02) and
|
|
1257
|
+
// bill deepseek credits — the harness sends 393216 for everything, which
|
|
1258
|
+
// every GLM model (real cap 131072) trips.
|
|
1259
|
+
if (typeof openAIBody.max_tokens === "number") sanitized.max_tokens = clampMaxOutput(upstreamModelId, openAIBody.max_tokens);
|
|
1260
|
+
if (typeof openAIBody.max_completion_tokens === "number") sanitized.max_tokens = clampMaxOutput(upstreamModelId, openAIBody.max_completion_tokens);
|
|
1182
1261
|
if (openAIBody.stop !== undefined) sanitized.stop = openAIBody.stop;
|
|
1183
1262
|
if (Array.isArray(openAIBody.tools) && openAIBody.tools.length > 0) sanitized.tools = openAIBody.tools;
|
|
1184
1263
|
if (openAIBody.tool_choice !== undefined) sanitized.tool_choice = openAIBody.tool_choice;
|
|
@@ -1189,8 +1268,10 @@ function buildSanitizedBody(openAIBody, upstreamModelId) {
|
|
|
1189
1268
|
export function stripProviderPrefix(modelId) { return String(modelId || "").replace(/^[a-z]+_/, ""); }
|
|
1190
1269
|
|
|
1191
1270
|
// Trae and other clients send content as text-object arrays that Zhipu rejects
|
|
1192
|
-
// (400/500) — flatten
|
|
1193
|
-
|
|
1271
|
+
// (400/500) — flatten those to plain strings. Arrays carrying anything
|
|
1272
|
+
// non-text (image_url parts — AutoClaw's cloud accepts native vision parts,
|
|
1273
|
+
// probe-verified 2026-09-04) are preserved untouched so images reach the model.
|
|
1274
|
+
export function normalizeClientMessages(body) {
|
|
1194
1275
|
return (body.messages || []).map(msg => {
|
|
1195
1276
|
const newMsg = { ...msg };
|
|
1196
1277
|
|
|
@@ -1199,8 +1280,10 @@ function normalizeClientMessages(body) {
|
|
|
1199
1280
|
newMsg.role = "system";
|
|
1200
1281
|
}
|
|
1201
1282
|
|
|
1202
|
-
// Flatten content array
|
|
1283
|
+
// Flatten content array only when it's all text blocks
|
|
1203
1284
|
if (Array.isArray(newMsg.content)) {
|
|
1285
|
+
const hasNonText = newMsg.content.some((c) => typeof c !== "string" && c?.type !== "text");
|
|
1286
|
+
if (hasNonText) return newMsg; // multimodal content — keep the part shapes
|
|
1204
1287
|
const textParts = [];
|
|
1205
1288
|
for (const c of newMsg.content) {
|
|
1206
1289
|
if (typeof c === "string") textParts.push(c);
|
package/lib/fallback-models.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"models": [
|
|
3
|
-
{ "id": "zai_auto", "name": "Auto", "contextWindow": 1048576, "maxTokens":
|
|
4
|
-
{ "id": "zaicoding_glm-5.3", "name": "GLM-5.3", "contextWindow": 1048576, "maxTokens":
|
|
3
|
+
{ "id": "zai_auto", "name": "Auto", "contextWindow": 1048576, "maxTokens": 131072 },
|
|
4
|
+
{ "id": "zaicoding_glm-5.3", "name": "GLM-5.3", "contextWindow": 1048576, "maxTokens": 131072 },
|
|
5
5
|
{ "id": "zai_glm-5-turbo", "name": "GLM-5-Turbo", "contextWindow": 204800, "maxTokens": 131072 },
|
|
6
6
|
{ "id": "zai_glm-5.3-flash", "name": "GLM-5.3-Flash", "contextWindow": 1048576, "maxTokens": 131072 },
|
|
7
7
|
{ "id": "tdpsk_deepseek-v4-flash-202605", "name": "Deepseek-V4-Flash", "contextWindow": 1048576, "maxTokens": 393216 },
|