The desktop assistant
Félagi Desktop has an assistant: a panel where somebody asks, in words, for what the app's own commands can do — "which issues are mine this cycle", "move ACM-42 to review". It is built on Elyra's assistant, and Elyra's assistant asks Claude.
It can ask through this server. Then no key is on anybody's laptop: the desktop sends the Messages API request it would have sent Anthropic, with the person's own API token, and the server adds its key, checks what is being asked, and passes the answer back.
Off by default, like the other two places where Félagi's server calls a model (minutes, tidying up prose — see Configuration). An installation that decided nothing sends nothing anywhere.
Turning it on
FELAGI_ASSISTANT_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-…
The key and the address are the AI SDK's own (ANTHROPIC_API_KEY, and ANTHROPIC_URL, which
defaults to https://api.anthropic.com/v1) — the ones minutes already use. anthropic is the
only provider: the desktop sends the Messages API's own body. Anything else, or no key, and
the route answers 503 without calling anybody.
| Variable | Default | |
|---|---|---|
FELAGI_ASSISTANT_PROVIDER |
empty — off | anthropic turns it on |
FELAGI_ASSISTANT_MODELS |
claude-opus-5-5,claude-sonnet-5-5 |
The models the desktop may ask for, comma-separated. Anything else is a 422 |
FELAGI_ASSISTANT_MAX_TOKENS |
16000 |
A larger max_tokens is lowered to this. Thinking counts against it, and Opus 5.5 always thinks — a cap of 4,096 cuts answers off |
FELAGI_ASSISTANT_PER_MINUTE |
30 |
Requests per person per minute, across all their devices and tokens. Over it is a 429 with retry-after |
FELAGI_ASSISTANT_STREAM |
false |
Pass stream: true through as Claude writes it. Off because Askr holds a streamed response until it is finished; the desktop then asks for each answer whole |
FELAGI_ASSISTANT_FALLBACKS |
true |
Ask the API to answer a refusal with its fallback model ("fallbacks": "default" and the server-side-fallback-2026-07-01 beta). Only against api.anthropic.com |
GET /api/v1/me says whether it is on, as features.assistant, so the desktop hides the
assistant instead of offering one that answers 503.
The route
POST /api/elyra/assistant/v1/messages
Authorization: Bearer fat_…
Content-Type: application/json
anthropic-version: 2026-10-21
{ "model": "claude-opus-5-5", "max_tokens": 16000,
"system": "…", "messages": [ … ], "tools": [ … ] }
Elyra's contract (Backend::assistant() in the desktop), not the API's, which is why it is
outside /api/v1: it versions with Elyra and with Anthropic. Any of the person's API tokens
— a read token is enough, since asking a question moves nobody's work — and the API's own
rate limit applies as well.
The answer is Claude's, unchanged: its status, its body and its retry-after, a 429,
529, 5xx or 400 included, so the desktop's own handling reads the real thing. On
success the server dispatches App\Events\AssistantUsed — the person, the workspace, the
model, inputTokens, outputTokens, never the prompt — and logs the same. Nothing else is
stored.
What is checked, in order
Every refusal is in the Messages API's error shape, which the desktop shows:
{ "type": "error", "error": { "type": "invalid_request_error", "message": "Streaming isn't proxied." } }
| Answer | |
|---|---|
| Not switched on | 503 api_error — The assistant isn't set up on the server. Nothing is sent anywhere |
| No token, or a bad one | 401, as everywhere in the API |
Over FELAGI_ASSISTANT_PER_MINUTE |
429 rate_limit_error, with retry-after in seconds |
| A body that is not a JSON object | 400 invalid_request_error |
A model not in FELAGI_ASSISTANT_MODELS |
422 invalid_request_error — The model `…` isn't allowed here. Exact ids: a dated id is refused |
stream: true with streaming off |
422 invalid_request_error — Streaming isn't proxied. Elyra reads this as "this server does not stream" and asks again whole |
A tool with a type other than custom — web search, code execution, any of Claude's own |
422 invalid_request_error — Server tools aren't allowed here. The desktop's tools are its commands |
max_tokens above the cap |
Lowered to it, not refused. Missing, it is the cap |
| Claude cannot be reached, or does not answer in 120 seconds | 502 api_error |
A proxy, deliberately raw
The body is read as JSON and written back as JSON with only max_tokens (and fallbacks)
touched, and sent with Laravel's HTTP client — not rebuilt through an SDK, which knows the
fields of the day it was released and drops the rest. A field the desktop starts sending next
month reaches Claude. Objects stay objects: a tool's "properties": {} is not turned into [].
anthropic-version is passed on (2026-10-21 when the desktop sends none), and so is
anthropic-beta, with the fallback beta added.
Streaming
With FELAGI_ASSISTANT_STREAM=true, stream: true is passed through as Claude writes it, as
text/event-stream with X-Accel-Buffering: no; the usage is read off the events on the way.
Leave it off behind anything that buffers.
What leaves the server
Whatever the desktop's assistant puts in the request: the person's words, the window's note of where they are, and what the app's commands returned to it — issue titles, comments, names. That is the honest cost, and it is why this is off until an operator decides. The key never leaves the server, and nothing about a request is logged but who, which model and how many tokens.