AIvantage
For developers

API documentation

AIvantage is an OpenAI- and Anthropic-compatible router. Your application changes only the base URL and the key; the calls otherwise stay the same.

Address and key

  • OpenAI formathttps://app.aivantage.fi/v1
  • Anthropic formathttps://app.aivantage.fi

OpenAI libraries use the address ending in /v1. Anthropic libraries and Claude Code add the path themselves, so give them the address without /v1.

You create the key for an application in the portal, and it is shown only once. Send it in the header Authorization: Bearer <AIVANTAGE_KEY> or x-api-key: <AIVANTAGE_KEY>. Either header works with either format. A wrong or revoked key returns 401 invalid_api_key.

API error messages are always in English.

Endpoints

EndpointPurpose
POST /v1/chat/completionsOpenAI Chat Completions. The cascade (aivantage/auto) or a locked model, streaming, tools and images.
POST /v1/responsesOpenAI Responses API for provider-hosted tools (xAI x_search and web_search). xAI models only, no streaming.
POST /v1/messagesAnthropic Messages. Anthropic models as they are, or aivantage/auto; the answer is always in Anthropic's format.
POST /v1/messages/count_tokensAnthropic's token counter, not billed. When Anthropic cannot count, for example when the cascade has no Anthropic model, the router estimates the count itself and adds the header x-aivantage-estimate: 1.
GET /v1/modelsThe models the application may use. With the anthropic-version header, in Anthropic's format.
GET /v1/models/{model_id}One model in the same shape. An unknown model: 404 model_not_found.

Models

Model nameBehaviour
aivantage/autoThe cascade. The request goes first to the policy's cheapest step, and quality gates move it to a stronger model only when needed. The steps are set in the portal. Works on /v1/chat/completions and /v1/messages. Without a model, /v1/chat/completions uses the cascade.
aivantage/<id>A locked model. The request goes straight to this model, without the cascade. The id also works without the prefix (e.g. claude-sonnet). GET /v1/models lists the models the application may use.
gpt-5.5, grok-4.7Provider model names do not work in the OpenAI format: the answer is 404 model_not_found. Use an id from /v1/models.
claude-sonnet-5-5On /v1/messages, Anthropic's own names, dated snapshots (e.g. -20251001) and the [1m] suffix also work. For other providers' models the answer is 400 messages_model_not_anthropic.

The response's model field names the model that answered (aivantage/<id>). If the application is locked to one model in the portal, the request goes to it. If the lock rejects other models, a request for another model returns 409 app_model_locked.

Streaming, tools and images

Streaming (stream: true)

CascadeWorks in both formats. The cascade settles the answer first, and the finished answer is sent as SSE chunks, so the first chunk arrives only when the answer is ready. If the policy's stream_mode is fixed, the stream starts at once from the fixed model.
Locked modelA true stream from the provider. On /v1/messages, Anthropic's events arrive unchanged.
/v1/responsesNo streaming: 400 stream_not_supported.

Tools (function calling)

CascadeWorks. A quality gate checks the tool call: the tool must be in the request and the arguments valid JSON with the required fields. An invalid call moves the request to the next step.
Locked modelWorks if the model supports tools. Otherwise 403 capability_not_supported.
/v1/messagesAnthropic's own tools (e.g. web search), documents and images in tool results work only with Anthropic models. In the cascade such a request skips the other steps.
/v1/responsesThe tools x_search, web_search and function.

Images

CascadeWorks (image_url; image in the Anthropic format). Steps whose target does not support images are skipped.
Locked modelWorks if the model supports images. Otherwise 403 capability_not_supported.
/v1/responsesinput_image is passed to the provider.

The privacy filter does not read images, so an image is always an uninspected attachment. If the policy blocks uninspected attachments, the request is refused before anything is sent: 403 privacy_blocked, reason attachment_not_inspected.

Headers

In the request

Authorization: Bearer
x-api-key
The AIvantage key. Either one.
X-AIvantage-End-UserAn end-user id for the privacy log. Only a keyed hash of it is stored. Alternatively the OpenAI field safety_identifier or user, or the Anthropic field metadata.user_id.
X-AIvantage-Session-IdA session id for the privacy log. Claude Code's X-Claude-Code-Session-Id is read the same way.
X-AIvantage-Privacy-OverrideA reason for letting a privacy block through. Accepted only from a key the policy names, with an allowed reason and for identifier types the policy lets through. The request is always logged, accepted or not.
anthropic-versionOn /v1/models, the list and errors come back in Anthropic's format. On /v1/messages the header is not forwarded: the router sends its own version.
anthropic-betaForwarded to Anthropic. Account-bound betas (e.g. files) are forwarded only on the tenant's own Anthropic key (BYOK).

In the response

x-aivantage-request-idThe request id (req_…). The same id is in the portal's request log and in the request_id field of an error body. Give it to support.
Retry-AfterOn a rate-limit 429: the seconds after which the request can be retried. Also x-ratelimit-limit-*, x-ratelimit-remaining-* and x-ratelimit-reset-*.
x-aivantage-estimateValue 1 when /v1/messages/count_tokens returns the router's own estimate.

Errors

In the OpenAI format the reason is in error.code and the class in error.type: routing_error, policy_blocked, privacy_blocked, invalid_request_error or billing_error.

{"error": {"message": "The model is not allowed by the tenant's policy.",
           "type": "policy_blocked", "code": "model_not_allowed",
           "request_id": "req_…"}}

In the Anthropic format (/v1/messages) the type follows the HTTP status as in Anthropic's API, and the code is at the end of the message in square brackets. 503 no_healthy_instance is returned as 529 overloaded_error so that the SDKs retry. When the provider itself refuses the request, its own error is returned unchanged.

{"type": "error",
 "error": {"type": "permission_error",
           "message": "The model is not allowed by the tenant's policy. [model_not_allowed]"},
 "request_id": "req_…"}

Retry on 429 (wait for Retry-After), 502 and 503. Other 4xx errors come from the request or the settings, and the same request will not succeed on retry.

HTTPerror.codeMeaning
400invalid_requestinvalid_request_errorThe body is not JSON or a required field is missing (model, messages, input).
400stream_not_supportedinvalid_request_error/v1/responses does not stream. Send the request without stream: true.
400unsupported_toolinvalid_request_error/v1/responses accepts only the tools x_search, web_search and function.
400responses_model_not_supportedrouting_error/v1/responses only supports models with provider-hosted tools (xAI, e.g. aivantage/grok).
400messages_model_not_anthropicrouting_errorThis endpoint serves Anthropic models and aivantage/auto; use /v1/chat/completions for other providers' models.
400messages_auto_needs_anthropicrouting_errorThe request uses an Anthropic-only feature, but the cascade has no usable Anthropic model.
400messages_shared_account_featurerouting_errorFiles, containers, skills and MCP servers require the customer's own Anthropic key (BYOK); they are not available with the shared AIvantage key.
400privacy_blockedprivacy_blockedBlocked by the privacy filter: the request contained an identifier the customer's policy blocks. It was not sent to any model.
401invalid_api_keyinvalid_request_errorThe key is missing, wrong or revoked. In the Anthropic format the type is authentication_error.
402pilot_endedbilling_errorThe pilot has ended and the service has not been activated. Type billing_error, the same in the Anthropic format.
402provider_billingrouting_errorThe provider refused the key: credits exhausted or monthly limit reached. The message carries the provider's own explanation.
403privacy_blockedprivacy_blockedBlocked by the privacy filter: the request contained an identifier the customer's policy blocks. It was not sent to any model.
403privacy_own_capacity_unsupportedprivacy_blockedThe request contained an identifier the customer's policy keeps on own capacity, and own capacity does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model.
403policy_invalidpolicy_blockedThe customer's processing policy is missing or invalid; the request was not processed.
403model_not_allowedpolicy_blockedThe customer's policy doesn't allow this model.
403no_eligible_instancepolicy_blockedNo target allowed by the policy serves this model.
403capability_not_supportedpolicy_blockedThe model's provider does not support a feature the request uses (streaming, tools or images).
403no_allowed_cascade_steppolicy_blockedThe cascade has no step allowed by the policy.
403jurisdiction_not_allowedpolicy_blockedThe jurisdiction is not allowed by the policy.
403external_not_allowedpolicy_blockedExternal cloud services are not allowed by the policy.
403provider_not_allowedpolicy_blockedThe provider is not allowed by the policy.
403sovereignty_offpolicy_blockedSovereignty is switched off, so AIvantage's machines are not used.
403dedicated_to_other_tenantpolicy_blockedThe instance is dedicated to another customer.
403critical_requires_dedicatedpolicy_blockedOnly own capacity dedicated to the customer is allowed.
403credential_not_configuredpolicy_blockedThe policy does not define whose key is used to call the provider (your own or the AIvantage key).
403billing_agreement_requiredpolicy_blockedAIvantage's own models require an approved billing agreement. Use your own API key (BYOK) or activate the service in the portal.
403platform_key_not_allowedpolicy_blockedAIvantage's API keys are not used for the customer's language model calls without an approved billing agreement. Use your own API key (BYOK).
403app_model_not_allowedpolicy_blockedThe application's model is not in the allowed regions. Choose an allowed model for the application.
403sovereignty_fi_unsupportedpolicy_blockedThe application's requests run only on AIvantage's hardware in Finland, and that hardware does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model.
404model_not_foundrouting_errorNo such model. Use aivantage/<id> or a plain <id> from /v1/models.
404endpoint_not_supportedinvalid_request_errorNo such path (e.g. /v1/embeddings). The message lists the supported endpoints.
405method_not_allowedinvalid_request_errorThe path exists but the method is wrong (e.g. GET /v1/chat/completions).
409app_model_lockedpolicy_blockedThe application is locked to one model, and its settings reject requests for other models.
413context_length_exceededrouting_errorThe input is longer than the context of the model the policy allows. No answer was returned.
429usage_limit_tokenspolicy_blockedThe application's monthly token limit has been reached.
429usage_limit_eurpolicy_blockedThe application's monthly spending limit (EUR) has been reached.
429platform_spend_limitpolicy_blockedThe customer's monthly cap on AIvantage's provider keys has been reached. Use your own key (BYOK) or ask AIvantage to raise the cap.
429rate_limit_exceededrequests | tokensThe tenant's rate limit is reached: requests or tokens per minute, or concurrent requests. The reason is in the reason field, the wait in the Retry-After header.
4xxupstream_rejectedrouting_errorThe provider rejected the request. The HTTP status and the message are the provider's own.
499client_disconnectedrouting_errorThe client closed the stream. Shown only in the request log.
500internal_errorrouting_errorAn internal router error. Give the request_id to support.
502all_targets_failedrouting_errorAll targets allowed by the policy failed; the request was not routed elsewhere.
502upstream_stream_errorrouting_errorThe stream broke at the provider's end during the answer. The error arrives as the last SSE event.
503no_healthy_instancerouting_errorAll instances allowed by the policy are down.
503locked_version_unavailablerouting_errorThe locked model version is unavailable; the version is not changed without approval.
503credential_missingrouting_errorThe provider key is missing: your own key (BYOK) has not been saved or the AIvantage key has not been set.
503app_model_unavailablepolicy_blockedThe application's locked model is not available right now, and no fallback model is chosen for the application.
503sovereignty_fi_unavailablepolicy_blockedThe application's requests run only on AIvantage's hardware in Finland, and that hardware is not available right now. The request was not sent to any model.
503privacy_filter_unavailableprivacy_blockedThe privacy filter could not finish its check, and the customer's policy requires one before sending (fail-closed). The request was not sent to any model.
503privacy_own_capacity_unavailableprivacy_blockedThe request contained an identifier the customer's policy keeps on own capacity, and no own capacity the policy allows can serve it. The request was not sent to any model.

Rate limits (429 rate_limit_exceeded)

The shape is the same as the providers' rate limits: error.type is requests or tokens, error.code is rate_limit_exceeded, and the router's own reason is in error.reason: tenant_rate_requests, tenant_rate_tokens, tenant_concurrency.

Privacy block (privacy_blocked)

A blocked request is not sent to any model. The error.privacy field gives the reason (reason), an explanation in Finnish and English (message_fi, message_en) and the locations (violations), never the blocked values.

HTTPerror.privacy.reasonMeaning
403entity_blockedThe request contains an identifier the policy blocks. Locations: error.privacy.violations (no values).
403attachment_not_inspectedAn attachment or image could not be inspected, and the policy blocks uninspected attachments.
400unsupported_contentA message has a content block of a type the filter does not know.
503request_too_longThere is more text to inspect than the tenant's settings allow.
503filter_timeoutThe check did not finish in time, and the policy requires it (fail-closed).
503filter_errorThe check failed, and the policy requires it (fail-closed).
503privacy_own_capacity_unavailableThe request contained an identifier the customer's policy keeps on own capacity, and no own capacity the policy allows can serve it. The request was not sent to any model.
403privacy_own_capacity_unsupportedThe request contained an identifier the customer's policy keeps on own capacity, and own capacity does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model.

If an override was requested but not accepted, the error.privacy.override field gives the reason:

  • key_not_allowed The policy does not let this key override the block.
  • reason_not_allowed The reason is not on the policy's list of allowed reasons.
  • type_not_overridable This identifier type may not be overridden.

Not supported

The router serves only the endpoints listed above. For example embeddings, image generation and audio are not available: /v1/embeddings, /v1/images/*, /v1/audio/*, /v1/files, /v1/batches, /v1/fine_tuning/*, /v1/moderations, /v1/completions, /v1/assistants, /v1/realtime.

An unknown /v1 path returns 404 and the supported endpoints:

{"error": {"message": "POST /v1/embeddings is not supported by AIvantage. Supported: …",
           "type": "invalid_request_error", "param": null,
           "code": "endpoint_not_supported"}}

Examples

Replace <AIVANTAGE_KEY> with the application's key. The application's page in the portal has the same examples with its models, plus the Claude Code and LangChain settings.

OpenAI · Python

pip install openai
from openai import OpenAI

client = OpenAI(
    base_url="https://app.aivantage.fi/v1",
    api_key="<AIVANTAGE_KEY>",
)
resp = client.chat.completions.create(
    model="aivantage/auto",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)

OpenAI · JS/TS

npm install openai
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://app.aivantage.fi/v1",
  apiKey: "<AIVANTAGE_KEY>",
});
const resp = await client.chat.completions.create({
  model: "aivantage/auto",
  messages: [{ role: "user", content: "Hello!" }],
});
console.log(resp.choices[0].message.content);

Anthropic · Python

pip install anthropic
import anthropic

client = anthropic.Anthropic(
    base_url="https://app.aivantage.fi",
    api_key="<AIVANTAGE_KEY>",
)
msg = client.messages.create(
    model="aivantage/auto",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)
print(msg.content[0].text)

cURL

OpenAI format: /v1/chat/completions
curl https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer <AIVANTAGE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "aivantage/auto", "messages": [{"role": "user", "content": "Hello!"}]}'
Anthropic format: /v1/messages
curl https://app.aivantage.fi/v1/messages \
  -H "x-api-key: <AIVANTAGE_KEY>" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model": "aivantage/auto", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello!"}]}'

Test the features

These requests check that each feature works. First put the app's key in a variable: export AIVANTAGE_API_KEY=…

Shield's Mask, Own capacity only and Block, attachment blocking and Sovereignty are in use once the service is activated. During the pilot Shield only logs.

Key and model list

Checks that the key works and which models the app may use.

curl
curl -s https://app.aivantage.fi/v1/models \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY"

What you should see: Status 200. The list has aivantage/auto, whose aivantage.cascade field holds the cascade steps, and every model the app may use. A model's aivantage.class field tells where it runs (own is AIvantage's own capacity, the others eu, us and cn), and aivantage.jurisdictions the countries.

Wrong key

Checks that a wrong key is refused.

curl
curl -s https://app.aivantage.fi/v1/models \
  -H "Authorization: Bearer vaara-avain"

What you should see: Status 401, error.code is invalid_api_key.

Cascade

Checks that the cascade picks a model and answers.

curl
curl -si https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Vastaa yhdellä sanalla: mikä on Suomen pääkaupunki?"}]}'

What you should see: Status 200. The model field holds the model that answered (aivantage/<id>), one of the cascade models of aivantage/auto in the model list. An easy question usually goes to the first step. The x-aivantage-request-id header equals the id field, and the request shows in the portal's request log under that id. The usage field holds the billed tokens.

Locked model

Checks that the request goes to exactly the requested model. Use a model id from the model list.

curl
curl -s https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/gpt-luna","messages":[{"role":"user","content":"Sano pelkkä: ok"}]}'

What you should see: Status 200, and the model field holds the requested model. A model not on the list returns 404 model_not_found.

Anthropic format

Checks that the Anthropic libraries' API works with the same key.

curl
curl -s https://app.aivantage.fi/v1/messages \
  -H "x-api-key: $AIVANTAGE_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","max_tokens":50,"messages":[{"role":"user","content":"Sano pelkkä: ok"}]}'

What you should see: Status 200. The answer is in Anthropic's format (type is message), and the model field holds the model that answered (aivantage/<id>).

Streaming

Checks that the answer arrives as SSE chunks.

curl
curl -sN https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","stream":true,"messages":[{"role":"user","content":"Laske yhdestä viiteen."}]}'

What you should see: Lines data: {…} where object is chat.completion.chunk, with the same id and model in every chunk. Last comes a chunk with usage, then data: [DONE].

Shield: personal identity code

Checks what Shield does with a personal identity code. The model writes the code one character at a time, so the answer shows what the model received. Setting: The action on the Personal identity code row of the portal's Shield page.

curl
curl -s https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Kirjoita tämä tunniste merkki kerrallaan välilyönnein erotettuna, ei muuta: 131052-308T"}]}'
SettingWhat you should see
LogStatus 200, and the answer has 1 3 1 0 5 2 - 3 0 8 T: the model received the value. The finding shows only in the portal.
MaskStatus 200, and the answer has H E T U _ 1: the model received a placeholder. When the model repeats the placeholder as is, AIvantage puts the original value back into the answer.
Own capacity onlyStatus 200, and the model field holds a model whose aivantage.class in the model list is own.
BlockStatus 403. error.code is privacy_blocked and error.privacy.reason entity_blocked. The error.privacy.violations field names the location, never the value.

Sovereignty

Checks that the request runs in Finland. Repeat the request without the identity code and compare. Setting: The app's Sovereignty. Critical in Finland needs the personal identity code on the Critical data list.

curl
curl -s https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Henkilötunnus 131052-308T. Sano pelkkä: ok"}]}'
SettingWhat you should see
Critical in FinlandWith the identity code, the model field holds a model whose aivantage.class in the model list is own and aivantage.jurisdictions FI. Without the code the request is routed as usual.
All in FinlandIn both requests the model field holds a model whose aivantage.class is own and aivantage.jurisdictions FI.
Finnish capacity unavailableStatus 503 sovereignty_fi_unavailable when the setting is Block. With Allow to the cloud and log a cloud model answers.

Uninspected attachment

Checks that an image Shield cannot read does not reach a model. Setting: Shield page, Advanced settings, Attachments: Block the request.

curl
curl -s https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","messages":[{"role":"user","content":[{"type":"text","text":"Mitä kuvassa on?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="}}]}]}'

What you should see: Status 403. error.code is privacy_blocked and error.privacy.reason attachment_not_inspected.

Rate limit

Checks that the limit stops a burst. The model aivantage/rajatesti does not exist, so the requests use no tokens.

curl
seq 800 | xargs -P 50 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://app.aivantage.fi/v1/chat/completions \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/rajatesti","messages":[{"role":"user","content":"hei"}]}' | sort | uniq -c

What you should see: The output has a 429 line and the rest 404. A single 429 response has error.code rate_limit_exceeded, error.reason tenant_rate_requests and the headers Retry-After and x-ratelimit-limit-requests.

Unknown endpoint

Checks that an endpoint AIvantage does not serve says so.

curl
curl -s https://app.aivantage.fi/v1/embeddings \
  -H "Authorization: Bearer $AIVANTAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"aivantage/auto","input":"hei"}'

What you should see: Status 404, error.code is endpoint_not_supported, and the message lists the supported endpoints.