Prevent xAI replay from exposing persisted Anthropic reasoning

The xAI Responses path reuses convertToResponsesApiInput() when replaying
persisted conversation history. Anthropic thinking blocks were being
flattened into ordinary assistant-visible output_text entries, which
changes conversation semantics and leaks hidden reasoning into later
prompt context.

This keeps the fix intentionally narrow: persisted Anthropic thinking
blocks are now skipped for Responses replay, and a regression test
covers the behavior.

Constraint: Keep the change scoped to the replay transform without changing provider selection or task persistence
Rejected: Preserve thinking by converting it to [Thinking] output_text | turns hidden reasoning into visible assistant content
Confidence: medium
Scope-risk: narrow
Reversibility: clean
Directive: If a provider-specific hidden reasoning replay format is added later, prefer that over flattening Anthropic thinking into visible text
Tested: pnpm --dir src exec vitest run api/transform/__tests__/responses-api-input.spec.ts api/providers/__tests__/xai.spec.ts
Not-tested: Full end-to-end provider-switch replay with live xAI credentials
This commit is contained in:
JunghwanNA 2026-04-17 01:03:22 +09:00
parent cb83656718
commit f8a2873be9
2 changed files with 27 additions and 8 deletions

View file

@ -280,6 +280,28 @@ describe("convertToResponsesApiInput", () => {
}),
)
})
it("should not replay Anthropic thinking blocks as assistant-visible text", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{ type: "thinking", thinking: "SECRET_CHAIN_OF_THOUGHT", signature: "sig-1" } as any,
{ type: "text", text: "visible answer" },
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "visible answer" }],
},
])
})
})
describe("multi-turn conversations", () => {

View file

@ -53,14 +53,11 @@ export function convertToResponsesApiInput(messages: Anthropic.Messages.MessageP
})
break
case "thinking":
// Include reasoning if it has content
if ((part as any).thinking && (part as any).thinking.trim().length > 0) {
input.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: `[Thinking] ${(part as any).thinking}` }],
})
}
// Anthropic thinking blocks represent hidden reasoning. The
// Responses API input format does not have a compatible hidden
// reasoning representation for these persisted blocks, so replaying
// them as normal assistant-visible text changes conversation
// semantics. Skip them instead of flattening them into output_text.
break
}
}