@@ -58,6 +58,63 @@ export function responseFromOpenAI(body: unknown): NormalizedResponse {
5858 } ;
5959}
6060
61+ const OPENAI_RESPONSES_INCOMPLETE : Record < string , FinishReason > = {
62+ max_output_tokens : 'length' ,
63+ content_filter : 'content_filter' ,
64+ } ;
65+
66+ function responseApiFinishReason ( root : Record < string , unknown > ) : FinishReason {
67+ if ( root . status === 'completed' ) return 'stop' ;
68+ if ( root . status !== 'incomplete' ) return 'unknown' ;
69+
70+ const details = isRecord ( root . incomplete_details ) ? root . incomplete_details : { } ;
71+ return OPENAI_RESPONSES_INCOMPLETE [ String ( details . reason ) ] ?? 'unknown' ;
72+ }
73+
74+ /** Normalizes an OpenAI Responses API response body. */
75+ export function responseFromOpenAIResponses ( body : unknown ) : NormalizedResponse {
76+ const root = isRecord ( body ) ? body : { } ;
77+ const output = Array . isArray ( root . output ) ? root . output : [ ] ;
78+ const textPieces : string [ ] = [ ] ;
79+ const toolCalls : OpenAIToolCall [ ] = [ ] ;
80+ let counter = 0 ;
81+
82+ for ( const item of output ) {
83+ if ( ! isRecord ( item ) ) continue ;
84+
85+ if ( item . type === 'message' ) {
86+ const content = Array . isArray ( item . content ) ? item . content : [ ] ;
87+ for ( const part of content ) {
88+ if ( ! isRecord ( part ) ) continue ;
89+ if ( typeof part . text === 'string' && ( part . type === 'output_text' || part . type === 'text' ) ) {
90+ textPieces . push ( part . text ) ;
91+ } else if ( part . type === 'refusal' && typeof part . refusal === 'string' ) {
92+ textPieces . push ( part . refusal ) ;
93+ }
94+ }
95+ } else if ( item . type === 'function_call' && typeof item . name === 'string' ) {
96+ const name = item . name ;
97+ const id =
98+ typeof item . call_id === 'string'
99+ ? item . call_id
100+ : typeof item . id === 'string'
101+ ? item . id
102+ : `call_${ name . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_' ) } _${ counter ++ } ` ;
103+ const args = typeof item . arguments === 'string' ? item . arguments : JSON . stringify ( item . arguments ?? { } ) ;
104+ toolCalls . push ( { id, type : 'function' , function : { name, arguments : args } } ) ;
105+ }
106+ }
107+
108+ const usage = isRecord ( root . usage ) ? root . usage : { } ;
109+ return {
110+ message : buildMessage ( textPieces . join ( '' ) , toolCalls ) ,
111+ finishReason : finalReason ( responseApiFinishReason ( root ) , toolCalls ) ,
112+ usage : { inputTokens : num ( usage . input_tokens ) , outputTokens : num ( usage . output_tokens ) } ,
113+ } ;
114+ }
115+
116+ export type ResponseProvider = Provider | 'openai-responses' ;
117+
61118/* ------------------------------- Anthropic ----------------------------- */
62119
63120const ANTHROPIC_FINISH : Record < string , FinishReason > = {
@@ -147,12 +204,14 @@ export function responseFromGemini(body: unknown, options: ConvertOptions = {}):
147204/** Normalizes a provider response body into the canonical shape. */
148205export function normalizeResponse (
149206 body : unknown ,
150- route : { from : Provider } ,
207+ route : { from : ResponseProvider } ,
151208 options : ConvertOptions = { } ,
152209) : NormalizedResponse {
153210 switch ( route . from ) {
154211 case 'openai' :
155212 return responseFromOpenAI ( body ) ;
213+ case 'openai-responses' :
214+ return responseFromOpenAIResponses ( body ) ;
156215 case 'anthropic' :
157216 return responseFromAnthropic ( body ) ;
158217 case 'gemini' :
0 commit comments