PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.4
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / anthropic.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago chatml.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago perplexity.php 1 year ago replicate.php 1 year ago
anthropic.php
706 lines
1 <?php
2
3 class Meow_MWAI_Engines_Anthropic extends Meow_MWAI_Engines_ChatML
4 {
5 // Streaming
6 protected $streamInTokens = null;
7 protected $streamOutTokens = null;
8 protected $streamBlocks;
9 protected $streamIsThinking = false;
10 protected $mcpServerNames = [];
11 protected $mcpTools = []; // Track MCP tools by ID
12 protected $mcpToolCount = 0;
13
14 public function __construct( $core, $env )
15 {
16 parent::__construct( $core, $env );
17 }
18
19 protected function isMCPTool( $toolName ) {
20 // Get all MCP tools from the filter
21 $mcpTools = apply_filters( 'mwai_mcp_tools', [] );
22
23 // Log available MCP tools for debugging
24 if ( empty( $mcpTools ) ) {
25 Meow_MWAI_Logging::log( "Anthropic: No MCP tools available from filter" );
26 }
27
28 foreach ( $mcpTools as $tool ) {
29 if ( isset( $tool['name'] ) && $tool['name'] === $toolName ) {
30 Meow_MWAI_Logging::log( "Anthropic: Found MCP tool match: {$toolName}" );
31 return true;
32 }
33 }
34
35 // If we have MCP servers but tool not found, it might be an issue
36 if ( !empty( $this->mcpServerNames ) && !empty( $toolName ) ) {
37 Meow_MWAI_Logging::log( "Anthropic: Tool '{$toolName}' not found in MCP tools list" );
38 }
39
40 return false;
41 }
42
43 public function reset_stream() {
44 $this->streamContent = null;
45 $this->streamBuffer = null;
46 $this->streamFunctionCall = null;
47 $this->streamToolCalls = [];
48 $this->streamLastMessage = null;
49 $this->streamInTokens = null;
50 $this->streamOutTokens = null;
51 $this->streamIsThinking = false;
52 $this->mcpTools = []; // Reset MCP tools tracking
53 $this->textStarted = false; // Reset text started flag
54 $this->requestSentEmitted = false; // Reset request sent flag
55 $this->emittedFunctionResults = []; // Reset function result tracking
56
57 $this->streamBlocks = [
58 'role' => 'assistant',
59 'content' => []
60 ];
61
62 $this->inModel = null;
63 $this->inId = null;
64 }
65
66 protected function set_environment() {
67 $env = $this->env;
68 $this->apiKey = $env['apikey'];
69 }
70
71 protected function build_url( $query, $endpoint = null ) {
72 $endpoint = apply_filters( 'mwai_anthropic_endpoint', 'https://api.anthropic.com/v1', $this->env );
73 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
74 $url = trailingslashit( $endpoint ) . 'messages';
75 }
76 else {
77 throw new Exception( 'AI Engine: Unsupported query type.' );
78 }
79 return $url;
80 }
81
82 protected function build_headers( $query ) {
83 parent::build_headers( $query );
84 $headers = array(
85 'Content-Type' => 'application/json',
86 'x-api-key' => $this->apiKey,
87 'anthropic-version' => '2023-06-01',
88 'anthropic-beta' => 'tools-2024-04-04, pdfs-2024-09-25, mcp-client-2025-04-04',
89 'User-Agent' => 'AI Engine',
90 );
91 return $headers;
92 }
93
94 public function final_checks( Meow_MWAI_Query_Base $query ) {
95 // We skip this completely.
96 // maxMessages is handed in build_messages().
97 }
98
99 protected function build_messages( $query ) {
100 $messages = [];
101
102 // Then, if any, we need to add the 'messages', they are already formatted.
103 foreach ( $query->messages as $message ) {
104 $messages[] = $message;
105 }
106
107 // Handle the maxMessages
108 if ( !empty( $query->maxMessages ) ) {
109 $messages = array_slice( $messages, -$query->maxMessages );
110 }
111
112 // If the first message is not a 'user' role, we remove it.
113 if ( !empty( $messages ) && $messages[0]['role'] !== 'user' ) {
114 array_shift( $messages );
115 }
116
117 if ( $query->attachedFile ) {
118 // https://docs.anthropic.com/claude/reference/messages-examples#vision
119 // Claude only supports image/jpeg, image/png, image/gif, and image/webp media types.
120 $mime = $query->attachedFile->get_mimeType();
121 // Claude only supports upload by data (base64), not by URL.
122 $data = $query->attachedFile->get_base64();
123 $message = $query->get_message();
124 $isPDF = $mime === 'application/pdf';
125 $isIMG = !$isPDF && $query->attachedFile->is_image();
126
127 if ( $isPDF ) {
128 if ( empty( $message ) ) {
129 // Claude doesn't support messages with only PDFs, so we add a text message.
130 $message = "I uploaded a PDF. Do not consider this message as part of the conversation.";
131 }
132 $messages[] = [
133 'role' => 'user',
134 'content' => [
135 [
136 "type" => "text",
137 "text" => $message
138 ],
139 [
140 "type" => "document",
141 "source" => [
142 "type" => "base64",
143 "media_type" => "application/pdf",
144 "data" => $data
145 ]
146 ]
147 ]
148 ];
149 }
150 else if ( $isIMG ) {
151 if ( empty( $message ) ) {
152 // Claude doesn't support messages with only images, so we add a text message.
153 $message = "I uploaded an image. Do not consider this message as part of the conversation.";
154 }
155 $messages[] = [
156 'role' => 'user',
157 'content' => [
158 [
159 "type" => "text",
160 "text" => $message
161 ],
162 [
163 "type" => "image",
164 "source" => [
165 "type" => "base64",
166 "media_type" => $mime,
167 "data" => $data
168 ]
169 ]
170 ]
171 ];
172 }
173 }
174 else {
175 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
176 }
177
178 return $messages;
179 }
180
181 // Define a function to recursively replace empty arrays with empty stdClass objects
182 // To avoid errors with OpenAI's API
183 private function replaceEmptyArrayWithObject( $item ) {
184 if ( is_array( $item ) ) {
185 if ( empty( $item ) ) {
186 return new stdClass(); // Replace empty array with empty object
187 }
188 foreach ( $item as $key => $value ) {
189 $item[$key] = $this->replaceEmptyArrayWithObject( $value ); // Recurse
190 }
191 }
192 return $item;
193 }
194
195 protected function build_body( $query, $streamCallback = null, $extra = null ) {
196 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
197 $body = array(
198 "model" => $query->model,
199 "max_tokens" => $query->maxTokens,
200 "temperature" => $query->temperature,
201 "stream" => !is_null( $streamCallback ),
202 "messages" => []
203 );
204
205 if ( !empty( $query->instructions ) ) {
206 $body['system'] = $query->instructions;
207 }
208
209 // Build the messages
210 $body['messages'][] = [ 'role' => 'user', 'content' => $query->message ];
211
212 if ( !empty( $query->blocks ) ) {
213 foreach ( $query->blocks as $feedback_block ) {
214 $contentBlock = $feedback_block['rawMessage']['content'];
215 $assistantMessageIndex = count($body['messages']);
216 $body['messages'][] = [
217 'role' => 'assistant',
218 'content' => $contentBlock
219 ];
220
221 foreach ( $feedback_block['feedbacks'] as $feedback ) {
222 $feedbackValue = $feedback['reply']['value'];
223 if ( !is_string( $feedbackValue ) ) {
224 $feedbackValue = json_encode( $feedbackValue );
225 }
226
227 // Check for the tool_use message and make sure input is not null.
228 foreach ( $body['messages'][$assistantMessageIndex]['content'] as &$message ) {
229 if ( $message['type'] === 'tool_use' && $message['id'] === $feedback['request']['toolId'] ) {
230 if ( empty( $message['input'] ) ) {
231 $message['input'] = new stdClass();
232 }
233 break;
234 }
235 }
236 unset( $message );
237
238 // Add a new message with tool_result if tool_use.
239 $body['messages'][] = [
240 'role' => 'user',
241 'content' => [
242 [
243 'type' => 'tool_result',
244 'tool_use_id' => $feedback['request']['toolId'],
245 'content' => $feedbackValue,
246 'is_error' => false // Cool, Anthropic supports errors!
247 ]
248 ]
249 ];
250
251 // Emit function result event if streaming and debug mode are enabled
252 if ( $this->currentDebugMode && !empty( $this->streamCallback ) ) {
253 $toolId = $feedback['request']['toolId'] ?? null;
254 // Only emit if we haven't already emitted for this tool
255 if ( $toolId && !isset( $this->emittedFunctionResults[$toolId] ) ) {
256 $this->emittedFunctionResults[$toolId] = true;
257
258 $functionName = $feedback['request']['name'];
259 $resultPreview = (string)$feedbackValue;
260 if ( strlen( $resultPreview ) > 100 ) {
261 $resultPreview = substr( $resultPreview, 0, 100 ) . '...';
262 }
263
264 $event = Meow_MWAI_Event::function_result( $functionName )
265 ->set_metadata( 'result', $resultPreview )
266 ->set_metadata( 'tool_use_id', $toolId );
267 call_user_func( $this->streamCallback, $event );
268 }
269 }
270 }
271 }
272 }
273
274 // TODO: This WAS COPIED FROM BELOW
275 // Support for functions
276 if ( !empty( $query->functions ) ) {
277 $model = $this->retrieve_model_info( $query->model );
278 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
279 Meow_MWAI_Logging::warn( 'The model "' . $query->model . '" doesn\'t support Function Calling.' );
280 }
281 else {
282 $body['tools'] = [];
283 // Dynamic function: they will interactively enhance the completion (tools).
284 foreach ( $query->functions as $function ) {
285 $body['tools'][] = $function->serializeForAnthropic();
286 }
287 // Static functions: they will be executed at the end of the completion.
288 //$body['function_call'] = $query->functionCall;
289 }
290 }
291
292 // To avoid errors with Anthropic's API, we need to replace empty arrays with empty objects
293 $body = $this->replaceEmptyArrayWithObject( $body );
294 return $body;
295 }
296 else if ( $query instanceof Meow_MWAI_Query_Text ) {
297 $body = array(
298 "model" => $query->model,
299 "stream" => !is_null( $streamCallback ),
300 );
301
302 if ( !empty( $query->maxTokens ) ) {
303 $body['max_tokens'] = $query->maxTokens;
304 }
305 else {
306 // https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-table
307 $body['max_tokens'] = 4096;
308 }
309
310 if ( !empty( $query->temperature ) ) {
311 $body['temperature'] = $query->temperature;
312 }
313
314 if ( !empty( $query->stop ) ) {
315 $body['stop'] = $query->stop;
316 }
317
318 // First, we need to add the first message (the instructions).
319 if ( !empty( $query->instructions ) ) {
320 $body['system'] = $query->instructions;
321 }
322
323 // If there is a context, we need to add it.
324 if ( !empty( $query->context ) ) {
325 if ( empty( $body['system'] ) ) {
326 $body['system'] = "";
327 }
328 $body['system'] = empty( $body['system'] ) ? '' : $body['system'] . "\n\n";
329 $body['system'] = $body['system'] . "Context:\n\n" . $query->context;
330 }
331
332 // Support for functions
333 if ( !empty( $query->functions ) ) {
334 $model = $this->retrieve_model_info( $query->model );
335 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
336 Meow_MWAI_Logging::warn( 'The model "' . $query->model . '" doesn\'t support Function Calling.' );
337 }
338 else {
339 $body['tools'] = [];
340 // Dynamic function: they will interactively enhance the completion (tools).
341 foreach ( $query->functions as $function ) {
342 $body['tools'][] = $function->serializeForAnthropic();
343 }
344 // Static functions: they will be executed at the end of the completion.
345 //$body['function_call'] = $query->functionCall;
346 }
347 }
348
349 $body['messages'] = $this->build_messages( $query );
350
351 // Add MCP servers if available
352 if ( isset( $query->mcpServers ) && is_array( $query->mcpServers ) && ! empty( $query->mcpServers ) ) {
353 $body['mcp_servers'] = [];
354 $mcp_envs = $this->core->get_option( 'mcp_envs' );
355 $this->mcpServerNames = []; // Reset MCP server names
356
357 foreach ( $query->mcpServers as $mcpServer ) {
358 if ( isset( $mcpServer['id'] ) ) {
359 // Find the full MCP server configuration by ID
360 foreach ( $mcp_envs as $env ) {
361 if ( $env['id'] === $mcpServer['id'] ) {
362 $mcp_config = [
363 'type' => 'url',
364 'url' => $env['url'],
365 'name' => $env['name']
366 ];
367
368 // Add authorization token if available
369 if ( ! empty( $env['token'] ) ) {
370 $mcp_config['authorization_token'] = $env['token'];
371 }
372
373 $body['mcp_servers'][] = $mcp_config;
374 $this->mcpServerNames[] = $env['name']; // Track MCP server names
375 break;
376 }
377 }
378 }
379 }
380 }
381
382 return $body;
383 }
384 else {
385 throw new Exception( 'AI Engine: Unsupported query type.' );
386 }
387 }
388
389 protected function stream_data_handler( $json ) {
390 $content = null;
391 $type = !empty( $json['type'] ) ? $json['type'] : null;
392 if ( is_null( $type ) ) {
393 return $content;
394 }
395
396 if ( $type === 'message_start' ) {
397 $usage = $json['message']['usage'];
398 $this->streamInTokens = $usage['input_tokens'];
399 $this->inModel = $json['message']['model'];
400 $this->inId = $json['message']['id'];
401
402 // Send MCP discovery event if MCP servers are configured
403 if ( $this->currentDebugMode && $this->streamCallback ) {
404 if ( !empty( $this->mcpServerNames ) ) {
405 $serverCount = count( $this->mcpServerNames );
406
407 // Get MCP tools count
408 $mcpTools = apply_filters( 'mwai_mcp_tools', [] );
409 $toolCount = count( $mcpTools );
410
411 $event = Meow_MWAI_Event::mcp_discovery( $serverCount, $toolCount )
412 ->set_metadata( 'servers', $this->mcpServerNames );
413 call_user_func( $this->streamCallback, $event );
414 }
415 }
416 }
417 else if ( $type === 'content_block_start' ) {
418 $this->streamBlocks['content'][] = $json['content_block'];
419
420 // Send "Generating response..." when we start a text block
421 if ( $this->currentDebugMode && $this->streamCallback ) {
422 $block = $json['content_block'];
423 if ( $block['type'] === 'text' && !isset( $this->textStarted ) ) {
424 $this->textStarted = true;
425 $event = Meow_MWAI_Event::generating_response();
426 call_user_func( $this->streamCallback, $event );
427 }
428 }
429 }
430 else if ( $type === 'content_block_delta' ) {
431 $index = $json['index'];
432 $block = $this->streamBlocks['content'][$index];
433 if ( $json['delta']['type'] === 'text_delta' ) {
434 $block['text'] .= $json['delta']['text'];
435 $isThinkingStart = strpos( $block['text'], '<thinking' ) === 0;
436 $isThinkingEnd = strpos( $block['text'], '</thinking>' ) === 0;
437
438 if ( $isThinkingStart ) {
439 $this->streamIsThinking = true;
440 // Send thinking start event
441 if ( $this->currentDebugMode && $this->streamCallback ) {
442 $event = Meow_MWAI_Event::thinking( 'Thinking...' );
443 call_user_func( $this->streamCallback, $event );
444 }
445 }
446 if ( $isThinkingEnd ) {
447 $this->streamIsThinking = false;
448 // Send thinking end event
449 if ( $this->currentDebugMode && $this->streamCallback ) {
450 $event = Meow_MWAI_Event::thinking( 'Thinking completed.' )
451 ->set_metadata( 'status', 'completed' );
452 call_user_func( $this->streamCallback, $event );
453 }
454 }
455 $content = $json['delta']['text'];
456 }
457 else if ( $json['delta']['type'] === 'input_json_delta' ) {
458 // Somehow, the input is set as an array, but it should be a string since it's JSON.
459 $block['input'] = is_array( $block['input'] ) ? "" : $block['input'];
460 $block['input'] .= $json['delta']['partial_json'];
461
462 // Skip sending tool arguments event - too verbose
463 // if ( $this->currentDebugMode && $this->streamCallback && isset($block['type']) && $block['type'] === 'tool_use' ) {
464 // $event = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['TOOL_ARGS'] ) )
465 // ->set_content( 'Streaming tool arguments...' )
466 // ->set_metadata( 'tool_name', $block['name'] ?? 'unknown' )
467 // ->set_metadata( 'partial_args', $json['delta']['partial_json'] );
468 // call_user_func( $this->streamCallback, $event );
469 // }
470 }
471 $this->streamBlocks['content'][$index] = $block;
472 }
473 // At the end of a block, let's look for any 'input' not yet decoded from JSON
474 else if ( $type === 'content_block_stop' ) {
475 $index = $json['index'];
476 $block = $this->streamBlocks['content'][$index];
477 if ( isset( $block['input'] ) && is_string( $block['input'] ) ) {
478 $block['input'] = json_decode( $block['input'], true );
479 }
480 $this->streamBlocks['content'][$index] = $block;
481
482 // Send event for content block completion
483 if ( $this->currentDebugMode && $this->streamCallback ) {
484 if ( $block['type'] === 'mcp_tool_use' ) {
485 // Store the tool name for later lookup when we get the result
486 $this->mcpTools[$block['id']] = $block['name'];
487
488 $event = Meow_MWAI_Event::mcp_calling( $block['name'], $block['id'], $block['input'] ?? [] )
489 ->set_metadata( 'server_name', $block['server_name'] ?? 'unknown' );
490 call_user_func( $this->streamCallback, $event );
491 }
492 else if ( $block['type'] === 'mcp_tool_result' ) {
493 // Look up the tool name from the tool_use_id
494 $tool_use_id = $block['tool_use_id'] ?? '';
495 $tool_name = isset( $this->mcpTools[$tool_use_id] ) ? $this->mcpTools[$tool_use_id] : 'unknown';
496
497 $event = Meow_MWAI_Event::mcp_result( $tool_name, $tool_use_id )
498 ->set_metadata( 'content', $block['content'] ?? '' );
499 call_user_func( $this->streamCallback, $event );
500 }
501 else if ( $block['type'] === 'tool_use' ) {
502 // Regular tool use (non-MCP)
503 $event = Meow_MWAI_Event::function_calling( $block['name'] ?? 'unknown', $block['input'] ?? [] )
504 ->set_metadata( 'tool_id', $block['id'] ?? '' );
505 call_user_func( $this->streamCallback, $event );
506 }
507 else if ( $block['type'] === 'text' ) {
508 // Don't send any event here - the text generation is handled by content deltas
509 // and completion is handled by message_stop
510 }
511 else if ( $block['type'] === 'ping' ) {
512 // https://docs.anthropic.com/en/docs/build-with-claude/streaming#ping-events
513 }
514 else {
515 Meow_MWAI_Logging::log( "Anthropic: Unknown block type in content_block_stop: " . $block['type'] );
516 }
517 }
518 }
519 else if ( $type === 'message_delta' ) {
520 $usage = $json['usage'];
521 $this->streamOutTokens = $usage['output_tokens'];
522 }
523 else if ( $type === 'error' ) {
524 $error = $json['error'];
525 $message = $error['message'];
526
527 // Send error event
528 if ( $this->currentDebugMode && $this->streamCallback ) {
529 $event = Meow_MWAI_Event::error( $message )
530 ->set_metadata( 'error_type', $error['type'] ?? 'unknown' );
531 call_user_func( $this->streamCallback, $event );
532 }
533
534 throw new Exception( $message );
535 }
536 else if ( $type === 'message_stop' ) {
537 // Skip sending completion event - too verbose
538 // if ( $this->currentDebugMode && $this->streamCallback ) {
539 // $event = Meow_MWAI_Event::stream_completed()
540 // ->set_metadata( 'total_tokens', ($this->streamInTokens ?? 0) + ($this->streamOutTokens ?? 0) );
541 // call_user_func( $this->streamCallback, $event );
542 // }
543 }
544 else {
545 Meow_MWAI_Logging::log( "Anthropic: Unknown stream data type: $type" );
546 }
547
548 // Avoid some endings
549 $endings = [ "<|im_end|>", "</s>" ];
550 if ( in_array( $content, $endings ) ) {
551 $content = null;
552 }
553
554 // If the stream is thinking, we don't want to return anything yet.
555 if ( $this->streamIsThinking ) {
556 $content = null;
557 }
558
559 return ( $content === '0' || !empty( $content ) ) ? $content : null;
560 }
561
562 // This create the "choices" (even though, often, it is only one choice).
563 // It is basically the reply, but one that is understood by the Meow_MWAI_Reply class.
564 public function create_choices( $data ) {
565 $returned_choices = [];
566 foreach ( $data['content'] as $content ) {
567 if ( $content['type'] === 'tool_use' ) {
568 // Regular function calls need local execution
569 $returned_choices[] = [
570 'message' => [
571 'tool_calls' => [
572 [
573 'id' => $content['id'],
574 'type' => 'function',
575 'function' => [
576 'name' => $content['name'],
577 'arguments' => empty( $content['input'] ) ? new stdClass() : $content['input'],
578 ]
579 ]
580
581 ]
582 ]
583 ];
584 }
585 else if ( $content['type'] === 'text' ) {
586 $returned_choices[] = [
587 'message' => [
588 'content' => $content['text']
589 ]
590 ];
591 }
592 }
593 return $returned_choices;
594 }
595
596 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
597 $isStreaming = !is_null( $streamCallback );
598
599 // Initialize debug mode
600 $this->init_debug_mode( $query );
601
602 if ( $isStreaming ) {
603 $this->streamCallback = $streamCallback;
604 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
605 }
606
607 $this->reset_stream();
608 $data = null;
609 $body = $this->build_body( $query, $streamCallback );
610 $url = $this->build_url( $query );
611 $headers = $this->build_headers( $query );
612 $options = $this->build_options( $headers, $body );
613
614 try {
615 $res = $this->run_query( $url, $options, $streamCallback );
616 $reply = new Meow_MWAI_Reply( $query );
617 $returned_id = null;
618 $returned_model = null;
619 $returned_choices = [];
620
621 // Streaming Mode
622 if ( $isStreaming ) {
623 $returned_id = $this->inId;
624 $returned_model = $this->inModel ? $this->inModel : $query->model;
625 if ( !is_null( $this->streamInTokens && !is_null( $this->streamOutTokens ) ) ) {
626 $returned_in_tokens = $this->streamInTokens;
627 $returned_out_tokens = $this->streamOutTokens;
628 }
629 $data = $this->streamBlocks;
630 $returned_choices = $this->create_choices( $this->streamBlocks );
631 }
632 // Standard Mode
633 else {
634 $data = $res['data'];
635 $returned_id = $data['id'];
636 $returned_model = $data['model'];
637 $usage = $data['usage'];
638 if ( !empty( $usage ) ) {
639 $returned_in_tokens = isset( $usage['input_tokens'] ) ? $usage['input_tokens'] : null;
640 $returned_out_tokens = isset( $usage['output_tokens'] ) ? $usage['output_tokens'] : null;
641 }
642 $returned_choices = $this->create_choices( $data );
643 }
644
645 $reply->set_choices( $returned_choices, $data );
646 if ( !empty( $returned_id ) ) {
647 $reply->set_id( $returned_id );
648 }
649
650 // Handle tokens.
651 $this->handle_tokens_usage( $reply, $query, $returned_model,
652 $returned_in_tokens, $returned_out_tokens
653 );
654
655 return $reply;
656 }
657 catch ( Exception $e ) {
658 $error = $e->getMessage();
659 $json = json_decode( $error, true );
660 if ( json_last_error() === JSON_ERROR_NONE ) {
661 if ( isset( $json['error'] ) && isset( $json['error']['message'] ) ) {
662 $error = $json['error']['message'];
663 }
664 }
665 Meow_MWAI_Logging::error( "(Anthropic) " . $error );
666 $service = $this->get_service_name();
667 $message = "From $service: " . $error;
668 throw new Exception( $message );
669 }
670 finally {
671 if ( $isStreaming ) {
672 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
673 }
674 }
675 }
676
677 protected function get_service_name() {
678 return "Anthropic";
679 }
680
681
682 public function get_models() {
683 return apply_filters( 'mwai_anthropic_models', MWAI_ANTHROPIC_MODELS );
684 }
685
686 static public function get_models_static() {
687 return MWAI_ANTHROPIC_MODELS;
688 }
689
690 public function handle_tokens_usage( $reply, $query, $returned_model,
691 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
692 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
693 $returned_in_tokens : $reply->get_in_tokens( $query );
694 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
695 $returned_out_tokens : $reply->get_out_tokens();
696 if ( !empty( $reply->id ) ) {
697 // Would be cool to retrieve the usage from the API, but it's not possible.
698 }
699 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
700 $reply->set_usage( $usage );
701 }
702
703 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
704 return parent::get_price( $query, $reply );
705 }
706 }