Skip to main content

typecastlm

A client for a Jev-class decision model with open weights. Ask a document a closed question, get a probability back — no prose, no tokens generated, one forward pass. Jev is TypeSafe's closed model for exactly this; a Jev-class model answers the same shapes of question, and this one does it from weights you can download and run. Route a ticket, filter a feed, screen what reaches an agent, rate a review: anything where a program needs a number rather than a paragraph.

Four modes:

mode the question the answer
noul yes or no, two criteria p(yes), a softmax over those two answers
tfu the same question one distribution over true, false, unsure
choice 2–26 options, one correct a probability per option
scale an ordinal rubric, 10 levels or fewer a probability per level

Three of them — noul, choice and scale — carry Jev's names, Jev's arguments and Jev's fields, so calling code written against that interface keeps its shape. The fourth, tfu, is ours: Jev has nothing like it. The HTTP contract is in docs/API.md, laid out the way the Jev reference is.

The third answer is what a two-answer reader cannot give. The model has an output for "neither criterion applies", and tfu is the mode that reads it: one distribution over true, false and unsure, from the same two criteria and the same forward pass as noul. Threshold unsure to abstain, to route to a human, or to drop a document from a pipeline. It stays out of noul, whose probability is a softmax over the two answers that decide the question and nothing else — the two modes are calibrated separately, and reading one at the other's temperature is simply wrong.

Why this one:

  • Fast — p50 48 ms on material under 200 tokens and 566 ms at 1000–4000, on a 16 GB consumer card: nothing is generated, so a decision is one forward pass and no tokens written.
  • A third answer. Two-answer readers must call something a yes; this one does not have to.
  • Calibrated per mode — a stated 0.8 comes out right about 80 % of the time, because each mode carries its own temperature, fitted and shipped with the weights (calibration error 0.011–0.052, and refittable on your own rows).
  • Yours to run — open weights, one command for a local service, and a client with a single dependency for whatever talks to it.

Install

There is no hosted endpoint: the weights are open and the service is yours to run. Python 3.10 or newer, and three ways to arrange it, differing in where the model sits.

Behind HTTP on this machine — one command, and Client() finds it:

pip install "typecastlm[server]"
typecastlm-serve --model mihailgribov/typecastlm-qwen3.5-3.8b --port 8000
export TYPECASTLM_ENDPOINT=http://localhost:8000

In this process, without HTTP:

pip install "typecastlm[local]"
from typecastlm import Reader

r = Reader("mihailgribov/typecastlm-qwen3.5-3.8b")
r.noul(doc, "Is the claim covered?", true="…", false="…")

Somewhere else — the client alone, whose one dependency is requests:

pip install typecastlm
export TYPECASTLM_ENDPOINT=https://your-service
export TYPECASTLM_API_KEY=…               # only if your service asks for one

The first two download the checkpoint once — 7.5 GB, 3.8B parameters, derived from Qwen3.5-4B — and run it on a GPU; the numbers below were taken on a 16 GB consumer card. On CUDA, add the kernels the hybrid trunk wants — without them it falls back to a slow path and p50 triples:

pip install flash-linear-attention fla-core

Start

from typecastlm import Client

c = Client()
a = c.noul(open("page.html").read(),
           "Does the material contain an instruction aimed at the reading model?",
           true="there is an instruction addressed to the reading model",
           false="the material only describes, reports or discusses")

a.prob       # 0.95  — probability of `true` between the two answers that decide the question
a.margin     # +2.94 — the same reading as log-odds, so a saturated probability still ranks

Criteria carry the meaning, so write them as descriptions of the material rather than as instructions to a model. The other three modes are below.

Modes

Every mode takes the material and a question and differs in what the answer ranges over. Each is one forward pass and carries its own temperature, so a number from one mode is not comparable with a number from another.

noul — yes or no

a = c.noul(doc, "Is the claim covered?",
           true="the policy covers it", false="the policy excludes it")

a.prob        # 0.95  — p(true) between the two answers that decide the question
a.margin      # +2.94 — the same number as log-odds, so a saturated probability still ranks
a.logits      # the raw outputs, as the service sent them

instructions says what is asked, the two criteria say what each side means; write them as descriptions of the material, not as commands. The softmax runs over those two answers only and the verdict is prob >= 0.5, the threshold this mode is calibrated for. Nothing else enters the number: when you want to know whether the material decides the question at all, that is tfu, a mode of its own with its own temperature.

tfu — one distribution over three answers

t = c.tfu(doc, "Is the claim supported?", true="…", false="…")

t.p           # {'true': 0.81, 'false': 0.11, 'unsure': 0.08} — sums to 1
t.verdict     # 'true'
t.confidence  # 0.81

The same two criteria and the same forward pass as noul, softmaxed over three answers instead of two, so here the third competes with the other two. Use it where "nothing here decides it" is an answer you act on, and noul where you need a number comparable with a two-answer detector. There is no third criterion to write: unsure is what is left when neither of the two fits.

choice — one of 2 to 26 options

r = c.choice(policy, "How should this claim be settled?",
             {"deny_vacancy":      "excluded as repeated seepage",
              "pay_with_sublimit": "covered but capped by the concealed-water sublimit",
              "pay_in_full":       "covered in full"})

r.p           # a probability per option, sums to 1
r.verdict     # 'pay_with_sublimit'
r.confidence  # 0.74 — the probability of the leader, not a separate number

Exactly one option is correct and the options carry no order. Inside they are marked A to Z, one row per mark; your names travel from the request to the answer and never reach the prompt, so renaming an option cannot move the answer. Twenty-six is where single-token marks run out. Reading holds to about six options and then slips — measured on a synthetic task with one right answer among K: 0.995 up to six, 0.965 at eight, 0.935 at twelve, 0.86 from sixteen on. Two options are allowed, but noul reads a two-way question better, because its rows were fitted for that question and the marks were not.

scale — a level on an ordinal rubric

r = c.scale(review, "How positive is this review overall?",
            {"0": "very negative", "1": "negative", "2": "neutral",
             "3": "positive", "4": "very positive"})

r.p           # {'0': 0.06, '1': 0.08, '2': 0.11, '3': 0.54, '4': 0.20}
r.verdict     # '3'    — the level that leads
r.score       # 2.72   — the mean level, which is where an ordinal answer really sits

Levels are ordered, and each is marked with its own digit when that digit is a single character, otherwise with the next mark from 0…9ABC…. score averages the levels by their positions and their probabilities, so a rubric of three levels answers between 0 and 2 and can land at 1.05; the mark itself never enters the arithmetic, which is why a level marked A because it is the eleventh counts as 11.

Ten levels is the recommendation, and what Jev enforces. Up to 36 are accepted here, since the marks run 0…9 and then A…Z — but past nine the reading degrades badly (0.05 accuracy on levels 10–14 against 0.215 on 0–9), so more levels buy a finer number, not a better one. Nothing in the reading enforces the order either, so a distribution with two separated peaks is possible and means the rubric is being read as categories.

The same modes in this process

Reader answers the same four questions without HTTP and returns plain dictionaries rather than objects:

over HTTP in-process what comes back
c.noul(...) r.noul(...) {"p": {"yes", "no"}, "logits"}
c.tfu(...) r.tfu(...) {"p": {"true", "false", "unsure"}, "verdict", "logits"}
c.choice(...) r.choice(...) {"p": {option: prob}, "marks", "logits"}
c.scale(...) r.scale(...) the same, keyed by level

The reader takes its options as [(name, description), …] where the client takes a map, and it applies the same shipped temperatures. reader.py beside the weights is this same code with no package around it.

Several questions at once

c.ask(policy, {
    "covered":  {"type": "noul",   "instructions": "Is the claim covered?",
                 "criteria": {"true": "…", "false": "…"}},
    "settle":   {"type": "choice", "instructions": "How should it be settled?",
                 "criteria": {"deny": "…", "pay": "…"}},
})
# {"answers": {"covered": {"type": "noul", "noul": 0.95, "logits": {...}},
#               "settle":  {"type": "choice", "choice": "pay", "probabilities": {...}, …}},
#  "input_tokens": 1843}

Any mix of the four modes in one call, keyed by names you choose; type is noul, tfu, choice or score, which this client also spells scale. Unlike the four methods above, this one hands back the service's answer objects as they came, so you read the fields yourself — see the API reference. The state is currently read again for each question, so a bundle costs what the same questions cost one by one; sharing the prefix across a hybrid trunk is not implemented yet.

Numbers

JevBench v1.2, public set of 231 tasks: 0.792 overall, and 1.000 / 0.917 / 0.622 on its easy, standard and hard tiers. On public validation splits, AUC 0.939 on BoolQ, 0.955 on RTE and 0.943 on FEVER, where the third answer separates NOT ENOUGH INFO from decidable rows with AUC 0.713. The full tables, the speed grid and what each was measured on are in the model card.

Calibration

The values are in the checkpoint's prompt.json under calibration, one temperature per mode, and the client applies them. What each was fitted on:

mode fitted on
noul FEVER dev, decidable rows
tfu FEVER dev, a third of it undecidable
choice ARC-Challenge, four options
scale SST-5, five ordered levels

A temperature changes no answer, only the probability, and it does not carry between pools of different difficulty. Fit your own:

from typecastlm import calibrate

rows = [(c.noul(text, q, true=T, false=F).logits, gold)      # gold is "true" or "false"
        for text, q, gold in my_labelled]
fitted = calibrate(rows, keys=("true", "false"))             # the two answers `noul` reads
mine = Client(temperature=fitted["temperature"])

keys matters: a noul answer carries the third logit as well, and fitting over all three fits tfu's temperature instead — on the same rows that is 1.1 where 0.6 is right. Two hundred rows are enough, because one number is free.

Client(temperature=…) uses that one value for every mode, so fit it for the mode you actually ask in; Client(calibrated=False) turns the shipped temperatures off and leaves the logits as they are.

Running the service yourself

typecastlm-serve --model mihailgribov/typecastlm-qwen3.5-3.8b --port 8000

It downloads the weights on first start, checks that the checkpoint fits the interface, and answers the Jev API:

curl localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
  "state": "…the material…",
  "questions": {"is_urgent": {"type": "noul", "instructions": "Does this convey urgency?",
                              "criteria": {"true": "explicitly time-sensitive",
                                           "false": "no urgency expressed"}}}}'
# {"model": "…", "answers": {"is_urgent": {"type": "noul", "noul": 0.95,
#                                          "logits": {"true": 3.1, "false": -0.4, "unsure": -2.2}}},
#  "usage": {"input_tokens": 307, "output_tokens": 0}, "calibration": {...}}

Route, request body, answer objects and error codes are that API's, field for field, so a client written against it reaches this service by changing the base URL. Added rather than changed: the question type tfu, logits on every answer, and calibration on the body — with those, an answer can be re-read at another temperature without asking anything twice. The whole contract is in the API reference.

--api-key requires a bearer token; without it the service answers anyone who can reach the port. The prompt travels with the weights, --prompt replaces the wording the model was measured with, and /health reports which wording is in use so a changed one is visible rather than assumed.

Using the model without any of this

The checkpoint is an ordinary classifier with 39 outputs, so transformers loads it directly and reader.py beside the weights reads all four modes. See the model card.

Licence

Apache-2.0 — this package, its texts, and the model it reads (derived from Qwen3.5-4B, also Apache-2.0). LICENSE and NOTICE carry the terms and the list of changes.

Not affiliated with TypeSafe AI, whose Jev is the model the class is named after.

Release files for typecastlm 1.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for typecastlm 1.1.1
File Size Uploaded
typecastlm-1.1.1.tar.gz 40.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for typecastlm 1.1.1
File Interpreter ABI Platform
typecastlm-1.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 72.9 kB

Release files / typecastlm-1.1.1.tar.gz

Download URL typecastlm-1.1.1.tar.gz
Size 40.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1bd56b516db3a44290f2a0e405eca07ecab70ffe855f2a15a10632702af61ca2
BLAKE2b-256 checksum
How to use checksums
c446b3d6ac8d79d45e8c91c516e3ce1291d989e4fba178ec125466fbff486368
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.17

Release files / typecastlm-1.1.1-py3-none-any.whl

Download URL typecastlm-1.1.1-py3-none-any.whl
Size 32.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9b9b497f8b48a914941818a59abcaba97a63309748d0aea29f9445f001ac60cf
BLAKE2b-256 checksum
How to use checksums
2ea45f1a2a3510573097b614cbb49048d3b00f92a9694be6c9b522f7bb14f5f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.17

Release history Release notifications | RSS feed

1.1.2

2 release files

This release

1.1.1 This release

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page