diff --git a/gateway/src/api/responses.ts b/gateway/src/api/responses.ts index b68f147..db15a2f 100644 --- a/gateway/src/api/responses.ts +++ b/gateway/src/api/responses.ts @@ -46,7 +46,7 @@ export class ResponsesAPI extends BaseAPI { - return responseBody.output.map(mapOutputMessage) + return [mapOutputMessage(responseBody.output)] } // SafeExtractor implementation @@ -87,7 +87,7 @@ export class ResponsesAPI extends BaseAPI { - this.extractedResponse.outputMessages = responseBody.output.map(mapOutputMessage) + this.extractedResponse.outputMessages = [mapOutputMessage(responseBody.output)] }, } @@ -234,55 +234,54 @@ function mapInputMessage(input: ResponseInputItem): ChatMessage { ) } -function mapOutputMessage(output: ResponseOutputItem): OutputMessage { - return match(output) - .returnType() - .with({ content: P.array(P.union({ type: 'output_text' }, { type: 'refusal' })) }, (_output) => { - return { - role: _output.role, - parts: _output.content.map((part) => { - return ( - match(part) +function mapOutputMessage(output: ResponseOutputItem[]): OutputMessage { + return { role: 'assistant', parts: mapOutputParts(output) } +} + +function mapOutputParts(output: ResponseOutputItem[]): MessagePart[] { + const parts: MessagePart[] = [] + + for (const item of output) { + match(item) + .with({ type: 'reasoning' }, (item) => { + parts.push({ type: 'thinking' as const, content: item.summary.join('\n') }) + }) + .with({ content: P.array(P.union({ type: 'output_text' }, { type: 'refusal' })) }, (_item) => { + parts.push( + ..._item.content.map((part) => { + return match(part) .returnType() .with({ type: 'output_text' }, (_part) => { return { type: 'text', content: _part.text } }) - // TODO(Marcelo): How do we represent refusals? .with({ type: 'refusal' }, (_part) => { return { type: 'unknown', part: { ..._part } } }) .exhaustive() - ) - }), - } - }) - .with({ type: 'function_call' }, (_output) => { - return { - role: 'assistant', - parts: [{ type: 'tool_call', id: _output.call_id, name: _output.name, arguments: _output.arguments }], - } - }) - .with({ type: 'reasoning' }, (_output) => { - return { - role: 'assistant', - parts: _output.summary.map((summary) => ({ type: 'thinking', content: summary.text })), - } - }) - .with( - P.union( - { type: 'file_search_call' }, - { type: 'code_interpreter_call' }, - { type: 'image_generation_call' }, - { type: 'local_shell_call' }, - { type: 'computer_call' }, - { type: 'mcp_call' }, - { type: 'mcp_list_tools' }, - { type: 'mcp_approval_request' }, - { type: 'web_search_call' }, - ), - (_output) => { - return { role: 'assistant', parts: [{ type: 'unknown', part: { ..._output } }] } - }, - ) - .exhaustive() + }), + ) + }) + .with({ type: 'function_call' }, (_item) => { + parts.push({ type: 'tool_call', id: _item.call_id, name: _item.name, arguments: _item.arguments }) + }) + .with( + P.union( + { type: 'file_search_call' }, + { type: 'code_interpreter_call' }, + { type: 'image_generation_call' }, + { type: 'local_shell_call' }, + { type: 'computer_call' }, + { type: 'mcp_call' }, + { type: 'mcp_list_tools' }, + { type: 'mcp_approval_request' }, + { type: 'web_search_call' }, + ), + (_item) => { + parts.push({ type: 'unknown', part: { ..._item } }) + }, + ) + .exhaustive() + } + + return parts } diff --git a/gateway/test/providers/openai.spec.ts.snap b/gateway/test/providers/openai.spec.ts.snap index 4a91048..860f1c0 100644 --- a/gateway/test/providers/openai.spec.ts.snap +++ b/gateway/test/providers/openai.spec.ts.snap @@ -950,12 +950,12 @@ exports[`openai > openai responses > span 1`] = ` ], "gen_ai.operation.name": "chat", "gen_ai.output.messages": [ - { - "parts": [], - "role": "assistant", - }, { "parts": [ + { + "content": "", + "type": "thinking", + }, { "content": "Usually blue in daytime, but it can be red/orange at sunrise/sunset, gray when cloudy, and black at night.", "type": "text", @@ -1907,12 +1907,12 @@ exports[`openai > openai responses with builtin tools > span 1`] = ` ], "gen_ai.operation.name": "chat", "gen_ai.output.messages": [ - { - "parts": [], - "role": "assistant", - }, { "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "import math, decimal @@ -1926,15 +1926,10 @@ math.sqrt(n)", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "from decimal import Decimal, getcontext @@ -1950,15 +1945,10 @@ root", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "import math @@ -1973,15 +1963,10 @@ s, s*s <= n, (s+1)*(s+1) > n", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "s=351_997 @@ -1994,15 +1979,10 @@ s*s, (s+1)*(s+1)", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "from decimal import Decimal, getcontext, ROUND_HALF_UP @@ -2021,15 +2001,10 @@ str(rounded)", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "part": { "code": "from decimal import Decimal, getcontext, ROUND_HALF_UP @@ -2046,15 +2021,10 @@ root.quantize(q, rounding=ROUND_HALF_UP)", }, "type": "unknown", }, - ], - "role": "assistant", - }, - { - "parts": [], - "role": "assistant", - }, - { - "parts": [ + { + "content": "", + "type": "thinking", + }, { "content": "The square root of 123902139123 is approximately 351,997.356698882044317918.", "type": "text",