fix(claude): preserve intentional prefill from non-messages[] source formats

translateRequest detected the client's terminal role only from messages[],
so a Gemini contents[] or Responses input[] body ending on an explicit
model/assistant turn (real prefill) read as undefined and got the
"Continue." restoration appended, clobbering the prefill. Map the trailing
role per source shape; only model/assistant counts as prefill — every other
tail stays undefined so the emptied-turn fix still applies.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decoluaandClaude Code committed 2026-10-01 10:28:20 +07:00
1 parent 75834e96ff
commit 5e9bd464f2
2 files changed
+58 -1

No files matched your search

+16 -1
View File
@@ -9,6 +9,7 @@ import { applyThinking, captureThinking } from "./concerns/thinkingUnified.js";
import { captureSessionId } from "../utils/sessionManager.js"; import { captureSessionId } from "../utils/sessionManager.js";
import { AntigravityExecutor } from "../executors/antigravity.js"; import { AntigravityExecutor } from "../executors/antigravity.js";
import { PROVIDERS } from "../providers/index.js"; import { PROVIDERS } from "../providers/index.js";
import { ROLE, GEMINI_ROLE } from "./schema/roles.js";
// Registry for translators. Lazy-init guards against circular-import order: // Registry for translators. Lazy-init guards against circular-import order:
// translator modules call register() (side-effect) before this module's body runs. // translator modules call register() (side-effect) before this module's body runs.
@@ -49,12 +50,26 @@ function stripContentTypes(body, stripList = []) {
} }
} }
// Role the client's conversation actually ended on, in the source format's own
// shape — not every source uses messages[] (Gemini/Antigravity: contents[],
// Responses/Codex: input[]). Only an explicit trailing model/assistant turn is
// real prefill and must reach ensureTrailingUserTurn as ROLE.ASSISTANT; every
// other tail (including no role, e.g. a function output) stays undefined so
// the emptied-turn fix still applies.
function detectClientLastRole(body) {
if (Array.isArray(body?.messages)) return body.messages[body.messages.length - 1]?.role;
const items = Array.isArray(body?.contents) ? body.contents : Array.isArray(body?.input) ? body.input : null;
if (!items) return undefined;
const role = items[items.length - 1]?.role;
return role === ROLE.ASSISTANT || role === GEMINI_ROLE.MODEL ? ROLE.ASSISTANT : undefined;
}
// Translate request: source -> openai -> target // Translate request: source -> openai -> target
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null, clientTool = null) { export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null, clientTool = null) {
ensureInitialized(); ensureInitialized();
let result = body; let result = body;
// Role the client actually ended on, before any translator drops an emptied turn. // Role the client actually ended on, before any translator drops an emptied turn.
const clientLastRole = Array.isArray(body?.messages) ? body.messages[body.messages.length - 1]?.role : undefined; const clientLastRole = detectClientLastRole(body);
// Strip explicit content types (opt-in via strip[] in PROVIDER_MODELS entry) // Strip explicit content types (opt-in via strip[] in PROVIDER_MODELS entry)
stripContentTypes(result, stripList); stripContentTypes(result, stripList);
@@ -0,0 +1,42 @@
// Non-messages[] sources (Gemini contents[], Responses input[]) carry the client's
// terminal role in their own shape. An explicit trailing model/assistant turn is real
// prefill and must survive translation to Claude; an emptied trailing user turn must
// still get the "Continue." restoration.
import { describe, it, expect } from "vitest";
import { translateRequest } from "../../open-sse/translator/index.js";
const roles = (body) => body.messages.map((m) => m.role);
describe("trailing user turn: non-messages[] source formats", () => {
it("keeps a Gemini trailing model turn (real prefill)", () => {
const out = translateRequest("gemini", "claude", "claude-sonnet-4-5", {
contents: [
{ role: "user", parts: [{ text: "hi" }] },
{ role: "model", parts: [{ text: "The answer is" }] },
],
}, false);
expect(roles(out)).toEqual(["user", "assistant"]);
});
it("keeps a Responses trailing assistant message (real prefill)", () => {
const out = translateRequest("openai-responses", "claude", "claude-sonnet-4-5", {
model: "claude-sonnet-4-5",
input: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "The answer is" },
],
}, false);
expect(roles(out)).toEqual(["user", "assistant"]);
});
it("still restores a user turn when a Gemini trailing user turn is emptied", () => {
const out = translateRequest("gemini", "claude", "claude-sonnet-4-5", {
contents: [
{ role: "user", parts: [{ text: "hi" }] },
{ role: "model", parts: [{ text: "hello" }] },
{ role: "user", parts: [] },
],
}, false);
expect(roles(out)).toEqual(["user", "assistant", "user"]);
});
});