Skip to content

Migrate from OpenAI / OpenRouter

Two lines change: the base URL and the key. Below that, the list of what behaves differently.

The two lines

client = OpenAI(
    base_url="https://api.keln.ai/v1",   # was: default
    api_key=os.environ["KELN_API_KEY"],  # was: OPENAI_API_KEY
)
client = OpenAI(
    base_url="https://api.keln.ai/v1",   # was: https://openrouter.ai/api/v1
    api_key=os.environ["KELN_API_KEY"],  # was: OPENROUTER_API_KEY
)

Model names are the open ecosystem's own ids, canonically lowercase (deepseek-ai/deepseek-v4-flash, openai/gpt-oss-120b, …), the full list comes from /v1/models or the Models & pricing page. Matching is case-insensitive.

What carries over unchanged

  • messages, stream, temperature, top_p, max_tokens / max_completion_tokens, stop, n, penalties, seed, logprobs, the standard surface, normalized per model
  • Tool calling and structured output (tools, tool_choice, response_format with json_object / json_schema), with server-side validation you didn't have before
  • SSE streaming, same chunk shape, same [DONE] terminator, usage in the final chunk
  • The error envelope, {"error": {"message", "type", "code"}}, same as OpenAI. One caution: Keln's message strings were rewritten in plain language and can be reworded again. Match on the HTTP status and error.code, never on message text. Errors

What's different, and why

On Keln
Endpoints /v1/chat/completions and /v1/models. There is no legacy /v1/completions text endpoint.
Reasoning One standard reasoning_effort field on every model (vendor spellings accepted as aliases, including the OpenRouter/Vercel reasoning object). The trace returns in the canonical reasoning_content and is mirrored in reasoning, code that reads either gateway's response shape works unchanged. Details
Caching Automatic. No cache_control blocks (stray ones are tolerated and ignored), repeat prefixes just bill at the per-model cached rate, reported in usage.prompt_tokens_details.cached_tokens. Details
Provider routing There isn't any to configure. No provider preferences, no fallback model arrays, no route pinning, Keln routes on live health and price inside one published rate per model, and verifies quality so you don't choose vendors defensively. Unknown fields from your OpenRouter code are ignored, never fatal, the field ledger below accounts for each one.
Latency control Nothing to configure: Keln holds each route to its own predicted first-token window and hedges past it. There is no deadline knob and no timeout error. Keln retries slow attempts on faster routes. Details
Pricing One published price per model, no per-provider price spread, no surge. Every priced response carries usage.cost, the exact spend in USD, with no usage: {"include": true} flag needed. Models & pricing
Data retention Keln never stores prompts or completions. Details

Constraints you can stop sending, they're already guaranteed

Three OpenRouter provider filters exist to protect you from a marketplace. Keln already guarantees all three, so it accepts the fields and they are always satisfied:

  • provider: {"zdr": true}, every request is zero-data-retention. There is no non-ZDR capacity to route around. Privacy
  • provider: {"data_collection": "deny"}, Keln stores nothing and trains on nothing, so the flag is unnecessary.
  • provider: {"quantizations": [...]}, you filter quantizations on a marketplace because a host might serve a degraded quant. Here, continuous quality verification detects and excludes quant downgrades platform-side.

Every OpenRouter field, accounted for

The full ledger of OpenRouter fields Keln accepts but does not act on: what each one does on OpenRouter and what Keln does with it. None of these can fail a request, and none is silently ignored. Any request carrying inert fields gets them named in the x-keln-inert-params response header.

You send There Here
provider: {order / only / ignore / allow_fallbacks / sort / max_price} steer or filter the provider pool Stripped before dispatch, routing is Keln's job, inside one price and one SLO. See above for zdr / data_collection / quantizations.
models: [...], route: "fallback" cross-model fallback Stripped. Keln fails over and hedges within the same model instead, and one model per request keeps billing and the SLO clear. To use a different model, name it in the request.
transforms: ["middle-out"] silently compress an oversized prompt ⚠ Not offered. An oversized prompt returns 400 context_length_exceeded immediately, before billing. If you relied on middle-out, trim client-side against context_length from /v1/models.
require_parameters: true only route to providers supporting all your params Unnecessary, param-aware routing is always on: a demanded parameter routes to capacity that supports it, or you get an explicit 400 naming it. Details
usage: {"include": true} opt into cost accounting Accepted no-op, usage.cost is present on every priced response, flag or no flag. On streams it arrives in the final usage chunk.
reasoning: {"exclude": true} think internally, omit the trace Ignored, the trace is returned (billing is identical either way; reasoning bills as output tokens regardless). Skip rendering it client-side if you don't want it.
web_search_options, plugins: [{"id": "web"}] turn on web search Turns on web search here too, billed at $10 per 1,000 searches on top of tokens. Remove the field if you don't want it. Web search
prediction predicted outputs Stripped, outside an inference platform's scope; never forwarded, never fatal.
model: "…:nitro" / ":floor" throughput-first / price-first routing hint Suffix stripped, base model served.
model: "…:free" / ":online" free tier / OpenRouter's web search suffix Explicit 400 variant_not_supported, and these two are the entire rejected set. For search, send web_search_options instead (Web search).
numeric error.code handling OpenRouter mirrors the HTTP status as a number in the body Keln's code is a string (OpenAI-shaped, as on Vercel). Match on HTTP status or string codes, see Errors.
readers of response.provider, native_finish_reason, reasoning_details[] marketplace transparency fields Never emitted, responses don't identify serving capacity by design; the trace is returned in reasoning_content / reasoning.
readers of usage.completion_tokens_details.reasoning_tokens itemized reasoning spend, when the serving provider happens to report it Canonical on every route: passed through where the route reports a split, derived server-side with the model's own tokenizer where it doesn't. Reasoning bills as output either way.
readers of /v1/models catalog metadata Same OpenAI shape, three additions: performance (live tps_p50 / ttft_p50_ms / as_of, omitted when there's no fresh figure), added (the real date a model joined, created is a fixed constant kept only for strict clients), and input_modalities. No key needed, and it's CORS-open. Details
mid-stream error events (finish_reason: "error" at HTTP 200) the client handles provider failures mid-stream You can receive exactly one documented shape, and only as a last resort: if failover is exhausted after output began, the stream ends with a terminal chunk carrying finish_reason: "error" and error.code: "stream_interrupted", then [DONE]. Everything short of that is handled by failover. Your existing handler maps onto that one case, keep it. Details

Where to go next