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:
1 parent
75834e96ff
commit
5e9bd464f2
2 files changed
+58
-1
No files matched your search
@@ -9,6 +9,7 @@ import { applyThinking, captureThinking } from "./concerns/thinkingUnified.js";
|
||||
import { captureSessionId } from "../utils/sessionManager.js";
|
||||
import { AntigravityExecutor } from "../executors/antigravity.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:
|
||||
// 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
|
||||
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null, clientTool = null) {
|
||||
ensureInitialized();
|
||||
let result = body;
|
||||
// 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)
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user