How to Call Jev with FreeRouter: Decide First, Generate After
Software makes millions of small decisions a day: which queue owns this, how urgent is it, approve or flag, act now or queue for review. The naive path is asking a chat model and parsing the prose it writes back — slow, inconsistent, and billed by the paragraph.
Jev is a decision model built for exactly that job. You send it application state and a small set of typed questions. It returns answers your code can branch on — a choice from a closed set, a score on a rubric, or a probability — plus confidence. It does not write the email, the plan, or the next assistant message. If you want prose, that is a later, optional chat call. This post uses one running example throughout: routing a support ticket.
FreeRouter adds routing optionality to Jev. One systemone-shaped key reaches it on TypeSafe's native endpoint, on OpenRouter's Decisions API, or on Requesty — three providers, the same customer wire, ordered however you like. TypeSafe released Jev recently; here is the loop we actually run with it.
See the reaction on X
What a Jev call looks like
Three question types cover most decision routines. Choice picks from a closed set (which queue, which model, which function). Score places the state on an ordered rubric (how urgent, how frustrated). Noul returns a single 0–1 probability that a statement is true. Every question in one request is answered in parallel against the same state, so asking three questions costs barely more time than asking one — and Choice and Score answers carry a confidence value you can treat as a second axis: act, confirm, or escalate.
Two things Jev is not: it is not a chat model, so it never appears as a chat routing target, and a Jev answer is not a chat completion in disguise — it is a value your code branches on. Generation happens later, and only when the decision says it should.
flowchart LR
App["Your app"] --> Key["FreeRouter key, systemone shape"]
Key --> TS["TypeSafe"]
Key --> OR["OpenRouter"]
Key --> RQ["Requesty"]
TS --> Out["answers + confidence"]
OR --> Out
RQ --> Out
Out --> Code["your if statements"]
One key, three peer hosts, one wire format — answers land in your code.
What you need
- A FreeRouter account and one provider key: OpenRouter, Requesty, or TypeSafe (Providers). OpenRouter-only is a complete setup while TypeSafe stays invite-only.
- A FreeRouter key with the
systemoneshape, routed at that provider key. This is a Decisions key, not a chat key. - A second, chat-shaped key for the follow-through completion in step 5.
- Node 18 or later for the snippet below (or just curl for step 3).
Recipe — Five steps
How you do it with FreeRouter
- Store the provider key in the dashboard under provider keys.
- Create the
systemonekey and route it at that provider. Shape mismatch is the classic mistake here: a chat-shaped key cannot speak Decisions. - Decide:
POST /v1/systemonewith model, state, and questions. - Branch: read
answersin code against thresholds you own. - Act:
POST /v1/chat/completionson the chat key — only when the decision earns it.
Call one — Decide
Ask Jev with curl
Export your systemone-shaped key and paste this into a terminal. It sends one ticket as state with two questions — a Choice for the owning team and a Noul for urgency:
export FREEROUTER_API_KEY=fr_…
curl https://api.freerouter.com/v1/systemone \
-H "Authorization: Bearer $FREEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "jev-latest",
"state": "I was charged twice for order A-104. Please refund the duplicate.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations"
}
},
"is_urgent": {
"type": "noul",
"instructions": "Does this convey urgency?"
}
}
}'
The response is { model, answers, usage } — never choices. The Choice answer names the winner with probabilities across the closed set plus confidence; the Noul answer is one number between 0 and 1, where distance from 0.5 is the confidence. The X-FreeRouter-Provider response header tells you which host served it.
Call two — Generate
Decide, then complete, in Node
This snippet chains both calls: Jev classifies the ticket, and only when it picks billing with enough confidence does the script spend a chat completion drafting the reply. Save it as jev-decide-then-chat.js and run it with FREEROUTER_API_KEY (your systemone key) and FREEROUTER_CHAT_KEY (an ordinary chat-shaped key) in the environment. It uses only the Node standard library.
// jev-decide-then-chat.js — Jev decides, a chat model drafts the reply.
// Run: FREEROUTER_API_KEY=fr_… FREEROUTER_CHAT_KEY=fr_… node jev-decide-then-chat.js
async function fr(path, key, body) {
const res = await fetch(`https://api.freerouter.com${path}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} HTTP ${res.status}: ${await res.text()}`);
return res.json();
}
async function main() {
const decideKey = process.env.FREEROUTER_API_KEY;
const chatKey = process.env.FREEROUTER_CHAT_KEY;
if (!decideKey || !chatKey) throw new Error('Set FREEROUTER_API_KEY and FREEROUTER_CHAT_KEY.');
// 1. Decide: which team owns this ticket?
const state = 'I was charged twice for order A-104. Please refund the duplicate.';
const { answers } = await fr('/v1/systemone', decideKey, {
model: 'jev-latest',
state,
questions: {
department: {
type: 'choice',
instructions: 'Which team should handle this ticket?',
criteria: { billing: 'Payments, invoicing, refunds', technical: 'Bugs, outages, integrations' },
},
},
});
const { choice, confidence } = answers.department;
console.log(`department=${choice} confidence=${confidence}`);
// 2. Generate only when the decision says so.
if (choice === 'billing' && confidence >= 0.5) {
const chat = await fr('/v1/chat/completions', chatKey, {
model: 'openai/gpt-4o-mini',
messages: [
{ role: 'system', content: 'Draft a short refund-confirmation reply for a billing ticket.' },
{ role: 'user', content: state },
],
});
console.log(chat.choices[0].message.content);
} else {
console.log('Not billing, or too unsure — route to a human queue instead.');
}
}
main().catch((err) => { console.error(err.message); process.exit(1); });
This is the intent-routing pattern: a cheap classification call decides whether the expensive generation call happens at all. When the answer is a queue name, a function call, or a drop decision, generation never runs — and that is the point.
Stitching it altogether
Step back and look at what just happened: two FreeRouter calls, two different jobs. The first call is pure decisioning — Jev reads the state, fills in the typed form, and stops. Your code reads the answers and picks what happens next. The second call acts on the decision — a completion, a queue write, a function call, whatever the branch requires.
That split is where FreeRouter's power shows. Each leg is independently routable, so you can mix and match with ease: run the decision on Jev via OpenRouter while the completion rides a different gateway entirely, keep a failover behind each leg, or move either leg to a new provider later without touching the other. The decision leg and the action leg never need to share a host, a key, or a fate — they only share your thresholds.
flowchart TD
State["ticket state"] --> Decide["POST /v1/systemone"]
Decide --> Response["department=billing, confidence 0.9"]
Response --> Gate{"billing and confident?"}
Gate -->|yes| Chat["POST /v1/chat/completions"]
Gate -->|no| Human["human queue"]
The full path: decide, branch, and generate only when the decision earns it.
Going further with decision routines
A few ideas worth stealing once the basic loop works. Treat confidence as a second axis: above ~0.85 act automatically, between ~0.5 and 0.85 confirm or queue for review, below that fall back to your default path — you pick the thresholds per use case. Keep questions narrow and atomic rather than one big question; the option set is the prompt, so tuning criteria text moves results more than rewording instructions. Remember that state is the entire world Jev sees — no retrieval, no memory between calls — so include the policy or context the decision depends on.
If you would rather have FreeRouter run the decision for you, put a decisioner on a chat-shaped key: Jev picks among that key's existing routing targets before the normal proxy runs, with the inbound model as the fail-open path when Jev cannot pick. The client still sends and receives an ordinary chat completion.
Limits and failure modes
- No streaming on systemone. Decisions come back as one JSON body. If you need tokens as they generate, that is a chat call.
- A systemone key is not a chat key. Sending chat completions to one (or Decisions to a chat key) fails — keep the two keys in the snippet separate.
- TypeSafe is invite-only. Until you have an invite, OpenRouter or Requesty is the whole setup, not a degraded one.
- Bad questions fail; bad luck fails over. A malformed question returns 4xx so you fix it. Retryable host errors move to your next target instead.
- Low confidence means do not act. Below your threshold, fall back to the default path or a human — never split the difference between options.
- Companion Ads, MCP middleware, and chat remaps do not apply on the Decisions shape. For the wire-level details, see the Decisions shape reference.
Next steps
Get a FreeRouter key and run the curl above — decide first, generate after, and only when the decision earns it.