Skip to content

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)
  • :project is 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. global works.
  • :publisher/:model becomes the model name, e.g. google/gemini-2.5-flash or anthropic/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 is 403 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

{ "cache": { "mode": "strict", "ttl_seconds": 3600 } }
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=…