1717use Prism \Prism \Exceptions \PrismException ;
1818use Prism \Prism \Exceptions \PrismRateLimitedException ;
1919use Prism \Prism \Providers \OpenAI \Concerns \ProcessesRateLimits ;
20- use Prism \Prism \Providers \OpenAI \Maps \ChatMessageMap ;
2120use Prism \Prism \Providers \OpenAI \Maps \FinishReasonMap ;
21+ use Prism \Prism \Providers \OpenAI \Maps \MessageMap ;
2222use Prism \Prism \Providers \OpenAI \Maps \ToolChoiceMap ;
2323use Prism \Prism \Providers \OpenAI \Maps \ToolMap ;
2424use Prism \Prism \Text \Chunk ;
2525use Prism \Prism \Text \Request ;
2626use Prism \Prism \ValueObjects \Messages \AssistantMessage ;
2727use Prism \Prism \ValueObjects \Messages \ToolResultMessage ;
28+ use Prism \Prism \ValueObjects \Meta ;
2829use Prism \Prism \ValueObjects \ToolCall ;
2930use Psr \Http \Message \StreamInterface ;
3031use Throwable ;
@@ -50,37 +51,45 @@ public function handle(Request $request): Generator
5051 */
5152 protected function processStream (Response $ response , Request $ request , int $ depth = 0 ): Generator
5253 {
53- // Prevent infinite recursion with tool calls
54- if ($ depth >= $ request ->maxSteps ()) {
55- throw new PrismException ('Maximum tool call chain depth exceeded ' );
56- }
5754 $ text = '' ;
5855 $ toolCalls = [];
56+ $ reasoningItems = [];
5957
6058 while (! $ response ->getBody ()->eof ()) {
6159 $ data = $ this ->parseNextDataLine ($ response ->getBody ());
6260
63- // Skip empty data or DONE markers
6461 if ($ data === null ) {
6562 continue ;
6663 }
6764
68- // Process tool calls
69- if ($ this ->hasToolCalls ($ data )) {
70- $ toolCalls = $ this ->extractToolCalls ($ data , $ toolCalls );
65+ if ($ data ['type ' ] === 'response.created ' ) {
66+ yield new Chunk (
67+ text: '' ,
68+ finishReason: null ,
69+ meta: new Meta (
70+ id: $ data ['response ' ]['id ' ] ?? null ,
71+ model: $ data ['response ' ]['model ' ] ?? null ,
72+ ),
73+ chunkType: ChunkType::Meta,
74+ );
7175
7276 continue ;
7377 }
7478
75- // Handle tool call completion
76- if ($ this ->mapFinishReason ($ data ) === FinishReason::ToolCalls) {
77- yield from $ this ->handleToolCalls ($ request , $ text , $ toolCalls , $ depth );
79+ if ($ this ->hasReasoningItems ($ data )) {
80+ $ reasoningItems = $ this ->extractReasoningItems ($ data , $ reasoningItems );
7881
79- return ;
82+ continue ;
8083 }
8184
82- // Process regular content
83- $ content = data_get ($ data , 'choices.0.delta.content ' , '' ) ?? '' ;
85+ if ($ this ->hasToolCalls ($ data )) {
86+ $ toolCalls = $ this ->extractToolCalls ($ data , $ toolCalls , $ reasoningItems );
87+
88+ continue ;
89+ }
90+
91+ $ content = $ this ->extractOutputTextDelta ($ data );
92+
8493 $ text .= $ content ;
8594
8695 $ finishReason = $ this ->mapFinishReason ($ data );
@@ -90,10 +99,14 @@ protected function processStream(Response $response, Request $request, int $dept
9099 finishReason: $ finishReason !== FinishReason::Unknown ? $ finishReason : null
91100 );
92101 }
102+
103+ if ($ toolCalls !== []) {
104+ yield from $ this ->handleToolCalls ($ request , $ text , $ toolCalls , $ depth );
105+ }
93106 }
94107
95108 /**
96- * @return array<string, mixed>|null Parsed JSON data or null if line should be skipped
109+ * @return array<string, mixed>|null
97110 */
98111 protected function parseNextDataLine (StreamInterface $ stream ): ?array
99112 {
@@ -119,21 +132,46 @@ protected function parseNextDataLine(StreamInterface $stream): ?array
119132 /**
120133 * @param array<string, mixed> $data
121134 * @param array<int, array<string, mixed>> $toolCalls
135+ * @param array<int, array<string, mixed>> $reasoningItems
122136 * @return array<int, array<string, mixed>>
123137 */
124- protected function extractToolCalls (array $ data , array $ toolCalls ): array
138+ protected function extractToolCalls (array $ data , array $ toolCalls, array $ reasoningItems = [] ): array
125139 {
126- foreach (data_get ($ data , 'choices.0.delta.tool_calls ' , []) as $ index => $ toolCall ) {
127- if ($ name = data_get ($ toolCall , 'function.name ' )) {
128- $ toolCalls [$ index ]['name ' ] = $ name ;
129- $ toolCalls [$ index ]['arguments ' ] = '' ;
130- $ toolCalls [$ index ]['id ' ] = data_get ($ toolCall , 'id ' );
140+ $ type = data_get ($ data , 'type ' , '' );
141+
142+ if ($ type === 'response.output_item.added ' && data_get ($ data , 'item.type ' ) === 'function_call ' ) {
143+ $ index = (int ) data_get ($ data , 'output_index ' , count ($ toolCalls ));
144+
145+ $ toolCalls [$ index ]['id ' ] = data_get ($ data , 'item.id ' );
146+ $ toolCalls [$ index ]['call_id ' ] = data_get ($ data , 'item.call_id ' );
147+ $ toolCalls [$ index ]['name ' ] = data_get ($ data , 'item.name ' );
148+ $ toolCalls [$ index ]['arguments ' ] = '' ;
149+
150+ // Associate with the most recent reasoning item if available
151+ if ($ reasoningItems !== []) {
152+ $ latestReasoning = end ($ reasoningItems );
153+ $ toolCalls [$ index ]['reasoning_id ' ] = $ latestReasoning ['id ' ];
154+ $ toolCalls [$ index ]['reasoning_summary ' ] = $ latestReasoning ['summary ' ] ?? [];
131155 }
132156
133- $ arguments = data_get ($ toolCall , 'function.arguments ' );
157+ return $ toolCalls ;
158+ }
159+
160+ if ($ type === 'response.function_call_arguments.delta ' ) {
161+ // continue for now, only needed if we want to support streaming argument chunks
162+ }
134163
135- if (! is_null ($ arguments )) {
136- $ toolCalls [$ index ]['arguments ' ] .= $ arguments ;
164+ if ($ type === 'response.function_call_arguments.done ' ) {
165+ $ callId = data_get ($ data , 'item_id ' );
166+ $ arguments = data_get ($ data , 'arguments ' , '' );
167+
168+ foreach ($ toolCalls as &$ call ) {
169+ if (($ call ['id ' ] ?? null ) === $ callId ) {
170+ if ($ arguments !== '' ) {
171+ $ call ['arguments ' ] = $ arguments ;
172+ }
173+ break ;
174+ }
137175 }
138176 }
139177
@@ -169,8 +207,13 @@ protected function handleToolCalls(
169207 $ request ->addMessage (new AssistantMessage ($ text , $ toolCalls ));
170208 $ request ->addMessage (new ToolResultMessage ($ toolResults ));
171209
172- $ nextResponse = $ this ->sendRequest ($ request );
173- yield from $ this ->processStream ($ nextResponse , $ request , $ depth + 1 );
210+ $ depth ++;
211+
212+ if ($ depth < $ request ->maxSteps ()) {
213+ $ nextResponse = $ this ->sendRequest ($ request );
214+
215+ yield from $ this ->processStream ($ nextResponse , $ request , $ depth );
216+ }
174217 }
175218
176219 /**
@@ -183,9 +226,12 @@ protected function mapToolCalls(array $toolCalls): array
183226 {
184227 return collect ($ toolCalls )
185228 ->map (fn ($ toolCall ): ToolCall => new ToolCall (
186- data_get ($ toolCall , 'id ' ),
187- data_get ($ toolCall , 'name ' ),
188- data_get ($ toolCall , 'arguments ' ),
229+ id: data_get ($ toolCall , 'id ' ),
230+ name: data_get ($ toolCall , 'name ' ),
231+ arguments: data_get ($ toolCall , 'arguments ' ),
232+ resultId: data_get ($ toolCall , 'call_id ' ),
233+ reasoningId: data_get ($ toolCall , 'reasoning_id ' ),
234+ reasoningSummary: data_get ($ toolCall , 'reasoning_summary ' , []),
189235 ))
190236 ->toArray ();
191237 }
@@ -195,15 +241,68 @@ protected function mapToolCalls(array $toolCalls): array
195241 */
196242 protected function hasToolCalls (array $ data ): bool
197243 {
198- return (bool ) data_get ($ data , 'choices.0.delta.tool_calls ' );
244+ $ type = data_get ($ data , 'type ' , '' );
245+
246+ if (data_get ($ data , 'item.type ' ) === 'function_call ' ) {
247+ return true ;
248+ }
249+
250+ return in_array ($ type , [
251+ 'response.function_call_arguments.delta ' ,
252+ 'response.function_call_arguments.done ' ,
253+ ]);
254+ }
255+
256+ /**
257+ * @param array<string, mixed> $data
258+ */
259+ protected function hasReasoningItems (array $ data ): bool
260+ {
261+ $ type = data_get ($ data , 'type ' , '' );
262+
263+ return $ type === 'response.output_item.done ' && data_get ($ data , 'item.type ' ) === 'reasoning ' ;
264+ }
265+
266+ /**
267+ * @param array<string, mixed> $data
268+ * @param array<int, array<string, mixed>> $reasoningItems
269+ * @return array<int, array<string, mixed>>
270+ */
271+ protected function extractReasoningItems (array $ data , array $ reasoningItems ): array
272+ {
273+ if (data_get ($ data , 'type ' ) === 'response.output_item.done ' && data_get ($ data , 'item.type ' ) === 'reasoning ' ) {
274+ $ index = (int ) data_get ($ data , 'output_index ' , count ($ reasoningItems ));
275+
276+ $ reasoningItems [$ index ] = [
277+ 'id ' => data_get ($ data , 'item.id ' ),
278+ 'summary ' => data_get ($ data , 'item.summary ' , []),
279+ ];
280+ }
281+
282+ return $ reasoningItems ;
199283 }
200284
201285 /**
202286 * @param array<string, mixed> $data
203287 */
204288 protected function mapFinishReason (array $ data ): FinishReason
205289 {
206- return FinishReasonMap::map (data_get ($ data , 'choices.0.finish_reason ' ) ?? '' );
290+ $ eventType = Str::after (data_get ($ data , 'type ' ), 'response. ' );
291+ $ lastOutputType = data_get ($ data , 'response.output.{last}.type ' );
292+
293+ return FinishReasonMap::map ($ eventType , $ lastOutputType );
294+ }
295+
296+ /**
297+ * @param array<string, mixed> $data
298+ */
299+ protected function extractOutputTextDelta (array $ data ): string
300+ {
301+ if (data_get ($ data , 'type ' ) === 'response.output_text.delta ' ) {
302+ return (string ) data_get ($ data , 'delta ' , '' );
303+ }
304+
305+ return '' ;
207306 }
208307
209308 protected function sendRequest (Request $ request ): Response
@@ -214,18 +313,20 @@ protected function sendRequest(Request $request): Response
214313 ->withOptions (['stream ' => true ])
215314 ->throw ()
216315 ->post (
217- 'chat/completions ' ,
316+ 'responses ' ,
218317 array_merge ([
219318 'stream ' => true ,
220319 'model ' => $ request ->model (),
221- 'messages ' => (new ChatMessageMap ($ request ->messages (), $ request ->systemPrompts ()))(),
222- 'max_completion_tokens ' => $ request ->maxTokens (),
320+ 'input ' => (new MessageMap ($ request ->messages (), $ request ->systemPrompts ()))(),
321+ 'max_output_tokens ' => $ request ->maxTokens (),
223322 ], Arr::whereNotNull ([
224323 'temperature ' => $ request ->temperature (),
225324 'top_p ' => $ request ->topP (),
226325 'metadata ' => $ request ->providerOptions ('metadata ' ),
227326 'tools ' => ToolMap::map ($ request ->tools ()),
228327 'tool_choice ' => ToolChoiceMap::map ($ request ->toolChoice ()),
328+ 'previous_response_id ' => $ request ->providerOptions ('previous_response_id ' ),
329+ 'truncation ' => $ request ->providerOptions ('truncation ' ),
229330 ]))
230331 );
231332 } catch (Throwable $ e ) {
0 commit comments