Skip to content

Commit 7ab7875

Browse files
authored
feat(anthropic): text stream support (#266)
1 parent 6e6ffb8 commit 7ab7875

25 files changed

+2231
-161
lines changed

docs/providers/anthropic.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ foreach ($messageChunks as $messageChunk) {
246246
}
247247
```
248248

249+
Note that when using streaming, Anthropic does not stream citations in the same way. Instead, of building the context as above, yield text to the browser in the usual way and pair text up with the relevant footnote using the `citationIndex` on the text chunk's additionalContent parameter.
249250

250251
## Considerations
251252
### Message Order

src/Enums/ChunkType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prism\Prism\Enums;
6+
7+
enum ChunkType
8+
{
9+
case Text;
10+
case Thinking;
11+
case Meta;
12+
}

src/Providers/Anthropic/Anthropic.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Prism\Prism\Contracts\Provider;
1111
use Prism\Prism\Embeddings\Request as EmbeddingRequest;
1212
use Prism\Prism\Embeddings\Response as EmbeddingResponse;
13-
use Prism\Prism\Exceptions\PrismException;
13+
use Prism\Prism\Providers\Anthropic\Handlers\Stream;
1414
use Prism\Prism\Providers\Anthropic\Handlers\Structured;
1515
use Prism\Prism\Providers\Anthropic\Handlers\Text;
1616
use Prism\Prism\Structured\Request as StructuredRequest;
@@ -57,7 +57,12 @@ public function structured(StructuredRequest $request): StructuredResponse
5757
#[\Override]
5858
public function stream(TextRequest $request): Generator
5959
{
60-
throw PrismException::unsupportedProviderAction(__METHOD__, class_basename($this));
60+
$handler = new Stream($this->client(
61+
$request->clientOptions(),
62+
$request->clientRetry()
63+
));
64+
65+
return $handler->handle($request);
6166
}
6267

6368
#[\Override]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prism\Prism\Providers\Anthropic\Concerns;
6+
7+
use Illuminate\Http\Client\Response;
8+
use Illuminate\Support\Arr;
9+
use Illuminate\Support\Carbon;
10+
use Illuminate\Support\Str;
11+
use Prism\Prism\Enums\Provider;
12+
use Prism\Prism\Exceptions\PrismProviderOverloadedException;
13+
use Prism\Prism\Exceptions\PrismRateLimitedException;
14+
use Prism\Prism\Exceptions\PrismRequestTooLargeException;
15+
use Prism\Prism\ValueObjects\ProviderRateLimit;
16+
17+
trait HandlesResponse
18+
{
19+
protected function handleResponseExceptions(Response $response): void
20+
{
21+
if ($response->getStatusCode() === 429) {
22+
throw PrismRateLimitedException::make(
23+
rateLimits: $this->processRateLimits($response),
24+
retryAfter: $response->hasHeader('retry-after')
25+
? (int) $response->getHeader('retry-after')[0]
26+
: null
27+
);
28+
}
29+
30+
if ($response->getStatusCode() === 529) {
31+
throw PrismProviderOverloadedException::make(Provider::Anthropic);
32+
}
33+
34+
if ($response->getStatusCode() === 413) {
35+
throw PrismRequestTooLargeException::make(Provider::Anthropic);
36+
}
37+
}
38+
39+
/**
40+
* @return array<int, ProviderRateLimit>
41+
*/
42+
protected function processRateLimits(Response $response): array
43+
{
44+
$rate_limits = [];
45+
46+
foreach ($response->getHeaders() as $headerName => $headerValues) {
47+
if (Str::startsWith($headerName, 'anthropic-ratelimit-') === false) {
48+
continue;
49+
}
50+
51+
$limit_name = Str::of($headerName)->after('anthropic-ratelimit-')->beforeLast('-')->toString();
52+
$field_name = Str::of($headerName)->afterLast('-')->toString();
53+
$rate_limits[$limit_name][$field_name] = $headerValues[0];
54+
}
55+
56+
return array_values(Arr::map($rate_limits, function ($fields, $limit_name): ProviderRateLimit {
57+
$resets_at = data_get($fields, 'reset');
58+
59+
return new ProviderRateLimit(
60+
name: $limit_name,
61+
limit: data_get($fields, 'limit') !== null
62+
? (int) data_get($fields, 'limit')
63+
: null,
64+
remaining: data_get($fields, 'remaining') !== null
65+
? (int) data_get($fields, 'remaining')
66+
: null,
67+
resetsAt: data_get($fields, 'reset') !== null ? new Carbon($resets_at) : null
68+
);
69+
}));
70+
}
71+
}

src/Providers/Anthropic/Handlers/AnthropicHandlerAbstract.php

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@
77
use Illuminate\Http\Client\PendingRequest;
88
use Illuminate\Http\Client\Response;
99
use Illuminate\Support\Arr;
10-
use Illuminate\Support\Carbon;
11-
use Illuminate\Support\Str;
1210
use Prism\Prism\Contracts\PrismRequest;
1311
use Prism\Prism\Enums\Provider;
1412
use Prism\Prism\Exceptions\PrismException;
15-
use Prism\Prism\Exceptions\PrismProviderOverloadedException;
16-
use Prism\Prism\Exceptions\PrismRateLimitedException;
17-
use Prism\Prism\Exceptions\PrismRequestTooLargeException;
13+
use Prism\Prism\Providers\Anthropic\Concerns\HandlesResponse;
1814
use Prism\Prism\Providers\Anthropic\ValueObjects\MessagePartWithCitations;
19-
use Prism\Prism\ValueObjects\ProviderRateLimit;
2015
use Throwable;
2116

2217
abstract class AnthropicHandlerAbstract
2318
{
19+
use HandlesResponse;
20+
2421
protected Response $httpResponse;
2522

2623
public function __construct(protected PendingRequest $client, protected PrismRequest $request) {}
@@ -73,22 +70,7 @@ protected function extractCitations(array $data): ?array
7370

7471
protected function handleResponseErrors(): void
7572
{
76-
if ($this->httpResponse->getStatusCode() === 429) {
77-
throw PrismRateLimitedException::make(
78-
rateLimits: array_values($this->processRateLimits()),
79-
retryAfter: $this->httpResponse->hasHeader('retry-after')
80-
? (int) $this->httpResponse->getHeader('retry-after')[0]
81-
: null
82-
);
83-
}
84-
85-
if ($this->httpResponse->getStatusCode() === 529) {
86-
throw PrismProviderOverloadedException::make(Provider::Anthropic);
87-
}
88-
89-
if ($this->httpResponse->getStatusCode() === 413) {
90-
throw PrismRequestTooLargeException::make(Provider::Anthropic);
91-
}
73+
$this->handleResponseExceptions($this->httpResponse);
9274

9375
$data = $this->httpResponse->json();
9476

@@ -123,39 +105,4 @@ protected function extractThinking(array $data): array
123105
'thinking_signature' => data_get($thinking, 'signature'),
124106
];
125107
}
126-
127-
/**
128-
* @return ProviderRateLimit[]
129-
*/
130-
protected function processRateLimits(): array
131-
{
132-
$rate_limits = [];
133-
134-
foreach ($this->httpResponse->getHeaders() as $headerName => $headerValues) {
135-
if (Str::startsWith($headerName, 'anthropic-ratelimit-') === false) {
136-
continue;
137-
}
138-
139-
$limit_name = Str::of($headerName)->after('anthropic-ratelimit-')->beforeLast('-')->toString();
140-
141-
$field_name = Str::of($headerName)->afterLast('-')->toString();
142-
143-
$rate_limits[$limit_name][$field_name] = $headerValues[0];
144-
}
145-
146-
return array_values(Arr::map($rate_limits, function ($fields, $limit_name): ProviderRateLimit {
147-
$resets_at = data_get($fields, 'reset');
148-
149-
return new ProviderRateLimit(
150-
name: $limit_name,
151-
limit: data_get($fields, 'limit') !== null
152-
? (int) data_get($fields, 'limit')
153-
: null,
154-
remaining: data_get($fields, 'remaining') !== null
155-
? (int) data_get($fields, 'remaining')
156-
: null,
157-
resetsAt: data_get($fields, 'reset') !== null ? new Carbon($resets_at) : null
158-
);
159-
}));
160-
}
161108
}

0 commit comments

Comments
 (0)