Skip to content

Opt-in completion recovery

Recovery resends one failed provider completion. It preserves encoded request bytes and keeps transport attempts separate from broker tool rounds. It never restarts a session or executes a previously completed tool again.

Existing callers keep their SDK behavior and errors. Configure recovery_policy on an OllamaGateway, OMLXGateway, or OpenAIGateway to opt in for LLMBroker.generate, generate_response, generate_object, generate_stream, ChatSession.send, and send_stream:

from mojentic.llm import LLMBroker
from mojentic.llm.chat_session import ChatSession
from mojentic.llm.gateways.ollama import OllamaGateway
from mojentic.llm.recovery import AdmissionContext, RecoveryError, RecoveryPolicy


async def admit(context: AdmissionContext) -> str:
    # Replace with your own ownership/termination check. Reject until it succeeds.
    # HTTP 503/504, timeout, or socket close alone does not prove inference ended.
    return "reject"


gateway = OllamaGateway(recovery_policy=RecoveryPolicy(
    max_attempts=3,
    base_delay=0.1,
    delay_ceiling=30,
    budget=60,
    admission=admit,
))
broker = LLMBroker("your-model", gateway=gateway)
session = ChatSession(broker)
try:
    answer = session.send("Explain request recovery")
except RecoveryError as error:
    safe_summary = error.report.model_dump_json()
    # Explicit inspection is sensitive; apply your own storage policy.
    original_exception = error.inspect_cause()
    exact_response_bytes = error.inspect_response()
    exact_response_headers = error.inspect_headers()

A policy defaults to one attempt, including the original request. Eligible errors include transport/client timeouts and HTTP 429/500/502/503/504. A caller-selected status cannot override permanent 400/401/403 or semantic-output safety. Malformed responses, invalid structured output, provider errors, redirects, and capture failures never trigger a resend. Ordinary finish handling remains unchanged. The opted-in structured path raises a RecoveryError with the original Pydantic ValidationError; the legacy path retains its existing validation behavior.

Every ambiguous local resend requires explicit asynchronous admission. A pending coroutine remains pending. Only "allow" admits a retry; "reject" stops it. A hook that returns without either decision stops with admission_undecided, including a None return. This differs from the caller's explicit admission_rejected; an invalid return never grants permission. The lifecycle transition and ordinary RecoveryError.report.outcome use that stable value; streaming ends with a failed event whose StreamOutcome is ADMISSION_UNDECIDED and whose report carries the same value.

Without a hook, ambiguous local acceptance yields admission_required. An eligible local failure with transport trace evidence that TCP, Unix-socket or TLS connection establishment failed supplies acceptance no and may retry without a hook. An exception type alone is insufficient: caller validators or capture hooks can raise httpx.ConnectError or httpx.ConnectTimeout after an HTTP 200. Such failures retain the observed status, progress and inspectable cause, remain acceptance unknown, and cannot resend a completed response. Transports without connection trace evidence retain conservative acceptance. HTTP statuses, response bodies, read/write failures, silence and generated IDs are not evidence of nonacceptance. Other dispatched failures retain acceptance unknown. A supplied hook is still consulted even when acceptance is no. Neither generated IDs nor caller admission prove idempotency or remote inference termination.

Backoff uses full jitter below the smaller of the delay ceiling and exponential base delay. Retry-After seconds or HTTP dates set a minimum delay. Invalid and absent values are explicit; excessive minima cause refusal rather than shortening. A duration budget starts at the first failure; deadline is absolute monotonic time. Both constrain admission, backoff, and dispatch, including time consumed by request-capture and lifecycle callbacks. An admitted generation can finish later. No automatic whole-generation timeout is added; an explicitly configured Ollama timeout still applies. Clocks, sleeper, and jitter are injectable.

Asynchronous admission and cancellation

Use the native asynchronous gateway call in an event loop. Run the existing synchronous broker/session APIs with asyncio.to_thread when needed. Supply a fresh RecoveryCall for each concurrent call. recovery_call on the gateway is convenient for a single synchronous broker/session operation:

import asyncio

from mojentic.llm.gateways.models import LLMMessage
from mojentic.llm.recovery import RecoveryCall


async def complete(gateway):
    call = RecoveryCall()
    task = asyncio.create_task(gateway.complete_with_recovery(
        call=call, model="your-model", messages=[LLMMessage(content="Hello")]))
    # Your controller can call call.cancel() from another task or thread.
    return await task

Cancellation closes locally owned HTTP resources, admission, and backoff without a later attempt. The opted-in path raises RecoveryError with outcome cancelled, including when its task is cancelled. Closing the request does not establish that the provider terminated generation.

Anthropic structured recovery

Configure an AnthropicGateway with a RecoveryPolicy to enable Pydantic output through await gateway.complete_with_recovery(object_model=YourModel, ...) or broker.generate_object(messages, YourModel). Use a model supporting Anthropic JSON outputs. The request uses output_config.format with type: "json_schema", transformed by the SDK's public transform_schema helper. Local validation always uses the original Pydantic model, including constraints removed from the provider schema. The schema, system prompt, ordered user/assistant history, images and caller controls are encoded once before dispatch and admission. Retries resend those bytes even if admission changes the original inputs.

Migration: structured_recovery now reports true for Anthropic; older serialized records reporting false remain readable. Set recovery_policy=RecoveryPolicy() for a single attempt, or explicitly choose a larger bound for eligible recovery. Without a policy the ordinary SDK path retains its existing behavior; structured support is offered through the recovery path. Arbitrary configured JSON formats, tools and streaming remain unsupported. Malformed JSON and schema violations retain Pydantic ValidationError through RecoveryError.inspect_cause(); max_tokens termination retains LengthTerminationError. Capture failures retain CaptureError and its original inspectable cause. None of these failures replay.

anthropic-structured-v1 in anthropic_structured_spec.py runs the same scripted HTTP cases against the gateway provider role and broker consumer role. The conformance record identifies its hash and tested revision. These local checks do not verify the Anthropic service or external consumers; paired counterpart verification remains unfinished, and the coordinated publication hold remains.

Reports and explicit wire capture

generate_response and the async gateway return a response with a typed recovery_report. Ordinary responses also expose safe report metadata under response.metadata["recovery"]; streaming responses retain recovery_report. The report includes a final identity/progress and one history entry per failed wire attempt. Each history entry has explicit sensitive inspection methods. Pre-dispatch failure/cancellation records zero wire attempts and no attempt ID. Admission/backoff cancellation retains the prior failed attempt and adds a separate final cancellation classification. Logical IDs remain stable across retries; attempt UUIDs are distinct, unmasked correlation values.

observer receives immutable, payload-free LifecycleEvent models in order. wire_observer explicitly receives sensitive WireEvent objects for the actual request, response headers/status, and each raw response-body chunk. Request bytes include any images encoded once before sending. Capture includes HTTPX's outgoing headers and URL-based Basic authorization on the same request that is sent. The request hook precedes dispatch: it can prevent the request, so a capture alone does not prove a wire attempt occurred. Hooks must synchronously accept an event or raise; returning an awaitable is unsupported. The caller owns capture storage, access, redaction, and retention. The library never persists captures. A hook failure retains response evidence and a typed CaptureError cause, prevents success, and prohibits another request. Observed content, reasoning, or tool evidence is recorded before calling the body hook, even when nothing was delivered to the caller.

Typed policy, capture and length errors

Import RecoveryPolicyError, CaptureError and LengthTerminationError from mojentic.llm or mojentic.llm.recovery. Invalid policy construction, model_validate, model_validate_json (including invalid JSON) and model_validate_strings raise RecoveryPolicyError. Gateway constructors validate supplied policies; explicitly invoking recovery without a policy raises the same type before dispatch. This replaces Pydantic ValidationError for policy input errors. Frozen-policy assignment remains unsupported.

Failures during recovery still surface as RecoveryError, or a failed event containing that same error from complete_stream_with_recovery. Inspect its typed cause without parsing messages or reason strings:

from mojentic.llm import CaptureError, LengthTerminationError
from mojentic.llm.recovery import RecoveryError

try:
    broker.generate(messages)
except RecoveryError as error:
    cause = error.inspect_cause()
    if isinstance(cause, CaptureError):
        original_callback_exception = cause.inspect_cause()  # Sensitive, explicit access
    elif isinstance(cause, LengthTerminationError):
        pass  # The provider exhausted its output length limit
    raise

Migration: error.inspect_cause() and failure.inspect_cause() now return CaptureError for a wire-hook failure. The further inspect_cause() step returns the identical original callback exception. Reports, failed-attempt history, response bytes, request identities and observed/delivered progress remain available. Capture failure is terminal even before semantics or dispatch; an already delivered stream never triggers a resend.

LengthTerminationError subclasses ValueError. It distinguishes length from other unsafe stream finishes for Ollama, oMLX and OpenAI, including the OpenAI decoder's reuse of oMLX machinery. OpenAI structured completion uses it when rejecting a length finish, including through generate_object. Ordinary completions continue returning length metadata successfully; oMLX and Ollama structured parsing retain their existing behavior. Anthropic structured recovery uses the same type for max_tokens termination, even if the text parses. Anthropic tools and streaming remain unsupported. ChatSession has no structured-output operation. Raw single-turn event APIs retain their existing contract.

Default error strings, representations, traceback formatting and serialized reports omit callback messages and invalid policy values. Explicit private inspection, wire capture, successful output and application tracing remain sensitive. Source lines in tracebacks can themselves contain caller literals; the formatting guarantee cannot redact caller source code.

The executable Python binding python-typed-errors-v1 lives in src/mojentic/llm/gateways/typed_errors_spec.py and runs in the required pytest gate. It exercises in-repository consumers and the implementation against the same scripted HTTP cases, without independent SDK mocks. It is distinct from Elixir's typed-errors-v1; no shared cross-repository executable set or paired external consumer result is available. See CONFORMANCE-3.0.0.md for source identity, hash, named tests and unfinished coordination work. Publication remains on hold.

Cancellation requested by a request or attempt_started hook is checked before dispatch and produces no wire history. Header/body capture cancellation retains the received status, private headers, bytes and observed semantics, with no delivered output. An actual cancelled request emits attempt_failed before one terminal cancelled event. Admission/backoff cancellation retains the earlier failed request and adds a typed cancellation cause to the terminal failure.

Recovery uses a dedicated HTTP transport with retries and redirects disabled. No SDK or environment proxy retry layer runs underneath it. Provider text, credentials, tool arguments, and raw exception text are excluded from safe reports and lifecycle serialization. Unsupported, malformed and outbound-echoed provider codes and request IDs are omitted; validated non-echoed values are retained. A syntactically valid UUID can still echo an outbound credential or payload. Raw response headers remain available through Failure.inspect_headers() and RecoveryError.inspect_headers(); these methods are explicitly sensitive. Successful content, explicit application tracers, and explicit wire capture remain caller-owned sensitive data.

Capability boundaries

gateway.recovery_capabilities() exposes five documented endpoint facilities. The October 12 rule uses supported only for an explicitly documented facility, unsupported when the endpoint reference describes no such facility, and unknown for absent or ambiguous documentation. Inspection date: 2026-10-17.

Adapter / inspected completion endpoint Local cancellation Remote cancellation Request status Idempotency Exact-attempt termination
Ollama: POST /api/chat unknown unsupported unsupported unsupported unknown
oMLX: POST /v1/chat/completions unknown unsupported unsupported unsupported unknown
OpenAI: POST /v1/chat/completions unknown unsupported unsupported unsupported unknown
Anthropic: POST /v1/messages unknown unsupported unsupported unsupported unknown

Each returned value has evidence[name] containing endpoint, source, inspected_on and basis. The local cancellation documentation is silent: RecoveryCall.cancel() still cancels local work, but implementation behavior alone does not meet the amendment's documentation requirement for supported. The inspected endpoint references describe no remote cancel, in-flight status lookup or inference deduplication facility. OpenAI stored completions are completed output, and Responses cancellation and Batch APIs are separate endpoints. Model unload and aggregate activity are not exact-attempt evidence. No local connection closure proves remote termination. These documentation decisions do not change retry admission or any completion operation support. Custom endpoint deployments have not been independently inspected; these reports describe the documented provider endpoint, not proxy extensions.

The existing operation support remains:

Adapter / operation Recovery in this increment Remote cancellation / status / termination / idempotency
Ollama ordinary and structured completion Opt-in, loopback tested Unsupported / unsupported / unknown / unsupported
Ollama tool-capable streaming Opt-in recovery; explicit typed terminal outcomes and attempt evidence Unsupported / unsupported / unknown / unsupported
Ollama raw stream events Existing single-turn API; tools rejected Recovery configuration does not enable retries
oMLX ordinary and structured completion Opt-in, typed schema failures and retained format warnings Unsupported / unsupported / unknown / unsupported
oMLX tool-capable streaming Opt-in recovery; safe finish plus [DONE] required Unsupported / unsupported / unknown / unsupported
oMLX raw stream events Existing single-turn API; recovery policy is ignored Recovery configuration does not enable retries
OpenAI ordinary / structured / tool-capable streaming Opt-in owned HTTP; model controls adapted before immutable encoding Unsupported / unsupported / unknown / unsupported
oMLX / OpenAI structured streaming Unsupported; rejected before dispatch No recovery claim
OpenAI raw stream events Existing single-turn API; recovery policy is ignored Recovery configuration does not enable retries
Anthropic ordinary and structured completion Opt-in owned HTTP; ordered user/assistant history; immutable schema and controls; validated Pydantic objects Unsupported / unsupported / unknown / unsupported
Anthropic tools / streaming Unsupported; tool recovery requests fail before dispatch, no streaming entrypoint No new provider facility
Embeddings and realtime voice Outside completion recovery scope No recovery claim

The existing LLMMessage and Ollama adapter do not represent native reasoning in outgoing history. Recovery preserves the current encoded message history exactly and returns native reasoning from successful responses; it does not add native history support or change reasoning-disabled behavior. Replay after observed semantics, cross-port parity, live inference, and experiment efficacy remain unverified and pending. See the repository's RECOVERY-CONFORMANCE.md for actual assertion mappings.

Migrating from the preserved first recovery increment

Capability attribute access and all previous serialized fields retain their values. inference_termination remains the field name for exact-attempt termination. model_dump() and JSON now add local_cancellation and evidence. Update strict dictionary snapshots and consumers that forbid unknown keys. To send the old representation to a strict older reader, explicitly project it:

capabilities = gateway.recovery_capabilities()
legacy = capabilities.model_dump(exclude={"local_cancellation", "evidence"})
local_evidence = capabilities.evidence["local_cancellation"]
print(capabilities.local_cancellation, local_evidence.source)

New readers accept old payloads with the two additions omitted; the defaults are local cancellation unknown and empty evidence. Missing evidence does not establish documented support. Facility fields use the three-value vocabulary; exact-attempt termination still accepts only unknown. The local executable contract recovery-capabilities-v1 verifies old field projection, optional new fields, new Python/JSON round trips, invalid-value rejection and each facility through every gateway. Paired downstream verification remains open; this migration is not a claim that an older strict reader accepts new payloads.

The opt-in APIs and immutable payload policy are retained. Consumers of provider_request_id now receive exact validated provider IDs when supported. Use private header inspection for omitted or untrusted values; client logical/attempt UUIDs remain separate correlation metadata. Capture consumers now receive URL Basic authorization as sent and must protect it along with bodies. Cancellation consumers should expect an attempt_failed event for an active cancelled wire request before cancelled. Use the report's wire count/history rather than counting request-capture or attempt_started callbacks, since a callback can prevent dispatch. The legacy SDK path and ordinary finish behavior are unchanged. The next streaming increment described below adds recovery only to the tool-capable path.

Directory tool exception compatibility

CreateDirectoryTool continues to accept a FilesystemGateway directly:

from mojentic.llm.tools.file_manager import CreateDirectoryTool, FilesystemGateway

tool = CreateDirectoryTool(FilesystemGateway("/your/sandbox"))
result = tool.run("nested/directory")

Successful creation, including an existing directory, returns the same success string. Sandbox violations, permission failures and other OS errors retain their error strings. Unexpected programming failures propagate with their original exception object instead of being converted to strings. Callers that previously treated every failure as a returned string should let unexpected exceptions reach their application error boundary. The focused regression uses a real filesystem gateway and a failing path protocol object; it verifies exception identity and no filesystem mutation. This documents the preserved lint-cleanup correction and adds no new runtime behavior or completion-recovery guarantee.

Migrating tool-capable streams

Configuring recovery_policy now also opts OllamaGateway.complete_stream, LLMBroker.generate_stream, and ChatSession.send_stream into recovery of each individual completion. The synchronous APIs continue yielding their existing chunks or strings. They raise RecoveryError on interruption or exhaustion; they never commit partial session text as a completed assistant reply. Completed tools execute once. Subsequent completion retries preserve that tool's result, and transport attempts do not consume additional tool depth.

For native asynchronous consumption, use the additive typed stream API:

from contextlib import aclosing

from mojentic.llm import LLMMessage, StreamOutcome
from mojentic.llm.recovery import RecoveryCall


async def consume(gateway):
    call = RecoveryCall()
    async with aclosing(gateway.complete_stream_with_recovery(
        call=call, model="your-model", messages=[LLMMessage(content="Hello")]
    )) as events:
        async for event in events:
            if event.kind == "content":
                print(event.text, end="")
            elif event.kind == "completed":
                assert event.outcome is StreamOutcome.SUCCEEDED
                return event.response
            elif event.kind == "failed":
                # report is safe; inspecting error causes or raw bytes is sensitive.
                raise event.error

Events include content, reasoning, tool fragments, frame progress, terminal metrics, and exactly one completed or failed outcome. Tool fragments are observations; only a successful completed response provides executable tool calls. Every event carries the logical/attempt identity. frame_index starts at one for each attempt's decoded frames. StreamFrameProgress uses UTF-8 byte lengths; StreamMetrics preserves Ollama's counter names and nanosecond durations, with missing values left None. Throughput is calculated only from reported eval count and nonzero eval duration. Model and finish strings remain available in explicit metrics inspection and the completed response, but are excluded from safe metric formatting and serialization because providers can echo secrets. Semantic event fields and responses likewise require explicit inspection.

A structurally valid length terminal emits frame progress, metrics, then failure. It delivers no semantics or completed tools from that frame. Malformed frames emit no telemetry. Any observed content, reasoning, or tool fragment prevents replay, including when capture fails before delivery. Keepalive-only progress can recover after explicit admission. Capture failures remain terminal and retain the original exception, raw bytes, private headers, and observed evidence.

Recovery streams require newline-terminated NDJSON frames. At clean EOF or HTTP truncation, any unfinished frame is a protocol failure and cannot be replayed, even when admission would allow it. A complete JSON object without its trailing newline is still unfinished: its semantics may be observed privately but are never delivered as content, reasoning, executable tools, or success. Exact response bytes and headers remain inspectable; HTTP truncation retains its original typed transport cause while the failure category stays protocol.

EOF between complete frames without terminal proof is a transport interruption. Complete keepalive-only frames permit another attempt only after explicit caller admission; absent or rejected admission sends no second request. Previously observed semantics still prevent replay. These outcomes propagate through complete_stream_with_recovery, LLMBroker.generate_stream, and ChatSession.send_stream. Broker and session callers receive RecoveryError on failure and do not execute tools or store a completed assistant message from an unfinished frame. Ordinary and structured permanent HTTP status handling retains its existing behavior.

The stream owns its HTTP/admission tasks independently of consumer pacing. call.cancel() closes locally owned resources even while consumption is paused; cancellation wins over buffered terminal telemetry and completed responses. Close iterators when abandoning consumption. Socket closure is local evidence, not proof of remote inference termination. Local cancellation, request status, and idempotency capabilities retain the limits in the table above.

complete_stream_events and generate_stream_events remain single-turn raw compatibility APIs and reject tool use. Recovery configuration does not alter them. Without a recovery policy, tool-capable SDK streaming retains its existing finish handling and provider error types. Native reasoning history that the Python message model cannot represent remains an explicit gap; supported outgoing reasoning controls, ordinary/structured recovery and finish handling are preserved.

oMLX migration

Use the same policy, admission and cancellation objects with OMLXGateway:

from mojentic.llm.gateways.omlx import OMLXGateway

omlx = OMLXGateway(
    host="http://localhost:8000",
    recovery_policy=RecoveryPolicy(max_attempts=3, admission=admit),
)
omlx_session = ChatSession(LLMBroker("your-model", gateway=omlx))

complete, complete_with_recovery, complete_stream and complete_stream_with_recovery support request recovery. Existing broker and session APIs use these public completion paths. generate_response returns tools to its caller; it does not execute a tool loop. Structured streaming remains unsupported. complete_stream_events and broker generate_stream_events retain the existing single-request parser, finish rules, error events and no-tool API. Their legacy error details can contain provider text; use the recovery entrypoints for safe typed errors and recovery-event serialization.

With recovery disabled, the configured transport, timeout, settings/authentication, validation behavior and response-format warning logging remain unchanged. Opt-in completion recovery uses its own HTTP transport, with hidden retries, redirects and environment proxies disabled. Custom OMLXTransport instances continue to serve legacy, embedding and model-management calls; they are bypassed for opt-in completion recovery. No remote cancellation, request-status or idempotency API is claimed.

An opt-in oMLX request retains connect/write/pool timeouts but removes the active response read timeout. A recovery budget constrains waiting and resending rather than healthy generation. Use RecoveryCall to cancel active local HTTP ownership. This intentional opt-in difference does not alter legacy timeouts or prove remote termination. Model loading, unloading, listing and embeddings are outside scope.

Recovery preserves reasoning_effort, native returned reasoning, supported message history, tool arguments, schema and generation controls. The shared message model cannot represent native reasoning in outgoing history; no support is added here. Ordinary finish reasons (including length) are preserved. Recovering streams require stop or tool_calls followed by [DONE]. Observed reasoning, content or tool fragments prevent replay even before caller delivery. Keepalive bytes alone are raw progress and still require explicit admission before resending.

Reported usage, including nested cache details and timing values, is preserved on the completed response and event.metrics.usage. Arbitrary provider usage keys, model and finish strings are excluded from safe recovery-event serialization and representation; explicit access and successful responses are sensitive. Structured format warnings remain in successful response metadata, but the opt-in path does not log untrusted warning header values. Private inspection and wire capture retain exact response headers and raw bytes, including partial error bodies.

Migrating cloud completions

OpenAIGateway accepts the same recovery_policy and recovery_call options as local gateways. complete and complete_stream route through owned HTTP when a policy is present. LLMBroker.generate, generate_response, generate_object and generate_stream, and ChatSession.send and send_stream retain their existing return types. Asynchronous callers use complete_with_recovery or complete_stream_with_recovery; use aclosing around an asynchronous iterator when stopping early. A completed tool followed by a failed completion executes once, and retrying that completion does not consume another tool round. generate_response returns tool requests without executing them.

from mojentic.llm import LLMBroker
from mojentic.llm.gateways.openai import OpenAIGateway
from mojentic.llm.recovery import RecoveryPolicy

# API key and endpoint continue to use the gateway's existing environment defaults.
gateway = OpenAIGateway(recovery_policy=RecoveryPolicy(
    max_attempts=3, base_delay=0.1, delay_ceiling=5, budget=30,
))
broker = LLMBroker("your-openai-model", gateway=gateway)
# broker.generate(messages) / broker.generate_object(messages, YourModel)
# broker.generate_stream(messages, tools=your_tools)

Cloud providers may retry eligible failures without an admission hook, matching the reviewed Rust OpenAI distinction. Supplying a hook still makes recovery wait for its explicit decision. Ollama and oMLX continue requiring explicit admission for ambiguous local requests. Admission never confirms remote termination or idempotency. A cloud resend can incur another provider request and charge.

OpenAI adapts temperature, token limits, reasoning effort and tool support before encoding. Pydantic schemas use the SDK's public strict-schema conversion; the response content is validated into the requested model. Structured parsing retains the SDK requirement for strict function tools. A successful tool-only turn with content=None, finish_reason="tool_calls", and valid tool arguments returns object=None and preserves the exact tool ID, name and arguments. The caller can then execute that tool and continue its conversation. Non-null structured content still validates against the schema, even when tools are also returned; malformed arguments and unsafe structured finishes remain failures. This combined schema/tool shape is supported by OpenAIGateway.complete and complete_with_recovery; LLMBroker.generate_object offers no tools, and ChatSession has no structured-output entrypoint. Ordinary broker/session callers retain tool execution across retries without repeating completed tools. oMLX structured completion still requires non-null JSON content. Schema validation and unsafe structured finishes become typed recovery failures with privately inspectable original causes. Ordinary finish handling stays unchanged. Streaming requires a safe finish and [DONE]; once reasoning, content or a tool fragment has been observed, interruption never starts another request. A failed capture hook can have observed but undelivered semantics. Provider model, usage and finish telemetry remain inspectable; missing timing/token counters are not invented. Raw complete_stream_events / generate_stream_events continue their single-turn compatibility behavior and ignore the recovery policy.

AnthropicGateway(api_key=..., recovery_policy=...) supports ordinary complete / complete_with_recovery, LLMBroker.generate / generate_response, and ChatSession.send. Both SDK and recovery completions retain user and assistant history in its original order, including empty assistant content. System messages are joined with spaces into the separate system field; user images keep their existing serialization. First text-block response mapping is retained. A second ChatSession.send includes the first assistant answer, and recovery retries freeze and resend the entire history without rebuilding it. It has no streaming implementation. Nonempty tools or an object_model, or a configured JSON output format, produce a pre-dispatch RecoveryError whose private cause is NotImplementedError; recovery_capabilities() reports structured and streaming recovery as false. This increment adds no Anthropic tool execution or structured output capability. The SDK path sends the existing temperature value through extra_body for compatibility with locked Anthropic SDK 1.15.0.

The local anthropic-history-v1 specifications in src/mojentic/llm/gateways/anthropic_history_spec.py exercise both paths against loopback HTTP. They do not verify live vendor acceptance or a shared contract with a downstream consumer; see the partial CONFORMANCE-3.0.0.md decision map in the repository root.

Recovery bypasses both SDKs' HTTP transports. SDK transport customizations do not configure the owned recovery connection. It freezes literal UTF-8 bytes once, disables hidden retries, redirects and environment proxy discovery, preserves connect/write/pool timeout values, and removes the active response read timeout. Budget/deadline limits govern recovery decisions rather than total generation length. Use a fresh RecoveryCall to cancel an operation explicitly.

The opt-in path raises RecoveryError, replacing SDK transport/status errors at the public completion boundary. Safe errors, reports, lifecycle events and default stream serialization omit credentials, payload echoes, provider text and raw exception messages. Explicit inspect_cause, inspect_response, inspect_headers and wire_observer access retain exact sensitive evidence. Protect captures as credentials and conversation data. Capture consumers can inspect immutable request bytes and actual headers, including authentication; the library never stores those captures automatically. See RECOVERY-CONFORMANCE.md for executed entrypoints, assertion scope and retained external evidence.

The default OpenAI raw stream-event transport disables SDK retries and HTTP redirects, so a redirect is reported as an error after one send. Recovery remains disabled for these raw event APIs.

Validated provider metadata (October 11 correction)

Public failure reports, per-attempt history and lifecycle evidence retain exact accepted provider_request_id and provider_code values. Recovered responses retain failed-attempt metadata in recovery_report.history; ordinary responses also expose the serialized report in metadata["recovery"]. Broker and session errors propagate the same report. Streaming responses use recovery_report.

Ollama, oMLX and OpenAI read x-request-id. Anthropic prefers request-id, falling back to x-request-id only when the former header is absent. Only a single UUID spelling (canonical, simple hexadecimal, braced or UUID URN), optionally prefixed with req_, is accepted. Arbitrary IDs, malformed UUIDs and combined or duplicate header values are omitted. A valid spelling alone does not establish trust: IDs and their unprefixed forms occurring in outbound headers, immutable payload bytes, decoded JSON strings or credentials are also omitted. This includes URL Basic authentication and explicitly supplied Basic authorization. Matching is case insensitive and conservative; legitimate collisions are omitted.

Complete JSON errors expose only recognized error.code strings: rate_limit_exceeded, server_error, invalid_request_error, invalid_api_key, model_not_found, insufficient_quota, and overloaded_error. Anthropic prefers error.type, additionally recognizing rate_limit_error, authentication_error, permission_error, not_found_error, and api_error. Recognized codes echoed from outbound data are omitted too. No error-message text becomes public metadata. For streaming error frames, extraction occurs before capture or delivery callbacks. A truncated body retains numeric status and accepted header metadata; it cannot supply a code until a complete error object is available. Permanent statuses still prevent retries. This correction does not add unsupported operations.

These safeguards constrain copying provider data into public evidence; they do not certify that any provider-supplied UUID is an authentic correlation ID or sanitize arbitrary observer code. Explicit inspect_headers, inspect_response and inspect_cause retain original sensitive evidence, as does opt-in wire capture. Protect those channels separately. Formatted errors, serialized reports and lifecycle events omit rejected values. Assertions and external review/capture locations are recorded in RECOVERY-CONFORMANCE.md.