typellm 0.2.2__tar.gz → 0.2.3__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.2.2/typellm.egg-info → typellm-0.2.3}/PKG-INFO +7 -4
  2. {typellm-0.2.2 → typellm-0.2.3}/README.md +6 -3
  3. {typellm-0.2.2 → typellm-0.2.3}/pyproject.toml +1 -1
  4. {typellm-0.2.2 → typellm-0.2.3}/tests/test_concurrency.py +6 -1
  5. {typellm-0.2.2 → typellm-0.2.3}/tests/test_images.py +15 -0
  6. {typellm-0.2.2 → typellm-0.2.3}/typellm/runtime.py +5 -0
  7. {typellm-0.2.2 → typellm-0.2.3}/typellm/sglang.py +29 -1
  8. {typellm-0.2.2 → typellm-0.2.3/typellm.egg-info}/PKG-INFO +7 -4
  9. {typellm-0.2.2 → typellm-0.2.3}/LICENSE +0 -0
  10. {typellm-0.2.2 → typellm-0.2.3}/setup.cfg +0 -0
  11. {typellm-0.2.2 → typellm-0.2.3}/tests/test_batching.py +0 -0
  12. {typellm-0.2.2 → typellm-0.2.3}/tests/test_dependencies.py +0 -0
  13. {typellm-0.2.2 → typellm-0.2.3}/tests/test_grammar_fields.py +0 -0
  14. {typellm-0.2.2 → typellm-0.2.3}/tests/test_nullable.py +0 -0
  15. {typellm-0.2.2 → typellm-0.2.3}/tests/test_permutations.py +0 -0
  16. {typellm-0.2.2 → typellm-0.2.3}/tests/test_prefill.py +0 -0
  17. {typellm-0.2.2 → typellm-0.2.3}/tests/test_probabilities.py +0 -0
  18. {typellm-0.2.2 → typellm-0.2.3}/tests/test_prompts.py +0 -0
  19. {typellm-0.2.2 → typellm-0.2.3}/tests/test_protocols.py +0 -0
  20. {typellm-0.2.2 → typellm-0.2.3}/tests/test_text.py +0 -0
  21. {typellm-0.2.2 → typellm-0.2.3}/tests/test_typellm.py +0 -0
  22. {typellm-0.2.2 → typellm-0.2.3}/typellm/__init__.py +0 -0
  23. {typellm-0.2.2 → typellm-0.2.3}/typellm/__main__.py +0 -0
  24. {typellm-0.2.2 → typellm-0.2.3}/typellm/benchmark.py +0 -0
  25. {typellm-0.2.2 → typellm-0.2.3}/typellm/cli.py +0 -0
  26. {typellm-0.2.2 → typellm-0.2.3}/typellm/images.py +0 -0
  27. {typellm-0.2.2 → typellm-0.2.3}/typellm/protocol.py +0 -0
  28. {typellm-0.2.2 → typellm-0.2.3}/typellm/schema.py +0 -0
  29. {typellm-0.2.2 → typellm-0.2.3}/typellm.egg-info/SOURCES.txt +0 -0
  30. {typellm-0.2.2 → typellm-0.2.3}/typellm.egg-info/dependency_links.txt +0 -0
  31. {typellm-0.2.2 → typellm-0.2.3}/typellm.egg-info/entry_points.txt +0 -0
  32. {typellm-0.2.2 → typellm-0.2.3}/typellm.egg-info/requires.txt +0 -0
  33. {typellm-0.2.2 → typellm-0.2.3}/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.2
3
+ Version: 0.2.3
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 SGLang requests and tokens of the last call made in the
431
- current thread, including a call that failed partway. A timeout or cancel stops
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 SGLang requests and tokens of the last call made in the
416
- current thread, including a call that failed partway. A timeout or cancel stops
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "typellm"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Type-Safe Decoding for Autoregressive LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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)
@@ -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
- """What SGLang reported for the /generate requests of one generate() call."""
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
@@ -351,8 +360,27 @@ class SGLangClient:
351
360
  raise GenerationTimeout("generate() ran out of time") from exc
352
361
  raise
353
362
  scope.usage._add(response)
363
+ if scope.unmeasured_images:
364
+ self._measure_images(scope, payload, response)
354
365
  return response
355
366
 
367
+ def count_tokens(self, text: str) -> int:
368
+ return len(self._get_chat_tokenizer().encode(text, add_special_tokens=False))
369
+
370
+ def _measure_images(self, scope: _CallScope, payload: Mapping[str, Any], response: Any) -> None:
371
+ """Add the images' tokens to input_tokens: the server's prompt count less the local one."""
372
+ text = payload.get("text")
373
+ prompt = text if isinstance(text, str) else text[0] if isinstance(text, list) and text else None
374
+ item = response[0] if isinstance(response, list) and response else response
375
+ meta = item.get("meta_info") if isinstance(item, Mapping) else None
376
+ served = meta.get("prompt_tokens") if isinstance(meta, Mapping) else None
377
+ if not isinstance(prompt, str) or type(served) is not int:
378
+ return
379
+ # The server replaces each placeholder with the image's tokens.
380
+ placeholders = scope.unmeasured_images * self.count_tokens(self.image_placeholder())
381
+ scope.usage.input_tokens += max(0, served - self.count_tokens(prompt) + placeholders)
382
+ scope.unmeasured_images = 0
383
+
356
384
  def _with_images(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
357
385
  images = self._active_images.get()
358
386
  if not images:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typellm
3
- Version: 0.2.2
3
+ Version: 0.2.3
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 SGLang requests and tokens of the last call made in the
431
- current thread, including a call that failed partway. A timeout or cancel stops
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