osrforge.providers.reading
The reading provider: Claude's role in the tracing stage, and the recordings tests replay.
The tracing stage asks Claude to read a map page whole, to judge the items
Clef is unsure of, and to pick the offset between two halves of a map. Each
of those is a kind of request (RequestKind):
one system prompt, one output schema, one effort, and one max_tokens, fixed
for every request of that kind so that requests of a kind share prompt-cache
entries. A ReadingRequest is
the kind plus its content, arranged for the cache: the shared parts first,
each ending at a cache breakpoint, and the crop and the question last.
A recording is one answer filed under its key, the hash of the request's
pixels and parameters, so tests replay it without calling the service.
RecordedReadingProvider
replays a directory of recordings and
RecordingReadingProvider
writes them. The live adapter is
ClaudeProvider. A recording of
a private corpus member's map may contain the module's text or picture, so
those recordings live in the owner's private corpus directory and never in
the repository; only the committed corpus members' and the synthetic maps'
recordings are committed.
MAX_SHARED_PARTS
module-attribute
The most shared parts a request may have.
The API allows four cache breakpoints in one request. The system prompt takes one, so a request has at most three shared parts.
MAX_SHARED_PART_BLOCKS
module-attribute
The most content parts one shared part may have.
The cache looks back twenty blocks from each breakpoint for the previous one, so a shared part is a few blocks: the instructions as one text part, or a page as one image part, never a long run of parts.
THINKING
module-attribute
The one thinking setting every reading request sends.
Every request runs with the API's adaptive thinking, because Claude Opus 5.5 can't turn thinking off, and a change of thinking setting would break the prompt cache. The setting is part of every recording key.
Effort
module-attribute
Effort = Literal['low', 'medium', 'high', 'xhigh', 'max']
The API's effort setting: how much the model thinks before it answers.
ReadingProvider
Bases: Protocol
The reading role of the tracing stage, which Claude fills.
Any object with these two methods is a reading provider. A provider owns
its transport retries, its ledger, and its schema check: read returns
data that fits the kind's schema or raises.
read
read(request: ReadingRequest) -> ReadingResponse
Send one request and return its answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request. |
required |
Returns:
| Type | Description |
|---|---|
ReadingResponse
|
The schema-valid answer with its usage. |
Raises:
| Type | Description |
|---|---|
ReadingFailure
|
If the answer stopped at |
SchemaValidationError
|
If the answer doesn't fit the kind's schema. |
LedgerStopError
|
If the request's reservation would pass the ledger's stop. |
ProviderError
|
On transport, auth, or rate-limit exhaustion. |
read_all
read_all(requests: Sequence[ReadingRequest]) -> list[ReadingResponse | Exception]
Send several requests, ordered so each shared part is written to the cache once.
For each distinct cached prefix
(ReadingRequest.shared_key),
the first request is sent alone, and the others wait until that
request's answer starts to arrive. Requests whose every shared part
is already in the cache go at the same time. A request that fails
doesn't stop the others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
requests
|
Sequence[ReadingRequest]
|
The requests, in any order. |
required |
Returns:
| Type | Description |
|---|---|
list[ReadingResponse | Exception]
|
One item per request, in the requests' order: its answer, or the |
list[ReadingResponse | Exception]
|
exception |
ReadingRequest
dataclass
ReadingRequest(
kind: RequestKind,
shared: tuple[tuple[TextPart | ImagePart, ...], ...],
question: tuple[TextPart | ImagePart, ...],
retry: Retry | None = None,
)
One request to the reading provider.
The provider renders it as one user message: every shared part in order, then the question, with a cache breakpoint on the last block of each shared part. A retry adds the malformed answer as an assistant message and the fault as a second user message after that.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
RequestKind
|
The kind of request. |
shared |
tuple[tuple[TextPart | ImagePart, ...], ...]
|
The shared parts, in order: the instructions, the legend, the
symbol vocabulary, and any worked examples first, then the page
image when several requests are about one page. Each part is one
or more content parts, at most
|
question |
tuple[TextPart | ImagePart, ...]
|
The crop and the question, after the shared parts. At least one content part. |
retry |
Retry | None
|
The malformed answer and the fault, when this request is the one retry of an earlier request. |
key
Return the recording key: the hash of the request's pixels and parameters.
The key is the sha256 hex of the canonical JSON (sorted keys, compact
separators, UTF-8) of the model, the system prompt, each shared part
and the question as lists of blocks (a text part as its text, an image
part as its pixel_digest),
the effort, THINKING, the
schema, max_tokens, and the retry's answer and fault when there is
one. The shared parts stay grouped in that JSON, so moving a cache
breakpoint changes the key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
The model id the request would be sent to. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A 64-character sha256 hex digest. |
shared_key
Return the hash of the request's cached prefix: the model, the kind, and the shared parts.
Two requests with the same shared key read the same cache entries,
so ReadingProvider.read_all
sends one of them first and the rest once that one's answer starts to
arrive. The hash covers the model, the system prompt, the effort,
THINKING, the schema,
max_tokens, and the shared parts the same way key does, and
leaves out the question and the retry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
The model id the request would be sent to. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A 64-character sha256 hex digest. |
ReadingResponse
dataclass
ReadingResponse(data: object, usage: ReadingUsage, model_id: str)
ReadingUsage
Bases: BaseModel
One answer's token usage, with the cache reads and writes the ledger prices separately.
input_tokens
class-attribute
instance-attribute
Input tokens charged at the full price.
cache_read_tokens
class-attribute
instance-attribute
Input tokens read from the prompt cache.
RecordedReadingProvider
Replays recorded answers, so tests and continuous integration never call Claude.
A recording file is JSON with schema_version, key, kind, model,
request, and response. request is the exact body the live adapter
sent (request_body) with each
image block's data replaced by the image's pixel digest, so a reader can
see every parameter and the recording stays small. response has data,
usage, model_id, and stop_reason. On replay, the provider builds
the body the current code would send, in the same digest form, and
compares it with the stored one, so a recording whose key still matches
but whose body the code no longer builds fails as stale instead of
answering.
Bind to a directory of recordings made on one model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recordings
|
Path
|
The directory of |
required |
model
|
str
|
The model id the recordings were made on. It is part of every key, so a request is replayed only against recordings of its own model. |
required |
read
read(request: ReadingRequest) -> ReadingResponse
Replay the recording whose key matches the request.
The replayed data is checked against the kind's schema again, so a
changed schema fails as a SchemaValidationError, not as a wrong
answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request to replay. |
required |
Returns:
| Type | Description |
|---|---|
ReadingResponse
|
The recorded answer. |
Raises:
| Type | Description |
|---|---|
FixtureMissError
|
If no recording has the request's key. |
StaleRecordingError
|
If the stored body, with digests in place of image data, isn't the body the current code builds for the request. |
ReadingFailure
|
If the recording's |
SchemaValidationError
|
If the recorded data doesn't fit the kind's schema. |
ProviderError
|
If the recording's schema version isn't this package's. |
read_all
read_all(requests: Sequence[ReadingRequest]) -> list[ReadingResponse | Exception]
Replay each request in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
requests
|
Sequence[ReadingRequest]
|
The requests. |
required |
Returns:
| Type | Description |
|---|---|
list[ReadingResponse | Exception]
|
One item per request: its recorded answer, or the exception |
list[ReadingResponse | Exception]
|
|
RecordingReadingProvider
RecordingReadingProvider(inner: ReadingProvider, recordings: Path, model: str)
Passes each request to a live provider and writes its answer as a recording.
A failed answer (max_tokens or a refusal) is recorded too, with its
stop_reason, so a replay fails the same way. Writing is idempotent by
key.
Wrap a live provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inner
|
ReadingProvider
|
The provider that answers. |
required |
recordings
|
Path
|
Where recordings land; created if missing. |
required |
model
|
str
|
The model id |
required |
read
read(request: ReadingRequest) -> ReadingResponse
Read through the inner provider and record the exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request. |
required |
Returns:
| Type | Description |
|---|---|
ReadingResponse
|
The inner provider's answer, unchanged. |
read_all
read_all(requests: Sequence[ReadingRequest]) -> list[ReadingResponse | Exception]
Read all through the inner provider's read_all and record every exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
requests
|
Sequence[ReadingRequest]
|
The requests. |
required |
Returns:
| Type | Description |
|---|---|
list[ReadingResponse | Exception]
|
The inner provider's items, unchanged. |
RequestKind
dataclass
One kind of request: what stays the same for every request of that kind.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
A stable identifier like |
system |
str
|
The system prompt. The provider marks it as cached. |
schema |
dict[str, object]
|
The JSON Schema every answer of this kind must satisfy. The
API enforces it ( |
effort |
Effort
|
The effort for this kind. |
max_tokens |
int
|
The most output tokens a request of this kind allows, with room above the longest answer measured for the kind. |
Retry
dataclass
recording_filename
request_body
request_body(request: ReadingRequest, model: str) -> dict[str, Any]
Build the exact body the provider sends for a request.
The body has model, max_tokens, system as one text block with
cache_control, messages, thinking as {"type": "adaptive"}, and
output_config with the kind's effort and its schema as the JSON schema
format. The first message is the user message: the blocks of each shared
part in order, then the question's blocks. A text part is a text block
and an image part is a base64 PNG image block. The last block of each
shared part has cache_control: {"type": "ephemeral"}, and no block of
the question does. A retry adds an assistant message with the malformed
answer's text and a user message with the fault.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request. |
required |
model
|
str
|
The model id. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The body, as the keyword arguments of the SDK's |