@@ -27,62 +27,68 @@ trait MapsMessages
2727 protected function mapMessagesForInput (MessageCollection $ messages ): array
2828 {
2929 return $ messages
30- ->map (function (Message $ message ): array {
31- // Handle ToolMessage specifically for Responses API
32- if ($ message instanceof ToolMessage) {
33- return [
34- 'role ' => $ message ->role ->value ,
35- 'content ' => $ message ->content (),
36- 'tool_call_id ' => $ message ->id ,
37- ];
38- }
39-
40- // Handle AssistantMessage with tool calls
41- if ($ message instanceof AssistantMessage && $ message ->toolCalls ?->isNotEmpty()) {
42- $ baseMessage = [
43- 'role ' => $ message ->role ->value ,
44- ];
45-
46- // Add content if present
47- if ($ message ->content () !== null ) {
48- $ baseMessage ['content ' ] = $ message ->content ();
49- }
50-
51- // Add tool calls
52- $ baseMessage ['tool_calls ' ] = $ message ->toolCalls ->map (function (ToolCall $ toolCall ): array {
53- return [
54- 'id ' => $ toolCall ->id ,
55- 'type ' => 'function ' ,
56- 'function ' => [
57- 'name ' => $ toolCall ->function ->name ,
58- 'arguments ' => json_encode ($ toolCall ->function ->arguments ),
59- ],
60- ];
61- })->toArray ();
62-
63- return $ baseMessage ;
64- }
65-
66- // Handle all other message types
67- $ formattedMessage = [
30+ ->map (fn (Message $ message ): array => match (true ) {
31+ $ message instanceof ToolMessage => $ this ->mapToolMessage ($ message ),
32+ $ message instanceof AssistantMessage && $ message ->toolCalls ?->isNotEmpty() => $ this ->mapAssistantMessageWithToolCalls ($ message ),
33+ default => [
6834 'role ' => $ message ->role ()->value ,
6935 'content ' => $ this ->mapContentForInput ($ message ->content ()),
70- ];
71-
72- return $ formattedMessage ;
36+ ],
7337 })
7438 ->values ()
7539 ->toArray ();
7640 }
7741
7842 /**
79- * Map content to the OpenAI Responses API format.
43+ * @return array<string, mixed>
44+ */
45+ private function mapToolMessage (ToolMessage $ message ): array
46+ {
47+ return [
48+ 'role ' => $ message ->role ()->value ,
49+ 'content ' => $ message ->content (),
50+ 'tool_call_id ' => $ message ->id ,
51+ ];
52+ }
53+
54+ /**
55+ * @return array<string, mixed>
56+ */
57+ private function mapAssistantMessageWithToolCalls (AssistantMessage $ message ): array
58+ {
59+ $ mapped = [
60+ 'role ' => $ message ->role ()->value ,
61+ ];
62+
63+ $ content = $ this ->mapAssistantContentForInput ($ message ->content ());
64+
65+ if ($ content !== []) {
66+ $ mapped ['content ' ] = $ content ;
67+ }
68+
69+ $ mapped ['tool_calls ' ] = $ message ->toolCalls ->map (fn (ToolCall $ toolCall ): array => [
70+ 'id ' => $ toolCall ->id ,
71+ 'type ' => 'function ' ,
72+ 'function ' => [
73+ 'name ' => $ toolCall ->function ->name ,
74+ 'arguments ' => json_encode ($ toolCall ->function ->arguments ),
75+ ],
76+ ])->toArray ();
77+
78+ return $ mapped ;
79+ }
80+
81+ /**
82+ * Map assistant message content for the Responses API input.
83+ *
84+ * ReasoningContent is excluded because the Responses API does not
85+ * accept it back in the `input` field of subsequent requests.
8086 *
8187 * @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
8288 *
8389 * @return array<int, array<string, mixed>>
8490 */
85- protected function mapContentForInput (string |array |null $ content ): array
91+ protected function mapAssistantContentForInput (string |array |null $ content ): array
8692 {
8793 if ($ content === null ) {
8894 return [];
@@ -91,57 +97,99 @@ protected function mapContentForInput(string|array|null $content): array
9197 if (is_string ($ content )) {
9298 return [
9399 [
94- 'type ' => 'input_text ' ,
100+ 'type ' => 'output_text ' ,
95101 'text ' => $ content ,
96102 ],
97103 ];
98104 }
99105
100- return array_map (function (mixed $ item ): array {
101- if ($ item instanceof ImageContent) {
102- $ this ->supportsFeatureOrFail (ModelFeature::Vision);
106+ $ mapped = [];
103107
104- return [
105- 'type ' => 'input_image ' ,
106- 'image_url ' => $ item ->url ,
107- 'detail ' => 'auto ' , // Default detail level
108- ];
108+ foreach ($ content as $ item ) {
109+ // Skip reasoning content — the Responses API doesn't accept it in input
110+ if ($ item instanceof ReasoningContent) {
111+ continue ;
109112 }
110113
111- if ($ item instanceof AudioContent) {
112- $ this ->supportsFeatureOrFail (ModelFeature::AudioInput);
113-
114- return [
115- 'type ' => 'input_audio ' ,
116- 'data ' => $ item ->base64Data ,
117- 'format ' => $ item ->format ,
114+ if ($ item instanceof TextContent) {
115+ $ mapped [] = [
116+ 'type ' => 'output_text ' ,
117+ 'text ' => $ item ->text ?? '' ,
118118 ];
119119 }
120+ }
120121
121- if ($ item instanceof FileContent) {
122- return [
123- 'type ' => 'input_file ' ,
124- 'file_id ' => $ item ->fileName , // Assuming file_id should be the fileName
125- ];
126- }
122+ return $ mapped ;
123+ }
127124
128- if ($ item instanceof TextContent) {
129- return [
125+ /**
126+ * Map content to the OpenAI Responses API format.
127+ *
128+ * @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
129+ *
130+ * @return array<int, array<string, mixed>>
131+ */
132+ protected function mapContentForInput (string |array |null $ content ): array
133+ {
134+ if ($ content === null ) {
135+ return [];
136+ }
137+
138+ if (is_string ($ content )) {
139+ return [
140+ [
130141 'type ' => 'input_text ' ,
131- 'text ' => $ item ->text ?? '' ,
132- ];
133- }
142+ 'text ' => $ content ,
143+ ],
144+ ];
145+ }
134146
135- // Handle ReasoningContent and ToolContent
136- if ($ item instanceof ReasoningContent) {
137- return [
147+ return array_map (function (mixed $ item ): array {
148+ return match (true ) {
149+ $ item instanceof ImageContent => $ this ->mapImageContent ($ item ),
150+ $ item instanceof AudioContent => $ this ->mapAudioContent ($ item ),
151+ $ item instanceof FileContent => [
152+ 'type ' => 'input_file ' ,
153+ 'file_id ' => $ item ->fileName ,
154+ ],
155+ $ item instanceof TextContent => [
156+ 'type ' => 'input_text ' ,
157+ 'text ' => $ item ->text ?? '' ,
158+ ],
159+ $ item instanceof ReasoningContent => [
138160 'type ' => 'input_text ' ,
139161 'text ' => $ item ->reasoning ,
140- ];
141- }
142-
143- // Fallback for unknown content types
144- return [];
162+ ],
163+ default => [],
164+ };
145165 }, $ content );
146166 }
167+
168+ /**
169+ * @return array<string, mixed>
170+ */
171+ private function mapImageContent (ImageContent $ item ): array
172+ {
173+ $ this ->supportsFeatureOrFail (ModelFeature::Vision);
174+
175+ return [
176+ 'type ' => 'input_image ' ,
177+ 'image_url ' => $ item ->url ,
178+ 'detail ' => 'auto ' ,
179+ ];
180+ }
181+
182+ /**
183+ * @return array<string, mixed>
184+ */
185+ private function mapAudioContent (AudioContent $ item ): array
186+ {
187+ $ this ->supportsFeatureOrFail (ModelFeature::AudioInput);
188+
189+ return [
190+ 'type ' => 'input_audio ' ,
191+ 'data ' => $ item ->base64Data ,
192+ 'format ' => $ item ->format ,
193+ ];
194+ }
147195}
0 commit comments