PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / trunk
AI Engine – The Chatbot, AI Framework & MCP for WordPress vtrunk
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 / xai.php
ai-engine / classes / engines Last commit date
anthropic.php 8 hours ago chatml.php 1 week ago core.php 2 days ago custom.php 1 month ago factory.php 1 week ago google-interactions.php 2 days ago google.php 1 week ago mistral.php 1 week ago open-router.php 3 weeks ago openai.php 3 weeks ago ovh.php 1 week ago perplexity.php 6 months ago replicate.php 5 months ago xai.php 1 month ago
xai.php
332 lines
1 <?php
2
3 class Meow_MWAI_Engines_XAI extends Meow_MWAI_Engines_ChatML {
4 public function __construct( $core, $env ) {
5 parent::__construct( $core, $env );
6 }
7
8 protected function set_environment() {
9 $env = $this->env;
10 $this->apiKey = $env['apikey'] ?? null;
11 }
12
13 protected function get_service_name() {
14 return 'xAI';
15 }
16
17 public function get_models() {
18 // Prefer dynamically-fetched models when available (the env was synced against /v1/models).
19 // Fall back to the static list so users on accounts that haven't synced (or that errored out
20 // during sync) can still resolve well-known Grok model ids.
21 $dynamic = $this->core->get_engine_models( 'xai' );
22 if ( !empty( $dynamic ) ) {
23 return $dynamic;
24 }
25 return apply_filters( 'mwai_xai_models', MWAI_XAI_MODELS );
26 }
27
28 public static function get_models_static() {
29 return MWAI_XAI_MODELS;
30 }
31
32 protected function build_url( $query, $endpoint = null ) {
33 $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
34 $endpoint = rtrim( $endpoint, '/' );
35 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
36 return $endpoint . '/chat/completions';
37 }
38 if ( $query instanceof Meow_MWAI_Query_Embed ) {
39 return $endpoint . '/embeddings';
40 }
41 throw new Exception( 'Unsupported query type for xAI.' );
42 }
43
44 protected function build_headers( $query ) {
45 if ( $query->apiKey ) {
46 $this->apiKey = $query->apiKey;
47 }
48 if ( empty( $this->apiKey ) ) {
49 throw new Exception( 'No xAI API Key provided. Please check your settings.' );
50 }
51 return [
52 'Content-Type' => 'application/json',
53 'Authorization' => 'Bearer ' . $this->apiKey,
54 'User-Agent' => 'AI Engine',
55 ];
56 }
57
58 protected function build_body( $query, $streamCallback = null, $extra = null ) {
59 $body = parent::build_body( $query, $streamCallback, $extra );
60
61 // xAI follows OpenAI's older Chat Completions schema: it expects max_tokens,
62 // not max_completion_tokens.
63 if ( isset( $body['max_completion_tokens'] ) ) {
64 $body['max_tokens'] = $body['max_completion_tokens'];
65 unset( $body['max_completion_tokens'] );
66 }
67
68 return $body;
69 }
70
71 /**
72 * Map an xAI model id to a human-readable display name.
73 * xAI ids vary in shape: grok-4, grok-4-0709 (date-stamped), grok-4-1-fast-reasoning,
74 * grok-4.20-0309-non-reasoning, grok-4.3 (dot-versioned). We try to keep variants
75 * distinguishable in the model dropdown without leaking internal date stamps as version numbers.
76 */
77 private function generate_human_readable_name( $modelId ) {
78 if ( strpos( $modelId, 'grok-code' ) !== false ) {
79 return 'Grok Code Fast';
80 }
81
82 // Grok 4 variants — try to capture both dotted ("4.20", "4.3") and dashed ("4-1") versions.
83 if ( strpos( $modelId, 'grok-4' ) === 0 ) {
84 $name = 'Grok 4';
85 if ( preg_match( '/^grok-4\.(\d+)/', $modelId, $m ) ) {
86 $name = 'Grok 4.' . $m[1];
87 }
88 elseif ( preg_match( '/^grok-4-(\d{1,2})(?:-|$)/', $modelId, $m ) ) {
89 // Treat trailing 4-digit groups as date stamps, not versions.
90 if ( strlen( $m[1] ) <= 2 ) {
91 $name = 'Grok 4.' . $m[1];
92 }
93 }
94 if ( strpos( $modelId, 'heavy' ) !== false ) { $name .= ' Heavy'; }
95 elseif ( strpos( $modelId, 'fast' ) !== false ) { $name .= ' Fast'; }
96 if ( strpos( $modelId, 'multi-agent' ) !== false ) { $name .= ' Multi-Agent'; }
97 if ( strpos( $modelId, 'non-reasoning' ) !== false ) {
98 $name .= ' (Standard)';
99 }
100 elseif ( strpos( $modelId, 'reasoning' ) !== false ) {
101 $name .= ' (Reasoning)';
102 }
103 elseif ( preg_match( '/-(\d{4})$/', $modelId, $m ) ) {
104 // Date-stamped release like grok-4-0709 → "Grok 4 (0709)".
105 $name .= ' (' . $m[1] . ')';
106 }
107 return $name;
108 }
109
110 if ( strpos( $modelId, 'grok-3-mini' ) !== false ) { return 'Grok 3 Mini'; }
111 if ( strpos( $modelId, 'grok-3' ) !== false ) { return 'Grok 3'; }
112 if ( strpos( $modelId, 'grok-2-vision' ) !== false ) { return 'Grok 2 Vision'; }
113 if ( strpos( $modelId, 'grok-2' ) !== false ) { return 'Grok 2'; }
114
115 return ucwords( str_replace( [ '-', '_' ], ' ', $modelId ) );
116 }
117
118 /**
119 * Estimate per-million-token pricing for an xAI model. xAI's /models endpoint
120 * returns prices in cents per token, but the field is not always populated, so
121 * we keep an internal mapping as a fallback.
122 */
123 private function estimate_pricing( $modelId, $remote ) {
124 if ( isset( $remote['prompt_text_token_price'] ) && isset( $remote['completion_text_token_price'] ) ) {
125 // xAI returns prices in cents per token. Convert to USD per million tokens.
126 $in = ( (float) $remote['prompt_text_token_price'] ) / 100 * 1000000;
127 $out = ( (float) $remote['completion_text_token_price'] ) / 100 * 1000000;
128 return [ 'in' => round( $in, 4 ), 'out' => round( $out, 4 ) ];
129 }
130 if ( strpos( $modelId, 'grok-4-heavy' ) !== false ) {
131 return [ 'in' => 5.00, 'out' => 25.00 ];
132 }
133 if ( strpos( $modelId, 'grok-4-fast' ) !== false ) {
134 return [ 'in' => 0.20, 'out' => 0.50 ];
135 }
136 if ( strpos( $modelId, 'grok-4' ) !== false ) {
137 return [ 'in' => 3.00, 'out' => 15.00 ];
138 }
139 if ( strpos( $modelId, 'grok-code' ) !== false ) {
140 return [ 'in' => 0.20, 'out' => 1.50 ];
141 }
142 if ( strpos( $modelId, 'grok-3-mini' ) !== false ) {
143 return [ 'in' => 0.30, 'out' => 0.50 ];
144 }
145 if ( strpos( $modelId, 'grok-3' ) !== false ) {
146 return [ 'in' => 3.00, 'out' => 15.00 ];
147 }
148 return [ 'in' => 1.00, 'out' => 3.00 ];
149 }
150
151 /**
152 * Retrieve the available models from xAI's /v1/models endpoint.
153 */
154 public function retrieve_models() {
155 try {
156 $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
157 $url = rtrim( $endpoint, '/' ) . '/models';
158
159 if ( empty( $this->apiKey ) ) {
160 throw new Exception( 'No xAI API Key provided for model retrieval.' );
161 }
162
163 $options = [
164 'headers' => [
165 'Authorization' => 'Bearer ' . $this->apiKey,
166 'User-Agent' => 'AI Engine'
167 ],
168 'timeout' => 10,
169 'sslverify' => MWAI_SSL_VERIFY
170 ];
171
172 $response = wp_remote_get( $url, $options );
173 if ( is_wp_error( $response ) ) {
174 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
175 }
176
177 $body = json_decode( $response['body'], true );
178 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
179 throw new Exception( 'AI Engine: Invalid response for xAI models list.' );
180 }
181
182 $models = [];
183 $seen = [];
184 foreach ( $body['data'] as $remote ) {
185 $modelId = $remote['id'] ?? '';
186 if ( empty( $modelId ) || isset( $seen[$modelId] ) ) {
187 continue;
188 }
189 $seen[$modelId] = true;
190
191 // Skip image/video generation models — they don't speak the chat-completions schema
192 // and would only confuse users showing up in chat dropdowns. xAI exposes them under
193 // separate endpoints (/v1/images, /v1/videos) that AI Engine doesn't route here yet.
194 if ( strpos( $modelId, 'grok-2-image' ) !== false
195 || strpos( $modelId, 'grok-imagine' ) !== false
196 || strpos( $modelId, '-video' ) !== false
197 || strpos( $modelId, '-image' ) !== false ) {
198 continue;
199 }
200
201 $isEmbedding = strpos( $modelId, 'embed' ) !== false;
202 $isVision = strpos( $modelId, 'vision' ) !== false
203 || ( isset( $remote['input_modalities'] ) && is_array( $remote['input_modalities'] )
204 && in_array( 'image', $remote['input_modalities'], true ) );
205 // Reasoning detection: explicit "non-reasoning" suffix wins over the family default.
206 $isReasoning = false;
207 if ( strpos( $modelId, 'non-reasoning' ) !== false ) {
208 $isReasoning = false;
209 }
210 elseif ( strpos( $modelId, 'reasoning' ) !== false ) {
211 $isReasoning = true;
212 }
213 elseif ( strpos( $modelId, 'grok-4' ) !== false || strpos( $modelId, 'grok-3-mini' ) !== false ) {
214 $isReasoning = true;
215 }
216
217 $tags = [ 'core', $isEmbedding ? 'embedding' : 'chat' ];
218 if ( !$isEmbedding ) {
219 $tags[] = 'functions';
220 }
221 if ( $isVision ) {
222 $tags[] = 'vision';
223 }
224 if ( $isReasoning ) {
225 $tags[] = 'reasoning';
226 }
227
228 $features = $isEmbedding ? [ 'embedding' ] : [ 'completion' ];
229 if ( !$isEmbedding ) {
230 $features[] = 'functions';
231 }
232
233 $maxContext = isset( $remote['context_window'] ) ? (int) $remote['context_window']
234 : ( strpos( $modelId, 'grok-4' ) !== false ? 256000 : 131072 );
235 $maxCompletion = isset( $remote['max_output_tokens'] ) ? (int) $remote['max_output_tokens'] : 16384;
236
237 $price = $this->estimate_pricing( $modelId, $remote );
238
239 $modelData = [
240 'model' => $modelId,
241 'name' => $this->generate_human_readable_name( $modelId ),
242 'family' => 'grok',
243 'features' => $features,
244 'price' => $price,
245 'type' => 'token',
246 'unit' => 1 / 1000000,
247 'maxCompletionTokens' => $maxCompletion,
248 'maxContextualTokens' => $maxContext,
249 'tags' => $tags,
250 ];
251
252 if ( $isEmbedding ) {
253 $modelData['dimensions'] = isset( $remote['dimensions'] ) ? (int) $remote['dimensions'] : 1024;
254 }
255
256 $models[] = $modelData;
257 }
258
259 return $models;
260 }
261 catch ( Exception $e ) {
262 Meow_MWAI_Logging::error( 'xAI: Failed to retrieve models: ' . $e->getMessage() );
263 return [];
264 }
265 }
266
267 /**
268 * Connection check for the xAI API.
269 *
270 * We hit /v1/models directly here (rather than going through retrieve_models()) so the user
271 * sees actual error messages from xAI (e.g. "team has no credits") instead of an empty
272 * model list that looks like success.
273 */
274 public function connection_check() {
275 $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
276 $url = rtrim( $endpoint, '/' ) . '/models';
277 $details = [ 'endpoint' => $url ];
278
279 if ( empty( $this->apiKey ) ) {
280 return [
281 'success' => false, 'service' => 'xAI',
282 'error' => 'No xAI API Key configured.',
283 'details' => $details,
284 ];
285 }
286
287 $response = wp_remote_get( $url, [
288 'headers' => [ 'Authorization' => 'Bearer ' . $this->apiKey, 'User-Agent' => 'AI Engine' ],
289 'timeout' => 10, 'sslverify' => MWAI_SSL_VERIFY,
290 ] );
291
292 if ( is_wp_error( $response ) ) {
293 return [
294 'success' => false, 'service' => 'xAI',
295 'error' => $response->get_error_message(),
296 'details' => $details,
297 ];
298 }
299
300 $code = wp_remote_retrieve_response_code( $response );
301 $body = json_decode( wp_remote_retrieve_body( $response ), true );
302
303 if ( $code >= 400 || ( is_array( $body ) && isset( $body['error'] ) ) ) {
304 $message = is_array( $body ) && isset( $body['error'] )
305 ? ( is_string( $body['error'] ) ? $body['error'] : json_encode( $body['error'] ) )
306 : "HTTP {$code} from xAI.";
307 return [
308 'success' => false, 'service' => 'xAI',
309 'error' => $message,
310 'details' => array_merge( $details, [ 'http_code' => $code ] ),
311 ];
312 }
313
314 $modelIds = [];
315 if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
316 foreach ( array_slice( $body['data'], 0, 5 ) as $m ) {
317 if ( isset( $m['id'] ) ) {
318 $modelIds[] = $m['id'];
319 }
320 }
321 }
322 return [
323 'success' => true, 'service' => 'xAI',
324 'message' => 'Connection successful. Found ' . count( $body['data'] ?? [] ) . ' models.',
325 'details' => array_merge( $details, [
326 'model_count' => count( $body['data'] ?? [] ),
327 'sample_models' => $modelIds,
328 ] ),
329 ];
330 }
331 }
332