← All resources

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.

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 systemone shape, 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

  1. Store the provider key in the dashboard under provider keys.
  2. Create the systemone key and route it at that provider. Shape mismatch is the classic mistake here: a chat-shaped key cannot speak Decisions.
  3. Decide: POST /v1/systemone with model, state, and questions.
  4. Branch: read answers in code against thresholds you own.
  5. Act: POST /v1/chat/completions on 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.

Route your first request today.

Bring your own keys and start routing in minutes.

Get your key