Skip to content

Commit 038c60b

Browse files
committed
Implement Anthropic provider
1 parent 29fb7a0 commit 038c60b

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed

src/Provider/AnthropicProvider.php

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
/**
4+
* Part of the Joomla Framework AI Package
5+
*
6+
* @copyright Copyright (C) 2025 Open Source Matters, Inc. All rights reserved.
7+
* @license GNU General Public License version 2 or later; see LICENSE
8+
*/
9+
10+
namespace Joomla\AI\Provider;
11+
12+
use Joomla\AI\AbstractProvider;
13+
use Joomla\AI\Exception\AuthenticationException;
14+
use Joomla\AI\Exception\InvalidArgumentException;
15+
use Joomla\AI\Exception\ProviderException;
16+
use Joomla\AI\Interface\ProviderInterface;
17+
use Joomla\AI\Response\Response;
18+
use Joomla\Http\HttpFactory;
19+
20+
/**
21+
* Anthropic provider implementation.
22+
*
23+
* @since __DEPLOY_VERSION__
24+
*/
25+
class AnthropicProvider extends AbstractProvider implements ProviderInterface
26+
{
27+
/**
28+
* Custom base URL for API requests
29+
*
30+
* @var string
31+
* @since __DEPLOY_VERSION__
32+
*/
33+
private $baseUrl;
34+
35+
/**
36+
* Constructor.
37+
*
38+
* @param array|\ArrayAccess $options Provider options array.
39+
* @param HttpFactory $httpFactory The http factory
40+
*
41+
* @since __DEPLOY_VERSION__
42+
*/
43+
public function __construct(array $options = [], ?HttpFactory $httpFactory = null)
44+
{
45+
parent::__construct($options, $httpFactory);
46+
47+
$this->baseUrl = $this->getOption('base_url', 'https://api.anthropic.com/v1');
48+
49+
// Remove trailing slash if present
50+
if (substr($this->baseUrl, -1) === '/') {
51+
$this->baseUrl = rtrim($this->baseUrl, '/');
52+
}
53+
}
54+
55+
/**
56+
* Check if Anthropic provider is supported/configured.
57+
*
58+
* @return boolean True if API key is available
59+
* @since __DEPLOY_VERSION__
60+
*/
61+
public static function isSupported(): bool
62+
{
63+
return !empty($_ENV['ANTHROPIC_API_KEY']) ||
64+
!empty(getenv('ANTHROPIC_API_KEY'));
65+
}
66+
67+
/**
68+
* Get the provider name.
69+
*
70+
* @return string The provider name
71+
* @since __DEPLOY_VERSION__
72+
*/
73+
public function getName(): string
74+
{
75+
return 'Anthropic';
76+
}
77+
78+
/**
79+
* Get the messages endpoint URL.
80+
*
81+
* @return string The endpoint URL
82+
* @since __DEPLOY_VERSION__
83+
*/
84+
private function getMessagesEndpoint(): string
85+
{
86+
return $this->baseUrl . '/messages';
87+
}
88+
89+
/**
90+
* Send a message to Anthropic and return response.
91+
*
92+
* @param string $message The message to send
93+
* @param array $options Additional options for the request
94+
*
95+
* @return Response The AI response object
96+
* @since __DEPLOY_VERSION__
97+
*/
98+
public function chat(string $message, array $options = []): Response
99+
{
100+
$payload = $this->buildChatRequestPayload($message, $options);
101+
102+
$headers = $this->buildHeaders();
103+
104+
$httpResponse = $this->makePostRequest(
105+
$this->getMessagesEndpoint(),
106+
json_encode($payload),
107+
$headers
108+
);
109+
110+
return $this->parseAnthropicResponse($httpResponse->body);
111+
}
112+
113+
/**
114+
* Build HTTP headers for Anthropic API request.
115+
*
116+
* @return array HTTP headers
117+
* @since __DEPLOY_VERSION__
118+
*/
119+
private function buildHeaders(): array
120+
{
121+
$apiKey = $this->getApiKey();
122+
123+
return [
124+
'x-api-key' => $apiKey,
125+
'anthropic-version' => '2023-06-01',
126+
'content-type' => 'application/json'
127+
];
128+
}
129+
130+
/**
131+
* Get the Anthropic API key.
132+
*
133+
* @return string The API key
134+
* @throws AuthenticationException If API key is not found
135+
* @since __DEPLOY_VERSION__
136+
*/
137+
private function getApiKey(): string
138+
{
139+
$apiKey = $this->getOption('api_key') ??
140+
$_ENV['ANTHROPIC_API_KEY'] ??
141+
getenv('ANTHROPIC_API_KEY');
142+
143+
if (empty($apiKey)) {
144+
throw new AuthenticationException(
145+
$this->getName(),
146+
['message' => 'Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable or provide api_key option.']
147+
);
148+
}
149+
150+
return $apiKey;
151+
}
152+
153+
/**
154+
* Build payload for chat request.
155+
*
156+
* @param string $message The user message to send
157+
* @param array $options Additional options
158+
*
159+
* @return array The request payload
160+
* @since __DEPLOY_VERSION__
161+
*/
162+
private function buildChatRequestPayload(string $message, array $options = []): array
163+
{
164+
$model = $options['model'] ?? $this->getOption('model', 'claude-3-sonnet-20240229');
165+
166+
$messages = [
167+
[
168+
'role' => 'user',
169+
'content' => $message
170+
]
171+
];
172+
173+
$payload = [
174+
'model' => $model,
175+
'messages' => $messages,
176+
'max_tokens' => $options['max_tokens'] ?? 1024
177+
];
178+
179+
if (isset($options['system'])) {
180+
$payload['system'] = $options['system'];
181+
}
182+
183+
if (isset($options['temperature'])) {
184+
$payload['temperature'] = (float) $options['temperature'];
185+
}
186+
187+
return $payload;
188+
}
189+
190+
/**
191+
* Parse Anthropic API response into unified Response object.
192+
*
193+
* @param string $responseBody The JSON response body
194+
*
195+
* @return Response Unified response object
196+
* @since __DEPLOY_VERSION__
197+
*/
198+
private function parseAnthropicResponse(string $responseBody): Response
199+
{
200+
$data = $this->parseJsonResponse($responseBody);
201+
202+
// Get the text content from the first content block
203+
$content = '';
204+
if (!empty($data['content'][0]['text'])) {
205+
$content = $data['content'][0]['text'];
206+
}
207+
208+
$metadata = [
209+
'model' => $data['model'],
210+
'role' => $data['role'],
211+
'type' => $data['type'],
212+
'usage' => $data['usage'] ?? [],
213+
'stop_reason' => $data['stop_reason'],
214+
'stop_sequence' => $data['stop_sequence']
215+
];
216+
217+
return new Response(
218+
$content,
219+
$this->getName(),
220+
$metadata,
221+
200
222+
);
223+
}
224+
}

0 commit comments

Comments
 (0)