← Blog
Product

Ragen: what we shipped in the second half of September

LiteLLM stops being a required service, the queues move to BullMQ, and the API starts speaking the OpenAI format. A review of what changed in Ragen AI, with a warning for anyone already running a stack.

The second half of September had one theme: fewer things you have to maintain for Ragen to work. One required service is gone, a second stopped being required.

If you already run a stack, read the section about two installs on one machine before you upgrade. There is one thing there to do by hand.

Ragen calls model providers itself

The LiteLLM proxy was a service without which Ragen did not answer. Through the first half of the month it became one of two paths; now it is gone as a built-in mode. Ragen calls providers directly, and a route table says which model goes where.

An external gateway still attaches — any one that speaks the OpenAI format: Portkey, vLLM, Ollama, or LiteLLM itself. You declare it as a route with provider: openai-compatible and supply two variables, LLM_<NAME>_BASE_URL and LLM_<NAME>_API_KEY. The difference is that this is now a choice belonging to whoever runs the deployment, rather than a condition for the product working at all.

For a self-hoster that means one fewer service and one fewer set of credentials to keep in step. The price is that provider credentials now have to reach the web, api and worker processes rather than only a proxy container. LLM_GATEWAY, LITELLM_PROXY_URL and LITELLM_MASTER_KEY are out of the environment.

An Anthropic key now works on its own, with no Bedrock or Vertex in the path. OpenRouter joined the list of providers, and the installer offers it alongside Scaleway.

Most of the care in this change went into the things that could have switched themselves off in silence. The Cohere reranker resolved the proxy address as its fallback and degrades to “no reranking” when the reranker is unreachable, so deleting the proxy would have turned it off without a word. It was re-pointed. Three models the worker uses for fallback PDF parsing and SRT subtitles had no entry in the route table and would have stopped resolving — two of them are named by constants in source, which is why the preflight that exists to catch exactly this could not see them. It can now. Speech synthesis got fixed along the way: it was pointed at the proxy, which serves no audio route, so it returned 404 in every environment and nobody reported it, because it had never worked.

npm run gateway:preflight --probe makes one real call per configured model and says whether a deployment can actually serve what it is configured to use.

The queues move to BullMQ

Temporal was the engine behind background work: document ingest, nightly schedules, document generation. It is an adapter now, and the default engine is BullMQ on Redis. Temporal has left the default install.

Two places show it. The worker image is 180 MB smaller, because it ships one job runtime instead of two — 1.01 GB rather than 1.19 GB. And a queue dashboard arrived: the worker has served bull-board from the start. Set WORKER_ADMIN_URL on the admin app and a “Queue Dashboard” item appears in its sidebar.

If you use WORKER_RUNTIME=temporal, you need to rebuild the image. The Temporal SDK is a build-time dependency now, so the published image no longer starts on Temporal — it stops at boot. Building the worker image with apps/worker’s devDependencies installed restores it.

Ingest slimmed down internally while this happened: document text stopped riding in job payloads, which shrank to two identifiers.

Two installs on one machine

docker-compose.yml pinned its container, volume and network names — ragen-postgres, ragen-postgres-data, ragen-network — instead of letting Compose prefix them per project. Those names are global to the Docker daemon, so a second install collided on every container name and, worse and invisibly, attached to the first install’s Postgres and Qdrant volumes. The names belong to Compose now, and two checkouts coexist with no configuration at all.

If you already run a stack, this is the one thing to do by hand. Your data is in volumes named ragen-postgres-data and ragen-qdrant-data. A docker compose up after this change creates new, empty ones named for your project directory (ragen-app_postgres_data). Nothing is deleted — the old volumes are still there — but the stack comes up blank. To keep the data, copy it across once, with the stack stopped:

docker run --rm -v ragen-postgres-data:/from -v ragen-app_postgres_data:/to \
  alpine sh -c 'cd /from && cp -a . /to'

Substitute your own project name — docker compose config --format json | jq -r .name prints it — and repeat for qdrant. Or start fresh and re-ingest.

RAGEN_STACK_NAME is gone; COMPOSE_PROJECT_NAME does that job now, and the installer writes it into the new install’s .env. That matters, because Compose names a project after its directory, so two installs in directories with the same name would still have shared everything.

Published ports are not prefixed by anything and still collide. The installer probes them before it starts the stack and tells you which one is taken and what to set.

The API speaks the OpenAI format

POST /v1/chat/completions required an assistant_id field, which the OpenAI format has no slot for — so n8n, the OpenAI SDKs and anything else speaking that protocol got a 400 before the request reached anything. The choice of assistant moved onto the API key: a key reaches either one assistant or the whole knowledge base, the panel says which, and assistant_id became an optional field that has to agree with the key.

The key’s scope is a boundary, not a default. A key issued for one assistant refuses a request naming a different one rather than obliging it. The same rule now applies on /v1/chat, /v1/search, /v1/threads, /v1/assistants and /v1/files — so a key handed to an outside integrator reaches one assistant and not the rest of the organisation, which it could before.

GET /v1/models arrived, answering with the models this deployment can actually reach and the organisation is allowed to use. An OpenAI-compatible client fills its model picker from it instead of showing an error there. Read-only, and narrower than it sounds: it is the intersection of the catalogue, the route table and the per-org allowlist, so a model it lists is one that will answer.

The MCP server got the same treatment: assistant_id is optional in its tools, because a key scoped to the whole knowledge base — the default — previously could not call them at all.

We wrote this up separately, with node-by-node configuration: n8n asks, Ragen answers.

The installer and the first run

create-ragen-app reached 0.6.2 and stopped asking about things a fresh install does not need, while starting to ask about the ones that cost something.

PII masking is a question now, and the answer is no by default. Presidio is two containers and the analyser alone is the heaviest thing in the stack — 959 MB idle, more than the document parser — so a trial install no longer pays for a feature most evaluations never reach. Say yes and the wizard writes both Presidio URLs and starts the stack with the Compose profile those services sit behind. The docs also stopped claiming Presidio starts by default: a plain docker compose up has never started it, which makes the idle stack about 900 MB rather than the 1.9 GB the memory table adds up to.

A fresh install scaffolds onto BullMQ and writes the REDIS_URL that runtime needs. If you pick Temporal, the wizard asks where the server is instead of writing localhost:7233 on your behalf. The next-steps block also names npm run worker:dev — without a worker an upload is accepted and never parsed.

Ragen now states which Node versions it runs on, and the installer refuses the rest in the first second. jsdom, a transitive dependency that arrived with no version bump of ours, requires ^22.22.2 || ^24.15.0 || >=26.0.0, and the repository sets engine-strict=true, so npm stops rather than warns. engines.node said >=24, so create-ragen-app cleared Node 24.13, cloned, wrote .env.local, started Docker, and only then hit EBADENGINE. It is ^24.15.0 || >=26.0.0 now — a range, not a minimum, because that range skips the whole Node 25 line and no >= can say so. The refusal names the reason, which is what someone who already has “Node 24”, or a newer Node, needs to hear.

A busy Docker no longer ends the install with an error.

ragen-cli

npm i -g ragen-cli gives you a ragen command. Today it only scaffolds an installation — ragen create my-app is npx create-ragen-app@latest with the same flags — and the commands that are not built (login, doctor, kb, plugin) are listed in ragen help under “Not built yet”.

In brief

  • Re-indexing a document no longer leaves the old version in the index. Every re-embed added a fresh set of chunks without removing the previous ones, so retrieval quoted text that had been replaced, and quoted it twice (#1209)
  • An answer saying the documents contain nothing about a topic no longer carries a citation. The citation rule distinguished only between sentences that use the context and sentences that do not — a statement of absence is neither, so the model cited anyway. Measured: 5 refusals in 50 carrying a citation before the fix, 0 in 50 after (#1218)
  • GET /v1/files no longer answers for other organisations. It filtered on the API key’s assistant, and no key has ever carried one, so the filter matched nothing. Nothing was exposed, because no key has been issued anywhere yet — but the first one issued would have leaked (#1236)
  • Cancelling an ingest takes effect immediately, and keeps working after the job has stopped being tracked. It used to be a message sent into a running workflow’s memory: the file stayed “processing” in the interface, cancelling a job the engine had already forgotten raised an error, and a worker restart lost the request (#1207)
  • A deployment whose encryption key does not work says so at startup, instead of failing one message at a time. Having the variables set was treated as having a working key, so a missing permission or a key from another project produced a generic “an unexpected error occurred” on every question
  • The worker’s default concurrency is the measured one rather than the cautious one: ten cost a bulk upload about twice the latency of twenty, 24.9s per document against 12.5s (#1221)
  • A local install without TEMPORAL_SERVER_ADDRESS falls back to localhost:7233 instead of connecting to the literal string "undefined" (#1203)

Full release list: docs.ragen.ai/changelog


Related posts:

Want to see this on your own documents? Book a free consultation – in 30 minutes we will show Ragen on your own scenario.