HEX
Server: Apache/2.4.65 (Debian)
System: Linux 88f31f35b0b8 6.1.0-38-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.147-1 (2025-08-02) x86_64
User: www-data (33)
PHP: 8.2.29
Disabled: NONE
Upload Files
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'
        ]
      ];
    }
  }
}