Proxy apps & gateways¶
A proxy app is the thing your application talks to. Each one exposes the provider protocols as gateways under its URL:
https://<core>/v1/proxy-apps/<app-id>/openai/… OpenAI protocol
https://<core>/v1/proxy-apps/<app-id>/gemini/… Gemini API protocol
https://<core>/v1/proxy-apps/<app-id>/vertex/… Vertex AI protocol
Axium does not translate between protocols. The body you send goes to the
provider as-is; the only edits are the model field (replaced by the deployed
model name) and, for streamed chat completions, stream_options.include_usage
= true so usage can be recorded (your client receives one extra chunk with an
empty choices array). Provider errors come back verbatim.
Gateways¶
OpenAI gateway¶
Prefix /openai or /openai/v1. Serves openai and azure-openai
providers, and any provider whose models speak the OpenAI protocol.
| Method | Path | Surface | Streaming |
|---|---|---|---|
POST |
chat/completions |
openai.chat |
yes |
POST |
responses |
openai.responses |
yes |
GET |
models, models/:model |
openai.models |
|
POST |
files (multipart, forwarded intact; openai providers only) |
openai.files |
|
GET |
files, files/:id, files/:id/content (text only) |
openai.files |
|
DELETE |
files/:id |
openai.files |
Not available: /embeddings, /images, /audio, /batches, /uploads.
Gemini gateway¶
Prefixes /gemini, /gemini/v1, /gemini/v1beta. Serves google-aistudio
and google-enterprise providers.
| Method | Path | Surface | Streaming |
|---|---|---|---|
POST |
models/:model:generateContent |
gemini.generate |
|
POST |
models/:model:streamGenerateContent (?alt=sse optional) |
gemini.generate |
yes |
POST |
models/:model:countTokens (not billed) and other verbs |
gemini.generate |
|
GET |
models, models/:model |
gemini.models |
|
POST |
interactions (model or agent) |
gemini.interactions |
yes |
GET |
interactions/:id (?stream=true) |
gemini.interactions |
yes |
POST / DELETE |
interactions/:id/cancel, interactions/:id |
gemini.interactions |
|
POST |
/gemini/upload/v1beta/files, /gemini/upload/v1/files (resumable, 2 steps; google-aistudio only) |
gemini.files |
|
GET / DELETE |
files, files/:id |
gemini.files |
OpenAI-compatible facade (google-aistudio only): POST
/gemini[/v1|/v1beta]/openai/chat/completions, GET …/openai/models,
GET …/openai/models/:model. Lets the OpenAI SDK talk to Gemini models by
changing its base URL. No Responses API on this facade.
Resumable uploads return an upload URL for the second step. Set
PUBLIC_BASE_URL (and TRUST_PROXY) behind TLS so that URL points back
through Axium with the right scheme.
Vertex AI gateway¶
Prefix /vertex, /vertex/v1 or /vertex/v1beta1. Serves google-enterprise
providers only.
POST /vertex/v1/projects/:project/locations/:location/publishers/:publisher/models/:model:generateContent
POST …:streamGenerateContent (streaming)
POST …:countTokens (not billed)
:projectis ignored; the proxy app is what gets billed and the provider's configured project is what gets called.:location(^[a-z0-9-]+$) decides the Google region.globalworks.:publisher/:modelbecomes the model name, e.g.google/gemini-2.5-flashoranthropic/claude-sonnet-4-5.- There is no model catalogue under
/vertex/…/publishers/:publisher/models.
Not supported¶
No Anthropic Messages dialect (/v1/messages), no embeddings, image or audio
endpoints, no OpenAI Uploads API for files over 512 MB. Methods accepted on
the gateways: GET, POST, PUT, DELETE.
Request pipeline¶
Every runtime request goes through these checks, in order. The first failure
ends the request with the HTTP status shown and a trace with that
denial_reason (schema).
flowchart TD
A[API key valid · scoped to this app] -->|401 / 403| X((deny))
A --> B[Streaming supported on this surface]
B --> C[firewall.inbound · client IP]
C --> D[App exists · status ready]
D --> E[Budget for the current period]
E --> F[Model allowed · unambiguous · provider active · protocol matches]
F --> G[dlp_prompt · mask or block]
G --> H{cache strict?}
H -->|hit| R[200 · x-axium-cache: hit]
H -->|miss / off| P[Forward to provider]
P --> S[Record usage · emit trace]
Models and defaults¶
- A proxy app sees the models in its allow-list plus models marked
global. Manage with
PATCH /v1/proxy-apps/:id/allowed-models. - The client may name a model (
"model": "gpt-4o-mini") or omit it and get the app's default model (PATCH /v1/proxy-apps/:id/default-models). Omitting it on a surface where the SDK requires it still needs a placeholder; Axium ignores the value only when the app resolves a default. - A name matching more than one allowed model is
400 model_ambiguous; one matching none is403 model_not_allowed. - A fallback model can be configured but is not used by the runtime in this version.
Budget¶
| Field | Meaning |
|---|---|
budget_limit |
Decimal, in the currency of the model prices. |
budget_period |
daily, weekly, monthly or null. Calendar windows evaluated in the database's time zone. null = a lifetime budget that never resets. |
When the app's spend in the current window reaches the limit, requests are
refused with 429 and denial_reason = budget_exhausted. The counter resets
lazily on the first charge of the next window. Spend is computed from token
usage and model prices; models without prices do not count.
rate_limit_rpm and rate_limit_tpm exist on apps and models and are
stored but not enforced in this version. Put rate limiting at your
ingress if you need it.
Policies¶
Each proxy app carries a JSON policy document, validated on
PATCH /v1/proxy-apps/:id (a 400 lists each invalid field with a stable
code).
cache¶
mode |
Behaviour |
|---|---|
off |
Default. |
strict |
Exact-match cache keyed on an MD5 of the canonicalised body plus the requested model, per app and model. Hits answer 200 with x-axium-cache: hit, no tokens, no cost, no provider call. |
semantic |
Accepted but not implemented; behaves as off. |
Only non-streamed generations without previous_response_id /
previous_interaction_id are cached, and only successful JSON responses.
Entries live in rm.request_io; expired rows are removed with
DELETE /v1/proxy-apps/:id/cache/expired or the global
DELETE /v1/proxy-apps/cache/expired.
firewall¶
{
"firewall": {
"inbound": {
"default_action": "deny",
"rules": [
{ "action": "allow", "type": "cidr", "value": "10.0.0.0/8" },
{ "action": "allow", "type": "cidr", "value": "203.0.113.0/24" }
]
}
}
}
First matching rule wins; up to 256 rules. An unknown client IP (no
TRUST_PROXY behind a proxy) falls to default_action. Denials are 403
firewall_denied. The outbound section is stored but not applied.
dlp_prompt¶
{
"dlp_prompt": {
"enabled": true,
"options": {
"countries": ["ES", "FR", "DE"],
"policy": {
"default": "mask",
"kinds": { "financial": "block" },
"entities": { "EMAIL": "keep" }
}
}
}
}
Scans the prompt (instructions, user text) for personal identifiers using
@axium-lab/euro-pii. Per kind or per entity:
| Action | Effect |
|---|---|
mask |
The value is replaced in the forwarded body by <ENTITY>. Binary and URL parts are left alone. |
block |
403 with denial_reason = dlp_blocked and message Request contains data this proxy app does not allow. |
keep |
Forwarded unchanged; the finding is still recorded. |
If scanning fails the request proceeds and the trace notes action: error.
Findings (entity names and kinds, never values) land in
attributes.dlp_prompt of the trace. dlp_file and capture sections are
accepted but not applied in this version.
Headers¶
Request headers Axium reads¶
| Header | Goes to |
|---|---|
Authorization: Bearer ax_… or x-goog-api-key: ax_… |
Authentication |
x-axium-user-id |
trace client_user_id |
x-axium-meta-tag |
trace meta_tag |
x-axium-<anything> |
trace tags.<anything> |
No x-axium-* header is forwarded. Also stripped before forwarding:
authorization, x-goog-api-key, api-key, x-api-key, cookie, host,
hop-by-hop headers, and the key query parameter.
Response headers Axium adds¶
| Header | When |
|---|---|
x-axium-trace-id |
Always. The trace's id; log it on the client to correlate. |
x-axium-cache: hit |
Served from cache. |
content-type: text/event-stream, cache-control: no-cache, x-accel-buffering: no |
Streaming responses. |
Removed from provider responses: connection, content-encoding,
content-length, keep-alive, transfer-encoding.
Errors¶
Provider errors are relayed verbatim with their status. Errors Axium generates are encoded in the gateway's dialect (OpenAI or Gemini error JSON) with these statuses:
| Category | HTTP |
|---|---|
invalid_request, content_filter |
400 |
auth_error |
401 |
permission_denied |
403 |
not_found |
404 |
rate_limit, quota_exceeded |
429 |
server_error, unknown |
500 |
timeout |
504 |
Every error carries an x-axium-trace-id; the trace has error_origin,
error_category, error_message and, when Axium refused the request,
denial_reason.
Lifecycle of a proxy app¶
# 1. create (status: draft)
POST /v1/proxy-apps {"name":"billing-assistant","description":"…"}
# 2. allow models and pick a default
PATCH /v1/proxy-apps/:id/allowed-models {"model_ids":["…"]}
PATCH /v1/proxy-apps/:id/default-models {…}
# 3. budget and policies
PATCH /v1/proxy-apps/:id {"budget_limit":100,"budget_period":"monthly","policies":{…}}
# 4. publish (status: ready) and create a key
PATCH /v1/proxy-apps/:id/publish
POST /v1/rm/api-keys {"name":"prod","scope_type":"proxy_app","proxy_app_id":":id"}
# later
PATCH /v1/proxy-apps/:id/deactivate # 403 app_not_ready for every caller
GET /v1/proxy-apps/:id/stats?from=…&to=…