mirror of
https://github.com/symfony/ai-anthropic-platform.git
synced 2026-03-23 23:32:07 +01:00
187 lines
6.5 KiB
PHP
187 lines
6.5 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\AI\Platform\Bridge\Anthropic;
|
|
|
|
use Symfony\AI\Platform\Exception\RateLimitExceededException;
|
|
use Symfony\AI\Platform\Exception\RuntimeException;
|
|
use Symfony\AI\Platform\Model;
|
|
use Symfony\AI\Platform\Result\RawHttpResult;
|
|
use Symfony\AI\Platform\Result\RawResultInterface;
|
|
use Symfony\AI\Platform\Result\ResultInterface;
|
|
use Symfony\AI\Platform\Result\StreamResult;
|
|
use Symfony\AI\Platform\Result\TextResult;
|
|
use Symfony\AI\Platform\Result\ThinkingContent;
|
|
use Symfony\AI\Platform\Result\ToolCall;
|
|
use Symfony\AI\Platform\Result\ToolCallResult;
|
|
use Symfony\AI\Platform\ResultConverterInterface;
|
|
|
|
/**
|
|
* @author Christopher Hertel <mail@christopher-hertel.de>
|
|
*/
|
|
class ResultConverter implements ResultConverterInterface
|
|
{
|
|
public function supports(Model $model): bool
|
|
{
|
|
return $model instanceof Claude;
|
|
}
|
|
|
|
public function convert(RawHttpResult|RawResultInterface $result, array $options = []): ResultInterface
|
|
{
|
|
$response = $result->getObject();
|
|
|
|
if (429 === $response->getStatusCode()) {
|
|
$retryAfter = $response->getHeaders(false)['retry-after'][0] ?? null;
|
|
$retryAfterValue = $retryAfter ? (int) $retryAfter : null;
|
|
throw new RateLimitExceededException($retryAfterValue);
|
|
}
|
|
|
|
if ($options['stream'] ?? false) {
|
|
return new StreamResult($this->convertStream($result));
|
|
}
|
|
|
|
$data = $result->getData();
|
|
|
|
if (isset($data['type']) && 'error' === $data['type']) {
|
|
$type = $data['error']['type'] ?? 'Unknown';
|
|
$message = $data['error']['message'] ?? 'An unknown error occurred.';
|
|
throw new RuntimeException(\sprintf('API Error [%s]: "%s"', $type, $message));
|
|
}
|
|
|
|
if (!isset($data['content']) || [] === $data['content']) {
|
|
throw new RuntimeException('Response does not contain any content.');
|
|
}
|
|
|
|
$toolCalls = [];
|
|
$text = null;
|
|
foreach ($data['content'] as $content) {
|
|
if ('tool_use' === $content['type']) {
|
|
$toolCalls[] = new ToolCall($content['id'], $content['name'], $content['input']);
|
|
} elseif ('text' === $content['type']) {
|
|
$text = $content['text'];
|
|
}
|
|
}
|
|
|
|
if (null === $text && [] === $toolCalls) {
|
|
throw new RuntimeException('Response content does not contain any text nor tool calls.');
|
|
}
|
|
|
|
if ([] !== $toolCalls) {
|
|
return new ToolCallResult(...$toolCalls);
|
|
}
|
|
|
|
return new TextResult($text);
|
|
}
|
|
|
|
public function getTokenUsageExtractor(): TokenUsageExtractor
|
|
{
|
|
return new TokenUsageExtractor();
|
|
}
|
|
|
|
private function convertStream(RawResultInterface $result): \Generator
|
|
{
|
|
$toolCalls = [];
|
|
$currentToolCall = null;
|
|
$currentToolCallJson = '';
|
|
$currentThinking = null;
|
|
$currentThinkingSignature = null;
|
|
|
|
foreach ($result->getDataStream() as $data) {
|
|
$type = $data['type'] ?? '';
|
|
|
|
// Handle text content deltas
|
|
if ('content_block_delta' === $type && isset($data['delta']['text'])) {
|
|
yield $data['delta']['text'];
|
|
continue;
|
|
}
|
|
|
|
// Handle thinking content block start
|
|
if ('content_block_start' === $type
|
|
&& isset($data['content_block']['type'])
|
|
&& 'thinking' === $data['content_block']['type']
|
|
) {
|
|
$currentThinking = '';
|
|
$currentThinkingSignature = null;
|
|
continue;
|
|
}
|
|
|
|
// Handle thinking content deltas
|
|
if ('content_block_delta' === $type
|
|
&& isset($data['delta']['type'])
|
|
&& 'thinking_delta' === $data['delta']['type']
|
|
) {
|
|
$currentThinking .= $data['delta']['thinking'] ?? '';
|
|
continue;
|
|
}
|
|
|
|
// Handle thinking signature deltas
|
|
if ('content_block_delta' === $type
|
|
&& isset($data['delta']['type'])
|
|
&& 'signature_delta' === $data['delta']['type']
|
|
) {
|
|
$currentThinkingSignature = ($currentThinkingSignature ?? '').$data['delta']['signature'];
|
|
continue;
|
|
}
|
|
|
|
// Handle tool_use content block start
|
|
if ('content_block_start' === $type
|
|
&& isset($data['content_block']['type'])
|
|
&& 'tool_use' === $data['content_block']['type']
|
|
) {
|
|
$currentToolCall = [
|
|
'id' => $data['content_block']['id'],
|
|
'name' => $data['content_block']['name'],
|
|
];
|
|
$currentToolCallJson = '';
|
|
continue;
|
|
}
|
|
|
|
// Handle tool_use input JSON deltas
|
|
if ('content_block_delta' === $type
|
|
&& isset($data['delta']['type'])
|
|
&& 'input_json_delta' === $data['delta']['type']
|
|
) {
|
|
$currentToolCallJson .= $data['delta']['partial_json'] ?? '';
|
|
continue;
|
|
}
|
|
|
|
// Handle content block stop - finalize current thinking or tool call
|
|
if ('content_block_stop' === $type) {
|
|
if (null !== $currentThinking) {
|
|
yield new ThinkingContent($currentThinking, $currentThinkingSignature);
|
|
$currentThinking = null;
|
|
$currentThinkingSignature = null;
|
|
continue;
|
|
}
|
|
|
|
if (null !== $currentToolCall) {
|
|
$input = '' !== $currentToolCallJson
|
|
? json_decode($currentToolCallJson, true, flags: \JSON_THROW_ON_ERROR)
|
|
: [];
|
|
$toolCalls[] = new ToolCall(
|
|
$currentToolCall['id'],
|
|
$currentToolCall['name'],
|
|
$input
|
|
);
|
|
$currentToolCall = null;
|
|
$currentToolCallJson = '';
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Handle message stop - yield tool calls if any were collected
|
|
if ('message_stop' === $type && [] !== $toolCalls) {
|
|
yield new ToolCallResult(...$toolCalls);
|
|
}
|
|
}
|
|
}
|
|
}
|