typellm 0.3.0.dev2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {typellm-0.3.0.dev2/typellm.egg-info → typellm-0.4.0}/PKG-INFO +79 -65
  2. {typellm-0.3.0.dev2 → typellm-0.4.0}/README.md +78 -64
  3. {typellm-0.3.0.dev2 → typellm-0.4.0}/pyproject.toml +1 -1
  4. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_batching.py +129 -25
  5. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_concurrency.py +42 -38
  6. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_dependencies.py +18 -18
  7. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_grammar_fields.py +14 -14
  8. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_images.py +17 -17
  9. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_nullable.py +12 -12
  10. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_permutations.py +12 -12
  11. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_prefill.py +18 -30
  12. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_probabilities.py +3 -3
  13. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_prompts.py +5 -14
  14. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_protocols.py +22 -0
  15. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_text.py +18 -21
  16. {typellm-0.3.0.dev2 → typellm-0.4.0}/tests/test_typellm.py +235 -116
  17. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/__init__.py +2 -0
  18. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/cli.py +1 -1
  19. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/runtime.py +162 -87
  20. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/schema.py +85 -29
  21. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/sglang.py +62 -66
  22. {typellm-0.3.0.dev2 → typellm-0.4.0/typellm.egg-info}/PKG-INFO +79 -65
  23. {typellm-0.3.0.dev2 → typellm-0.4.0}/LICENSE +0 -0
  24. {typellm-0.3.0.dev2 → typellm-0.4.0}/setup.cfg +0 -0
  25. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/__main__.py +0 -0
  26. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/benchmark.py +0 -0
  27. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/images.py +0 -0
  28. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm/protocol.py +0 -0
  29. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm.egg-info/SOURCES.txt +0 -0
  30. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm.egg-info/dependency_links.txt +0 -0
  31. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm.egg-info/entry_points.txt +0 -0
  32. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm.egg-info/requires.txt +0 -0
  33. {typellm-0.3.0.dev2 → typellm-0.4.0}/typellm.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typellm
3
- Version: 0.3.0.dev2
3
+ Version: 0.4.0
4
4
  Summary: Type-Safe Decoding for Autoregressive LLMs
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://typellm.ai/
@@ -23,12 +23,14 @@ Dynamic: license-file
23
23
  <a href="https://typellm.ai/">Homepage</a>&nbsp; • &nbsp;
24
24
  <a href="https://typellm.ai/blog">Blog</a>&nbsp; • &nbsp;
25
25
  <a href="https://typellm.ai/docs">Docs</a>&nbsp; • &nbsp;
26
- <a href="https://typellm.ai/early-access">Early Access</a>&nbsp; • &nbsp;
26
+ <a href="https://typellm.ai/examples">Examples</a>&nbsp; • &nbsp;
27
+ <a href="https://typellm.ai/dashboard">API</a>&nbsp; • &nbsp;
27
28
  <a href="https://typellm.ai/contact">Contact</a>&nbsp;
28
29
  </h4>
29
30
  </div>
30
31
 
31
32
  ## Updates
33
+ - **[2026/09/29]** The [TypeLLM API](https://typellm.ai) is live: typed outputs without serving a model yourself, and $5 of credit for new accounts. [Get early access](https://typellm.ai/early-access) · [Quick start](#typellm-api-cloud) · [Examples](https://typellm.ai/examples).
32
34
  - **[2026/09/24]** Added [image input](#image-input) for vision-language models, tested with Qwen3.8-27B.
33
35
  - **[2026/09/23]** Added [JevBench results](https://github.com/TypeLLM/TypeLLM/blob/main/evals/jevbench/README.md): TypeLLM scored 195/231 without thinking and 228/231 with thinking.
34
36
  - **[2026/09/23]** Added [permutation averaging](#per-question-permutation-averaging) to improve the predictive distribution. See the [blog post](https://typellm.ai/blog/fair-die).
@@ -66,37 +68,33 @@ Evaluated on 231 public [JevBench](https://github.com/fstandhartinger/jevbench)
66
68
 
67
69
  ## Quick start
68
70
 
69
- ### Hosted API (early access)
71
+ ```bash
72
+ pip install -U typellm
73
+ ```
70
74
 
71
- No GPU? [Early-access](https://typellm.ai/early-access) users get an API key
72
- by email, and the hosted API runs the model:
75
+ Call the hosted TypeLLM API, or run TypeLLM against a model you serve yourself.
76
+ Both clients take the same requests.
73
77
 
74
- ```python
75
- from typellm import TypeLLMClient
78
+ ### TypeLLM API (cloud)
79
+
80
+ [Sign in](https://typellm.ai/login), enter your access code on the
81
+ [API keys](https://typellm.ai/dashboard/keys) page and create a key. No code yet?
82
+ [Get early access](https://typellm.ai/early-access).
76
83
 
77
- client = TypeLLMClient(api_key="YOUR_API_KEY") # calls https://api.typellm.ai
84
+ ```bash
85
+ export TYPELLM_API_KEY="tl-sk-..."
78
86
  ```
79
87
 
80
- `generate()` then works as in the example below, with these differences:
88
+ ```python
89
+ import os
90
+ from typellm import TypeLLMClient
81
91
 
82
- - The service compiles and runs the schema. Use `model`, `temperature`, `seed`
83
- and `timeout`; local compilation settings do not apply.
84
- A call runs for at most 300 seconds, 60 by default. Set `timeout` on the
85
- client for a new default, or on `generate()` to override it for one call.
86
- - Pass `questions`; `schema=`, `cancel` and `print_final_prompt` need your
87
- own server. Images can be files, bytes, PIL images or data: URIs, not URLs.
88
- - Each key runs a limited number of calls at once, told with the key. Calls
89
- beyond it fail right away with HTTP 429.
90
- - Errors raise `SGLangError` with the HTTP status in `.status`, and a call
91
- that runs out of time raises `GenerationTimeout`. The service caps the
92
- context size and the images, answering 400 past them, and stops string
93
- answers at 128 tokens (`maxLength` is refused).
94
- - Successful calls set `last_usage` from the service's input and thinking token
95
- counts. Failed hosted calls may not return usage.
92
+ client = TypeLLMClient(api_key=os.environ["TYPELLM_API_KEY"])
93
+ ```
96
94
 
97
- To run on your own GPU instead, start here:
95
+ See the [API reference](https://typellm.ai/docs/api) to call it over HTTP instead.
98
96
 
99
- ### 1. Serve a model with SGLang
97
+ ### Self-hosted with SGLang
100
98
 
101
99
  Use [SGLang](https://github.com/sgl-project/sglang) to configure and serve a
102
100
  compatible autoregressive model on your local GPU server. This example uses
@@ -106,12 +104,6 @@ to start it with prefix caching enabled.
106
104
 
107
105
  See [Supported models](#supported-models) for tested checkpoints and thinking behavior.
108
106
 
109
- ### 2. Run TypeLLM
110
-
111
- ```bash
112
- pip install -U typellm
113
- ```
114
-
115
107
  Point `TypeLLMClient` at the SGLang server's HTTP endpoint:
116
108
 
117
109
  ```python
@@ -123,10 +115,12 @@ client = TypeLLMClient(
123
115
  )
124
116
  ```
125
117
 
126
- Example request:
118
+ ### Make a request
119
+
120
+ With either client:
127
121
 
128
122
  ```python
129
- result = client.generate(
123
+ response = client.generate(
130
124
  context="""
131
125
  Receipt from Hilton London
132
126
  Total: £324.50
@@ -158,10 +152,12 @@ result = client.generate(
158
152
  },
159
153
  )
160
154
 
161
- print(result)
155
+ print(response.result)
162
156
  ```
163
157
 
164
- Example output:
158
+ `generate()` returns what the HTTP API does: the typed answers in `.result`, the
159
+ reasoning of each field that thought in `.thinking`, and the call's tokens in
160
+ `.usage`. Example `.result`:
165
161
 
166
162
  ```python
167
163
  {
@@ -190,7 +186,7 @@ Enum choices support `string`, `integer`, and `number` types, with at most 24 va
190
186
  A string without `enum` generates free text:
191
187
 
192
188
  ```python
193
- result = client.generate(
189
+ response = client.generate(
194
190
  context="The train ticket is for a client meeting.",
195
191
  questions={
196
192
  "summary": {"type": "string", "instructions": "Summarize in one sentence."},
@@ -199,12 +195,13 @@ result = client.generate(
199
195
  ```
200
196
 
201
197
  Free text stops after 128 tokens. Pass `text_max_tokens=` to the client for
202
- longer answers, or set `maxLength` on a field to cap its characters.
198
+ longer answers, and ask for the length you want in the field's instructions.
199
+ `maxLength` is not supported.
203
200
 
204
201
  Ask for a numeric answer without enumerating every possible value:
205
202
 
206
203
  ```python
207
- result = client.generate(
204
+ response = client.generate(
208
205
  context="Calculate the requested value accurately.",
209
206
  questions={
210
207
  "answer": {
@@ -214,7 +211,7 @@ result = client.generate(
214
211
  },
215
212
  )
216
213
 
217
- print(result)
214
+ print(response.result)
218
215
  # {"answer": 70.0}
219
216
  ```
220
217
 
@@ -240,7 +237,7 @@ Add `"null"` to the type to allow a missing value. The field returns `None`
240
237
  when the input has no value for it:
241
238
 
242
239
  ```python
243
- result = client.generate(
240
+ response = client.generate(
244
241
  context="Read the attached receipt.",
245
242
  images=["receipt.jpg"],
246
243
  questions={
@@ -266,7 +263,7 @@ Thinking is off by default. Turn it on for the fields that need it; the others
266
263
  answer at once, and the fields that think reason side by side:
267
264
 
268
265
  ```python
269
- result = client.generate(context=context, questions={
266
+ response = client.generate(context=context, questions={
270
267
  "total": {"type": "number"},
271
268
  "category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
272
269
  "policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
@@ -274,16 +271,35 @@ result = client.generate(context=context, questions={
274
271
  })
275
272
  ```
276
273
 
277
- `thinking_budget` caps a field's reasoning; there is no budget by default.
278
- `TypeLLMClient(..., thinking_budget=2048)` sets one for every field that thinks
279
- without its own. When reasoning reaches the budget, TypeLLM closes it and moves
280
- on to the typed answer.
274
+ `thinking_budget` caps a field's reasoning. Without one, a field may reason until
275
+ the model's context is full. When reasoning reaches the budget, TypeLLM closes it
276
+ and moves on to the typed answer.
277
+
278
+ `response.thinking` maps each field that thought to its reasoning, and
279
+ `response.usage.thinking_tokens` counts the reasoning tokens.
280
+
281
+ ### Thinking effort
282
+
283
+ `thinking_effort` sets the budget by level instead: `"none"` does not think,
284
+ `"low"` thinks up to 512 tokens, `"medium"` up to 2048 and `"high"` up to 4096.
281
285
 
282
- After a call, `client.last_thinking` maps each field that thought to its
283
- reasoning, and `client.last_usage.thinking_tokens` counts the reasoning tokens.
286
+ With `"thinking": "auto"`, TypeLLM picks the level on each call. It first asks
287
+ the model how much reasoning the field needs, without thinking, then answers the
288
+ field at that level. The field's own prompt is unchanged, and
289
+ `response.thinking_effort` reports the level each `"auto"` field got:
290
+
291
+ ```python
292
+ response = client.generate(context=context, questions={
293
+ "category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
294
+ "policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
295
+ "thinking": "auto"},
296
+ })
297
+ response.thinking_effort # {"policy_ok": "low"}
298
+ ```
284
299
 
285
- Models with always-on thinking reason on every field; `thinking_budget` applies
286
- to them too. See [Supported models](#supported-models).
300
+ `"auto"` adds one quick step after the field's dependencies and before the field;
301
+ that step sees the same dependency results. `thinking_effort` and
302
+ `thinking_budget` cannot be combined with `"auto"` or with each other.
287
303
 
288
304
  ## Image input
289
305
 
@@ -291,7 +307,7 @@ Pass images with `images=` alongside the text context. The served model must be
291
307
  a vision-language model, such as `Qwen/Qwen3.8-27B`.
292
308
 
293
309
  ```python
294
- result = client.generate(
310
+ response = client.generate(
295
311
  context="The customer says this receipt was charged twice.",
296
312
  images=["receipt.png"],
297
313
  questions={
@@ -311,7 +327,7 @@ Fields run together by default, and each sees only the original context.
311
327
  When a field needs earlier results, list them in `depends_on`:
312
328
 
313
329
  ```python
314
- result = client.generate(
330
+ response = client.generate(
315
331
  context="The payments service is returning errors after a deployment.",
316
332
  questions={
317
333
  "system": {
@@ -349,7 +365,7 @@ step reuses its parent's cached prompt. Unknown names and cycles raise
349
365
  Set `return_probabilities` on individual enum or boolean fields:
350
366
 
351
367
  ```python
352
- result = client.generate(
368
+ response = client.generate(
353
369
  context=context,
354
370
  questions={
355
371
  "expense_type": {
@@ -401,7 +417,7 @@ For a one-off request, use the convenience function:
401
417
  ```python
402
418
  from typellm import run_schema
403
419
 
404
- result = run_schema(
420
+ response = run_schema(
405
421
  context=context,
406
422
  questions=questions,
407
423
  base_url="http://127.0.0.1:30000",
@@ -414,7 +430,7 @@ result = run_schema(
414
430
  Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
415
431
 
416
432
  ```python
417
- result = client.generate(
433
+ response = client.generate(
418
434
  context="A single roll of a fair die.",
419
435
  questions={"roll": {
420
436
  "type": "string",
@@ -441,25 +457,25 @@ support this option.
441
457
  ## Serving
442
458
 
443
459
  One `TypeLLMClient` can be shared by many threads. Each call keeps its own
444
- prompts and usage, and connections to SGLang are reused.
460
+ prompts and usage, and connections to SGLang or the hosted API are reused.
445
461
 
446
462
  ```python
447
463
  import threading
448
464
 
449
465
  cancel = threading.Event()
450
- result = client.generate(
466
+ response = client.generate(
451
467
  context=context,
452
468
  questions=questions,
453
469
  seed=7, # this call's random choices only
454
470
  timeout=30, # seconds for the whole call; raises GenerationTimeout
455
471
  cancel=cancel, # set it from another thread; raises GenerationCancelled
456
472
  )
457
- print(client.last_usage)
458
- # Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0, input_tokens=410)
473
+ print(response.usage)
474
+ # Usage(input_tokens=410, thinking_tokens=0, requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7)
459
475
  ```
460
476
 
461
- `last_usage` reports the tokens of the last call made in the current thread,
462
- including a call that failed partway. `input_tokens` is what you sent, each part
477
+ `usage` reports the tokens of the call. When a call fails partway, the
478
+ exception's `.usage` holds what it spent. `input_tokens` is what you sent, each part
463
479
  counted once: the context, the questions as JSON, and the images. The other
464
480
  counts are SGLang's for the call's requests, where every prompt carries the
465
481
  shared context. A timeout or cancel stops
@@ -490,16 +506,14 @@ server.
490
506
  | --- | --- |
491
507
  | `Qwen/Qwen3.8-27B` | On / off |
492
508
  | `Qwen/Qwen3.5-0.8B/4B/9B` | On / off |
493
- | `openbmb/MiniCPM5-1B` | On / off |
494
- | `inclusionAI/Ling-mini-2.0` | Off only |
495
- | `inclusionAI/Ring-mini-2.0` | Always on |
496
509
 
497
510
  Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
498
511
 
499
512
  [Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
500
513
 
501
- Use the checkpoint ID as `model=`. If the server's tokenizer path is unavailable
502
- locally, set `tokenizer=` to its matching Hugging Face ID or local directory.
514
+ Use the checkpoint ID as `model=`. TypeLLM loads the tokenizer from the server's
515
+ paths, then its served model name; if none of those loads locally, set
516
+ `tokenizer=` to the matching Hugging Face ID or local directory.
503
517
  The tokenizer must load from standard artifacts without custom model code.
504
518
 
505
519
  ## Comparison with Jev-style models
@@ -8,12 +8,14 @@
8
8
  <a href="https://typellm.ai/">Homepage</a>&nbsp; • &nbsp;
9
9
  <a href="https://typellm.ai/blog">Blog</a>&nbsp; • &nbsp;
10
10
  <a href="https://typellm.ai/docs">Docs</a>&nbsp; • &nbsp;
11
- <a href="https://typellm.ai/early-access">Early Access</a>&nbsp; • &nbsp;
11
+ <a href="https://typellm.ai/examples">Examples</a>&nbsp; • &nbsp;
12
+ <a href="https://typellm.ai/dashboard">API</a>&nbsp; • &nbsp;
12
13
  <a href="https://typellm.ai/contact">Contact</a>&nbsp;
13
14
  </h4>
14
15
  </div>
15
16
 
16
17
  ## Updates
18
+ - **[2026/09/29]** The [TypeLLM API](https://typellm.ai) is live: typed outputs without serving a model yourself, and $5 of credit for new accounts. [Get early access](https://typellm.ai/early-access) · [Quick start](#typellm-api-cloud) · [Examples](https://typellm.ai/examples).
17
19
  - **[2026/09/24]** Added [image input](#image-input) for vision-language models, tested with Qwen3.8-27B.
18
20
  - **[2026/09/23]** Added [JevBench results](https://github.com/TypeLLM/TypeLLM/blob/main/evals/jevbench/README.md): TypeLLM scored 195/231 without thinking and 228/231 with thinking.
19
21
  - **[2026/09/23]** Added [permutation averaging](#per-question-permutation-averaging) to improve the predictive distribution. See the [blog post](https://typellm.ai/blog/fair-die).
@@ -51,37 +53,33 @@ Evaluated on 231 public [JevBench](https://github.com/fstandhartinger/jevbench)
51
53
 
52
54
  ## Quick start
53
55
 
54
- ### Hosted API (early access)
56
+ ```bash
57
+ pip install -U typellm
58
+ ```
55
59
 
56
- No GPU? [Early-access](https://typellm.ai/early-access) users get an API key
57
- by email, and the hosted API runs the model:
60
+ Call the hosted TypeLLM API, or run TypeLLM against a model you serve yourself.
61
+ Both clients take the same requests.
58
62
 
59
- ```python
60
- from typellm import TypeLLMClient
63
+ ### TypeLLM API (cloud)
64
+
65
+ [Sign in](https://typellm.ai/login), enter your access code on the
66
+ [API keys](https://typellm.ai/dashboard/keys) page and create a key. No code yet?
67
+ [Get early access](https://typellm.ai/early-access).
61
68
 
62
- client = TypeLLMClient(api_key="YOUR_API_KEY") # calls https://api.typellm.ai
69
+ ```bash
70
+ export TYPELLM_API_KEY="tl-sk-..."
63
71
  ```
64
72
 
65
- `generate()` then works as in the example below, with these differences:
73
+ ```python
74
+ import os
75
+ from typellm import TypeLLMClient
66
76
 
67
- - The service compiles and runs the schema. Use `model`, `temperature`, `seed`
68
- and `timeout`; local compilation settings do not apply.
69
- A call runs for at most 300 seconds, 60 by default. Set `timeout` on the
70
- client for a new default, or on `generate()` to override it for one call.
71
- - Pass `questions`; `schema=`, `cancel` and `print_final_prompt` need your
72
- own server. Images can be files, bytes, PIL images or data: URIs, not URLs.
73
- - Each key runs a limited number of calls at once, told with the key. Calls
74
- beyond it fail right away with HTTP 429.
75
- - Errors raise `SGLangError` with the HTTP status in `.status`, and a call
76
- that runs out of time raises `GenerationTimeout`. The service caps the
77
- context size and the images, answering 400 past them, and stops string
78
- answers at 128 tokens (`maxLength` is refused).
79
- - Successful calls set `last_usage` from the service's input and thinking token
80
- counts. Failed hosted calls may not return usage.
77
+ client = TypeLLMClient(api_key=os.environ["TYPELLM_API_KEY"])
78
+ ```
81
79
 
82
- To run on your own GPU instead, start here:
80
+ See the [API reference](https://typellm.ai/docs/api) to call it over HTTP instead.
83
81
 
84
- ### 1. Serve a model with SGLang
82
+ ### Self-hosted with SGLang
85
83
 
86
84
  Use [SGLang](https://github.com/sgl-project/sglang) to configure and serve a
87
85
  compatible autoregressive model on your local GPU server. This example uses
@@ -91,12 +89,6 @@ to start it with prefix caching enabled.
91
89
 
92
90
  See [Supported models](#supported-models) for tested checkpoints and thinking behavior.
93
91
 
94
- ### 2. Run TypeLLM
95
-
96
- ```bash
97
- pip install -U typellm
98
- ```
99
-
100
92
  Point `TypeLLMClient` at the SGLang server's HTTP endpoint:
101
93
 
102
94
  ```python
@@ -108,10 +100,12 @@ client = TypeLLMClient(
108
100
  )
109
101
  ```
110
102
 
111
- Example request:
103
+ ### Make a request
104
+
105
+ With either client:
112
106
 
113
107
  ```python
114
- result = client.generate(
108
+ response = client.generate(
115
109
  context="""
116
110
  Receipt from Hilton London
117
111
  Total: £324.50
@@ -143,10 +137,12 @@ result = client.generate(
143
137
  },
144
138
  )
145
139
 
146
- print(result)
140
+ print(response.result)
147
141
  ```
148
142
 
149
- Example output:
143
+ `generate()` returns what the HTTP API does: the typed answers in `.result`, the
144
+ reasoning of each field that thought in `.thinking`, and the call's tokens in
145
+ `.usage`. Example `.result`:
150
146
 
151
147
  ```python
152
148
  {
@@ -175,7 +171,7 @@ Enum choices support `string`, `integer`, and `number` types, with at most 24 va
175
171
  A string without `enum` generates free text:
176
172
 
177
173
  ```python
178
- result = client.generate(
174
+ response = client.generate(
179
175
  context="The train ticket is for a client meeting.",
180
176
  questions={
181
177
  "summary": {"type": "string", "instructions": "Summarize in one sentence."},
@@ -184,12 +180,13 @@ result = client.generate(
184
180
  ```
185
181
 
186
182
  Free text stops after 128 tokens. Pass `text_max_tokens=` to the client for
187
- longer answers, or set `maxLength` on a field to cap its characters.
183
+ longer answers, and ask for the length you want in the field's instructions.
184
+ `maxLength` is not supported.
188
185
 
189
186
  Ask for a numeric answer without enumerating every possible value:
190
187
 
191
188
  ```python
192
- result = client.generate(
189
+ response = client.generate(
193
190
  context="Calculate the requested value accurately.",
194
191
  questions={
195
192
  "answer": {
@@ -199,7 +196,7 @@ result = client.generate(
199
196
  },
200
197
  )
201
198
 
202
- print(result)
199
+ print(response.result)
203
200
  # {"answer": 70.0}
204
201
  ```
205
202
 
@@ -225,7 +222,7 @@ Add `"null"` to the type to allow a missing value. The field returns `None`
225
222
  when the input has no value for it:
226
223
 
227
224
  ```python
228
- result = client.generate(
225
+ response = client.generate(
229
226
  context="Read the attached receipt.",
230
227
  images=["receipt.jpg"],
231
228
  questions={
@@ -251,7 +248,7 @@ Thinking is off by default. Turn it on for the fields that need it; the others
251
248
  answer at once, and the fields that think reason side by side:
252
249
 
253
250
  ```python
254
- result = client.generate(context=context, questions={
251
+ response = client.generate(context=context, questions={
255
252
  "total": {"type": "number"},
256
253
  "category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
257
254
  "policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
@@ -259,16 +256,35 @@ result = client.generate(context=context, questions={
259
256
  })
260
257
  ```
261
258
 
262
- `thinking_budget` caps a field's reasoning; there is no budget by default.
263
- `TypeLLMClient(..., thinking_budget=2048)` sets one for every field that thinks
264
- without its own. When reasoning reaches the budget, TypeLLM closes it and moves
265
- on to the typed answer.
259
+ `thinking_budget` caps a field's reasoning. Without one, a field may reason until
260
+ the model's context is full. When reasoning reaches the budget, TypeLLM closes it
261
+ and moves on to the typed answer.
262
+
263
+ `response.thinking` maps each field that thought to its reasoning, and
264
+ `response.usage.thinking_tokens` counts the reasoning tokens.
265
+
266
+ ### Thinking effort
267
+
268
+ `thinking_effort` sets the budget by level instead: `"none"` does not think,
269
+ `"low"` thinks up to 512 tokens, `"medium"` up to 2048 and `"high"` up to 4096.
266
270
 
267
- After a call, `client.last_thinking` maps each field that thought to its
268
- reasoning, and `client.last_usage.thinking_tokens` counts the reasoning tokens.
271
+ With `"thinking": "auto"`, TypeLLM picks the level on each call. It first asks
272
+ the model how much reasoning the field needs, without thinking, then answers the
273
+ field at that level. The field's own prompt is unchanged, and
274
+ `response.thinking_effort` reports the level each `"auto"` field got:
275
+
276
+ ```python
277
+ response = client.generate(context=context, questions={
278
+ "category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
279
+ "policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
280
+ "thinking": "auto"},
281
+ })
282
+ response.thinking_effort # {"policy_ok": "low"}
283
+ ```
269
284
 
270
- Models with always-on thinking reason on every field; `thinking_budget` applies
271
- to them too. See [Supported models](#supported-models).
285
+ `"auto"` adds one quick step after the field's dependencies and before the field;
286
+ that step sees the same dependency results. `thinking_effort` and
287
+ `thinking_budget` cannot be combined with `"auto"` or with each other.
272
288
 
273
289
  ## Image input
274
290
 
@@ -276,7 +292,7 @@ Pass images with `images=` alongside the text context. The served model must be
276
292
  a vision-language model, such as `Qwen/Qwen3.8-27B`.
277
293
 
278
294
  ```python
279
- result = client.generate(
295
+ response = client.generate(
280
296
  context="The customer says this receipt was charged twice.",
281
297
  images=["receipt.png"],
282
298
  questions={
@@ -296,7 +312,7 @@ Fields run together by default, and each sees only the original context.
296
312
  When a field needs earlier results, list them in `depends_on`:
297
313
 
298
314
  ```python
299
- result = client.generate(
315
+ response = client.generate(
300
316
  context="The payments service is returning errors after a deployment.",
301
317
  questions={
302
318
  "system": {
@@ -334,7 +350,7 @@ step reuses its parent's cached prompt. Unknown names and cycles raise
334
350
  Set `return_probabilities` on individual enum or boolean fields:
335
351
 
336
352
  ```python
337
- result = client.generate(
353
+ response = client.generate(
338
354
  context=context,
339
355
  questions={
340
356
  "expense_type": {
@@ -386,7 +402,7 @@ For a one-off request, use the convenience function:
386
402
  ```python
387
403
  from typellm import run_schema
388
404
 
389
- result = run_schema(
405
+ response = run_schema(
390
406
  context=context,
391
407
  questions=questions,
392
408
  base_url="http://127.0.0.1:30000",
@@ -399,7 +415,7 @@ result = run_schema(
399
415
  Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
400
416
 
401
417
  ```python
402
- result = client.generate(
418
+ response = client.generate(
403
419
  context="A single roll of a fair die.",
404
420
  questions={"roll": {
405
421
  "type": "string",
@@ -426,25 +442,25 @@ support this option.
426
442
  ## Serving
427
443
 
428
444
  One `TypeLLMClient` can be shared by many threads. Each call keeps its own
429
- prompts and usage, and connections to SGLang are reused.
445
+ prompts and usage, and connections to SGLang or the hosted API are reused.
430
446
 
431
447
  ```python
432
448
  import threading
433
449
 
434
450
  cancel = threading.Event()
435
- result = client.generate(
451
+ response = client.generate(
436
452
  context=context,
437
453
  questions=questions,
438
454
  seed=7, # this call's random choices only
439
455
  timeout=30, # seconds for the whole call; raises GenerationTimeout
440
456
  cancel=cancel, # set it from another thread; raises GenerationCancelled
441
457
  )
442
- print(client.last_usage)
443
- # Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0, input_tokens=410)
458
+ print(response.usage)
459
+ # Usage(input_tokens=410, thinking_tokens=0, requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7)
444
460
  ```
445
461
 
446
- `last_usage` reports the tokens of the last call made in the current thread,
447
- including a call that failed partway. `input_tokens` is what you sent, each part
462
+ `usage` reports the tokens of the call. When a call fails partway, the
463
+ exception's `.usage` holds what it spent. `input_tokens` is what you sent, each part
448
464
  counted once: the context, the questions as JSON, and the images. The other
449
465
  counts are SGLang's for the call's requests, where every prompt carries the
450
466
  shared context. A timeout or cancel stops
@@ -475,16 +491,14 @@ server.
475
491
  | --- | --- |
476
492
  | `Qwen/Qwen3.8-27B` | On / off |
477
493
  | `Qwen/Qwen3.5-0.8B/4B/9B` | On / off |
478
- | `openbmb/MiniCPM5-1B` | On / off |
479
- | `inclusionAI/Ling-mini-2.0` | Off only |
480
- | `inclusionAI/Ring-mini-2.0` | Always on |
481
494
 
482
495
  Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
483
496
 
484
497
  [Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
485
498
 
486
- Use the checkpoint ID as `model=`. If the server's tokenizer path is unavailable
487
- locally, set `tokenizer=` to its matching Hugging Face ID or local directory.
499
+ Use the checkpoint ID as `model=`. TypeLLM loads the tokenizer from the server's
500
+ paths, then its served model name; if none of those loads locally, set
501
+ `tokenizer=` to the matching Hugging Face ID or local directory.
488
502
  The tokenizer must load from standard artifacts without custom model code.
489
503
 
490
504
  ## Comparison with Jev-style models
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "typellm"
7
- version = "0.3.0.dev2"
7
+ version = "0.4.0"
8
8
  description = "Type-Safe Decoding for Autoregressive LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"