typellm 0.2.4__tar.gz → 0.2.6__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.
- {typellm-0.2.4/typellm.egg-info → typellm-0.2.6}/PKG-INFO +35 -31
- {typellm-0.2.4 → typellm-0.2.6}/README.md +34 -30
- {typellm-0.2.4 → typellm-0.2.6}/pyproject.toml +1 -1
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_batching.py +13 -15
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_concurrency.py +33 -29
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_dependencies.py +18 -18
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_grammar_fields.py +9 -9
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_images.py +9 -9
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_nullable.py +8 -8
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_permutations.py +7 -7
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_prefill.py +12 -26
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_probabilities.py +2 -2
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_prompts.py +4 -13
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_protocols.py +22 -0
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_text.py +15 -18
- {typellm-0.2.4 → typellm-0.2.6}/tests/test_typellm.py +81 -80
- {typellm-0.2.4 → typellm-0.2.6}/typellm/__init__.py +2 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm/cli.py +4 -3
- {typellm-0.2.4 → typellm-0.2.6}/typellm/runtime.py +65 -79
- {typellm-0.2.4 → typellm-0.2.6}/typellm/schema.py +10 -23
- {typellm-0.2.4 → typellm-0.2.6}/typellm/sglang.py +50 -76
- {typellm-0.2.4 → typellm-0.2.6/typellm.egg-info}/PKG-INFO +35 -31
- {typellm-0.2.4 → typellm-0.2.6}/LICENSE +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/setup.cfg +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm/__main__.py +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm/benchmark.py +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm/images.py +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm/protocol.py +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm.egg-info/SOURCES.txt +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm.egg-info/dependency_links.txt +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm.egg-info/entry_points.txt +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/typellm.egg-info/requires.txt +0 -0
- {typellm-0.2.4 → typellm-0.2.6}/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.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Type-Safe Decoding for Autoregressive LLMs
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://typellm.ai/
|
|
@@ -96,7 +96,7 @@ client = TypeLLMClient(
|
|
|
96
96
|
Example request:
|
|
97
97
|
|
|
98
98
|
```python
|
|
99
|
-
|
|
99
|
+
response = client.generate(
|
|
100
100
|
context="""
|
|
101
101
|
Receipt from Hilton London
|
|
102
102
|
Total: £324.50
|
|
@@ -128,10 +128,12 @@ result = client.generate(
|
|
|
128
128
|
},
|
|
129
129
|
)
|
|
130
130
|
|
|
131
|
-
print(result)
|
|
131
|
+
print(response.result)
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
`generate()` returns what the HTTP API does: the typed answers in `.result`, the
|
|
135
|
+
reasoning of each field that thought in `.thinking`, and the call's tokens in
|
|
136
|
+
`.usage`. Example `.result`:
|
|
135
137
|
|
|
136
138
|
```python
|
|
137
139
|
{
|
|
@@ -160,7 +162,7 @@ Enum choices support `string`, `integer`, and `number` types, with at most 24 va
|
|
|
160
162
|
A string without `enum` generates free text:
|
|
161
163
|
|
|
162
164
|
```python
|
|
163
|
-
|
|
165
|
+
response = client.generate(
|
|
164
166
|
context="The train ticket is for a client meeting.",
|
|
165
167
|
questions={
|
|
166
168
|
"summary": {"type": "string", "instructions": "Summarize in one sentence."},
|
|
@@ -169,12 +171,13 @@ result = client.generate(
|
|
|
169
171
|
```
|
|
170
172
|
|
|
171
173
|
Free text stops after 128 tokens. Pass `text_max_tokens=` to the client for
|
|
172
|
-
longer answers,
|
|
174
|
+
longer answers, and ask for the length you want in the field's instructions.
|
|
175
|
+
`maxLength` is not supported.
|
|
173
176
|
|
|
174
177
|
Ask for a numeric answer without enumerating every possible value:
|
|
175
178
|
|
|
176
179
|
```python
|
|
177
|
-
|
|
180
|
+
response = client.generate(
|
|
178
181
|
context="Calculate the requested value accurately.",
|
|
179
182
|
questions={
|
|
180
183
|
"answer": {
|
|
@@ -184,7 +187,7 @@ result = client.generate(
|
|
|
184
187
|
},
|
|
185
188
|
)
|
|
186
189
|
|
|
187
|
-
print(result)
|
|
190
|
+
print(response.result)
|
|
188
191
|
# {"answer": 70.0}
|
|
189
192
|
```
|
|
190
193
|
|
|
@@ -210,7 +213,7 @@ Add `"null"` to the type to allow a missing value. The field returns `None`
|
|
|
210
213
|
when the input has no value for it:
|
|
211
214
|
|
|
212
215
|
```python
|
|
213
|
-
|
|
216
|
+
response = client.generate(
|
|
214
217
|
context="Read the attached receipt.",
|
|
215
218
|
images=["receipt.jpg"],
|
|
216
219
|
questions={
|
|
@@ -236,7 +239,7 @@ Thinking is off by default. Turn it on for the fields that need it; the others
|
|
|
236
239
|
answer at once, and the fields that think reason side by side:
|
|
237
240
|
|
|
238
241
|
```python
|
|
239
|
-
|
|
242
|
+
response = client.generate(context=context, questions={
|
|
240
243
|
"total": {"type": "number"},
|
|
241
244
|
"category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
|
|
242
245
|
"policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
|
|
@@ -244,13 +247,12 @@ result = client.generate(context=context, questions={
|
|
|
244
247
|
})
|
|
245
248
|
```
|
|
246
249
|
|
|
247
|
-
`thinking_budget` caps a field's reasoning
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
on to the typed answer.
|
|
250
|
+
`thinking_budget` caps a field's reasoning. Without one, a field may reason until
|
|
251
|
+
the model's context is full. When reasoning reaches the budget, TypeLLM closes it
|
|
252
|
+
and moves on to the typed answer.
|
|
251
253
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
+
`response.thinking` maps each field that thought to its reasoning, and
|
|
255
|
+
`response.usage.thinking_tokens` counts the reasoning tokens.
|
|
254
256
|
|
|
255
257
|
Models with always-on thinking reason on every field; `thinking_budget` applies
|
|
256
258
|
to them too. See [Supported models](#supported-models).
|
|
@@ -261,7 +263,7 @@ Pass images with `images=` alongside the text context. The served model must be
|
|
|
261
263
|
a vision-language model, such as `Qwen/Qwen3.8-27B`.
|
|
262
264
|
|
|
263
265
|
```python
|
|
264
|
-
|
|
266
|
+
response = client.generate(
|
|
265
267
|
context="The customer says this receipt was charged twice.",
|
|
266
268
|
images=["receipt.png"],
|
|
267
269
|
questions={
|
|
@@ -281,7 +283,7 @@ Fields run together by default, and each sees only the original context.
|
|
|
281
283
|
When a field needs earlier results, list them in `depends_on`:
|
|
282
284
|
|
|
283
285
|
```python
|
|
284
|
-
|
|
286
|
+
response = client.generate(
|
|
285
287
|
context="The payments service is returning errors after a deployment.",
|
|
286
288
|
questions={
|
|
287
289
|
"system": {
|
|
@@ -319,7 +321,7 @@ step reuses its parent's cached prompt. Unknown names and cycles raise
|
|
|
319
321
|
Set `return_probabilities` on individual enum or boolean fields:
|
|
320
322
|
|
|
321
323
|
```python
|
|
322
|
-
|
|
324
|
+
response = client.generate(
|
|
323
325
|
context=context,
|
|
324
326
|
questions={
|
|
325
327
|
"expense_type": {
|
|
@@ -347,19 +349,20 @@ result = client.generate(
|
|
|
347
349
|
Only opted-in fields return `value` and `probabilities`; other fields return plain values.
|
|
348
350
|
The option is not supported on open Numeric or Text fields.
|
|
349
351
|
|
|
350
|
-
|
|
352
|
+
`temperature` is 0 by default: each field gets its most likely answer. Above 0,
|
|
353
|
+
TypeLLM samples at that temperature:
|
|
351
354
|
|
|
352
355
|
```python
|
|
353
356
|
client = TypeLLMClient(
|
|
354
357
|
"http://127.0.0.1:30000",
|
|
355
|
-
mode="sample",
|
|
356
358
|
temperature=0.8,
|
|
357
359
|
seed=42,
|
|
358
360
|
)
|
|
359
361
|
```
|
|
360
362
|
|
|
361
363
|
Sampling applies to finite candidates for Choice fields and to token generation
|
|
362
|
-
for Numeric and Text fields. `temperature`
|
|
364
|
+
for Numeric and Text fields. `generate(temperature=...)` sets it for one call.
|
|
365
|
+
The older `mode="argmax"` / `mode="sample"` still works for now but is deprecated.
|
|
363
366
|
|
|
364
367
|
A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift
|
|
365
368
|
with its prefix cache and batching, so the same seed may still give different
|
|
@@ -370,7 +373,7 @@ For a one-off request, use the convenience function:
|
|
|
370
373
|
```python
|
|
371
374
|
from typellm import run_schema
|
|
372
375
|
|
|
373
|
-
|
|
376
|
+
response = run_schema(
|
|
374
377
|
context=context,
|
|
375
378
|
questions=questions,
|
|
376
379
|
base_url="http://127.0.0.1:30000",
|
|
@@ -383,7 +386,7 @@ result = run_schema(
|
|
|
383
386
|
Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
|
|
384
387
|
|
|
385
388
|
```python
|
|
386
|
-
|
|
389
|
+
response = client.generate(
|
|
387
390
|
context="A single roll of a fair die.",
|
|
388
391
|
questions={"roll": {
|
|
389
392
|
"type": "string",
|
|
@@ -416,19 +419,19 @@ prompts and usage, and connections to SGLang are reused.
|
|
|
416
419
|
import threading
|
|
417
420
|
|
|
418
421
|
cancel = threading.Event()
|
|
419
|
-
|
|
422
|
+
response = client.generate(
|
|
420
423
|
context=context,
|
|
421
424
|
questions=questions,
|
|
422
425
|
seed=7, # this call's random choices only
|
|
423
426
|
timeout=30, # seconds for the whole call; raises GenerationTimeout
|
|
424
427
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
425
428
|
)
|
|
426
|
-
print(
|
|
427
|
-
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7
|
|
429
|
+
print(response.usage)
|
|
430
|
+
# Usage(input_tokens=410, thinking_tokens=0, requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7)
|
|
428
431
|
```
|
|
429
432
|
|
|
430
|
-
`
|
|
431
|
-
|
|
433
|
+
`usage` reports the tokens of the call. When a call fails partway, the
|
|
434
|
+
exception's `.usage` holds what it spent. `input_tokens` is what you sent, each part
|
|
432
435
|
counted once: the context, the questions as JSON, and the images. The other
|
|
433
436
|
counts are SGLang's for the call's requests, where every prompt carries the
|
|
434
437
|
shared context. A timeout or cancel stops
|
|
@@ -467,8 +470,9 @@ Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
|
|
|
467
470
|
|
|
468
471
|
[Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
|
|
469
472
|
|
|
470
|
-
Use the checkpoint ID as `model=`.
|
|
471
|
-
|
|
473
|
+
Use the checkpoint ID as `model=`. TypeLLM loads the tokenizer from the server's
|
|
474
|
+
paths, then its served model name; if none of those loads locally, set
|
|
475
|
+
`tokenizer=` to the matching Hugging Face ID or local directory.
|
|
472
476
|
The tokenizer must load from standard artifacts without custom model code.
|
|
473
477
|
|
|
474
478
|
## Comparison with Jev-style models
|
|
@@ -81,7 +81,7 @@ client = TypeLLMClient(
|
|
|
81
81
|
Example request:
|
|
82
82
|
|
|
83
83
|
```python
|
|
84
|
-
|
|
84
|
+
response = client.generate(
|
|
85
85
|
context="""
|
|
86
86
|
Receipt from Hilton London
|
|
87
87
|
Total: £324.50
|
|
@@ -113,10 +113,12 @@ result = client.generate(
|
|
|
113
113
|
},
|
|
114
114
|
)
|
|
115
115
|
|
|
116
|
-
print(result)
|
|
116
|
+
print(response.result)
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
`generate()` returns what the HTTP API does: the typed answers in `.result`, the
|
|
120
|
+
reasoning of each field that thought in `.thinking`, and the call's tokens in
|
|
121
|
+
`.usage`. Example `.result`:
|
|
120
122
|
|
|
121
123
|
```python
|
|
122
124
|
{
|
|
@@ -145,7 +147,7 @@ Enum choices support `string`, `integer`, and `number` types, with at most 24 va
|
|
|
145
147
|
A string without `enum` generates free text:
|
|
146
148
|
|
|
147
149
|
```python
|
|
148
|
-
|
|
150
|
+
response = client.generate(
|
|
149
151
|
context="The train ticket is for a client meeting.",
|
|
150
152
|
questions={
|
|
151
153
|
"summary": {"type": "string", "instructions": "Summarize in one sentence."},
|
|
@@ -154,12 +156,13 @@ result = client.generate(
|
|
|
154
156
|
```
|
|
155
157
|
|
|
156
158
|
Free text stops after 128 tokens. Pass `text_max_tokens=` to the client for
|
|
157
|
-
longer answers,
|
|
159
|
+
longer answers, and ask for the length you want in the field's instructions.
|
|
160
|
+
`maxLength` is not supported.
|
|
158
161
|
|
|
159
162
|
Ask for a numeric answer without enumerating every possible value:
|
|
160
163
|
|
|
161
164
|
```python
|
|
162
|
-
|
|
165
|
+
response = client.generate(
|
|
163
166
|
context="Calculate the requested value accurately.",
|
|
164
167
|
questions={
|
|
165
168
|
"answer": {
|
|
@@ -169,7 +172,7 @@ result = client.generate(
|
|
|
169
172
|
},
|
|
170
173
|
)
|
|
171
174
|
|
|
172
|
-
print(result)
|
|
175
|
+
print(response.result)
|
|
173
176
|
# {"answer": 70.0}
|
|
174
177
|
```
|
|
175
178
|
|
|
@@ -195,7 +198,7 @@ Add `"null"` to the type to allow a missing value. The field returns `None`
|
|
|
195
198
|
when the input has no value for it:
|
|
196
199
|
|
|
197
200
|
```python
|
|
198
|
-
|
|
201
|
+
response = client.generate(
|
|
199
202
|
context="Read the attached receipt.",
|
|
200
203
|
images=["receipt.jpg"],
|
|
201
204
|
questions={
|
|
@@ -221,7 +224,7 @@ Thinking is off by default. Turn it on for the fields that need it; the others
|
|
|
221
224
|
answer at once, and the fields that think reason side by side:
|
|
222
225
|
|
|
223
226
|
```python
|
|
224
|
-
|
|
227
|
+
response = client.generate(context=context, questions={
|
|
225
228
|
"total": {"type": "number"},
|
|
226
229
|
"category": {"type": "string", "enum": ["meal", "travel", "equipment"]},
|
|
227
230
|
"policy_ok": {"type": "boolean", "instructions": "Does it meet the travel policy?",
|
|
@@ -229,13 +232,12 @@ result = client.generate(context=context, questions={
|
|
|
229
232
|
})
|
|
230
233
|
```
|
|
231
234
|
|
|
232
|
-
`thinking_budget` caps a field's reasoning
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
on to the typed answer.
|
|
235
|
+
`thinking_budget` caps a field's reasoning. Without one, a field may reason until
|
|
236
|
+
the model's context is full. When reasoning reaches the budget, TypeLLM closes it
|
|
237
|
+
and moves on to the typed answer.
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
`response.thinking` maps each field that thought to its reasoning, and
|
|
240
|
+
`response.usage.thinking_tokens` counts the reasoning tokens.
|
|
239
241
|
|
|
240
242
|
Models with always-on thinking reason on every field; `thinking_budget` applies
|
|
241
243
|
to them too. See [Supported models](#supported-models).
|
|
@@ -246,7 +248,7 @@ Pass images with `images=` alongside the text context. The served model must be
|
|
|
246
248
|
a vision-language model, such as `Qwen/Qwen3.8-27B`.
|
|
247
249
|
|
|
248
250
|
```python
|
|
249
|
-
|
|
251
|
+
response = client.generate(
|
|
250
252
|
context="The customer says this receipt was charged twice.",
|
|
251
253
|
images=["receipt.png"],
|
|
252
254
|
questions={
|
|
@@ -266,7 +268,7 @@ Fields run together by default, and each sees only the original context.
|
|
|
266
268
|
When a field needs earlier results, list them in `depends_on`:
|
|
267
269
|
|
|
268
270
|
```python
|
|
269
|
-
|
|
271
|
+
response = client.generate(
|
|
270
272
|
context="The payments service is returning errors after a deployment.",
|
|
271
273
|
questions={
|
|
272
274
|
"system": {
|
|
@@ -304,7 +306,7 @@ step reuses its parent's cached prompt. Unknown names and cycles raise
|
|
|
304
306
|
Set `return_probabilities` on individual enum or boolean fields:
|
|
305
307
|
|
|
306
308
|
```python
|
|
307
|
-
|
|
309
|
+
response = client.generate(
|
|
308
310
|
context=context,
|
|
309
311
|
questions={
|
|
310
312
|
"expense_type": {
|
|
@@ -332,19 +334,20 @@ result = client.generate(
|
|
|
332
334
|
Only opted-in fields return `value` and `probabilities`; other fields return plain values.
|
|
333
335
|
The option is not supported on open Numeric or Text fields.
|
|
334
336
|
|
|
335
|
-
|
|
337
|
+
`temperature` is 0 by default: each field gets its most likely answer. Above 0,
|
|
338
|
+
TypeLLM samples at that temperature:
|
|
336
339
|
|
|
337
340
|
```python
|
|
338
341
|
client = TypeLLMClient(
|
|
339
342
|
"http://127.0.0.1:30000",
|
|
340
|
-
mode="sample",
|
|
341
343
|
temperature=0.8,
|
|
342
344
|
seed=42,
|
|
343
345
|
)
|
|
344
346
|
```
|
|
345
347
|
|
|
346
348
|
Sampling applies to finite candidates for Choice fields and to token generation
|
|
347
|
-
for Numeric and Text fields. `temperature`
|
|
349
|
+
for Numeric and Text fields. `generate(temperature=...)` sets it for one call.
|
|
350
|
+
The older `mode="argmax"` / `mode="sample"` still works for now but is deprecated.
|
|
348
351
|
|
|
349
352
|
A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift
|
|
350
353
|
with its prefix cache and batching, so the same seed may still give different
|
|
@@ -355,7 +358,7 @@ For a one-off request, use the convenience function:
|
|
|
355
358
|
```python
|
|
356
359
|
from typellm import run_schema
|
|
357
360
|
|
|
358
|
-
|
|
361
|
+
response = run_schema(
|
|
359
362
|
context=context,
|
|
360
363
|
questions=questions,
|
|
361
364
|
base_url="http://127.0.0.1:30000",
|
|
@@ -368,7 +371,7 @@ result = run_schema(
|
|
|
368
371
|
Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
|
|
369
372
|
|
|
370
373
|
```python
|
|
371
|
-
|
|
374
|
+
response = client.generate(
|
|
372
375
|
context="A single roll of a fair die.",
|
|
373
376
|
questions={"roll": {
|
|
374
377
|
"type": "string",
|
|
@@ -401,19 +404,19 @@ prompts and usage, and connections to SGLang are reused.
|
|
|
401
404
|
import threading
|
|
402
405
|
|
|
403
406
|
cancel = threading.Event()
|
|
404
|
-
|
|
407
|
+
response = client.generate(
|
|
405
408
|
context=context,
|
|
406
409
|
questions=questions,
|
|
407
410
|
seed=7, # this call's random choices only
|
|
408
411
|
timeout=30, # seconds for the whole call; raises GenerationTimeout
|
|
409
412
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
410
413
|
)
|
|
411
|
-
print(
|
|
412
|
-
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7
|
|
414
|
+
print(response.usage)
|
|
415
|
+
# Usage(input_tokens=410, thinking_tokens=0, requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7)
|
|
413
416
|
```
|
|
414
417
|
|
|
415
|
-
`
|
|
416
|
-
|
|
418
|
+
`usage` reports the tokens of the call. When a call fails partway, the
|
|
419
|
+
exception's `.usage` holds what it spent. `input_tokens` is what you sent, each part
|
|
417
420
|
counted once: the context, the questions as JSON, and the images. The other
|
|
418
421
|
counts are SGLang's for the call's requests, where every prompt carries the
|
|
419
422
|
shared context. A timeout or cancel stops
|
|
@@ -452,8 +455,9 @@ Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
|
|
|
452
455
|
|
|
453
456
|
[Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
|
|
454
457
|
|
|
455
|
-
Use the checkpoint ID as `model=`.
|
|
456
|
-
|
|
458
|
+
Use the checkpoint ID as `model=`. TypeLLM loads the tokenizer from the server's
|
|
459
|
+
paths, then its served model name; if none of those loads locally, set
|
|
460
|
+
`tokenizer=` to the matching Hugging Face ID or local directory.
|
|
457
461
|
The tokenizer must load from standard artifacts without custom model code.
|
|
458
462
|
|
|
459
463
|
## Comparison with Jev-style models
|
|
@@ -96,7 +96,7 @@ class PrefixWarmupTests(unittest.TestCase):
|
|
|
96
96
|
with self.subTest(thinking=thinking):
|
|
97
97
|
client = TypeLLMClient(model="fake")
|
|
98
98
|
client.sglang = FakeServer()
|
|
99
|
-
client.generate(context="Receipt", questions=thinks(self.MIXED) if thinking else self.MIXED)
|
|
99
|
+
client.generate(context="Receipt", questions=thinks(self.MIXED) if thinking else self.MIXED).result
|
|
100
100
|
warm = client.sglang.requests("count")
|
|
101
101
|
self.assertEqual(len(warm), 1)
|
|
102
102
|
# First, before the thinking, number, text and scoring batches.
|
|
@@ -107,7 +107,7 @@ class PrefixWarmupTests(unittest.TestCase):
|
|
|
107
107
|
def test_open_fields_alone_are_not_warmed(self):
|
|
108
108
|
client = TypeLLMClient(model="fake")
|
|
109
109
|
client.sglang = FakeServer()
|
|
110
|
-
client.generate(context="Receipt", questions={"a": {"type": "integer"}, "n": {"type": "string"}})
|
|
110
|
+
client.generate(context="Receipt", questions={"a": {"type": "integer"}, "n": {"type": "string"}}).result
|
|
111
111
|
self.assertEqual(client.sglang.requests("count"), [])
|
|
112
112
|
|
|
113
113
|
|
|
@@ -123,7 +123,7 @@ class PerFieldThinkingTests(unittest.TestCase):
|
|
|
123
123
|
"hard": {"type": "boolean", "instructions": "Hard?", "thinking": True},
|
|
124
124
|
"easy": {"type": "boolean", "instructions": "Easy?"},
|
|
125
125
|
"count": {"type": "integer"},
|
|
126
|
-
})
|
|
126
|
+
}).result
|
|
127
127
|
self.assertEqual(result, {"hard": True, "easy": True, "count": 7})
|
|
128
128
|
[prompt] = self.thinking_prompts(client.sglang) # one batch, one prompt
|
|
129
129
|
self.assertIn("Hard?", prompt)
|
|
@@ -139,7 +139,7 @@ class PerFieldThinkingTests(unittest.TestCase):
|
|
|
139
139
|
"a": {"type": "boolean", "instructions": "A?", "thinking": True},
|
|
140
140
|
"b": {"type": "boolean", "instructions": "B?", "thinking": True},
|
|
141
141
|
"skip": {"type": "boolean", "instructions": "Skip?", "thinking": False},
|
|
142
|
-
})
|
|
142
|
+
}).result
|
|
143
143
|
prompts = self.thinking_prompts(client.sglang)
|
|
144
144
|
self.assertEqual(len(client.sglang.requests("think")), 1)
|
|
145
145
|
self.assertEqual(len(prompts), 2)
|
|
@@ -153,7 +153,7 @@ class PerFieldThinkingTests(unittest.TestCase):
|
|
|
153
153
|
"short": {"type": "boolean", "instructions": "Short?", "thinking": True, "thinking_budget": 100},
|
|
154
154
|
"long": {"type": "boolean", "instructions": "Long?", "thinking": True, "thinking_budget": 300},
|
|
155
155
|
"default": {"type": "boolean", "instructions": "Default?", "thinking": True},
|
|
156
|
-
})
|
|
156
|
+
}).result
|
|
157
157
|
[request] = client.sglang.requests("think")
|
|
158
158
|
budgets = {next(q for q in ("Short?", "Long?", "Default?") if q in t): params["max_new_tokens"]
|
|
159
159
|
for t, params in zip(request["text"], request["sampling_params"])}
|
|
@@ -172,16 +172,14 @@ class PerFieldThinkingTests(unittest.TestCase):
|
|
|
172
172
|
|
|
173
173
|
client = TypeLLMClient(model="fake")
|
|
174
174
|
client.sglang = Metered()
|
|
175
|
-
client.generate(context="Receipt", questions={
|
|
175
|
+
done = client.generate(context="Receipt", questions={
|
|
176
176
|
"hard": {"type": "boolean", "instructions": "Hard?", "thinking": True},
|
|
177
177
|
"easy": {"type": "boolean", "instructions": "Easy?"},
|
|
178
178
|
})
|
|
179
|
-
self.assertEqual(
|
|
180
|
-
self.assertEqual(
|
|
181
|
-
self.assertGreater(
|
|
182
|
-
|
|
183
|
-
client.generate(questions={"x": {"type": "boolean"}})
|
|
184
|
-
self.assertEqual(client.last_thinking, {})
|
|
179
|
+
self.assertEqual(done.thinking, {"hard": "Reasoned."})
|
|
180
|
+
self.assertEqual(done.usage.thinking_tokens, 40)
|
|
181
|
+
self.assertGreater(done.usage.completion_tokens, 40) # answers count too
|
|
182
|
+
self.assertEqual(client.generate(context="Receipt", questions={"x": {"type": "boolean"}}).thinking, {})
|
|
185
183
|
|
|
186
184
|
def test_invalid_settings_are_schema_errors(self):
|
|
187
185
|
from typellm import SchemaError
|
|
@@ -202,7 +200,7 @@ class BatchedThinkingTests(unittest.TestCase):
|
|
|
202
200
|
def test_one_thinking_request_covers_every_prompt_in_a_layer(self):
|
|
203
201
|
client = TypeLLMClient(model="fake")
|
|
204
202
|
client.sglang = FakeServer()
|
|
205
|
-
result = client.generate(context="Receipt", questions=thinks(self.QUESTIONS))
|
|
203
|
+
result = client.generate(context="Receipt", questions=thinks(self.QUESTIONS)).result
|
|
206
204
|
self.assertEqual(result, {"flag": True, "count": 7, "name": "blue", "pick": "x"})
|
|
207
205
|
[think] = client.sglang.requests("think")
|
|
208
206
|
# Four fields plus the reversed ordering of "pick".
|
|
@@ -219,14 +217,14 @@ class BatchedThinkingTests(unittest.TestCase):
|
|
|
219
217
|
"a": {"type": "boolean"}, "b": {"type": "integer"},
|
|
220
218
|
"c": {"type": "boolean", "depends_on": ["a", "b"]},
|
|
221
219
|
"d": {"type": "string", "depends_on": ["a"]},
|
|
222
|
-
}))
|
|
220
|
+
})).result
|
|
223
221
|
self.assertEqual([width(p) for p in client.sglang.requests("think")], [2, 2])
|
|
224
222
|
|
|
225
223
|
def test_images_count_prompt_tokens_in_one_batch(self):
|
|
226
224
|
client = TypeLLMClient(model="fake")
|
|
227
225
|
client.sglang = FakeServer()
|
|
228
226
|
client.generate(context="Receipt", images=[PNG],
|
|
229
|
-
questions=thinks({"a": {"type": "boolean"}, "b": {"type": "boolean"}}))
|
|
227
|
+
questions=thinks({"a": {"type": "boolean"}, "b": {"type": "boolean"}})).result
|
|
230
228
|
counts = [p for p in client.sglang.requests("count") if width(p) > 1]
|
|
231
229
|
self.assertEqual([width(p) for p in counts], [2])
|
|
232
230
|
[think] = client.sglang.requests("think")
|
|
@@ -42,9 +42,9 @@ class SharedClientTests(unittest.TestCase):
|
|
|
42
42
|
def run(n):
|
|
43
43
|
client.generate(context=f"Receipt {n}", questions={
|
|
44
44
|
"a": {"type": "integer"}, "b": {"type": "boolean"},
|
|
45
|
-
})
|
|
45
|
+
}).result
|
|
46
46
|
finished.wait() # Every call has finished before any reads its prompts.
|
|
47
|
-
return n, client.
|
|
47
|
+
return n, client._last_prompts.get()
|
|
48
48
|
|
|
49
49
|
with ThreadPoolExecutor(8) as pool:
|
|
50
50
|
for n, prompts in pool.map(run, range(16)):
|
|
@@ -56,22 +56,22 @@ class SharedClientTests(unittest.TestCase):
|
|
|
56
56
|
client = TypeLLMClient(model="fake", seed=1)
|
|
57
57
|
client.sglang = FakeServer()
|
|
58
58
|
state = client.rng.getstate()
|
|
59
|
-
client.generate(context="Roll", questions=PICK, seed=5)
|
|
60
|
-
first = client.
|
|
61
|
-
client.generate(context="Roll", questions=PICK, seed=5)
|
|
62
|
-
self.assertEqual(client.
|
|
59
|
+
client.generate(context="Roll", questions=PICK, seed=5).result
|
|
60
|
+
first = client._last_prompts.get()
|
|
61
|
+
client.generate(context="Roll", questions=PICK, seed=5).result
|
|
62
|
+
self.assertEqual(client._last_prompts.get(), first)
|
|
63
63
|
self.assertEqual(client.rng.getstate(), state)
|
|
64
|
-
client.generate(context="Roll", questions=PICK)
|
|
64
|
+
client.generate(context="Roll", questions=PICK).result
|
|
65
65
|
self.assertNotEqual(client.rng.getstate(), state)
|
|
66
66
|
|
|
67
67
|
def test_clients_pickle_after_a_call(self):
|
|
68
68
|
client = TypeLLMClient(model="fake")
|
|
69
69
|
client.sglang = FakeServer()
|
|
70
|
-
client.generate(context="Receipt", questions={"b": {"type": "boolean"}})
|
|
70
|
+
client.generate(context="Receipt", questions={"b": {"type": "boolean"}}).result
|
|
71
71
|
copy = pickle.loads(pickle.dumps(client))
|
|
72
|
-
self.assertEqual(copy.
|
|
73
|
-
copy.generate(context="Receipt", questions={"b": {"type": "boolean"}})
|
|
74
|
-
self.assertEqual(len(copy.
|
|
72
|
+
self.assertEqual(copy._last_prompts.get(), [])
|
|
73
|
+
copy.generate(context="Receipt", questions={"b": {"type": "boolean"}}).result
|
|
74
|
+
self.assertEqual(len(copy._last_prompts.get()), 1)
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class MeteredServer(SlowServer):
|
|
@@ -102,19 +102,23 @@ class UsageTests(unittest.TestCase):
|
|
|
102
102
|
def test_usage_sums_every_generate_request_of_the_call(self):
|
|
103
103
|
client = TypeLLMClient(model="fake")
|
|
104
104
|
client.sglang = MeteredServer()
|
|
105
|
-
|
|
106
|
-
client.generate(context="Receipt", questions=self.QUESTIONS)
|
|
105
|
+
usage = client.generate(context="Receipt", questions=self.QUESTIONS).usage
|
|
107
106
|
n = prompts_sent(client.sglang)
|
|
108
107
|
# The input counts once, though every prompt carries it.
|
|
109
108
|
sent = client.sglang.count_tokens("Receipt") + client.sglang.count_tokens(json.dumps(self.QUESTIONS))
|
|
110
|
-
self.assertEqual(
|
|
109
|
+
self.assertEqual(usage, Usage(
|
|
111
110
|
requests=len(client.sglang.payloads),
|
|
112
111
|
prompt_tokens=10 * n, cached_tokens=4 * n, completion_tokens=n, input_tokens=sent,
|
|
113
112
|
))
|
|
114
|
-
self.assertGreater(
|
|
113
|
+
self.assertGreater(usage.requests, 1)
|
|
115
114
|
client.sglang.payloads.clear()
|
|
116
|
-
client.generate(context="Receipt", questions={"b": {"type": "boolean"}})
|
|
117
|
-
self.assertEqual(
|
|
115
|
+
usage = client.generate(context="Receipt", questions={"b": {"type": "boolean"}}).usage
|
|
116
|
+
self.assertEqual(usage.requests, len(client.sglang.payloads))
|
|
117
|
+
|
|
118
|
+
def test_usage_shows_billed_tokens_first_and_only_counts_it_has(self):
|
|
119
|
+
self.assertEqual(repr(Usage(input_tokens=66)), "Usage(input_tokens=66, thinking_tokens=0)")
|
|
120
|
+
self.assertEqual(repr(Usage(input_tokens=5, requests=2, prompt_tokens=30)),
|
|
121
|
+
"Usage(input_tokens=5, thinking_tokens=0, requests=2, prompt_tokens=30)")
|
|
118
122
|
|
|
119
123
|
def test_concurrent_calls_count_only_their_own_requests(self):
|
|
120
124
|
client = TypeLLMClient(model="fake")
|
|
@@ -122,9 +126,9 @@ class UsageTests(unittest.TestCase):
|
|
|
122
126
|
finished = threading.Barrier(4)
|
|
123
127
|
|
|
124
128
|
def run(questions):
|
|
125
|
-
client.generate(context="Receipt", questions=questions)
|
|
129
|
+
usage = client.generate(context="Receipt", questions=questions).usage
|
|
126
130
|
finished.wait()
|
|
127
|
-
return
|
|
131
|
+
return usage.requests
|
|
128
132
|
|
|
129
133
|
one = {"b": {"type": "boolean"}}
|
|
130
134
|
with ThreadPoolExecutor(4) as pool:
|
|
@@ -137,13 +141,13 @@ class UsageTests(unittest.TestCase):
|
|
|
137
141
|
def test_a_failed_call_still_reports_the_requests_it_made(self):
|
|
138
142
|
client = TypeLLMClient(model="fake")
|
|
139
143
|
client.sglang = MeteredServer(fail_after=2)
|
|
140
|
-
with self.assertRaisesRegex(SGLangError, "went away"):
|
|
144
|
+
with self.assertRaisesRegex(SGLangError, "went away") as caught:
|
|
141
145
|
client.generate(context="Receipt", questions=self.QUESTIONS)
|
|
142
|
-
self.assertEqual(
|
|
143
|
-
self.assertGreater(
|
|
144
|
-
with self.assertRaises(ValueError):
|
|
146
|
+
self.assertEqual(caught.exception.usage.requests, 2)
|
|
147
|
+
self.assertGreater(caught.exception.usage.input_tokens, 0)
|
|
148
|
+
with self.assertRaises(ValueError) as caught:
|
|
145
149
|
client.generate(questions=self.QUESTIONS)
|
|
146
|
-
self.
|
|
150
|
+
self.assertFalse(hasattr(caught.exception, "usage")) # refused before any work
|
|
147
151
|
|
|
148
152
|
|
|
149
153
|
class DeadlineTests(unittest.TestCase):
|
|
@@ -158,13 +162,13 @@ class DeadlineTests(unittest.TestCase):
|
|
|
158
162
|
|
|
159
163
|
client = TypeLLMClient(model="fake")
|
|
160
164
|
client.sglang = Slower()
|
|
161
|
-
client.generate(context="Receipt", questions=self.QUESTIONS)
|
|
165
|
+
client.generate(context="Receipt", questions=self.QUESTIONS).result
|
|
162
166
|
needed = len(client.sglang.payloads)
|
|
163
167
|
client.sglang.payloads.clear()
|
|
164
|
-
with self.assertRaises(GenerationTimeout):
|
|
168
|
+
with self.assertRaises(GenerationTimeout) as caught:
|
|
165
169
|
client.generate(context="Receipt", questions=self.QUESTIONS, timeout=0.05)
|
|
166
170
|
self.assertLess(len(client.sglang.payloads), needed)
|
|
167
|
-
self.assertEqual(
|
|
171
|
+
self.assertEqual(caught.exception.usage.requests, len(client.sglang.payloads))
|
|
168
172
|
|
|
169
173
|
def test_a_cancelled_call_sends_no_further_requests(self):
|
|
170
174
|
cancel = threading.Event()
|
|
@@ -179,7 +183,7 @@ class DeadlineTests(unittest.TestCase):
|
|
|
179
183
|
client = TypeLLMClient(model="fake")
|
|
180
184
|
client.sglang = Cancelling()
|
|
181
185
|
with self.assertRaises(GenerationCancelled):
|
|
182
|
-
client.generate(context="Receipt", questions=self.QUESTIONS, cancel=cancel)
|
|
186
|
+
client.generate(context="Receipt", questions=self.QUESTIONS, cancel=cancel).result
|
|
183
187
|
self.assertEqual(len(client.sglang.payloads), 2)
|
|
184
188
|
|
|
185
189
|
def test_socket_timeouts_never_outlast_the_call(self):
|
|
@@ -256,7 +260,7 @@ class DeadlineTests(unittest.TestCase):
|
|
|
256
260
|
for kwargs in ({"timeout": 0}, {"timeout": -1}, {"timeout": True}, {"timeout": "5"},
|
|
257
261
|
{"cancel": True}):
|
|
258
262
|
with self.subTest(**kwargs), self.assertRaises(ValueError):
|
|
259
|
-
client.generate(context="Receipt", questions={"b": {"type": "boolean"}}, **kwargs)
|
|
263
|
+
client.generate(context="Receipt", questions={"b": {"type": "boolean"}}, **kwargs).result
|
|
260
264
|
self.assertEqual(client.sglang.paths, []) # Rejected before even compiling.
|
|
261
265
|
|
|
262
266
|
|