12 KiB
12 KiB
REST API Contract
Base URL: /api behind web proxy, or server root directly in local/dev.
Authentication:
- If
ADMIN_TOKENis set on server, sendAuthorization: Bearer <token>. - If
ADMIN_TOKENis unset, API is open for local/dev use.
Content type:
- Requests with bodies use
application/json. - Responses are JSON unless noted otherwise.
Chat upload limits:
- Chat completion and direct message payloads support inline attachments up to a 32 MB request body.
- Up to 8 attachments per message.
- Image attachments: PNG or JPEG only, max 6 MB each.
- Text attachments: up to 8 MB source size each; server accepts at most 200,000 characters of inlined text content per attachment.
Health + Auth
GET /health
- Response:
{ "ok": true }
GET /v1/auth/session
- Response:
{ "authenticated": true, "mode": "open" | "token" }
Models
GET /v1/models
- Response:
{
"providers": {
"openai": { "models": ["gpt-4.1-mini"], "loadedAt": "2026-02-14T00:00:00.000Z", "error": null },
"anthropic": { "models": ["claude-3-5-sonnet-latest"], "loadedAt": null, "error": null },
"xai": { "models": ["grok-3-mini"], "loadedAt": null, "error": null }
}
}
- OpenAI model lists are filtered to models that are expected to work with the backend's Responses API implementation.
Chats
GET /v1/chats
- Response:
{ "chats": ChatSummary[] }
POST /v1/chats
- Body:
{
"title": "optional title",
"provider": "optional openai|anthropic|xai",
"model": "optional model id",
"messages": [
{
"role": "system|user|assistant|tool",
"content": "string",
"name": "optional",
"attachments": []
}
]
}
- Response:
{ "chat": ChatSummary }
Behavior notes:
providerandmodelmust be supplied together when present.- When
provider/modelare supplied, the new chat initializesinitiatedProvider/initiatedModelandlastUsedProvider/lastUsedModel. - Optional
messagesare inserted as the initial transcript. Attachment metadata uses the same schema and limits as chat completion messages.
PATCH /v1/chats/:chatId
- Body:
{ "title": string } - Response:
{ "chat": ChatSummary } - Not found:
404 { "message": "chat not found" }
POST /v1/chats/title/suggest
- Body:
{
"chatId": "chat-id",
"content": "user request text"
}
- Response:
{ "chat": ChatSummary }
Behavior notes:
- If the chat already has a non-empty title, server returns the existing chat unchanged.
- Server always uses OpenAI
gpt-4.1-minito generate a one-line title (up to ~4 words), updates the chat title, and returns the updated chat.
DELETE /v1/chats/:chatId
- Response:
{ "deleted": true } - Not found:
404 { "message": "chat not found" }
GET /v1/chats/:chatId
- Response:
{ "chat": ChatDetail }
POST /v1/chats/:chatId/messages
- Body:
{
"role": "system|user|assistant|tool",
"content": "string",
"name": "optional",
"metadata": {},
"attachments": [
{
"kind": "image",
"id": "attachment-id",
"filename": "photo.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 12345,
"dataUrl": "data:image/jpeg;base64,..."
},
{
"kind": "text",
"id": "attachment-id",
"filename": "notes.md",
"mimeType": "text/markdown",
"sizeBytes": 4567,
"text": "# Notes\\n...",
"truncated": false
}
]
}
- Response:
{ "message": Message }
Notes:
attachmentsis optional and is merged into storedmessage.metadata.attachments.- Tool messages should not include attachments.
Chat Completions (non-streaming)
POST /v1/chat-completions
- Body:
{
"chatId": "optional-chat-id",
"provider": "openai|anthropic|xai",
"model": "string",
"messages": [
{
"role": "system|user|assistant|tool",
"content": "string",
"name": "optional",
"attachments": [
{
"kind": "image",
"id": "attachment-id",
"filename": "photo.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 12345,
"dataUrl": "data:image/jpeg;base64,..."
},
{
"kind": "text",
"id": "attachment-id",
"filename": "notes.md",
"mimeType": "text/markdown",
"sizeBytes": 4567,
"text": "# Notes\\n...",
"truncated": false
}
]
}
],
"temperature": 0.2,
"maxTokens": 256
}
- Response:
{
"chatId": "chat-id-or-null",
"provider": "openai",
"model": "gpt-4.1-mini",
"message": { "role": "assistant", "content": "..." },
"usage": { "inputTokens": 10, "outputTokens": 20, "totalTokens": 30 },
"raw": {}
}
Behavior notes:
- If
chatIdis present, server validates chat existence. - For
chatIdcalls, server stores only new non-assistant messages from provided history to avoid duplicates. - Server persists final assistant output and call metadata (
LlmCall) in DB. - Server updates chat-level model metadata on each call:
lastUsedProvider/lastUsedModel; first successful/failed call also initializesinitiatedProvider/initiatedModelif unset. - Attachments are optional and currently apply to
usermessages. Persisted chat history stores them undermessage.metadata.attachments. - Images are forwarded inline to providers as multimodal image parts. Use PNG or JPEG for cross-provider compatibility.
- Text files are forwarded as explicit text blocks rather than provider-managed file references. Large text attachments should already be truncated client-side before submission.
- For
openai, backend calls OpenAI's Responses API and enables internal tool use with an internal system instruction. - For
xai, backend calls xAI's OpenAI-compatible Chat Completions API and enables internal tool use with the same internal system instruction. - For
openai, image attachments are sent as Responsesinput_imageitems and text attachments are sent asinput_textitems. - For
xai, image attachments are sent as Chat Completions content parts alongside text. - For
openai, Responses calls that can enter the server-managed tool loop usestore: trueso reasoning and function-call items can be passed between tool rounds. - For
anthropic, image attachments are sent as Messages APIimageblocks using base64 source data; text attachments are added astextblocks. - Available tool calls for chat:
web_searchandfetch_url. WhenCHAT_CODEX_TOOL_ENABLED=true,codex_execis also available. WhenCHAT_SHELL_TOOL_ENABLED=true,shell_execis also available. web_searchreturns ranked results with per-result summaries/snippets. Its backend engine is selected byCHAT_WEB_SEARCH_ENGINE(exadefault, orsearxngwithSEARXNG_BASE_URLset). SearXNG mode requires the instance to allowformat=json.fetch_urlfetches a URL and returns plaintext page content (HTML converted to text server-side).codex_execdelegates coding, shell, repository inspection, and other complex software tasks to a persistent remote Codex CLI workspace over SSH. The server runscodex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check <non-interactive wrapped prompt>on the configured devbox insideCHAT_CODEX_REMOTE_WORKDIR, with SSH stdin closed.shell_execruns arbitrary non-interactive shell commands on the same configured devbox, starting inCHAT_CODEX_REMOTE_WORKDIR. It usesbash -lcwhen bash exists, otherwisesh -lc, closes SSH stdin, and does not run inside the Sybil server container.- Devbox tool configuration:
CHAT_MAX_TOOL_ROUNDS=100(optional; maximum model/tool result cycles before the backend returns a limit message)CHAT_CODEX_TOOL_ENABLED=trueCHAT_SHELL_TOOL_ENABLED=trueCHAT_CODEX_REMOTE_HOST=<host-or-ip>(required when enabled)CHAT_CODEX_REMOTE_USER=<ssh-user>(optional; omitted ifCHAT_CODEX_REMOTE_HOSTalready containsuser@host)CHAT_CODEX_REMOTE_PORT=22(optional)CHAT_CODEX_REMOTE_WORKDIR=/workspace/sybil-codex(optional; created on the remote host if missing)CHAT_CODEX_SSH_KEY_PATH=/run/secrets/codex_ssh_key(recommended private-key delivery via read-only volume mount)CHAT_CODEX_SSH_PRIVATE_KEY_B64=<base64-private-key>(optional fallback when a volume mount is not practical)CHAT_CODEX_EXEC_TIMEOUT_MS=600000(optional)CHAT_SHELL_EXEC_TIMEOUT_MS=120000(optional)
- When a tool call is executed, backend stores a chat
Messagewithrole: "tool"and tool metadata (metadata.kind = "tool_call"). Streaming requests persist each completed tool call as its SSEtool_callevent is emitted, then store the assistant output when the completion finishes. anthropiccurrently runs without server-managed tool calls.
Searches
GET /v1/searches
- Response:
{ "searches": SearchSummary[] }
POST /v1/searches
- Body:
{ "title"?: string, "query"?: string } - Response:
{ "search": SearchSummary }
DELETE /v1/searches/:searchId
- Response:
{ "deleted": true } - Not found:
404 { "message": "search not found" }
GET /v1/searches/:searchId
- Response:
{ "search": SearchDetail }
POST /v1/searches/:searchId/chat
- Body:
{ "title"?: string } - Response:
{ "chat": ChatSummary } - Not found:
404 { "message": "search not found" }
Behavior notes:
- Creates a new chat seeded with a hidden
systemmessage containing the search query, answer text, answer citations, and top search results. - Clients should include existing
systemmessages when sending the chat history to/v1/chat-completionsor/v1/chat-completions/stream; they may hide those messages in the transcript UI. - The default chat title is
Search: <query-or-title>, unlesstitleis supplied.
POST /v1/searches/:searchId/run
- Body:
{
"query": "optional override",
"title": "optional override",
"type": "auto|fast|deep|instant",
"numResults": 10,
"includeDomains": ["example.com"],
"excludeDomains": ["example.org"]
}
- Response:
{ "search": SearchDetail }
Search run notes:
- Backend executes Exa search and Exa answer.
- Search mode is independent from chat
web_searchtool configuration and remains Exa-only. - Persists answer text/citations + ranked results.
- If both search and answer fail, endpoint returns an error.
Type Shapes
ChatSummary
{
"id": "...",
"title": null,
"createdAt": "...",
"updatedAt": "...",
"initiatedProvider": "openai|anthropic|xai|null",
"initiatedModel": "string|null",
"lastUsedProvider": "openai|anthropic|xai|null",
"lastUsedModel": "string|null"
}
Message
{
"id": "...",
"createdAt": "...",
"role": "system|user|assistant|tool",
"content": "...",
"name": null,
"metadata": {
"attachments": [
{
"kind": "image",
"id": "attachment-id",
"filename": "photo.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 12345,
"dataUrl": "data:image/jpeg;base64,..."
},
{
"kind": "text",
"id": "attachment-id",
"filename": "notes.md",
"mimeType": "text/markdown",
"sizeBytes": 4567,
"text": "# Notes\\n...",
"truncated": false
}
]
}
}
metadata remains nullable. Tool-call log messages still use metadata.kind = "tool_call"; regular user messages with attachments use metadata.attachments.
ChatDetail
{
"id": "...",
"title": null,
"createdAt": "...",
"updatedAt": "...",
"initiatedProvider": "openai|anthropic|xai|null",
"initiatedModel": "string|null",
"lastUsedProvider": "openai|anthropic|xai|null",
"lastUsedModel": "string|null",
"messages": [Message]
}
SearchSummary
{ "id": "...", "title": null, "query": null, "createdAt": "...", "updatedAt": "..." }
SearchDetail
{
"id": "...",
"title": "...",
"query": "...",
"createdAt": "...",
"updatedAt": "...",
"requestId": "...",
"latencyMs": 123,
"error": null,
"answerText": "...",
"answerRequestId": "...",
"answerCitations": [],
"answerError": null,
"results": []
}
For streaming contracts, see docs/api/streaming-chat.md.