Story 1650: the egress half of a thinking level — send the effort, pin the route, never silently cheat

Why this exists (the catastrophe it prevents). A thinking level resolves to {model, reasoning_effort} — Wave-0 built that resolver. But the resolved effort never reached the wire: the OpenRouter adapter sent the model and the privacy knobs and dropped the effort on the floor. Worse, the request had no allow_fallbacks:false, so the broker was free to substitute a cheaper or different provider to satisfy availability or cost — exactly the silent escape a legal-grade ZDR route must forbid. And there was no answer to the awkward real-world case: the frontier model a firm's Ultra Max pill wants may not be eligible under that firm's residency/ZDR policy. Without a rule, you either (a) crash the turn, or (b) quietly serve a different model and tell no one. Both are wrong. This Story wires the effort through per-model (some models have no effort knob — and you must never swap the model to fit an effort field), pins allow_fallbacks:false on every call, and formalizes degrade-with-notice within compliant routes, fail-closed if none — with the exact model/effort/degrade facts captured in audit provenance while counsel sees only a tier hint.

Scope & doctrine. Wave-1 T3 of the Agentic Interaction Epic (#1646), after Wave-0 contracts (#1647/#1648) merged. Grounded in RFC-APB-004 §D/§F (ACCEPTED) + pramaan-docs ADR-116 (quality-first ZDR), ADR-120 (resolver choke-point, adapter is the only provider caller, fail-closed), ADR-129 (level → model+effort). One repo (pramaan-functions). The adapter stays a config-driven mechanism: model slugs and effort capability are yaml, swappable with no deploy.

🛰️ Satellite: a level's effort + desired model meet the firm's compliant route

  resolve_thinking(level)            resolve_inference_route(firm)   ← ADR-120 choke-point
    → {desired_model, effort}          → ResolvedRoute (best ELIGIBLE/ZDR/residency
            │                              compliant model, or FAIL-CLOSED if none)
            │                                       │
            └──────────────►  plan_egress  ◄────────┘
                                  │
        ┌─────────────────────────┼───────────────────────────────┐
        ▼                         ▼                               ▼
  EFFORT decision           DEGRADE notice                   the COMPLIANT route
  (per SERVED model)        (desired ≠ served?)              is authoritative —
  ignored → omit            counsel: tier hint only          the plan never widens it
  native  → send            audit:   exact models
  max     → clamp to high
                                  │
                                  ▼
                    OpenRouterAdapter.build_request_body
            provider: { allow_fallbacks:false, zdr:true,
                        data_collection:"deny", only:[host] }   ← pinned EVERY call
            reasoning: { effort:<value> }  ← ONLY if served model supports it
                                  │
                                  ▼
                    InferenceResult ──► inference_provenance row
              (served model/provider/cost + requested/effective effort
               + degraded flag + exact desired→served audit reason)

The key idea: the resolver still owns the trust boundary. It already picks the single best model the firm is allowed to use, and fails closed when nothing is eligible. This Story does not fork that decision — it reconciles the level's desired model against the route the resolver minted: same model → serve clean; different model → serve the compliant one and attach a notice; no route at all → the resolver already raised route_denied. Effort capability is consulted for the model that actually serves.

✈️ Airplane: the change surface

FileRole
src/shared/inference/effort_capabilities.yamlNew config (AC3). Per-model effort_mode: native | mapped | ignored + a fail-safe default_mode: ignored. The single source for "does this model have an effort knob." A model swap/retune is a yaml edit, no deploy.
src/shared/inference/effort.pyNew egress planner. resolve_effort(model, effort) (send/omit/clamp, no-op-safe), degrade_notice(desired, route) (counsel hint vs audit reason), and plan_egress(...) — the one call the broker makes to get effort + degrade together. Loads + validates the capability yaml fail-closed.
src/shared/inference/openrouter_adapter.pybuild_request_body now pins provider.allow_fallbacks:false on every call (AC2) and adds reasoning.effort only where the served model supports it (AC1). complete() threads the effort label, stamps requested/effective effort on the result, and enriches the egress span with the low-cardinality effective effort (ADR-124 T2).
src/domains/inference_provenance/entity.py + migration 0125Four new audit columns: requested_effort, effective_effort, degraded (bool), degrade_reason. Append-only RLS table — plain nullable adds + one defaulted bool, no policy/trigger/constraint change. Clean upgrade↔downgrade round-trip.
src/shared/inference/provenance.pyThe writer (and the _from_result convenience) persist requested/effective effort + the degrade flag/reason. Effort comes off the result; the degrade is supplied by the broker (the adapter does not know the level's desired model).
TestsUnit: capability default, send/omit/clamp, degrade-vs-no-degrade, counsel-hint-names-no-model, plan_egress totality; adapter allow_fallbacks/reasoning body + effort provenance + span attr; migration static + reversibility. Integration: the effort/degrade columns persist and read back against a freshly seeded firm (system boundary).

🔬 Microscopic: three rules that keep the egress honest

1. Never downgrade the model to fit an effort field (AC1). OpenRouter's chat API accepts reasoning.effort ∈ {minimal, low, medium, high}. PRAMAAN's internal max (the max/ultra_max levels) is not a valid wire value — it clamps to high, and the clamp is recorded so audit sees requested=max, effective=high. A model with no effort knob is ignored: the field is simply omitted — the model is untouched. The point: the model choice carries the quality step; effort only refines it.

def resolve_effort(model, requested_effort):
    mode = effort_mode_for(model)                  # default-safe: ignored
    if requested_effort is None or mode is IGNORED:
        return EffortDecision(send=False, ...)      # OMIT — never swap the model
    normalized = {"low":"low","medium":"medium","high":"high","max":"high",...}.get(...)
    if normalized is None:                          # unknown label → omit, not garbage
        return EffortDecision(send=False, ...)
    return EffortDecision(send=True, openrouter_effort=normalized,
                          requested_effort=requested_effort,   # audit sees the clamp
                          effective_effort=normalized)

2. Pin the route on every call (AC2). provider.allow_fallbacks:false goes in unconditionally, alongside the existing zdr, data_collection:"deny", and the only:[host] allowlist. A legal-grade route must never let the broker substitute an unlisted provider to save money or dodge an outage — a cost/availability fallback is a policy escape. The existing fail-closed guard (a brokered route with no provider.only refuses to build) is unchanged.

3. Degrade within compliant routes, fail closed if none (AC4) — and counsel sees a tier hint, not a model (AC5/§F). The resolver already returns the best allowed model or raises route_denied. So degrade is a pure comparison: is the model the resolver gave us the one the level wanted? If not, the firm's compliant route is the best allowed model — serve it, and emit a notice whose counsel-facing copy names no model; the exact desired→served pair lives only in degrade_reason (audit).

def degrade_notice(desired_model, route):
    served = route.exact_model
    if desired_model == served:
        return None                                 # no degrade
    return DegradeNotice(
        counsel_hint="The requested model is not available on your firm's "
                     "compliant routes; using the best available model.",  # no model name
        desired_model=desired_model, served_model=served,                   # audit-only
        audit_reason=f"compliant-route degrade: desired {desired_model!r} ...")

Fail-closed is not re-implemented here — it is inherited: when current_processors returns nothing eligible, resolve_inference_route raises PramaanInferenceRouteDeniedError before plan_egress is ever reached. Degrade only ever runs when a compliant route exists.

Why the effort capability is checked against the served model. plan_egress resolves effort for route.exact_model — the model that will actually run — not the desired one. So a degrade (desired frontier → served Opus) still picks up the served model's effort knob correctly, instead of asking "does the model we didn't use support effort?"

Verification (system boundary, not component boundary). Inference unit + adapter + effort suites green (74 passed in the inference slice; 971 passed across tests/unit — the 2 failures are a pre-existing, unrelated test_firm_support_staff issue that also fails on the clean base commit). Migration 0125 applied to a freshly provisioned Docker Postgres with a clean upgrade → downgrade → upgrade round-trip and alembic check drift-free. The effort/degrade provenance columns are proven to persist and read back through record_inference_provenance_from_result against a newly seeded firm with no policy row (the empty first-run state, default-Global) — not a fixture, not a mock. make openapi regenerated with no contract drift (no route/DTO change). ruff clean; changed files type-clean (modulo the repo-wide missing types-PyYAML stub that already affects thinking.py). ADR-124: no new executable boundary — the egress span gains one low-cardinality effort attribute (T2 enrichment); the new pure helpers are covered by the existing adapter span.

One-sentence recall: a thinking level's effort now actually reaches the wire — sent only where the model supports it and never by swapping the model — while allow_fallbacks:false pins the compliant route, a desired-but-ineligible model degrades to the best allowed one with a counsel-facing tier hint (exact models in audit), and nothing eligible still fails closed.