Skip to content

Typed Decisions

Ask typed questions about a piece of text or JSON and get probabilities back instead of generated text. Send a support email, get the department, the urgency and whether the customer wants a refund, each with a confidence. The model is Laya, an open-weight decision model (Apache-2.0) that runs on our own Apple Silicon hardware. A call takes tens of milliseconds.

Local and stateless

Laya runs on our infrastructure in the EU. There is no sub-processor and no transfer outside the EU. The state and questions stay in memory for the length of the call and are never written to disk; the usage log keeps token counts only.

Endpoints

EndpointUse case
POST /api/llm/decideNative endpoint. Synchronous: the answers, usage and cost come back in the response, no queue id.
POST /v1/systemoneSame wire protocol as TypeSafe Jev. A Jev client works here once you change its base URL.

Question types

questions is an object keyed by your own ids. Every question has a type and instructions; criteria depends on the type.

TypecriteriaAnswer
choice2 to 100 options: an object of label to description, or a list of unique labels.choice (the top label), probabilities per label, confidence.
score2 to 10 levels, lowest first.score (expected level, zero-based, so 1.84 sits close to level 2), legend, probabilities, confidence.
noulOptional { "true": "...", "false": "..." } descriptions.noul (probability that the statement is true), confidence.

confidence is 1 minus the normalized entropy for choice and score, and max(p, 1 - p) for noul. It is not the same number Jev reports, so thresholds tuned on Jev do not carry over.

Request

bash
curl -X POST https://api.ai.canarycoders.es/api/llm/decide \
  -H "Authorization: Bearer $CANARY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "laya-multilingual",
    "state": "Hola, me habéis cobrado dos veces la factura de marzo. Quiero que me devolváis el dinero.",
    "questions": {
      "department": {
        "type": "choice",
        "instructions": "Which department should handle this email?",
        "criteria": {
          "billing": "invoices, payments, refunds",
          "technical": "bugs, outages",
          "sales": "pricing, new contracts"
        }
      },
      "urgency": {
        "type": "score",
        "instructions": "How urgent is this request?",
        "criteria": ["not urgent", "soon", "blocking"]
      },
      "refund": {
        "type": "noul",
        "instructions": "Does the customer ask for money back?"
      }
    },
    "tag": "support-triage"
  }'

state can be a string, an object or an array; objects and arrays are sent to the model as JSON. model defaults to laya-multilingual.

Response

json
{
  "success": true,
  "data": {
    "answers": {
      "department": {
        "type": "choice",
        "choice": "billing",
        "probabilities": { "billing": 0.91, "technical": 0.05, "sales": 0.04 },
        "confidence": 0.68
      },
      "urgency": {
        "type": "score",
        "score": 1.21,
        "legend": { "0": "not urgent", "1": "soon", "2": "blocking" },
        "probabilities": { "0": 0.12, "1": 0.55, "2": 0.33 },
        "confidence": 0.13
      },
      "refund": { "type": "noul", "noul": 0.94, "confidence": 0.94 }
    },
    "model": "laya-multilingual",
    "provider": "laya",
    "requestId": "req_...",
    "usage": { "inputTokens": 212, "outputTokens": 0, "totalTokens": 212 },
    "routing": { "model": "multilingual", "reason": "..." },
    "inferenceTimeMs": 14.2,
    "cost": { "customerPrice": 0.000009, "listPrice": 0.000009, "currency": "USD", "billing": "metered" }
  }
}

Numbers are illustrative. Laya also returns an action field per answer; it carries no reliable signal, so ignore it.

Jev-compatible endpoint

/v1/systemone takes the same body and answers in Jev's shape: model, answers and usage with input_tokens and output_tokens. Leave model out, or send a Jev id like jev-latest, and the router picks the checkpoint from the language of the state. Unlike Jev, output_tokens is always 0 and the state budget is smaller (see limits below).

bash
curl -X POST https://api.ai.canarycoders.es/v1/systemone \
  -H "Authorization: Bearer $CANARY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "jev-latest",
    "state": "My order arrived broken, I want a refund.",
    "questions": {
      "refund": { "type": "noul", "instructions": "Does the customer ask for money back?" }
    }
  }'

TypeScript SDK

typescript
import { CanaryCodersAI } from "@canarycoders/ai";

const client = new CanaryCodersAI({ apiKey: process.env.CANARY_AI_API_KEY });

const { answers, usage, cost } = await client.decisions.decide({
  state: email.body,
  questions: {
    department: {
      type: "choice",
      instructions: "Which department should handle this email?",
      criteria: ["billing", "technical", "sales"],
    },
    refund: { type: "noul", instructions: "Does the customer ask for money back?" },
  },
});

if (answers.department.confidence > 0.5) route(answers.department.choice);

Models

ModelBudget per questionNotes
laya-multilingual (default)1024 tokensUse this for Dutch, Spanish and other non-English text.
laya-english512 tokensEnglish only. On other scripts it gives wrong answers with high confidence.

Limits

  • Up to 64 questions per call and 512 options across all questions.
  • Each question is one model input: instructions, options and the state together must fit the model's budget (512 or 1024 tokens).
  • State that does not fit returns 422 and is not billed. We never truncate it.
  • State is capped at 50k characters before tokenization.

Pricing

$0.042 per million input tokens. Output is free. Laya reads the state once per question, so a 300-token email with three questions bills a bit over 900 tokens. usage.inputTokens in the response is the exact number billed, and cost is the price of the call.

Where it works well

Laya is good at triage, routing and guardrails with a handful of clear options. It is weaker as a zero-shot decision engine on long option lists, so keep these in mind:

  • Keep choice questions under about 20 options. Past 10 options, treat confidence as uncalibrated.
  • Score is the weakest type. Two to four levels work better than ten.
  • Don't use yes, no, true or false as choice labels.
  • A noul question can follow its own true/false descriptions instead of the state. If that happens, ask a two-option choice with neutral labels such as A and B.
  • Negations in the state are unreliable.
  • Out of the box the probabilities run over-confident. Check them against a sample of your own data before you set thresholds.