generated from john/python-template
7.0 KiB
7.0 KiB
description, applyTo
| description | applyTo |
|---|---|
| Provider adapter rules for evidence capture, secret safety, and client lifecycle. | src/transcription/providers/**/*.py |
Provider Adapters
Primary references:
docs/invariant/ai_evidence_and_provenance.md(canonical; provider adapters own provider-boundary evidence capture)docs/architecture.mddocs/schema.md
The provider layer is where an external API becomes application data. It is also the only place that can capture what actually crossed the wire — once a response reaches a service, the evidence it did not preserve is gone permanently. Treat capture correctness as the primary job of this layer and text extraction as secondary.
Layer Boundary
- Adapters may depend on
transcription.config,transcription.providers.*, the HTTP client, and the provider SDK. They must not importservices,db,ui, orapi. - Provider specifics — headers, model slugs, payload shapes, SDK types, error classes — stop here.
Callers receive
TranscriptionResultandProviderErrorsubclasses only. - Adapters raise provider/domain exceptions. They must not emit user-facing text, notifications, or remediation wording; that translation belongs to services and UI. See error-handling instructions.
- Adapters do not persist. They return evidence; services decide what is written and when.
Enforced by tests/test_provider_boundaries.py.
Contract Surface
- Every adapter satisfies the
TranscriptionProviderprotocol inbase.py. Failed-call evidence is returned through the caller-ownedProviderCallEvidencesink passed totranscribe(), so evidence stays scoped to one invocation instead of living on mutable adapter instance state. TranscriptionResult,RequestManifest, andTransportEvidenceareextra="forbid"and frozen. Add a field to the contract rather than smuggling data through an untyped dict.- Evidence contracts in
evidence.pyare versioned (schema_name+schema_version). A change to the meaning or shape of a captured field requires a version bump, not a silent redefinition — stored evidence must keep its original meaning.
Transport Evidence
The rules below implement docs/invariant/ai_evidence_and_provenance.md §3.4-3.5. That document
wins if this file drifts from it.
- Capture the response body at the HTTP boundary, before SDK parsing, so fields the SDK does
not model are not lost.
_CapturingAsyncClientexists for this; do not replace it with a post-parsemodel_dump()and call the result transport evidence. - Reset per-call capture state at the start of every call. Without it, a connection failure can
attach the previous call's response as evidence for this one. Guarded by
tests/test_evidence_provenance.py::test_openrouter_does_not_reuse_prior_response_on_connection_failure. - Keep transport capture scoped to the call, not the adapter instance. Concurrent
transcribe()calls on one adapter must not be able to overwrite each other's response evidence. - Handle the streamed-body case (
httpx.ResponseNotRead) rather than assumingresponse.contentis always available. - When no response arrives — timeout, DNS, connection reset — emit
TransportEvidence(response_received=False). Absence of a response is itself evidence and must be explicit, never an empty body or a missing record. - Preserve safe response evidence for unsuccessful calls too, whenever a response was received.
- Never relabel an SDK snapshot or normalized metadata as transport evidence, and never backfill it into an execution that predates capture.
Secret Safety
- Persist response headers only through
filter_safe_response_headersand theSAFE_RESPONSE_HEADERSallowlist. Allowlist, never denylist: capture-then-redact is prohibited, because an unknown header is unsafe by default. - Adding a header to the allowlist is a deliberate evidence decision. Confirm it carries no credential, cookie, or session material, and state why it is needed for correlation, content interpretation, rate-limit diagnosis, or audit.
- API keys,
Authorization, and cookies must never appear in a manifest, evidence record, log line, or exception message. - The request manifest references source content by identity (digest, size, media type, page).
Do not duplicate base64 source bytes into it —
_replace_embedded_mediaexists for this.
Execution Specification
The manifest must let a reader reconstruct what was asked, per invariant §3.3:
- Provider, requested model, full effective prompt text, and prompt digest.
- Every explicitly supplied parameter, and — separately — which optional parameters were
omitted. Omission is not the same as a null value or an assumed provider default; the
optional_parameter_statesdistinction betweenomitted,null, andvalueis deliberate. - Timeout budget, retry policy, source reference, and
SoftwareContextversions. - Manifest digests use
canonical_json_bytes. Do not hash a plainjson.dumps(); key order and separators must stay deterministic or digests become uncomparable.
Client Lifecycle and Async Safety
- Reuse one pooled
AsyncClientper adapter instance; do not construct a client per request. - Accept an injected client so tests can drive the adapter without network access.
- Derive timeouts from
Settings(worker_provider_timeout_seconds) rather than hard-coding, and keep the client timeout aligned with the configured budget so the SDK cannot expire first and hide the real failure. - Implement
aclose()and release pooled resources. An adapter that creates a client owns closing it; one given a client must not close a caller-owned resource it did not create. - Never block the event loop. Offload CPU-bound work (hashing large payloads, image encoding) with
asyncio.to_thread. - Propagate
asyncio.CancelledErroruntouched — do not convert cancellation into a provider error.
Failure Handling
- Raise
ProviderAuthErrorfor authentication,ProviderResponseErrorfor malformed or unusable responses, andProviderErrorotherwise. - Always attach
request_manifest,transport_evidence, and an accuratefailure_phaseto raised errors.failure_phasemust distinguish a received-but-failed response from a call that never reached the provider. - Validate responses with Pydantic rather than indexing into raw dicts.
- Invalid optional metadata (for example unparsable token counts) must not discard an otherwise valid transcript. Degrade the metadata, not the result.
Contract Sync Rule
If capture behavior, evidence schema, or the header allowlist changes:
- Update
docs/invariant/ai_evidence_and_provenance.mdonly if the durable preservation contract itself is changing — that revision is deliberate and reviewed, not incidental. - Update
docs/schema.mdwhen persisted evidence fields change. - Update or add tests in the same change (
tests/providers/,tests/test_evidence_provenance.py). - Bump the affected evidence
schema_versionwhen a field's meaning changes.