Ragmux

Integrations

Ragmux as an OpenAI-compatible API gateway

Put OpenAI, DeepSeek, vLLM or any Chat Completions server behind one endpoint — how field passthrough, streaming usage and error codes actually behave.

Updated: Verified against Ragmux 0.5.0

Most of what makes Ragmux useful for an OpenAI-compatible provider isn’t translation — openai, deepseek and custom_openai connections already speak the shape Ragmux exposes, so there’s nothing to rewrite. What a gateway adds instead is everything around the request: one endpoint regardless of which of those three actually answers, budgets and rate limits enforced before the provider is ever called, and a passthrough contract precise enough that provider-specific parameters keep working without Ragmux having to know about each one by name.

Adding the connection

POST /admin/api/models creates it. provider_type: openai and provider_type: deepseek default to their own hosted APIs and only need an api_key and a model_name; provider_type: custom_openai is the general case — vLLM, LM Studio, LiteLLM’s own proxy, text-generation-inference’s OpenAI route, Ollama’s /v1 shim, or literally anything else speaking the Chat Completions format — and requires an explicit base_url, used exactly as given rather than having a suffix appended the way openai and deepseek get, so the /v1 has to be included yourself (http://vllm:8000/v1, for instance). A host on a private or local network — a Compose service name like vllm, localhost, the Docker host — is refused by default; add it to PRIVATE_UPSTREAM_ALLOWLIST, or set ALLOW_PRIVATE_UPSTREAMS=true, before the connection will save. See Private upstreams and the Ollama & vLLM gateway page for the full mechanics. An api_key is optional for custom_openai, matching that most self-hosted servers don’t require one. POST /admin/api/models/{id}/test with {"mode": "chat"} sends one bounded ping — the model is asked to reply with the word pong, capped at 256 tokens — and records last_test_ok, last_test_latency_ms and, on failure, a redacted last_test_error on the connection itself, so a broken key or an unreachable server shows up on the connection’s own status rather than in front of a real caller.

What gets forwarded exactly as sent

/v1/chat/completions only actually parses the fields it needs to route and bill a request — model, messages, stream, temperature, top_p, max_tokens (or max_completion_tokens), stop, n, tools, tool_choice, response_format, stream_options, user. Every other top-level field in the request body is kept verbatim and, for an OpenAI-compatible connection, sent upstream unchanged. That’s what lets a seed, a logprobs flag, an OpenAI reasoning_effort value, or vLLM’s guided_json keep working through Ragmux with no gateway-side support for that specific field — the passthrough is the feature, not a gap waiting to be closed. The same mechanism carries one gateway-specific addition the other direction: an optional top-level ragmux object is read and consumed by the gateway itself, never forwarded upstream, and {"ragmux": {"include_context": true}} is what asks a RAG-backed project to return the retrieved sources on a non-streaming response.

Three fields are the exception, because the gateway itself needs to control them: model is handled by the gateway and replaced with the effective model name before the provider is called. On a project without chat routes the project’s connection decides which model answers, and whatever value you send is only echoed back in the response. Once the project has chat routes, model selects one: a route’s exact name, the default connection’s model name, or an empty value; anything else is a 404 with code model_not_found. The response still carries the value you sent. stream reflects what the client actually requested. And stream_options is rewritten to {"include_usage": true} against the upstream on every streamed request, regardless of what the client sent, because the request log, budgets and cost estimate all need token counts — but see below for what that means for what the client itself receives back.

The streaming usage trailer is opt-in, on every provider

Ragmux always asks its upstream for usage on a streamed response, but it only relays the final trailer chunk — one data: line carrying an empty choices array and a usage object, just before data: [DONE] — to a client whose own request set stream_options: {"include_usage": true}. This isn’t specific to OpenAI-compatible connections; the same rule holds for Anthropic, Gemini and Ollama, whose adapters all produce that trailer internally regardless. Leave stream_options off, and a stream ends at [DONE] with no trailing empty-choices frame to trip up code that indexes choices[0] on every chunk without checking its length first. Ask for it, and the last chunk before [DONE] is exactly what OpenAI’s own streaming usage feature would send.

That said, a choices: [] frame isn’t always Ragmux’s own trailer — some upstreams emit frames of their own with an empty array, such as Azure’s content-filter chunk. Those pass through untouched rather than being dropped, since they carry information the gateway doesn’t own the right to discard. Code that guards array access before indexing handles both cases the same way.

Errors: the provider’s 4xx, a mapped 5xx, or a specific Ragmux code

A malformed request — bad JSON, a missing messages field, or no user message for an Anthropic or Gemini connection — is a 400 before anything reaches a provider, as is an image URL that a Gemini connection can’t inline. Tools are not a source of such errors: every chat provider type supports them, and the Gemini adapter sanitizes a tool schema and logs the keywords it drops rather than refusing it. Budget and rate-limit rejections are 429 with a code naming which tier and which limit (rate_limit_rpm, budget_daily, and so on) so a client can distinguish “slow down” from “this month’s ceiling is spent” programmatically instead of parsing a message string. A provider’s 4xx is relayed with its own status and message, credentials redacted. Upstream 5xx responses are mapped instead: 503 and 504 pass through, Anthropic’s 529 becomes 503, and every other 5xx becomes 502, so a client can tell “the upstream is overloaded or slow, retry” from “the upstream broke”.

GET /v1/models lists what the project can actually answer

An OpenAI SDK that lists available models before choosing one still works against Ragmux, but it’s worth knowing what it returns: {"object": "list", "data": [...]} in OpenAI’s own shape, describing what the calling project can reach, not the provider’s full catalogue.

A project without chat routes lists its single connection’s model, and {id} in GET /v1/models/{id} is ignored, because there’s only one answer it could give. A project with chat routes lists each chat route’s name, plus the default connection’s model name unless a route already uses it; with more than one entry, {id} has to match one of them or the request is a 404. A sk-user-… key that spans several projects and names none lists one entry per granted project’s connection instead, deduplicated by model name. Both endpoints need the models scope on a sk-user-… key; the project’s sk-proj-… key carries no scopes and reaches them without one.

This follows from how selection works: Ragmux doesn’t send a request to whichever model the client names, so listing every model the upstream provider offers would describe capabilities the project can’t reach. A client that wants an end user to pick between gpt-4o and gpt-4o-mini defines one route per choice, each with its own target, and the listing then shows those route names.

Swapping the provider behind an existing integration

Because the client only ever holds a project key and calls /v1/chat/completions, moving a project from openai to custom_openai — pointing an existing integration at a self-hosted vLLM server instead of a hosted API, say — is a change to the model connection, not to any application code. The same is true in the other direction: a project can start on a local server during development and move to a hosted provider before launch without either environment’s calling code ever mentioning which one is active. That’s also what makes tracking spend per project hold up across the swap — the request log, budgets and cost estimate all key off the project, not the provider answering it, so a mid-life provider change doesn’t fragment the history into two things that need reconciling by hand.

Full reference: /docs/api/.

Point an OpenAI-compatible client at Ragmux

  1. Create a model connection

    POST /admin/api/models with provider_type openai, deepseek or custom_openai, an api_key, and a model_name. custom_openai also needs an explicit base_url — used exactly as given, so include the /v1 suffix yourself — covering vLLM, LM Studio, LiteLLM or any other Chat Completions server; a host on a private or local network (a Compose service name, localhost) is refused unless it's added to PRIVATE_UPSTREAM_ALLOWLIST, or ALLOW_PRIVATE_UPSTREAMS=true is set.

  2. Test the connection

    POST /admin/api/models/{id}/test with {"mode": "chat"} sends a bounded ping and records the outcome on the connection, so a bad key or unreachable server is caught before a project depends on it.

  3. Attach it to a project and get a key

    POST /admin/api/projects with model_connection_id set returns a project API key exactly once. That key, not the provider's own, is what every application uses from here on.

  4. Point your existing OpenAI SDK at Ragmux

    Set base_url to your Ragmux instance and api_key to the project key; every field your client already sends — including provider-specific ones — reaches the upstream unless the request targets a translated provider like Anthropic or Gemini.

Frequently asked questions

Does Ragmux support every OpenAI Chat Completions field?
Yes for the OpenAI-compatible providers — openai, deepseek and custom_openai — since the request is forwarded as-is apart from model, stream and stream_options, which the gateway itself controls; anything else, known or provider-specific, passes through unchanged.
Can I send vLLM-specific fields like guided_json through Ragmux?
Yes — /v1/chat/completions parses only the fields it knows about (model, messages, temperature, tools, and similar) and keeps every other top-level field verbatim, so a custom_openai connection to vLLM receives guided_json, seed, logprobs or any other provider-specific option exactly as sent.
Why didn't I get a final usage chunk on a streamed response?
Ragmux only sends the usage-only trailer chunk when the client's own request set stream_options: {"include_usage": true}, matching OpenAI's own behavior; the gateway always asks the upstream for token counts internally to build the request log, but withholds the trailer from a client that never asked for it, since indexing choices[0] on it would break unprepared code.
What happens if I send tools to a provider that doesn't support them the way OpenAI does?
Every chat provider type supports tools. An OpenAI-compatible connection forwards them as-is since the format already matches; Anthropic and Gemini connections translate them, and Gemini sanitizes the tool schema, logging any keywords it has to drop instead of rejecting the request. A request that can't be translated is a 400 only in a few narrow cases, such as a body with no user message.
Does the model name I send in the request actually pick the model?
Only if the project has chat routes. Without routes, model is not used for routing: the project's own connection decides which model answers, and the value you sent is only echoed back in the response's model field (an empty value is replaced with the connection's model name). With chat routes, model selects a route by exact name, or the default connection's model name, or an empty value; anything else is a 404 model_not_found.

Arrow keys to move, Enter to open.