osrforge.providers.claude
The Claude adapter: the only module allowed to import anthropic.
It fills the tracing stage's reading role
(ReadingProvider) through the
Messages API: it streams every response, sends images as base64 PNG, asks
for each answer as JSON that fits the kind's schema (output_config.format),
runs every request with adaptive thinking at the kind's effort, and marks a
cache breakpoint on the system prompt and at the end of each shared part. It
reserves each request's worst-case cost on the Claude ledger before sending
and settles it from the answer's usage. The body it sends is
request_body. The server-side
refusal fallbacks parameter stays off, because a fallback would answer on
another model mid-run and change the recording key with it.
PRICES
module-attribute
PRICES: dict[str, ModelPrices] = {
"claude-sonnet-5-5": ModelPrices(input=2e-06, output=1e-05, cache_write=2.5e-06, cache_read=2e-07),
"claude-opus-5-5": ModelPrices(input=4e-06, output=2e-05, cache_write=5e-06, cache_read=2e-07),
}
The list prices of the models the provider accepts, in dollars per token.
A model missing here can't be reserved against the ledger, so the provider refuses it.
ClaudeProvider
ClaudeProvider(
settings: ClaudeSettings,
client: Any | None = None,
sleep: Callable[[float], None] = sleep,
now: Callable[[], str] | None = None,
)
Claude over the Messages API, with the ledger and the cache discipline.
The constructor performs no I/O. Transport policy: at most five attempts
per request, with exponential backoff bounded at 30 seconds, on 408,
409, 429, every 5xx, and connection failures, including a transport
failure while the stream is read. A Retry-After header sets the wait,
within the same bound. An error event inside a stream is retried like a
5xx; the SDK raises it with the stream's own status, which is 200. Any
other 4xx, including an auth failure, raises at once. The attempts share
the request's one reservation, settled once at the end, also when the
answer can't be read. A malformed answer isn't re-prompted here, because
the tracing stage makes the one retry with its own recording. No error
message contains the API key.
Bind to a model and a ledger.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
ClaudeSettings
|
The settings. |
required |
client
|
Any | None
|
An injectable client with the |
None
|
sleep
|
Callable[[float], None]
|
The backoff sleeper, injectable for tests. |
sleep
|
now
|
Callable[[], str] | None
|
Gives the ledger entry's |
None
|
Raises:
| Type | Description |
|---|---|
ProviderError
|
If the settings' model has no prices. |
read
read(request: ReadingRequest) -> ReadingResponse
Send one request and return its answer.
Before sending, the provider reserves
worst_case_usd on a new
ledger entry {"t", "script": "osrforge", "label": "<kind> <effort>",
"model"} in one step
(Ledger.open_reserved),
so a refusal writes nothing to the ledger. It streams the response,
settles the reservation at
cost_usd of the answer's usage
whether or not the answer is good (a request whose attempts all fail
keeps its worst case), and then checks the answer: a
stop_reason of max_tokens or refusal is a
ReadingFailure, and the first text
block's JSON is checked against the kind's schema.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request. |
required |
Returns:
| Type | Description |
|---|---|
ReadingResponse
|
The answer, with |
ReadingResponse
|
from the usage's |
ReadingResponse
|
|
Raises:
| Type | Description |
|---|---|
ReadingFailure
|
If the answer stopped at |
SchemaValidationError
|
If the answer doesn't fit the kind's schema. |
LedgerStopError
|
If the reservation would pass the stop. |
ProviderError
|
On transport, auth, or rate-limit exhaustion. |
read_all
read_all(requests: Sequence[ReadingRequest]) -> list[ReadingResponse | Exception]
Send several requests, each shared part written to the cache by one request before the rest read it.
Requests are sent on threads. A request is sent when every shared part it contains is warm, or when it is the one request chosen to warm the first of its parts that isn't. A part becomes warm when the first event of that request's stream arrives. So the first request of a run goes alone, and requests about different pages that share the same instructions go one after the other at first and then together.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
requests
|
Sequence[ReadingRequest]
|
The requests. |
required |
Returns:
| Type | Description |
|---|---|
list[ReadingResponse | Exception]
|
One item per request, in order: its answer, or the exception |
list[ReadingResponse | Exception]
|
|
ClaudeSettings
Bases: BaseModel
The provider's settings: the key, the model, and the ledger.
Examples:
from pathlib import Path
from pydantic import SecretStr
from osrforge.providers.claude import ClaudeProvider, ClaudeSettings
settings = ClaudeSettings(api_key=SecretStr("sk-..."), ledger_path=Path("claude-ledger.json"))
provider = ClaudeProvider(settings)
api_key
instance-attribute
api_key: SecretStr
The API key. Never written to a log, a recording, or an error message.
model
class-attribute
instance-attribute
model: str = DEFAULT_MODEL
The model id. One model serves every kind of request. It must be in PRICES.
ledger_stop_usd
class-attribute
instance-attribute
ledger_stop_usd: float | None = None
The stop, or None to use the ledger file's own.
from_env
classmethod
from_env() -> ClaudeSettings
Build settings from the environment.
Returns:
| Type | Description |
|---|---|
ClaudeSettings
|
Settings from |
ClaudeSettings
|
(both required), |
ClaudeSettings
|
|
Raises:
| Type | Description |
|---|---|
ProviderError
|
If a required variable is missing or empty, or the stop isn't a number. |
ModelPrices
Bases: BaseModel
One model's list prices in dollars per token.
cost_usd
cost_usd(model: str, usage: ReadingUsage) -> float
Return what an answer cost at list price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
The model id, a key of |
required |
usage
|
ReadingUsage
|
The answer's usage. |
required |
Returns:
| Type | Description |
|---|---|
float
|
`input_tokens * input + output_tokens * output + cache_write_tokens * cache_write + cache_read_tokens * |
float
|
cache_read`, in dollars, rounded to whole micro-dollars as the ledger keeps it. |
Raises:
| Type | Description |
|---|---|
ProviderError
|
If the model has no prices. |
estimate_input_tokens
estimate_input_tokens(request: ReadingRequest) -> int
Estimate a request's input tokens from above, for the reservation.
The estimate is one token per UTF-8 byte of every text part, the system
prompt, the retry's answer and fault, and the schema's compact JSON
(sorted keys, compact separators), plus ceil(width * height / 750) for
each image, plus 64. An image is counted at the size the API reads it:
an image longer than 2,576 pixels on its long edge, or larger than 3.75
megapixels, is scaled down to fit both limits with its proportions kept,
and each side of the scaled size is rounded up. A token is never shorter
than a byte, and the API charges an image about one token per 784 pixels
of that size (about 4,784 tokens at the limit), so the estimate is an
upper bound on what the answer's usage reports, and the
over-reservation is small beside max_tokens at the output price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
ReadingRequest
|
The request. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The estimate. |
worst_case_usd
worst_case_usd(model: str, request: ReadingRequest) -> float
Return a request's worst-case cost: its input at the cache-write price and max_tokens at the output price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
The model id, a key of |
required |
request
|
ReadingRequest
|
The request. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The worst-case cost in dollars, rounded to whole micro-dollars as |
float
|
the ledger keeps it. |
Raises:
| Type | Description |
|---|---|
ProviderError
|
If the model has no prices. |