<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * AI プロバイダ呼び出し層: capabilities / モデル一覧 / API 呼び出し
 *
 * 元 includes/class-aiap-ai-client.php から分離。
 * Claude / OpenAI / Gemini / OpenAI-compatible 4プロバイダの実装を集約。
 * 共通基盤 (get_settings 等) は親クラス AIAP_AI_Client にあるため直接参照する。
 */
class AIAP_AI_Providers {

    // ================================================================
    // Capabilities / Endpoints / Key fields
    // ================================================================

    /**
     * プロバイダ×モデルごとの機能サポートマトリクス
     *
     * OpenAI系は近年 max_tokens → max_completion_tokens への移行や、
     * Reasoning モデル(o1/o3/o4/gpt-5 系)固有の制約があるため、
     * モデルIDから系統を判定してAPIリクエストを動的に組み立てる必要がある。
     *
     * @param string $provider プロバイダキー
     * @param string $model    モデルID (空なら既定値)
     * @return array {
     *     @type string $token_param              トークン上限パラメータ名 ('max_tokens' | 'max_completion_tokens' | '')
     *     @type bool   $supports_system_message  system ロールを受け付けるか
     *     @type string $family                   API系統 (openai-chat / openai-reasoning / claude / gemini / openai-compatible)
     * }
     */
    public static function get_model_capabilities( $provider, $model = '' ) {
        if ( $model === '' ) {
            $settings = AIAP_AI_Client::get_settings();
            $model    = $settings[ $provider . '_model' ] ?? '';
        }

        if ( $provider === 'claude' ) {
            return array(
                'token_param'             => 'max_tokens',
                'supports_system_message' => true,
                'family'                  => 'claude',
            );
        }

        if ( $provider === 'gemini' ) {
            // Geminiは独自フォーマット(system_instruction / contents)
            return array(
                'token_param'             => '',
                'supports_system_message' => true,
                'family'                  => 'gemini',
            );
        }

        if ( $provider === 'openai' ) {
            if ( self::is_openai_reasoning_model( $model ) ) {
                return array(
                    'token_param'             => 'max_completion_tokens',
                    'supports_system_message' => true, // 現行のo1/o3/gpt-5はsystemロール可 (developerロール相当として扱われる)
                    'family'                  => 'openai-reasoning',
                );
            }
            return array(
                'token_param'             => 'max_tokens',
                'supports_system_message' => true,
                'family'                  => 'openai-chat',
            );
        }

        // OpenAI互換(Mistral/Groq/DeepSeek/OpenRouter)は各社とも現状 max_tokens を受け付ける
        return array(
            'token_param'             => 'max_tokens',
            'supports_system_message' => true,
            'family'                  => 'openai-compatible',
        );
    }

    /**
     * OpenAIの Reasoning 系モデル(max_completion_tokens 必須)かを判定
     *
     * 判定は AIAP_Model_Registry::is_reasoning_model() に委譲する。
     * レジストリは: (1) 登録済モデルはメタデータ参照、(2) 未登録モデルは
     * OpenAI 系パターン (gpt-5* / o[1-9]*) でフォールバック判定する。
     * これにより fetch_models_from_api() で動的取得したモデルや、ユーザーが
     * 自由入力したモデル ID にも対応できる。
     */
    public static function is_openai_reasoning_model( $model ) {
        return AIAP_Model_Registry::is_reasoning_model( 'openai', $model );
    }

    /**
     * OpenAI互換プロバイダのエンドポイント定義
     */
    private static function get_openai_compatible_endpoints() {
        return array(
            'mistral' => array(
                'url'   => 'https://api.mistral.ai/v1/chat/completions',
                'key'   => 'mistral_api_key',
                'model' => 'mistral_model',
            ),
            'groq' => array(
                'url'   => 'https://api.groq.com/openai/v1/chat/completions',
                'key'   => 'groq_api_key',
                'model' => 'groq_model',
            ),
            'deepseek' => array(
                'url'   => 'https://api.deepseek.com/chat/completions',
                'key'   => 'deepseek_api_key',
                'model' => 'deepseek_model',
            ),
            'openrouter' => array(
                'url'   => 'https://openrouter.ai/api/v1/chat/completions',
                'key'   => 'openrouter_api_key',
                'model' => 'openrouter_model',
            ),
        );
    }

    /**
     * APIキーフィールド一覧
     */
    public static function get_key_fields() {
        return array( 'claude_api_key', 'openai_api_key', 'gemini_api_key', 'mistral_api_key', 'groq_api_key', 'deepseek_api_key', 'openrouter_api_key' );
    }

    /**
     * AI プロバイダ設定スキーマ (既定値)。
     *
     * モデル ID 既定値は AIAP_Model_Registry に集約 (単一情報源)。
     * 各プロバイダの最新世代を反映するときはレジストリ側 1 箇所だけ更新する。
     *
     * @return array<string,mixed>
     */

    // ================================================================
    // Provider / Model listing & caching
    // ================================================================

    /**
     * 全プロバイダ一覧
     */
    public static function get_providers() {
        return array( 'claude', 'openai', 'gemini', 'mistral', 'groq', 'deepseek', 'openrouter' );
    }

    /**
     * 各プロバイダのモデル選択肢
     */
    const MODEL_CACHE_PREFIX = 'aiap_models_';
    const MODEL_CACHE_TTL = DAY_IN_SECONDS;

    /**
     * 指定プロバイダのモデル一覧を返す。キャッシュ(transient)があればそれを優先。
     */
    public static function get_models_for( $provider ) {
        $cached = get_transient( self::MODEL_CACHE_PREFIX . $provider );
        if ( is_array( $cached ) && ! empty( $cached ) ) {
            return $cached;
        }
        $defaults = self::get_model_options();
        return $defaults[ $provider ] ?? array();
    }

    /**
     * 動的取得したモデル一覧をキャッシュに保存
     */
    public static function cache_models( $provider, array $models ) {
        set_transient( self::MODEL_CACHE_PREFIX . $provider, $models, self::MODEL_CACHE_TTL );
    }

    /**
     * 各プロバイダのAPIからモデル一覧を取得
     * @return array|WP_Error array( model_id => label, ... )
     */
    public static function fetch_models_from_api( $provider ) {
        $settings = AIAP_AI_Client::get_settings();
        switch ( $provider ) {
            case 'claude':
                return self::fetch_claude_models( $settings['claude_api_key'] ?? '' );
            case 'openai':
                return self::fetch_openai_models( $settings['openai_api_key'] ?? '', 'text' );
            case 'gemini':
                return self::fetch_gemini_models( $settings['gemini_api_key'] ?? '', 'text' );
            case 'mistral':
                return self::fetch_openai_compatible_models( $settings['mistral_api_key'] ?? '', 'https://api.mistral.ai/v1/models' );
            case 'groq':
                return self::fetch_openai_compatible_models( $settings['groq_api_key'] ?? '', 'https://api.groq.com/openai/v1/models' );
            case 'deepseek':
                return self::fetch_openai_compatible_models( $settings['deepseek_api_key'] ?? '', 'https://api.deepseek.com/v1/models' );
            case 'openrouter':
                return self::fetch_openai_compatible_models( $settings['openrouter_api_key'] ?? '', 'https://openrouter.ai/api/v1/models' );
            // 画像プロバイダ: 画像AIプロバイダ設定のAPIキーを使う
            case 'dalle3':
                if ( class_exists( 'AIAP_Image_Client' ) ) {
                    $image_settings = AIAP_Image_Client::get_settings();
                    return self::fetch_openai_models( $image_settings['image_openai_api_key'] ?? '', 'image' );
                }
                return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
            case 'imagen':
                if ( class_exists( 'AIAP_Image_Client' ) ) {
                    $image_settings = AIAP_Image_Client::get_settings();
                    return self::fetch_gemini_models( $image_settings['image_gemini_api_key'] ?? '', 'image' );
                }
                return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
        }
        /* translators: %s: provider key */
        return new WP_Error( 'unsupported_provider', sprintf( __( 'Dynamic fetch not supported for: %s', 'ai-auto-poster' ), $provider ) );
    }

    private static function fetch_claude_models( $api_key ) {
        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
        }
        $r = AIAP_Http::get( 'https://api.anthropic.com/v1/models', array(
            'x-api-key'         => $api_key,
            'anthropic-version' => '2023-06-01',
        ) );
        if ( is_wp_error( $r ) ) {
            return $r;
        }
        if ( $r['code'] !== 200 ) {
            /* translators: %d: HTTP status code returned by the Claude API */
            return new WP_Error( 'api_error', sprintf( __( 'Claude API: HTTP %d', 'ai-auto-poster' ), $r['code'] ) );
        }
        $data = is_array( $r['json'] ) ? $r['json'] : array();
        if ( empty( $data['data'] ) || ! is_array( $data['data'] ) ) {
            return new WP_Error( 'empty_response', __( 'No models returned', 'ai-auto-poster' ) );
        }
        $result = array();
        foreach ( $data['data'] as $m ) {
            $id = $m['id'] ?? '';
            if ( $id === '' ) continue;
            $name = $m['display_name'] ?? $id;
            $result[ $id ] = $id . ( $name !== $id ? ' — ' . $name : '' );
        }
        return $result;
    }

    private static function fetch_openai_models( $api_key, $kind = 'text' ) {
        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
        }
        $r = AIAP_Http::get( 'https://api.openai.com/v1/models', AIAP_Http::bearer( $api_key ) );
        if ( is_wp_error( $r ) ) {
            return $r;
        }
        if ( $r['code'] !== 200 ) {
            /* translators: %d: HTTP status code returned by the OpenAI API */
            return new WP_Error( 'api_error', sprintf( __( 'OpenAI API: HTTP %d', 'ai-auto-poster' ), $r['code'] ) );
        }
        $data = is_array( $r['json'] ) ? $r['json'] : array();
        if ( empty( $data['data'] ) || ! is_array( $data['data'] ) ) {
            return new WP_Error( 'empty_response', __( 'No models returned', 'ai-auto-poster' ) );
        }
        $result = array();
        foreach ( $data['data'] as $m ) {
            $id = $m['id'] ?? '';
            if ( $id === '' ) continue;
            if ( $kind === 'text' ) {
                // gpt-*, o1/o3/o4系(ハイフン有無問わず), chatgpt-* のうち
                // embedding/audio/moderation/whisper/tts 系を除外
                if ( ! preg_match( '/^(gpt-|chatgpt-|o[1-9](?:$|[-\d]))/', $id ) ) continue;
                if ( preg_match( '/(embedding|audio|whisper|tts|moderation|realtime|transcribe|search)/i', $id ) ) continue;
            } else {
                // image: dall-e-*, gpt-image-*
                if ( ! preg_match( '/^(dall-e-|gpt-image-)/', $id ) ) continue;
            }
            $result[ $id ] = $id;
        }
        ksort( $result );
        return $result;
    }

    private static function fetch_gemini_models( $api_key, $kind = 'text' ) {
        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
        }
        $url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . rawurlencode( $api_key );
        $r = AIAP_Http::get( $url );
        if ( is_wp_error( $r ) ) {
            return $r;
        }
        if ( $r['code'] !== 200 ) {
            /* translators: %d: HTTP status code returned by the Gemini API */
            return new WP_Error( 'api_error', sprintf( __( 'Gemini API: HTTP %d', 'ai-auto-poster' ), $r['code'] ) );
        }
        $data = is_array( $r['json'] ) ? $r['json'] : array();
        if ( empty( $data['models'] ) || ! is_array( $data['models'] ) ) {
            return new WP_Error( 'empty_response', __( 'No models returned', 'ai-auto-poster' ) );
        }
        $result = array();
        foreach ( $data['models'] as $m ) {
            $name = $m['name'] ?? '';
            if ( strpos( $name, 'models/' ) === 0 ) {
                $id = substr( $name, 7 );
            } else {
                $id = $name;
            }
            if ( $id === '' ) continue;
            $methods = $m['supportedGenerationMethods'] ?? array();
            if ( $kind === 'text' ) {
                if ( strpos( $id, 'imagen' ) === 0 ) continue;
                if ( strpos( $id, 'embedding' ) !== false ) continue;
                if ( strpos( $id, 'aqa' ) !== false ) continue;
                if ( ! in_array( 'generateContent', $methods, true ) ) continue;
            } else {
                if ( strpos( $id, 'imagen' ) !== 0 ) continue;
            }
            $result[ $id ] = $id;
        }
        ksort( $result );
        return $result;
    }

    private static function fetch_openai_compatible_models( $api_key, $url ) {
        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'API key is required', 'ai-auto-poster' ) );
        }
        $r = AIAP_Http::get( $url, AIAP_Http::bearer( $api_key ) );
        if ( is_wp_error( $r ) ) {
            return $r;
        }
        if ( $r['code'] !== 200 ) {
            /* translators: %d: HTTP status code returned by the upstream API */
            return new WP_Error( 'api_error', sprintf( __( 'HTTP %d', 'ai-auto-poster' ), $r['code'] ) );
        }
        $data = is_array( $r['json'] ) ? $r['json'] : array();
        $items = $data['data'] ?? ( $data['models'] ?? array() );
        if ( empty( $items ) || ! is_array( $items ) ) {
            return new WP_Error( 'empty_response', __( 'No models returned', 'ai-auto-poster' ) );
        }
        $result = array();
        foreach ( $items as $m ) {
            $id = $m['id'] ?? ( $m['name'] ?? '' );
            if ( $id === '' ) continue;
            // embedding/audio などを除外
            if ( preg_match( '/(embedding|audio|whisper|tts|moderation|rerank|search|vision)/i', $id ) ) continue;
            $result[ $id ] = $id;
        }
        ksort( $result );
        return $result;
    }

    /**
     * 全プロバイダのモデル選択肢を返す
     *
     * テキスト/画像プロバイダのモデル定義は AIAP_Model_Registry に集約されているため、
     * ここはレジストリから組み立てるだけのアダプタとして残す
     * (UI 側の get_models_for() がプロバイダキー指定でフォールバック参照する想定)。
     *
     * @return array provider_key => array( model_id => display_label )
     */
    public static function get_model_options() {
        $result = array();

        $text_providers = AIAP_Model_Registry::text_providers();
        foreach ( $text_providers as $provider => $_def ) {
            $result[ $provider ] = AIAP_Model_Registry::text_model_options( $provider );
        }

        // 画像プロバイダのデフォルトモデルリストは、画像プロバイダのうち
        // モデル選択 UI を持つ dalle3 / imagen のみここに含める。
        // (stability / flux は admin の render_image_provider 側で個別に扱う)
        $result['dalle3'] = AIAP_Model_Registry::image_model_options( 'dalle3' );
        $result['imagen'] = AIAP_Model_Registry::image_model_options( 'imagen' );

        return $result;
    }

    /**
     * 言語設定の選択肢
     */

    // ================================================================
    // API calls (provider-specific)
    // ================================================================

    /**
     * Claude API呼び出し
     */
    public static function call_claude( $settings, $system, $prompt ) {
        if ( empty( $settings['claude_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Claude API key is not set', 'ai-auto-poster' ) );
        }

        $r = AIAP_Http::post_json( 'https://api.anthropic.com/v1/messages', array(
            'model'      => $settings['claude_model'],
            // 出力上限。日本語の長文記事は 4096 では途中打ち切り頻発のため 16000 に拡大。
            // max_tokens は「最大値」であり実際の課金は AI が出力した分のみ。
            'max_tokens' => 16000,
            'system'     => $system,
            'messages'   => array(
                array( 'role' => 'user',      'content' => $prompt ),
                // assistant プレフィル: 応答を必ず '{' から始めさせ、思考過程・前置き文・
                // コードブロックの混入を物理的に防ぐ。戻り値側で '{' を補完する。
                array( 'role' => 'assistant', 'content' => '{' ),
            ),
        ), array(
            'x-api-key'         => $settings['claude_api_key'],
            'anthropic-version' => '2023-06-01',
        ) );

        if ( is_wp_error( $r ) ) {
            return $r;
        }

        $code = $r['code'];
        $data = is_array( $r['json'] ) ? $r['json'] : array();

        if ( $code !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'claude_error', 'Claude', $r['response'] );
        }

        $content = isset( $data['content'][0]['text'] ) ? (string) $data['content'][0]['text'] : '';
        if ( trim( $content ) === '' ) {
            $stop_reason = isset( $data['stop_reason'] ) ? (string) $data['stop_reason'] : 'unknown';
            /* translators: %s: stop_reason returned by the Claude API */
            return new WP_Error( 'empty_ai_response', sprintf( __( 'Claude returned empty response (stop_reason: %s)', 'ai-auto-poster' ), $stop_reason ) );
        }
        // assistant プレフィルで送った '{' は応答に含まれないため、先頭に補完する。
        return '{' . $content;
    }

    /**
     * OpenAI API呼び出し
     *
     * NOTE: トークン上限を指定する場合は、必ず self::get_model_capabilities('openai', $model)['token_param']
     *       を参照すること。Reasoning系モデル(gpt-5/o1/o3/o4)は max_tokens ではなく
     *       max_completion_tokens を要求する。直接 'max_tokens' をハードコードすると400エラーになる。
     */
    public static function call_openai( $settings, $system, $prompt ) {
        if ( empty( $settings['openai_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'OpenAI API key is not set', 'ai-auto-poster' ) );
        }

        // モデルごとに正しいトークン上限パラメータ名を使う
        // (Reasoningモデル=gpt-5/o1/o3/o4は max_completion_tokens 必須)
        $caps = self::get_model_capabilities( 'openai', $settings['openai_model'] );
        $body = array(
            'model'    => $settings['openai_model'],
            'messages' => array(
                array( 'role' => 'system', 'content' => $system ),
                array( 'role' => 'user',   'content' => $prompt ),
            ),
            // JSON 出力強制(system/user メッセージに 'JSON' の語が含まれる必要があるが、
            //  generate() の instruction に 'Return ONLY ... JSON format' があるので満たす)
            'response_format' => array( 'type' => 'json_object' ),
        );
        if ( ! empty( $caps['token_param'] ) ) {
            // 出力上限。日本語の長文記事は 4096 では途中打ち切り頻発のため 16000 に拡大。
            // Reasoning 系 (o1/o3 等) は思考トークン分も消費するため同じく 16000。
            // max_tokens は「最大値」であり実際の課金は AI が出力した分のみ。
            $body[ $caps['token_param'] ] = 16000;
        }

        $r = AIAP_Http::post_json( 'https://api.openai.com/v1/chat/completions', $body, AIAP_Http::bearer( $settings['openai_api_key'] ) );

        if ( is_wp_error( $r ) ) {
            return $r;
        }

        $code = $r['code'];
        $data = is_array( $r['json'] ) ? $r['json'] : array();

        if ( $code !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'openai_error', 'OpenAI', $r['response'] );
        }

        $content = isset( $data['choices'][0]['message']['content'] ) ? (string) $data['choices'][0]['message']['content'] : '';
        if ( trim( $content ) === '' ) {
            $finish = isset( $data['choices'][0]['finish_reason'] ) ? (string) $data['choices'][0]['finish_reason'] : 'unknown';
            /* translators: %s: finish_reason returned by the OpenAI API */
            return new WP_Error( 'empty_ai_response', sprintf( __( 'OpenAI returned empty response (finish_reason: %s)', 'ai-auto-poster' ), $finish ) );
        }
        return $content;
    }

    /**
     * Gemini API呼び出し
     */
    public static function call_gemini( $settings, $system, $prompt ) {
        if ( empty( $settings['gemini_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Gemini API key is not set', 'ai-auto-poster' ) );
        }

        $url = 'https://generativelanguage.googleapis.com/v1beta/models/'
             . rawurlencode( $settings['gemini_model'] )
             . ':generateContent?key=' . rawurlencode( $settings['gemini_api_key'] );

        $r = AIAP_Http::post_json( $url, array(
            'system_instruction' => array(
                'parts' => array( array( 'text' => $system ) ),
            ),
            'contents' => array(
                array(
                    'role'  => 'user',
                    'parts' => array( array( 'text' => $prompt ) ),
                ),
            ),
            // 出力上限。Gemini はデフォルト 8192 程度のため、日本語長文記事だと
            // 途中打ち切りが発生する。明示的に 16000 を指定する。
            // 加えて responseMimeType を application/json にして JSON 出力を強制する。
            'generationConfig' => array(
                'maxOutputTokens' => 16000,
                'responseMimeType' => 'application/json',
            ),
        ) );

        if ( is_wp_error( $r ) ) {
            return $r;
        }

        $code = $r['code'];
        $data = is_array( $r['json'] ) ? $r['json'] : array();

        if ( $code !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'gemini_error', 'Gemini', $r['response'] );
        }

        $content = isset( $data['candidates'][0]['content']['parts'][0]['text'] )
            ? (string) $data['candidates'][0]['content']['parts'][0]['text']
            : '';
        if ( trim( $content ) === '' ) {
            $finish = isset( $data['candidates'][0]['finishReason'] ) ? (string) $data['candidates'][0]['finishReason'] : 'unknown';
            /* translators: %s: finishReason returned by the Gemini API */
            return new WP_Error( 'empty_ai_response', sprintf( __( 'Gemini returned empty response (finish_reason: %s)', 'ai-auto-poster' ), $finish ) );
        }
        return $content;
    }

    /**
     * OpenAI互換API呼び出し（Mistral / Groq / DeepSeek / OpenRouter）
     */
    /**
     * OpenAI互換API呼び出し (Mistral / Groq / DeepSeek / OpenRouter)
     *
     * NOTE: トークン上限を指定する場合は self::get_model_capabilities($provider, $model)['token_param']
     *       を参照すること。OpenRouter は OpenAI のモデル(gpt-5 等)もプロキシ配信するため、
     *       選択モデル次第で max_completion_tokens 側になる可能性がある。
     */
    public static function call_openai_compatible( $provider, $settings, $system, $prompt ) {
        $endpoints = self::get_openai_compatible_endpoints();

        if ( ! isset( $endpoints[ $provider ] ) ) {
            /* translators: %s: provider key */
            return new WP_Error( 'unknown_provider', sprintf( __( 'Unknown provider: %s', 'ai-auto-poster' ), $provider ) );
        }

        $ep    = $endpoints[ $provider ];
        $key   = isset( $settings[ $ep['key'] ] ) ? $settings[ $ep['key'] ] : '';
        $model = isset( $settings[ $ep['model'] ] ) ? $settings[ $ep['model'] ] : '';

        if ( empty( $key ) ) {
            return new WP_Error( 'no_api_key', ucfirst( $provider ) . ' API key is not set' );
        }

        $headers = array(
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . $key,
        );

        // OpenRouter requires additional headers
        if ( $provider === 'openrouter' ) {
            $headers['HTTP-Referer'] = home_url();
            $headers['X-Title']     = 'AI Auto Poster';
        }

        // モデルごとに正しいトークン上限パラメータ名を使う
        // (OpenRouter は OpenAI のgpt-5系もプロキシ配信するため可変)
        $caps = self::get_model_capabilities( $provider, $model );
        $body = array(
            'model'    => $model,
            'messages' => array(
                array( 'role' => 'system', 'content' => $system ),
                array( 'role' => 'user',   'content' => $prompt ),
            ),
            // JSON 出力強制(主要互換APIで対応。OpenRouter の一部モデルは非対応で
            // 400 になる場合があるが、その場合はモデルを変更すること)
            'response_format' => array( 'type' => 'json_object' ),
        );
        if ( ! empty( $caps['token_param'] ) ) {
            // 出力上限。日本語の長文記事は 4096 では途中打ち切り頻発のため 16000 に拡大。
            // 一部のプロバイダ(Mistral 等)は実際の上限が低い場合もあるが、その場合は
            // API 側で内部的にクランプされるため害なし。
            $body[ $caps['token_param'] ] = 16000;
        }

        // Bug fix (audit r9): 旧実装は wp_remote_post 直接呼出だったが、AIAP_Http 経由に統一。
        // openai_compatible は Content-Type 以外に Authorization と OpenRouter 用の追加ヘッダ
        // (HTTP-Referer / X-Title) を持つため、$headers から Content-Type を除いた残りを渡す。
        unset( $headers['Content-Type'] );

        $r = AIAP_Http::post_json( $ep['url'], $body, $headers );

        if ( is_wp_error( $r ) ) {
            return $r;
        }

        $code = $r['code'];
        $data = is_array( $r['json'] ) ? $r['json'] : array();

        if ( $code !== 200 ) {
            // Bug fix (audit r9): 旧実装は生 WP_Error を返していたため retryable データが
            // 付かず、generate_with_retry が 400/401/403 等の永続エラーでも「不明 → 再試行可」と
            // 判定して無駄なリトライを繰り返していた。他プロバイダ (Claude/OpenAI/Gemini) と同じく
            // build_api_error_wp_error 経由で classify_api_error を通すことで、リトライ可否・
            // ヒント文 (i18n)・raw_message を正しく付与する。
            return AIAP_AI_Errors::build_api_error_wp_error( $provider . '_error', ucfirst( $provider ), $r['response'] );
        }

        $content = isset( $data['choices'][0]['message']['content'] ) ? (string) $data['choices'][0]['message']['content'] : '';
        if ( trim( $content ) === '' ) {
            $finish = isset( $data['choices'][0]['finish_reason'] ) ? (string) $data['choices'][0]['finish_reason'] : 'unknown';
            return new WP_Error(
                'empty_ai_response',
                sprintf(
                    /* translators: 1: AI provider name, 2: finish_reason returned by the provider */
                    __( '%1$s returned empty response (finish_reason: %2$s)', 'ai-auto-poster' ),
                    ucfirst( $provider ),
                    $finish
                )
            );
        }
        return $content;
    }

    /**
     * content の末尾や先頭にある「Image Prompt: ...」行を抽出し、contentから取り除く
     * (AIがJSONキーではなくcontent内に画像プロンプトを書いた場合のサルベージ)
     *
     * @param string $content
     * @return array array( 'prompt' => string, 'content' => string )
     */
}
