typellm 0.2.5__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.5/typellm.egg-info → typellm-0.2.6}/PKG-INFO +31 -28
- {typellm-0.2.5 → typellm-0.2.6}/README.md +30 -27
- {typellm-0.2.5 → typellm-0.2.6}/pyproject.toml +1 -1
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_batching.py +13 -15
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_concurrency.py +33 -29
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_dependencies.py +18 -18
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_grammar_fields.py +8 -8
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_images.py +9 -9
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_nullable.py +8 -8
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_permutations.py +6 -6
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_prefill.py +12 -26
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_probabilities.py +2 -2
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_prompts.py +4 -13
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_protocols.py +22 -0
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_text.py +15 -18
- {typellm-0.2.5 → typellm-0.2.6}/tests/test_typellm.py +55 -80
- {typellm-0.2.5 → typellm-0.2.6}/typellm/__init__.py +2 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm/cli.py +1 -1
- {typellm-0.2.5 → typellm-0.2.6}/typellm/runtime.py +25 -66
- {typellm-0.2.5 → typellm-0.2.6}/typellm/schema.py +10 -23
- {typellm-0.2.5 → typellm-0.2.6}/typellm/sglang.py +50 -76
- {typellm-0.2.5 → typellm-0.2.6/typellm.egg-info}/PKG-INFO +31 -28
- {typellm-0.2.5 → typellm-0.2.6}/LICENSE +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/setup.cfg +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm/__main__.py +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm/benchmark.py +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm/images.py +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm/protocol.py +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm.egg-info/SOURCES.txt +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm.egg-info/dependency_links.txt +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm.egg-info/entry_points.txt +0 -0
- {typellm-0.2.5 → typellm-0.2.6}/typellm.egg-info/requires.txt +0 -0
- {typellm-0.2.5 → 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": {
|
|
@@ -371,7 +373,7 @@ For a one-off request, use the convenience function:
|
|
|
371
373
|
```python
|
|
372
374
|
from typellm import run_schema
|
|
373
375
|
|
|
374
|
-
|
|
376
|
+
response = run_schema(
|
|
375
377
|
context=context,
|
|
376
378
|
questions=questions,
|
|
377
379
|
base_url="http://127.0.0.1:30000",
|
|
@@ -384,7 +386,7 @@ result = run_schema(
|
|
|
384
386
|
Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
|
|
385
387
|
|
|
386
388
|
```python
|
|
387
|
-
|
|
389
|
+
response = client.generate(
|
|
388
390
|
context="A single roll of a fair die.",
|
|
389
391
|
questions={"roll": {
|
|
390
392
|
"type": "string",
|
|
@@ -417,19 +419,19 @@ prompts and usage, and connections to SGLang are reused.
|
|
|
417
419
|
import threading
|
|
418
420
|
|
|
419
421
|
cancel = threading.Event()
|
|
420
|
-
|
|
422
|
+
response = client.generate(
|
|
421
423
|
context=context,
|
|
422
424
|
questions=questions,
|
|
423
425
|
seed=7, # this call's random choices only
|
|
424
426
|
timeout=30, # seconds for the whole call; raises GenerationTimeout
|
|
425
427
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
426
428
|
)
|
|
427
|
-
print(
|
|
428
|
-
# 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)
|
|
429
431
|
```
|
|
430
432
|
|
|
431
|
-
`
|
|
432
|
-
|
|
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
|
|
433
435
|
counted once: the context, the questions as JSON, and the images. The other
|
|
434
436
|
counts are SGLang's for the call's requests, where every prompt carries the
|
|
435
437
|
shared context. A timeout or cancel stops
|
|
@@ -468,8 +470,9 @@ Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
|
|
|
468
470
|
|
|
469
471
|
[Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
|
|
470
472
|
|
|
471
|
-
Use the checkpoint ID as `model=`.
|
|
472
|
-
|
|
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.
|
|
473
476
|
The tokenizer must load from standard artifacts without custom model code.
|
|
474
477
|
|
|
475
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": {
|
|
@@ -356,7 +358,7 @@ For a one-off request, use the convenience function:
|
|
|
356
358
|
```python
|
|
357
359
|
from typellm import run_schema
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
response = run_schema(
|
|
360
362
|
context=context,
|
|
361
363
|
questions=questions,
|
|
362
364
|
base_url="http://127.0.0.1:30000",
|
|
@@ -369,7 +371,7 @@ result = run_schema(
|
|
|
369
371
|
Add `permutations` to an `enum` question to reduce option-order bias. TypeLLM averages the probabilities and keeps the same return format.
|
|
370
372
|
|
|
371
373
|
```python
|
|
372
|
-
|
|
374
|
+
response = client.generate(
|
|
373
375
|
context="A single roll of a fair die.",
|
|
374
376
|
questions={"roll": {
|
|
375
377
|
"type": "string",
|
|
@@ -402,19 +404,19 @@ prompts and usage, and connections to SGLang are reused.
|
|
|
402
404
|
import threading
|
|
403
405
|
|
|
404
406
|
cancel = threading.Event()
|
|
405
|
-
|
|
407
|
+
response = client.generate(
|
|
406
408
|
context=context,
|
|
407
409
|
questions=questions,
|
|
408
410
|
seed=7, # this call's random choices only
|
|
409
411
|
timeout=30, # seconds for the whole call; raises GenerationTimeout
|
|
410
412
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
411
413
|
)
|
|
412
|
-
print(
|
|
413
|
-
# 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)
|
|
414
416
|
```
|
|
415
417
|
|
|
416
|
-
`
|
|
417
|
-
|
|
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
|
|
418
420
|
counted once: the context, the questions as JSON, and the images. The other
|
|
419
421
|
counts are SGLang's for the call's requests, where every prompt carries the
|
|
420
422
|
shared context. A timeout or cancel stops
|
|
@@ -453,8 +455,9 @@ Other sizes in the Qwen3.5 and Qwen3.8 families are expected to be compatible.
|
|
|
453
455
|
|
|
454
456
|
[Image input](#image-input) has been tested with `Qwen/Qwen3.8-27B`.
|
|
455
457
|
|
|
456
|
-
Use the checkpoint ID as `model=`.
|
|
457
|
-
|
|
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.
|
|
458
461
|
The tokenizer must load from standard artifacts without custom model code.
|
|
459
462
|
|
|
460
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
|
|