fix(sse): prevent Anthropic 400s for Claude-native handoffs (#12668)

* fix(sse): prevent Anthropic 400s for Claude-native handoffs

* docs(changelog): add fragment for Claude-native handoff 400 fix

* refactor(sse): satisfy file-size and complexity ratchets

Keep the Claude wire-body guard while staying under the frozen per-file line baselines and the complexity ratchets measured against release/v3.8.51.

Extract the final constraint coordinator, split system-message normalization into focused helpers, and isolate handoff response parsing. Reflow the universal-handoff explanation to absorb the added source-format argument without growing the frozen file.
This commit is contained in:
Innokentiy Solntsev
2026-09-17 21:26:25 +02:00
committed by GitHub
parent dd70dbdaa0
commit c97f61b2ac
13 changed files with 491 additions and 103 deletions

View File

@@ -0,0 +1 @@
- **fix(sse):** Claude-native context handoffs now land in Anthropic's top-level `system` parameter instead of a leading `role: "system"` message, and the final Claude executor dispatch hoists any remaining leading prompt system/developer messages and relocates directive-only `output_config` envelopes away from `messages[0]`, preventing the `messages.0: use the top-level 'system' parameter` HTTP 400 on model switches ([#12668](https://github.com/diegosouzapw/OmniRoute/pull/12668)).

View File

@@ -64,7 +64,7 @@ import {
appendAnthropicBetaHeader,
CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA,
CONTEXT_1M_BETA_HEADER,
enforceThinkingTemperature,
finalizeClaudeBodyConstraints,
modelHasNativeContext1m,
modelSupportsContext1mBeta,
} from "../services/claudeCodeCompatible.ts";
@@ -1368,7 +1368,7 @@ export class BaseExecutor {
// routing mode (grouped/raw/combo) and the native passthrough share,
// before fingerprinting and CCH signing serialize the body.
if (this.provider === "claude" || usesClaudeCodeProtocol) {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
finalizeClaudeBodyConstraints(transformedBody as Record<string, unknown>);
}
// Delegated Context Editing (opt-in): attach the clear_tool_uses strategy so

View File

@@ -14,6 +14,10 @@
export type HoistedCacheBoundary = "moved" | "kept" | "dropped";
// Re-exported from its canonical home in claudeCodeConstraints.ts so existing
// importers of this module keep working.
export { relocateDirectiveOnlyMessages } from "../../services/claudeCodeConstraints.ts";
/** Effective cache TTL of a `cache_control` value; Anthropic defaults to 5m when `ttl` is absent. */
function effectiveTtl(marker: unknown): string {
const ttl = (marker as Record<string, unknown> | null | undefined)?.ttl;
@@ -163,85 +167,3 @@ export function extractSystemRoleMessages(payload: Record<string, unknown>): voi
}
payload.messages = messages.filter((m) => !isSystemRole(m.role));
}
/**
* Moves a directive-only system message (empty content array + message-level
* `output_config`, the shape Claude Code clients emit) off `messages[0]`.
*
* Anthropic treats `messages[0]` as the initial system prompt position and
* rejects the directive-only form there ("use the top-level 'system' parameter
* for the initial system prompt"), while accepting it at any other position.
* The mid-conversation-system passthrough (provider `claude` + 1M-context beta
* models) deliberately keeps system-role messages inside `messages[]`, so a
* directive that arrived first would go upstream unchanged and 400. Relocate it
* past the first real turn instead; when the conversation has no real turn at
* all, fold the `output_config` into the top-level parameter (which wins when
* already present) and drop the now-empty message.
*/
export function relocateDirectiveOnlyMessages(payload: Record<string, unknown>): void {
if (!Array.isArray(payload.messages) || payload.messages.length === 0) return;
const messages = payload.messages as Array<Record<string, unknown>>;
const isSystemRole = (role: unknown): boolean =>
typeof role === "string" &&
(role.toLowerCase() === "system" || role.toLowerCase() === "developer");
const isEmptySystem = (m: Record<string, unknown>): boolean =>
m != null &&
typeof m === "object" &&
isSystemRole(m.role) &&
Array.isArray(m.content) &&
m.content.length === 0;
const isDirectiveOnly = (m: Record<string, unknown>): boolean =>
isEmptySystem(m) &&
m.output_config != null &&
typeof m.output_config === "object" &&
!Array.isArray(m.output_config);
if (!isEmptySystem(messages[0])) {
return;
}
// Collect the whole leading run of empty system messages so consecutive
// directives are all relocated in one pass (handling only messages[0] would
// leave the second directive at the rejected position).
let runEnd = 0;
while (runEnd < messages.length && isEmptySystem(messages[runEnd])) {
runEnd++;
}
const lead = messages.slice(0, runEnd);
const directives = lead.filter(isDirectiveOnly);
// First real (user/assistant) turn after the run. System messages with text
// content are not safe insertion anchors — keep walking past them, and past
// any non-object entries a malformed body may carry.
let insertAfter = -1;
for (let i = runEnd; i < messages.length; i++) {
const candidate = messages[i];
if (
candidate != null &&
typeof candidate === "object" &&
!isSystemRole(candidate.role)
) {
insertAfter = i;
break;
}
}
if (insertAfter === -1) {
// No real turn to relocate after: fold the first directive's
// output_config into the top-level parameter (an explicit top-level value
// wins) and drop the whole run.
if (payload.output_config == null && directives.length > 0) {
payload.output_config = directives[0].output_config;
}
payload.messages = messages.slice(runEnd);
return;
}
// Move the directives (in order) past the first real turn; plain empty
// system messages carry nothing and are dropped.
payload.messages = [
...messages.slice(runEnd, insertAfter + 1),
...directives,
...messages.slice(insertAfter + 1),
];
}

View File

@@ -403,6 +403,7 @@ export { computeFingerprint } from "./claudeCodeFingerprint.ts";
export { obfuscateSensitiveWords, setSensitiveWords } from "./claudeCodeObfuscation.ts";
export {
enforceThinkingTemperature,
finalizeClaudeBodyConstraints,
disableThinkingIfToolChoiceForced,
enforceCacheControlLimit,
} from "./claudeCodeConstraints.ts";

View File

@@ -32,6 +32,148 @@ export function enforceThinkingTemperature(body: Record<string, unknown>): void
}
}
/** Applies the final Anthropic wire-body invariants before serialization. */
export function finalizeClaudeBodyConstraints(body: Record<string, unknown>): void {
hoistLeadingSystemMessages(body);
relocateDirectiveOnlyMessages(body);
enforceThinkingTemperature(body);
}
function isSystemRole(role: unknown): boolean {
return (
typeof role === "string" &&
(role.toLowerCase() === "system" || role.toLowerCase() === "developer")
);
}
function hasOutputConfig(message: Record<string, unknown>): boolean {
return (
message.output_config != null &&
typeof message.output_config === "object" &&
!Array.isArray(message.output_config)
);
}
function isEmptySystemMessage(message: unknown): message is Record<string, unknown> {
if (message == null || typeof message !== "object") return false;
const candidate = message as Record<string, unknown>;
return (
isSystemRole(candidate.role) &&
Array.isArray(candidate.content) &&
candidate.content.length === 0
);
}
function isDirectiveOnlyMessage(message: unknown): boolean {
return isEmptySystemMessage(message) && hasOutputConfig(message);
}
/**
* Moves a directive-only system message (empty content array + message-level
* `output_config`) off `messages[0]`, which Anthropic reserves for the initial
* system-prompt position. Legitimate directives already later in the conversation
* stay untouched.
*/
export function relocateDirectiveOnlyMessages(payload: Record<string, unknown>): void {
if (!Array.isArray(payload.messages) || payload.messages.length === 0) return;
const messages = payload.messages as Array<Record<string, unknown>>;
if (!isEmptySystemMessage(messages[0])) return;
let runEnd = 0;
while (runEnd < messages.length && isEmptySystemMessage(messages[runEnd])) runEnd++;
const directives = messages.slice(0, runEnd).filter(isDirectiveOnlyMessage);
let insertAfter = -1;
for (let i = runEnd; i < messages.length; i++) {
const candidate = messages[i];
if (candidate != null && typeof candidate === "object" && !isSystemRole(candidate.role)) {
insertAfter = i;
break;
}
}
if (insertAfter === -1) {
if (payload.output_config == null && directives.length > 0) {
payload.output_config = directives[0].output_config;
}
payload.messages = messages.slice(runEnd);
return;
}
payload.messages = [
...messages.slice(runEnd, insertAfter + 1),
...directives,
...messages.slice(insertAfter + 1),
];
}
/** Extracts non-empty text blocks from string or array message content. */
function textBlocksFromContent(content: unknown): Array<Record<string, unknown>> {
if (typeof content === "string" && content.length > 0) {
return [{ type: "text", text: content }];
}
if (!Array.isArray(content)) return [];
const blocks: Array<Record<string, unknown>> = [];
for (const block of content) {
if (block == null || typeof block !== "object") continue;
const contentBlock = block as Record<string, unknown>;
if (
contentBlock.type === "text" &&
typeof contentBlock.text === "string" &&
contentBlock.text.length > 0
) {
blocks.push({ ...contentBlock });
}
}
return blocks;
}
/** Merges hoisted blocks into the existing top-level system value. */
function mergeSystemBlocks(
existing: unknown,
extra: Array<Record<string, unknown>>
): Array<Record<string, unknown>> {
if (typeof existing === "string" && existing.length > 0) {
return [{ type: "text", text: existing }, ...extra];
}
if (Array.isArray(existing)) {
return [...(existing as Array<Record<string, unknown>>), ...extra];
}
return extra;
}
/**
* Hoists only the initial system/developer run into Anthropic's top-level `system` field.
* Directive-only entries remain in `messages` for the positional relocation pass, and
* mid-conversation system entries remain untouched for the context-1m beta path.
*/
export function hoistLeadingSystemMessages(payload: Record<string, unknown>): void {
if (!Array.isArray(payload.messages) || payload.messages.length === 0) return;
const messages = payload.messages as Array<Record<string, unknown>>;
let runEnd = 0;
while (runEnd < messages.length && isSystemRole(messages[runEnd]?.role)) runEnd++;
if (runEnd === 0) return;
const extraBlocks: Array<Record<string, unknown>> = [];
const directives: Array<Record<string, unknown>> = [];
for (const message of messages.slice(0, runEnd)) {
if (isDirectiveOnlyMessage(message)) {
directives.push(message);
continue;
}
extraBlocks.push(...textBlocksFromContent(message.content));
if (payload.output_config == null && hasOutputConfig(message)) {
payload.output_config = message.output_config;
}
}
if (extraBlocks.length > 0) {
payload.system = mergeSystemBlocks(payload.system, extraBlocks);
}
payload.messages = [...directives, ...messages.slice(runEnd)];
}
export function disableThinkingIfToolChoiceForced(body: Record<string, unknown>): void {
const toolChoice = body.tool_choice as Record<string, unknown> | string | undefined;
if (!toolChoice) return;

View File

@@ -990,6 +990,7 @@ async function handleComboChatInner({
stickyWeightedLimit,
getWeightedStepKeyForTarget,
universalHandoffConfig,
sourceFormat,
relayOptions,
relayConfig,
};

View File

@@ -103,6 +103,7 @@ export type AttemptLoopDeps = {
stickyWeightedLimit?: number;
getWeightedStepKeyForTarget?: (target: ResolvedComboTarget) => string | null;
universalHandoffConfig?: UniversalHandoffConfig;
sourceFormat?: string | null;
relayOptions?: { sessionId?: string | null } | null;
relayConfig?: ContextRelayConfig | null;
};

View File

@@ -299,14 +299,13 @@ export async function executeTargetAttempt(opts: {
}
}
// Universal handoff: inject existing handoff if model changed. i === 0
// only: a fallback target (i > 0) serves the SAME client request the
// failed primary target would have served, with the original messages
// already intact -- there's nothing to hand off, since the client never
// saw the earlier target fail. Injecting a handoff note there replaces
// real context with a context-free note, which weaker fallback models
// have been observed treating as license to fabricate content instead
// of just answering the actual request (#12227 follow-up).
// Universal handoff: inject on model change only when i === 0. A fallback
// target (i > 0) serves the SAME client request the failed primary target
// would have served, with the original messages already intact -- there is
// nothing to hand off, since the client never saw the earlier target fail.
// Injecting a handoff note there replaces real context with a context-free
// note, which weaker fallback models have been observed treating as license
// to fabricate content instead of answering the request (#12227 follow-up).
if (
i === 0 &&
universalHandoffConfig.enabled &&
@@ -322,7 +321,8 @@ export async function executeTargetAttempt(opts: {
modelStr,
`Model routing: ${lastModel}${modelStr}`,
existingHandoff,
universalHandoffConfig.relayMode
universalHandoffConfig.relayMode,
deps.sourceFormat
);
}
}

View File

@@ -532,10 +532,39 @@ You are continuing a conversation that was transferred from another account due
The context above contains a concise summary of the prior work. Continue seamlessly from where the session left off.`;
}
/**
* Appends a handoff text block to Anthropic's top-level `system` parameter.
*
* Anthropic's Messages API rejects a non-empty `role: "system"` entry at
* `messages[0]` ("use the top-level 'system' parameter for the initial system
* prompt"), so a handoff injected into a Claude-native body must never become a
* leading system message. An existing string system prompt is preserved as the
* first block so prompt order is unchanged; `messages` is left untouched.
*/
function injectClaudeSystemHandoff(
body: Record<string, unknown>,
handoffContent: string
): Record<string, unknown> {
const handoffBlock = { type: "text", text: handoffContent };
const existingSystem = body.system;
let system: unknown[];
if (Array.isArray(existingSystem)) {
system = [...existingSystem, handoffBlock];
} else if (typeof existingSystem === "string" && existingSystem.length > 0) {
system = [{ type: "text", text: existingSystem }, handoffBlock];
} else {
system = [handoffBlock];
}
return { ...body, system };
}
export function injectHandoffIntoBody(
body: Record<string, unknown>,
payload: HandoffPayload,
_relayMode?: "schema-locked" | "standard"
_relayMode?: "schema-locked" | "standard",
sourceFormat?: string | null
): Record<string, unknown> {
const handoffContent = buildHandoffSystemMessage(payload);
const isResponsesRequest =
@@ -562,6 +591,10 @@ export function injectHandoffIntoBody(
return nextBody;
}
if (sourceFormat === "claude") {
return injectClaudeSystemHandoff(body, handoffContent);
}
const handoffMessage = {
role: "system",
content: handoffContent,
@@ -721,6 +754,18 @@ function logUniversalHandoffOutcome(
console.warn(`[universal-handoff] ${outcome} (combo=${comboName}): ${detail}`);
}
/** Reads the summary model's reply as text, tolerating non-JSON bodies. */
async function readUniversalHandoffResponse(response: Response): Promise<string> {
try {
return getResponseText((await response.clone().json()) as Record<string, unknown>);
} catch {
return await response
.clone()
.text()
.catch(() => "");
}
}
/**
* Generate a universal handoff summary for any model/provider switch.
*/
@@ -778,17 +823,13 @@ async function generateUniversalHandoffAsync(options: {
return "unavailable";
}
let content = "";
try {
content = getResponseText((await response.clone().json()) as Record<string, unknown>);
} catch {
content = await response.clone().text().catch(() => "");
}
const content = await readUniversalHandoffResponse(response);
const parsed = parseHandoffJSON(content);
if (!parsed) {
const preview = JSON.stringify(content.slice(0, 200));
logUniversalHandoffOutcome("unparseable", options.comboName, `model=${summaryModel} contentPreview=${preview}`);
const detail = `model=${summaryModel} contentPreview=${preview}`;
logUniversalHandoffOutcome("unparseable", options.comboName, detail);
return "unparseable";
}
@@ -875,7 +916,8 @@ export function injectUniversalHandoffBody(
currModel: string,
reason: string,
existingPayload?: HandoffPayload | null,
_relayMode?: "schema-locked" | "standard"
_relayMode?: "schema-locked" | "standard",
sourceFormat?: string | null
): Record<string, unknown> {
const handoffContent = buildUniversalHandoffSystemMessage(
prevModel,
@@ -906,6 +948,10 @@ export function injectUniversalHandoffBody(
return nextBody;
}
if (sourceFormat === "claude") {
return injectClaudeSystemHandoff(body, handoffContent);
}
const handoffMessage = {
role: "system",
content: handoffContent,

View File

@@ -1896,7 +1896,7 @@ async function handleSingleModelChat(
if (handoff && handoff.fromAccount !== credentials.connectionId) {
// Inject only after a real account switch. The combo loop itself cannot
// reliably detect this because account selection happens inside auth.
requestBody = injectHandoffIntoBody(requestBody, handoff);
requestBody = injectHandoffIntoBody(requestBody, handoff, undefined, sourceFormat);
injectedHandoff = handoff;
log.info(
"CONTEXT_RELAY",

View File

@@ -0,0 +1,175 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-directive-dispatch-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
const originalFetch = globalThis.fetch;
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
type CapturedBody = {
messages?: Array<Record<string, unknown>>;
output_config?: Record<string, unknown>;
system?: string | Array<Record<string, unknown>>;
};
function captureFetchBodies(): { bodies: CapturedBody[]; restore: () => void } {
const bodies: CapturedBody[] = [];
const original = globalThis.fetch;
globalThis.fetch = (async (_url: unknown, init: { body?: unknown } = {}) => {
bodies.push(JSON.parse(String(init.body ?? "{}")) as CapturedBody);
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}) as typeof globalThis.fetch;
function restore(): void {
globalThis.fetch = original;
}
return { bodies, restore };
}
async function executeClaude(body: Record<string, unknown>): Promise<Response> {
return new DefaultExecutor("claude").execute({
model: "claude-opus-5",
body,
stream: false,
credentials: { accessToken: "sk-ant-oat-test-token" },
clientHeaders: { "x-app": "cli" },
});
}
test("final Claude dispatch hoists a leading non-empty system message", async () => {
const { bodies, restore } = captureFetchBodies();
try {
await executeClaude({
model: "claude-opus-5",
max_tokens: 64,
system: [{ type: "text", text: "Existing top-level prompt." }],
tools: [
{
name: "lookup",
description: "Look up a value",
input_schema: { type: "object", properties: {} },
},
],
messages: [
{
role: "system",
content: [
{
type: "text",
text: "Escaped initial prompt.",
cache_control: { type: "ephemeral" },
},
],
},
{ role: "user", content: [{ type: "text", text: "hello" }] },
],
});
} finally {
restore();
}
const captured = bodies.at(-1);
assert.ok(captured, "fetch did not capture a request body");
assert.equal(captured.messages?.[0]?.role, "user");
assert.equal(
captured.messages?.some((message) =>
["system", "developer"].includes(String(message.role).toLowerCase())
),
false
);
assert.ok(Array.isArray(captured.system), "system must be serialized as content blocks");
assert.deepEqual(
captured.system.slice(-2),
[
{ type: "text", text: "Existing top-level prompt." },
{
type: "text",
text: "Escaped initial prompt.",
cache_control: { type: "ephemeral", ttl: "1h" },
},
],
"existing and hoisted system prompts must retain their order and cache boundary"
);
});
test("final Claude dispatch relocates leading directive-only messages", async () => {
const { bodies, restore } = captureFetchBodies();
const firstDirective = {
role: "system",
content: [],
output_config: { effort: "medium" },
};
const laterDirective = {
role: "system",
content: [],
output_config: { effort: "low" },
};
try {
await executeClaude({
model: "claude-opus-5",
max_tokens: 64,
messages: [
firstDirective,
{ role: "developer", content: [] },
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi" },
laterDirective,
{ role: "user", content: "continue" },
],
});
} finally {
restore();
}
const messages = bodies[0]?.messages;
assert.ok(messages, "fetch did not capture a messages array");
assert.equal(messages[0]?.role, "user");
assert.equal(messages[0]?.content, "hello");
assert.deepEqual(messages[1], firstDirective);
assert.equal(
messages.some((message) => message.role === "developer"),
false
);
assert.deepEqual(messages[3], laterDirective, "valid later directive must retain its position");
});
test("final Claude dispatch folds a sole directive without overriding top-level output_config", async () => {
const { bodies, restore } = captureFetchBodies();
try {
await executeClaude({
model: "claude-opus-5",
max_tokens: 64,
output_config: { effort: "high" },
messages: [
{
role: "system",
content: [],
output_config: { effort: "medium" },
},
],
});
} finally {
restore();
}
assert.deepEqual(bodies[0]?.messages, []);
assert.deepEqual(bodies[0]?.output_config, { effort: "high" });
});

View File

@@ -69,6 +69,62 @@ test("buildHandoffSystemMessage and injectHandoffIntoBody preserve existing hist
assert.equal(body.messages.length, 2);
});
test("injectHandoffIntoBody appends Claude-native handoff to top-level system", () => {
const payload = {
sessionId: "sess-1",
comboName: "relay-combo",
fromAccount: "conn-a",
summary: "Keep the Claude-native request shape",
keyDecisions: ["keep system prompts top-level"],
taskProgress: "Continue the request",
activeEntities: ["contextHandoff.ts"],
messageCount: 8,
model: "claude/claude-opus-5",
warningThresholdPct: 0.85,
generatedAt: "2099-04-08T12:00:00.000Z",
expiresAt: "2099-04-08T17:00:00.000Z",
};
const body = {
system: "Existing Claude prompt",
max_tokens: 64,
messages: [{ role: "user", content: [{ type: "text", text: "Continue" }] }],
};
const injected = contextHandoff.injectHandoffIntoBody(body, payload, undefined, "claude");
const system = injected.system as Array<Record<string, unknown>>;
assert.strictEqual(injected.messages, body.messages);
assert.deepEqual(system[0], { type: "text", text: "Existing Claude prompt" });
assert.match(String(system[1]?.text), /<context_handoff>/);
assert.equal(system[1]?.type, "text");
});
test("injectHandoffIntoBody keeps OpenAI system fields on the message-based path", () => {
const payload = {
sessionId: "sess-1",
comboName: "relay-combo",
fromAccount: "conn-a",
summary: "Keep the OpenAI request shape",
keyDecisions: [],
taskProgress: "Continue",
activeEntities: [],
messageCount: 1,
model: "openai/gpt-5",
warningThresholdPct: 0.85,
generatedAt: "2099-04-08T12:00:00.000Z",
expiresAt: "2099-04-08T17:00:00.000Z",
};
const body = {
system: undefined,
messages: [{ role: "user", content: "Continue" }],
};
const injected = contextHandoff.injectHandoffIntoBody(body, payload, undefined, "openai");
assert.equal((injected.messages as Array<Record<string, unknown>>)[0]?.role, "system");
assert.equal(injected.system, undefined);
});
test("injectHandoffIntoBody preserves Responses API shape for native Codex requests", () => {
const payload = {
sessionId: "sess-1",

View File

@@ -314,6 +314,49 @@ test("injectUniversalHandoffBody preserves original system message", () => {
assert.strictEqual(r.messages[1].content, "Be helpful");
});
test("injectUniversalHandoffBody appends Claude-native handoff to top-level system", () => {
const body = {
model: CURR,
system: [{ type: "text", text: "Existing Claude prompt" }],
max_tokens: 64,
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
};
const r = injectUniversalHandoffBody(body, PREV, CURR, REASON, null, undefined, "claude");
const system = r.system as Array<Record<string, unknown>>;
assert.strictEqual(r.messages, body.messages);
assert.strictEqual(system[0], body.system[0]);
assert.match(String(system[1]?.text), /<context_handoff>/);
assert.equal(system[1]?.type, "text");
});
test("injectUniversalHandoffBody keeps OpenAI bodies with top-level system on messages path", () => {
const body = {
system: null,
messages: [{ role: "user", content: "Hello" }],
};
const r = injectUniversalHandoffBody(body, PREV, CURR, REASON, null, undefined, "openai");
assert.equal((r.messages as Array<Record<string, unknown>>)[0]?.role, "system");
assert.equal(r.system, null);
});
test("injectUniversalHandoffBody creates Claude top-level system when absent", () => {
const body = {
model: CURR,
max_tokens: 64,
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
};
const r = injectUniversalHandoffBody(body, PREV, CURR, REASON, null, undefined, "claude");
const system = r.system as Array<Record<string, unknown>>;
assert.strictEqual(r.messages, body.messages);
assert.match(String(system[0]?.text), /<context_handoff>/);
});
test("injectUniversalHandoffBody Responses API with instructions", () => {
const body = { input: "Hi", instructions: "Be nice" };
const r = injectUniversalHandoffBody(body, PREV, CURR, REASON, null);