Migration
A base URL and a key move an OpenAI client across. This page also states what the API does not do, because a migration that discovers that in production is not a migration.
The drop-in shape
Point the client’s base URL at https://api.llmeu.com/v1 and give it an API key from the console. The request shape does not change.
from openai import OpenAI
client = OpenAI(base_url="https://api.llmeu.com/v1", api_key="llmeu_live_xxxxxxxx.your-secret")
resp = client.chat.completions.create(
model="llmeu-auto",
messages=[{"role": "user", "content": "Say hello from EU hosting."}],
)
print(resp.choices[0].message.content)
What the chat endpoint accepts
The request validator in apps/api/src/app.mjs checks the following; what it does not check is passed through or ignored rather than rejected, so a newer client keeps working.
- model and messages are required; messages must be a non-empty array.
- Each message needs a role (system, user, assistant, tool or developer) and content as a string or an array of parts.
- stream must be a boolean when present.
- temperature, top_p, max_tokens, presence_penalty, frequency_penalty, seed and n must be numbers when present.
- max_tokens must be at least 1.
- n must be exactly 1; anything else is refused.
- stop may be a string or an array of strings.
- tools must be an array when present.
- llmeu must be an object; it is never required.
- tool_choice and response_format are forwarded to the backend together with the fields above; an unknown extra field is not rejected.
Request extras under llmeu
A namespaced llmeu object in the request body; a client that ignores unknown fields can send it safely.
| Field | Value | Effect |
|---|---|---|
| residency | eu-hosted | eu-owned | de | any | Narrows routing to endpoints in that residency; an empty eligible set returns 409 no_endpoint_for_policy rather than widening the filter. |
| task | a task name | The task the router fits the model to. |
| retention | zero | Only zero is implemented. Asking for more than the policy allows returns 403 retention_exceeds_policy; an allowed effective value other than zero returns 501 retention_not_implemented. |
| data_class | public | internal | confidential | restricted | Checked against the policy’s allowed data classes: a class outside them returns 403 data_class_not_allowed, and an unknown class returns 400 invalid_data_class. |
| policy_id | uuid | Names a policy of the same organization; an unknown id returns 404 policy_not_found. |
| max_usd_per_1m | a number | A price ceiling per million tokens for this request. |
| allow_partners | true | false | Whether partner endpoints may be used for this request. |
Unknown keys inside llmeu are ignored; a known key with an invalid value is refused, because a bad data_class has to fail closed rather than silently mean no data class.
curl https://api.llmeu.com/v1/chat/completions \
-H "Authorization: Bearer $LLMEU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "llmeu-auto",
"messages": [{"role": "user", "content": "Summarise this contract."}],
"llmeu": {"residency": "eu-owned", "data_class": "confidential", "retention": "zero"}
}'
What does not exist
These answers are deliberate, and a client that expects them sees the error code rather than a silent substitution.
| Request | Result |
|---|---|
| POST /v1/completions | 501 not_implemented — the legacy completions endpoint is not offered; use /v1/chat/completions. |
| n > 1 | 400 unsupported_n — only n = 1 is supported. |
| POST /v1/embeddings | Embeddings need an embedding model, and the default requested id is bge-m3. They are currently served by the deterministic local backend, whose response carries llmeu.mock: true and the note that deterministic placeholder vectors are returned and no embedding weights are served on this deployment. |
| /v1/files, /v1/batches, /v1/assistants, /v1/fine_tuning, /v1/audio, /v1/images | 404 unknown_route — not routed at all; a /v1 path the router does not know answers 404 unknown_route, and any other path answers 404 not_found. |
Taking your data out
The account export me.json holds selected account fields, organizations, API-key metadata without secrets, current policies, the latest 200 traces per organization and daily usage aggregates for 365 days. It is not a complete copy of all personal data and not a historical policy record.
The NDJSON audit export defaults to 7 days, allows at most 90 days and caps traces at 5000; its manifest carries counts.truncated, which marks an export that reached the cap.
GET /app/exports/me.json
GET /app/exports/audit.ndjson?days=7
GET /app/exports/traces.json
GET /app/exports/usage.json
GET /app/exports/policies.json
Deleting the account itself happens on the profile page and is subject to the ownership rule on the console page.
A session cookie is not an API credential
A browser session authenticates the console, not the API. An OpenAI client that sends one is refused with 403 api_key_required and has to use an API key instead.
A refusal here names a code, so a migration can branch on it, and the llmeu block on every response says what actually happened.