Skip to content

Commit 1e5c764

Browse files
authored
Merge branch 'main' into feat/prism-events
2 parents 4ee8d17 + fd1ac9a commit 1e5c764

27 files changed

+1514
-703
lines changed

src/Providers/Gemini/Handlers/Stream.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ protected function sendRequest(Request $request): Response
245245
'maxOutputTokens' => $request->maxTokens(),
246246
'thinkingConfig' => Arr::whereNotNull([
247247
'thinkingBudget' => $providerOptions['thinkingBudget'] ?? null,
248-
]),
248+
]) ?: null,
249249
]),
250250
'tools' => $tools !== [] ? $tools : null,
251251
'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null,

src/Providers/OpenAI/Handlers/Stream.php

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
use Prism\Prism\Exceptions\PrismException;
1818
use Prism\Prism\Exceptions\PrismRateLimitedException;
1919
use Prism\Prism\Providers\OpenAI\Concerns\ProcessesRateLimits;
20-
use Prism\Prism\Providers\OpenAI\Maps\ChatMessageMap;
2120
use Prism\Prism\Providers\OpenAI\Maps\FinishReasonMap;
21+
use Prism\Prism\Providers\OpenAI\Maps\MessageMap;
2222
use Prism\Prism\Providers\OpenAI\Maps\ToolChoiceMap;
2323
use Prism\Prism\Providers\OpenAI\Maps\ToolMap;
2424
use Prism\Prism\Text\Chunk;
2525
use Prism\Prism\Text\Request;
2626
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
2727
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
28+
use Prism\Prism\ValueObjects\Meta;
2829
use Prism\Prism\ValueObjects\ToolCall;
2930
use Psr\Http\Message\StreamInterface;
3031
use 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

Comments
 (0)