typellm 0.2.3__tar.gz → 0.2.5__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.3/typellm.egg-info → typellm-0.2.5}/PKG-INFO +5 -4
  2. {typellm-0.2.3 → typellm-0.2.5}/README.md +4 -3
  3. {typellm-0.2.3 → typellm-0.2.5}/pyproject.toml +1 -1
  4. {typellm-0.2.3 → typellm-0.2.5}/tests/test_concurrency.py +32 -0
  5. {typellm-0.2.3 → typellm-0.2.5}/tests/test_grammar_fields.py +1 -1
  6. {typellm-0.2.3 → typellm-0.2.5}/tests/test_permutations.py +1 -1
  7. {typellm-0.2.3 → typellm-0.2.5}/tests/test_typellm.py +26 -0
  8. {typellm-0.2.3 → typellm-0.2.5}/typellm/cli.py +3 -2
  9. {typellm-0.2.3 → typellm-0.2.5}/typellm/runtime.py +40 -13
  10. {typellm-0.2.3 → typellm-0.2.5}/typellm/sglang.py +33 -20
  11. {typellm-0.2.3 → typellm-0.2.5/typellm.egg-info}/PKG-INFO +5 -4
  12. {typellm-0.2.3 → typellm-0.2.5}/LICENSE +0 -0
  13. {typellm-0.2.3 → typellm-0.2.5}/setup.cfg +0 -0
  14. {typellm-0.2.3 → typellm-0.2.5}/tests/test_batching.py +0 -0
  15. {typellm-0.2.3 → typellm-0.2.5}/tests/test_dependencies.py +0 -0
  16. {typellm-0.2.3 → typellm-0.2.5}/tests/test_images.py +0 -0
  17. {typellm-0.2.3 → typellm-0.2.5}/tests/test_nullable.py +0 -0
  18. {typellm-0.2.3 → typellm-0.2.5}/tests/test_prefill.py +0 -0
  19. {typellm-0.2.3 → typellm-0.2.5}/tests/test_probabilities.py +0 -0
  20. {typellm-0.2.3 → typellm-0.2.5}/tests/test_prompts.py +0 -0
  21. {typellm-0.2.3 → typellm-0.2.5}/tests/test_protocols.py +0 -0
  22. {typellm-0.2.3 → typellm-0.2.5}/tests/test_text.py +0 -0
  23. {typellm-0.2.3 → typellm-0.2.5}/typellm/__init__.py +0 -0
  24. {typellm-0.2.3 → typellm-0.2.5}/typellm/__main__.py +0 -0
  25. {typellm-0.2.3 → typellm-0.2.5}/typellm/benchmark.py +0 -0
  26. {typellm-0.2.3 → typellm-0.2.5}/typellm/images.py +0 -0
  27. {typellm-0.2.3 → typellm-0.2.5}/typellm/protocol.py +0 -0
  28. {typellm-0.2.3 → typellm-0.2.5}/typellm/schema.py +0 -0
  29. {typellm-0.2.3 → typellm-0.2.5}/typellm.egg-info/SOURCES.txt +0 -0
  30. {typellm-0.2.3 → typellm-0.2.5}/typellm.egg-info/dependency_links.txt +0 -0
  31. {typellm-0.2.3 → typellm-0.2.5}/typellm.egg-info/entry_points.txt +0 -0
  32. {typellm-0.2.3 → typellm-0.2.5}/typellm.egg-info/requires.txt +0 -0
  33. {typellm-0.2.3 → typellm-0.2.5}/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
3
+ Version: 0.2.5
4
4
  Summary: Type-Safe Decoding for Autoregressive LLMs
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://typellm.ai/
@@ -347,19 +347,20 @@ result = client.generate(
347
347
  Only opted-in fields return `value` and `probabilities`; other fields return plain values.
348
348
  The option is not supported on open Numeric or Text fields.
349
349
 
350
- Argmax is the default. To enable sampling:
350
+ `temperature` is 0 by default: each field gets its most likely answer. Above 0,
351
+ TypeLLM samples at that temperature:
351
352
 
352
353
  ```python
353
354
  client = TypeLLMClient(
354
355
  "http://127.0.0.1:30000",
355
- mode="sample",
356
356
  temperature=0.8,
357
357
  seed=42,
358
358
  )
359
359
  ```
360
360
 
361
361
  Sampling applies to finite candidates for Choice fields and to token generation
362
- for Numeric and Text fields. `temperature` controls sampling in each case.
362
+ for Numeric and Text fields. `generate(temperature=...)` sets it for one call.
363
+ The older `mode="argmax"` / `mode="sample"` still works for now but is deprecated.
363
364
 
364
365
  A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift
365
366
  with its prefix cache and batching, so the same seed may still give different
@@ -332,19 +332,20 @@ result = client.generate(
332
332
  Only opted-in fields return `value` and `probabilities`; other fields return plain values.
333
333
  The option is not supported on open Numeric or Text fields.
334
334
 
335
- Argmax is the default. To enable sampling:
335
+ `temperature` is 0 by default: each field gets its most likely answer. Above 0,
336
+ TypeLLM samples at that temperature:
336
337
 
337
338
  ```python
338
339
  client = TypeLLMClient(
339
340
  "http://127.0.0.1:30000",
340
- mode="sample",
341
341
  temperature=0.8,
342
342
  seed=42,
343
343
  )
344
344
  ```
345
345
 
346
346
  Sampling applies to finite candidates for Choice fields and to token generation
347
- for Numeric and Text fields. `temperature` controls sampling in each case.
347
+ for Numeric and Text fields. `generate(temperature=...)` sets it for one call.
348
+ The older `mode="argmax"` / `mode="sample"` still works for now but is deprecated.
348
349
 
349
350
  A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift
350
351
  with its prefix cache and batching, so the same seed may still give different
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "typellm"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Type-Safe Decoding for Autoregressive LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -212,6 +212,38 @@ class DeadlineTests(unittest.TestCase):
212
212
  client._generate({"text": "x"})
213
213
  self.assertNotIsInstance(caught.exception, GenerationTimeout)
214
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
+
215
247
  def test_invalid_budgets_are_rejected(self):
216
248
  class Recording(FakeServer):
217
249
  def _request(self, path, payload=None, *, allow_text=False):
@@ -87,7 +87,7 @@ class GrammarNumberTests(unittest.TestCase):
87
87
  self.assertIn("null", patterns[3])
88
88
 
89
89
  def test_sampling_passes_the_temperature_and_no_truncation(self):
90
- client = TypeLLMClient(model="fake", mode="sample", temperature=0.7, seed=1)
90
+ client = TypeLLMClient(model="fake", temperature=0.7, seed=1)
91
91
  client.sglang = FakeServer()
92
92
  client.generate(context="Receipt", questions={"a": {"type": "integer"}})
93
93
  [params] = regex_requests(client.sglang)[0]["sampling_params"]
@@ -39,7 +39,7 @@ class PermutationTests(unittest.TestCase):
39
39
 
40
40
  def test_sampling_uses_averaged_distribution(self):
41
41
  with patch('typellm.runtime._sample', return_value='B') as sample:
42
- _client, result = self.run_case(False, ['one', 'two', 'three'], mode='sample', temperature=.5)
42
+ _client, result = self.run_case(False, ['one', 'two', 'three'], temperature=.5)
43
43
  self.assertEqual(result['roll']['value'], 'two')
44
44
  self.assertEqual(sample.call_count, 2) # one final choice plus the boolean
45
45
  for p in sample.call_args_list[0].args[0].values():
@@ -909,3 +909,29 @@ class JsonSchemaExecutionTests(unittest.TestCase):
909
909
 
910
910
  if __name__ == "__main__":
911
911
  unittest.main()
912
+
913
+
914
+ class DecodingTests(unittest.TestCase):
915
+ def test_temperature_alone_decides(self):
916
+ from typellm.runtime import _resolve_decoding
917
+
918
+ self.assertEqual(_resolve_decoding(None, None), ("argmax", 0.0))
919
+ self.assertEqual(_resolve_decoding(None, 0), ("argmax", 0.0))
920
+ self.assertEqual(_resolve_decoding(None, 0.7), ("sample", 0.7))
921
+ for bad in (-1, float("nan"), True, "1"):
922
+ with self.assertRaises(ValueError):
923
+ _resolve_decoding(None, bad)
924
+
925
+ def test_deprecated_mode_still_works(self):
926
+ from typellm.runtime import _resolve_decoding
927
+
928
+ with self.assertWarns(DeprecationWarning):
929
+ self.assertEqual(_resolve_decoding("sample", None), ("sample", 1.0))
930
+ with self.assertWarns(DeprecationWarning):
931
+ self.assertEqual(_resolve_decoding("argmax", 0.7)[0], "argmax")
932
+ with self.assertWarns(DeprecationWarning), self.assertRaises(ValueError):
933
+ _resolve_decoding("sample", 0)
934
+
935
+ def test_call_temperature_overrides_client(self):
936
+ client = TypeLLMClient(model="fake", temperature=0.7)
937
+ self.assertEqual((client.mode, client.temperature), ("sample", 0.7))
@@ -51,8 +51,9 @@ def main() -> None:
51
51
  default=os.environ.get("TYPELLM_TOKENIZER"),
52
52
  help="Tokenizer path or Hugging Face ID; normally discovered from SGLang",
53
53
  )
54
- parser.add_argument("--mode", choices=("argmax", "sample"), default="argmax")
55
- parser.add_argument("--temperature", type=float, default=1.0)
54
+ parser.add_argument("--temperature", type=float, default=0.0,
55
+ help="0 picks the most likely answer; above 0 samples")
56
+ parser.add_argument("--mode", choices=("argmax", "sample"), help="deprecated; use --temperature")
56
57
  parser.add_argument("--numeric-max-digits", type=int, default=32)
57
58
  parser.add_argument("--seed", type=int, default=0)
58
59
  parser.add_argument("--probabilities", action="store_true")
@@ -8,6 +8,7 @@ import math
8
8
  import os
9
9
  import random
10
10
  import threading
11
+ import warnings
11
12
  from contextlib import nullcontext
12
13
  from contextvars import ContextVar
13
14
  from dataclasses import dataclass, replace
@@ -149,8 +150,8 @@ class TypeLLMClient:
149
150
  base_url: str = "http://127.0.0.1:30000",
150
151
  model: str | None = None,
151
152
  *,
152
- mode: str = "argmax",
153
- temperature: float = 1.0,
153
+ mode: str | None = None,
154
+ temperature: float | None = None,
154
155
  seed: int | None = None,
155
156
  timeout: float = 120.0,
156
157
  label_pool: Sequence[str] | None = None,
@@ -159,7 +160,10 @@ class TypeLLMClient:
159
160
  thinking_budget: int | None = None,
160
161
  text_max_tokens: int = 128,
161
162
  ) -> None:
162
- _validate_decoding(mode, temperature)
163
+ """temperature 0 (the default) picks the most likely answer; above 0
164
+ samples at that temperature. mode is deprecated: temperature alone decides.
165
+ """
166
+ mode, temperature = _resolve_decoding(mode, temperature)
163
167
  if type(numeric_max_digits) is not int or numeric_max_digits <= 0:
164
168
  raise ValueError("numeric_max_digits must be a positive integer")
165
169
  self.sglang = SGLangClient(
@@ -387,9 +391,13 @@ class TypeLLMClient:
387
391
  if not isinstance(questions, Mapping):
388
392
  raise SchemaError("questions must be a mapping of field names to definitions")
389
393
  schema = {"type": "object", "properties": questions}
390
- active_mode = self.mode if mode is None else mode
391
- active_temperature = self.temperature if temperature is None else temperature
392
- _validate_decoding(active_mode, active_temperature)
394
+ if mode is None and temperature is None:
395
+ active_mode, active_temperature = self.mode, self.temperature
396
+ else:
397
+ # A call's own temperature decides for it, whatever the client's mode.
398
+ if temperature is None:
399
+ temperature = self.temperature or None
400
+ active_mode, active_temperature = _resolve_decoding(mode, temperature)
393
401
  rng = self.rng if seed is None else random.Random(seed)
394
402
  # The time budget covers compiling too: labels are tokenized by SGLang.
395
403
  with call_scope(timeout, cancel) as scope:
@@ -462,11 +470,30 @@ def _sample(probs: Mapping[str, float], rng: random.Random) -> str:
462
470
  return last
463
471
 
464
472
 
465
- def _validate_decoding(mode: str, temperature: float) -> None:
466
- if mode not in {"argmax", "sample"}:
467
- raise ValueError("mode must be 'argmax' or 'sample'")
468
- if mode == "sample" and (not math.isfinite(temperature) or temperature <= 0):
469
- raise ValueError("temperature must be finite and > 0 in sample mode")
473
+ def _resolve_decoding(mode: str | None, temperature: float | None) -> tuple[str, float]:
474
+ """The (mode, temperature) that temperature, and the deprecated mode, ask for.
475
+
476
+ temperature 0 picks the most likely answer (argmax); above 0 samples. For
477
+ older code, mode="sample" without a temperature samples at 1.0 and
478
+ mode="argmax" is argmax whatever the temperature.
479
+ """
480
+ if mode is not None:
481
+ warnings.warn("mode is deprecated; temperature 0 picks the most likely answer and "
482
+ "above 0 samples", DeprecationWarning, stacklevel=3)
483
+ if mode not in {"argmax", "sample"}:
484
+ raise ValueError("mode must be 'argmax' or 'sample'")
485
+ if mode == "sample" and temperature is None:
486
+ temperature = 1.0
487
+ if temperature is None:
488
+ temperature = 0.0
489
+ if (isinstance(temperature, bool) or not isinstance(temperature, (int, float))
490
+ or not math.isfinite(temperature) or temperature < 0):
491
+ raise ValueError("temperature must be a finite number >= 0")
492
+ if mode == "argmax":
493
+ return "argmax", float(temperature)
494
+ if mode == "sample" and temperature == 0:
495
+ raise ValueError("temperature must be > 0 in sample mode")
496
+ return ("sample" if temperature > 0 else "argmax"), float(temperature)
470
497
 
471
498
 
472
499
  def _numeric_text_is_complete(text: str, numeric_type: str) -> bool:
@@ -922,8 +949,8 @@ def _print_final_prompts(prefixes: Sequence[str]) -> None:
922
949
  def run_schema(
923
950
  context: str | None = None,
924
951
  schema: Mapping[str, Any] | None = None,
925
- mode: str = "argmax",
926
- temperature: float = 1.0,
952
+ mode: str | None = None,
953
+ temperature: float | None = None,
927
954
  *,
928
955
  state: str | None = None,
929
956
  questions: Mapping[str, Any] | None = None,
@@ -252,9 +252,11 @@ class SGLangClient:
252
252
  def _http(self) -> httpx.Client:
253
253
  with self._load_lock:
254
254
  if self._http_client is None:
255
- # 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.
256
258
  self._http_client = httpx.Client(limits=httpx.Limits(
257
- max_connections=None, max_keepalive_connections=64,
259
+ max_connections=None, max_keepalive_connections=64, keepalive_expiry=2.0,
258
260
  ))
259
261
  return self._http_client
260
262
 
@@ -269,24 +271,15 @@ class SGLangClient:
269
271
  scope = _call_scope.get()
270
272
  timeout = self.timeout if scope is None else scope.socket_timeout(self.timeout)
271
273
  try:
272
- # Streamed so a failed body read still reports the status it follows.
273
- with self._http().stream(
274
- "GET" if payload is None else "POST",
275
- self.base_url + path,
276
- content=body,
277
- headers={"Content-Type": "application/json"},
278
- timeout=timeout,
279
- ) as response:
280
- if response.status_code >= 400:
281
- try:
282
- detail = response.read().decode("utf-8", errors="replace")
283
- except httpx.HTTPError as read_error:
284
- detail = f"Could not read error response: {read_error}"
285
- raise SGLangError(
286
- f"SGLang {path} returned HTTP {response.status_code}: {detail}",
287
- status=response.status_code,
288
- )
289
- 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)
290
283
  except httpx.HTTPError as exc:
291
284
  # Connection failures, timeouts, resets and truncated responses.
292
285
  raise SGLangError(
@@ -301,6 +294,26 @@ class SGLangClient:
301
294
  f"SGLang {path} returned non-JSON data: {raw[:500]}"
302
295
  ) from exc
303
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
+
304
317
  @contextmanager
305
318
  def images(self, images: Sequence[str]) -> Iterator[None]:
306
319
  """Attach encoded images to every /generate request made in this block."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typellm
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Type-Safe Decoding for Autoregressive LLMs
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://typellm.ai/
@@ -347,19 +347,20 @@ result = client.generate(
347
347
  Only opted-in fields return `value` and `probabilities`; other fields return plain values.
348
348
  The option is not supported on open Numeric or Text fields.
349
349
 
350
- Argmax is the default. To enable sampling:
350
+ `temperature` is 0 by default: each field gets its most likely answer. Above 0,
351
+ TypeLLM samples at that temperature:
351
352
 
352
353
  ```python
353
354
  client = TypeLLMClient(
354
355
  "http://127.0.0.1:30000",
355
- mode="sample",
356
356
  temperature=0.8,
357
357
  seed=42,
358
358
  )
359
359
  ```
360
360
 
361
361
  Sampling applies to finite candidates for Choice fields and to token generation
362
- for Numeric and Text fields. `temperature` controls sampling in each case.
362
+ for Numeric and Text fields. `generate(temperature=...)` sets it for one call.
363
+ The older `mode="argmax"` / `mode="sample"` still works for now but is deprecated.
363
364
 
364
365
  A seed fixes TypeLLM's own random choices. SGLang's log probabilities can shift
365
366
  with its prefix cache and batching, so the same seed may still give different
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