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