Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions open-sse/executors/cliproxyapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions open-sse/executors/glm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand Down
83 changes: 83 additions & 0 deletions tests/unit/issue-14629-remaining-executors-400-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
Loading