Skip to content

Commit 7057263

Browse files
authored
fix(openai): streaming output response map (#409)
1 parent 2936d2e commit 7057263

File tree

4 files changed

+200
-116
lines changed

4 files changed

+200
-116
lines changed

src/Providers/OpenAI/Handlers/Stream.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
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;
2021
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;
@@ -218,7 +218,7 @@ protected function sendRequest(Request $request): Response
218218
array_merge([
219219
'stream' => true,
220220
'model' => $request->model(),
221-
'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(),
221+
'messages' => (new ChatMessageMap($request->messages(), $request->systemPrompts()))(),
222222
'max_completion_tokens' => $request->maxTokens(),
223223
], Arr::whereNotNull([
224224
'temperature' => $request->temperature(),
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prism\Prism\Providers\OpenAI\Maps;
6+
7+
use Exception;
8+
use Prism\Prism\Contracts\Message;
9+
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
10+
use Prism\Prism\ValueObjects\Messages\Support\Document;
11+
use Prism\Prism\ValueObjects\Messages\Support\Image;
12+
use Prism\Prism\ValueObjects\Messages\Support\OpenAIFile;
13+
use Prism\Prism\ValueObjects\Messages\SystemMessage;
14+
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
15+
use Prism\Prism\ValueObjects\Messages\UserMessage;
16+
use Prism\Prism\ValueObjects\ToolCall;
17+
18+
class ChatMessageMap
19+
{
20+
/** @var array<int, mixed> */
21+
protected array $mappedMessages = [];
22+
23+
/**
24+
* @param array<int, Message> $messages
25+
* @param SystemMessage[] $systemPrompts
26+
*/
27+
public function __construct(
28+
protected array $messages,
29+
protected array $systemPrompts
30+
) {
31+
$this->messages = array_merge(
32+
$this->systemPrompts,
33+
$this->messages
34+
);
35+
}
36+
37+
/**
38+
* @return array<int, mixed>
39+
*/
40+
public function __invoke(): array
41+
{
42+
array_map(
43+
fn (Message $message) => $this->mapMessage($message),
44+
$this->messages
45+
);
46+
47+
return $this->mappedMessages;
48+
}
49+
50+
protected function mapMessage(Message $message): void
51+
{
52+
match ($message::class) {
53+
UserMessage::class => $this->mapUserMessage($message),
54+
AssistantMessage::class => $this->mapAssistantMessage($message),
55+
ToolResultMessage::class => $this->mapToolResultMessage($message),
56+
SystemMessage::class => $this->mapSystemMessage($message),
57+
default => throw new Exception('Could not map message type '.$message::class),
58+
};
59+
}
60+
61+
protected function mapSystemMessage(SystemMessage $message): void
62+
{
63+
$this->mappedMessages[] = [
64+
'role' => 'system',
65+
'content' => $message->content,
66+
];
67+
}
68+
69+
protected function mapToolResultMessage(ToolResultMessage $message): void
70+
{
71+
foreach ($message->toolResults as $toolResult) {
72+
$this->mappedMessages[] = [
73+
'type' => 'function_call_output',
74+
'call_id' => $toolResult->toolCallResultId,
75+
'output' => $toolResult->result,
76+
];
77+
}
78+
}
79+
80+
protected function mapUserMessage(UserMessage $message): void
81+
{
82+
$this->mappedMessages[] = [
83+
'role' => 'user',
84+
'content' => [
85+
['type' => 'text', 'text' => $message->text()],
86+
...self::mapImageParts($message->images()),
87+
...self::mapDocumentParts($message->documents()),
88+
...self::mapFileParts($message->files()),
89+
],
90+
];
91+
}
92+
93+
/**
94+
* @param Image[] $images
95+
* @return array<int, mixed>
96+
*/
97+
protected static function mapImageParts(array $images): array
98+
{
99+
return array_map(fn (Image $image): array => (new ImageMapper($image))->toPayload(), $images);
100+
}
101+
102+
/**
103+
* @param Document[] $documents
104+
* @return array<int,mixed>
105+
*/
106+
protected static function mapDocumentParts(array $documents): array
107+
{
108+
return array_map(fn (Document $document): array => (new DocumentMapper($document))->toPayload(), $documents);
109+
}
110+
111+
/**
112+
* @param OpenAIFile[] $files
113+
* @return array<int, mixed>
114+
*/
115+
protected static function mapFileParts(array $files): array
116+
{
117+
return array_map(fn (OpenAIFile $file): array => [
118+
'type' => 'input_file',
119+
'file_id' => $file->fileId,
120+
], $files);
121+
}
122+
123+
protected function mapAssistantMessage(AssistantMessage $message): void
124+
{
125+
if ($message->content !== '' && $message->content !== '0') {
126+
$this->mappedMessages[] = [
127+
'role' => 'assistant',
128+
'content' => $message->content,
129+
];
130+
}
131+
132+
if ($message->toolCalls !== []) {
133+
array_push(
134+
$this->mappedMessages,
135+
...array_filter(
136+
array_map(fn (ToolCall $toolCall): ?array => is_null($toolCall->reasoningId) ? null : [
137+
'type' => 'reasoning',
138+
'id' => $toolCall->reasoningId,
139+
'summary' => $toolCall->reasoningSummary,
140+
], $message->toolCalls)
141+
),
142+
...array_map(fn (ToolCall $toolCall): array => [
143+
'id' => $toolCall->id,
144+
'call_id' => $toolCall->resultId,
145+
'type' => 'function_call',
146+
'name' => $toolCall->name,
147+
'arguments' => json_encode($toolCall->arguments()),
148+
], $message->toolCalls)
149+
);
150+
}
151+
}
152+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]}
2+
3+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}]}
4+
5+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}]}
6+
7+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" an"},"logprobs":null,"finish_reason":null}]}
8+
9+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" artificial"},"logprobs":null,"finish_reason":null}]}
10+
11+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" intelligence"},"logprobs":null,"finish_reason":null}]}
12+
13+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" designed"},"logprobs":null,"finish_reason":null}]}
14+
15+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}]}
16+
17+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}]}
18+
19+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" with"},"logprobs":null,"finish_reason":null}]}
20+
21+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" tasks"},"logprobs":null,"finish_reason":null}]}
22+
23+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}]}
24+
25+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" answer"},"logprobs":null,"finish_reason":null}]}
26+
27+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" questions"},"logprobs":null,"finish_reason":null}]}
28+
29+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}]}
30+
31+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}]}
32+
33+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" best"},"logprobs":null,"finish_reason":null}]}
34+
35+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}]}
36+
37+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" my"},"logprobs":null,"finish_reason":null}]}
38+
39+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" ability"},"logprobs":null,"finish_reason":null}]}
40+
41+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]}
42+
43+
data: {"id":"chatcmpl-Bhk6EgmSSUhFYOW0WQZAmCHBxc2qu","object":"chat.completion.chunk","created":1749765010,"model":"gpt-4-0613","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}
44+
45+
data: [DONE]
46+

0 commit comments

Comments
 (0)