fix: unwrap Cline response envelope (#6046)

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
This commit is contained in:
KooshaPari
2026-07-03 04:40:43 -07:00
committed by GitHub
parent 5d902e5b66
commit 772fea3f4d
6 changed files with 58 additions and 1 deletions

View File

@@ -28,6 +28,8 @@
- **antigravity/gemini tool calls (`400 Unknown name "multipleOf"`):** requests routed to antigravity/gemini models with tools that declare a `multipleOf` numeric constraint failed with a hard upstream `400` (`Invalid JSON payload received. Unknown name "multipleOf"`). `multipleOf` is not part of the Gemini/antigravity OpenAPI 3.0 schema subset and was not being stripped from `function_declarations`. It is now removed at every schema level (top-level, nested, and array `items`), alongside the other unsupported constraints; `minimum`/`maximum` remain untouched. Regression guard: `tests/unit/gemini-multipleof-2309.test.ts`. (thanks @abil0321)
- **cline (empty/false-502 non-streaming responses):** the Cline gateway can wrap OpenAI-compatible chat completions in a `{ success, data: { choices, usage, … } }` envelope. The non-streaming path checked the top-level body for empty content before unwrapping, so a valid Cline response was treated as malformed. The body is now unwrapped via `unwrapClineNonStreamingEnvelope()` right after the provider-envelope unwrap and before the empty-content check, closing the remaining gap from #5956/#5924. Regression guard: `tests/unit/cline-response-envelope.test.ts`. ([#5956](https://github.com/diegosouzapw/OmniRoute/issues/5956) — thanks @KooshaPari)
### 📝 Maintenance
- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))

View File

@@ -311,7 +311,7 @@
"tests/unit/translator-helper-branches.test.ts": 870,
"tests/unit/translator-openai-responses-req.test.ts": 1172,
"tests/unit/translator-openai-to-gemini.test.ts": 1579,
"tests/unit/translator-openai-to-kiro.test.ts": 1088,
"tests/unit/translator-openai-to-kiro.test.ts": 1093,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1633,
"tests/unit/vscode-token-routes.test.ts": 1212,

View File

@@ -252,6 +252,7 @@ import { isCompactResponsesEndpoint } from "../executors/codex.ts";
import { buildCodexQuotaPersistence } from "./chatCore/codexQuota.ts";
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
import { translateNonStreamingResponse } from "./responseTranslator.ts";
import { unwrapClineNonStreamingEnvelope } from "./chatCore/clineResponseEnvelope.ts";
import { extractUsageFromResponse } from "./usageExtractor.ts";
import {
sanitizeOpenAIResponse,
@@ -3612,6 +3613,7 @@ export async function handleChatCore({
}
responseBody = unwrapped;
}
responseBody = unwrapClineNonStreamingEnvelope(provider, responseBody);
// Check for empty content response (fake success) - trigger fallback
if (isEmptyContentResponse(responseBody)) {

View File

@@ -0,0 +1,25 @@
type JsonRecord = Record<string, unknown>;
function isRecord(value: unknown): value is JsonRecord {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function hasOpenAIChoices(value: unknown): boolean {
return isRecord(value) && Array.isArray(value.choices);
}
export function unwrapClineNonStreamingEnvelope(provider: string, responseBody: unknown): unknown {
if (provider !== "cline" || !isRecord(responseBody)) {
return responseBody;
}
const data = responseBody.data;
if (!hasOpenAIChoices(data)) {
return responseBody;
}
return {
...data,
usage: isRecord(data) && data.usage !== undefined ? data.usage : responseBody.usage,
};
}

View File

@@ -92,6 +92,7 @@
"tests/unit/claude-passthrough-stream-boolean.test.ts",
"tests/unit/claude-passthrough-thinking-2454.test.ts",
"tests/unit/cli-simulate.test.ts",
"tests/unit/cline-response-envelope.test.ts",
"tests/unit/clinepass-provider.test.ts",
"tests/unit/codex-failover.test.ts",
"tests/unit/codex-session-affinity-reset-aware-5903.test.ts",

View File

@@ -0,0 +1,27 @@
import test from "node:test";
import assert from "node:assert/strict";
const { unwrapClineNonStreamingEnvelope } = await import(
"../../open-sse/handlers/chatCore/clineResponseEnvelope.ts"
);
test("unwrapClineNonStreamingEnvelope extracts Cline wrapped chat completions", () => {
const wrapped = {
success: true,
data: {
id: "chatcmpl_cline",
model: "cline/model",
choices: [{ index: 0, message: { role: "assistant", content: "ok" } }],
usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 },
},
};
assert.deepEqual(unwrapClineNonStreamingEnvelope("cline", wrapped), wrapped.data);
});
test("unwrapClineNonStreamingEnvelope keeps non-Cline and malformed envelopes untouched", () => {
const wrapped = { success: true, data: { message: "missing choices" } };
assert.equal(unwrapClineNonStreamingEnvelope("openai", wrapped), wrapped);
assert.equal(unwrapClineNonStreamingEnvelope("cline", wrapped), wrapped);
});