mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(responses-bridge): encode assistant text as output_text, not input_text (#129)
When a Claude Code /v1/messages request is bridged to a GPT model on the Responses path (bridgedGptMessages), prior assistant text turns were lowered to content parts of type input_text. The OpenAI Responses API requires assistant-role message content to be output_text; input_text under role:assistant is rejected with 400, breaking every multi-turn session to a Responses-routed GPT model after the first assistant reply. The bridge's read side already maps assistant text to/from output_text — only the write side was wrong. Thread the role into inputParts and pick the text type accordingly; user/system stay input_text. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
b754d95295
commit
fbe2c251b8
@@ -36,14 +36,18 @@ function imageUrl(source: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function inputParts(content: unknown, location = 'message'): JsonObject[] {
|
||||
if (typeof content === 'string') return [{ type: 'input_text', text: content }];
|
||||
function inputParts(content: unknown, location = 'message', role = 'user'): JsonObject[] {
|
||||
// Responses requires assistant-role message text to be `output_text`;
|
||||
// `input_text` is only valid for user/system. Emitting input_text under
|
||||
// role:"assistant" (any replayed assistant turn) is a 400.
|
||||
const textType = role === 'assistant' ? 'output_text' : 'input_text';
|
||||
if (typeof content === 'string') return [{ type: textType, text: content }];
|
||||
if (!Array.isArray(content)) invalidRequest(`${location} content must be a string or an array`);
|
||||
const out: JsonObject[] = [];
|
||||
for (const raw of content) {
|
||||
const part = object(raw);
|
||||
if (part?.type === 'text' && typeof part.text === 'string') {
|
||||
out.push({ type: 'input_text', text: part.text });
|
||||
out.push({ type: textType, text: part.text });
|
||||
} else if (part?.type === 'image') {
|
||||
const image_url = imageUrl(part.source);
|
||||
if (!image_url) invalidRequest(`Unsupported ${location} image source`);
|
||||
@@ -120,7 +124,7 @@ export function anthropicMessagesToOpenAIResponses(body: Uint8Array): Uint8Array
|
||||
}
|
||||
const content = message.content;
|
||||
if (!Array.isArray(content)) {
|
||||
const ordinary = inputParts(content, `${String(message.role)} message`);
|
||||
const ordinary = inputParts(content, `${String(message.role)} message`, String(message.role));
|
||||
if (ordinary.length) input.push({ role: message.role, content: ordinary });
|
||||
continue;
|
||||
}
|
||||
@@ -149,7 +153,7 @@ export function anthropicMessagesToOpenAIResponses(body: Uint8Array): Uint8Array
|
||||
output: functionOutput(part.content, part.is_error === true),
|
||||
});
|
||||
} else {
|
||||
ordinary.push(...inputParts([rawPart], `${String(message.role)} message`));
|
||||
ordinary.push(...inputParts([rawPart], `${String(message.role)} message`, String(message.role)));
|
||||
}
|
||||
}
|
||||
flushOrdinary();
|
||||
|
||||
@@ -29,6 +29,26 @@ describe('anthropicMessagesToOpenAIResponses — message roles', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('encodes assistant text as output_text (Responses rejects input_text under role:assistant)', () => {
|
||||
const out = toResponses({
|
||||
model: 'm',
|
||||
messages: [
|
||||
{ role: 'user', content: 'hi' },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'Hello!' }] },
|
||||
{ role: 'assistant', content: 'plain string reply' },
|
||||
{ role: 'user', content: 'continue' },
|
||||
],
|
||||
});
|
||||
const assistant = out.input.filter((item: any) => item.role === 'assistant');
|
||||
expect(assistant).toEqual([
|
||||
{ role: 'assistant', content: [{ type: 'output_text', text: 'Hello!' }] },
|
||||
{ role: 'assistant', content: [{ type: 'output_text', text: 'plain string reply' }] },
|
||||
]);
|
||||
// User text stays input_text.
|
||||
const user = out.input.filter((item: any) => item.role === 'user');
|
||||
expect(user[0].content[0].type).toBe('input_text');
|
||||
});
|
||||
|
||||
it('drops empty system-role messages and rejects unknown roles', () => {
|
||||
const out = toResponses({
|
||||
model: 'm',
|
||||
|
||||
Reference in New Issue
Block a user