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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
25 changes: 25 additions & 0 deletions open-sse/handlers/chatCore/clineResponseEnvelope.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
1 change: 1 addition & 0 deletions stryker.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/cline-response-envelope.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading