Skip to main content
typellm-banner

TypeLLM: LLMs with type-safe generation

Homepage  •   Blog  •   Docs  •   Early Access  •   Contact 

Updates

  • [2026/09/24] Added image input for vision-language models, tested with Qwen3.8-27B.
  • [2026/09/23] Added JevBench results: TypeLLM scored 195/231 without thinking and 228/231 with thinking.
  • [2026/09/23] Added permutation averaging to improve the predictive distribution. See the blog post.
  • [2026/09/22] Added depends_on dependency graphs with incremental prefix reuse. See the blog post.
  • [2026/09/19] Added optional thinking mode with a per-field budget.
  • [2026/09/18] Added constrained integer and number outputs.

Introduction

TypeLLM brings type-safe generation to existing autoregressive LLMs without changing their architecture or weights. Inspired by TypeSafe AI's Jev, it lets models retain their native thinking and free-form generation while producing schema-guaranteed outputs through JSON Schema. Built on SGLang, TypeLLM also supports richer interaction patterns beyond independent typed decisions.

Supported output types

String · Integer · Number · Boolean · Enum choice — See schemas and examples.

Features

  1. No out-of-schema hallucinations — Choices stay within the allowed values.
  2. Negligible output-token cost — Single-token categorical selection and bounded numeric decoding; optional thinking adds tokens.
  3. Shared-prefix reuse — KV caching avoids reprocessing shared context.
  4. Dependency-aware execution — Independent fields run together; declare depends_on to form a dependency graph.
  5. Made for open autoregressive LLMs — Use compatible models you already serve with SGLang.
  6. Supports thinking mode — Enable reasoning before the final constrained answer.
  7. Image input — Pass images to vision-language models alongside the text context. See Image input.
  8. Permutation averaging — Reduce option-order bias on explicit enum questions with balanced, sampled or exhaustive orderings. See the docs.

JevBench results

Evaluated on 231 public JevBench tasks.

Accuracy Benchmark — 231 public tasks from JevBench

Full results and all per-task answers · Method and configuration

Quick start

1. Serve a model with SGLang

Use SGLang to configure and serve a compatible autoregressive model on your local GPU server. This example uses Qwen3.8-27B; follow the Qwen3.8-27B SGLang deployment guide to start it with prefix caching enabled.

See Supported models for tested checkpoints and thinking behavior.

2. Run TypeLLM

pip install -U typellm

Point TypeLLMClient at the SGLang server's HTTP endpoint:

from typellm import TypeLLMClient

client = TypeLLMClient(
    "http://127.0.0.1:30000",
    model="Qwen/Qwen3.8-27B",
)

Example request:

result = client.generate(
    context="""
    Receipt from Hilton London
    Total: £324.50
    Employee travelled to London for a client meeting.
    """,
    questions={
        "merchant": {
            "type": "string",
            "instructions": "Return only the merchant name.",
        },
        "total": {
            "type": "number",
            "instructions": "Extract the total amount in GBP.",
        },
        "expense_type": {
            "type": "string",
            "enum": ["meal", "travel", "equipment"],
            "instructions": "What type of expense is this?",
        },
        "reimbursable": {
            "type": "boolean",
            "instructions": "Should this expense be reimbursed?",
        },
        "confidence": {
            "type": "number",
            "enum": [0.0, 0.25, 0.5, 0.75, 1.0],
            "instructions": "How confident are you?",
        },
    },
)

print(result)

Example output:

{
    "merchant": "Hilton London",
    "total": 324.5,
    "expense_type": "travel",
    "reimbursable": True,
    "confidence": 0.75,
}

Output types

TypeLLM supports finite decisions, numeric fields, and free text:

Field Schema Returned value
Text {"type": "string"} str
Integer {"type": "integer"} int
Number {"type": "number"} float
Boolean {"type": "boolean"} bool
Enum choice {"type": "string", "enum": ["meal", "travel"]} Candidate type: str, int, or float

Enum choices support string, integer, and number types, with at most 24 values. The declared type validates the candidate values.

A string without enum generates free text:

result = client.generate(
    context="The train ticket is for a client meeting.",
    questions={
        "summary": {"type": "string", "instructions": "Summarize in one sentence."},
    },
)

Free text stops after 128 tokens. Pass text_max_tokens= to the client for longer answers, or set maxLength on a field to cap its characters.

Ask for a numeric answer without enumerating every possible value:

result = client.generate(
    context="Calculate the requested value accurately.",
    questions={
        "answer": {
            "type": "number",
            "instructions": "What is 17.5 multiplied by 4?",
        },
    },
)

print(result)
# {"answer": 70.0}

Numeric answers use plain decimal notation with at most 32 digits by default; set TypeLLMClient(numeric_max_digits=...) to adjust this limit.

Use instructions to tell the model what decision to make:

{
    "type": "string",
    "enum": ["billing", "technical", "account"],
    "instructions": "Which team should handle this ticket?",
}

If instructions is omitted, TypeLLM uses description or an instruction generated from the field name.

Nullable fields

Add "null" to the type to allow a missing value. The field returns None when the input has no value for it:

result = client.generate(
    context="Read the attached receipt.",
    images=["receipt.jpg"],
    questions={
        "tip": {"type": ["number", "null"], "instructions": "Tip amount."},
        "table": {"type": ["string", "null"], "instructions": "Table number."},
        "paid_in_cash": {"type": ["boolean", "null"], "instructions": "Was the bill paid in cash?"},
        "card": {"type": ["string", "null"], "enum": ["VISA", "MASTERCARD", None],
                 "instructions": "Card network, if paid by card."},
    },
)
# {"tip": None, "table": "7A", "paid_in_cash": False, "card": None}
  • type takes one type plus "null". A nullable boolean adds null as a third choice. As in JSON Schema, a nullable enum returns null only if its enum lists None.
  • return_probabilities works for nullable booleans and enums, and its probabilities include None.

Thinking mode

Thinking is off by default. Turn it on for the fields that need it; the others answer at once, and the fields that think reason side by side:

result = client.generate(context=context, questions={
    "total": {"type": "number"},
    "category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
    "policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
                  "thinking": True, "thinking_budget": 1024},
})

thinking_budget caps a field's reasoning; there is no budget by default. TypeLLMClient(..., thinking_budget=2048) sets one for every field that thinks without its own. When reasoning reaches the budget, TypeLLM closes it and moves on to the typed answer.

After a call, client.last_thinking maps each field that thought to its reasoning, and client.last_usage.thinking_tokens counts the reasoning tokens.

Models with always-on thinking reason on every field; thinking_budget applies to them too. See Supported models.

Image input

Pass images with images= alongside the text context. The served model must be a vision-language model, such as Qwen/Qwen3.8-27B.

result = client.generate(
    context="The customer says this receipt was charged twice.",
    images=["receipt.png"],
    questions={
        "total": {"type": "number", "instructions": "What is the receipt total?"},
        "paid": {"type": "boolean", "instructions": "Is the receipt marked as paid?"},
    },
)

Each image can be a local file path, an http(s) URL, a data: URI, raw bytes, or a PIL image. Local files are read by the client, so the SGLang server does not need access to your filesystem.

Dependency-aware generation

Fields run together by default, and each sees only the original context. When a field needs earlier results, list them in depends_on:

result = client.generate(
    context="The payments service is returning errors after a deployment.",
    questions={
        "system": {
            "type": "string",
            "enum": ["payments", "accounts", "search"],
            "instructions": "Which system is affected?",
        },
        "severity": {
            "type": "string",
            "enum": ["low", "medium", "high"],
            "instructions": "Assess severity for the affected system.",
            "depends_on": ["system"],
        },
        "deployment_related": {
            "type": "boolean",
            "instructions": "Is the incident related to a deployment?",
            "depends_on": ["system"],
        },
        "rollback": {
            "type": "boolean",
            "instructions": "Based on the incident assessments, should we roll back?",
            "depends_on": ["severity", "deployment_related"],
        },
    },
)

This runs system, then severity and deployment_related, then rollback. A field sees the results of its direct and transitive dependencies, and each step reuses its parent's cached prompt. Unknown names and cycles raise SchemaError.

Probabilities and sampling

Set return_probabilities on individual enum or boolean fields:

result = client.generate(
    context=context,
    questions={
        "expense_type": {
            "type": "string",
            "enum": ["meal", "travel", "equipment"],
            "return_probabilities": True,
        },
    },
)
{
    "expense_type": {
        "value": "travel",
        "probabilities": {
            "meal": 0.04,
            "travel": 0.93,
            "equipment": 0.03,
        },
    }
}

Only opted-in fields return value and probabilities; other fields return plain values. The option is not supported on open Numeric or Text fields.

Argmax is the default. To enable sampling:

client = TypeLLMClient(
    "http://127.0.0.1:30000",
    mode="sample",
    temperature=0.8,
    seed=42,
)

Sampling applies to finite candidates for Choice fields and to token generation for Numeric and Text fields. temperature controls sampling in each case.

A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift with its prefix cache and batching, so the same seed may still give different results.

For a one-off request, use the convenience function:

from typellm import run_schema

result = run_schema(
    context=context,
    questions=questions,
    base_url="http://127.0.0.1:30000",
    model="Qwen/Qwen3.8-27B",
)

Per-question permutation averaging

Add permutations to an enum question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.

result = client.generate(
    context="A single roll of a fair die.",
    questions={"roll": {
        "type": "string",
        "enum": ["one", "two", "three", "four", "five", "six"],
        "instructions": "What number will come up on this roll?",
        "permutations": "auto",
        "return_probabilities": True,
    }},
)
  • "auto" evaluates a balanced set of orderings: each option takes every position, and follows every other option, equally often. That is K orderings for K options (2K when K is odd), and the result does not depend on the order the enum was written in.
  • "all" evaluates every ordering (up to 720).
  • An integer greater than 1 samples that many distinct orderings at random.

Omit it or use 1 to keep the original behavior. Only explicit enum fields support this option.

Docs · Read the blog

Serving

One TypeLLMClient can be shared by many threads. Each call keeps its own prompts and usage, and connections to SGLang are reused.

import threading

cancel = threading.Event()
result = client.generate(
    context=context,
    questions=questions,
    seed=7,          # this call's random choices only
    timeout=30,      # seconds for the whole call; raises GenerationTimeout
    cancel=cancel,   # set it from another thread; raises GenerationCancelled
)
print(client.last_usage)
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0)

last_usage reports the SGLang requests and tokens of the last call made in the current thread, including a call that failed partway. A timeout or cancel stops the call before its next SGLang request.

Cost analysis

Enum and boolean fields use one output token each. Numeric, text, and optional thinking outputs use multiple tokens.

For a chain of D dependent fields, C original context tokens, and roughly S new tokens per field, input prefill counts are:

without prefix reuse: O(D*C + D^2*S)
with prefix reuse:    O(C + D*S)

For independent fields, the shared context is prefilled once, followed by each field's question.

Supported models

The following models have been tested with TypeLLM on a live SGLang GPU server.

Model / checkpoint Thinking support
Qwen/Qwen3.8-27B On / off
Qwen/Qwen3.5-0.8B/4B/9B On / off
openbmb/MiniCPM5-1B On / off
inclusionAI/Ling-mini-2.0 Off only
inclusionAI/Ring-mini-2.0 Always on

Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.

Image input has been tested with Qwen/Qwen3.8-27B.

Use the checkpoint ID as model=. If the server's tokenizer path is unavailable locally, set tokenizer= to its matching Hugging Face ID or local directory. The tokenizer must load from standard artifacts without custom model code.

Comparison with Jev-style models

Feature TypeLLM Jev openjev-sglang system-one-open OpenJev DeBERTa
Enum selection ✓ ✓ ✓ ✓ ✓
Boolean decisions ✓ ✓ ✓ ✓ ✓
Rubric scoring Numeric enum; no dedicated Score API Score Score Score Score
integer/decimal type ✓ — — — —
string type ✓ — — — —
Enable Thinking ✓ — — — —
Image input ✓ Not documented — Not documented —
Multi-field execution Batch, DAG Batch Batch Batch Batch
Built-in field dependency graph ✓ — — — —
KV prefix reuse Shared context + dependency paths Not disclosed Shared context Not documented Not applicable

© 2026 TypeLLM

Release files for typellm 0.2.2

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

Source distribution (sdist)

Source distribution for typellm 0.2.2
File Size Uploaded
typellm-0.2.2.tar.gz 73.0 kB Details

Built distribution (wheel)

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

Total release size: 117.0 kB

Release files / typellm-0.2.2.tar.gz

Download URL typellm-0.2.2.tar.gz
Size 73.0 kB
Tags Source
SHA-256 checksum
How to use checksums
41e0a1f94f388f979f57501232cb10db93fcd9e83f9a3e4536768381d360de3c
BLAKE2b-256 checksum
How to use checksums
15632ca4906f24448ea66bf67188ef241a8d3c75380ceb04441bc4da79bb1eb8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / typellm-0.2.2-py3-none-any.whl

Download URL typellm-0.2.2-py3-none-any.whl
Size 44.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fba3b5bd8f95b4ea9d3000542b630ffb776b9845b9bd0b660030343c0feaa685
BLAKE2b-256 checksum
How to use checksums
5620dda38834d67c887a1f25a16396d53b254d3ca72d83ada9b1cfc7b2fed49e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.3

2 release files

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.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