راهنمای فنی برای توسعه دهندگان- سرویس API پلاتونیا (گیت‌وی)

برای استفاده از مدل‌های متنی OpenAI مانند gptها o1 و o3 و ...

خلاصه
– گیت‌وی پلاتونیا یک لایه امن بین وب‌اپ شما و OpenAI است. شما به‌جای کلید OpenAI، از کلید اختصاصی Platonia (به‌صورت Bearer) استفاده می‌کنید.
– پرداخت و شارژ اعتبار کاملاً ریالی (کارت‌های شتاب) است؛ زحمت پرداخت بین‌المللی ندارید.
– با توجه به استفاده پلاتونیا از حساب‌های Tier 5 OpenAI، ظرفیت و محدودیت‌ها بازتر از حساب‌های سطح پایین است (سقف‌ها همچنان تابع سیاست‌های OpenAI و مدل انتخابی هستند).
– مدل‌ها را در دو کلاس فراخوانی می‌کنید:
  – Chat Completions: برای GPT کلاسیک (مثل gpt-4o، gpt-4.1، gpt-4o-mini)
  – Responses (Reasoning): برای o1، o3 و gpt-5
– دامنه‌های مجاز برای هر کلید را حتماً در پنل پلاتونیا ثبت کنید تا CORS و امنیت برقرار باشد.
۱) مفاهیم کلیدی سرویس
– Endpoint ثابت گیت‌وی (نمونه):
 https://platonia.co/wp-json/platonia-dev/v1/proxy
– احراز هویت: Authorization: Bearer USER_API_KEY (کلید اختصاصی پلاتونیا با پیشوند pltn_)
– بدنه درخواست:
  – فیلد openai_path مسیر اصلی OpenAI را مشخص می‌کند:
    – chat/completions برای GPTهای کلاسیک
    – responses برای مدل‌های reasoning مثل o1، o3 و gpt-5
  – فیلد model باید یکی از مدل‌های مجاز فعال‌شده در پلاتونیا باشد.
– هدرهای بازگشتی کمکی:
  – X-Gateway: platonia-dev
  – X-Tokens-Used: توکن خام مصرف‌شده (از usage OpenAI یا تخمین)
  – X-Cost-Credits: هزینه اعشاری قبل از گرد کردن
  – X-Deducted-Units: واحد کسر شده از اعتبار (ceil(tokens*multiplier))
  – X-Elapsed-ms: زمان اجرای درخواست
– خطاهای پرتکرار:
  – 401 invalid_api_key: کلید نادرست/غیرفعال
  – 403 domain_forbidden: دامنه شما در لیست مجاز کلید نیست
  – 400 model_not_allowed: مدل در لیست مدل‌های مجاز فعال نشده است
  – 429 quota_exceeded / quota_would_be_exceeded: اعتبار کافی نیست
  – 500 server_misconfigured: کلید OpenAI سمت سرور تنظیم نشده
  – 502 upstream_error: خطای ارتباط با OpenAI
۲) پیش‌نیازهای استفاده
– در پلاتونیا ثبت‌نام کنید و وارد حساب کاربری شوید.
– از پنل «سرویس API»، کلید API اختصاصی بسازید (pltn_…).
– دامنه‌های مجاز را برای همان کلید ثبت کنید (example.com یا *.example.com).
– اعتبار خود را به‌صورت ریالی شارژ کنید.
– مدل‌های مورد نیاز را از لیست مدل‌های مجاز پلاتونیا فعال کنید (ادمین سرویس این کار را انجام می‌دهد).
۳) انتخاب مسیر مناسب: chat/completions یا responses
– chat/completions: برای  gpt-4o، gpt-4.1، gpt-4o-mini، gpt-4 و سایر GPTهای کلاسیک
  – payload نمونه: { openai_path: “chat/completions”, model, messages, temperature, … }
– responses: برای o1، o3 و gpt-5
  – این مدل‌ها معمولاً temperature سفارشی نمی‌پذیرند (فقط مقدار پیش‌فرض 1). در نتیجه در payload برای این‌ها temperature نفرستید.
  – قالب ورودی responses متفاوت است:
    – input: آرایه‌ای از پیام‌ها (ترجیحاً role و content با type درست: input_text برای ورودی)
    – instructions: نقش سیستم (system) به‌صورت جداگانه (اختیاری اما مفید)
    – پارامترهایی مثل max_output_tokens  (و برای o1/o3 می‌توانید reasoning: { effort: “medium” } بدهید)

۴) نمونه درخواست‌ها (چند زبان)

الف) JavaScript (Browser) – GPT کلاسیک (chat/completions)
				
					const ENDPOINT = 'https://platonia.co/wp-json/platonia-dev/v1/proxy';
const USER_KEY = 'pltn_xxx...'; // کلید پلاتونیا

async function askGPT(message) {
  const payload = {
    openai_path: 'chat/completions',
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: 'You are a helpful assistant. پاسخ‌ها را فارسی بده.' },
      { role: 'user', content: message }
    ],
    temperature: 0.5
  };

  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + USER_KEY
    },
    body: JSON.stringify(payload)
  });

  const data = await res.json();
  if (!res.ok) throw new Error((data && (data.message || data.error)) || 'Request failed');
  return data.choices?.[0]?.message?.content || data.choices?.[0]?.text || '';
}

askGPT('یک لطیفه کوتاه بگو').then(console.log).catch(console.error);
				
			
ب) JavaScript (Browser) – مدل reasoning (responses) مثل o1/o3/gpt-5
				
					const ENDPOINT = 'https://platonia.co/wp-json/platonia-dev/v1/proxy';
const USER_KEY = 'pltn_xxx...';

async function askReasoning(message) {
  const payload = {
    openai_path: 'responses',
    model: 'o1', // یا 'o3' یا 'gpt-5' (اگر در دسترس شماست)
    instructions: 'You are a helpful assistant. پاسخ‌ها را فارسی بده.',
    input: [
      {
        role: 'user',
        content: [{ type: 'input_text', text: message }]
      }
    ],
    max_output_tokens: 800
    // دقت کنید temperature ارسال نکنید
    // برای o1/o3 می‌توانید اضافه کنید: reasoning: { effort: 'medium' }
  };

  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + USER_KEY
    },
    body: JSON.stringify(payload)
  });

  const data = await res.json();
  if (!res.ok) throw new Error((data && (data.message || (data.error && data.error.message))) || 'Request failed');

  // استخراج متن از ساختار responses
  if (typeof data.output_text === 'string' && data.output_text.trim() !== '') return data.output_text;
  if (Array.isArray(data.output) && data.output.length) {
    const parts = [];
    data.output.forEach(out => {
      (out.content || []).forEach(seg => {
        if (seg.type === 'output_text' && typeof seg.text === 'string') parts.push(seg.text);
        else if (typeof seg.text === 'string') parts.push(seg.text);
      });
    });
    if (parts.length) return parts.join('\n');
  }
  return '';
}

askReasoning('در سه خط توضیح بده رگرسیون خطی چیست').then(console.log).catch(console.error);

				
			
پ) Node.js (axios)
				
					const axios = require('axios');

const ENDPOINT = 'https://platonia.co/wp-json/platonia-dev/v1/proxy';
const USER_KEY = 'pltn_xxx...';

async function callChat() {
  const payload = {
    openai_path: 'chat/completions',
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: 'You are a helpful assistant. پاسخ فارسی بده.' },
      { role: 'user', content: 'چطور backoff نمایی را پیاده‌سازی کنم؟' }
    ],
    temperature: 0.5
  };
  const { data } = await axios.post(ENDPOINT, payload, {
    headers: { Authorization: `Bearer ${USER_KEY}` }
  });
  console.log(data.choices?.[0]?.message?.content || '');
}

callChat().catch(console.error);
				
			
ت) Python (requests)
				
					import requests

ENDPOINT = 'https://platonia.co/wp-json/platonia-dev/v1/proxy'
USER_KEY = 'pltn_xxx...'

payload = {
    'openai_path': 'chat/completions',
    'model': 'gpt-4o-mini',
    'messages': [
        {'role': 'system', 'content': 'You are a helpful assistant. پاسخ فارسی بده.'},
        {'role': 'user', 'content': 'سه مزیت استفاده از گیت‌وی پلاتونیا چیست؟'}
    ],
    'temperature': 0.6
}
r = requests.post(ENDPOINT, json=payload, headers={'Authorization': f'Bearer {USER_KEY}'})
data = r.json()
if not r.ok:
    raise SystemExit(data.get('message') or data.get('error') or 'Request failed')

print(data['choices'][0]['message']['content'])
				
			
ث) PHP (cURL)
				
					<?php
$endpoint = 'https://platonia.co/wp-json/platonia-dev/v1/proxy';
$userKey = 'pltn_xxx...';

$payload = [
  'openai_path' => 'chat/completions',
  'model' => 'gpt-4o-mini',
  'messages' => [
    ['role'=>'system','content'=>'You are a helpful assistant. فارسی جواب بده.'],
    ['role'=>'user','content'=>'یک نکته درباره ریت‌لیمیت‌ها بگو.']
  ],
  'temperature' => 0.5
];

$ch = curl_init($endpoint);
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $userKey
  ],
  CURLOPT_POSTFIELDS => json_encode($payload),
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 90
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($resp, true);
if ($http < 200 || $http >= 300) {
  die(($data['message'] ?? $data['error'] ?? 'Request failed'));
}
echo $data['choices'][0]['message']['content'] ?? '';
				
			
ج) cURL (CLI)
				
					curl -X POST https://platonia.co/wp-json/platonia-dev/v1/proxy \
  -H "Authorization: Bearer pltn_xxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "openai_path":"responses",
    "model":"o1",
    "instructions":"You are a helpful assistant. پاسخ فارسی بده.",
    "input":[{"role":"user","content":[{"type":"input_text","text":"دو کاربرد مدل‌های reasoning را نام ببر"}]}],
    "max_output_tokens":600
  }'
				
			
۵) مدیریت خطا، ریت‌لیمیت و پایداری
– همیشه خطا را از data.message یا data.error.message بخوانید؛ نمایش [object Object] ناشی از stringify‌نکردن درست آبجکت خطاست.
 
– max_output_tokens را نزدیک به اندازه مورد انتظار پاسخ تنظیم کنید تا هزینه/خطاهای محدودیتی کمتر شود.
– اگر خطاهای 429 گرفتید: زمان‌بندی را جابه‌جا کنید، درخواست‌ها را تجمیع کنید، یا backoff را افزایش دهید.
– توجه: گیت‌وی محدودیت تعداد درخواست را به‌صورت مستقل اعمال نمی‌کند و تابع محدودیت‌های OpenAI و اعتبار شماست.

– Backoff نمایی (نمونه ساده JS):

				
					async function withBackoff(fn, max=5) {
  let delay = 800;
  for (let i=0;i<max;i++) {
    try { return await fn(); }
    catch (e) {
      if (i === max-1) throw e;
      await new Promise(r=>setTimeout(r, delay * (1 + Math.random())));
      delay *= 2;
    }
  }
}
				
			
۶) امنیت، CORS و دامنه‌های مجاز
– اگر از مرورگر کال می‌کنید، Origin/Referer باید در دامنه‌های مجاز کلید ثبت شده باشد؛ وگرنه 403 domain_forbidden دریافت می‌کنید.
– در تولید (Production) ترجیحاً درخواست‌ها را از بک‌اند خودتان (Server-side) ارسال کنید و کلید را در محیط امن نگه دارید. (چت‌بات تست که در ادامه کد آن را آورده‌ایم صرفاً برای تست سریع و دموست.)
– Wildcard به شکل *.example.com پشتیبانی می‌شود.
۷) تفاوت استخراج پاسخ‌ها
– chat/completions:
  – data.choices[0].message.content یا data.choices[0].text
– responses (o1/o3/gpt-5):
  – data.output_text (اگر موجود بود)
  – یا ترکیب قطعات data.output[].content[] با type=output_text
۸) نکات مهم برای عملکرد بهتر (Best Practices)
– در تولید، درخواست‌ها را تا حد امکان از سرور خود بفرستید (Backend) و کلید پلاتونیا را در محیط امن نگه دارید.
– برای مرورگر، دامنه‌های مجاز را دقیق تنظیم کنید؛ در صورت نیاز از wildcard به‌صورت آگاهانه استفاده کنید.
– از مدل‌های بهینه‌تر از نظر هزینه استفاده کنید (مثلاً gpt-4o-mini برای وظایف سبک).
– سقف اعتبار کاربران خود را تنظیم کنید و در صورت نیاز شارژ خودکار تعریف کنید.
– لاگ‌ها و هدرهای ریت‌لیمیت را بررسی و backoff نمایی پیاده‌سازی کنید.
– max_output_tokens را منطقی تنظیم کنید تا هزینه و خطا کاهش یابد.
– اگر به Stream نیاز دارید: در نسخه فعلی گیت‌وی، پاسخ‌ها به‌صورت JSON عادی بازمی‌گردد؛ استریمینگ (SSE) در حال حاضر پشتیبانی نمی‌شود.
 
۹) عیب‌یابی سریع
– 401 invalid_api_key: کلید اشتباه است یا کلید Revoke شده. کلید باید با pltn_ شروع شود.
– 403 domain_forbidden: Origin/Referer شما در لیست دامنه‌های مجاز آن کلید نیست.
– 400 model_not_allowed: مدل را در لیست مدل‌های مجاز پلاتونیا فعال کنید یا نام را دقیق وارد کنید. امکان match پیشوندی برقرار است (مثلاً تعریف gpt-4o، برای gpt-4o-latest هم کافی است).
– 429 quota_exceeded: اعتبار تمام شده؛ اعتبار را شارژ کنید یا مصرف را کاهش دهید.
– 400 Unsupported value: ‘temperature’… برای o1/o3/gpt-5 temperature نفرستید (کد نمونه رعایت کرده است).
– 502 upstream_error: اختلال موقتی یا خطای شبکه؛ با backoff دوباره تلاش کنید.
۱۰) معرفی افزونه «چت‌بات تست» و راه‌اندازی در وردپرس (دانلود افزونه)
این افزونه یک چت‌بات ساده (Shortcode) به سایت وردپرسی شما اضافه می‌کند تا گیت‌وی پلاتونیا را تست کنید. از chat/completions برای GPTهای کلاسیک و از responses برای o1/o3 و gpt-5 استفاده می‌کند. کلید کاربر در LocalStorage ذخیره می‌شود (صرفاً جهت تست/دمو).
گام‌های نصب
– وارد پیشخوان وردپرس شوید و به مسیر افزونه‌ها > افزونه جدید > ایجاد (یا از فایل زیپ دلخواه) بروید.
– یک پوشه به نام platonia-dev-chatbot در wp-content/plugins بسازید و فایل PHP زیر را داخل آن قرار دهید.
– افزونه را فعال کنید.
– یک برگه جدید بسازید و شورت‌کد زیر را (بدون گیومه) در آن قرار دهید:
[“platonia_dev_chatbot”]
– برگه را باز کنید، کلید API کاربر (pltn_…) را که از بخش «سرویس API» وبسایت پلاتونیا دریافت کرده‌اید وارد کنید، یک پیام بنویسید و ارسال کنید.
– از هر دامنه‌ای که کار می‌کنید، آن دامنه باید در پنل «سرویس API» پلاتونیا، در دامنه‌های مجاز کلید ثبت شده باشد.

کد کامل افزونه‌ی چت‌بات تست

				
					<?php
/**
 * Plugin Name: Platonia Dev - Chatbot Tester
 * Description: چت‌بات ساده برای تست Gateway افزونه Platonia Dev. برای GPTها از chat/completions و برای مدل‌های reasoning مثل o1/o3 و gpt-5 از responses استفاده می‌کند.
 * Version: 1.2.0
 * Author: Platonia
 * Text Domain: platonia-dev-chatbot
 */




if (!defined('ABSPATH')) exit;




class Platonia_Dev_Chatbot_Tester {
    public static function init() {
        add_shortcode('platonia_dev_chatbot', [__CLASS__, 'render_chatbot']);
    }




    public static function render_chatbot($atts = []) {
        $atts = shortcode_atts([
            'endpoint'       => 'https://platonia.co/wp-json/platonia-dev/v1/proxy',
            'model'          => 'gpt-4o-mini',
            'system_prompt'  => 'You are a helpful assistant. پاسخ‌ها را تا حد امکان به زبان فارسی ارائه بده.',
            'temperature'    => '0.5',
        ], $atts, 'platonia_dev_chatbot');




        $container_id = 'pdc_' . wp_generate_uuid4();
        $endpoint = esc_url_raw($atts['endpoint']);




        ob_start();
        ?>




        <style>
            #<?php echo esc_attr($container_id); ?> .pdc-box { border: 1px solid #ddd; border-radius: 8px; max-width: 720px; margin: 10px 0 20px; font-family: sans-serif; }
            #<?php echo esc_attr($container_id); ?> .pdc-header { padding: 10px 12px; border-bottom: 1px solid #eee; background: #f9f9f9; display: flex; align-items: center; justify-content: space-between; }
            #<?php echo esc_attr($container_id); ?> .pdc-body { padding: 12px; max-height: 520px; overflow-y: auto; background: #fff; }
            #<?php echo esc_attr($container_id); ?> .pdc-controls { padding: 12px; border-top: 1px solid #eee; background: #fafafa; }
            #<?php echo esc_attr($container_id); ?> .pdc-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
            #<?php echo esc_attr($container_id); ?> .pdc-row label { font-size: 12px; color: #555; }
            #<?php echo esc_attr($container_id); ?> .pdc-input, #<?php echo esc_attr($container_id); ?> .pdc-select { padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; }
            #<?php echo esc_attr($container_id); ?> .pdc-input { width: 100%; max-width: 380px; }
            #<?php echo esc_attr($container_id); ?> .pdc-select { width: auto; }
            #<?php echo esc_attr($container_id); ?> .pdc-messages { display: flex; flex-direction: column; gap: 10px; }
            #<?php echo esc_attr($container_id); ?> .pdc-msg { padding: 10px 12px; border-radius: 10px; max-width: 80%; line-height: 1.5; white-space: pre-wrap; }
            #<?php echo esc_attr($container_id); ?> .pdc-msg.user { margin-left: auto; background: #e6f2ff; border: 1px solid #cfe6ff; }
            #<?php echo esc_attr($container_id); ?> .pdc-msg.assistant { margin-right: auto; background: #f3f3f3; border: 1px solid #e5e5e5; }
            #<?php echo esc_attr($container_id); ?> .pdc-footer { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
            #<?php echo esc_attr($container_id); ?> .pdc-textarea { width: 100%; min-height: 60px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; resize: vertical; }
            #<?php echo esc_attr($container_id); ?> .pdc-btn { padding: 8px 12px; background: #2271b1; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
            #<?php echo esc_attr($container_id); ?> .pdc-btn:disabled { background: #9bbcdf; cursor: not-allowed; }
            #<?php echo esc_attr($container_id); ?> .pdc-status { font-size: 12px; color: #666; margin-left: 6px; }
            #<?php echo esc_attr($container_id); ?> .pdc-error { color: #b71c1c; background: #fdecea; border: 1px solid #f5c6cb; padding: 8px 10px; border-radius: 6px; }
            #<?php echo esc_attr($container_id); ?> .pdc-keyhint { font-size: 12px; color: #777; }
        </style>




        <div id="<?php echo esc_attr($container_id); ?>">
            <div class="pdc-box">
                <div class="pdc-header">
                    <strong>Platonia Dev - Chatbot Tester</strong>
                    <span class="pdc-status" id="<?php echo esc_attr($container_id); ?>_status">آماده</span>
                </div>
                <div class="pdc-controls">
                    <div class="pdc-row">
                        <label>کلید API کاربر:</label>
                        <input class="pdc-input" type="password" placeholder="pltn_..." id="<?php echo esc_attr($container_id); ?>_apikey" />
                        <button class="pdc-btn" id="<?php echo esc_attr($container_id); ?>_savekey">ذخیره در مرورگر</button>
                        <button class="pdc-btn" id="<?php echo esc_attr($container_id); ?>_clearkey" style="background:#888;">حذف کلید ذخیره‌شده</button>
                    </div>
                    <div class="pdc-row">
                        <label>مدل:</label>
                        <select class="pdc-select" id="<?php echo esc_attr($container_id); ?>_model">
                            <option value="gpt-4o-mini" selected>gpt-4o-mini</option>
                            <option value="gpt-4o">gpt-4o</option>
                            <option value="gpt-4.1-mini">gpt-4.1-mini</option>
                            <option value="gpt-4.1">gpt-4.1</option>                                                  
                            <option value="o1-mini">o1-mini</option>                            
                            <option value="o3-mini">o3-mini</option>
                        </select>
                        <label>Temperature:</label>
                        <input class="pdc-input" style="max-width:80px" type="number" step="0.1" min="0" max="2" id="<?php echo esc_attr($container_id); ?>_temp" value="<?php echo esc_attr($atts['temperature']); ?>" />
                    </div>
                    <div class="pdc-row">
                        <span class="pdc-keyhint">کلید باید همان کلید اختصاصی Platonia Dev باشد. در LocalStorage مرورگر ذخیره می‌شود.</span>
                    </div>
                </div>
                <div class="pdc-body">
                    <div class="pdc-messages" id="<?php echo esc_attr($container_id); ?>_messages"></div>
                </div>
                <div class="pdc-controls">
                    <div id="<?php echo esc_attr($container_id); ?>_error" class="pdc-error" style="display:none;"></div>
                    <div class="pdc-footer">
                        <textarea class="pdc-textarea" id="<?php echo esc_attr($container_id); ?>_input" placeholder="پیام خود را بنویسید..."></textarea>
                        <button class="pdc-btn" id="<?php echo esc_attr($container_id); ?>_send">ارسال</button>
                    </div>
                </div>
            </div>
        </div>




        <script>
        (function(){
            const cId = <?php echo wp_json_encode($container_id); ?>;
            const endpoint = <?php echo wp_json_encode($endpoint); ?>;
            const defaultModel = <?php echo wp_json_encode($atts['model']); ?>;
            const systemPromptDefault = <?php echo wp_json_encode($atts['system_prompt']); ?>;




            const el = (id) => document.getElementById(id);
            const apikeyInput = el(cId + '_apikey');
            const saveKeyBtn = el(cId + '_savekey');
            const clearKeyBtn = el(cId + '_clearkey');
            const modelSel = el(cId + '_model');
            const tempInput = el(cId + '_temp');
            const msgList = el(cId + '_messages');
            const statusEl = el(cId + '_status');
            const errorEl = el(cId + '_error');
            const inputEl = el(cId + '_input');
            const sendBtn = el(cId + '_send');




            const LS_KEY = 'pdc_user_api_key';




            try { const saved = localStorage.getItem(LS_KEY); if (saved) apikeyInput.value = saved; } catch (e) {}
            modelSel.value = defaultModel;




            const conversation = [
                { role: 'system', content: systemPromptDefault }
            ];




            function setStatus(text) { statusEl.textContent = text; }
            function showError(msg) { errorEl.style.display = 'block'; errorEl.textContent = msg || 'خطای نامشخص'; }
            function hideError() { errorEl.style.display = 'none'; errorEl.textContent = ''; }
            function appendMessage(role, content) {
                const wrap = document.createElement('div');
                wrap.className = 'pdc-msg ' + (role === 'user' ? 'user' : 'assistant');
                wrap.textContent = content;
                msgList.appendChild(wrap);
                msgList.scrollTop = msgList.scrollHeight;
            }
            function sanitizeModel(m) { return String(m || '').trim() || 'gpt-4o-mini'; }
            function isValidUserKey(k) {
                const s = String(k || '').trim();
                return s.startsWith('pltn_') && s.length >= 12 && !/^https?:\/\//i.test(s);
            }




            // الگوی مدل‌هایی که باید از Responses API استفاده کنند: o1، o3 و gpt-5
            function isResponsesModel(model) {
                const m = String(model || '').toLowerCase();
                return /^(o1|o3)(?:-|$)/.test(m) || /^gpt-5(?:-|$)/.test(m);
            }
            function isO1O3(model) {
                const m = String(model || '').toLowerCase();
                return /^(o1|o3)(?:-|$)/.test(m);
            }
            function isGpt5(model) {
                return /^gpt-5(?:-|$)/i.test(String(model || ''));
            }




            // نمایش/غیرفعال‌سازی فیلد Temperature برای مدل‌های responses
            function updateTemperatureField() {
                const resp = isResponsesModel(modelSel.value);
                tempInput.disabled = resp;
                tempInput.title = resp ? 'این مدل از temperature پشتیبانی نمی‌کند.' : '';
            }




            // instructions از پیام‌های system
            function extractSystemInstructions(conv) {
                return conv.filter(m => m.role === 'system').map(m => String(m.content || '')).filter(Boolean).join('\n');
            }
            function stripSystem(conv) { return conv.filter(m => m.role !== 'system'); }




            // برای responses: ورودی تجمیع‌شده user با input_text
            function toResponsesInput(convNoSystem) {
                const lines = convNoSystem.map(m => {
                    const prefix = m.role === 'assistant' ? 'Assistant: ' : 'User: ';
                    return prefix + String(m.content || '');
                }).join('\n\n');
                return [{
                    role: 'user',
                    content: [{ type: 'input_text', text: lines }]
                }];
            }




            // ساخت payload
            function buildRequest(model, conversationAll, temperature) {
                const m = sanitizeModel(model);
                if (isResponsesModel(m)) {
                    const instructions = extractSystemInstructions(conversationAll);
                    const convNoSystem = stripSystem(conversationAll);
                    const req = {
                        openai_path: 'responses',
                        model: m,
                        input: toResponsesInput(convNoSystem),
                        max_output_tokens: 800
                    };
                    // برای o1/o3 می‌توان reasoning تنظیم کرد؛ برای gpt-5 فعلاً ارسال نمی‌کنیم
                    if (isO1O3(m)) req.reasoning = { effort: 'medium' };
                    if (instructions) req.instructions = instructions;
                    return req;
                } else {
                    return {
                        openai_path: 'chat/completions',
                        model: m,
                        messages: conversationAll,
                        temperature: temperature
                    };
                }
            }




            // استخراج متن پاسخ
            function extractAssistantText(data) {
                // chat/completions
                if (data && Array.isArray(data.choices) && data.choices.length) {
                    const ch = data.choices[0];
                    if (ch.message && ch.message.content) return ch.message.content;
                    if (ch.text) return ch.text;
                }
                // responses: output_text
                if (data && typeof data.output_text === 'string' && data.output_text.trim() !== '') {
                    return data.output_text;
                }
                // responses: قطعه‌ای
                if (data && Array.isArray(data.output)) {
                    const pieces = [];
                    data.output.forEach(out => {
                        if (out && Array.isArray(out.content)) {
                            out.content.forEach(seg => {
                                if (!seg || typeof seg !== 'object') return;
                                if (seg.type === 'output_text' && typeof seg.text === 'string') pieces.push(seg.text);
                                else if (seg.type === 'summary_text' && typeof seg.text === 'string') pieces.push(seg.text);
                                else if (typeof seg.text === 'string') pieces.push(seg.text);
                            });
                        }
                    });
                    if (pieces.length) return pieces.join('\n');
                }
                // برخی پاسخ‌ها ممکن است message.content داشته باشند
                if (data && data.message && Array.isArray(data.message.content)) {
                    const parts = data.message.content.map(seg => seg && (seg.text || seg.output_text || '')).filter(Boolean);
                    if (parts.length) return parts.join('\n');
                }
                return '[پاسخ نامشخص]';
            }




            function getErrorMessage(data) {
                if (!data) return 'خطای نامشخص';
                if (typeof data.error === 'string') return data.error;
                if (data.error && typeof data.error === 'object') return data.error.message || JSON.stringify(data.error);
                if (data.message) return data.message;
                try { return JSON.stringify(data); } catch(e) { return 'خطای نامشخص'; }
            }




            async function sendMessage() {
                hideError();




                const key = (apikeyInput.value || '').trim();
                if (!isValidUserKey(key)) {
                    showError('کلید API معتبر نیست. باید با pltn_ شروع شود و URL نباشد.');
                    return;
                }




                const userText = (inputEl.value || '').trim();
                if (!userText) return;




                appendMessage('user', userText);
                conversation.push({ role: 'user', content: userText });
                inputEl.value = '';




                const model = sanitizeModel(modelSel.value);
                const temp = parseFloat(tempInput.value || '0.5') || 0.5;




                const payload = buildRequest(model, conversation, temp);




                sendBtn.disabled = true;
                setStatus('ارسال درخواست...');




                try {
                    const res = await fetch(endpoint, {
                        method: 'POST',
                        mode: 'cors',
                        credentials: 'omit',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': 'Bearer ' + key
                        },
                        body: JSON.stringify(payload)
                    });




                    const tokensUsed = res.headers.get('X-Tokens-Used') || '';
                    const elapsed = res.headers.get('X-Elapsed-ms') || '';




                    let data = null;
                    try { data = await res.json(); } catch(e) { data = null; }




                    if (!res.ok) {
                        const msg = getErrorMessage(data);
                        showError(msg);
                        setStatus('خطا' + (elapsed ? ' • ' + elapsed + 'ms' : ''));
                        return;
                    }




                    const assistantText = extractAssistantText(data);
                    appendMessage('assistant', assistantText);
                    conversation.push({ role: 'assistant', content: assistantText });




                    let st = 'موفق';
                    if (tokensUsed) st += ' • ' + tokensUsed + ' tokens';
                    if (elapsed) st += ' • ' + elapsed + 'ms';
                    setStatus(st);




                } catch (err) {
                    showError('خطای شبکه یا CORS: ' + (err && err.message ? err.message : String(err)));
                    setStatus('خطا');
                } finally {
                    sendBtn.disabled = false;
                }
            }




            // رویدادها
            sendBtn.addEventListener('click', sendMessage);
            inputEl.addEventListener('keydown', (e) => {
                if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') sendMessage();
            });
            saveKeyBtn.addEventListener('click', function(){
                const key = (apikeyInput.value || '').trim();
                if (!isValidUserKey(key)) { alert('کلید API معتبر نیست.'); return; }
                try { localStorage.setItem(LS_KEY, key); alert('کلید ذخیره شد.'); } catch(e) { alert('عدم امکان ذخیره‌سازی LocalStorage'); }
            });
            clearKeyBtn.addEventListener('click', function(){
                try { localStorage.removeItem(LS_KEY); } catch(e) {}
                apikeyInput.value = '';
                alert('کلید پاک شد.');
            });




            // مدیریت temperature بر اساس مدل انتخاب‌شده
            modelSel.addEventListener('change', updateTemperatureField);
            updateTemperatureField();




            appendMessage('assistant', 'سلام! کلید API را وارد کنید. برای مدل‌های o1/o3 و gpt-5 از API responses استفاده می‌شود و temperature ارسال نمی‌گردد.');
        })();
        </script>




        <?php
        return ob_get_clean();
    }
}




Platonia_Dev_Chatbot_Tester::init();
				
			

نکات کلیدی کد افزونه تست چت‌بات

– ثبت شورت‌کد platonia_dev_chatbot: رندر جعبه چت، ورودی کلید، انتخاب مدل و دکمه ارسال.
– ذخیره امن کلید کاربر در LocalStorage (برای تست). در تولید، توصیه به استفاده از بک‌اند خودتان.
– تشخیص نوع مدل: برای o1/o3/gpt-5 به‌صورت خودکار به openai_path=responses سوییچ می‌کند؛ برای GPTهای کلاسیک به chat/completions.
– ساخت payload:
  – chat/completions: model + messages + temperature (در صورت نیاز)
  – responses: instructions (از پیام سیستم)، input با type: input_text، و max_output_tokens. برای o1/o3 اختیاری reasoning: { effort: ‘medium’ }.
– ارسال درخواست با fetch به گیت‌وی و هدر Authorization: Bearer pltn_… .
– استخراج پاسخ:
  – chat: data.choices[0].message.content
  – responses: data.output_text یا تجمیع قطعات output[].content[].text با type=output_text
– مدیریت خطا: نمایش data.message یا data.error.message و جلوگیری از نمایش [object Object].
– نکته CORS: برای تست در سایت وردپرسی‌تان، دامنه همان سایت را در دامنه‌های مجاز کلید کاربر ثبت کنید.