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/xai.php
<?php

class Meow_MWAI_Engines_XAI 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 'xAI';
  }

  public function get_models() {
    // Prefer dynamically-fetched models when available (the env was synced against /v1/models).
    // Fall back to the static list so users on accounts that haven't synced (or that errored out
    // during sync) can still resolve well-known Grok model ids.
    $dynamic = $this->core->get_engine_models( 'xai' );
    if ( !empty( $dynamic ) ) {
      return $dynamic;
    }
    return apply_filters( 'mwai_xai_models', MWAI_XAI_MODELS );
  }

  public static function get_models_static() {
    return MWAI_XAI_MODELS;
  }

  protected function build_url( $query, $endpoint = null ) {
    $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
    $endpoint = rtrim( $endpoint, '/' );
    if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
      return $endpoint . '/chat/completions';
    }
    if ( $query instanceof Meow_MWAI_Query_Embed ) {
      return $endpoint . '/embeddings';
    }
    throw new Exception( 'Unsupported query type for xAI.' );
  }

  protected function build_headers( $query ) {
    if ( $query->apiKey ) {
      $this->apiKey = $query->apiKey;
    }
    if ( empty( $this->apiKey ) ) {
      throw new Exception( 'No xAI API Key provided. Please check your settings.' );
    }
    return [
      'Content-Type' => 'application/json',
      'Authorization' => 'Bearer ' . $this->apiKey,
      'User-Agent' => 'AI Engine',
    ];
  }

  protected function build_body( $query, $streamCallback = null, $extra = null ) {
    $body = parent::build_body( $query, $streamCallback, $extra );

    // xAI follows OpenAI's older Chat Completions schema: it expects max_tokens,
    // not max_completion_tokens.
    if ( isset( $body['max_completion_tokens'] ) ) {
      $body['max_tokens'] = $body['max_completion_tokens'];
      unset( $body['max_completion_tokens'] );
    }

    return $body;
  }

  /**
   * Map an xAI model id to a human-readable display name.
   * xAI ids vary in shape: grok-4, grok-4-0709 (date-stamped), grok-4-1-fast-reasoning,
   * grok-4.20-0309-non-reasoning, grok-4.3 (dot-versioned). We try to keep variants
   * distinguishable in the model dropdown without leaking internal date stamps as version numbers.
   */
  private function generate_human_readable_name( $modelId ) {
    if ( strpos( $modelId, 'grok-code' ) !== false ) {
      return 'Grok Code Fast';
    }

    // Grok 4 variants — try to capture both dotted ("4.20", "4.3") and dashed ("4-1") versions.
    if ( strpos( $modelId, 'grok-4' ) === 0 ) {
      $name = 'Grok 4';
      if ( preg_match( '/^grok-4\.(\d+)/', $modelId, $m ) ) {
        $name = 'Grok 4.' . $m[1];
      }
      elseif ( preg_match( '/^grok-4-(\d{1,2})(?:-|$)/', $modelId, $m ) ) {
        // Treat trailing 4-digit groups as date stamps, not versions.
        if ( strlen( $m[1] ) <= 2 ) {
          $name = 'Grok 4.' . $m[1];
        }
      }
      if ( strpos( $modelId, 'heavy' ) !== false ) { $name .= ' Heavy'; }
      elseif ( strpos( $modelId, 'fast' ) !== false ) { $name .= ' Fast'; }
      if ( strpos( $modelId, 'multi-agent' ) !== false ) { $name .= ' Multi-Agent'; }
      if ( strpos( $modelId, 'non-reasoning' ) !== false ) {
        $name .= ' (Standard)';
      }
      elseif ( strpos( $modelId, 'reasoning' ) !== false ) {
        $name .= ' (Reasoning)';
      }
      elseif ( preg_match( '/-(\d{4})$/', $modelId, $m ) ) {
        // Date-stamped release like grok-4-0709 → "Grok 4 (0709)".
        $name .= ' (' . $m[1] . ')';
      }
      return $name;
    }

    if ( strpos( $modelId, 'grok-3-mini' ) !== false ) { return 'Grok 3 Mini'; }
    if ( strpos( $modelId, 'grok-3' ) !== false ) { return 'Grok 3'; }
    if ( strpos( $modelId, 'grok-2-vision' ) !== false ) { return 'Grok 2 Vision'; }
    if ( strpos( $modelId, 'grok-2' ) !== false ) { return 'Grok 2'; }

    return ucwords( str_replace( [ '-', '_' ], ' ', $modelId ) );
  }

  /**
   * Estimate per-million-token pricing for an xAI model. xAI's /models endpoint
   * returns prices in cents per token, but the field is not always populated, so
   * we keep an internal mapping as a fallback.
   */
  private function estimate_pricing( $modelId, $remote ) {
    if ( isset( $remote['prompt_text_token_price'] ) && isset( $remote['completion_text_token_price'] ) ) {
      // xAI returns prices in cents per token. Convert to USD per million tokens.
      $in = ( (float) $remote['prompt_text_token_price'] ) / 100 * 1000000;
      $out = ( (float) $remote['completion_text_token_price'] ) / 100 * 1000000;
      return [ 'in' => round( $in, 4 ), 'out' => round( $out, 4 ) ];
    }
    if ( strpos( $modelId, 'grok-4-heavy' ) !== false ) {
      return [ 'in' => 5.00, 'out' => 25.00 ];
    }
    if ( strpos( $modelId, 'grok-4-fast' ) !== false ) {
      return [ 'in' => 0.20, 'out' => 0.50 ];
    }
    if ( strpos( $modelId, 'grok-4' ) !== false ) {
      return [ 'in' => 3.00, 'out' => 15.00 ];
    }
    if ( strpos( $modelId, 'grok-code' ) !== false ) {
      return [ 'in' => 0.20, 'out' => 1.50 ];
    }
    if ( strpos( $modelId, 'grok-3-mini' ) !== false ) {
      return [ 'in' => 0.30, 'out' => 0.50 ];
    }
    if ( strpos( $modelId, 'grok-3' ) !== false ) {
      return [ 'in' => 3.00, 'out' => 15.00 ];
    }
    return [ 'in' => 1.00, 'out' => 3.00 ];
  }

  /**
   * Retrieve the available models from xAI's /v1/models endpoint.
   */
  public function retrieve_models() {
    try {
      $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
      $url = rtrim( $endpoint, '/' ) . '/models';

      if ( empty( $this->apiKey ) ) {
        throw new Exception( 'No xAI 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 );
      if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
        throw new Exception( 'AI Engine: Invalid response for xAI models list.' );
      }

      $models = [];
      $seen = [];
      foreach ( $body['data'] as $remote ) {
        $modelId = $remote['id'] ?? '';
        if ( empty( $modelId ) || isset( $seen[$modelId] ) ) {
          continue;
        }
        $seen[$modelId] = true;

        // Skip image/video generation models — they don't speak the chat-completions schema
        // and would only confuse users showing up in chat dropdowns. xAI exposes them under
        // separate endpoints (/v1/images, /v1/videos) that AI Engine doesn't route here yet.
        if ( strpos( $modelId, 'grok-2-image' ) !== false
          || strpos( $modelId, 'grok-imagine' ) !== false
          || strpos( $modelId, '-video' ) !== false
          || strpos( $modelId, '-image' ) !== false ) {
          continue;
        }

        $isEmbedding = strpos( $modelId, 'embed' ) !== false;
        $isVision = strpos( $modelId, 'vision' ) !== false
          || ( isset( $remote['input_modalities'] ) && is_array( $remote['input_modalities'] )
               && in_array( 'image', $remote['input_modalities'], true ) );
        // Reasoning detection: explicit "non-reasoning" suffix wins over the family default.
        $isReasoning = false;
        if ( strpos( $modelId, 'non-reasoning' ) !== false ) {
          $isReasoning = false;
        }
        elseif ( strpos( $modelId, 'reasoning' ) !== false ) {
          $isReasoning = true;
        }
        elseif ( strpos( $modelId, 'grok-4' ) !== false || strpos( $modelId, 'grok-3-mini' ) !== false ) {
          $isReasoning = true;
        }

        $tags = [ 'core', $isEmbedding ? 'embedding' : 'chat' ];
        if ( !$isEmbedding ) {
          $tags[] = 'functions';
        }
        if ( $isVision ) {
          $tags[] = 'vision';
        }
        if ( $isReasoning ) {
          $tags[] = 'reasoning';
        }

        $features = $isEmbedding ? [ 'embedding' ] : [ 'completion' ];
        if ( !$isEmbedding ) {
          $features[] = 'functions';
        }

        $maxContext = isset( $remote['context_window'] ) ? (int) $remote['context_window']
          : ( strpos( $modelId, 'grok-4' ) !== false ? 256000 : 131072 );
        $maxCompletion = isset( $remote['max_output_tokens'] ) ? (int) $remote['max_output_tokens'] : 16384;

        $price = $this->estimate_pricing( $modelId, $remote );

        $modelData = [
          'model' => $modelId,
          'name' => $this->generate_human_readable_name( $modelId ),
          'family' => 'grok',
          'features' => $features,
          'price' => $price,
          'type' => 'token',
          'unit' => 1 / 1000000,
          'maxCompletionTokens' => $maxCompletion,
          'maxContextualTokens' => $maxContext,
          'tags' => $tags,
        ];

        if ( $isEmbedding ) {
          $modelData['dimensions'] = isset( $remote['dimensions'] ) ? (int) $remote['dimensions'] : 1024;
        }

        $models[] = $modelData;
      }

      return $models;
    }
    catch ( Exception $e ) {
      Meow_MWAI_Logging::error( 'xAI: Failed to retrieve models: ' . $e->getMessage() );
      return [];
    }
  }

  /**
   * Connection check for the xAI API.
   *
   * We hit /v1/models directly here (rather than going through retrieve_models()) so the user
   * sees actual error messages from xAI (e.g. "team has no credits") instead of an empty
   * model list that looks like success.
   */
  public function connection_check() {
    $endpoint = apply_filters( 'mwai_xai_endpoint', 'https://api.x.ai/v1', $this->env );
    $url = rtrim( $endpoint, '/' ) . '/models';
    $details = [ 'endpoint' => $url ];

    if ( empty( $this->apiKey ) ) {
      return [
        'success' => false, 'service' => 'xAI',
        'error' => 'No xAI API Key configured.',
        'details' => $details,
      ];
    }

    $response = wp_remote_get( $url, [
      'headers' => [ 'Authorization' => 'Bearer ' . $this->apiKey, 'User-Agent' => 'AI Engine' ],
      'timeout' => 10, 'sslverify' => MWAI_SSL_VERIFY,
    ] );

    if ( is_wp_error( $response ) ) {
      return [
        'success' => false, 'service' => 'xAI',
        'error' => $response->get_error_message(),
        'details' => $details,
      ];
    }

    $code = wp_remote_retrieve_response_code( $response );
    $body = json_decode( wp_remote_retrieve_body( $response ), true );

    if ( $code >= 400 || ( is_array( $body ) && isset( $body['error'] ) ) ) {
      $message = is_array( $body ) && isset( $body['error'] )
        ? ( is_string( $body['error'] ) ? $body['error'] : json_encode( $body['error'] ) )
        : "HTTP {$code} from xAI.";
      return [
        'success' => false, 'service' => 'xAI',
        'error' => $message,
        'details' => array_merge( $details, [ 'http_code' => $code ] ),
      ];
    }

    $modelIds = [];
    if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
      foreach ( array_slice( $body['data'], 0, 5 ) as $m ) {
        if ( isset( $m['id'] ) ) {
          $modelIds[] = $m['id'];
        }
      }
    }
    return [
      'success' => true, 'service' => 'xAI',
      'message' => 'Connection successful. Found ' . count( $body['data'] ?? [] ) . ' models.',
      'details' => array_merge( $details, [
        'model_count' => count( $body['data'] ?? [] ),
        'sample_models' => $modelIds,
      ] ),
    ];
  }
}