One line changes, the rest stays
Modelion speaks an OpenAI-compatible wire format. You keep your existing SDK, your existing request body and your existing streaming code; the only thing that changes is base_url. This page covers what is added — the extensions, the response headers, and the contract on the policy side.
1. Quickstart
Create a virtual key in the console, change base_url, send the request. Nothing else changes except passing a combo key where a model id would go.
- 1In the console, API Keys → new key. Keys begin with
mk_live_and are shown once. - 2Set
base_urltohttps://api.modelion.ai/v1. - 3Pass either a catalog model id or
combo/<key>in themodelfield.
1from openai import OpenAI 2 3client = OpenAI( 4 # The only line that changes. 5 base_url="https://api.modelion.ai/v1", 6 api_key="mk_live_...", 7) 8 9response = client.chat.completions.create(10 # A combo, not a model: the candidate chain lives in the console.11 model="combo/production-chat",12 messages=[{"role": "user", "content": "Summarise this invoice."}],13 stream=True,14)1516for chunk in response:17 print(chunk.choices[0].delta.content or "", end="")The decision digest that comes back with the response
Modelion-Decision: v1;
rule=kvkk-pii-route-resident;
effect=route;
model=gpt-4o-mini;
region=tr-west-1;
obligations=redact,no-fallback;
cache=off;
eval=181us;
requestId=req_01JQ8Z3K7M2N4P6R8T0V2X4Y6AEvery sample uses your mk_live_… virtual key. A key is bound to one organization, one region and one budget.
2. Endpoints
The surface is deliberately small. There is nothing beyond the following; governance and routing live in policy rather than in the request body.
| POST | /v1/chat/completions | Chat completions. SSE with stream: true. |
| POST | /v1/embeddings | Vector generation. Anthropic models have no embeddings; route to an OpenAI-wire or vLLM model. |
| GET | /v1/models | The models your key can reach — with policy constraints already applied. |
| POST | /v1/prompts/{id}/completions | Calling a prompt from the registry by its id. |
3. Headers
One header is required on the request. Two come back on the response, so you can explain the decision afterwards.
Authorization: Bearer mk_live_…RequestRequired | Your virtual key. You never supply a provider key; Modelion holds those. |
X-Modelion-OrganizationRequest | Which organization you are calling on behalf of, when you belong to several. Not needed with one. |
Modelion-Route-TagRequest | A free-form tag for selecting a branch in a combo's decision graph. Policy sees it as input.request.routeTag. |
Modelion-DecisionResponse | A digest of the decision under 512 bytes: winning rule, effect, model, region, obligations, evaluation time. |
Modelion-Request-IdResponse | The id used to pull the full decision trace. |
4. Combos
When you pass combo/<key> in the model field, a candidate chain decides which model actually runs. The chain is defined in the console; changing it is not a deploy. A constraint raised by policy intersects that chain — policy can remove a candidate from the list, but never add one.
1{ 2 "model": "combo/production-chat" 3} 4 5# A combo is a candidate chain, resolved at request time: 6# 7# 1. gpt-4o-mini @ tr-west-1 8# 2. llama-3.3-70b @ tr-west-1 9# x claude-sonnet-4 @ eu-east-1 ← removed by the residency constraint10#11# The chain lives in the console, not in your code. Changing it is not12# a deploy.5. Streaming
stream: true returns SSE in the OpenAI shape. That holds regardless of which provider is used: Anthropic's multi-event framing is mapped onto this stream, so your client code does not change when the route does. No failover happens after the first chunk — once the stream has started it is committed.
1# Server-sent events, in the OpenAI shape regardless of upstream. 2# An Anthropic model's multi-event framing is mapped onto this stream, 3# so the client code does not change when the route does. 4 5data: {"choices":[{"delta":{"content":"Müşteri"},"index":0}]} 6data: {"choices":[{"delta":{"content":" limiti"},"index":0}]} 7data: {"choices":[{"delta":{},"finish_reason":"stop","index":0}]} 8data: [DONE]6. Calling a prompt by id
You do not have to ship prompt text with your application. You send an id and the variables; the template is filled in at the gateway. The ordering matters: hydration runs before policy, because the personal data is in the variables rather than the template. The guardrail pipeline has to see the real text.
An unknown variable returns 400, and so does a missing required one. Nothing is silently substituted with an empty string — discovering in production that half a prompt was missing is expensive.
7. Policy rule sets
Rules are evaluated in order, the first match wins, and the losers are recorded. There are five effects: route, constrain, deny, redact, cache. The merge rule is one sentence: constraints intersect, obligations union. A constraint written at the platform baseline cannot be loosened by an organization rule beneath it.
1apiVersion: modelion.ai/v1 2kind: PolicyRuleSet 3metadata: 4 name: kvkk-baseline 5 class: compliance 6 7spec: 8 - name: pii-detected-pin-resident 9 when:10 match: all11 clauses:12 - field: signals.pii.types13 operator: containsAny14 values: [tckn_tr, iban_tr]15 - field: signals.pii.available16 operator: is17 value: true18 then:19 effect: route20 route:21 candidates: [gpt-4o-mini]22 fallbackToOriginal: false23 obligations:24 redactTypes: detected25 cache: { mode: off }26 auditTags: [pii_detected]2728 - name: scan-unavailable-be-careful29 when:30 - field: signals.pii.available31 operator: is32 value: false33 then:34 effect: constrain35 constraints:36 allowedModels: [gpt-4o-mini, llama-3.3-70b]37 obligations:38 cache: { mode: off }39 auditTags: [pii_scan_unavailable]signals.pii.detected | Whether PII was found |
signals.pii.types | Types found: tckn_tr, credit_card, iban_tr, email, phone |
signals.pii.available | Whether the scan could run — do not mistake its absence for a clean prompt |
signals.injection.detected | A prompt injection attempt |
signals.secrets.detected | Keys or tokens inside the prompt |
principal.subscription.tier | The caller's tier |
request.promptTokens | Input token count — for routing by range |
request.headers.* | Your own headers, a data class for instance |
runtime.health.* | Provider health, bucketed |
8. Error codes
These are the ones that come from policy or budget. Provider errors are returned with normal OpenAI semantics.
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_prompt_variable | A variable not declared in the template was sent. |
| 400 | missing_prompt_variable | A variable the template requires is missing. |
| 403 | policy_denied | A rule refused the request. The body carries the user message the rule wrote. |
| 429 | rate_limited | The key's or the organization's rate limit. |
| 429 | budget_exceeded | A budget ceiling was exceeded. The budget hierarchy applies separately at key and organization level. |
| 503 | no_eligible_candidate | No candidate left. Where a residency rule is in force this is deliberate — the request is not moved to another region. The way to reduce it is to define several candidates inside the region. |
9. Explaining the decision afterwards
Every response carries a Modelion-Decision digest. You pull the full trace with the requestId. The trace also lists the rules that matched and lost — in an audit, the answer to "did this rule fire" can be "it matched but a more specific rule won", and that is a different answer from "it did not match".
1curl "https://api.modelion.ai/v1/traces/req_01JQ8Z3K7M2N4P6R8T0V2X4Y6A" \ 2 -H "Authorization: Bearer $MODELION_API_KEY" 3 4{ 5 "requestId": "req_01JQ8Z3K7M2N4P6R8T0V2X4Y6A", 6 "decision": { 7 "winner": "pii-detected-pin-resident", 8 "effect": "route", 9 "matchedButLost": ["default-allow-with-cache"],10 "obligations": ["redact", "no-fallback", "audit"],11 "evalMicros": 18112 },13 "route": { "model": "gpt-4o-mini", "region": "tr-west-1", "attempts": 1 },14 "usage": { "promptTokens": 1204, "completionTokens": 286, "costUsd": 0.00042 }15}This page is a summary of the wire format. The full field-level reference and the OpenAPI schema are in the console, after you sign in.
To try it with your own traffic
A sandbox key and a divergence report measured in shadow mode are what the first session produces.