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
residencyeu-hosted | eu-owned | de | anyNarrows routing to endpoints in that residency; an empty eligible set returns 409 no_endpoint_for_policy rather than widening the filter.
taska task nameThe task the router fits the model to.
retentionzeroOnly 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_classpublic | internal | confidential | restrictedChecked 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_iduuidNames a policy of the same organization; an unknown id returns 404 policy_not_found.
max_usd_per_1ma numberA price ceiling per million tokens for this request.
allow_partnerstrue | falseWhether 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/completions501 not_implemented — the legacy completions endpoint is not offered; use /v1/chat/completions.
n > 1400 unsupported_n — only n = 1 is supported.
POST /v1/embeddingsEmbeddings 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/images404 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.