Jev API Channels: Official API vs OpenRouter (How to Choose)

Updated Applies to Jev 1.13

Jev reaches you through two doors: the official TypeSafe AI channel and OpenRouter, the model’s main demo channel. Both speak the OpenAI-compatible request/response dialect, so neither choice locks you in — but they differ meaningfully in setup speed, billing and how early you see new versions. This guide compares them, then goes past the comparison into the parts that actually cost you time: what “OpenAI-compatible” does and does not guarantee, which request fields are marketplace-only, how to write one adapter that covers both, and a migration runbook you can follow without a rewrite.

TL;DR: Start on OpenRouter: it is the fastest path, priced per token (about $0.0462 per 1M input tokens per third-party listings — confirm on the model page). Consider the official channel when you need enterprise terms, direct support or version guarantees; its endpoints are documented at typesafe.ai. Keep all Jev calls behind one client function and switching later is an afternoon, not a rewrite.

The two channels compared

DimensionOpenRouter (demo channel)Official TypeSafe AI API
Setup speedMinutes: account, credits, keySlower: account/provisioning per official docs
Request formatOpenAI-compatible chat completionsConfirm the documented format at typesafe.ai
PricingPer-token; listed around $0.0462 / 1M input tokens (third-party data, confirm on the model page)Per the official terms; check typesafe.ai
BillingPrepaid credits on OpenRouter, spend caps per keyPer the official billing arrangement
Version availabilityListed on the model page; slug may lag or lead the official docsFirst-hand; confirm release notes officially
Provider routingMultiple upstreams behind one slug, routable by price, latency or throughputDetermined by the official channel
Request-side extrasModel fallback arrays, per-request cost reporting, data-collection filtersNot guaranteed; verify each field in the official docs
Spend observabilityKey-level usage and remaining-limit endpointsPer the official tooling
SupportMarketplace-levelDirect, per official channels
Best forPrototyping, small-to-mid production, quick experimentsEnterprise workloads, compliance needs, volume commitments

Two honest caveats. First, the exact model id can differ between channels — typesafe/jev-1.13 is the OpenRouter listing at the time of writing, and slugs change with versions, so confirm on the model page. Second, this guide deliberately does not invent official endpoint URLs; whatever the official channel offers, its documentation at typesafe.ai is the source of truth, as is the announcement post introducing System One models and Jev.

What “OpenAI-compatible” actually guarantees

Compatibility is a spectrum, and knowing which half of it you are relying on tells you how much a channel switch will hurt.

LayerTransfers between channels?Why it matters
Auth header shape (Authorization: Bearer …)UsuallyYour HTTP client and secret plumbing survive
Body shape (model, messages[])YesThe request you already build stays valid
Response envelope (choices[0].message.content)YesYour parsing entry point stays the same
Model slugNoThe single most common cause of a 404 after a switch
Extra request fieldsNoMarketplace features are not part of the dialect
Error body shapeNot guaranteedWrite your error handling against status codes, not message strings
Usage/token accounting fieldsNot guaranteedLog tokens yourself rather than assuming a field name
Rate-limit headers and semanticsNot guaranteedBackoff behavior has to be re-tuned per channel

The practical rule: depend on the three layers marked “yes”, and treat everything else as channel-specific configuration. Code that reads answer and confidence out of parsed message content moves cleanly. Code that greps the raw HTTP body for the word “billing” does not.

OpenRouter request fields worth knowing

These are the marketplace features that make the demo channel more than a passthrough. They are also the ones you must strip out when targeting a channel that does not document them — an unknown field is either rejected with a 400 or, worse, ignored silently, and silent ignoring is how a “we set a data-collection filter” claim turns out to be false.

Model fallback array. Sending models: ["typesafe/jev-1.13", "<backup-slug>"] lets the router try a second model if the first errors. Useful during a version transition, dangerous for judgment work: a fallback model has its own calibration, so a confidence of 0.94 from a different model does not mean what your threshold assumes. If you use fallbacks, log which model actually served the request.

Provider routing. A provider object narrows or orders the upstreams behind a slug: order to prefer a specific provider, allow_fallbacks: false to refuse substitutes, sort by "price", "latency" or "throughput", and data_collection: "deny" to exclude upstreams that retain or train on inputs. For a moderation pipeline that is latency-bound, sorting by latency is worth measuring; for a pipeline handling personal data, the data-collection filter is worth documenting — while remembering it is a marketplace control, not a contract.

Per-request cost reporting. Adding usage: {"include": true} returns token counts and cost alongside the answer, which lets you attribute spend per pipeline without joining against a separate billing export. It is OpenRouter-specific; the adapter below keeps it out of other channels’ payloads.

Attribution headers. HTTP-Referer and X-Title identify your app in the marketplace dashboard. Cheap to set, and the only way to tell two projects apart when they share a billing account.

A request using three of these:

# Confirm the exact model slug on the OpenRouter model page
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "HTTP-Referer: https://your-app.example" \
  -H "X-Title: inbox-router" \
  -d '{
    "model": "typesafe/jev-1.13",
    "provider": {"sort": "latency", "data_collection": "deny"},
    "usage": {"include": true},
    "messages": [{"role": "user", "content": "Choice question: route this email to sales, billing or support? Email: \"...\""}]
  }'

What OpenRouter looks like in practice

One call, one key, the endpoint you already know:

# Confirm the exact model slug on the OpenRouter model page
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "typesafe/jev-1.13",
    "messages": [{"role": "user", "content": "Choice question: route this email to sales, billing or support? Email: \"...\""}]
  }'

A standard chat completions request — the routing semantics live in the question phrasing.

The parsed answer — example fixture, confirm exact field names in the official documentation:

{
  "answer": "billing",
  "confidence": 0.94,
  "rationale": "The email disputes a subscription charge, which maps to the billing queue."
}

This shape is why OpenRouter is the recommended first channel: your existing OpenAI client library, retry logic and observability all work unchanged.

One adapter, two configs

The whole migration-cost argument rests on this file. Put every channel difference in data, not in branches scattered through the codebase:

import json, os, requests
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Channel:
    name: str
    base_url: str
    api_key_env: str
    model: str
    extra_headers: dict = field(default_factory=dict)
    extra_body: dict = field(default_factory=dict)

CHANNELS = {
    "openrouter": Channel(
        name="openrouter",
        base_url="https://openrouter.ai/api/v1/chat/completions",
        api_key_env="OPENROUTER_API_KEY",
        model=os.environ.get("JEV_MODEL_SLUG", "typesafe/jev-1.13"),
        extra_headers={
            "HTTP-Referer": "https://your-app.example",
            "X-Title": "inbox-router",
        },
        extra_body={"usage": {"include": True}},
    ),
    # Official TypeSafe AI channel: fill these from the official documentation.
    "official": Channel(
        name="official",
        base_url=os.environ.get("TYPESAFE_BASE_URL", ""),
        api_key_env="TYPESAFE_API_KEY",
        model=os.environ.get("TYPESAFE_MODEL", ""),
    ),
}

def ask(question: str, channel: str = "openrouter", timeout: int = 30) -> dict:
    ch = CHANNELS[channel]
    if not ch.base_url:
        raise RuntimeError(f"{ch.name}: base URL not configured")
    resp = requests.post(
        ch.base_url,
        headers={
            "Authorization": f"Bearer {os.environ[ch.api_key_env]}",
            "Content-Type": "application/json",
            **ch.extra_headers,
        },
        json={
            "model": ch.model,
            "messages": [{"role": "user", "content": question}],
            **ch.extra_body,
        },
        timeout=timeout,
    )
    resp.raise_for_status()
    envelope = resp.json()
    return json.loads(envelope["choices"][0]["message"]["content"])

Three properties matter here. The channel is a runtime parameter, so a canary can route 5% of traffic elsewhere with no deploy. The marketplace-only fields live in extra_headers and extra_body, so they vanish automatically for any channel that does not declare them. And the parsed result is the fixture shape shown above — treat the field names as an example fixture to confirm in the official documentation, not as a spec.

Migration cost: low, if you architect for it

The switching cost is mostly self-inflicted, which means it is mostly avoidable:

  1. One client function. All Jev calls go through a single wrapper that takes a question and returns the parsed answer.
  2. Config-driven base URL, key and model id. Environment variables, not literals.
  3. Fixture-tolerant parsing. Parse the message content defensively; treat unknown extra fields as ignorable rather than fatal.

With that structure, moving from OpenRouter to the official channel is changing three configuration values and re-checking field names against the official docs — the response shape above is an example fixture, and the official channel’s exact field names should be confirmed in its documentation.

A migration runbook

Config alone does not tell you whether the new channel behaves like the old one. Run these six steps in order; each has an exit criterion, so you always know whether to proceed.

StepActionExit criterion
1. Freeze the golden setExport 50–200 real inputs with your own human labelsEvery case has an expected answer recorded
2. Pin slugs per channelPut each channel’s model id in config, not codeBoth values resolve; a bad slug fails at startup
3. Replay offlineRun the golden set through both channels, no live trafficAnswer agreement ≥ 95% on labelled cases
4. Compare distributionsDiff confidence values and latency, not just answersMedian confidence delta within 0.05; p95 latency within +30%
5. CanarySend 5% of live traffic to the new channel, log bothOne week with no threshold breach in the fallback rate
6. Cut over and holdFlip the default; keep the old channel warmRollback is one environment variable, verified under 5 minutes

Step 4 is the one teams skip, and it is the one that catches real problems. Two channels can agree on 96% of answers and still disagree on when to be uncertain — if the new channel reports systematically higher confidence, your fallback threshold silently stops firing and misroutes start reaching customers. The confidence-and-fallback guide covers how to choose that threshold in the first place.

The replay harness is short enough to write once and reuse:

GOLDEN = [
    ("Judgment question: does the string 'abc' contain the letter 'z'?", "no"),
    ("Choice question: which word is a colour? Options: red, seven, run. Word: 'seven'", "seven"),
    ("Scoring question (1-10): rate the similarity of 'cat' and 'cat'.", 10),
]

def compare(channel_a: str, channel_b: str) -> dict:
    agree, deltas = 0, []
    for question, expected in GOLDEN:
        a = ask(question, channel=channel_a)
        b = ask(question, channel=channel_b)
        agree += int(str(a["answer"]) == str(b["answer"]) == str(expected))
        deltas.append(abs(a["confidence"] - b["confidence"]))
    return {"agreement": agree / len(GOLDEN), "median_conf_delta": sorted(deltas)[len(deltas) // 2]}

Answers in both results are the example fixture shape; confirm the official channel’s field names before you wire this into CI.

Per-channel visibility

You cannot compare channels you cannot measure. Both channels give you status codes and latency; OpenRouter additionally documents endpoints that answer the operational questions you will actually ask.

QuestionOpenRouter surfaceUse
How much has this key spent, and how much is left?Key-info endpoint (usage, limit, remaining)Nightly budget guard per project
Is the account balance running out?Credits endpoint (total credits, total usage)Balance alarm before a batch job
What did that specific call cost?Generation-lookup by request idJoin cost to your own logs
Is my configured slug still listed?Models listing with pricingNightly slug-drift check

That last one is worth automating on day one. Slugs change with versions, and a slug that disappears turns every call into a 404 at the same second. A daily job that fetches the model listing, asserts your configured slug is present, and alerts on absence converts a production outage into a ticket. The troubleshooting guide has the full error-to-fix table for when something does slip through.

Rate limits differ, so tune concurrency per channel

Rate limits are the one setting that looks portable and is not. A marketplace typically meters per key and ties the ceiling to your credit tier; an official channel sets quotas in its own agreement. The same max_workers=10 thread pool can therefore be comfortable on one channel and a 429 generator on the other.

Two consequences for the migration runbook. First, treat concurrency as per-channel configuration alongside the base URL, and re-measure after a cutover instead of carrying the old value across. Start at 10 concurrent calls, watch the retry rate for ten minutes, and step down if 429s exceed roughly 1% of requests. Second, remember that canary traffic is not a fair load test: 5% of production through a fresh key on a fresh quota tells you about correctness, not about how the channel behaves at your real volume. Track the retry rate per channel as its own metric so a rising rate is visible before it becomes a backlog.

Recommendation by project stage

StageChannelWhy
Evaluation / prototypeOpenRouterFastest path to a working call
Early productionOpenRouterOpenAI-compatible tooling, per-key spend caps
Scaling / enterpriseOfficial API (evaluate)Support, terms, version guarantees — verify on typesafe.ai
HybridEitherKeep the adapter; route by workload

The email routing case shows the OpenRouter path powering a three-team setup in production. For the money side of the comparison, the cost-and-latency guide puts real numbers on the per-token economics.

Constraints that override preference

Four requirements decide the channel for you, regardless of what the table above recommends.

Anti-patterns that cost a week

Keeping spend bounded per channel

Prepaid credits make one failure mode very cheap to prevent: a retry loop or a runaway batch draining the balance mid-month. Two habits cover it. Cap each key at a spend limit so a leaked credential cannot empty the account, and check the remaining limit before starting any bulk job, aborting if the projected cost exceeds what is left. Estimate the projection with measured prompt tokens from your own logs rather than a guess — prompt size is nearly the whole cost model, as the cost-and-latency guide works through.

For a hybrid setup, keep the two channels’ keys in separate secrets with separate limits. Then a pricing surprise or an incident on one channel degrades that channel only, and your fallback is still funded.

This guide applies to Jev 1.13.

Frequently asked questions

+ Which channel should I start with for Jev?

OpenRouter. It is the main demo channel, exposes an OpenAI-compatible endpoint, and takes minutes to wire up — ideal for prototyping and moderate-volume production.

+ What does the official TypeSafe AI channel add?

Direct support, enterprise terms and first-hand version information. Endpoint details and features are documented on typesafe.ai — confirm there before planning a migration.

+ How expensive is it to switch channels later?

Low, if you isolate the call in one client function. Because both paths are OpenAI-compatible request/response, swapping URLs, keys and model slugs is the bulk of the work.

+ Can I run both channels at the same time?

Yes, and it is the usual migration path: keep OpenRouter as the default while you replay a labelled golden set and a small canary slice through the official channel, then flip the default in config.

+ Do OpenRouter-only request fields work on the official channel?

Do not assume so. Provider routing, model fallback arrays and response-cost fields are marketplace features; treat them as optional decorations that the adapter strips for any channel that does not document them.

Keep reading