44
55namespace Cortex \LLM \Drivers ;
66
7- use Generator ;
7+ use Exception ;
88use Throwable ;
99use DateTimeImmutable ;
10- use Cortex \Support \Utils ;
1110use Cortex \LLM \Data \Usage ;
1211use Cortex \LLM \AbstractLLM ;
1312use Illuminate \Support \Arr ;
2019use Cortex \Events \ChatModelStart ;
2120use Cortex \LLM \Contracts \Message ;
2221use Cortex \LLM \Data \FunctionCall ;
23- use Cortex \Events \ChatModelStream ;
2422use Cortex \LLM \Enums \FinishReason ;
2523use Cortex \Exceptions \LLMException ;
2624use Cortex \LLM \Data \ChatGeneration ;
3028use Cortex \LLM \Data \ResponseMetadata ;
3129use OpenAI \Contracts \ResponseContract ;
3230use Cortex \LLM \Data \ToolCallCollection ;
33- use Cortex \LLM \Data \ChatGenerationChunk ;
3431use Cortex \ModelInfo \Enums \ModelFeature ;
3532use Cortex \LLM \Data \Messages \ToolMessage ;
3633use Cortex \ModelInfo \Enums \ModelProvider ;
37- use Cortex \OutputParsers \JsonOutputParser ;
38- use Cortex \Exceptions \OutputParserException ;
3934use Cortex \LLM \Data \Messages \AssistantMessage ;
4035use OpenAI \Responses \Responses \CreateResponse ;
4136use Cortex \LLM \Data \Messages \MessageCollection ;
4439use Cortex \LLM \Data \Messages \Content \AudioContent ;
4540use Cortex \LLM \Data \Messages \Content \ImageContent ;
4641use OpenAI \Responses \Responses \CreateResponseUsage ;
47- use OpenAI \Responses \Responses \CreateResponseChoice ;
4842use OpenAI \Responses \Responses \Output \OutputMessage ;
4943use Cortex \LLM \Data \Messages \Content \ReasoningContent ;
50- use OpenAI \Responses \Responses \CreateResponseToolCall ;
5144use OpenAI \Responses \Responses \Output \OutputReasoning ;
52- use OpenAI \Responses \Responses \Output \OutputComputerToolCall ;
5345use OpenAI \Responses \Responses \Output \OutputFunctionToolCall ;
54- use OpenAI \Responses \Responses \Output \OutputWebSearchToolCall ;
55- use OpenAI \Responses \Responses \Output \OutputFileSearchToolCall ;
5646use OpenAI \Responses \Responses \Output \OutputMessageContentRefusal ;
5747use OpenAI \Responses \Responses \Output \OutputMessageContentOutputText ;
5848use OpenAI \Testing \Responses \Fixtures \Responses \CreateResponseFixture ;
@@ -101,24 +91,24 @@ protected function mapResponse(CreateResponse $response): ChatResult
10191 $ finishReason = static ::mapFinishReason ($ response ->status );
10292
10393 /** @var \OpenAI\Responses\Responses\Output\OutputMessage $outputMessage */
104- $ outputMessage = $ output ->filter (fn (ResponseContract $ item ) => $ item instanceof OutputMessage)->first ();
94+ $ outputMessage = $ output ->filter (fn (ResponseContract $ item ): bool => $ item instanceof OutputMessage)->first ();
10595
10696 $ outputMessageContent = collect ($ outputMessage ->content );
10797
108- if ($ outputMessageContent ->contains (fn (ResponseContract $ item ) => $ item instanceof OutputMessageContentRefusal)) {
98+ if ($ outputMessageContent ->contains (fn (ResponseContract $ item ): bool => $ item instanceof OutputMessageContentRefusal)) {
10999 throw new LLMException ('LLM refusal: ' . $ outputMessageContent ->first ()->refusal );
110100 }
111101
112102 /** @var \OpenAI\Responses\Responses\Output\OutputMessageContentOutputText $textContent */
113103 $ textContent = $ outputMessageContent
114- ->filter (fn (ResponseContract $ item ) => $ item instanceof OutputMessageContentOutputText)
104+ ->filter (fn (ResponseContract $ item ): bool => $ item instanceof OutputMessageContentOutputText)
115105 ->first ();
116106
117107 // TODO: Ignore other provider specific tool calls
118108 // and only support function tool calls for now
119109 $ toolCalls = $ output
120- ->filter (fn (ResponseContract $ item ) => $ item instanceof OutputFunctionToolCall)
121- ->map (fn (OutputFunctionToolCall $ toolCall ) => new ToolCall (
110+ ->filter (fn (ResponseContract $ item ): bool => $ item instanceof OutputFunctionToolCall)
111+ ->map (fn (OutputFunctionToolCall $ toolCall ): ToolCall => new ToolCall (
122112 $ toolCall ->id ,
123113 new FunctionCall (
124114 $ toolCall ->name ,
@@ -128,16 +118,15 @@ protected function mapResponse(CreateResponse $response): ChatResult
128118 ->all ();
129119
130120 $ reasoningContent = $ output
131- ->filter (fn (ResponseContract $ item ) => $ item instanceof OutputReasoning)
132- ->map (fn (OutputReasoning $ reasoning ) => new ReasoningContent (
121+ ->filter (fn (ResponseContract $ item ): bool => $ item instanceof OutputReasoning)
122+ ->map (fn (OutputReasoning $ reasoning ): ReasoningContent => new ReasoningContent (
133123 $ reasoning ->id ,
134- $ reasoning ->summary [ 0 ] ->text ,
124+ Arr:: first ( $ reasoning ->summary ) ->text ?? '' ,
135125 ))
136126 ->all ();
137127
138128 $ generation = new ChatGeneration (
139129 message: new AssistantMessage (
140- id: $ outputMessage ->id ,
141130 content: [
142131 ...$ reasoningContent ,
143132 new TextContent ($ textContent ->text ),
@@ -150,6 +139,7 @@ protected function mapResponse(CreateResponse $response): ChatResult
150139 finishReason: $ finishReason ,
151140 usage: $ usage ,
152141 ),
142+ id: $ outputMessage ->id ,
153143 ),
154144 index: 0 ,
155145 createdAt: DateTimeImmutable::createFromFormat ('U ' , (string ) $ response ->createdAt ),
@@ -164,7 +154,7 @@ protected function mapResponse(CreateResponse $response): ChatResult
164154 $ result = new ChatResult (
165155 [$ generation ],
166156 $ usage ,
167- $ rawResponse ,
157+ $ rawResponse , // @phpstan-ignore argument.type
168158 );
169159
170160 $ this ->dispatchEvent (new ChatModelEnd ($ result ));
@@ -175,14 +165,13 @@ protected function mapResponse(CreateResponse $response): ChatResult
175165 /**
176166 * Map a streaming response to a ChatStreamResult.
177167 *
178- * @param StreamResponse<\OpenAI\Responses\Chat \CreateStreamedResponse> $response
168+ * @param StreamResponse<\OpenAI\Responses\Responses \CreateStreamedResponse> $response
179169 *
180- * @return ChatStreamResult<ChatGenerationChunk>
170+ * @return \Cortex\LLM\Data\ ChatStreamResult<\Cortex\LLM\Data\ ChatGenerationChunk>
181171 */
182172 protected function mapStreamResponse (StreamResponse $ response ): ChatStreamResult
183173 {
184- throw new \Exception ('Not implemented ' );
185-
174+ throw new Exception ('Not implemented ' );
186175 // return new ChatStreamResult(function () use ($response): Generator {
187176 // $contentSoFar = '';
188177 // $toolCallsSoFar = [];
@@ -301,80 +290,124 @@ protected function mapUsage(CreateResponseUsage $usage): Usage
301290 protected function mapMessagesForInput (MessageCollection $ messages ): array
302291 {
303292 return $ messages
304- ->map (function (Message $ message ) {
305- // if ($message instanceof ToolMessage) {
306- // return [
307- // 'tool_call_id' => $message->id,
308- // 'role' => $message->role->value,
309- // 'content' => $message->content,
310- // ];
311- // }
293+ ->map (function (Message $ message ): array {
294+ // Handle ToolMessage specifically for Responses API
295+ if ($ message instanceof ToolMessage) {
296+ return [
297+ 'role ' => $ message ->role ->value ,
298+ 'content ' => $ this ->mapContentForResponsesAPI ($ message ->content ()),
299+ 'tool_call_id ' => $ message ->id ,
300+ ];
301+ }
312302
303+ // Handle AssistantMessage with tool calls
313304 if ($ message instanceof AssistantMessage && $ message ->toolCalls ?->isNotEmpty()) {
314- $ formattedMessage = $ message ->toArray ();
315-
316- // Ensure the function arguments are encoded as a string
317- foreach ($ message ->toolCalls as $ index => $ toolCall ) {
318- Arr::set (
319- $ formattedMessage ,
320- 'tool_calls. ' . $ index . '.function.arguments ' ,
321- json_encode ($ toolCall ->function ->arguments ),
322- );
323- }
305+ $ baseMessage = [
306+ 'role ' => $ message ->role ->value ,
307+ ];
324308
325- return $ formattedMessage ;
326- }
309+ // Add content if present
310+ if ($ message ->content () !== null ) {
311+ $ baseMessage ['content ' ] = $ this ->mapContentForResponsesAPI ($ message ->content ());
312+ }
327313
328- $ formattedMessage = $ message ->toArray ();
329-
330- if (isset ($ formattedMessage ['content ' ]) && is_array ($ formattedMessage ['content ' ])) {
331- $ formattedMessage ['content ' ] = array_map (function (mixed $ content ) {
332- if ($ content instanceof ImageContent) {
333- $ this ->supportsFeatureOrFail (ModelFeature::Vision);
334-
335- return [
336- 'type ' => 'image_url ' ,
337- 'image_url ' => [
338- 'url ' => $ content ->url ,
339- ],
340- ];
341- }
342-
343- if ($ content instanceof AudioContent) {
344- $ this ->supportsFeatureOrFail (ModelFeature::AudioInput);
345-
346- return [
347- 'type ' => 'input_audio ' ,
348- 'input_audio ' => [
349- 'data ' => $ content ->base64Data ,
350- 'format ' => $ content ->format ,
351- ],
352- ];
353- }
354-
355- return match (true ) {
356- $ content instanceof TextContent => [
357- 'type ' => 'text ' ,
358- 'text ' => $ content ->text ,
359- ],
360- $ content instanceof FileContent => [
361- 'type ' => 'file ' ,
362- 'file ' => [
363- 'filename ' => $ content ->fileName ,
364- 'file_data ' => $ content ->toDataUrl ()->toString (),
365- ],
314+ // Add tool calls
315+ $ baseMessage ['tool_calls ' ] = $ message ->toolCalls ->map (function (ToolCall $ toolCall ): array {
316+ return [
317+ 'id ' => $ toolCall ->id ,
318+ 'type ' => 'function ' ,
319+ 'function ' => [
320+ 'name ' => $ toolCall ->function ->name ,
321+ 'arguments ' => json_encode ($ toolCall ->function ->arguments ),
366322 ],
367- default => $ content ,
368- };
369- }, $ formattedMessage ['content ' ]);
323+ ];
324+ })->toArray ();
325+
326+ return $ baseMessage ;
370327 }
371328
329+ // Handle all other message types
330+ $ formattedMessage = [
331+ 'role ' => $ message ->role ()->value ,
332+ 'content ' => $ this ->mapContentForResponsesAPI ($ message ->content ()),
333+ ];
334+
372335 return $ formattedMessage ;
373336 })
374337 ->values ()
375338 ->toArray ();
376339 }
377340
341+ /**
342+ * Map content to the OpenAI Responses API format.
343+ *
344+ * @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
345+ *
346+ * @return array<int, array<string, mixed>>
347+ */
348+ protected function mapContentForResponsesAPI (string |array |null $ content ): array
349+ {
350+ if ($ content === null ) {
351+ return [];
352+ }
353+
354+ if (is_string ($ content )) {
355+ return [
356+ [
357+ 'type ' => 'input_text ' ,
358+ 'text ' => $ content ,
359+ ],
360+ ];
361+ }
362+
363+ return array_map (function (mixed $ item ): array {
364+ if ($ item instanceof ImageContent) {
365+ $ this ->supportsFeatureOrFail (ModelFeature::Vision);
366+
367+ return [
368+ 'type ' => 'input_image ' ,
369+ 'image_url ' => $ item ->url ,
370+ 'detail ' => 'auto ' , // Default detail level
371+ ];
372+ }
373+
374+ if ($ item instanceof AudioContent) {
375+ $ this ->supportsFeatureOrFail (ModelFeature::AudioInput);
376+
377+ return [
378+ 'type ' => 'input_audio ' ,
379+ 'data ' => $ item ->base64Data ,
380+ 'format ' => $ item ->format ,
381+ ];
382+ }
383+
384+ if ($ item instanceof FileContent) {
385+ return [
386+ 'type ' => 'input_file ' ,
387+ 'file_id ' => $ item ->fileName , // Assuming file_id should be the fileName
388+ ];
389+ }
390+
391+ if ($ item instanceof TextContent) {
392+ return [
393+ 'type ' => 'input_text ' ,
394+ 'text ' => $ item ->text ?? '' ,
395+ ];
396+ }
397+
398+ // Handle ReasoningContent and ToolContent
399+ if ($ item instanceof ReasoningContent) {
400+ return [
401+ 'type ' => 'input_text ' ,
402+ 'text ' => $ item ->reasoning ,
403+ ];
404+ }
405+
406+ // Fallback for unknown content types
407+ return [];
408+ }, $ content );
409+ }
410+
378411 protected static function mapFinishReason (?string $ finishReason ): ?FinishReason
379412 {
380413 if ($ finishReason === null ) {
0 commit comments