Skip to content
Developers

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.

  1. 1In the console, API Keys → new key. Keys begin with mk_live_ and are shown once.
  2. 2Set base_url to https://api.modelion.ai/v1.
  3. 3Pass either a catalog model id or combo/<key> in the model field.
 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_01JQ8Z3K7M2N4P6R8T0V2X4Y6A

Every 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/completionsChat completions. SSE with stream: true.
POST/v1/embeddingsVector generation. Anthropic models have no embeddings; route to an OpenAI-wire or vLLM model.
GET/v1/modelsThe models your key can reach — with policy constraints already applied.
POST/v1/prompts/{id}/completionsCalling 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_…RequestRequiredYour virtual key. You never supply a provider key; Modelion holds those.
X-Modelion-OrganizationRequestWhich organization you are calling on behalf of, when you belong to several. Not needed with one.
Modelion-Route-TagRequestA free-form tag for selecting a branch in a combo's decision graph. Policy sees it as input.request.routeTag.
Modelion-DecisionResponseA digest of the decision under 512 bytes: winning rule, effect, model, region, obligations, evaluation time.
Modelion-Request-IdResponseThe 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.

kvkk-baseline.yaml
 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.detectedWhether PII was found
signals.pii.typesTypes found: tckn_tr, credit_card, iban_tr, email, phone
signals.pii.availableWhether the scan could run — do not mistake its absence for a clean prompt
signals.injection.detectedA prompt injection attempt
signals.secrets.detectedKeys or tokens inside the prompt
principal.subscription.tierThe caller's tier
request.promptTokensInput 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.

StatusCodeMeaning
400unknown_prompt_variableA variable not declared in the template was sent.
400missing_prompt_variableA variable the template requires is missing.
403policy_deniedA rule refused the request. The body carries the user message the rule wrote.
429rate_limitedThe key's or the organization's rate limit.
429budget_exceededA budget ceiling was exceeded. The budget hierarchy applies separately at key and organization level.
503no_eligible_candidateNo 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.