20 KiB
20 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 },
"hermes-agent": { "models": ["hermes-agent"], "loadedAt": null, "error": null }
}
}
- OpenAI model lists are filtered to models that are expected to work with the backend's Responses API implementation.
hermes-agentis included only whenHERMES_AGENT_API_KEYis configured. Set it to HermesAPI_SERVER_KEY, or any non-empty value if that local server does not require auth.HERMES_AGENT_API_BASE_URLdefaults tohttp://127.0.0.1:8642/v1; setHERMES_AGENT_MODELonly when you need an additional fallback/override model id.- The backend loads provider model lists at startup and refreshes them about once every 24 hours. If a later provider refresh fails, the response keeps the last loaded model list for that provider and sets
errorto the latest failure message.
Chat Tools
GET /v1/chat-tools
- Response:
{
"tools": [
{ "name": "web_search", "description": "..." },
{ "name": "fetch_url", "description": "..." }
]
}
Behavior notes:
- Lists Sybil-managed chat tools that can be enabled for
openaiandxaichat completions. - Optional tools such as
codex_execandshell_execappear only when enabled by server environment configuration.
Active Runs
GET /v1/active-runs
- Response:
{
"chats": ["chat-id-with-active-stream"],
"searches": ["search-id-with-active-stream"]
}
Behavior notes:
- Lists in-memory chat/search streams that are still running on this server process.
- Clients should use this after app start or page refresh to restore per-row generating indicators.
- The lists are not durable across server restarts.
Workspace Items
GET /v1/workspace-items
- Response:
{ "items": WorkspaceItem[] } WorkspaceItemis a discriminated union sorted byupdatedAtdescending:
{
"items": [
{
"type": "chat",
"id": "chat-id",
"title": "optional title",
"createdAt": "2026-02-14T00:00:00.000Z",
"updatedAt": "2026-02-14T00:00:00.000Z",
"starred": true,
"starredAt": "2026-02-14T01:00:00.000Z",
"initiatedProvider": "openai",
"initiatedModel": "gpt-4.1-mini",
"lastUsedProvider": "openai",
"lastUsedModel": "gpt-4.1-mini",
"additionalSystemPrompt": null,
"enabledTools": ["web_search", "fetch_url"]
},
{
"type": "search",
"id": "search-id",
"title": "optional title",
"query": "search query",
"createdAt": "2026-02-14T00:00:00.000Z",
"updatedAt": "2026-02-14T00:00:00.000Z",
"starred": false,
"starredAt": null
}
]
}
Behavior notes:
- This endpoint is intended for combined conversation/search lists such as sidebars.
- The legacy
GET /v1/chatsandGET /v1/searchesendpoints remain available for clients that need separate collections. - The response currently combines up to 100 chats and up to 100 searches.
starred/starredAtare backed by membership in a reservedProjectwith idstarred; future project folders can reuse the same project item model.
Chats
GET /v1/chats
- Response:
{ "chats": ChatSummary[] }
POST /v1/chats
- Body:
{
"title": "optional title",
"provider": "optional openai|anthropic|xai|hermes-agent",
"model": "optional model id",
"additionalSystemPrompt": "optional stored system prompt",
"enabledTools": ["web_search", "fetch_url"],
"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. additionalSystemPromptis trimmed and stored on the chat; blank values are stored asnull.enabledToolsstores the enabled Sybil-managed tool names for future chat completions. Unknown tool names are ignored; omitted values default to all currently available tools.- Optional
messagesare inserted as the initial transcript. Attachment metadata uses the same schema and limits as chat completion messages.
PATCH /v1/chats/:chatId
- Body: any subset of
{ "title": string, "additionalSystemPrompt": string|null, "enabledTools": string[] } - Response:
{ "chat": ChatSummary } - Blank titles are rejected. The server trims surrounding whitespace before storing the title.
additionalSystemPrompt: nullclears the stored prompt. Blank string values are also stored asnull.enabledTools: []disables Sybil-managed tools for this chat. Omitted settings are left unchanged.- Updating chat fields changes the returned chat's
updatedAt. - Not found:
404 { "message": "chat not found" }
PATCH /v1/chats/:chatId/star
- Body:
{ "starred": boolean } - Response:
{ "chat": ChatSummary } - Not found:
404 { "message": "chat not found" }
Behavior notes:
- Starring adds the chat to the reserved
starredproject and setsstarredAtto the membership creation time. - Unstarring removes that membership and returns
starred: false,starredAt: null. - This does not modify the chat transcript or chat
updatedAt.
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.
- If a title is set while suggestion generation is in flight, server returns the current chat instead of overwriting that title.
- When no title exists at write time, server 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|hermes-agent",
"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
}
]
}
],
"additionalSystemPrompt": "optional one-off system prompt",
"enabledTools": ["web_search", "fetch_url"],
"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. additionalSystemPrompt, when present directly or loaded from stored chat settings, is prepended to the provider request as asystemmessage and is not inserted into the persisted chat transcript by this endpoint.enabledToolslimits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.- 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
hermes-agent, backend calls the configured Hermes Agent OpenAI-compatible Chat Completions API without adding Sybil-managed tool definitions; Hermes Agent handles its own tools server-side. - For
openai, image attachments are sent as Responsesinput_imageitems and text attachments are sent asinput_textitems. - For
xaiandhermes-agent, 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 Sybil-managed tool calls for
openaiandxai: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 emit an initiated SSEtool_callevent before execution, then persist each completed or failed tool call as its terminal 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, "reuseByQuery"?: boolean } - Response:
{ "search": SearchSummary, "reused": boolean, "cacheHit": boolean }
Behavior notes:
reuseByQuerydefaults tofalse, preserving the normal create-a-new-search behavior.- When
reuseByQueryistrueandqueryis present, the backend normalizes the query withtrim().toLowerCase()and returns the most recently updated existing search with that normalized query instead of creating a duplicate. cacheHitistrueonly when the reused search has persisted results or answer text, is not currently streaming, and was updated within the 24-hour search cache window. Clients can then fetchGET /v1/searches/:searchIdand display it without running another search.- If a matching search exists but
cacheHitisfalse, clients may run the search again on the returnedsearch.id; the run endpoints replace that search's persisted results and answer with the latest run.
PATCH /v1/searches/:searchId/star
- Body:
{ "starred": boolean } - Response:
{ "search": SearchSummary } - Not found:
404 { "message": "search not found" }
Behavior notes:
- Starring adds the search to the reserved
starredproject and setsstarredAtto the membership creation time. - Unstarring removes that membership and returns
starred: false,starredAt: null. - This does not modify the search results or search
updatedAt.
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.
POST /v1/searches/:searchId/run/stream
- Body: same as
POST /v1/searches/:searchId/run - Response:
text/event-stream
Events:
search_results:{ "requestId": string|null, "results": SearchResultItem[] }search_error:{ "error": string }answer:{ "answerText": string|null, "answerRequestId": string|null, "answerCitations": SearchDetail["answerCitations"] }answer_error:{ "error": string }- terminal
done:{ "search": SearchDetail } - terminal
error:{ "message": string }
Behavior notes:
- The stream is owned by the backend after it starts. If the original HTTP client disconnects, the backend keeps running and persists the final search state.
- While a search stream is active,
GET /v1/active-runsincludes thesearchId. - If a stream is already active for the same
searchId, this endpoint attaches to the existing stream instead of starting a second run.
POST /v1/searches/:searchId/run/stream/attach
- Body: none
- Response:
text/event-streamwith the same event names asPOST /v1/searches/:searchId/run/stream - Not found:
404 { "message": "active search stream not found" }
Behavior notes:
- Replays buffered events for the active in-memory stream, then emits new events until
doneorerror. - Intended for clients that discovered a pending search via
GET /v1/active-runs, such as after browser refresh.
Type Shapes
ChatSummary
{
"id": "...",
"title": null,
"createdAt": "...",
"updatedAt": "...",
"starred": false,
"starredAt": null,
"initiatedProvider": "openai|anthropic|xai|hermes-agent|null",
"initiatedModel": "string|null",
"lastUsedProvider": "openai|anthropic|xai|hermes-agent|null",
"lastUsedModel": "string|null",
"additionalSystemPrompt": null,
"enabledTools": ["web_search", "fetch_url"]
}
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": "...",
"starred": false,
"starredAt": null,
"initiatedProvider": "openai|anthropic|xai|hermes-agent|null",
"initiatedModel": "string|null",
"lastUsedProvider": "openai|anthropic|xai|hermes-agent|null",
"lastUsedModel": "string|null",
"additionalSystemPrompt": null,
"enabledTools": ["web_search", "fetch_url"],
"messages": [Message]
}
SearchSummary
{ "id": "...", "title": null, "query": null, "createdAt": "...", "updatedAt": "...", "starred": false, "starredAt": null }
SearchDetail
{
"id": "...",
"title": "...",
"query": "...",
"createdAt": "...",
"updatedAt": "...",
"starred": false,
"starredAt": null,
"requestId": "...",
"latencyMs": 123,
"error": null,
"answerText": "...",
"answerRequestId": "...",
"answerCitations": [],
"answerError": null,
"results": []
}
For streaming contracts, see docs/api/streaming-chat.md.