Request
Path params
| Name | Type | Source |
|---|---|---|
botId | UUID | bots.id |
Headers
| Header | Required | Meaning |
|---|---|---|
Content-Type | yes | Must include application/json |
x-llm-api-key | yes | The user’s BYO LLM API key |
x-llm-azure-endpoint | yes if Azure | Azure OpenAI endpoint URL |
x-llm-azure-api-version | optional, Azure | Azure API version (defaults to provider default) |
Body
| Field | Type | Constraint |
|---|---|---|
message | string | 1–8,000 chars |
Total body size cap
The route measures the raw byte length and rejects payloads over 16,384 bytes with413 request_too_large, regardless of Content-Length.
Lifecycle
- Content-Type check - non-JSON →
415 unsupported_media_type - Key read - missing
x-llm-api-key→400 missing_llm_key - Body size cap - > 16,384 bytes →
413 request_too_large - JSON parse - malformed →
400 invalid_json - Zod validate -
messageout of bounds →400 validation_failed - Bot lookup -
bots.id+is_active = true; miss →404 bot_not_found - Owner lookup - owner row missing →
404 bot_not_found - Rate limit - 2-tier per-bot →
429 rate_limit - Input sanitize - match →
400 blocked - Provider dispatch - Azure requires
x-llm-azure-endpoint; missing →400 missing_llm_key - Provider call → on error, map
ProviderError.categorytoinvalid_llm_key/provider_rate_limit/provider_unavailable - Output sanitize - strip key shapes, system-prompt echo, PII, internal errors
- Return
{ reply }
Responses
200 OK
400 Bad Request - missing key
400 Bad Request - invalid JSON
400 Bad Request - validation failure
400 Bad Request - blocked by sanitizer
reason is a coarse category string (prompt_injection, role_override, credential_probe, system_prompt_extraction, jailbreak). The matched substring is not returned.
400 Bad Request - invalid LLM key
404 Not Found
413 Payload Too Large
415 Unsupported Media Type
429 Too Many Requests - local rate limit
429 Too Many Requests - provider rate limit
502 Bad Gateway
cURL
Anthropic / OpenAI / Google
Azure OpenAI
Notes
- The route owner determines provider + model (
users.llm_provider,users.llm_model). Callers cannot override them. - The system prompt is rebuilt per-request from the freshly-fetched bot row; there is no cache to invalidate.
- The output sanitizer never returns an empty string - if the entire reply got stripped, the route returns a polite fallback string, not a 500.