Tools can use an internal AI engine ("OpenAI Engine") to analyze content and power platform features — without ever exposing API keys in the frontend.
This is a user manual that describes:
401, 403, and 422 mistakesThe platform now makes a deliberate distinction between:
That distinction matters because they are not the same thing.
Important: POST /api/ai/socialgpt/respond is not the main/general-purpose Tools "respond" endpoint. It exists for the SocialGPT / Social Media Tools client contract. Internal runtimes such as IRCWatch should normally use POST /api/ai/internal/respond instead.
ai.socialgpt (ai.client is kept as a legacy alias)provider_socialgpt (legacy tools_ai_bearer rows still work), but the runtime auth decision now follows the token scope instead of the provider name itself/keys/mine) and the admin review queue /admin/openaiURL:
/admin/openaiRequirements:
openai.manageIn the web UI you can:
The model dropdowns in /admin/openai are no longer hardcoded.
GET /v1/models server-sideallowed_models is configured, the dropdown is intersected with that allowlistThis means the admin model picker is based on what the current provider key can actually use, without exposing any key in the browser.
503, depending on endpoint).Note: provider keys are managed under API Keys and are never shown in plain text.
If Slack audit forwarding is enabled for OpenAI / SocialGPT request categories, audit entries now also include:
user_id, and name/email when available)error_reason when an upstream/provider request failsThis makes it easier for operators to see who triggered an AI request and why a failed request was rejected.
Users who are allowed to use Tools AI can create one or more personal bearer tokens in Tools:
/keys/mine/keys/mine now shows the known Tools access scopes as a checkbox list instead of expecting users to guess the scope names manuallyThese Tools-side AI client tokens are:
ai.socialgpt, ai.internal, etc.), so the provider/name is only a label/categoryThis makes it possible to keep one token per assignment (for example one internal automation, one batch job, one research client) without sharing the same bearer everywhere.
Tools still also has a separate dedicated SocialGPT token flow:
provider_socialgpt token row for conveniencetools_ai_bearer tokens still work for compatibilityai.socialgptUse it like this:
Authorization: Bearer <token>
Each token works only for the endpoints covered by its own access scopes and is tied to your user account + permissions.
The current built-in access scopes shown in My API Keys are:
ai.socialgptai.internalwhisper.apimail-support-assistant.relaysms.sendsms.gatewayTools now also supports other personal per-system tokens (for example provider_ircwatch or provider_mail_support_assistant) as long as they are:
is_ai=1) and/or given the explicit scope ai.socialgptImportant distinction:
provider_openai is the upstream provider secret used towards OpenAI and is never treated as an AI receiver tokenIf you are integrating a client against Tools AI, start here.
Do not start by pasting one random AI URL into a client and hoping it behaves like the public OpenAI API.
Tools currently has multiple AI-related endpoints with different contracts:
| Use case | Endpoint | Token scope | Important note |
|---|---|---|---|
| SocialGPT-style reply / verify / modify workflow | POST /api/ai/socialgpt/respond |
ai.socialgpt |
This is a Tools-specific contract, not an OpenAI-compatible one |
| Internal runtime / assistant / automation client | POST /api/ai/internal/respond |
ai.internal |
Requires client_slug and at least context or user_prompt |
| IRCWatch / server-side bot runtime | POST /api/ai/internal/respond |
ai.internal |
Prefer an internal client slug such as ircwatch; do not treat SocialGPT as the default runtime endpoint |
| Analyze one remote URL server-side | POST /api/ai/url/analyze |
approved OpenAI access for the authenticated user | Not meant as a generic chat endpoint |
| Validate a SocialGPT-style bearer token only | GET /api/social-media-tools/extension/validate-token |
ai.socialgpt |
Checks the token, not whether the user may actually run OpenAI |
This is the most common integration mistake.
Tools endpoints such as:
POST /api/ai/socialgpt/respondPOST /api/ai/internal/respondare not drop-in OpenAI /v1/chat/completions replacements.
That means:
POST /api/ai/internal/respond requires Tools-specific fields such as client_slugIf your client cannot control the JSON request body, it may need an adapter/proxy instead of direct connection to the internal Tools endpoint.
For bearer-token integrations, two separate checks matter:
Examples:
ai.internal and still be rejected with 403 if the owning user lacks approved OpenAI accessvalidate-token and still fail on a real AI request because the token owner lacks provider_openaiBefore debugging one browser extension, Copilot-style form, or desktop settings page, test the exact endpoint manually with curl or another raw HTTP client.
That immediately answers:
Use:
POST /api/ai/socialgpt/respondwhen the client can send Tools-specific fields such as:
contextuser_promptmodifierrequest_modeclient_name, client_version, client_platform)Use:
POST /api/ai/internal/respondwhen the client can send a Tools-specific JSON body that includes:
client_slugcontext or user_promptThis is the normal choice for Tools-side runtimes such as IRCWatch, cron jobs, background workers, and other non-SocialGPT integrations.
Do not assume that POST /api/ai/internal/respond is the right URL.
If the client only knows how to send OpenAI-native payloads such as messages[], input, or standard Chat Completions fields, you currently need one of these instead:
Without that adapter layer, the client may fail with 422 Unprocessable Content even though the token is correct.
Regular new users are no longer allowed to use OpenAI-backed Tools features automatically just because a daily budget exists.
User flow:
/keys/mine/openai/access/request directly in the browser, Tools now redirects you back to the built-in request card on My API Keys instead of leaving you on a generic error page/openai/access/requst/admin/openaiprovider_openai access right and can then create one or more Tools OpenAI forwarder tokens in My API KeysAdmin flow:
/admin/openai/admin/openaiprovider_tools_openai forwarder tokens, not only the legacy Tools AI bearer row)Endpoint:
POST /api/ai/url/analyzePurpose:
Form data or JSON:
url (required) — URL to analyzequestion (optional) — analysis focus/questionprofile (optional) — prompt profile name (default: URL Analyzer if it exists, otherwise the engine falls back to a minimal default profile)JSON:
ok — true/falserequest_id — internal request idlatency_ms — approximate latencymodel — model usedresponse — model output (if ok)error — error message (if ok=false)To use the endpoint you need:
401 Unauthenticatedis_admin=1) — always allowedprovider_openaiIf the OpenAI provider isn't configured (missing global provider_openai API key), the endpoint typically returns 503.
Endpoint:
POST /api/ai/socialgpt/respondAuth / access rules:
ai.socialgpt can authenticatetools_ai_bearer / provider_socialgpt rows still work because they are backfilled to that same scopeapi_keys.is_ai=1), because that legacy flag is still interpreted as ai.socialgptprovider_openai)If the bearer token belongs to a user without approved OpenAI access, the endpoint returns 403.
Additive SocialGPT request fields:
client_nameclient_versionclient_platformThese fields are optional and let Tools identify which client build made the request. The response can also include an additive client object echoing the accepted metadata.
Failure-handling note:
error as normal text.Security behavior:
.env values, passwords, tokens, API keys, or other Tools internals.Endpoint:
POST /api/ai/internal/respondPurpose:
dnsbl-engine call the central OpenAI engine directly instead of inheriting the SocialGPT settings layerclient_slug identifier so defaults, auditing, and usage stats can be tied back to that caller from /admin/openaiai.internal, so one internal job can be rotated or disabled without affecting anotherAuth / access rules:
ai.internalprovider_openai)Important compatibility note:
Request fields:
client_slug (required) — caller identifier such as mail_support_assistant_standalone, mail_support_assistant_tools_admin, dnsbl_engine, or any other stable non-empty slug your integration choosescontext (optional) — supporting context/body datauser_prompt (optional) — the direct instruction for the taskmodifier (optional) — shorter follow-up modifier textmood / custom_mood (optional) — one-request tone override on top of the saved client configresponder_name_override (optional)persona_profile_override (optional)custom_instruction_override (optional)model (optional)reasoning_effort (optional) — none|low|medium|high|xhighresponse_language (optional) — auto|sv|en|da|no|de|fr|esmax_tokens (optional)temperature (optional)use_web_search (optional)web_search_required (optional)Validation rule that causes many 422 responses:
client_slug is requiredcontext or user_prompt must be non-emptyImportant runtime behavior:
/admin/openaiclient_slug is accepted as the caller identifierSo a payload like this will fail:
{
"model": "gpt-4"
}
with a validation-style 422 / “unprocessable content” response.
{
"client_slug": "client_browser_companion",
"user_prompt": "Reply with ok",
"context": "Connection test"
}
curl -X POST "https://tools.tornevall.net/api/ai/internal/respond" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"client_slug": "client_browser_companion",
"context": "Temporary demo feature",
"user_prompt": "Reply with ok",
"model": "gpt-4o",
"response_language": "en"
}'
Success response includes:
okrequest_idmodelresponseusageused_fallback_modelclient (slug, name, description)applied_settings (responder_name, persona_profile_excerpt, custom_instruction_excerpt, mood, response_language, reasoning_effort)web_search metadata (requested, required, used, citations[])Admin UI note:
/admin/openai now also contains a centralized Internal AI clients section where operators can edit saved defaults for each internal client_slug, including slugs that were first auto-registered from live traffic.For internal assistant-style clients, the main control point is not the URL itself.
The behavior comes from two layers:
client_slug (when one exists)That means the normal way to “instruct Copilot for this assistant” is:
client_slug for that runtime/admin/openai if you want saved defaults/stat tracking thereresponder_name_overridepersona_profile_overridecustom_instruction_overridemoodresponse_languageIf the external client UI does not let you send these extra fields, then the integration is limited by that client UI, not by the Tools token itself.
401 UnauthenticatedUsually means:
403 ForbiddenUsually means:
provider_openai)This is not the same thing as “bad token”.
422 Unprocessable ContentUsually means:
For POST /api/ai/internal/respond, the usual cause is one of these:
client_slugcontext and user_prompt are emptyThat is expected when you only tested:
GET /api/social-media-tools/extension/validate-tokenbecause that endpoint only verifies the token itself.
It does not prove that the user may execute OpenAI-backed requests right now.
For a new integration, test in this order:
curl request against the real target endpoint with a minimum valid payloadThis section is intentionally a design sketch only.
It documents one recommended direction for a future Tools-side compatibility adapter so external clients that only understand the OpenAI-style request format can still talk to Tools without sending the current Tools-specific client_slug / context / user_prompt payload shape directly.
This endpoint does not exist yet as a public contract.
Some clients only support fields such as:
modelmessages[]temperaturemax_tokensand a single “provider URL” textbox.
Those clients cannot easily call:
POST /api/ai/internal/respondbecause the internal Tools endpoint expects fields such as:
client_slugcontextuser_promptAn adapter endpoint would solve that mismatch.
Suggested future endpoint:
POST /api/openai/v1/chat/completionsSuggested auth model:
ai.internalSuggested backend behavior:
client_slug from one of these sources:
client_slugX-Tools-Client-Sluggeneric_openai_compat_clientmessages[] into the internal Tools request shape:
system / developer / assistant history → contextuser message → user_promptmodeltemperaturemax_tokensPOST /api/ai/internal/respond service layer internally{
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": "Summarize this page in three bullets."
}
]
}
These are optional ideas for a future adapter. They are not active public API fields today.
{
"client_slug": "generic_openai_compat_client",
"response_language": "en",
"tools_context": "Temporary browser-side assistant session"
}
For maximum compatibility, return something structurally close to OpenAI Chat Completions, for example:
{
"id": "tools-chatcmpl-123",
"object": "chat.completion",
"created": 1760000000,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 40,
"total_tokens": 140
}
}
If this adapter is implemented later, keep the error semantics explicit:
401 → missing/invalid token403 → token accepted, but the token owner lacks OpenAI access422 → missing messages[], missing usable user message, or missing client resolution503 → Tools OpenAI engine/provider unavailableIf implemented later, the cleanest direction is usually:
app/Http/Controllers/AiGatewayController.phproutes/api.phpInternalOpenAiClientServiceThe snippets below are intentionally generic. They do not depend on one specific internal client such as client_browser_companion.
Replace placeholder values like:
YOUR_API_TOKENYOUR_CLIENT_SLUGYOUR_TEXT_HEREwith values that match your own Tools setup.
{
"client_slug": "YOUR_CLIENT_SLUG",
"user_prompt": "YOUR_TEXT_HERE",
"context": "Optional supporting context"
}
curl examplecurl -X POST "https://tools.tornevall.net/api/ai/internal/respond" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"client_slug": "generic_internal_assistant",
"context": "Optional page or workflow context.",
"user_prompt": "Summarize the important actions from this page.",
"model": "gpt-4o",
"response_language": "en"
}'
const response = await fetch("https://tools.tornevall.net/api/ai/internal/respond", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN"
},
body: JSON.stringify({
client_slug: "generic_internal_assistant",
context: "Optional page or workflow context.",
user_prompt: "Summarize the important actions from this page.",
model: "gpt-4o",
response_language: "en"
})
});
const data = await response.json();
console.log(data);
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Bearer YOUR_API_TOKEN"
}
$body = @{
client_slug = "generic_internal_assistant"
context = "Optional page or workflow context."
user_prompt = "Summarize the important actions from this page."
model = "gpt-4o"
response_language = "en"
} | ConvertTo-Json
Invoke-RestMethod \
-Method Post \
-Uri "https://tools.tornevall.net/api/ai/internal/respond" \
-Headers $headers \
-Body $body
import requests
response = requests.post(
"https://tools.tornevall.net/api/ai/internal/respond",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_TOKEN",
},
json={
"client_slug": "generic_internal_assistant",
"context": "Optional page or workflow context.",
"user_prompt": "Summarize the important actions from this page.",
"model": "gpt-4o",
"response_language": "en",
},
timeout=60,
)
print(response.status_code)
print(response.json())
/admin/openai instruction template for internal assistantsIf you are creating one internal assistant profile in /admin/openai, this generic starter template is often a better first step than hardcoding one client-specific persona immediately.
Use this style for assistants that should:
You are a practical internal assistant for Tools-connected clients.
Your job is to help the user understand the supplied context, summarize important information, rewrite or clarify text, and suggest useful next actions.
Priorities:
1. Be clear and operationally useful.
2. Prefer short, direct answers unless the user explicitly asks for more detail.
3. When summarizing, focus on the most relevant facts, decisions, blockers, deadlines, and action items.
4. If the supplied context is incomplete, say what is missing instead of inventing details.
5. Do not claim to have clicked, fetched, verified, or inspected anything beyond the supplied context.
6. If the user asks for a rewrite, preserve the meaning but improve structure, tone, and clarity.
7. If the user asks what to do next, provide a short ordered action list.
Tone:
- calm
- competent
- helpful
- not overly chatty
Avoid:
- hype
- vague consultant language
- pretending that uncertain information is verified fact
- long introductions or closing filler unless the user asks for a more formal style
One good generic starting value in /admin/openai is:
clear, concise, practicalRelated extension endpoints:
GET /api/social-media-tools/extension/validate-tokenGET /api/social-media-tools/extension/testPOST /api/social-media-tools/extension/testImportant difference:
validate-token only verifies that the supplied personal AI-capable token itself is validtest performs a real OpenAI-backed smoke test and therefore requires approved OpenAI access for non-admin usersThis lets clients distinguish "the token belongs to a real user" from "that user is actually allowed to run OpenAI requests right now".
curl -X POST "https://tools.tornevall.net/api/ai/url/analyze" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{
"url": "https://example.com",
"question": "What is this page about?",
"profile": "URL Analyzer"
}'
Endpoint:
GET /api/social-media-tools/extension/modelsPurpose:
Response fields include:
models — array of available model optionsdefault_model — effective default model for this user/contextsource — whether the list came from live provider discovery or a configured fallbackwarning — optional fallback/discovery message