Skip to content

Commit 7ebf450

Browse files
authored
fix(streaming): artifact data protocol key (#881)
1 parent ec81339 commit 7ebf450

3 files changed

Lines changed: 74 additions & 11 deletions

File tree

docs/core-concepts/streaming-output.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ eventSource.addEventListener('text_delta', (event) => {
417417

418418
### Artifact Events with Vercel AI SDK
419419

420-
When using `asDataStreamResponse()`, artifacts are sent as data protocol messages with type `artifact`:
420+
When using `asDataStreamResponse()`, artifacts are sent as custom data parts with type `data-artifact`:
421421

422422
```javascript
423423
import { useChat } from '@ai-sdk/react';
@@ -433,8 +433,8 @@ export default function Chat() {
433433
},
434434
onData: (data) => {
435435
// Handle artifact data messages
436-
if (data.type === 'artifact') {
437-
setArtifacts(prev => [...prev, data.artifact]);
436+
if (data.type === 'data-artifact') {
437+
setArtifacts(prev => [...prev, data.data.artifact]);
438438
}
439439
},
440440
});

src/Streaming/Adapters/DataProtocolAdapter.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,16 @@ protected function handleToolResult(ToolResultEvent $event): ?array
276276
protected function handleArtifact(ArtifactEvent $event): array
277277
{
278278
return [
279-
'type' => 'artifact',
280-
'toolCallId' => $event->toolCallId,
281-
'toolName' => $event->toolName,
282-
'artifact' => [
283-
'id' => $event->artifact->id,
284-
'mimeType' => $event->artifact->mimeType,
285-
'data' => $event->artifact->data,
286-
'metadata' => $event->artifact->metadata,
279+
'type' => 'data-artifact',
280+
'data' => [
281+
'toolCallId' => $event->toolCallId,
282+
'toolName' => $event->toolName,
283+
'artifact' => [
284+
'id' => $event->artifact->id,
285+
'mimeType' => $event->artifact->mimeType,
286+
'data' => $event->artifact->data,
287+
'metadata' => $event->artifact->metadata,
288+
],
287289
],
288290
];
289291
}

tests/Streaming/Adapters/DataProtocolAdapterTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Prism\Prism\Enums\FinishReason;
77
use Prism\Prism\Exceptions\PrismException;
88
use Prism\Prism\Streaming\Adapters\DataProtocolAdapter;
9+
use Prism\Prism\Streaming\Events\ArtifactEvent;
910
use Prism\Prism\Streaming\Events\ErrorEvent;
1011
use Prism\Prism\Streaming\Events\StreamEndEvent;
1112
use Prism\Prism\Streaming\Events\StreamEvent;
@@ -19,6 +20,7 @@
1920
use Prism\Prism\Streaming\Events\ToolCallEvent;
2021
use Prism\Prism\Streaming\Events\ToolResultEvent;
2122
use Prism\Prism\Text\PendingRequest;
23+
use Prism\Prism\ValueObjects\Artifact;
2224
use Prism\Prism\ValueObjects\ToolCall;
2325
use Prism\Prism\ValueObjects\ToolResult;
2426
use Prism\Prism\ValueObjects\Usage;
@@ -527,3 +529,62 @@ function createThrowingGenerator(array $eventsBeforeError, Throwable $exception)
527529
fclose($outputBuffer);
528530
}
529531
});
532+
533+
it('formats artifact events using data- prefix convention', function (): void {
534+
$artifact = new Artifact(
535+
data: 'iVBORw0KGgo=',
536+
mimeType: 'image/png',
537+
metadata: ['width' => 512],
538+
id: 'artifact-123',
539+
);
540+
541+
$events = [
542+
new ArtifactEvent('evt-1', 1640995200, $artifact, 'tool-call-456', 'generate_image', 'msg-789'),
543+
];
544+
545+
$adapter = new DataProtocolAdapter;
546+
$response = ($adapter)(createDataEventGenerator($events));
547+
$callback = $response->getCallback();
548+
549+
$outputBuffer = fopen('php://memory', 'r+');
550+
ob_start(function ($buffer) use ($outputBuffer): string {
551+
fwrite($outputBuffer, $buffer);
552+
553+
return '';
554+
});
555+
556+
try {
557+
$callback();
558+
ob_end_flush();
559+
560+
rewind($outputBuffer);
561+
$capturedOutput = stream_get_contents($outputBuffer);
562+
563+
// Verify data- prefix convention is used (not bare "artifact")
564+
expect($capturedOutput)->toContain('data: {"type":"data-artifact"');
565+
expect($capturedOutput)->not->toContain('"type":"artifact"');
566+
567+
// Verify payload is nested under "data" key
568+
expect($capturedOutput)->toContain('"data":{');
569+
expect($capturedOutput)->toContain('"toolCallId":"tool-call-456"');
570+
expect($capturedOutput)->toContain('"toolName":"generate_image"');
571+
expect($capturedOutput)->toContain('"mimeType":"image\/png"');
572+
expect($capturedOutput)->toContain('"id":"artifact-123"');
573+
574+
// Parse the JSON to verify structure
575+
$lines = explode("\n", trim($capturedOutput));
576+
$dataLines = array_filter($lines, fn (string $line): bool => str_starts_with($line, 'data: ') && $line !== 'data: [DONE]');
577+
$artifactLine = array_values($dataLines)[0];
578+
$json = json_decode(substr($artifactLine, 6), true);
579+
580+
expect($json['type'])->toBe('data-artifact');
581+
expect($json['data']['toolCallId'])->toBe('tool-call-456');
582+
expect($json['data']['toolName'])->toBe('generate_image');
583+
expect($json['data']['artifact']['id'])->toBe('artifact-123');
584+
expect($json['data']['artifact']['mimeType'])->toBe('image/png');
585+
expect($json['data']['artifact']['data'])->toBe('iVBORw0KGgo=');
586+
expect($json['data']['artifact']['metadata'])->toBe(['width' => 512]);
587+
} finally {
588+
fclose($outputBuffer);
589+
}
590+
});

0 commit comments

Comments
 (0)