Files
transcription/.github/instructions/providers.instructions.md
T
Jim Lancaster 27ec81ca5b
Quality Gate / gate (push) Successful in 2m37s
Implement Rec - Phase 3 complete
2026-09-02 16:15:33 -05:00

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.md
  • docs/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 import services, db, ui, or api.
  • Provider specifics — headers, model slugs, payload shapes, SDK types, error classes — stop here. Callers receive TranscriptionResult and ProviderError subclasses 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 TranscriptionProvider protocol in base.py. Failed-call evidence is returned through the caller-owned ProviderCallEvidence sink passed to transcribe(), so evidence stays scoped to one invocation instead of living on mutable adapter instance state.
  • TranscriptionResult, RequestManifest, and TransportEvidence are extra="forbid" and frozen. Add a field to the contract rather than smuggling data through an untyped dict.
  • Evidence contracts in evidence.py are 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. _CapturingAsyncClient exists for this; do not replace it with a post-parse model_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_v42_evidence.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 assuming response.content is 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_headers and the SAFE_RESPONSE_HEADERS allowlist. 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_media exists 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_states distinction between omitted, null, and value is deliberate.
  • Timeout budget, retry policy, source reference, and SoftwareContext versions.
  • Manifest digests use canonical_json_bytes. Do not hash a plain json.dumps(); key order and separators must stay deterministic or digests become uncomparable.

Client Lifecycle and Async Safety

  • Reuse one pooled AsyncClient per 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.CancelledError untouched — do not convert cancellation into a provider error.

Failure Handling

  • Raise ProviderAuthError for authentication, ProviderResponseError for malformed or unusable responses, and ProviderError otherwise.
  • Always attach request_manifest, transport_evidence, and an accurate failure_phase to raised errors. failure_phase must 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:

  1. Update docs/invariant/ai_evidence_and_provenance.md only if the durable preservation contract itself is changing — that revision is deliberate and reviewed, not incidental.
  2. Update docs/schema.md when persisted evidence fields change.
  3. Update or add tests in the same change (tests/providers/, tests/test_v42_evidence.py).
  4. Bump the affected evidence schema_version when a field's meaning changes.