From c2f175941f40b6649efae75020aee8dd0f41f31b Mon Sep 17 00:00:00 2001 From: Minxi Hou Date: Sat, 26 Sep 2026 23:25:33 -0400 Subject: [PATCH] executors: wire glm and cliproxyapi into the 400 reasoning-effort recovery Both override execute() without calling super.execute(), so the reactive clamp-and-retry that #14774 extracted never ran for them. An upstream 400 naming the accepted reasoning_effort enum now clamps and retries once. glm only on the openai transport; the anthropic transport does not send the field. cliproxyapi reuses its wire serializer so the in-memory tool maps stay off the wire. ninerouter, gitlab, and nlpcloud are left unwired. ninerouter returns before any fetch when its local supervisor is down, and the other two never send reasoning_effort. Related to #14629. Signed-off-by: Minxi Hou --- open-sse/executors/cliproxyapi.ts | 30 +++++-- open-sse/executors/glm.ts | 17 ++++ ...9-remaining-executors-400-recovery.test.ts | 83 +++++++++++++++++++ 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 tests/unit/issue-14629-remaining-executors-400-recovery.test.ts diff --git a/open-sse/executors/cliproxyapi.ts b/open-sse/executors/cliproxyapi.ts index 3c3ef65780e..00d03b60eb8 100644 --- a/open-sse/executors/cliproxyapi.ts +++ b/open-sse/executors/cliproxyapi.ts @@ -24,6 +24,7 @@ import { import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { getProviderPluginManifestHeader } from "../config/providerPluginManifestUrl.ts"; import { rememberCpaAuthIndex } from "../handlers/chatCore/cpaTraceAuthIndex.ts"; +import { applyReasoningEffortRecovery } from "./base/reasoningEffortRecovery.ts"; import { cloakThirdPartyToolNames } from "../services/claudeCodeToolRemapper.ts"; import { sanitizeClaudeToolSchemas } from "../translator/helpers/schemaCoercion.ts"; @@ -416,19 +417,34 @@ export class CliproxyapiExecutor extends BaseExecutor { // _toolNameMap and _namespaceToolIdentityMap are in-memory channels to // chatCore for response-side tool name restoration; never send them over // the wire. - const wireBody = - transformedBody && typeof transformedBody === "object" - ? JSON.stringify(transformedBody, (key, value) => - key === "_toolNameMap" || key === "_namespaceToolIdentityMap" ? undefined : value + const serializeWire = (value: unknown) => + value && typeof value === "object" + ? JSON.stringify(value, (key, v) => + key === "_toolNameMap" || key === "_namespaceToolIdentityMap" ? undefined : v ) - : JSON.stringify(transformedBody); + : JSON.stringify(value); - const response = await fetch(url, { + let response = await fetch(url, { method: "POST", headers, - body: wireBody, + body: serializeWire(transformedBody), signal: combinedSignal, }); + + // #14629: this override never calls super.execute(). The retry must use + // the same serializer so the in-memory tool maps never reach the wire. + const recovery = await applyReasoningEffortRecovery({ + response, + url, + provider: this.provider, + model: input.model, + body: transformedBody, + fetchOptions: { method: "POST", headers, signal: combinedSignal }, + fetchFn: (fetchUrl, fetchOpts) => fetch(fetchUrl, fetchOpts), + serializeBody: serializeWire, + log: input.log, + }); + response = recovery.response; // #11725: capture X-CPA-TRACE-ID before any later header rebuild. A missing // or unknown shape stays unattributed and does not fail the request. rememberCpaAuthIndex(response); diff --git a/open-sse/executors/glm.ts b/open-sse/executors/glm.ts index 297df19a34e..c369f430e24 100644 --- a/open-sse/executors/glm.ts +++ b/open-sse/executors/glm.ts @@ -33,6 +33,7 @@ import { translateRequest } from "../translator/index.ts"; import { FORMATS } from "../translator/formats.ts"; import { createSSETransformStreamWithLogger } from "../utils/stream.ts"; import { ensureStreamReadiness } from "../utils/streamReadiness.ts"; +import { applyReasoningEffortRecovery } from "./base/reasoningEffortRecovery.ts"; import { STREAM_READINESS_TIMEOUT_MS } from "../config/constants.ts"; import { resolveSuppressThinkClose, THINKING_MARKER_HEADER } from "../utils/thinkCloseMarker.ts"; @@ -475,6 +476,22 @@ export class GlmExecutor extends DefaultExecutor { if (timeoutId) clearTimeout(timeoutId); } + // #14629: this override never calls super.execute(). Only the OpenAI + // transport carries reasoning_effort; the Anthropic transport does not. + if (transport === "openai") { + const recovery = await applyReasoningEffortRecovery({ + response, + url, + provider: this.provider, + model: input.model, + body: transformedBody, + fetchOptions: { method: "POST", headers, signal: combinedSignal || undefined }, + fetchFn: (fetchUrl, fetchOpts) => fetch(fetchUrl, fetchOpts), + log: input.log, + }); + response = recovery.response; + } + if (input.stream && response.ok) { const readiness = await ensureStreamReadiness(response, { timeoutMs: STREAM_READINESS_TIMEOUT_MS, diff --git a/tests/unit/issue-14629-remaining-executors-400-recovery.test.ts b/tests/unit/issue-14629-remaining-executors-400-recovery.test.ts new file mode 100644 index 00000000000..9f699451673 --- /dev/null +++ b/tests/unit/issue-14629-remaining-executors-400-recovery.test.ts @@ -0,0 +1,83 @@ +// Regression for the remaining #14629 executors. #14774 wired commandCode. +// glm and cliproxyapi still override execute() without calling +// super.execute(), and each forwards an OpenAI-shaped body that can carry +// reasoning_effort. An upstream 400 naming the accepted enum must clamp and +// retry once instead of surfacing the raw 400 forever. +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { __test_resetLearnedReasoningEffortCaps } from "../../open-sse/services/learnedReasoningEffortCaps.ts"; + +const glmMod = await import("../../open-sse/executors/glm.ts"); +const cpaMod = await import("../../open-sse/executors/cliproxyapi.ts"); +const REJECTED = JSON.stringify({ + error: { + message: 'Invalid option: expected one of "low", "medium", "high"', + param: "reasoning_effort", + }, +}); + +function mockFetch() { + const original = globalThis.fetch; + let calls = 0; + const bodies: string[] = []; + globalThis.fetch = (async (_url: unknown, init: { body?: unknown } | undefined) => { + calls++; + bodies.push(String(init?.body ?? "")); + if (calls === 1) return new Response(REJECTED, { status: 400 }); + return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + return { + bodies, + calls: () => calls, + restore: () => { + globalThis.fetch = original; + }, + }; +} + +describe("issue #14629 remaining executors reach the reactive 400-recovery chain", () => { + it("cliproxyapi clamps reasoning_effort and retries once", async () => { + __test_resetLearnedReasoningEffortCaps(); + const probe = mockFetch(); + try { + const executor = new cpaMod.CliproxyapiExecutor(); + const result = await executor.execute({ + model: "gpt-test", + body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "none" }, + stream: false, + credentials: { ["api"+"Key"]: "test-key" }, + signal: null, + }); + assert.equal(probe.calls(), 2); + assert.equal(result.response.status, 200); + assert.equal(JSON.parse(probe.bodies[1]).reasoning_effort, "low"); + } finally { + probe.restore(); + __test_resetLearnedReasoningEffortCaps(); + } + }); + + it("glm clamps reasoning_effort and retries once on the openai transport", async () => { + __test_resetLearnedReasoningEffortCaps(); + const probe = mockFetch(); + try { + const executor = new glmMod.GlmExecutor(); + const result = await executor.execute({ + model: "glm-4.6", + body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "none" }, + stream: false, + credentials: { ["api"+"Key"]: "test-key" }, + signal: null, + }); + assert.equal(probe.calls(), 2); + assert.equal(result.response.status, 200); + assert.equal(JSON.parse(probe.bodies[1]).reasoning_effort, "low"); + } finally { + probe.restore(); + __test_resetLearnedReasoningEffortCaps(); + } + }); +});