Skip to main content
Xynthis exposes two local HTTP surfaces. The daemon (xynthis serve) wraps the agent loop and the brain on http://127.0.0.1:3939; this is what the macOS app talks to. The model server (xynthis-llm-serve) exposes the on-device model on http://127.0.0.1:8080 behind a standard OpenAI-compatible API. Both bind to loopback by default and have no authentication; do not expose them beyond the machine.

The daemon (127.0.0.1:3939)

Start it with xynthis serve [--port 3939]. Error handling follows one convention: when the brain is unreachable, handlers return HTTP 200 with an error envelope ({"op": "error", "message": "brain daemon offline"} or {"ok": false, "error": "..."} depending on the route), so clients poll one shape.

POST /api/chat

Runs one agent turn. Blocks until the turn finishes.
{
  "message": "rename the README",
  "sessionId": "b3f2c9e1-...",
  "tier": "pro",
  "guide_mode": false,
  "cwd": "/Users/me/project"
}
Only message is required. sessionId is a stable per-conversation id; the daemon maps each one onto its own on-disk session so chat threads keep separate histories. Ids are restricted to [A-Za-z0-9_-], 1-128 characters; anything else is ignored and the daemon stays on its current session. tier ("free" / "pro" / "team") sets the memory-recall window for the turn: an explicit "free" caps recall to the last 7 days; "pro", "team", and absent mean unlimited. cwd binds the agent’s file tools and bash to a project root for the turn. A request with fleet_mode: true and tier: "free" is rejected with HTTP 402: multi-agent execution is Pro/Team only. Response:
{
  "text": "Done — renamed README.md to README.old.md",
  "tools": [
    { "name": "bash", "args": { "command": "mv README.md README.old.md" }, "result": "..." }
  ],
  "session_id": "b3f2c9e1-...",
  "iterations": 3
}
iterations is how many LLM round-trips the agent ran. POST /api/chat/stop cancels an in-flight turn.

GET /api/status

Returns the brain’s status JSON: uptime, perception counters, last dream, working-memory size, knowledge-graph size. Responses are cached for 1.5 seconds server-side, sized for the app’s 3-second poll; offline errors are never cached, so recovery shows immediately.

GET /api/events

A Server-Sent Events stream of agent activity. Each event is a JSON object tagged with an event field in snake_case: turn_started, llm_call_started, llm_call_completed, tool_called, tool_result, tool_denied, memory_recalled, memory_perceived, recall_judged, turn_completed, and others. Every event carries a ts unix timestamp. A consumer that falls behind receives {"event": "lagged", "dropped": N} and the stream continues; the daemon never drops a slow client. Streams end when the daemon shuts down.

GET /api/recall

GET /api/recall?q=<query>&top_k=<n>&tier=<tier>
Runs the brain’s layered retriever and returns its hits. tier=free caps results to the 7-day window, same as chat.

POST /api/remember

{ "content": "text to store", "kind": "note" }
Stores a perception in the brain. kind defaults to "note".

GET /api/sessions

Lists saved chat sessions, newest first. Sessions with zero messages are omitted.
{
  "sessions": [
    { "id": "...", "title": "...", "created_at": 1760000000, "updated_at": 1760000100, "message_count": 12 }
  ]
}

GET /api/session/transcript

GET /api/session/transcript?id=<session-id>
Returns the clean user/assistant transcript of a session: {"id": "...", "title": "...", "messages": [{"role": "user", "text": "..."}]}. Internal scaffolding (injected memory context, working-memory blocks, tool-result turns) is stripped, so the messages are exactly what a chat UI should render.

GET /api/llm/list

Every provider configured in config.toml, with availability and the active marker. Powers the app’s model picker.
{
  "providers": [
    {
      "name": "anthropic",
      "kind": "anthropic-messages",
      "model": "claude-sonnet-4-5",
      "lite": false,
      "base_url": "https://api.anthropic.com/v1/messages",
      "launch_command": null,
      "available": true,
      "local": false,
      "active": true
    }
  ],
  "active_provider": "anthropic",
  "active_model": "claude-sonnet-4-5"
}
available comes from the credential scanner for remote providers; for local providers it is a live TCP probe of the configured port, so a configured-but-down local server shows as unavailable. Providers are ordered active first, then available, then alphabetical.

POST /api/llm/set

{ "provider": "ollama", "model": "llama3.3" }
Validates that the provider exists, sets [models] active in config.toml, and saves. model is optional; it falls back to the provider’s default_model. Returns {"ok": true, "active": "ollama/llama3.3", "active_provider": "...", "active_model": "...", "active_base_url": "..."} or {"ok": false, "error": "..."}.

GET /api/extensions

Returns a snapshot of the loaded external MCP/webhook extensions: the same payload the agent’s own extension_list tool sees.

Other routes

The daemon exposes further routes the app consumes: corpus management (/api/corpus, /api/corpus/add|remove|watch|update), permissions (/api/permissions, /api/permissions/set|preset), lessons, semantic search (POST /api/search/semantic), cortex, knowledge graph (/api/kg/graph), auth scanning (/api/auth/scan|apply), and voice transcription. They follow the same envelope conventions; the routes above are the stable integration surface.

The model server (127.0.0.1:8080)

xynthis-llm-serve exposes the on-device model over the OpenAI chat-completions standard, so any OpenAI-compatible client works against it unchanged. Port and bind address come from XYNTHIS_LLM_PORT (default 8080) and XYNTHIS_LLM_BIND (default 127.0.0.1). Only bind beyond loopback behind a tunnel or VPN.

GET /health

Returns ok. Liveness probe.

GET /status

{ "model_id": "xynthis-local", "serving": "teacher_backbone", "vocab_size": 49152, "jspace": false }
serving is "student" once a fine-tuned LoRA adapter is attached, otherwise "teacher_backbone". jspace reports whether workspace capture is on for this process.

GET /v1/models

Advertises the model id (xynthis-local by default, override with XYNTHIS_LLM_MODEL_ID). Also reachable as /models; the /v1 prefix is optional on every route.

POST /v1/chat/completions

Standard OpenAI request body: model, messages (array of {role, content}), temperature, top_p, max_tokens, stream. One extension field:
  • session_id (string, optional): treat successive requests with the same id as one conversation. The server keeps the KV cache per session and reuses it when the new prompt extends the previous turn, which skips re-processing the shared prefix. Idle sessions are evicted after 30 minutes.
With "stream": true the response is SSE: one chunk per token delta, then a finish chunk and data: [DONE].

The xynthis_jspace field

When the server runs with XYNTHIS_JSPACE=1, every chat response carries an additive xynthis_jspace block: the reply’s dominant concepts, read out from the model’s own activations while it generated:
"xynthis_jspace": {
  "concepts": [
    { "label": "rust", "weight": 0.31, "rank": 1 },
    { "label": "memory", "weight": 0.18, "rank": 2 }
  ],
  "generated_positions": 42
}
Non-streaming responses carry it as a top-level field; streaming responses attach it to the final finish chunk. Standard OpenAI clients ignore the unknown field. Capture is off by default and costs one top-k softmax per generated token when on. See Workspace readouts.