verifiers 0.3.2.dev131__py3-none-any.whl → 0.3.2.dev133__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.
- verifiers/v1/dialects/anthropic.py +8 -0
- verifiers/v1/dialects/base.py +12 -0
- verifiers/v1/dialects/responses.py +41 -0
- verifiers/v1/harnesses/utils/core.py +3 -1
- verifiers/v1/interception/server.py +180 -371
- {verifiers-0.3.2.dev131.dist-info → verifiers-0.3.2.dev133.dist-info}/METADATA +1 -1
- {verifiers-0.3.2.dev131.dist-info → verifiers-0.3.2.dev133.dist-info}/RECORD +10 -10
- {verifiers-0.3.2.dev131.dist-info → verifiers-0.3.2.dev133.dist-info}/WHEEL +0 -0
- {verifiers-0.3.2.dev131.dist-info → verifiers-0.3.2.dev133.dist-info}/entry_points.txt +0 -0
- {verifiers-0.3.2.dev131.dist-info → verifiers-0.3.2.dev133.dist-info}/licenses/LICENSE +0 -0
|
@@ -512,6 +512,14 @@ class AnthropicDialect(Dialect[AnthropicMessage]):
|
|
|
512
512
|
"error": {"type": "invalid_request_error", "message": message},
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
def stream_keepalive(self, first: bool) -> bytes:
|
|
516
|
+
# Anthropic's own keepalive; its SDKs skip it anywhere in the stream.
|
|
517
|
+
return b'event: ping\ndata: {"type": "ping"}\n\n'
|
|
518
|
+
|
|
519
|
+
def stream_error(self, error: dict) -> bytes:
|
|
520
|
+
# The Anthropic SDKs raise only on a named `error` event.
|
|
521
|
+
return b"event: error\ndata: " + json.dumps(error).encode() + b"\n\n"
|
|
522
|
+
|
|
515
523
|
def parse_request(self, body: RawRequest) -> Request:
|
|
516
524
|
native_tools = body.get("tools") or []
|
|
517
525
|
if not isinstance(native_tools, list) or any(
|
verifiers/v1/dialects/base.py
CHANGED
|
@@ -317,6 +317,18 @@ class Dialect(ABC, Generic[RespT]):
|
|
|
317
317
|
"""An error payload in this format's error shape (OpenAI by default)."""
|
|
318
318
|
return {"error": {"message": message, "type": "invalid_request_error"}}
|
|
319
319
|
|
|
320
|
+
def stream_keepalive(self, first: bool) -> bytes:
|
|
321
|
+
"""A keepalive for a committed SSE stream whose turn is still being produced (`first`
|
|
322
|
+
on the stream's first one). A comment line by default: these clients count any bytes
|
|
323
|
+
as activity. A dialect whose clients only count events sends a no-op event instead."""
|
|
324
|
+
# Don't terminate an empty event; some SSE clients try to JSON-decode it.
|
|
325
|
+
return b": keepalive\n"
|
|
326
|
+
|
|
327
|
+
def stream_error(self, error: dict) -> bytes:
|
|
328
|
+
"""An `error_body` as an SSE event, for a failure after the stream is committed.
|
|
329
|
+
OpenAI SDKs raise on any event carrying `error`."""
|
|
330
|
+
return b"data: " + json.dumps(error).encode() + b"\n\n"
|
|
331
|
+
|
|
320
332
|
@abstractmethod
|
|
321
333
|
def mediate_external_capabilities(
|
|
322
334
|
self, body: RawRequest, policy: NetworkPolicyConfig
|
|
@@ -49,6 +49,20 @@ from verifiers.v1.types import (
|
|
|
49
49
|
UserMessage,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
+
# The placeholder response a committed stream's keepalives carry until its turn is ready:
|
|
53
|
+
# schema-valid, so strictly validating clients accept the events that carry it.
|
|
54
|
+
_KEEPALIVE_RESPONSE = {
|
|
55
|
+
"id": "resp_keepalive",
|
|
56
|
+
"object": "response",
|
|
57
|
+
"created_at": 0,
|
|
58
|
+
"model": "",
|
|
59
|
+
"status": "in_progress",
|
|
60
|
+
"output": [],
|
|
61
|
+
"parallel_tool_calls": True,
|
|
62
|
+
"tool_choice": "auto",
|
|
63
|
+
"tools": [],
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
FINAL_EVENTS = ("response.completed", "response.incomplete", "response.failed")
|
|
53
67
|
# Byte markers for the terminal event types above, in both compact and spaced JSON, so the
|
|
54
68
|
# interception server can cheaply spot the turn-ending event without parsing each delta.
|
|
@@ -648,6 +662,33 @@ class ResponsesDialect(Dialect[OpenAIResponse]):
|
|
|
648
662
|
if "output_text" in raw:
|
|
649
663
|
raw["output_text"] = text
|
|
650
664
|
|
|
665
|
+
def stream_keepalive(self, first: bool) -> bytes:
|
|
666
|
+
# Codex's idle timer resets only on data events (it drops comments), and the OpenAI
|
|
667
|
+
# SDK's stream helper rejects any event before `response.created`: open with a
|
|
668
|
+
# placeholder `response.created`, then repeat `response.in_progress`. The turn's own
|
|
669
|
+
# `response.created` follows and supersedes it; clients take the turn's id and final
|
|
670
|
+
# state from its own events, and its sequence numbers restart at 0.
|
|
671
|
+
kind = "response.created" if first else "response.in_progress"
|
|
672
|
+
payload = {"type": kind, "sequence_number": 0, "response": _KEEPALIVE_RESPONSE}
|
|
673
|
+
return f"event: {kind}\ndata: {json.dumps(payload)}\n\n".encode()
|
|
674
|
+
|
|
675
|
+
def stream_error(self, error: dict) -> bytes:
|
|
676
|
+
# `response.failed` is what Responses clients (codex) act on; the `error` key is what
|
|
677
|
+
# the OpenAI SDK raises on.
|
|
678
|
+
message = error["error"]["message"]
|
|
679
|
+
failed = {
|
|
680
|
+
**_KEEPALIVE_RESPONSE,
|
|
681
|
+
"status": "failed",
|
|
682
|
+
"error": {"code": "server_error", "message": message},
|
|
683
|
+
}
|
|
684
|
+
payload = {
|
|
685
|
+
"type": "response.failed",
|
|
686
|
+
"sequence_number": 0,
|
|
687
|
+
"response": failed,
|
|
688
|
+
**error,
|
|
689
|
+
}
|
|
690
|
+
return f"event: response.failed\ndata: {json.dumps(payload)}\n\n".encode()
|
|
691
|
+
|
|
651
692
|
def stream_events(self, raw: dict) -> list[bytes]:
|
|
652
693
|
item = raw["output"][0]
|
|
653
694
|
part = item["content"][0]
|
|
@@ -240,7 +240,9 @@ async def chat(
|
|
|
240
240
|
*,
|
|
241
241
|
tool_choice: str | None = None,
|
|
242
242
|
):
|
|
243
|
-
kwargs = {"model": model, "messages": messages
|
|
243
|
+
kwargs = {"model": model, "messages": messages}
|
|
244
|
+
if tools:
|
|
245
|
+
kwargs["tools"] = tools
|
|
244
246
|
if tools and tool_choice is not None:
|
|
245
247
|
kwargs["tool_choice"] = tool_choice
|
|
246
248
|
raw_stream = await client.chat.completions.create(
|
|
@@ -27,10 +27,9 @@ import logging
|
|
|
27
27
|
import secrets
|
|
28
28
|
import time
|
|
29
29
|
import traceback
|
|
30
|
-
from collections.abc import AsyncIterator, Collection, Mapping
|
|
30
|
+
from collections.abc import AsyncIterator, Awaitable, Collection, Mapping
|
|
31
31
|
from contextlib import asynccontextmanager
|
|
32
32
|
from dataclasses import dataclass
|
|
33
|
-
from tempfile import SpooledTemporaryFile
|
|
34
33
|
from typing import Literal
|
|
35
34
|
|
|
36
35
|
import httpx
|
|
@@ -41,6 +40,7 @@ from pydantic_core import PydanticSerializationError, from_json, to_json
|
|
|
41
40
|
from verifiers.v1 import graph
|
|
42
41
|
from verifiers.v1.clients import Client, resolve_client
|
|
43
42
|
from verifiers.v1.clients.base import join_url
|
|
43
|
+
from verifiers.v1.clients.client import RelayReply
|
|
44
44
|
from verifiers.v1.configs.client import (
|
|
45
45
|
BaseClientConfig,
|
|
46
46
|
TrainClientConfig,
|
|
@@ -66,7 +66,7 @@ from verifiers.v1.interception.tunnel import (
|
|
|
66
66
|
from verifiers.v1.semantic import ACPInfo, extract_acp_info
|
|
67
67
|
from verifiers.v1.session import IdempotentRequest, ReplayResponse, RolloutSession
|
|
68
68
|
from verifiers.v1.trace import Error, ModelCall, PolicyEvent, TimeSpan
|
|
69
|
-
from verifiers.v1.types import FinishReason,
|
|
69
|
+
from verifiers.v1.types import FinishReason, Response, Usage
|
|
70
70
|
|
|
71
71
|
logger = logging.getLogger(__name__)
|
|
72
72
|
|
|
@@ -77,8 +77,10 @@ logger = logging.getLogger(__name__)
|
|
|
77
77
|
# context window are the real limits, this is just a host-OOM backstop.
|
|
78
78
|
MAX_REQUEST_BODY = 1024**3 # 1 GiB (aiohttp's default is 1 MiB)
|
|
79
79
|
KEEPALIVE_INTERVAL_SECONDS = 3
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
# A streamed turn commits its SSE stream only after this long: a result within it keeps
|
|
81
|
+
# its HTTP status (so the harness SDK can retry 5xx/429), and a longer one is kept alive
|
|
82
|
+
# well inside the tunnel's response-header timeout.
|
|
83
|
+
KEEPALIVE_GRACE_SECONDS = 60
|
|
82
84
|
# blake2b saturates ~1.7 GB/s, so a body up to this size hashes inline in well under a
|
|
83
85
|
# millisecond; a larger one (bodies may reach `MAX_REQUEST_BODY`) is hashed off the event
|
|
84
86
|
# loop instead — see `_request_digest`.
|
|
@@ -204,18 +206,108 @@ def _prune_idempotent_requests(session: RolloutSession, now: float) -> None:
|
|
|
204
206
|
session.idempotent_requests.pop(key)
|
|
205
207
|
|
|
206
208
|
|
|
207
|
-
async def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
209
|
+
async def _collect_stream(
|
|
210
|
+
dialect: Dialect, reply: RelayReply
|
|
211
|
+
) -> tuple[Response, bytes]:
|
|
212
|
+
"""Read a relayed provider stream whole: the assembled response and the events to serve
|
|
213
|
+
for it. Comment-only events (the provider's own keepalives) are dropped; the served
|
|
214
|
+
stream sends its own."""
|
|
215
|
+
events = bytearray()
|
|
216
|
+
parser = dialect.stream_parser()
|
|
217
|
+
saw_terminal = False
|
|
212
218
|
try:
|
|
213
|
-
async for chunk in chunks:
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
async for chunk in reply.chunks:
|
|
220
|
+
if not any(line.startswith(b"data:") for line in chunk.splitlines()):
|
|
221
|
+
continue
|
|
222
|
+
events += chunk
|
|
223
|
+
saw_terminal |= dialect.is_terminal_event(chunk)
|
|
224
|
+
if parser.on_done is not None and is_sse_done_event(chunk):
|
|
225
|
+
parser.on_done()
|
|
226
|
+
parser.feed(chunk)
|
|
227
|
+
if not saw_terminal:
|
|
228
|
+
raise ProviderError("upstream stream ended before its terminal event")
|
|
229
|
+
return parser.finish(), bytes(events)
|
|
230
|
+
except RolloutError:
|
|
231
|
+
raise
|
|
232
|
+
except Exception as e: # a malformed provider stream
|
|
233
|
+
raise ProviderError(str(e)) from e
|
|
216
234
|
finally:
|
|
217
|
-
await
|
|
218
|
-
|
|
235
|
+
await reply.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def _buffered_stream(
|
|
239
|
+
request: web.Request,
|
|
240
|
+
dialect: Dialect,
|
|
241
|
+
pending: Awaitable[web.Response],
|
|
242
|
+
trace_id: str,
|
|
243
|
+
) -> web.StreamResponse:
|
|
244
|
+
"""Serve a turn to an SSE client once it is committed, keeping the connection alive
|
|
245
|
+
while it is produced. A result within the grace period is served as is; after it the
|
|
246
|
+
stream is committed and sent the dialect's keepalives, so neither a proxy's timeout
|
|
247
|
+
nor a client's own idle timeout cuts a long turn. Once committed, a failure is framed
|
|
248
|
+
as the dialect's SSE error. A reader that goes away leaves the turn running for its
|
|
249
|
+
retries to coalesce onto."""
|
|
250
|
+
task = asyncio.ensure_future(pending)
|
|
251
|
+
started = time.monotonic()
|
|
252
|
+
try:
|
|
253
|
+
done, _ = await asyncio.wait({task}, timeout=KEEPALIVE_GRACE_SECONDS)
|
|
254
|
+
if done:
|
|
255
|
+
return task.result()
|
|
256
|
+
stream = web.StreamResponse(
|
|
257
|
+
headers={
|
|
258
|
+
"Content-Type": "text/event-stream",
|
|
259
|
+
"Cache-Control": "no-cache",
|
|
260
|
+
"Connection": "keep-alive",
|
|
261
|
+
"X-Accel-Buffering": "no",
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
connected = True
|
|
265
|
+
try:
|
|
266
|
+
await stream.prepare(request)
|
|
267
|
+
first = True
|
|
268
|
+
while not task.done():
|
|
269
|
+
await stream.write(dialect.stream_keepalive(first))
|
|
270
|
+
first = False
|
|
271
|
+
await asyncio.wait({task}, timeout=KEEPALIVE_INTERVAL_SECONDS)
|
|
272
|
+
except ConnectionResetError:
|
|
273
|
+
# A reader that goes away mid-turn is the failure a tunnel or proxy drop looks
|
|
274
|
+
# like from here; its retry (if any) coalesces onto this turn.
|
|
275
|
+
connected = False
|
|
276
|
+
logger.warning(
|
|
277
|
+
"intercept stream: reader disconnected: id=%s after=%.1fs",
|
|
278
|
+
trace_id,
|
|
279
|
+
time.monotonic() - started,
|
|
280
|
+
)
|
|
281
|
+
response = await task
|
|
282
|
+
replay = _capture_response(response)
|
|
283
|
+
# Release coalesced retries now rather than after the write to this reader.
|
|
284
|
+
_finish_idempotent_attempt(request, replay)
|
|
285
|
+
if connected:
|
|
286
|
+
try:
|
|
287
|
+
await stream.write(
|
|
288
|
+
replay.body
|
|
289
|
+
if response.status < 400
|
|
290
|
+
else dialect.stream_error(from_json(replay.body))
|
|
291
|
+
)
|
|
292
|
+
await stream.write_eof()
|
|
293
|
+
except ConnectionResetError:
|
|
294
|
+
logger.warning(
|
|
295
|
+
"intercept stream: reader disconnected before the turn was served: "
|
|
296
|
+
"id=%s after=%.1fs",
|
|
297
|
+
trace_id,
|
|
298
|
+
time.monotonic() - started,
|
|
299
|
+
)
|
|
300
|
+
return stream
|
|
301
|
+
finally:
|
|
302
|
+
if not task.done():
|
|
303
|
+
logger.info(
|
|
304
|
+
"intercept stream: turn cancelled: id=%s after=%.1fs",
|
|
305
|
+
trace_id,
|
|
306
|
+
time.monotonic() - started,
|
|
307
|
+
)
|
|
308
|
+
task.cancel()
|
|
309
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
310
|
+
await task
|
|
219
311
|
|
|
220
312
|
|
|
221
313
|
class InterceptionServerConfig(BaseInterceptionConfig):
|
|
@@ -503,10 +595,10 @@ class InterceptionServer(Interception):
|
|
|
503
595
|
body = json.loads(raw)
|
|
504
596
|
body = dialect.apply_overrides(body, session.ctx.model, session.ctx.sampling)
|
|
505
597
|
streaming = dialect.streaming(body)
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
)
|
|
509
|
-
req_hash = await _request_digest(raw)
|
|
598
|
+
# A streamed request is served whole once its turn commits: the eval client's
|
|
599
|
+
# provider stream is read to the end, and the train client generates the response.
|
|
600
|
+
relay = streaming and not isinstance(session.ctx.client, TrainClientConfig)
|
|
601
|
+
req_hash = await _request_digest(raw)
|
|
510
602
|
# Keep `read()` for aiohttp's size guard, then release its cache and our local
|
|
511
603
|
# alias after parsing so the wire body does not survive model inference.
|
|
512
604
|
request._read_bytes = None
|
|
@@ -516,10 +608,11 @@ class InterceptionServer(Interception):
|
|
|
516
608
|
except ValueError as error:
|
|
517
609
|
return web.json_response(dialect.error_body(str(error)), status=400)
|
|
518
610
|
logger.debug(
|
|
519
|
-
"intercept %s: id=%s stream=%s",
|
|
611
|
+
"intercept %s: id=%s stream=%s retry=%s",
|
|
520
612
|
request.path,
|
|
521
613
|
session.trace.id,
|
|
522
614
|
streaming,
|
|
615
|
+
request.headers.get(RETRY_COUNT_HEADER, "0"),
|
|
523
616
|
)
|
|
524
617
|
# Graph atomicity under retries: one logical buffered call must commit at most
|
|
525
618
|
# one turn. An explicit key identifies that call directly; otherwise only the SDK's
|
|
@@ -528,18 +621,9 @@ class InterceptionServer(Interception):
|
|
|
528
621
|
retried = is_retried_request(request.headers)
|
|
529
622
|
idempotent: IdempotentRequest | None = None
|
|
530
623
|
idempotency_key = request.headers.get(IDEMPOTENCY_KEY_HEADER)
|
|
531
|
-
replay_key: str | None = None
|
|
532
624
|
binding = (request.path, req_hash)
|
|
533
625
|
if idempotency_key:
|
|
534
|
-
|
|
535
|
-
return web.json_response(
|
|
536
|
-
dialect.error_body(
|
|
537
|
-
"Idempotency-Key is not supported for streaming requests"
|
|
538
|
-
),
|
|
539
|
-
status=400,
|
|
540
|
-
)
|
|
541
|
-
if not relay_streaming:
|
|
542
|
-
replay_key = f"explicit:{idempotency_key}"
|
|
626
|
+
replay_key = f"explicit:{idempotency_key}"
|
|
543
627
|
# This key identifies the harness-to-interception hop. The server owns its
|
|
544
628
|
# replay semantics, and the body has since been rewritten with rollout model
|
|
545
629
|
# and sampling overrides, so never expose the local key to the provider.
|
|
@@ -548,27 +632,26 @@ class InterceptionServer(Interception):
|
|
|
548
632
|
for name, value in upstream_headers.items()
|
|
549
633
|
if name.lower() != IDEMPOTENCY_KEY_HEADER.lower()
|
|
550
634
|
}
|
|
551
|
-
|
|
635
|
+
else:
|
|
552
636
|
replay_key = f"retry:{request.path}:{req_hash.hex()}"
|
|
553
637
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return _replay_response(idempotent.response)
|
|
638
|
+
now = time.monotonic()
|
|
639
|
+
_prune_idempotent_requests(session, now)
|
|
640
|
+
if idempotency_key or retried:
|
|
641
|
+
idempotent = session.idempotent_requests.get(replay_key)
|
|
642
|
+
if idempotent is not None and idempotent.binding != binding:
|
|
643
|
+
return web.json_response(
|
|
644
|
+
dialect.error_body(
|
|
645
|
+
"Idempotency-Key was reused with a different request"
|
|
646
|
+
),
|
|
647
|
+
status=400,
|
|
648
|
+
)
|
|
649
|
+
if idempotent is not None and idempotent.response is not None:
|
|
650
|
+
logger.debug(
|
|
651
|
+
"intercept replay: id=%s (idempotent request)", session.trace.id
|
|
652
|
+
)
|
|
653
|
+
idempotent.completed_at = now
|
|
654
|
+
return _replay_response(idempotent.response)
|
|
572
655
|
|
|
573
656
|
try:
|
|
574
657
|
model_request = dialect.parse_request(body)
|
|
@@ -583,7 +666,7 @@ class InterceptionServer(Interception):
|
|
|
583
666
|
dialect.error_body(f"rollout stopped: {session.trace.stop_condition}"),
|
|
584
667
|
status=400,
|
|
585
668
|
)
|
|
586
|
-
if
|
|
669
|
+
if idempotent is None:
|
|
587
670
|
idempotent = IdempotentRequest(binding=binding)
|
|
588
671
|
session.idempotent_requests[replay_key] = idempotent
|
|
589
672
|
|
|
@@ -600,20 +683,22 @@ class InterceptionServer(Interception):
|
|
|
600
683
|
)
|
|
601
684
|
return _replay_response(response)
|
|
602
685
|
|
|
603
|
-
if idempotent is not None:
|
|
604
|
-
if
|
|
605
|
-
return await
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
686
|
+
if idempotent.inflight is not None:
|
|
687
|
+
if streaming:
|
|
688
|
+
return await _buffered_stream(
|
|
689
|
+
request, dialect, coalesced(idempotent.inflight), session.trace.id
|
|
690
|
+
)
|
|
691
|
+
return await coalesced(idempotent.inflight)
|
|
692
|
+
future: asyncio.Future[ReplayResponse | None] = (
|
|
693
|
+
asyncio.get_running_loop().create_future()
|
|
694
|
+
)
|
|
695
|
+
idempotent.inflight = future
|
|
696
|
+
request[_IDEMPOTENT_ATTEMPT] = _IdempotentAttempt(
|
|
697
|
+
session=session,
|
|
698
|
+
key=replay_key,
|
|
699
|
+
request=idempotent,
|
|
700
|
+
future=future,
|
|
701
|
+
)
|
|
617
702
|
|
|
618
703
|
try:
|
|
619
704
|
refused = await session.refused()
|
|
@@ -668,46 +753,27 @@ class InterceptionServer(Interception):
|
|
|
668
753
|
# turns): live watchers see it now rather than with the model's reply.
|
|
669
754
|
session.trace.preview(turn, turn.tail)
|
|
670
755
|
|
|
671
|
-
|
|
672
|
-
# released, so a stream is buffered for them too.
|
|
673
|
-
inspect_response = bool(
|
|
674
|
-
session.response_interceptors
|
|
675
|
-
or session.response_stops
|
|
676
|
-
or session.request_interceptors
|
|
677
|
-
or session.request_stops
|
|
678
|
-
)
|
|
679
|
-
if relay_streaming:
|
|
680
|
-
return await self._stream(
|
|
681
|
-
request,
|
|
682
|
-
session,
|
|
683
|
-
dialect,
|
|
684
|
-
body,
|
|
685
|
-
model_request,
|
|
686
|
-
turn=turn,
|
|
687
|
-
inspect_response=inspect_response,
|
|
688
|
-
policy_paths=policy_paths,
|
|
689
|
-
acp=acp,
|
|
690
|
-
upstream_headers=upstream_headers,
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
def serve(response: Response) -> web.Response:
|
|
756
|
+
def serve(response: Response, events: bytes | None) -> web.Response:
|
|
694
757
|
if streaming:
|
|
695
|
-
#
|
|
696
|
-
#
|
|
758
|
+
# The committed turn as SSE: the provider's own events when relayed
|
|
759
|
+
# unchanged, else framed from the response (training generates it whole,
|
|
760
|
+
# and a hook may have rewritten a relayed one).
|
|
697
761
|
served = web.Response(
|
|
698
|
-
body=
|
|
762
|
+
body=events
|
|
763
|
+
if events is not None
|
|
764
|
+
else b"".join(dialect.stream_events(response.raw or {})),
|
|
699
765
|
content_type="text/event-stream",
|
|
700
766
|
)
|
|
701
767
|
else:
|
|
702
768
|
served = _completion_response(response.raw)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
idempotent.completed_at = time.monotonic()
|
|
769
|
+
idempotent.response = _capture_response(served)
|
|
770
|
+
idempotent.completed_at = time.monotonic()
|
|
706
771
|
return served
|
|
707
772
|
|
|
708
773
|
async def sample() -> web.Response:
|
|
709
774
|
session.error = None
|
|
710
775
|
call_response: Response | None = None
|
|
776
|
+
events: bytes | None = None
|
|
711
777
|
node: int | None = None
|
|
712
778
|
error: Exception | None = None
|
|
713
779
|
started = time.time()
|
|
@@ -715,14 +781,23 @@ class InterceptionServer(Interception):
|
|
|
715
781
|
try:
|
|
716
782
|
# What actually goes upstream: the native body with the rollout's model +
|
|
717
783
|
# sampling imposed — recorded raw on the trace, per call.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
784
|
+
if relay:
|
|
785
|
+
reply = await session.client.relay(
|
|
786
|
+
dialect,
|
|
787
|
+
body,
|
|
788
|
+
headers=upstream_headers,
|
|
789
|
+
session_id=session.trace.id,
|
|
790
|
+
)
|
|
791
|
+
call_response, events = await _collect_stream(dialect, reply)
|
|
792
|
+
else:
|
|
793
|
+
call_response = await session.client.get_response(
|
|
794
|
+
dialect,
|
|
795
|
+
body,
|
|
796
|
+
session.ctx.sampling,
|
|
797
|
+
headers=upstream_headers,
|
|
798
|
+
session_id=session.trace.id,
|
|
799
|
+
turn=turn,
|
|
800
|
+
)
|
|
726
801
|
logger.debug(
|
|
727
802
|
"intercept turn: id=%s tools=%d",
|
|
728
803
|
session.trace.id,
|
|
@@ -741,6 +816,7 @@ class InterceptionServer(Interception):
|
|
|
741
816
|
stopped,
|
|
742
817
|
) = await session.rewrite_response(call_response)
|
|
743
818
|
if response_rewrites:
|
|
819
|
+
events = None
|
|
744
820
|
assert call_response.raw is not None
|
|
745
821
|
dialect.rewrite_response(
|
|
746
822
|
call_response.raw, call_response.message.content or ""
|
|
@@ -816,279 +892,12 @@ class InterceptionServer(Interception):
|
|
|
816
892
|
policy_paths=policy_paths,
|
|
817
893
|
acp=acp,
|
|
818
894
|
)
|
|
819
|
-
return serve(call_response)
|
|
895
|
+
return serve(call_response, events)
|
|
820
896
|
|
|
897
|
+
if streaming:
|
|
898
|
+
return await _buffered_stream(request, dialect, sample(), session.trace.id)
|
|
821
899
|
return await sample()
|
|
822
900
|
|
|
823
|
-
async def _stream(
|
|
824
|
-
self,
|
|
825
|
-
request: web.Request,
|
|
826
|
-
session: RolloutSession,
|
|
827
|
-
dialect: Dialect,
|
|
828
|
-
body: dict,
|
|
829
|
-
model_request: Request,
|
|
830
|
-
*,
|
|
831
|
-
turn: graph.PendingTurn,
|
|
832
|
-
inspect_response: bool,
|
|
833
|
-
policy_paths: list[str] | None = None,
|
|
834
|
-
acp: ACPInfo | None = None,
|
|
835
|
-
upstream_headers: Mapping[str, str] | None = None,
|
|
836
|
-
) -> web.StreamResponse:
|
|
837
|
-
"""A streamed (SSE) model turn: relay the provider's stream through to the program,
|
|
838
|
-
incrementally assembling the response to record on the trace (the only client that
|
|
839
|
-
streams is the eval relay)."""
|
|
840
|
-
session.error = None
|
|
841
|
-
reply = None
|
|
842
|
-
response: Response | None = None
|
|
843
|
-
node: int | None = None
|
|
844
|
-
error: Exception | None = None
|
|
845
|
-
started = time.time()
|
|
846
|
-
try:
|
|
847
|
-
try:
|
|
848
|
-
reply = await session.client.relay(
|
|
849
|
-
dialect,
|
|
850
|
-
body,
|
|
851
|
-
headers=upstream_headers,
|
|
852
|
-
session_id=session.trace.id,
|
|
853
|
-
)
|
|
854
|
-
except RolloutError as e:
|
|
855
|
-
error = e
|
|
856
|
-
session.error = e
|
|
857
|
-
logger.warning(
|
|
858
|
-
"model call failed: id=%s %s: %s",
|
|
859
|
-
session.trace.id,
|
|
860
|
-
type(e).__name__,
|
|
861
|
-
e,
|
|
862
|
-
)
|
|
863
|
-
return web.json_response(
|
|
864
|
-
dialect.error_body(str(e)), status=getattr(e, "status_code", 502)
|
|
865
|
-
)
|
|
866
|
-
except Exception as e: # noqa: BLE001 - surface as an API error
|
|
867
|
-
error = e
|
|
868
|
-
logger.warning("model call failed: id=%s %s", session.trace.id, e)
|
|
869
|
-
return web.json_response(dialect.error_body(str(e)), status=502)
|
|
870
|
-
|
|
871
|
-
if inspect_response:
|
|
872
|
-
buffered = SpooledTemporaryFile( # noqa: SIM115 - closed before every exit
|
|
873
|
-
max_size=STREAM_MEMORY_BUFFER
|
|
874
|
-
)
|
|
875
|
-
parser = dialect.stream_parser()
|
|
876
|
-
saw_terminal = False
|
|
877
|
-
try:
|
|
878
|
-
async for chunk in reply.chunks:
|
|
879
|
-
buffered.write(chunk)
|
|
880
|
-
saw_terminal |= dialect.is_terminal_event(chunk)
|
|
881
|
-
if parser.on_done is not None and is_sse_done_event(chunk):
|
|
882
|
-
parser.on_done()
|
|
883
|
-
parser.feed(chunk)
|
|
884
|
-
if not saw_terminal:
|
|
885
|
-
raise ProviderError(
|
|
886
|
-
"upstream stream ended before its terminal event"
|
|
887
|
-
)
|
|
888
|
-
response = parser.finish()
|
|
889
|
-
response_rewrites = []
|
|
890
|
-
stopped = None
|
|
891
|
-
if session.response_interceptors or session.response_stops:
|
|
892
|
-
(
|
|
893
|
-
response,
|
|
894
|
-
response_rewrites,
|
|
895
|
-
stopped,
|
|
896
|
-
) = await session.rewrite_response(response)
|
|
897
|
-
if response_rewrites:
|
|
898
|
-
assert response.raw is not None
|
|
899
|
-
dialect.rewrite_response(
|
|
900
|
-
response.raw, response.message.content or ""
|
|
901
|
-
)
|
|
902
|
-
raw_response = response.raw
|
|
903
|
-
response = dialect.parse_response(
|
|
904
|
-
dialect.validate_response(raw_response)
|
|
905
|
-
)
|
|
906
|
-
response.raw = raw_response
|
|
907
|
-
except RolloutError as e:
|
|
908
|
-
buffered.close()
|
|
909
|
-
error = e
|
|
910
|
-
session.error = e
|
|
911
|
-
return self._fail(session, dialect, e)
|
|
912
|
-
except Exception as e: # noqa: BLE001 - malformed provider stream
|
|
913
|
-
buffered.close()
|
|
914
|
-
error = ProviderError(str(e))
|
|
915
|
-
session.error = error
|
|
916
|
-
return self._fail(session, dialect, error)
|
|
917
|
-
finally:
|
|
918
|
-
await reply.close()
|
|
919
|
-
|
|
920
|
-
if session.released or session.stopped:
|
|
921
|
-
buffered.close()
|
|
922
|
-
return web.json_response(
|
|
923
|
-
dialect.error_body(
|
|
924
|
-
"rollout concluded"
|
|
925
|
-
if session.released
|
|
926
|
-
else f"rollout stopped: {session.trace.stop_condition}"
|
|
927
|
-
),
|
|
928
|
-
status=409 if session.released else 400,
|
|
929
|
-
)
|
|
930
|
-
node = turn.commit(response)
|
|
931
|
-
session.consume_prepared(turn.tail)
|
|
932
|
-
session.trace.response_rewrites.extend(response_rewrites)
|
|
933
|
-
if stopped is None:
|
|
934
|
-
try:
|
|
935
|
-
stopped = await session.gate_tool_calls(node)
|
|
936
|
-
except RolloutError as e:
|
|
937
|
-
error = e
|
|
938
|
-
buffered.close()
|
|
939
|
-
return self._fail(session, dialect, e)
|
|
940
|
-
if stopped is not None:
|
|
941
|
-
buffered.close()
|
|
942
|
-
session.trace.stop(stopped)
|
|
943
|
-
return web.json_response(
|
|
944
|
-
dialect.error_body(f"rollout stopped: {stopped}"),
|
|
945
|
-
status=400,
|
|
946
|
-
)
|
|
947
|
-
|
|
948
|
-
resp = web.StreamResponse(
|
|
949
|
-
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
|
|
950
|
-
)
|
|
951
|
-
resp.content_type = reply.content_type.split(";")[0].strip()
|
|
952
|
-
try:
|
|
953
|
-
await resp.prepare(request)
|
|
954
|
-
if response_rewrites:
|
|
955
|
-
for event in dialect.stream_events(response.raw or {}):
|
|
956
|
-
await resp.write(event)
|
|
957
|
-
else:
|
|
958
|
-
buffered.seek(0)
|
|
959
|
-
while chunk := buffered.read(64 * 1024):
|
|
960
|
-
await resp.write(chunk)
|
|
961
|
-
await resp.write_eof()
|
|
962
|
-
except ConnectionResetError:
|
|
963
|
-
pass
|
|
964
|
-
finally:
|
|
965
|
-
buffered.close()
|
|
966
|
-
return resp
|
|
967
|
-
|
|
968
|
-
resp = web.StreamResponse(
|
|
969
|
-
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
|
|
970
|
-
)
|
|
971
|
-
resp.content_type = reply.content_type.split(";")[0].strip()
|
|
972
|
-
# Parse complete events as they relay, avoiding a full-stream byte copy.
|
|
973
|
-
parser = dialect.stream_parser()
|
|
974
|
-
feed_event = parser.feed
|
|
975
|
-
on_done = parser.on_done
|
|
976
|
-
# One bounded producer avoids per-event tasks; keepalive timeouts only cancel readiness waits.
|
|
977
|
-
queue: asyncio.Queue[bytes | None] = asyncio.Queue(
|
|
978
|
-
maxsize=STREAM_QUEUE_MAXSIZE
|
|
979
|
-
)
|
|
980
|
-
ready = asyncio.Event()
|
|
981
|
-
producer = asyncio.create_task(_queue_chunks(reply.chunks, queue, ready))
|
|
982
|
-
parser_error: Exception | None = None
|
|
983
|
-
# SSE events from the turn-ending one onward (the terminal event and any trailing
|
|
984
|
-
# `[DONE]`), withheld until the turn is committed: a client that ends its turn on the
|
|
985
|
-
# terminal event (e.g. codex on `response.completed`) would otherwise reach scoring
|
|
986
|
-
# with the turn still unrecorded.
|
|
987
|
-
deferred: list[bytes] = []
|
|
988
|
-
try:
|
|
989
|
-
await resp.prepare(request)
|
|
990
|
-
while True:
|
|
991
|
-
try:
|
|
992
|
-
async with asyncio.timeout(KEEPALIVE_INTERVAL_SECONDS):
|
|
993
|
-
await ready.wait()
|
|
994
|
-
except TimeoutError:
|
|
995
|
-
# Don't terminate an empty event; some SSE clients try to JSON-decode it.
|
|
996
|
-
await resp.write(b": keepalive\n")
|
|
997
|
-
continue
|
|
998
|
-
chunk = queue.get_nowait()
|
|
999
|
-
if queue.empty():
|
|
1000
|
-
ready.clear()
|
|
1001
|
-
if chunk is None:
|
|
1002
|
-
await producer
|
|
1003
|
-
break
|
|
1004
|
-
# We send our own keepalive above. Some clients treat a complete
|
|
1005
|
-
# comment-only event from upstream as an empty JSON payload.
|
|
1006
|
-
if not any(
|
|
1007
|
-
line.startswith(b"data:") for line in chunk.splitlines()
|
|
1008
|
-
):
|
|
1009
|
-
await resp.write(b": keepalive\n")
|
|
1010
|
-
continue
|
|
1011
|
-
if deferred or dialect.is_terminal_event(chunk):
|
|
1012
|
-
if parser_error is None:
|
|
1013
|
-
try:
|
|
1014
|
-
if on_done is not None and is_sse_done_event(chunk):
|
|
1015
|
-
on_done()
|
|
1016
|
-
feed_event(chunk)
|
|
1017
|
-
except Exception as e: # noqa: BLE001 - defer parser failure
|
|
1018
|
-
parser_error = e
|
|
1019
|
-
# forwarded after the turn is committed, below
|
|
1020
|
-
deferred.append(chunk)
|
|
1021
|
-
continue
|
|
1022
|
-
await resp.write(chunk)
|
|
1023
|
-
if parser_error is None:
|
|
1024
|
-
try:
|
|
1025
|
-
feed_event(chunk)
|
|
1026
|
-
except Exception as e: # noqa: BLE001 - defer parser failure
|
|
1027
|
-
parser_error = e
|
|
1028
|
-
except ConnectionResetError as e:
|
|
1029
|
-
# The harness went away mid-stream; the provider exchange still happened.
|
|
1030
|
-
error = e
|
|
1031
|
-
return resp
|
|
1032
|
-
finally:
|
|
1033
|
-
producer.cancel()
|
|
1034
|
-
# Let a canceled producer enqueue EOF while unwinding.
|
|
1035
|
-
if queue.full():
|
|
1036
|
-
queue.get_nowait()
|
|
1037
|
-
await asyncio.gather(producer, return_exceptions=True)
|
|
1038
|
-
await reply.close()
|
|
1039
|
-
|
|
1040
|
-
try:
|
|
1041
|
-
if parser_error is not None:
|
|
1042
|
-
raise parser_error
|
|
1043
|
-
response = parser.finish()
|
|
1044
|
-
if not session.released and not session.stopped:
|
|
1045
|
-
node = turn.commit(response)
|
|
1046
|
-
session.consume_prepared(turn.tail)
|
|
1047
|
-
logger.debug("intercept stream turn: id=%s", session.trace.id)
|
|
1048
|
-
elif session.stopped:
|
|
1049
|
-
with contextlib.suppress(ConnectionResetError):
|
|
1050
|
-
await resp.write_eof()
|
|
1051
|
-
return resp
|
|
1052
|
-
finally:
|
|
1053
|
-
# Release the withheld events only now — after the commit — then close.
|
|
1054
|
-
with contextlib.suppress(ConnectionResetError):
|
|
1055
|
-
for event in deferred:
|
|
1056
|
-
await resp.write(event)
|
|
1057
|
-
await resp.write_eof()
|
|
1058
|
-
return resp
|
|
1059
|
-
except RolloutError as e:
|
|
1060
|
-
# A streamed terminal provider failure is discovered only after the
|
|
1061
|
-
# response body has been relayed. Keep it off the graph and preserve
|
|
1062
|
-
# the typed cause for the rollout if the native SDK does not retry it.
|
|
1063
|
-
if node is None:
|
|
1064
|
-
error = e
|
|
1065
|
-
session.error = e
|
|
1066
|
-
raise
|
|
1067
|
-
except BaseException as e:
|
|
1068
|
-
# Anything that propagates (a mid-relay upstream failure, a parser or commit
|
|
1069
|
-
# error, a cancellation) ends a real exchange; couple it to the record unless
|
|
1070
|
-
# the turn already committed (then only post-commit delivery failed).
|
|
1071
|
-
if node is None:
|
|
1072
|
-
error = e
|
|
1073
|
-
raise
|
|
1074
|
-
finally:
|
|
1075
|
-
if node is None:
|
|
1076
|
-
turn.abandon()
|
|
1077
|
-
# The turn's one per-exchange record: settings, timing, outcome, and the
|
|
1078
|
-
# error that ended it (if any).
|
|
1079
|
-
self.record_call(
|
|
1080
|
-
session,
|
|
1081
|
-
dialect,
|
|
1082
|
-
body,
|
|
1083
|
-
started,
|
|
1084
|
-
node=node,
|
|
1085
|
-
finish_reason=response.finish_reason if response is not None else None,
|
|
1086
|
-
usage=response.usage if response is not None else None,
|
|
1087
|
-
error=error,
|
|
1088
|
-
policy_paths=policy_paths,
|
|
1089
|
-
acp=acp,
|
|
1090
|
-
)
|
|
1091
|
-
|
|
1092
901
|
async def handle_aux(
|
|
1093
902
|
self, request: web.Request, dialect: Dialect, route: str
|
|
1094
903
|
) -> web.Response:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: verifiers
|
|
3
|
-
Version: 0.3.2.
|
|
3
|
+
Version: 0.3.2.dev133
|
|
4
4
|
Summary: Verifiers: Environments for LLM Reinforcement Learning
|
|
5
5
|
Project-URL: Homepage, https://github.com/primeintellect-ai/verifiers
|
|
6
6
|
Project-URL: Documentation, https://github.com/primeintellect-ai/verifiers
|
|
@@ -60,10 +60,10 @@ verifiers/v1/configs/cli/init.py,sha256=JvIUOdhfZfNOXg2wm2i06jLLHpb97tNMzZqmcK_9
|
|
|
60
60
|
verifiers/v1/configs/cli/replay.py,sha256=vUCNs-9j1CXcAZhGozN1EaztcXK_dYVK3iN_fRmbSXw,2929
|
|
61
61
|
verifiers/v1/configs/cli/validate.py,sha256=z7iueq4ndDFKOQ1GA2vaK3C8RhxhDUim-Nn-71T3kGw,3694
|
|
62
62
|
verifiers/v1/dialects/__init__.py,sha256=MNOYM2NdTlSxGK4VtoEjMtD5zya2MpjPDX5jMvZUWEo,736
|
|
63
|
-
verifiers/v1/dialects/anthropic.py,sha256=
|
|
64
|
-
verifiers/v1/dialects/base.py,sha256=
|
|
63
|
+
verifiers/v1/dialects/anthropic.py,sha256=rkxS8K4xvSTZwS86Mz_a4MfHzrkYf9Vvu8wCerzVNIw,26111
|
|
64
|
+
verifiers/v1/dialects/base.py,sha256=7peaHJGdVT93G3erTnlAcRJi7tGvJ9BUkd_W5O4HGms,16140
|
|
65
65
|
verifiers/v1/dialects/chat.py,sha256=KOrDW2f4WMLl37Pv2EilKddCXUUgRNjl4tQseMiPC-o,23775
|
|
66
|
-
verifiers/v1/dialects/responses.py,sha256=
|
|
66
|
+
verifiers/v1/dialects/responses.py,sha256=GXI4xzvZWL5Dugi9uHPhrSdgV6RQ5zQuuPfzVztu2Go,30457
|
|
67
67
|
verifiers/v1/envs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
68
|
verifiers/v1/envs/agentic_judge/__init__.py,sha256=LfEcRHLG_siKzhzWmODDXWSLpLgfum3UFdQDK9XG0gk,325
|
|
69
69
|
verifiers/v1/envs/agentic_judge/env.py,sha256=GovnHqgRm8nQLAM7wMx57-A_wJBRYopsfiy2Bgf32nA,15339
|
|
@@ -118,14 +118,14 @@ verifiers/v1/harnesses/terminus_2/harness.py,sha256=cKQJluW01mkxEccTLFwSWsVMavyi
|
|
|
118
118
|
verifiers/v1/harnesses/terminus_2/program.py,sha256=X8MZdyItU3gbXGXnGOlJ9LWcUTnqIDaxlhkZvoRiqKY,2703
|
|
119
119
|
verifiers/v1/harnesses/utils/__init__.py,sha256=eTS-Ft2HwoLiYn5u1t_KueLmaZB_W12cdYmcVsE-G5A,364
|
|
120
120
|
verifiers/v1/harnesses/utils/compaction.py,sha256=DeY6CYLEsv7-vJm26sT2mRaPHu3W2ywiZhvFr_pi6Hg,10495
|
|
121
|
-
verifiers/v1/harnesses/utils/core.py,sha256=
|
|
121
|
+
verifiers/v1/harnesses/utils/core.py,sha256=emAeNqtha__3cR0u7pvoQGnl2sE57wadE0wlKGtO1Yk,18520
|
|
122
122
|
verifiers/v1/harnesses/utils/install.py,sha256=BDkkUVZ1brSzCrbQOvMO5mVIE2DsV2pE7rTC5yROYUw,2992
|
|
123
123
|
verifiers/v1/harnesses/utils/launch.py,sha256=RqPuUY-kT2XVuLSGtXW7kRQEaP-o2KzkiJ7vcoo2neM,2883
|
|
124
124
|
verifiers/v1/harnesses/utils/mcp.py,sha256=oNhKfLjAW-wScJcDIsW_HfuvKY-JkaGBBZrRLqgRK1g,7355
|
|
125
125
|
verifiers/v1/interception/__init__.py,sha256=5Kvo7tcBUVaa65s9A1NiOH3CPpzw-P5btXwxB4Nbbvs,4251
|
|
126
126
|
verifiers/v1/interception/base.py,sha256=Mg16Y104lyZilF9shdivdqYoqz4OYp978i_myyrulu8,2529
|
|
127
127
|
verifiers/v1/interception/pool.py,sha256=sk_zcdTV8EVRuzqlYMXxon_42tSDAlFvwcyOO2EF7hw,6473
|
|
128
|
-
verifiers/v1/interception/server.py,sha256=
|
|
128
|
+
verifiers/v1/interception/server.py,sha256=DnPvQcZZX0JYvxrTLOQuJn4QFX5Z44edGjpvfdxZmRw,45773
|
|
129
129
|
verifiers/v1/interception/tunnel/__init__.py,sha256=eVNZJszj6myrKhx9jmJz7VRd9qlfj0AUvUZwI2bXEj8,893
|
|
130
130
|
verifiers/v1/interception/tunnel/base.py,sha256=DZB6uPLwM4Qy7n7m0vKeyg-Px87MhsmpYTpe2vpPEng,1998
|
|
131
131
|
verifiers/v1/interception/tunnel/custom.py,sha256=yL4UbGf4bAuMsxk42mEP-t1wITQCT8BE9FHQIz0qRMA,1708
|
|
@@ -190,8 +190,8 @@ verifiers/v1/utils/retries.py,sha256=Y2ZgrAjn-qNkRKZP_RVNL_7EK0iaRcZeCa404lpGYi4
|
|
|
190
190
|
verifiers/v1/utils/scope.py,sha256=bzUEiWDOVdbj7dXOwGOlzsQ_-Xu0j9NR5TalQB-8TS8,838
|
|
191
191
|
verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
|
|
192
192
|
verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
|
|
193
|
-
verifiers-0.3.2.
|
|
194
|
-
verifiers-0.3.2.
|
|
195
|
-
verifiers-0.3.2.
|
|
196
|
-
verifiers-0.3.2.
|
|
197
|
-
verifiers-0.3.2.
|
|
193
|
+
verifiers-0.3.2.dev133.dist-info/METADATA,sha256=Ff2GNRZ7xB_YdcKn5Cu6_FLNmVH6PuNJYVXZ-brj6mE,4161
|
|
194
|
+
verifiers-0.3.2.dev133.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
195
|
+
verifiers-0.3.2.dev133.dist-info/entry_points.txt,sha256=iugElcdWPKbQM7uFF0lZ8iUpHsNr17-BwEAAjJWxV3U,259
|
|
196
|
+
verifiers-0.3.2.dev133.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
|
|
197
|
+
verifiers-0.3.2.dev133.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|