AI chat proxy (X-Ai-Chat-Secret)
const url = 'https://example.com/wp-json/wp2tutor/ai/v1/chat';const options = { method: 'POST', headers: {'X-Ai-Chat-Secret': '<X-Ai-Chat-Secret>', 'Content-Type': 'application/json'}, body: '"What is RAG?"'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/wp-json/wp2tutor/ai/v1/chat \ --header 'Content-Type: application/json' \ --header 'X-Ai-Chat-Secret: <X-Ai-Chat-Secret>' \ --data '"What is RAG?"'Proxy to the selected LLM (ChatProxyService::chat). Permission is not cookie and not manage_options: check_secret_permission compares header X-Ai-Chat-Secret to SecretKeyManager::get() (option wp2lms_ai_chat_secret_key) with hash_equals. Empty or wrong secret → WP_Error rest_forbidden (status 401, message Missing or invalid secret).
Body is raw $request->get_body(), not a Chat Completions schema. AiRequestBodyNormalizer: JSON string "prompt" or base64 of that JSON string (frontend when encode_request). PHP does not parse a messages object.
Rate limit RequestRateLimiter: 30 requests / 60 s, key ai-chat|{ip}|{first 8 chars of secret}. Over limit → 429 with { error } (not WP_Error).
Success: HTTP 200, body = provider data string (LLM text). Config/provider errors are not forwarded: 400 if the internal error string contains API key, otherwise 500; body is always { error: AI request failed. Please try again later. } (i18n).
Authorizations
Section titled “Authorizations”Request Body
Section titled “Request Body”JSON string prompt (on the wire "text"). PHP reads the raw body; AiRequestBodyNormalizer json_decode → string.
Example
What is RAG?Raw body, including base64 of the JSON prompt string (frontend encode_request).
Responses
Section titled “Responses”ChatProxyService success (success true). Body is data (LLM string), not a success/error object. If data is missing, JSON array [].
Internal error contains substring API key (missing key API key not configured or API key error: …). Client gets { error } with the generic text, not WP_Error.
object
Client-facing text. 429 — Too many requests… (i18n). 400/500 — generic AI request failed. Please try again later. (i18n). Not a WP_Error code.
Example
{ "error": "AI request failed. Please try again later."}Rest_forbidden — missing or invalid X-Ai-Chat-Secret. WP_Error, data.status 401.
object
WP_Error code. For POST /chat 401 — rest_forbidden. DualRest cookie — rest_forbidden / rest_not_logged_in / rest_cookie_invalid_nonce.
WP_Error message (for the chat secret — Missing or invalid secret.).
Usually { status: HTTP code }, as PHP stores in WP_Error data.
object
Example
{ "code": "rest_forbidden", "message": "Missing or invalid secret.", "data": { "status": 401 }}Rest_forbidden — capability / DualRest cookie nonce rejection when the request is treated as a cookie session (rare for this secret-gated route). WP_Error, data.status 403.
object
WP_Error code. For POST /chat 401 — rest_forbidden. DualRest cookie — rest_forbidden / rest_not_logged_in / rest_cookie_invalid_nonce.
WP_Error message (for the chat secret — Missing or invalid secret.).
Usually { status: HTTP code }, as PHP stores in WP_Error data.
object
Example
{ "code": "rest_forbidden", "message": "Missing or invalid secret.", "data": { "status": 401 }}Too many requests (30 / 60 s per IP+secret prefix). Not WP_Error: { error: Too many requests. Please try again later. } (i18n).
object
Client-facing text. 429 — Too many requests… (i18n). 400/500 — generic AI request failed. Please try again later. (i18n). Not a WP_Error code.
Example
{ "error": "Too many requests. Please try again later."}Other proxy failures (empty prompt No messages in request, provider not implemented, LLM failure). Same { error } body, not WP_Error.
object
Client-facing text. 429 — Too many requests… (i18n). 400/500 — generic AI request failed. Please try again later. (i18n). Not a WP_Error code.
Example
{ "error": "AI request failed. Please try again later."}