typellm 0.2.2__tar.gz → 0.2.4__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.2/typellm.egg-info → typellm-0.2.4}/PKG-INFO +7 -4
- {typellm-0.2.2 → typellm-0.2.4}/README.md +6 -3
- {typellm-0.2.2 → typellm-0.2.4}/pyproject.toml +1 -1
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_concurrency.py +38 -1
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_images.py +15 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/runtime.py +5 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/sglang.py +62 -21
- {typellm-0.2.2 → typellm-0.2.4/typellm.egg-info}/PKG-INFO +7 -4
- {typellm-0.2.2 → typellm-0.2.4}/LICENSE +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/setup.cfg +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_batching.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_dependencies.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_grammar_fields.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_nullable.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_permutations.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_prefill.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_probabilities.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_prompts.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_protocols.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_text.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/tests/test_typellm.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/__init__.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/__main__.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/benchmark.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/cli.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/images.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/protocol.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm/schema.py +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm.egg-info/SOURCES.txt +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm.egg-info/dependency_links.txt +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm.egg-info/entry_points.txt +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/typellm.egg-info/requires.txt +0 -0
- {typellm-0.2.2 → typellm-0.2.4}/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.4
|
|
4
4
|
Summary: Type-Safe Decoding for Autoregressive LLMs
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://typellm.ai/
|
|
@@ -424,11 +424,14 @@ result = client.generate(
|
|
|
424
424
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
425
425
|
)
|
|
426
426
|
print(client.last_usage)
|
|
427
|
-
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0)
|
|
427
|
+
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0, input_tokens=410)
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
-
`last_usage` reports the
|
|
431
|
-
|
|
430
|
+
`last_usage` reports the tokens of the last call made in the current thread,
|
|
431
|
+
including a call that failed partway. `input_tokens` is what you sent, each part
|
|
432
|
+
counted once: the context, the questions as JSON, and the images. The other
|
|
433
|
+
counts are SGLang's for the call's requests, where every prompt carries the
|
|
434
|
+
shared context. A timeout or cancel stops
|
|
432
435
|
the call before its next SGLang request.
|
|
433
436
|
|
|
434
437
|
## Cost analysis
|
|
@@ -409,11 +409,14 @@ result = client.generate(
|
|
|
409
409
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
410
410
|
)
|
|
411
411
|
print(client.last_usage)
|
|
412
|
-
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0)
|
|
412
|
+
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0, input_tokens=410)
|
|
413
413
|
```
|
|
414
414
|
|
|
415
|
-
`last_usage` reports the
|
|
416
|
-
|
|
415
|
+
`last_usage` reports the tokens of the last call made in the current thread,
|
|
416
|
+
including a call that failed partway. `input_tokens` is what you sent, each part
|
|
417
|
+
counted once: the context, the questions as JSON, and the images. The other
|
|
418
|
+
counts are SGLang's for the call's requests, where every prompt carries the
|
|
419
|
+
shared context. A timeout or cancel stops
|
|
417
420
|
the call before its next SGLang request.
|
|
418
421
|
|
|
419
422
|
## Cost analysis
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import pickle
|
|
2
3
|
import sys
|
|
3
4
|
import threading
|
|
@@ -104,10 +105,13 @@ class UsageTests(unittest.TestCase):
|
|
|
104
105
|
self.assertIsNone(client.last_usage)
|
|
105
106
|
client.generate(context="Receipt", questions=self.QUESTIONS)
|
|
106
107
|
n = prompts_sent(client.sglang)
|
|
108
|
+
# The input counts once, though every prompt carries it.
|
|
109
|
+
sent = client.sglang.count_tokens("Receipt") + client.sglang.count_tokens(json.dumps(self.QUESTIONS))
|
|
107
110
|
self.assertEqual(client.last_usage, Usage(
|
|
108
111
|
requests=len(client.sglang.payloads),
|
|
109
|
-
prompt_tokens=10 * n, cached_tokens=4 * n, completion_tokens=n,
|
|
112
|
+
prompt_tokens=10 * n, cached_tokens=4 * n, completion_tokens=n, input_tokens=sent,
|
|
110
113
|
))
|
|
114
|
+
self.assertGreater(client.last_usage.requests, 1)
|
|
111
115
|
client.sglang.payloads.clear()
|
|
112
116
|
client.generate(context="Receipt", questions={"b": {"type": "boolean"}})
|
|
113
117
|
self.assertEqual(client.last_usage.requests, len(client.sglang.payloads))
|
|
@@ -136,6 +140,7 @@ class UsageTests(unittest.TestCase):
|
|
|
136
140
|
with self.assertRaisesRegex(SGLangError, "went away"):
|
|
137
141
|
client.generate(context="Receipt", questions=self.QUESTIONS)
|
|
138
142
|
self.assertEqual(client.last_usage.requests, 2)
|
|
143
|
+
self.assertGreater(client.last_usage.input_tokens, 0)
|
|
139
144
|
with self.assertRaises(ValueError):
|
|
140
145
|
client.generate(questions=self.QUESTIONS)
|
|
141
146
|
self.assertIsNone(client.last_usage)
|
|
@@ -207,6 +212,38 @@ class DeadlineTests(unittest.TestCase):
|
|
|
207
212
|
client._generate({"text": "x"})
|
|
208
213
|
self.assertNotIsInstance(caught.exception, GenerationTimeout)
|
|
209
214
|
|
|
215
|
+
def dropping(self, error, times):
|
|
216
|
+
"""A client whose server drops the first `times` requests with `error`."""
|
|
217
|
+
seen = []
|
|
218
|
+
|
|
219
|
+
def handler(request):
|
|
220
|
+
seen.append(request)
|
|
221
|
+
if len(seen) <= times:
|
|
222
|
+
raise error("connection dropped", request=request)
|
|
223
|
+
return httpx.Response(200, json={"text": "ok"})
|
|
224
|
+
|
|
225
|
+
client = SGLangClient()
|
|
226
|
+
client._http_client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
227
|
+
return client, seen
|
|
228
|
+
|
|
229
|
+
def test_a_dropped_connection_is_retried_once(self):
|
|
230
|
+
for error in (httpx.ReadError, httpx.RemoteProtocolError, httpx.WriteError):
|
|
231
|
+
client, seen = self.dropping(error, 1)
|
|
232
|
+
with self.subTest(error=error.__name__):
|
|
233
|
+
self.assertEqual(client._request("/generate", {"text": "x"}), {"text": "ok"})
|
|
234
|
+
self.assertEqual(len(seen), 2)
|
|
235
|
+
client, seen = self.dropping(httpx.ReadError, 2)
|
|
236
|
+
with self.assertRaisesRegex(SGLangError, "Could not reach"):
|
|
237
|
+
client._request("/generate", {"text": "x"})
|
|
238
|
+
self.assertEqual(len(seen), 2)
|
|
239
|
+
|
|
240
|
+
def test_a_refused_connection_is_not_retried(self):
|
|
241
|
+
for error in (httpx.ConnectError, httpx.ReadTimeout):
|
|
242
|
+
client, seen = self.dropping(error, 1)
|
|
243
|
+
with self.subTest(error=error.__name__), self.assertRaises(SGLangError):
|
|
244
|
+
client._request("/generate", {"text": "x"})
|
|
245
|
+
self.assertEqual(len(seen), 1)
|
|
246
|
+
|
|
210
247
|
def test_invalid_budgets_are_rejected(self):
|
|
211
248
|
class Recording(FakeServer):
|
|
212
249
|
def _request(self, path, payload=None, *, allow_text=False):
|
|
@@ -271,6 +271,21 @@ class ImageRequestTests(unittest.TestCase):
|
|
|
271
271
|
self.assertEqual(rest, [])
|
|
272
272
|
|
|
273
273
|
|
|
274
|
+
class ImageInputTokenTests(unittest.TestCase):
|
|
275
|
+
def test_images_count_as_the_server_expands_them(self):
|
|
276
|
+
client = TypeLLMClient(model="fake-vl")
|
|
277
|
+
client.sglang = FakeServerClient()
|
|
278
|
+
questions = {"a": {"type": "boolean"}}
|
|
279
|
+
client.generate(context="Receipt", images=[PNG], questions=questions)
|
|
280
|
+
first = client.sglang.generate_payloads[0]["text"]
|
|
281
|
+
first = first if isinstance(first, str) else first[0]
|
|
282
|
+
count = client.sglang.count_tokens
|
|
283
|
+
# The server reported 900 prompt tokens, with the placeholder expanded.
|
|
284
|
+
image = 900 - count(first) + count(VISION)
|
|
285
|
+
self.assertEqual(client.last_usage.input_tokens,
|
|
286
|
+
count("Receipt") + count('{"a": {"type": "boolean"}}') + image)
|
|
287
|
+
|
|
288
|
+
|
|
274
289
|
class RuntimeContentTests(unittest.TestCase):
|
|
275
290
|
def test_fake_backends_receive_image_parts(self):
|
|
276
291
|
class Recording(FakeSGLang):
|
|
@@ -395,6 +395,11 @@ class TypeLLMClient:
|
|
|
395
395
|
with call_scope(timeout, cancel) as scope:
|
|
396
396
|
try:
|
|
397
397
|
decisions = self.compile_schema(schema)
|
|
398
|
+
count = getattr(self.sglang, "count_tokens", None)
|
|
399
|
+
if callable(count):
|
|
400
|
+
scope.usage.input_tokens = sum(map(count, (
|
|
401
|
+
context, json.dumps(questions if questions is not None else schema, ensure_ascii=False))))
|
|
402
|
+
scope.unmeasured_images = len(encoded_images)
|
|
398
403
|
# Independent fields run together; depends_on turns the fields into a
|
|
399
404
|
# graph whose layers run in order.
|
|
400
405
|
run = (_execute_dependency_decisions if any(d.depends_on is not None for d in decisions)
|
|
@@ -37,7 +37,13 @@ class GenerationCancelled(RuntimeError):
|
|
|
37
37
|
|
|
38
38
|
@dataclass
|
|
39
39
|
class Usage:
|
|
40
|
-
"""
|
|
40
|
+
"""The tokens of one generate() call.
|
|
41
|
+
|
|
42
|
+
input_tokens is what the caller sent, each part counted once: the context,
|
|
43
|
+
the questions or schema as JSON, and the images as the server expands them.
|
|
44
|
+
The other counts are what SGLang reported for the call's /generate requests,
|
|
45
|
+
where the shared context is part of every prompt.
|
|
46
|
+
"""
|
|
41
47
|
|
|
42
48
|
requests: int = 0
|
|
43
49
|
prompt_tokens: int = 0
|
|
@@ -45,6 +51,7 @@ class Usage:
|
|
|
45
51
|
completion_tokens: int = 0
|
|
46
52
|
# The part of completion_tokens that is reasoning, not typed answers.
|
|
47
53
|
thinking_tokens: int = 0
|
|
54
|
+
input_tokens: int = 0
|
|
48
55
|
|
|
49
56
|
def _add(self, response: Any) -> None:
|
|
50
57
|
self.requests += 1
|
|
@@ -75,6 +82,8 @@ class _CallScope:
|
|
|
75
82
|
usage: Usage = field(default_factory=Usage)
|
|
76
83
|
deadline: float | None = None # time.monotonic() when the call runs out of time
|
|
77
84
|
cancel: threading.Event | None = None
|
|
85
|
+
# Images whose tokens input_tokens still lacks; the first response measures them.
|
|
86
|
+
unmeasured_images: int = 0
|
|
78
87
|
|
|
79
88
|
def expired(self) -> bool:
|
|
80
89
|
return self.deadline is not None and time.monotonic() >= self.deadline
|
|
@@ -243,9 +252,11 @@ class SGLangClient:
|
|
|
243
252
|
def _http(self) -> httpx.Client:
|
|
244
253
|
with self._load_lock:
|
|
245
254
|
if self._http_client is None:
|
|
246
|
-
# Keep-alive connections: one call makes many small requests.
|
|
255
|
+
# Keep-alive connections: one call makes many small requests. Idle
|
|
256
|
+
# ones are dropped before SGLang closes them (5 s), so a request
|
|
257
|
+
# rarely goes out on a connection the server has just closed.
|
|
247
258
|
self._http_client = httpx.Client(limits=httpx.Limits(
|
|
248
|
-
max_connections=None, max_keepalive_connections=64,
|
|
259
|
+
max_connections=None, max_keepalive_connections=64, keepalive_expiry=2.0,
|
|
249
260
|
))
|
|
250
261
|
return self._http_client
|
|
251
262
|
|
|
@@ -260,24 +271,15 @@ class SGLangClient:
|
|
|
260
271
|
scope = _call_scope.get()
|
|
261
272
|
timeout = self.timeout if scope is None else scope.socket_timeout(self.timeout)
|
|
262
273
|
try:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
detail = response.read().decode("utf-8", errors="replace")
|
|
274
|
-
except httpx.HTTPError as read_error:
|
|
275
|
-
detail = f"Could not read error response: {read_error}"
|
|
276
|
-
raise SGLangError(
|
|
277
|
-
f"SGLang {path} returned HTTP {response.status_code}: {detail}",
|
|
278
|
-
status=response.status_code,
|
|
279
|
-
)
|
|
280
|
-
raw = response.read().decode("utf-8")
|
|
274
|
+
try:
|
|
275
|
+
raw = self._send(path, payload is None, body, timeout)
|
|
276
|
+
except (httpx.ReadError, httpx.WriteError, httpx.RemoteProtocolError):
|
|
277
|
+
# The connection dropped before an answer, most often a pooled one
|
|
278
|
+
# the server had just closed. SGLang is fine: try once on a new one.
|
|
279
|
+
if scope is not None:
|
|
280
|
+
scope.check()
|
|
281
|
+
timeout = scope.socket_timeout(self.timeout)
|
|
282
|
+
raw = self._send(path, payload is None, body, timeout)
|
|
281
283
|
except httpx.HTTPError as exc:
|
|
282
284
|
# Connection failures, timeouts, resets and truncated responses.
|
|
283
285
|
raise SGLangError(
|
|
@@ -292,6 +294,26 @@ class SGLangClient:
|
|
|
292
294
|
f"SGLang {path} returned non-JSON data: {raw[:500]}"
|
|
293
295
|
) from exc
|
|
294
296
|
|
|
297
|
+
def _send(self, path: str, get: bool, body: bytes | None, timeout: float) -> str:
|
|
298
|
+
# Streamed so a failed body read still reports the status it follows.
|
|
299
|
+
with self._http().stream(
|
|
300
|
+
"GET" if get else "POST",
|
|
301
|
+
self.base_url + path,
|
|
302
|
+
content=body,
|
|
303
|
+
headers={"Content-Type": "application/json"},
|
|
304
|
+
timeout=timeout,
|
|
305
|
+
) as response:
|
|
306
|
+
if response.status_code >= 400:
|
|
307
|
+
try:
|
|
308
|
+
detail = response.read().decode("utf-8", errors="replace")
|
|
309
|
+
except httpx.HTTPError as read_error:
|
|
310
|
+
detail = f"Could not read error response: {read_error}"
|
|
311
|
+
raise SGLangError(
|
|
312
|
+
f"SGLang {path} returned HTTP {response.status_code}: {detail}",
|
|
313
|
+
status=response.status_code,
|
|
314
|
+
)
|
|
315
|
+
return response.read().decode("utf-8")
|
|
316
|
+
|
|
295
317
|
@contextmanager
|
|
296
318
|
def images(self, images: Sequence[str]) -> Iterator[None]:
|
|
297
319
|
"""Attach encoded images to every /generate request made in this block."""
|
|
@@ -351,8 +373,27 @@ class SGLangClient:
|
|
|
351
373
|
raise GenerationTimeout("generate() ran out of time") from exc
|
|
352
374
|
raise
|
|
353
375
|
scope.usage._add(response)
|
|
376
|
+
if scope.unmeasured_images:
|
|
377
|
+
self._measure_images(scope, payload, response)
|
|
354
378
|
return response
|
|
355
379
|
|
|
380
|
+
def count_tokens(self, text: str) -> int:
|
|
381
|
+
return len(self._get_chat_tokenizer().encode(text, add_special_tokens=False))
|
|
382
|
+
|
|
383
|
+
def _measure_images(self, scope: _CallScope, payload: Mapping[str, Any], response: Any) -> None:
|
|
384
|
+
"""Add the images' tokens to input_tokens: the server's prompt count less the local one."""
|
|
385
|
+
text = payload.get("text")
|
|
386
|
+
prompt = text if isinstance(text, str) else text[0] if isinstance(text, list) and text else None
|
|
387
|
+
item = response[0] if isinstance(response, list) and response else response
|
|
388
|
+
meta = item.get("meta_info") if isinstance(item, Mapping) else None
|
|
389
|
+
served = meta.get("prompt_tokens") if isinstance(meta, Mapping) else None
|
|
390
|
+
if not isinstance(prompt, str) or type(served) is not int:
|
|
391
|
+
return
|
|
392
|
+
# The server replaces each placeholder with the image's tokens.
|
|
393
|
+
placeholders = scope.unmeasured_images * self.count_tokens(self.image_placeholder())
|
|
394
|
+
scope.usage.input_tokens += max(0, served - self.count_tokens(prompt) + placeholders)
|
|
395
|
+
scope.unmeasured_images = 0
|
|
396
|
+
|
|
356
397
|
def _with_images(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
357
398
|
images = self._active_images.get()
|
|
358
399
|
if not images:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typellm
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Type-Safe Decoding for Autoregressive LLMs
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://typellm.ai/
|
|
@@ -424,11 +424,14 @@ result = client.generate(
|
|
|
424
424
|
cancel=cancel, # set it from another thread; raises GenerationCancelled
|
|
425
425
|
)
|
|
426
426
|
print(client.last_usage)
|
|
427
|
-
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0)
|
|
427
|
+
# Usage(requests=4, prompt_tokens=1830, cached_tokens=1504, completion_tokens=7, thinking_tokens=0, input_tokens=410)
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
-
`last_usage` reports the
|
|
431
|
-
|
|
430
|
+
`last_usage` reports the tokens of the last call made in the current thread,
|
|
431
|
+
including a call that failed partway. `input_tokens` is what you sent, each part
|
|
432
|
+
counted once: the context, the questions as JSON, and the images. The other
|
|
433
|
+
counts are SGLang's for the call's requests, where every prompt carries the
|
|
434
|
+
shared context. A timeout or cancel stops
|
|
432
435
|
the call before its next SGLang request.
|
|
433
436
|
|
|
434
437
|
## Cost analysis
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|