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.
- typevet/__init__.py +67 -0
- typevet/_version.py +43 -0
- typevet/adapters/__init__.py +16 -0
- typevet/adapters/diagnostics/__init__.py +59 -0
- typevet/adapters/diagnostics/fields.py +108 -0
- typevet/adapters/diagnostics/generation_events.py +75 -0
- typevet/adapters/diagnostics/http_events.py +91 -0
- typevet/adapters/diagnostics/logs.py +116 -0
- typevet/adapters/diagnostics/redaction.py +94 -0
- typevet/adapters/diagnostics/settings.py +77 -0
- typevet/adapters/inbound/__init__.py +65 -0
- typevet/adapters/inbound/api.py +61 -0
- typevet/adapters/inbound/backend_settings.py +608 -0
- typevet/adapters/inbound/cord_semantic_acceptance_cli.py +135 -0
- typevet/adapters/inbound/helpers.py +67 -0
- typevet/adapters/inbound/settings.py +154 -0
- typevet/adapters/outbound/__init__.py +62 -0
- typevet/adapters/outbound/async_fake.py +123 -0
- typevet/adapters/outbound/async_llama_cpp.py +168 -0
- typevet/adapters/outbound/chat_completion.py +148 -0
- typevet/adapters/outbound/fake.py +119 -0
- typevet/adapters/outbound/gemma/__init__.py +84 -0
- typevet/adapters/outbound/gemma/answer_binding.py +290 -0
- typevet/adapters/outbound/gemma/scoring_prefix.py +102 -0
- typevet/adapters/outbound/gemma/served_template.py +131 -0
- typevet/adapters/outbound/gemma_native_vision_factory.py +286 -0
- typevet/adapters/outbound/generation_finite.py +50 -0
- typevet/adapters/outbound/http_errors.py +38 -0
- typevet/adapters/outbound/judgment_scoring.py +471 -0
- typevet/adapters/outbound/llama_cpp.py +187 -0
- typevet/adapters/outbound/llama_cpp_http.py +103 -0
- typevet/adapters/outbound/llama_cpp_multimodal.py +145 -0
- typevet/adapters/outbound/llama_cpp_scoring.py +333 -0
- typevet/adapters/outbound/vllm_content.py +63 -0
- typevet/adapters/outbound/vllm_generation.py +163 -0
- typevet/adapters/outbound/vllm_generation_async.py +257 -0
- typevet/adapters/outbound/vllm_http.py +121 -0
- typevet/adapters/outbound/vllm_judgment_factory.py +210 -0
- typevet/adapters/outbound/vllm_scoring.py +372 -0
- typevet/domain/__init__.py +193 -0
- typevet/domain/candidate_scoring_request.py +134 -0
- typevet/domain/candidate_scoring_response.py +96 -0
- typevet/domain/candidate_scoring_validate.py +197 -0
- typevet/domain/decision_compile.py +368 -0
- typevet/domain/decision_execute.py +229 -0
- typevet/domain/decisions.py +129 -0
- typevet/domain/errors.py +235 -0
- typevet/domain/field_instructions.py +134 -0
- typevet/domain/judgment_answers.py +198 -0
- typevet/domain/judgment_normalize.py +243 -0
- typevet/domain/judgment_questions.py +187 -0
- typevet/domain/judgment_response.py +102 -0
- typevet/domain/media.py +101 -0
- typevet/domain/models.py +115 -0
- typevet/domain/question_schema.py +254 -0
- typevet/domain/scoring_stage.py +34 -0
- typevet/evaluation/__init__.py +19 -0
- typevet/evaluation/consumer_http_accounting.py +135 -0
- typevet/evaluation/cord_expense_call_accounting.py +108 -0
- typevet/evaluation/cord_expense_live_harness.py +75 -0
- typevet/evaluation/cord_expense_receipt_requirement.py +212 -0
- typevet/evaluation/cord_expense_smoke.py +328 -0
- typevet/evaluation/cord_semantic_acceptance.py +463 -0
- typevet/evaluation/cord_semantic_acceptance_report.py +90 -0
- typevet/evaluation/cord_semantic_metrics.py +57 -0
- typevet/evaluation/datasets/__init__.py +30 -0
- typevet/evaluation/datasets/banking77.py +270 -0
- typevet/evaluation/datasets/boolq.py +550 -0
- typevet/evaluation/datasets/boolq_download.py +132 -0
- typevet/evaluation/datasets/civil_comments.py +450 -0
- typevet/evaluation/datasets/clinc.py +125 -0
- typevet/evaluation/datasets/clinc_domains.json +172 -0
- typevet/evaluation/datasets/clinc_download.py +103 -0
- typevet/evaluation/datasets/clinc_plus_intent_names.json +153 -0
- typevet/evaluation/datasets/clinc_rows.py +306 -0
- typevet/evaluation/datasets/clinc_shard.py +156 -0
- typevet/evaluation/datasets/cord_expense.py +381 -0
- typevet/evaluation/datasets/difraud.py +256 -0
- typevet/evaluation/datasets/go_emotions.py +368 -0
- typevet/evaluation/datasets/go_emotions_download.py +154 -0
- typevet/evaluation/datasets/hyperpartisan.py +562 -0
- typevet/evaluation/datasets/partner_guard.py +170 -0
- typevet/evaluation/datasets/psai.py +415 -0
- typevet/evaluation/datasets/psai_download.py +167 -0
- typevet/evaluation/datasets/psai_evidence_pilot.py +570 -0
- typevet/evaluation/datasets/psai_schema.py +157 -0
- typevet/evaluation/datasets/psai_stream.py +166 -0
- typevet/evaluation/datasets/psai_vision.py +534 -0
- typevet/evaluation/datasets/psai_vision_controls.py +502 -0
- typevet/evaluation/datasets/pubmedqa.py +420 -0
- typevet/evaluation/experiment_identity.py +677 -0
- typevet/evaluation/psai_vision_consumer_accounting.py +190 -0
- typevet/evaluation/psai_vision_consumer_dispatch.py +216 -0
- typevet/evaluation/psai_vision_consumer_harness.py +342 -0
- typevet/evaluation/psai_vision_consumer_live.py +332 -0
- typevet/evaluation/psai_vision_consumer_live_identity.py +156 -0
- typevet/evaluation/psai_vision_consumer_live_receipt.py +132 -0
- typevet/evaluation/psai_vision_consumer_live_router.py +162 -0
- typevet/evaluation/psai_vision_consumer_offline.py +418 -0
- typevet/evaluation/psai_vision_consumer_outcomes.py +224 -0
- typevet/evaluation/psai_vision_consumer_protocol.py +69 -0
- typevet/evaluation/psai_vision_consumer_receipt.py +161 -0
- typevet/evaluation/psai_vision_consumer_receipt_structure.py +263 -0
- typevet/evaluation/psai_vision_probability_evidence.py +336 -0
- typevet/evaluation/runner/__init__.py +56 -0
- typevet/evaluation/runner/core.py +104 -0
- typevet/evaluation/runner/datasets.py +186 -0
- typevet/evaluation/runner/live_gate.py +149 -0
- typevet/evaluation/runner/report.py +116 -0
- typevet/evaluation/tpjep/__init__.py +81 -0
- typevet/evaluation/tpjep/loader.py +237 -0
- typevet/evaluation/tpjep/outcome.py +101 -0
- typevet/evaluation/tpjep/records.py +428 -0
- typevet/evaluation/tpjep/runner.py +325 -0
- typevet/ports/__init__.py +39 -0
- typevet/ports/async_generation.py +57 -0
- typevet/ports/framing.py +59 -0
- typevet/ports/generation.py +58 -0
- typevet/ports/judgment.py +71 -0
- typevet/ports/scoring.py +64 -0
- typevet/py.typed +0 -0
- typevet/runtime/__init__.py +50 -0
- typevet/runtime/categorical.py +196 -0
- typevet/runtime/judgment.py +17 -0
- typevet/runtime/llama_cpp_gemma_vision.py +28 -0
- typevet/runtime/scoring_prefix.py +14 -0
- typevet/runtime/vllm_judgment.py +28 -0
- typevet/testing/__init__.py +22 -0
- typevet/testing/fakes.py +152 -0
- typevet-0.1.0.dev1.dist-info/METADATA +125 -0
- typevet-0.1.0.dev1.dist-info/RECORD +133 -0
- typevet-0.1.0.dev1.dist-info/WHEEL +4 -0
- 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)
|