Documentation

API reference

Authentication, the OpenAI-compatible endpoint surface, streaming, reasoning, and the error taxonomy.

Authentication

Every request to /v1/* authenticates with a virtual key in the Authorization: Bearer header. Mint keys in the Console; the secret is shown once and only its hash is stored. A key carries its own rate limits, budget, tags, an optional model allow-list (empty = any model), and sparse option_overrides that deviate from organisation policy (data-protection mode, NER model, injection-scan mode).

A request whose model is not on the key's non-empty allow-list is refused with a sealed 403 permission_error before anything is dispatched; the refusal itself lands in the audit chain.

Minting a production key requires a verified email address.

curl https://api.sluis.ai/v1/models \
  -H "Authorization: Bearer $SLUIS_KEY"

# a model outside the key's allow-list never dispatches:
# → 403 permission_error · the refusal is sealed in the audit chain

Endpoints

Sluis exposes the OpenAI-compatible surface below. Endpoints without a first-class handler are proxied verbatim to the routed provider, streaming included, so OpenAI-compatible upstreams keep full fidelity.

EndpointPurpose
POST /v1/chat/completionsChat completions, the primary surface: routing, data protection, caching, streaming.
POST /v1/completionsLegacy text completions.
POST /v1/embeddingsEmbeddings; also feeds the semantic cache.
POST /v1/moderationsModeration classification.
POST /v1/responsesThe OpenAI Responses API.
POST /v1/ocrDocument OCR (Mistral, or fully local with model sluis/ocr) · billed per page. With the dlp_documents policy on, inline documents are anonymized before they leave.
POST /v1/documents/anonymizeDocument anonymization · PII swapped for «MERGE_TAG»s, images blurred · billed per page/image.
POST /v1/documents/anonymize/jobsAsync anonymization jobs · enqueue large documents, poll status, fetch the result via a time-limited signed URL that needs no API key.
GET /v1/modelsThe models your policy and credentials can actually reach, nothing hypothetical.
GET /v1/models/{id}One model's metadata.
POST /v1/audio/*Transcription, translation, speech · proxied to the routed provider.
POST /v1/images/*Image generation and edits · proxied.
POST /v1/video/generationsVideo generation · proxied.
/v1/filesFile operations · proxied.
POST /v1/messagesNative Anthropic Messages ingress · point any Anthropic-SDK tool at Sluis; any connected model.
POST /v1/messages/count_tokensLocal token estimate for Anthropic-SDK context management; nothing is dispatched.
POST /v1beta/models/{model}:generateContentNative Google Gemini ingress · point a Gemini SDK at Sluis (:streamGenerateContent streams).
curl https://api.sluis.ai/v1/chat/completions \
  -H "Authorization: Bearer $SLUIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "mistral/mistral-large-latest",
        "messages": [{ "role": "user", "content": "Say hi" }] }'
# Document OCR, billed per page. Returns pages[] markdown + usage_info.pages_processed.
# model "sluis/ocr" runs the gateway's own OCR engine: fully local, no provider egress (inline data: URLs only).
curl https://api.sluis.ai/v1/ocr \
  -H "Authorization: Bearer $SLUIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "mistral/mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://example.com/invoice.pdf" } }'
# Document anonymization, billed per page/image. PII becomes «MERGE_TAG»s; images are blurred.
curl https://api.sluis.ai/v1/documents/anonymize \
  -H "Authorization: Bearer $SLUIS_KEY" \
  -F file=@contract.docx \
  -F 'options={ "entities": ["person_name", "email", "iban"], "include_mapping": true }'

Document input

Attach a PDF, Word, image, or plain-text file to a chat request as a "type": "file" content part carrying a base64 file_data data URL. Any routed model accepts it: providers that understand file parts natively receive them unchanged, and for every other provider the gateway converts the document before dispatch. Provider-side file_id references are not resolved; inline the bytes as file_data.

Vision-capable models get each PDF page rendered as an image_url part (48-page budget per request, shared across all attached documents); models without image input get the document's extractable text inlined as prompt text, which the data-protection scan then covers like any other text. Longer or heavier documents belong on /v1/ocr. With the dlp_documents policy on, the document is anonymized or refused before anything leaves the gateway. When it is off and the organisation's DLP mode is protective (tokenize, mask or block), documents that would travel as uninspectable images are refused; under allow_log they pass with an explicit unscanned marker in the audit trail.

# Attach a document as an OpenAI-style `file` content part. The gateway
# normalizes it for the routed provider, so this works on any model.
curl https://api.sluis.ai/v1/chat/completions \
  -H "Authorization: Bearer $SLUIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "scaleway/gemma-4-26b-a4b-it",
        "messages": [{ "role": "user", "content": [
          { "type": "file",
            "file": { "filename": "drawing.pdf",
                      "file_data": "data:application/pdf;base64,'"$(base64 -w0 drawing.pdf)"'" } },
          { "type": "text", "text": "Which discipline does this drawing document?" }
        ] }] }'

Streaming

Set stream: true and the response arrives as server-sent events: each frame is a chat.completion.chunk delta and the stream ends with data: [DONE]. Streams are never buffered in the gateway: they tee through it, and the audit seal and metering happen even if the client hangs up early.

Ask for stream_options.include_usage and the final frame carries exact token usage, the same numbers the gateway meters and bills.

stream = client.chat.completions.create(
    model="mistral/mistral-large-latest",
    messages=[{"role": "user", "content": "Write a haiku"}],
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")

Reasoning

Pass the OpenAI reasoning_effort parameter (minimal | low | medium | high) on any thinking-capable model. Sluis translates it per provider (Gemini's thinking level, Claude's adaptive thinking effort) and omits it where a model would reject it, so one parameter works across the whole catalog.

resp = client.chat.completions.create(
    model="vertex/claude-opus-4-8",
    messages=[{"role": "user", "content": "Prove it step by step…"}],
    reasoning_effort="high",  # minimal | low | medium | high
)

Error codes

Errors use the OpenAI error envelope; the error.type value mirrors the HTTP status, so your SDK's error handling keeps working unchanged.

CodeWhen
400 invalid_request_errorMalformed request body or parameters.
400 invalid_request_errorModel id missing its provider prefix. Every callable id is provider/model, e.g. mistral/mistral-large-latest; the body reads: model must be provider-prefixed.
400 invalid_request_errorRequest carries the removed x-sluis-dlp header. Per-request overrides were replaced by per-key option overrides; configure them in Console → API keys.
401 authentication_errorMissing or unknown API key.
402 insufficient_quotaNo active plan or budget reached. Activate a plan or raise the budget; the request never reaches a provider.
403 permission_errorThe key lacks permission, for example the model is not on its allow-list.
422 invalid_request_errorRefused before dispatch, for example data protection in block mode matched the request.
422 document_too_large_to_scanA document page stays over the render pixel cap even at the minimum scan resolution. Large-format pages (A0/A1 drawings) render downscaled automatically; the dedicated code lets a client shrink the page and resubmit.
429 rate_limit_errorRate limit reached. Enforced at the gateway; the request never hits a provider.
451 permission_errorBlocked by residency policy: no allowed jurisdiction serves the request. The body includes the reason.
5xx api_errorUpstream provider failure after retries; the circuit breaker steers traffic around unhealthy providers.
{
  "error": {
    "message": "model must be provider-prefixed, e.g. mistral/mistral-large-latest",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request"
  }
}