File: /var/www/html/wp-content/plugins/ai-engine/classes/engines/mistral.php
<?php
class Meow_MWAI_Engines_Mistral extends Meow_MWAI_Engines_ChatML {
public function __construct( $core, $env ) {
parent::__construct( $core, $env );
}
protected function set_environment() {
$env = $this->env;
$this->apiKey = $env['apikey'] ?? null;
}
protected function get_service_name() {
return 'Mistral';
}
public function get_models() {
// Return dynamically fetched models only
return $this->core->get_engine_models( 'mistral' );
}
public static function get_models_static() {
return MWAI_MISTRAL_MODELS;
}
protected function build_url( $query, $endpoint = null ) {
$endpoint = apply_filters( 'mwai_mistral_endpoint', 'https://api.mistral.ai/v1', $this->env );
if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
return $endpoint . '/chat/completions';
}
else if ( $query instanceof Meow_MWAI_Query_Embed ) {
return $endpoint . '/embeddings';
}
else {
throw new Exception( 'Unsupported query type for Mistral.' );
}
}
protected function build_headers( $query ) {
if ( $query->apiKey ) {
$this->apiKey = $query->apiKey;
}
if ( empty( $this->apiKey ) ) {
throw new Exception( 'No Mistral API Key provided. Please check your settings.' );
}
return [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->apiKey,
'User-Agent' => 'AI Engine',
];
}
protected function build_messages( $query ) {
$messages = parent::build_messages( $query );
// For feedback queries with tool results, ensure proper format for Mistral
if ( $query instanceof Meow_MWAI_Query_Feedback ) {
foreach ( $messages as &$message ) {
// Mistral expects tool messages to have specific format
if ( isset( $message['role'] ) && $message['role'] === 'tool' ) {
// Ensure content is never empty
if ( empty( $message['content'] ) ) {
$message['content'] = json_encode( [ 'result' => 'success' ] );
}
// Ensure content is a string (Mistral requirement)
if ( !is_string( $message['content'] ) ) {
$message['content'] = json_encode( $message['content'] );
}
}
}
}
return $messages;
}
protected function build_body( $query, $streamCallback = null, $extra = null ) {
// Use parent's build_body for standard ChatML format
$body = parent::build_body( $query, $streamCallback, $extra );
// Mistral embedding models don't support the dimensions parameter
if ( $query instanceof Meow_MWAI_Query_Embed ) {
unset( $body['dimensions'] );
return $body;
}
// Mistral uses 'max_tokens' instead of 'max_completion_tokens'
if ( isset( $body['max_completion_tokens'] ) ) {
$body['max_tokens'] = $body['max_completion_tokens'];
unset( $body['max_completion_tokens'] );
}
// TEMPORARILY DISABLED: Function calling for Mistral
// Remove tools/functions from the request until feedback loop is properly debugged
if ( isset( $body['tools'] ) ) {
unset( $body['tools'] );
}
if ( isset( $body['tool_choice'] ) ) {
unset( $body['tool_choice'] );
}
return $body;
}
/**
* Generate a human-readable name from model ID
* Based on Mistral's official naming conventions
*/
private function generate_human_readable_name( $modelId ) {
// Extract version from model ID (e.g., "2508" becomes "25.08")
$versionMatch = [];
preg_match( '/(\d{4})$/', $modelId, $versionMatch );
$version = isset( $versionMatch[1] ) ?
substr( $versionMatch[1], 0, 2 ) . '.' . substr( $versionMatch[1], 2 ) : '';
// Handle special cases for latest versions
if ( strpos( $modelId, '-latest' ) !== false ) {
$modelId = str_replace( '-latest', '', $modelId );
$version = 'Latest';
}
// Build the base name
$name = '';
// Magistral models (reasoning)
if ( strpos( $modelId, 'magistral' ) !== false ) {
if ( strpos( $modelId, 'medium' ) !== false ) {
$name = 'Magistral Medium';
}
else if ( strpos( $modelId, 'small' ) !== false ) {
$name = 'Magistral Small';
}
// Add version number for Magistral
// No version suffix for latest models
}
// Mistral models
else if ( strpos( $modelId, 'mistral' ) !== false ) {
if ( strpos( $modelId, 'large' ) !== false ) {
$name = 'Mistral Large';
}
else if ( strpos( $modelId, 'medium' ) !== false ) {
$name = 'Mistral Medium';
}
else if ( strpos( $modelId, 'small' ) !== false ) {
$name = 'Mistral Small';
}
else if ( strpos( $modelId, 'saba' ) !== false ) {
$name = 'Mistral Saba';
}
else if ( strpos( $modelId, 'tiny' ) !== false || strpos( $modelId, 'nemo' ) !== false ) {
$name = 'Mistral Nemo';
}
else if ( strpos( $modelId, 'embed' ) !== false ) {
$name = 'Mistral Embed';
}
}
// Pixtral models (vision)
else if ( strpos( $modelId, 'pixtral' ) !== false ) {
if ( strpos( $modelId, 'large' ) !== false ) {
$name = 'Pixtral Large';
}
else if ( strpos( $modelId, '12b' ) !== false ) {
$name = 'Pixtral 12B';
}
// No (Latest) suffix needed
}
// Codestral models (code)
else if ( strpos( $modelId, 'codestral' ) !== false ) {
if ( strpos( $modelId, 'embed' ) !== false ) {
$name = 'Codestral Embed';
}
else {
$name = 'Codestral';
// No version suffix for Codestral
}
}
// Devstral models (dev tools)
else if ( strpos( $modelId, 'devstral' ) !== false ) {
if ( strpos( $modelId, 'medium' ) !== false ) {
$name = 'Devstral Medium';
}
else if ( strpos( $modelId, 'small' ) !== false ) {
$name = 'Devstral Small';
// No version suffix for Devstral
}
// No (Latest) suffix needed
}
// Ministral models (edge)
else if ( strpos( $modelId, 'ministral' ) !== false ) {
if ( strpos( $modelId, '8b' ) !== false ) {
$name = 'Ministral 8B';
}
else if ( strpos( $modelId, '3b' ) !== false ) {
$name = 'Ministral 3B';
}
// No (Latest) suffix needed
}
// Voxtral models (audio)
else if ( strpos( $modelId, 'voxtral' ) !== false ) {
if ( strpos( $modelId, 'small' ) !== false ) {
$name = 'Voxtral Small';
}
else if ( strpos( $modelId, 'mini' ) !== false ) {
$name = 'Voxtral Mini';
}
if ( strpos( $modelId, 'transcribe' ) !== false ) {
$name .= ' Transcribe';
}
}
// Open models
else if ( strpos( $modelId, 'open-' ) === 0 ) {
if ( strpos( $modelId, 'mistral-7b' ) !== false ) {
$name = 'Mistral 7B (Open)';
}
else if ( strpos( $modelId, 'mistral-nemo' ) !== false ) {
$name = 'Mistral Nemo (Open)';
}
else if ( strpos( $modelId, 'mixtral-8x7b' ) !== false ) {
$name = 'Mixtral 8x7B (Open)';
}
else if ( strpos( $modelId, 'mixtral-8x22b' ) !== false ) {
$name = 'Mixtral 8x22B (Open)';
}
}
// Fallback to cleaned model ID if no pattern matches
if ( empty( $name ) ) {
$name = ucwords( str_replace( ['-', '_'], ' ', $modelId ) );
}
return $name;
}
/**
* Retrieve the models from Mistral API
* Mistral supports a models endpoint similar to OpenAI
*/
public function retrieve_models() {
try {
$endpoint = apply_filters( 'mwai_mistral_endpoint', 'https://api.mistral.ai/v1', $this->env );
$url = $endpoint . '/models';
if ( empty( $this->apiKey ) ) {
throw new Exception( 'No Mistral API Key provided for model retrieval.' );
}
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'User-Agent' => 'AI Engine'
],
'timeout' => 10,
'sslverify' => MWAI_SSL_VERIFY
];
$response = wp_remote_get( $url, $options );
if ( is_wp_error( $response ) ) {
throw new Exception( 'AI Engine: ' . $response->get_error_message() );
}
$body = json_decode( $response['body'], true );
// Debug: Log the complete models response from Mistral
// error_log( "AI Engine: Mistral Models Response:\n" . print_r( $body, true ) );
if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
throw new Exception( 'AI Engine: Invalid response for Mistral models list.' );
}
$models = [];
$seenModels = []; // Track models we've already added to avoid duplicates
foreach ( $body['data'] as $model ) {
$modelId = $model['id'] ?? '';
// Generate human-readable name based on model ID
$modelName = $this->generate_human_readable_name( $modelId );
// Skip if we've already seen this model name (to avoid alias duplicates)
if ( isset( $seenModels[$modelName] ) ) {
continue;
}
// Skip specialized models that shouldn't appear in general chat lists
// These are models for specific tasks like moderation, OCR, transcription
$skipPatterns = [
'moderation', // Moderation models
'ocr', // OCR-specific models
'transcribe', // Transcription-specific models
];
$shouldSkip = false;
foreach ( $skipPatterns as $pattern ) {
if ( strpos( $modelId, $pattern ) !== false ) {
$shouldSkip = true;
break;
}
}
if ( $shouldSkip ) {
continue;
}
// Skip models that are just aliases (they appear in other model's aliases array)
// We'll keep the primary model, not the alias entries
$isAlias = false;
if ( isset( $model['aliases'] ) && is_array( $model['aliases'] ) && count( $model['aliases'] ) > 0 ) {
// If this model ID appears in its own aliases, it's likely an alias entry
foreach ( $model['aliases'] as $alias ) {
if ( $alias !== $modelId && isset( $seenModels[$alias] ) ) {
$isAlias = true;
break;
}
}
}
if ( $isAlias ) {
continue;
}
// Set defaults based on model type
$maxCompletionTokens = 32768;
$maxContextualTokens = 128000;
$features = ['completion'];
$tags = ['core', 'chat'];
// Parse capabilities from the API response
$capabilities = $model['capabilities'] ?? [];
// TEMPORARILY DISABLED: Function calling tags
// Not adding 'functions' tag since function calling is disabled for Mistral
// if ( in_array( 'function_calling', $capabilities ) ||
// ( isset( $model['supports_tool_choice'] ) && $model['supports_tool_choice'] ) ) {
// $tags[] = 'functions';
// $features[] = 'functions';
// }
// Check for vision capability
if ( in_array( 'vision', $capabilities ) ) {
$tags[] = 'vision';
}
// Check for embeddings capability
$dimensions = null;
if ( strpos( $modelId, 'embed' ) !== false ) {
// Skip only the dated legacy version
if ( $modelId === 'mistral-embed-2312' ) {
continue;
}
$features = ['embedding'];
$tags = ['core', 'embedding'];
// Set dimensions based on model type
// mistral-embed: 1024 dimensions (fixed)
// codestral-embed: 3072 dimensions (fixed)
if ( strpos( $modelId, 'codestral' ) !== false ) {
$dimensions = 3072;
}
else {
$dimensions = 1024;
}
}
// Check for audio capability (voxtral models for chat, not transcription)
$capabilities = $model['capabilities'] ?? [];
if ( isset( $capabilities['audio'] ) && $capabilities['audio'] &&
strpos( $modelId, 'transcribe' ) === false ) {
$tags[] = 'audio';
}
// Use max_tokens if available
if ( isset( $model['max_tokens'] ) ) {
$maxCompletionTokens = (int) $model['max_tokens'];
}
// Use context_length if available
if ( isset( $model['max_context_length'] ) ) {
$maxContextualTokens = (int) $model['max_context_length'];
}
else if ( isset( $model['context_window'] ) ) {
$maxContextualTokens = (int) $model['context_window'];
}
// Determine pricing based on model (prices per million tokens)
$priceIn = 0;
$priceOut = 0;
// Updated Mistral pricing (as of 2025)
if ( strpos( $modelId, 'magistral' ) !== false ) {
// Magistral reasoning models
if ( strpos( $modelId, 'medium' ) !== false ) {
$priceIn = 4.00;
$priceOut = 12.00;
}
else {
$priceIn = 2.00;
$priceOut = 6.00;
}
}
else if ( strpos( $modelId, 'mistral-large' ) !== false || strpos( $modelId, 'pixtral-large' ) !== false ) {
$priceIn = 3.00;
$priceOut = 9.00;
}
else if ( strpos( $modelId, 'mistral-medium' ) !== false ) {
$priceIn = 2.70;
$priceOut = 8.10;
}
else if ( strpos( $modelId, 'mistral-small' ) !== false ) {
$priceIn = 1.00;
$priceOut = 3.00;
}
else if ( strpos( $modelId, 'codestral' ) !== false ) {
if ( strpos( $modelId, '2501' ) !== false || strpos( $modelId, '2508' ) !== false ) {
$priceIn = 0.30;
$priceOut = 0.90;
}
else {
$priceIn = 1.00;
$priceOut = 3.00;
}
}
else if ( strpos( $modelId, 'devstral' ) !== false ) {
$priceIn = 0.50;
$priceOut = 1.50;
}
else if ( strpos( $modelId, 'ministral' ) !== false ) {
$priceIn = 0.10;
$priceOut = 0.10;
}
else if ( strpos( $modelId, 'pixtral-12b' ) !== false ) {
$priceIn = 0.15;
$priceOut = 0.15;
}
else if ( strpos( $modelId, 'voxtral' ) !== false ) {
$priceIn = 0.50;
$priceOut = 1.50;
}
else if ( strpos( $modelId, 'mistral-saba' ) !== false ) {
$priceIn = 0.20;
$priceOut = 0.60;
}
else if ( strpos( $modelId, 'open-mistral' ) !== false || strpos( $modelId, 'mistral-tiny' ) !== false ) {
$priceIn = 0.15;
$priceOut = 0.15;
}
else if ( strpos( $modelId, 'open-mixtral-8x7b' ) !== false ) {
$priceIn = 0.50;
$priceOut = 0.50;
}
else if ( strpos( $modelId, 'open-mixtral-8x22b' ) !== false ) {
$priceIn = 0.90;
$priceOut = 0.90;
}
else if ( strpos( $modelId, 'embed' ) !== false ) {
$priceIn = 0.10;
$priceOut = 0.00;
}
else {
// Default pricing for unknown models
$priceIn = 1.00;
$priceOut = 3.00;
}
// Only include latest models and key open-source versions
// This keeps the list clean and manageable
$preferredModels = [
// Latest versions (primary models)
'mistral-large-latest',
'mistral-medium-latest',
'mistral-small-latest',
'mistral-tiny-latest',
'mistral-saba-latest',
'pixtral-large-latest',
'pixtral-12b-latest',
'codestral-latest',
'devstral-small-latest',
'devstral-medium-latest',
'magistral-medium-latest',
'magistral-small-latest',
'voxtral-small-latest',
'voxtral-mini-latest',
'ministral-3b-latest',
'ministral-8b-latest',
// Open-source models (always include)
'open-mistral-7b',
'open-mistral-nemo',
'open-mixtral-8x7b',
'open-mixtral-8x22b'
];
// We're focusing on latest versions, so no versioned models
$versionedModels = [];
// Check if this is a model we want to include
$includeModel = in_array( $modelId, $preferredModels ) ||
in_array( $modelId, $versionedModels ) ||
strpos( $modelId, 'embed' ) !== false; // Always include embedding models
if ( !$includeModel ) {
continue;
}
// Mark this model as seen (after confirming it will be included)
$seenModels[$modelName] = true;
$modelData = [
'model' => $modelId,
'name' => $modelName,
'family' => 'mistral',
'features' => $features,
'price' => [
'in' => $priceIn,
'out' => $priceOut,
],
'type' => 'token',
'unit' => 1 / 1000000,
'maxCompletionTokens' => $maxCompletionTokens,
'maxContextualTokens' => $maxContextualTokens,
'tags' => $tags,
];
// Add dimensions for embedding models (fixed, not configurable)
if ( $dimensions !== null ) {
$modelData['dimensions'] = $dimensions;
}
$models[] = $modelData;
}
return $models;
}
catch ( Exception $e ) {
Meow_MWAI_Logging::error( 'Mistral: Failed to retrieve models: ' . $e->getMessage() );
// Return empty array on error - models must be fetched from API
return [];
}
}
/**
* Connection check for Mistral API
* Tests the API key by listing models
*/
public function connection_check() {
try {
// Use the retrieve_models method to check connection
$models = $this->retrieve_models();
if ( !is_array( $models ) ) {
throw new Exception( 'Invalid response format from Mistral' );
}
$modelCount = count( $models );
$availableModels = [];
// Get first 5 models for display
$displayModels = array_slice( $models, 0, 5 );
foreach ( $displayModels as $model ) {
if ( isset( $model['model'] ) ) {
$availableModels[] = $model['model'];
}
}
return [
'success' => true,
'service' => 'Mistral',
'message' => "Connection successful. Found {$modelCount} models.",
'details' => [
'endpoint' => 'https://api.mistral.ai/v1/models',
'model_count' => $modelCount,
'sample_models' => $availableModels
]
];
}
catch ( Exception $e ) {
return [
'success' => false,
'service' => 'Mistral',
'error' => $e->getMessage(),
'details' => [
'endpoint' => 'https://api.mistral.ai/v1/models'
]
];
}
}
}