typevet 0.1.0.dev1__py3-none-any.whl

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 (133) hide show
  1. typevet/__init__.py +67 -0
  2. typevet/_version.py +43 -0
  3. typevet/adapters/__init__.py +16 -0
  4. typevet/adapters/diagnostics/__init__.py +59 -0
  5. typevet/adapters/diagnostics/fields.py +108 -0
  6. typevet/adapters/diagnostics/generation_events.py +75 -0
  7. typevet/adapters/diagnostics/http_events.py +91 -0
  8. typevet/adapters/diagnostics/logs.py +116 -0
  9. typevet/adapters/diagnostics/redaction.py +94 -0
  10. typevet/adapters/diagnostics/settings.py +77 -0
  11. typevet/adapters/inbound/__init__.py +65 -0
  12. typevet/adapters/inbound/api.py +61 -0
  13. typevet/adapters/inbound/backend_settings.py +608 -0
  14. typevet/adapters/inbound/cord_semantic_acceptance_cli.py +135 -0
  15. typevet/adapters/inbound/helpers.py +67 -0
  16. typevet/adapters/inbound/settings.py +154 -0
  17. typevet/adapters/outbound/__init__.py +62 -0
  18. typevet/adapters/outbound/async_fake.py +123 -0
  19. typevet/adapters/outbound/async_llama_cpp.py +168 -0
  20. typevet/adapters/outbound/chat_completion.py +148 -0
  21. typevet/adapters/outbound/fake.py +119 -0
  22. typevet/adapters/outbound/gemma/__init__.py +84 -0
  23. typevet/adapters/outbound/gemma/answer_binding.py +290 -0
  24. typevet/adapters/outbound/gemma/scoring_prefix.py +102 -0
  25. typevet/adapters/outbound/gemma/served_template.py +131 -0
  26. typevet/adapters/outbound/gemma_native_vision_factory.py +286 -0
  27. typevet/adapters/outbound/generation_finite.py +50 -0
  28. typevet/adapters/outbound/http_errors.py +38 -0
  29. typevet/adapters/outbound/judgment_scoring.py +471 -0
  30. typevet/adapters/outbound/llama_cpp.py +187 -0
  31. typevet/adapters/outbound/llama_cpp_http.py +103 -0
  32. typevet/adapters/outbound/llama_cpp_multimodal.py +145 -0
  33. typevet/adapters/outbound/llama_cpp_scoring.py +333 -0
  34. typevet/adapters/outbound/vllm_content.py +63 -0
  35. typevet/adapters/outbound/vllm_generation.py +163 -0
  36. typevet/adapters/outbound/vllm_generation_async.py +257 -0
  37. typevet/adapters/outbound/vllm_http.py +121 -0
  38. typevet/adapters/outbound/vllm_judgment_factory.py +210 -0
  39. typevet/adapters/outbound/vllm_scoring.py +372 -0
  40. typevet/domain/__init__.py +193 -0
  41. typevet/domain/candidate_scoring_request.py +134 -0
  42. typevet/domain/candidate_scoring_response.py +96 -0
  43. typevet/domain/candidate_scoring_validate.py +197 -0
  44. typevet/domain/decision_compile.py +368 -0
  45. typevet/domain/decision_execute.py +229 -0
  46. typevet/domain/decisions.py +129 -0
  47. typevet/domain/errors.py +235 -0
  48. typevet/domain/field_instructions.py +134 -0
  49. typevet/domain/judgment_answers.py +198 -0
  50. typevet/domain/judgment_normalize.py +243 -0
  51. typevet/domain/judgment_questions.py +187 -0
  52. typevet/domain/judgment_response.py +102 -0
  53. typevet/domain/media.py +101 -0
  54. typevet/domain/models.py +115 -0
  55. typevet/domain/question_schema.py +254 -0
  56. typevet/domain/scoring_stage.py +34 -0
  57. typevet/evaluation/__init__.py +19 -0
  58. typevet/evaluation/consumer_http_accounting.py +135 -0
  59. typevet/evaluation/cord_expense_call_accounting.py +108 -0
  60. typevet/evaluation/cord_expense_live_harness.py +75 -0
  61. typevet/evaluation/cord_expense_receipt_requirement.py +212 -0
  62. typevet/evaluation/cord_expense_smoke.py +328 -0
  63. typevet/evaluation/cord_semantic_acceptance.py +463 -0
  64. typevet/evaluation/cord_semantic_acceptance_report.py +90 -0
  65. typevet/evaluation/cord_semantic_metrics.py +57 -0
  66. typevet/evaluation/datasets/__init__.py +30 -0
  67. typevet/evaluation/datasets/banking77.py +270 -0
  68. typevet/evaluation/datasets/boolq.py +550 -0
  69. typevet/evaluation/datasets/boolq_download.py +132 -0
  70. typevet/evaluation/datasets/civil_comments.py +450 -0
  71. typevet/evaluation/datasets/clinc.py +125 -0
  72. typevet/evaluation/datasets/clinc_domains.json +172 -0
  73. typevet/evaluation/datasets/clinc_download.py +103 -0
  74. typevet/evaluation/datasets/clinc_plus_intent_names.json +153 -0
  75. typevet/evaluation/datasets/clinc_rows.py +306 -0
  76. typevet/evaluation/datasets/clinc_shard.py +156 -0
  77. typevet/evaluation/datasets/cord_expense.py +381 -0
  78. typevet/evaluation/datasets/difraud.py +256 -0
  79. typevet/evaluation/datasets/go_emotions.py +368 -0
  80. typevet/evaluation/datasets/go_emotions_download.py +154 -0
  81. typevet/evaluation/datasets/hyperpartisan.py +562 -0
  82. typevet/evaluation/datasets/partner_guard.py +170 -0
  83. typevet/evaluation/datasets/psai.py +415 -0
  84. typevet/evaluation/datasets/psai_download.py +167 -0
  85. typevet/evaluation/datasets/psai_evidence_pilot.py +570 -0
  86. typevet/evaluation/datasets/psai_schema.py +157 -0
  87. typevet/evaluation/datasets/psai_stream.py +166 -0
  88. typevet/evaluation/datasets/psai_vision.py +534 -0
  89. typevet/evaluation/datasets/psai_vision_controls.py +502 -0
  90. typevet/evaluation/datasets/pubmedqa.py +420 -0
  91. typevet/evaluation/experiment_identity.py +677 -0
  92. typevet/evaluation/psai_vision_consumer_accounting.py +190 -0
  93. typevet/evaluation/psai_vision_consumer_dispatch.py +216 -0
  94. typevet/evaluation/psai_vision_consumer_harness.py +342 -0
  95. typevet/evaluation/psai_vision_consumer_live.py +332 -0
  96. typevet/evaluation/psai_vision_consumer_live_identity.py +156 -0
  97. typevet/evaluation/psai_vision_consumer_live_receipt.py +132 -0
  98. typevet/evaluation/psai_vision_consumer_live_router.py +162 -0
  99. typevet/evaluation/psai_vision_consumer_offline.py +418 -0
  100. typevet/evaluation/psai_vision_consumer_outcomes.py +224 -0
  101. typevet/evaluation/psai_vision_consumer_protocol.py +69 -0
  102. typevet/evaluation/psai_vision_consumer_receipt.py +161 -0
  103. typevet/evaluation/psai_vision_consumer_receipt_structure.py +263 -0
  104. typevet/evaluation/psai_vision_probability_evidence.py +336 -0
  105. typevet/evaluation/runner/__init__.py +56 -0
  106. typevet/evaluation/runner/core.py +104 -0
  107. typevet/evaluation/runner/datasets.py +186 -0
  108. typevet/evaluation/runner/live_gate.py +149 -0
  109. typevet/evaluation/runner/report.py +116 -0
  110. typevet/evaluation/tpjep/__init__.py +81 -0
  111. typevet/evaluation/tpjep/loader.py +237 -0
  112. typevet/evaluation/tpjep/outcome.py +101 -0
  113. typevet/evaluation/tpjep/records.py +428 -0
  114. typevet/evaluation/tpjep/runner.py +325 -0
  115. typevet/ports/__init__.py +39 -0
  116. typevet/ports/async_generation.py +57 -0
  117. typevet/ports/framing.py +59 -0
  118. typevet/ports/generation.py +58 -0
  119. typevet/ports/judgment.py +71 -0
  120. typevet/ports/scoring.py +64 -0
  121. typevet/py.typed +0 -0
  122. typevet/runtime/__init__.py +50 -0
  123. typevet/runtime/categorical.py +196 -0
  124. typevet/runtime/judgment.py +17 -0
  125. typevet/runtime/llama_cpp_gemma_vision.py +28 -0
  126. typevet/runtime/scoring_prefix.py +14 -0
  127. typevet/runtime/vllm_judgment.py +28 -0
  128. typevet/testing/__init__.py +22 -0
  129. typevet/testing/fakes.py +152 -0
  130. typevet-0.1.0.dev1.dist-info/METADATA +125 -0
  131. typevet-0.1.0.dev1.dist-info/RECORD +133 -0
  132. typevet-0.1.0.dev1.dist-info/WHEEL +4 -0
  133. typevet-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,608 @@
1
+ """Backend selection and vLLM settings for the composition root.
2
+
3
+ ``TYPEVET_BACKEND`` selects ``llama_cpp`` (the default) or ``vllm``. The vLLM
4
+ branch reads ``TYPEVET_VLLM__*`` variables and builds one ``httpx.Client``
5
+ that carries the base URL, timeout and optional bearer key.
6
+ ``TYPEVET_VLLM__MAX_CONCURRENCY`` sets ``VllmSettings.max_concurrency``, the
7
+ POST limit for the ``AsyncVllmGenerationAdapter`` that
8
+ ``async_vllm_generation_adapter`` builds on an ``httpx.AsyncClient`` with the
9
+ same settings. The optional
10
+ ``TYPEVET_VLLM__USER_AGENT`` sets the ``User-Agent`` header; when it is unset
11
+ the client sends the httpx default. Outbound adapters never read the
12
+ environment.
13
+
14
+ The configured key never reaches the caller in clear text. ``VllmSettings``
15
+ leaves it out of ``repr``. The adapters that ``generation_adapter`` and
16
+ ``async_vllm_generation_adapter`` return catch each ``GenerationError`` from
17
+ ``generate``, and the port that ``open_judgment`` yields catches each one from
18
+ ``judge``. When the error text, an attribute such as a parsed payload, or the
19
+ cause chain holds the raw or JSON-escaped key, the error is raised again as
20
+ the same type with the key replaced by ``***`` and with no cause or context.
21
+ A server that echoes the ``Authorization`` header therefore cannot put the key
22
+ into a ``BackendHttpError``. Successful results are not changed.
23
+ The HTTP clients are built the same way with or without a key, so environment
24
+ proxy settings apply in both cases. Error messages name variables, never
25
+ values, and a parse failure raises with no cause or context.
26
+
27
+ Examples:
28
+ ```python
29
+ from typevet.adapters.inbound.backend_settings import (
30
+ async_vllm_generation_adapter,
31
+ generation_adapter,
32
+ open_judgment,
33
+ )
34
+
35
+ with generation_adapter() as port:
36
+ pass # port.generate(...)
37
+ with open_judgment() as session:
38
+ pass # session.port.judge(...)
39
+ async_port = async_vllm_generation_adapter() # await async_port.generate(...)
40
+ ```
41
+
42
+ See Also:
43
+ - [typevet.adapters.inbound.settings][]: ``TYPEVET_LLAMA__*`` settings
44
+ - [typevet.adapters.outbound.vllm_generation][]: vLLM generation adapter
45
+ - [typevet.adapters.outbound.vllm_generation_async][]: Async vLLM adapter
46
+ - [typevet.adapters.outbound.vllm_judgment_factory][]: vLLM judgment factory
47
+ - [typevet.adapters.diagnostics.redaction][]: ``REDACTED`` (``***``) mask
48
+ - docs/reference/configuration.md: Environment variable reference
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ import os
55
+ from collections.abc import Iterator, Mapping
56
+ from contextlib import contextmanager
57
+ from dataclasses import dataclass, field, replace
58
+ from typing import TYPE_CHECKING, Any, Literal
59
+
60
+ import httpx
61
+
62
+ from typevet.adapters.diagnostics.redaction import REDACTED
63
+ from typevet.adapters.inbound.settings import llama_cpp_adapter, load_llama_settings
64
+ from typevet.adapters.outbound.gemma_native_vision_factory import (
65
+ GemmaNativeVisionSession,
66
+ open_gemma_native_vision_judgment,
67
+ )
68
+ from typevet.adapters.outbound.llama_cpp import LlamaCppGenerationAdapter
69
+ from typevet.adapters.outbound.vllm_generation import VllmGenerationAdapter
70
+ from typevet.adapters.outbound.vllm_generation_async import AsyncVllmGenerationAdapter
71
+ from typevet.adapters.outbound.vllm_judgment_factory import (
72
+ VllmJudgmentSession,
73
+ open_vllm_judgment,
74
+ )
75
+ from typevet.domain.errors import GenerationError
76
+ from typevet.domain.models import GenerationRequest, GenerationResult
77
+
78
+ if TYPE_CHECKING:
79
+ from typevet.domain.judgment_questions import Question
80
+ from typevet.domain.judgment_response import JudgmentResponse
81
+ from typevet.domain.media import ImageInput
82
+ from typevet.ports.judgment import JudgmentPort
83
+
84
+ Backend = Literal["llama_cpp", "vllm"]
85
+
86
+ _DEFAULT_TIMEOUT = 300.0
87
+ _BACKENDS: tuple[Backend, ...] = ("llama_cpp", "vllm")
88
+ MASK = REDACTED
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class VllmSettings:
93
+ """Connection options for a vLLM OpenAI-compatible server.
94
+
95
+ Attributes:
96
+ base_url (str): Server root without a trailing slash.
97
+ model (str): Served model name.
98
+ timeout (float): HTTP request timeout in seconds.
99
+ api_key (str | None): Bearer key, or ``None``. Excluded from ``repr``.
100
+ max_concurrency (int): Maximum POSTs in flight for one async adapter.
101
+ user_agent (str | None): ``User-Agent`` header value, or ``None`` for
102
+ the httpx default.
103
+
104
+ Examples:
105
+ ```python
106
+ from typevet.adapters.inbound.backend_settings import VllmSettings
107
+
108
+ VllmSettings(base_url="http://127.0.0.1:8000", model="served-model")
109
+ ```
110
+ """
111
+
112
+ base_url: str
113
+ model: str
114
+ timeout: float = _DEFAULT_TIMEOUT
115
+ api_key: str | None = field(default=None, repr=False)
116
+ max_concurrency: int = 1
117
+ user_agent: str | None = None
118
+
119
+
120
+ def _masked(value: Any, needles: tuple[str, ...]) -> Any:
121
+ """Replace each key form in ``value`` with ``MASK``.
122
+
123
+ Strings are masked in place of each needle. Dicts, lists and tuples are
124
+ copied as plain containers with each key and item masked, so a parsed payload that holds the
125
+ key loses it. Other values are returned unchanged.
126
+
127
+ Args:
128
+ value: String, container or other attribute value.
129
+ needles: Key forms to replace.
130
+
131
+ Returns:
132
+ The masked value.
133
+ """
134
+ if isinstance(value, str):
135
+ for needle in needles:
136
+ value = value.replace(needle, MASK)
137
+ return value
138
+ if isinstance(value, dict):
139
+ return {_masked(k, needles): _masked(v, needles) for k, v in value.items()}
140
+ if isinstance(value, list):
141
+ return [_masked(item, needles) for item in value]
142
+ if isinstance(value, tuple):
143
+ return tuple(_masked(item, needles) for item in value)
144
+ return value
145
+
146
+
147
+ def _texts(value: Any) -> Iterator[str]:
148
+ """Yield every string inside ``value``, including dict keys.
149
+
150
+ Args:
151
+ value: String, container or other attribute value.
152
+
153
+ Yields:
154
+ Each string that ``_masked`` would mask.
155
+ """
156
+ if isinstance(value, str):
157
+ yield value
158
+ elif isinstance(value, dict):
159
+ for key, item in value.items():
160
+ yield from _texts(key)
161
+ yield from _texts(item)
162
+ elif isinstance(value, list | tuple):
163
+ for item in value:
164
+ yield from _texts(item)
165
+
166
+
167
+ def _chain_text(exc: BaseException) -> str:
168
+ parts: list[str] = []
169
+ seen: set[int] = set()
170
+ current: BaseException | None = exc
171
+ while current is not None and id(current) not in seen:
172
+ seen.add(id(current))
173
+ parts += [str(current), repr(current)]
174
+ parts += [text for v in vars(current).values() for text in _texts(v)]
175
+ current = current.__cause__ or current.__context__
176
+ return "\n".join(parts)
177
+
178
+
179
+ def _masked_error(exc: GenerationError, needles: tuple[str, ...]) -> GenerationError:
180
+ """Rebuild ``exc`` as the same type with the key masked and no chain.
181
+
182
+ ``BaseException.__new__`` makes the copy without calling ``__init__``, so
183
+ every ``GenerationError`` subclass keeps its type and attributes.
184
+
185
+ Args:
186
+ exc: Error raised by the vLLM adapter.
187
+ needles: Key forms to replace with ``MASK``.
188
+
189
+ Returns:
190
+ A new error whose arguments and attributes are masked, including
191
+ strings inside dict, list and tuple values such as a payload.
192
+ """
193
+ masked = type(exc).__new__(type(exc))
194
+ masked.args = _masked(exc.args, needles)
195
+ for name, value in vars(exc).items():
196
+ setattr(masked, name, _masked(value, needles))
197
+ return masked
198
+
199
+
200
+ def _key_needles(key: str | None) -> tuple[str, ...]:
201
+ if key is None:
202
+ return ()
203
+ return tuple(sorted({key, json.dumps(key)[1:-1]}, key=len, reverse=True))
204
+
205
+
206
+ def _masked_if_keyed(
207
+ exc: GenerationError, needles: tuple[str, ...]
208
+ ) -> GenerationError | None:
209
+ """Return a masked copy of ``exc`` when its chain holds a key form.
210
+
211
+ Args:
212
+ exc: Error raised by a vLLM adapter.
213
+ needles: Key forms from ``_key_needles``.
214
+
215
+ Returns:
216
+ The masked copy, or ``None`` when no key form is present.
217
+ """
218
+ chain = _chain_text(exc)
219
+ if not any(needle in chain for needle in needles):
220
+ return None
221
+ return _masked_error(exc, needles)
222
+
223
+
224
+ class _KeyMaskingJudgmentPort:
225
+ """``JudgmentPort`` wrapper that masks the configured key in errors.
226
+
227
+ Attributes:
228
+ _port (JudgmentPort): Wrapped vLLM judgment port.
229
+ _needles (tuple[str, ...]): Raw and JSON-escaped key, or empty.
230
+
231
+ Examples:
232
+ ```python
233
+ _KeyMaskingJudgmentPort(session.port, _key_needles("sk-..."))
234
+ ```
235
+ """
236
+
237
+ def __init__(self, port: JudgmentPort, needles: tuple[str, ...]) -> None:
238
+ self._port = port
239
+ self._needles = needles
240
+
241
+ def judge(
242
+ self,
243
+ state: str | dict[str, Any] | list[Any],
244
+ questions: Mapping[str, Question | Mapping[str, Any]],
245
+ model: str,
246
+ *,
247
+ media: tuple[ImageInput, ...] | None = None,
248
+ ) -> JudgmentResponse:
249
+ """Judge, and mask the configured key in any raised error.
250
+
251
+ Args:
252
+ state: Content under evaluation.
253
+ questions: Question names to typed or raw questions.
254
+ model: Served model name.
255
+ media: Images to condition every scored field on.
256
+
257
+ Returns:
258
+ The response from the wrapped port, unchanged.
259
+
260
+ Raises:
261
+ GenerationError: The port error, masked when it holds the key.
262
+ """
263
+ try:
264
+ return self._port.judge(state, questions, model, media=media)
265
+ except GenerationError as exc:
266
+ masked = _masked_if_keyed(exc, self._needles)
267
+ if masked is None:
268
+ raise
269
+ raise masked
270
+
271
+
272
+ class _ClientOwningVllmAdapter(VllmGenerationAdapter):
273
+ """vLLM adapter that owns its client and masks the key in errors.
274
+
275
+ Attributes:
276
+ _settings_client (httpx.Client): Client from ``vllm_http_client``.
277
+ _needles (tuple[str, ...]): Raw and JSON-escaped key, or empty.
278
+
279
+ Examples:
280
+ ```python
281
+ settings = VllmSettings(base_url="http://127.0.0.1:8000", model="m")
282
+ _ClientOwningVllmAdapter(settings, vllm_http_client(settings))
283
+ ```
284
+ """
285
+
286
+ def __init__(self, settings: VllmSettings, client: httpx.Client) -> None:
287
+ super().__init__(settings.base_url, timeout=settings.timeout, client=client)
288
+ self._settings_client = client
289
+ self._needles = _key_needles(settings.api_key)
290
+
291
+ def generate(self, request: GenerationRequest) -> GenerationResult:
292
+ """Generate, and mask the configured key in any raised error.
293
+
294
+ A successful result is returned unchanged. ``_masked_if_keyed``
295
+ checks each error: one whose text or cause chain holds the key is
296
+ raised again as the same type with the key replaced by ``MASK`` and
297
+ with no cause or context.
298
+
299
+ Args:
300
+ request: Prompt, schema and served model name.
301
+
302
+ Returns:
303
+ The result from ``VllmGenerationAdapter.generate``.
304
+
305
+ Raises:
306
+ GenerationError: The adapter error, masked when it holds the key.
307
+ """
308
+ try:
309
+ return super().generate(request)
310
+ except GenerationError as exc:
311
+ masked = _masked_if_keyed(exc, self._needles)
312
+ if masked is None:
313
+ raise
314
+ raise masked
315
+
316
+ def close(self) -> None:
317
+ """Close the composition-root client."""
318
+ super().close()
319
+ self._settings_client.close()
320
+
321
+
322
+ class _ClientOwningAsyncVllmAdapter(AsyncVllmGenerationAdapter):
323
+ """Async vLLM adapter that owns its client and masks the key in errors.
324
+
325
+ Attributes:
326
+ _settings_client (httpx.AsyncClient): Client with the same base URL,
327
+ timeout and headers as ``vllm_http_client``.
328
+ _needles (tuple[str, ...]): Raw and JSON-escaped key, or empty.
329
+
330
+ Examples:
331
+ ```python
332
+ settings = VllmSettings(base_url="http://127.0.0.1:8000", model="m")
333
+ _ClientOwningAsyncVllmAdapter(settings, httpx.AsyncClient())
334
+ ```
335
+ """
336
+
337
+ def __init__(self, settings: VllmSettings, client: httpx.AsyncClient) -> None:
338
+ super().__init__(
339
+ settings.base_url,
340
+ timeout=settings.timeout,
341
+ client=client,
342
+ max_concurrency=settings.max_concurrency,
343
+ )
344
+ self._settings_client = client
345
+ self._needles = _key_needles(settings.api_key)
346
+ self._binds_loop = True
347
+
348
+ async def generate(self, request: GenerationRequest) -> GenerationResult:
349
+ """Generate, and mask the configured key in any raised error.
350
+
351
+ Masking follows ``_ClientOwningVllmAdapter.generate``: an error whose
352
+ text, attributes or cause chain holds the key is raised again as the
353
+ same type with the key replaced by ``MASK`` and with no cause or
354
+ context. A successful result is returned unchanged.
355
+
356
+ Args:
357
+ request: Prompt, schema and served model name.
358
+
359
+ Returns:
360
+ The result from ``AsyncVllmGenerationAdapter.generate``.
361
+
362
+ Raises:
363
+ GenerationError: The adapter error, masked when it holds the key.
364
+ RuntimeError: When the call runs on a different event loop from
365
+ the first call. It is not masked, because it holds no key.
366
+ """
367
+ try:
368
+ return await super().generate(request)
369
+ except GenerationError as exc:
370
+ masked = _masked_if_keyed(exc, self._needles)
371
+ if masked is None:
372
+ raise
373
+ raise masked
374
+
375
+ async def close(self) -> None:
376
+ """Close the composition-root client."""
377
+ await super().close()
378
+ await self._settings_client.aclose()
379
+
380
+
381
+ def _read_timeout(source: Mapping[str, str], name: str) -> float:
382
+ raw = source.get(name, "").strip()
383
+ if not raw:
384
+ return _DEFAULT_TIMEOUT
385
+ try:
386
+ timeout = float(raw)
387
+ except ValueError:
388
+ timeout = 0.0
389
+ if timeout <= 0:
390
+ msg = f"{name} must be a positive number of seconds"
391
+ raise ValueError(msg) from None
392
+ return timeout
393
+
394
+
395
+ def _read_max_concurrency(source: Mapping[str, str], name: str) -> int:
396
+ raw = source.get(name, "").strip()
397
+ if not raw:
398
+ return 1
399
+ try:
400
+ limit = int(raw)
401
+ except ValueError:
402
+ limit = 0
403
+ if limit < 1:
404
+ msg = f"{name} must be a positive integer"
405
+ raise ValueError(msg) from None
406
+ return limit
407
+
408
+
409
+ def _read_required(source: Mapping[str, str], name: str) -> str:
410
+ value = source.get(name, "").strip()
411
+ if not value:
412
+ msg = f"{name} is required when TYPEVET_BACKEND is vllm"
413
+ raise ValueError(msg)
414
+ return value
415
+
416
+
417
+ def load_backend(environ: Mapping[str, str] | None = None) -> Backend:
418
+ """Read ``TYPEVET_BACKEND``.
419
+
420
+ Args:
421
+ environ: Mapping to read. Defaults to ``os.environ``.
422
+
423
+ Returns:
424
+ ``"llama_cpp"`` when unset or empty, else the named backend.
425
+
426
+ Raises:
427
+ ValueError: When the value is not ``llama_cpp`` or ``vllm``.
428
+ """
429
+ source = os.environ if environ is None else environ
430
+ raw = source.get("TYPEVET_BACKEND", "").strip()
431
+ if not raw:
432
+ return "llama_cpp"
433
+ for backend in _BACKENDS:
434
+ if raw == backend:
435
+ return backend
436
+ msg = "TYPEVET_BACKEND must be llama_cpp or vllm"
437
+ raise ValueError(msg)
438
+
439
+
440
+ def load_vllm_settings(environ: Mapping[str, str] | None = None) -> VllmSettings:
441
+ """Read ``TYPEVET_VLLM__*`` variables.
442
+
443
+ Args:
444
+ environ: Mapping to read. Defaults to ``os.environ``.
445
+
446
+ Returns:
447
+ Frozen settings. An empty ``TYPEVET_VLLM__API_KEY`` or
448
+ ``TYPEVET_VLLM__USER_AGENT`` gives ``None``.
449
+
450
+ Raises:
451
+ ValueError: When ``TYPEVET_VLLM__BASE_URL`` or ``TYPEVET_VLLM__MODEL``
452
+ is missing, ``TYPEVET_VLLM__TIMEOUT`` is not a positive number,
453
+ ``TYPEVET_VLLM__MAX_CONCURRENCY`` is not a positive integer, or
454
+ ``TYPEVET_VLLM__API_KEY`` holds a non-ASCII character.
455
+ """
456
+ source = os.environ if environ is None else environ
457
+ api_key = source.get("TYPEVET_VLLM__API_KEY", "").strip()
458
+ if not api_key.isascii():
459
+ msg = "TYPEVET_VLLM__API_KEY must contain only ASCII characters"
460
+ raise ValueError(msg)
461
+ return VllmSettings(
462
+ base_url=_read_required(source, "TYPEVET_VLLM__BASE_URL").rstrip("/"),
463
+ model=_read_required(source, "TYPEVET_VLLM__MODEL"),
464
+ timeout=_read_timeout(source, "TYPEVET_VLLM__TIMEOUT"),
465
+ api_key=api_key or None,
466
+ max_concurrency=_read_max_concurrency(source, "TYPEVET_VLLM__MAX_CONCURRENCY"),
467
+ user_agent=source.get("TYPEVET_VLLM__USER_AGENT", "").strip() or None,
468
+ )
469
+
470
+
471
+ def vllm_http_client(
472
+ settings: VllmSettings,
473
+ *,
474
+ transport: httpx.BaseTransport | None = None,
475
+ ) -> httpx.Client:
476
+ """Build the shared vLLM HTTP client.
477
+
478
+ Args:
479
+ settings: vLLM connection settings.
480
+ transport: Optional transport, for example ``httpx.MockTransport``.
481
+
482
+ Returns:
483
+ A client with ``base_url`` and ``timeout`` set. When a key is set, the
484
+ client also sends ``Authorization: Bearer <key>``. When a user agent
485
+ is set, the client sends it as ``User-Agent``. Nothing else changes,
486
+ so environment proxy settings apply with or without a key. The async
487
+ client from ``async_vllm_generation_adapter`` gets the same headers.
488
+ """
489
+ return httpx.Client(
490
+ base_url=settings.base_url,
491
+ timeout=settings.timeout,
492
+ headers=_vllm_headers(settings),
493
+ transport=transport,
494
+ )
495
+
496
+
497
+ def _vllm_headers(settings: VllmSettings) -> dict[str, str]:
498
+ headers: dict[str, str] = {}
499
+ if settings.api_key is not None:
500
+ headers["Authorization"] = f"Bearer {settings.api_key}"
501
+ if settings.user_agent is not None:
502
+ headers["User-Agent"] = settings.user_agent
503
+ return headers
504
+
505
+
506
+ def generation_adapter(
507
+ environ: Mapping[str, str] | None = None,
508
+ *,
509
+ transport: httpx.BaseTransport | None = None,
510
+ ) -> LlamaCppGenerationAdapter | VllmGenerationAdapter:
511
+ """Build the generation adapter that ``TYPEVET_BACKEND`` selects.
512
+
513
+ Args:
514
+ environ: Mapping to read. Defaults to ``os.environ``.
515
+ transport: Optional transport for the vLLM client. The llama.cpp
516
+ branch ignores it.
517
+
518
+ Returns:
519
+ A llama.cpp adapter from ``load_llama_settings``, or a vLLM adapter on
520
+ the ``vllm_http_client``. Closing the vLLM adapter closes its client,
521
+ and its errors never show the configured key.
522
+
523
+ Raises:
524
+ ValueError: When a backend or vLLM variable is invalid.
525
+ """
526
+ source = os.environ if environ is None else environ
527
+ if load_backend(source) == "llama_cpp":
528
+ return llama_cpp_adapter(load_llama_settings(source))
529
+ settings = load_vllm_settings(source)
530
+ client = vllm_http_client(settings, transport=transport)
531
+ return _ClientOwningVllmAdapter(settings, client)
532
+
533
+
534
+ def async_vllm_generation_adapter(
535
+ environ: Mapping[str, str] | None = None,
536
+ *,
537
+ transport: httpx.AsyncBaseTransport | None = None,
538
+ ) -> AsyncVllmGenerationAdapter:
539
+ """Build the async vLLM generation adapter from ``TYPEVET_VLLM__*``.
540
+
541
+ The adapter reads the vLLM settings whatever ``TYPEVET_BACKEND`` says. Its
542
+ ``httpx.AsyncClient`` has the same base URL, timeout, headers and proxy
543
+ handling as ``vllm_http_client``, and ``TYPEVET_VLLM__MAX_CONCURRENCY``
544
+ sets its POST limit. That client binds to the first event loop that uses
545
+ it, so build one adapter per event loop, for example per ``asyncio.run``.
546
+ The adapter records the first running loop that calls ``generate``, and a
547
+ call on a different loop raises ``RuntimeError`` before any request.
548
+
549
+ Args:
550
+ environ: Mapping to read. Defaults to ``os.environ``.
551
+ transport: Optional async transport, for example
552
+ ``httpx.MockTransport``.
553
+
554
+ Returns:
555
+ An adapter that closes its client on ``close`` and whose errors never
556
+ show the configured key.
557
+
558
+ Raises:
559
+ ValueError: When a vLLM variable is invalid.
560
+ """
561
+ settings = load_vllm_settings(environ)
562
+ client = httpx.AsyncClient(
563
+ base_url=settings.base_url,
564
+ timeout=settings.timeout,
565
+ headers=_vllm_headers(settings),
566
+ transport=transport,
567
+ )
568
+ return _ClientOwningAsyncVllmAdapter(settings, client)
569
+
570
+
571
+ @contextmanager
572
+ def open_judgment(
573
+ environ: Mapping[str, str] | None = None,
574
+ *,
575
+ transport: httpx.BaseTransport | None = None,
576
+ ) -> Iterator[GemmaNativeVisionSession | VllmJudgmentSession]:
577
+ """Open the judgment session that ``TYPEVET_BACKEND`` selects.
578
+
579
+ Args:
580
+ environ: Mapping to read. Defaults to ``os.environ``.
581
+ transport: Optional transport for the vLLM client. The llama.cpp
582
+ branch ignores it.
583
+
584
+ Yields:
585
+ A llama.cpp session from ``open_gemma_native_vision_judgment`` with
586
+ ``load_llama_settings``, or a vLLM session from ``open_vllm_judgment``
587
+ on the ``vllm_http_client``. The vLLM client closes on exit, and
588
+ judgment errors never show the configured key.
589
+
590
+ Raises:
591
+ ValueError: When a backend or vLLM variable is invalid.
592
+ """
593
+ source = os.environ if environ is None else environ
594
+ if load_backend(source) == "llama_cpp":
595
+ with open_gemma_native_vision_judgment(
596
+ settings=load_llama_settings(source)
597
+ ) as llama_session:
598
+ yield llama_session
599
+ return
600
+ settings = load_vllm_settings(source)
601
+ with (
602
+ vllm_http_client(settings, transport=transport) as client,
603
+ open_vllm_judgment(
604
+ client=client, model=settings.model, base_url=settings.base_url
605
+ ) as session,
606
+ ):
607
+ masked = _KeyMaskingJudgmentPort(session.port, _key_needles(settings.api_key))
608
+ yield replace(session, port=masked)