Skip to content

Commit c33e918

Browse files
committed
add anthropic driver
1 parent 67a78c5 commit c33e918

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

classes/Ai/Drivers/Anthropic.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace tobimori\Seo\Ai\Drivers;
4+
5+
use Generator;
6+
use tobimori\Seo\Ai\Chunk;
7+
use tobimori\Seo\Ai\Driver;
8+
use tobimori\Seo\Ai\SseStream;
9+
10+
class Anthropic extends Driver
11+
{
12+
protected const string DEFAULT_ENDPOINT = 'https://api.anthropic.com/v1/messages';
13+
protected const string DEFAULT_MODEL = 'claude-4-5-haiku';
14+
15+
/**
16+
* @inheritDoc
17+
*/
18+
public function stream(
19+
string $prompt,
20+
string|null $model = null,
21+
): Generator {
22+
$apiKey = $this->config('apiKey', required: true);
23+
$headers = [
24+
'Content-Type: application/json',
25+
'Accept: text/event-stream',
26+
"x-api-key: {$apiKey}",
27+
'anthropic-version: 2023-06-01',
28+
];
29+
30+
$payload = [
31+
'model' => $model ?? $this->config('model', static::DEFAULT_MODEL),
32+
'messages' => [
33+
['role' => 'user', 'content' => $prompt]
34+
],
35+
'max_tokens' => 4096,
36+
'stream' => true,
37+
];
38+
39+
$stream = new SseStream($this->config('endpoint', static::DEFAULT_ENDPOINT), $headers, $payload, (int)$this->config('timeout', 120));
40+
yield from $stream->stream(function (array $event): Generator {
41+
$type = $event['type'] ?? null;
42+
43+
// handle message start event
44+
if ($type === 'message_start') {
45+
yield Chunk::streamStart($event);
46+
return;
47+
}
48+
49+
// handle content block start (beginning of text output)
50+
if ($type === 'content_block_start') {
51+
$contentBlock = $event['content_block'] ?? [];
52+
if (($contentBlock['type'] ?? null) === 'text') {
53+
yield Chunk::textStart($event);
54+
}
55+
return;
56+
}
57+
58+
// handle content block delta (text chunks)
59+
if ($type === 'content_block_delta') {
60+
$delta = $event['delta'] ?? [];
61+
if (($delta['type'] ?? null) === 'text_delta') {
62+
$text = $delta['text'] ?? '';
63+
if ($text !== '') {
64+
yield Chunk::textDelta($text, $event);
65+
}
66+
}
67+
return;
68+
}
69+
70+
// handle content block stop (end of text block)
71+
if ($type === 'content_block_stop') {
72+
yield Chunk::textComplete($event);
73+
return;
74+
}
75+
76+
// handle message stop (end of stream)
77+
if ($type === 'message_stop') {
78+
yield Chunk::streamEnd($event);
79+
return;
80+
}
81+
82+
// handle ping events (keep-alive)
83+
if ($type === 'ping') {
84+
// ignore ping events
85+
return;
86+
}
87+
88+
// handle error events
89+
if ($type === 'error') {
90+
$error = $event['error'] ?? [];
91+
$message = $error['message'] ?? 'Unknown Anthropic streaming error.';
92+
yield Chunk::error($message, $event);
93+
return;
94+
}
95+
96+
// handle message delta (contains usage info)
97+
if ($type === 'message_delta') {
98+
// we could extract usage info here if needed
99+
return;
100+
}
101+
});
102+
}
103+
}

0 commit comments

Comments
 (0)