sendPrompt([['role' => 'user', 'content' => 'Hello']]); * echo $res['content']; */ class svcAnthropic extends LLMProvider { private const API_BASE = 'https://api.anthropic.com/v1'; private const API_CHAT = self::API_BASE . '/messages'; private const API_VERSION = '2023-06-01'; private static array $availableModels = [ ['id' => 'claude-opus-4-6', 'label' => 'Claude Opus 4.6'], ['id' => 'claude-sonnet-4-6', 'label' => 'Claude Sonnet 4.6'], ['id' => 'claude-haiku-4-5-20251001', 'label' => 'Claude Haiku 4.5'], ]; public function __construct(array $config) { if (empty($config['secretKey'])) { throw new \Exception('Anthropic secretKey is required'); } $this->apiKey = $config['secretKey']; $this->model = $config['model'] ?? 'claude-sonnet-4-6'; $this->maxTokens = (int)($config['max_tokens'] ?? 4096); $this->temperature = (float)($config['temperature'] ?? 0.7); } /** * Send a chat prompt and return the full response. */ public function sendPrompt(array $messages, array $options = []): array { try { $payload = $this->buildPayload($messages, $options); $response = $this->curlPost(self::API_CHAT, $this->headers(!empty($options['cache_system'])), $payload); $content = $response['content'][0]['text'] ?? ''; $usage = $response['usage'] ?? []; return $this->successResponse($content, [ 'prompt_tokens' => $usage['input_tokens'] ?? 0, 'completion_tokens' => $usage['output_tokens'] ?? 0, 'total_tokens' => ($usage['input_tokens'] ?? 0) + ($usage['output_tokens'] ?? 0), ]); } catch (\Exception $e) { return $this->errorResponse($e->getMessage()); } } /** * Stream a chat prompt, calling $callback with each text chunk. * * @param callable $callback function(string $chunk) — called with each partial text */ public function streamPrompt(array $messages, array $options, callable $callback): array { try { $payload = $this->buildPayload($messages, $options); $streamError = null; $this->curlStream(self::API_CHAT, $this->headers(!empty($options['cache_system'])), $payload, function(array $chunk) use ($callback, &$streamError) { $type = $chunk['type'] ?? ''; if ($type === 'content_block_delta') { $delta = $chunk['delta']['text'] ?? ''; if ($delta !== '') { $callback($delta); } } elseif ($type === 'error') { // Anthropic sends inline error events during streaming $streamError = $chunk['error']['message'] ?? 'Anthropic streaming error'; } }); if ($streamError) { return ['success' => false, 'error' => $streamError]; } return ['success' => true, 'error' => '']; } catch (\Exception $e) { return ['success' => false, 'error' => $e->getMessage()]; } } /** * Return supported Claude models. */ public function getModels(): array { return self::$availableModels; } /** * Validate the API key with a minimal single-token request. */ public function testConnection(): array { try { $start = microtime(true); $payload = [ 'model' => $this->model, 'max_tokens' => 1, 'messages' => [['role' => 'user', 'content' => 'hi']], ]; $this->curlPost(self::API_CHAT, $this->headers(), $payload); $latency = (int)((microtime(true) - $start) * 1000); return ['success' => true, 'latency_ms' => $latency, 'error' => '']; } catch (\Exception $e) { return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; } } // ─── Private Helpers ───────────────────────────────────────────────────── private function headers(bool $withCache = false): array { $h = [ 'Content-Type: application/json', 'x-api-key: ' . $this->apiKey, 'anthropic-version: ' . self::API_VERSION, ]; if ($withCache) { $h[] = 'anthropic-beta: prompt-caching-2024-07-31'; } return $h; } private function buildPayload(array $messages, array $options): array { $model = $options['model'] ?? $this->model; $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); $temperature = (float)($options['temperature'] ?? $this->temperature); $cacheSystem = !empty($options['cache_system']); $payload = [ 'model' => $model, 'max_tokens' => $maxTokens, 'temperature' => $temperature, 'messages' => $messages, ]; // Anthropic uses a top-level 'system' key. // When cache_system is set, use block format with cache_control so // the large static report payload is cached between turns. if ($this->systemPrompt !== '') { if ($cacheSystem) { $payload['system'] = [[ 'type' => 'text', 'text' => $this->systemPrompt, 'cache_control' => ['type' => 'ephemeral'], ]]; } else { $payload['system'] = $this->systemPrompt; } } return $payload; } }