Skip to content

Commit 776c37f

Browse files
authored
feat(openai): streaming usage (#424)
1 parent fd1ac9a commit 776c37f

File tree

4 files changed

+49
-2
lines changed

4 files changed

+49
-2
lines changed

docs/core-concepts/streaming-output.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@ foreach ($response as $chunk) {
3232
// The text fragment in this chunk
3333
echo $chunk->text;
3434

35+
if ($chunk->usage) {
36+
echo "Prompt tokens: " . $chunk->usage->promptTokens;
37+
echo "Completion tokens: " . $chunk->usage->completionTokens;
38+
}
39+
3540
// Check if this is the final chunk
36-
if ($chunk->finishReason) {
41+
if ($chunk->finishReason === FinishReason::Stop) {
3742
echo "Generation complete: " . $chunk->finishReason->name;
3843
}
3944
}

src/Providers/OpenAI/Handlers/Stream.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
2828
use Prism\Prism\ValueObjects\Meta;
2929
use Prism\Prism\ValueObjects\ToolCall;
30+
use Prism\Prism\ValueObjects\Usage;
3031
use Psr\Http\Message\StreamInterface;
3132
use Throwable;
3233

@@ -98,6 +99,19 @@ protected function processStream(Response $response, Request $request, int $dept
9899
text: $content,
99100
finishReason: $finishReason !== FinishReason::Unknown ? $finishReason : null
100101
);
102+
103+
if (data_get($data, 'type') === 'response.completed') {
104+
yield new Chunk(
105+
text: '',
106+
usage: new Usage(
107+
promptTokens: data_get($data, 'response.usage.input_tokens'),
108+
completionTokens: data_get($data, 'response.usage.output_tokens'),
109+
cacheReadInputTokens: data_get($data, 'response.usage.input_tokens_details.cached_tokens'),
110+
thoughtTokens: data_get($data, 'response.usage.output_tokens_details.reasoning_tokens')
111+
),
112+
chunkType: ChunkType::Meta,
113+
);
114+
}
101115
}
102116

103117
if ($toolCalls !== []) {

src/Text/Chunk.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Prism\Prism\ValueObjects\Meta;
1010
use Prism\Prism\ValueObjects\ToolCall;
1111
use Prism\Prism\ValueObjects\ToolResult;
12+
use Prism\Prism\ValueObjects\Usage;
1213

1314
readonly class Chunk
1415
{
@@ -23,6 +24,7 @@ public function __construct(
2324
public array $toolResults = [],
2425
public ?FinishReason $finishReason = null,
2526
public ?Meta $meta = null,
27+
public ?Usage $usage = null,
2628
public array $additionalContent = [],
2729
public ChunkType $chunkType = ChunkType::Text
2830
) {}

tests/Providers/OpenAI/StreamTest.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Prism\Prism\Exceptions\PrismRateLimitedException;
1111
use Prism\Prism\Facades\Tool;
1212
use Prism\Prism\Prism;
13+
use Prism\Prism\ValueObjects\Usage;
1314
use Tests\Fixtures\FixtureResponse;
1415

1516
beforeEach(function (): void {
@@ -31,7 +32,7 @@
3132
$model = null;
3233

3334
foreach ($response as $chunk) {
34-
if ($chunk->chunkType === ChunkType::Meta) {
35+
if ($chunk->meta) {
3536
$responseId = $chunk->meta?->id;
3637
$model = $chunk->meta?->model;
3738
}
@@ -217,21 +218,46 @@
217218

218219
$fullResponse = '';
219220
$toolCallCount = 0;
221+
/** @var Usage[] $usage */
222+
$usage = [];
220223

221224
foreach ($response as $chunk) {
222225
if ($chunk->toolCalls !== []) {
223226
$toolCallCount += count($chunk->toolCalls);
224227
}
225228
$fullResponse .= $chunk->text;
229+
230+
if ($chunk->usage) {
231+
$usage[] = $chunk->usage;
232+
}
226233
}
227234

228235
expect($toolCallCount)->toBe(2);
229236
expect($fullResponse)->not->toBeEmpty();
230237

238+
// Verify reasoning usage
239+
expect($usage[0]->thoughtTokens)->toBeGreaterThan(0);
240+
231241
// Verify we made multiple requests for a conversation with tool calls
232242
Http::assertSentCount(3);
233243
});
234244

245+
it('emits usage information', function (): void {
246+
FixtureResponse::fakeResponseSequence('v1/responses', 'openai/stream-basic-text-responses');
247+
248+
$response = Prism::text()
249+
->using('openai', 'gpt-4')
250+
->withPrompt('Who are you?')
251+
->asStream();
252+
253+
foreach ($response as $chunk) {
254+
if ($chunk->usage) {
255+
expect($chunk->usage->promptTokens)->toBeGreaterThan(0);
256+
expect($chunk->usage->completionTokens)->toBeGreaterThan(0);
257+
}
258+
}
259+
});
260+
235261
it('throws a PrismRateLimitedException with a 429 response code', function (): void {
236262
Http::fake([
237263
'*' => Http::response(

0 commit comments

Comments
 (0)