Jev API Channels: Official API vs OpenRouter (How to Choose)
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
| Dimension | OpenRouter (demo channel) | Official TypeSafe AI API |
|---|---|---|
| Setup speed | Minutes: account, credits, key | Slower: account/provisioning per official docs |
| Request format | OpenAI-compatible chat completions | Confirm the documented format at typesafe.ai |
| Pricing | Per-token; listed around $0.0462 / 1M input tokens (third-party data, confirm on the model page) | Per the official terms; check typesafe.ai |
| Billing | Prepaid credits on OpenRouter, spend caps per key | Per the official billing arrangement |
| Version availability | Listed on the model page; slug may lag or lead the official docs | First-hand; confirm release notes officially |
| Provider routing | Multiple upstreams behind one slug, routable by price, latency or throughput | Determined by the official channel |
| Request-side extras | Model fallback arrays, per-request cost reporting, data-collection filters | Not guaranteed; verify each field in the official docs |
| Spend observability | Key-level usage and remaining-limit endpoints | Per the official tooling |
| Support | Marketplace-level | Direct, per official channels |
| Best for | Prototyping, small-to-mid production, quick experiments | Enterprise 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.
| Layer | Transfers between channels? | Why it matters |
|---|---|---|
Auth header shape (Authorization: Bearer …) | Usually | Your HTTP client and secret plumbing survive |
Body shape (model, messages[]) | Yes | The request you already build stays valid |
Response envelope (choices[0].message.content) | Yes | Your parsing entry point stays the same |
| Model slug | No | The single most common cause of a 404 after a switch |
| Extra request fields | No | Marketplace features are not part of the dialect |
| Error body shape | Not guaranteed | Write your error handling against status codes, not message strings |
| Usage/token accounting fields | Not guaranteed | Log tokens yourself rather than assuming a field name |
| Rate-limit headers and semantics | Not guaranteed | Backoff 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:
- One client function. All Jev calls go through a single wrapper that takes a question and returns the parsed answer.
- Config-driven base URL, key and model id. Environment variables, not literals.
- 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.
| Step | Action | Exit criterion |
|---|---|---|
| 1. Freeze the golden set | Export 50–200 real inputs with your own human labels | Every case has an expected answer recorded |
| 2. Pin slugs per channel | Put each channel’s model id in config, not code | Both values resolve; a bad slug fails at startup |
| 3. Replay offline | Run the golden set through both channels, no live traffic | Answer agreement ≥ 95% on labelled cases |
| 4. Compare distributions | Diff confidence values and latency, not just answers | Median confidence delta within 0.05; p95 latency within +30% |
| 5. Canary | Send 5% of live traffic to the new channel, log both | One week with no threshold breach in the fallback rate |
| 6. Cut over and hold | Flip the default; keep the old channel warm | Rollback 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.
| Question | OpenRouter surface | Use |
|---|---|---|
| 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 id | Join cost to your own logs |
| Is my configured slug still listed? | Models listing with pricing | Nightly 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
| Stage | Channel | Why |
|---|---|---|
| Evaluation / prototype | OpenRouter | Fastest path to a working call |
| Early production | OpenRouter | OpenAI-compatible tooling, per-key spend caps |
| Scaling / enterprise | Official API (evaluate) | Support, terms, version guarantees — verify on typesafe.ai |
| Hybrid | Either | Keep 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.
- Procurement. If finance needs a purchase order and an invoice rather than a prepaid card, the marketplace is the wrong door. Confirm the official channel’s contracting path before you build on it.
- Data handling. A marketplace data-collection filter narrows which upstreams see your inputs. An enterprise agreement is a different instrument. If regulators or customers are asking the question, you need the second one, and you need it in writing.
- Version pinning. If your acceptance tests were written against one model version, you need a channel that keeps that version available on a schedule you control. Marketplaces list and delist at their own cadence.
- Multi-model routing. If Jev is one of several models in a pipeline, a single endpoint, key and bill is worth real engineering time — that is the marketplace’s core competence.
Anti-patterns that cost a week
- Hardcoding the base URL in five places. The switch becomes a search-and-replace with a missed call site.
- One key shared by every project. Spend becomes unattributable and rotation becomes a company-wide outage.
- String-matching the raw response.
if "billing" in bodymatches the rationale too — a call that routed to sales can still mention billing while explaining why. Parse the field. - Assuming slugs match across channels. They frequently do not, and the failure mode is a
404in production. - Treating marketplace fields as portable. Provider routing and fallback arrays do not travel; strip them per channel.
- No per-channel metrics. Without separate latency and fallback-rate series you cannot tell which channel is degrading, and a canary becomes guesswork.
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.