An AI feature often needs a decision, not a paragraph. A support tool might need to route a ticket to one team. A game agent might need to choose one goal from a small list. Asking a chat model for prose and then parsing that prose adds work at exactly the point where code needs a predictable value.

Jev is built for that decision-shaped part of an application. It accepts state and typed questions, then returns answers in a known shape with probabilities. The application still owns the rules, validation, and actions that follow. That split makes Jev worth considering for classification and routing, including a narrow role in GOAP goal selection.

What is Jev?

Jev is a structured decision model from TypeSafe, available through OpenRouter. It belongs to TypeSafe’s System One model family, which is designed for fast decisions inside software. Jev does not write explanations or free-form responses. Instead, the caller supplies a state object and asks one or more typed questions about it.

A state can be a string, an object, or an array of text data. The questions use a small set of primitives: choice, noul, and score. Each primitive declares the kind of answer the caller expects. Jev returns the selected value or probability, rather than asking your program to extract a label from a sentence.

That changes the integration boundary. A chat completion might return “The agent should rest because its fatigue is high,” and code then has to parse “rest” while deciding what to do with the explanation. Jev can instead answer a choice question with a stable value such as FindRest, plus probabilities for the options. The application validates that value and decides whether it is safe to act on.

Jev does not return its reasoning trace. The probabilities help express uncertainty, but they are still model output, not a proof that a decision is correct. If a feature needs a human-readable explanation, store the relevant state and decision data yourself or ask a separate generative model to explain an already validated result.

What is a classifier?

A classifier assigns an input to one or more predefined labels. Given a message, a classifier might choose billing, technical, or sales. Given an agent snapshot, it might choose rest, fulfill-needs, or stay-with-group.

A conventional classifier usually exposes a fixed input/output contract: supply an input, receive a label and often a confidence score. Jev can implement a similar decision, but its questions make the contract explicit in the request. The application says what it is deciding, describes the possible answers, and sends the state those answers should be based on.

That distinction is useful when the labels belong to the application. A Jev choice answer can be a stable ID from a game’s goal registry, an enum-like route key, or a moderation category. The model chooses among the options the caller gives it. The caller still owns the catalogue and what each value means.

Classification can also be single-label or multi-label. If exactly one category should apply, use a choice question. If several independent tags may apply at once, ask a separate yes/no noul question for each tag and apply thresholds in code. Avoid forcing overlapping labels into one choice; Jev must return one of the alternatives, even when more than one label fits.

Calling Jev through OpenRouter

OpenRouter exposes Jev through the Decisions API at https://openrouter.ai/api/alpha/decisions. This is a different request shape from a chat-completions call: send a model, a state, and a questions object. The model ID typesafe/jev-1.13 pins a specific Jev version. OpenRouter also documents a latest-version alias, but a pinned ID is easier to evaluate when the application depends on stable decisions.

The API key belongs on a trusted server. A key embedded in a Unity build can be extracted from the client, so a shipped game should send a compact decision request to its own backend. That backend can authenticate the player, apply rate limits, call OpenRouter, validate the result, and return only the permitted decision to the client. During local experiments, keep the key outside source control and out of player-facing builds.

Here is a small request using the choice primitive:

curl https://openrouter.ai/api/alpha/decisions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "typesafe/jev-1.13",
    "state": {
      "ticket": "The checkout screen stays blank after I select Pay.",
      "customerTier": "enterprise"
    },
    "questions": {
      "owner": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {
          "billing": "Payment processing, invoices, or refunds.",
          "frontend": "Rendering, layout, or browser compatibility.",
          "account": "Login, permissions, or profile settings."
        }
      }
    }
  }'

The response places typed answers under answers. A choice answer includes the selected key and probabilities for the listed options; a confidence value may also be present. The response’s usage reports request cost in usage.cost. Use that value for accounting instead of estimating each call from prompt length.

A .NET backend can send the same body with HttpClient. This client reads the key from the server environment, posts JSON, and returns the parsed response:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

public sealed class JevClient
{
    private static readonly HttpClient Http = new HttpClient();

    public async Task<JsonDocument> DecideAsync(
        object state,
        object questions,
        CancellationToken cancellationToken)
    {
        string apiKey = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
        if (string.IsNullOrWhiteSpace(apiKey))
            throw new InvalidOperationException("OPENROUTER_API_KEY is not configured.");

        var body = new
        {
            model = "typesafe/jev-1.13",
            state,
            questions
        };

        using var request = new HttpRequestMessage(
            HttpMethod.Post,
            "https://openrouter.ai/api/alpha/decisions");
        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", apiKey);
        request.Content = new StringContent(
            JsonSerializer.Serialize(body),
            Encoding.UTF8,
            "application/json");

        using HttpResponseMessage response = await Http.SendAsync(
            request,
            cancellationToken);
        string responseBody = await response.Content.ReadAsStringAsync();
        if (!response.IsSuccessStatusCode)
            throw new HttpRequestException(
                $"OpenRouter returned HTTP {(int)response.StatusCode}.");

        return JsonDocument.Parse(responseBody);
    }
}

Dispose the returned JsonDocument after reading it. In the GOAP example below, the state and questions arguments are the agent snapshot and candidate-goal criteria. Do not send the request to /api/v1/chat/completions and expect Jev’s typed response there; that endpoint is for chat-style models.

Jev answers the questions in one request independently. A second question cannot inspect the first question’s answer. This makes one request useful for several labels about the same state, but it does not turn the questions into a multi-step chain. If the second decision depends on the first, make that dependency explicit in your own code or send a follow-up request with the updated state.

At the time of writing, OpenRouter’s Jev 1.13 page lists a price of $0.042 per million input tokens and no output-token charge. Check the model page before budgeting because rates can change. Jev’s documented context window is 32,000 tokens, but a small state is still preferable: it costs less, reduces irrelevant context, and makes the decision easier to inspect.

Jev’s data types and when to use them

The three primitives describe different decisions. Pick the one that matches the shape of the answer instead of asking the model for a broad judgment and trying to retrofit the result later.

choice: select one option

Use choice when exactly one option should win. The criteria object maps each stable key to a description of when that option applies. Jev returns one key and a probability for each option.

Examples include selecting a support queue, a permitted operation, or one eligible GOAP goal. Add an other or none option when the candidate list may not contain a valid answer. If the application must never accept an unknown value, validate the returned key against its own allowlist anyway.

noul: estimate whether a condition is true

Use noul for a yes/no question. The response contains a probability that the answer is yes. The criteria describe concrete true and false cases, which is more useful than an ambiguous prompt such as “Is this important?”

Examples include asking whether a report describes a defect, whether a message requests a refund, or whether a character’s state suggests an eligible goal. noul questions are independent, so they work for multi-label tagging: ask whether each candidate tag applies, then choose the thresholds that match the product’s tolerance for false positives and false negatives.

score: place something on an ordered scale

Use score when the answer has a real order, such as low, medium, and high urgency. Its criteria are an ordered list from the lowest level to the highest. Jev returns a probability-weighted score and probabilities for the levels; a confidence value may also be present.

A score is not a category with an arbitrary numeric label. The order needs to mean something. “Routine, should be handled today, blocks the simulation” has a progression; “urgent, technical, refund” does not. For a small fixed set of mutually exclusive choices, choice is clearer.

Using Jev to select a GOAP goal

GOAP separates a desired world state from the actions that can make it true. A planner receives a goal and the current world state, then searches the authored action set for a valid plan. Jev can help choose which goal deserves attention when several are eligible. It should not invent actions or replace the planner’s preconditions, effects, and target checks.

A useful boundary looks like this:

Jev at the GOAP boundary

A crowd simulation already has structured state that can support this request: agent role, current goal, utility urgency, fulfilled and unfulfilled motivations, relationships, recent events, and the registered goals available to that role. Keep the snapshot compact and send only fields that can change the decision. The goal catalogue should remain authored data with stable wire IDs and descriptions.

For example, a civilian may have high fatigue, an unfulfilled motivation, and a current Idle goal. Local code can first remove goals that are unavailable for that agent role or invalid in the current world. The remaining candidates might be FindRest and FulfillNeeds. A choice request can ask Jev which goal best fits the current snapshot:

{
  "model": "typesafe/jev-1.13",
  "state": {
    "agent": {
      "id": "agent-42",
      "decisionRevision": 18,
      "role": "Civilian",
      "currentGoal": "Idle",
      "fatigueUrgency": "Critical",
      "unfulfilledMotivations": ["Rest", "FindFood"]
    },
    "scenario": {
      "danger": "Low",
      "groupStatus": "Safe"
    },
    "eligibleGoals": ["FindRest", "FulfillNeeds"]
  },
  "questions": {
    "nextGoal": {
      "type": "choice",
      "instructions": "Which eligible goal should this agent pursue next?",
      "criteria": {
        "FindRest": "Reduce critical fatigue by reaching a rest spot.",
        "FulfillNeeds": "Address an unfulfilled motivation when immediate rest is not the stronger need."
      }
    }
  }
}

The descriptions are part of the decision contract. “Find rest” is too vague if the agent needs a particular condition or role. “Reduce critical fatigue by reaching a rest spot” tells Jev what success means without asking it to plan the movement or decide whether a rest spot exists. The code should derive eligibleGoals from the registry and current state, not trust a model to discover which goal classes exist.

The response’s nextGoal.choice can then be mapped through the project’s stable GOAP wire-ID registry. Before requesting a plan, the caller should check all of the following:

  • The key parses to a registered goal ID.
  • The goal is in the allowlist for this agent and remains eligible.
  • The agent and scenario versions still match the snapshot used for the request.
  • The answer has enough confidence for the application’s chosen policy.

A small adapter can enforce those checks before handing the goal to the existing GOAP provider. This example fails closed when confidence is missing; its caller can keep the behavior graph’s current decision when it returns false:

using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

static bool TryApplyChoice(
    string responseJson,
    AgentCortex cortex,
    int requestedDecisionRevision,
    int currentDecisionRevision,
    ISet<GoapGoalId> currentlyEligibleGoals,
    double minimumConfidence)
{
    if (string.IsNullOrWhiteSpace(responseJson) || cortex == null ||
        requestedDecisionRevision != currentDecisionRevision ||
        currentlyEligibleGoals == null)
        return false;

    JObject root;
    try
    {
        root = JObject.Parse(responseJson);
    }
    catch (JsonException)
    {
        return false;
    }

    JToken answer = root["answers"]?["nextGoal"];
    if (answer == null || answer["choice"]?.Type != JTokenType.String)
        return false;

    JToken confidenceToken = answer["confidence"];
    if (confidenceToken == null ||
        (confidenceToken.Type != JTokenType.Float &&
         confidenceToken.Type != JTokenType.Integer))
        return false;

    double confidence = confidenceToken.Value<double>();
    if (confidence < 0 || confidence > 1 || confidence < minimumConfidence)
        return false;

    string wireId = (string)answer["choice"];
    if (!GoapGoalRegistry.TryParseWireId(wireId, out GoapGoalId goalId) ||
        !currentlyEligibleGoals.Contains(goalId))
        return false;

    return cortex.TryExecuteAvailableGoals(new[] { goalId });
}

The example’s currentlyEligibleGoals set should be rebuilt from the current role’s registered goals after deterministic state checks. requestedDecisionRevision must cover every agent and scenario field sent to Jev; increment it whenever any of those inputs changes. The reference snapshot combines AgentContext data with utility urgency and motivations read from other systems, so AgentContext.StateVersion alone may miss a fatigue or motivation change. TryExecuteAvailableGoals adds one final local validation before requesting the goal.

When the response arrives, reject it if the decision revision changed or the selected goal is no longer eligible. If the JSON is malformed or confidence is below the application’s threshold, keep the existing behavior-graph decision or use a deterministic local fallback. Choose that threshold from evaluation data rather than intuition alone.

Once accepted, pass the typed goal to the GOAP provider. The planner still checks its own conditions, sensors, targets, costs, and effects. For FindRest, for example, the goal condition is tied to critical fatigue, and the rest capability has a target sensor for a nearby rest spot. If the sensor finds no usable target or the action becomes invalid, normal GOAP validation and replanning remain responsible for recovery. Jev selected intent; the planner still decides whether the authored plan can run.

The example borrows a useful boundary from a crowd-simulation codebase: build an agent snapshot, limit the candidate list to registered goals, and validate returned IDs and versions. That project’s documentation describes behavior-graph nodes selecting goals in the live runtime, with an LLM interpreting spawn requests. The Jev flow here is a proposed decision provider at the goal-selection boundary, not a claim that Jev already drives that simulation.

Keep the model at the decision boundary

Calling a remote model for every agent on every frame would make latency, cost, and stale responses part of the simulation loop. Schedule decisions when meaningful state changes, batch requests where the API and game design allow it, and keep a deterministic behavior available while a request is in flight. Log the snapshot version, model ID, selected key, probabilities, latency, and usage.cost so the decision can be reviewed later.

Start with one decision boundary and a small candidate set. Log how often Jev’s selection passes validation, how often the local fallback takes over, and whether the chosen goals lead to valid plans. If those measurements do not show a useful change in behavior, keep the existing behavior-graph decision.

References