Files
pxpipe/tests/responses-bridge-roles.test.ts
Shubham Srivastava fbe2c251b8 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>
2026-07-20 22:14:18 -04:00

66 lines
2.7 KiB
TypeScript

/**
* Messages -> Responses bridge: message-role handling.
*
* Pins the role validation that newer Claude Code builds exercise by injecting
* role:"system" entries inside `messages` (system reminders). The bridge must
* map them to Responses system input items rather than rejecting the request.
*
* Run just this file: pnpm vitest run tests/responses-bridge-roles.test.ts
*/
import { describe, expect, it } from 'vitest';
import { anthropicMessagesToOpenAIResponses } from '../src/core/messages-responses-bridge.js';
const enc = (obj: unknown): Uint8Array => new TextEncoder().encode(JSON.stringify(obj));
const dec = (b: Uint8Array): any => JSON.parse(new TextDecoder().decode(b));
const toResponses = (req: unknown): any => dec(anthropicMessagesToOpenAIResponses(enc(req)));
describe('anthropicMessagesToOpenAIResponses — message roles', () => {
it('maps in-conversation system-role messages to system input items', () => {
const out = toResponses({
model: 'm',
messages: [
{ role: 'user', content: [{ type: 'text', text: 'hi' }] },
{ role: 'system', content: [{ type: 'text', text: 'Available agent types: cdp' }] },
],
});
const system = out.input.filter((item: any) => item.role === 'system');
expect(system).toEqual([
{ role: 'system', content: [{ type: 'input_text', text: 'Available agent types: cdp' }] },
]);
});
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',
messages: [
{ role: 'system', content: [] },
{ role: 'user', content: 'q' },
],
});
expect(out.input.some((item: any) => item.role === 'system')).toBe(false);
expect(() =>
toResponses({ model: 'm', messages: [{ role: 'tool', content: 'x' }] }),
).toThrow(/user, assistant, or system role/);
});
});