# PLAN — ADR-132 rollout: universal AI provider priority across all 360 DLM PWAs

**Authored:** 2026-08-06 (DL session, Opus). **Governs:** ADR-132. **Status:** DRAFT — execution
gated on hkl go-ahead. **Standing constraints:** dev-first (`dev.srv1111289.hstgr.cloud`), NO prod
cutover without explicit hkl sign-off (several targets are prod-shared). Model-tiering per
`feedback_plan_first_model_tiering`: each chunk tagged with the model tier that should execute it via
the Agent tool; orchestrator stays Sonnet; findings logged back into this doc as chunks complete.

**Agent-dispatch companion:** docs/EXEC-ADR-132-agent-dispatch.md maps every chunk to a specific model agent (Opus/Sonnet/Haiku) with a self-sufficient brief + a ready-to-run orchestration script.

## Target chain (from ADR-132)

```
T1 Claude OAuth ($0)    claude-proxy:8788  /generate (text)  /vision (image)   [JSON, tokenless, internal]
T2 Gemini OAuth ($0)    gemini-proxy       /generate (text, TO BUILD)  /analyze (image) [multipart+Hub]
T3 OpenRouter (paid)    openrouter.ai      free models first, then paid
T4 Anthropic REST       api.anthropic.com  approval-gated (ADR-062 §5 need_api_permission 503)
T5 Ollama (last resort) ollama:11434       only where already wired; never the sole tier
```

Two variants in the shared helper: `chain_text()` and `chain_vision()`. ADR-062 §2–§6 (review gate,
UX, 503 contract, degradation) unchanged — this plan only changes provider set + order.

---

## Phase 0 — Central prerequisites (do FIRST; everything else depends on these)

| # | Task | File(s) | Model | Notes |
|---|---|---|---|---|
| P1 | **Build Gemini text endpoint** `/gemini-proxy/generate {system,user,model}` running `gemini -m <model> -p '<system+user>'`, `GEMINI_API_KEY` popped (mirror `analyze_via_cli` minus `@file`); return `{ok,result}` | `/opt/gemini-proxy/main.py`, `gemini.py` | Sonnet | Restart gemini-proxy after. Live-test: text prompt -> JSON back, confirm `tier=gemini-cli/oauth` in logs (not vertex). |
| P2 | **Build shared chain helpers** `ai_chain.py` + `ai_chain.mjs`, variants `chain_text`/`chain_vision`, 5-tier order, ADR-062 §5 response shape + `need_api_permission` 503, per-tier structured logging (`mode:<tier>`) | new `/opt/ai-chain/ai_chain.py`, `ai_chain.mjs` (bind-mount into each sidecar) | Sonnet/Opus | This is the contract-defining artifact — review carefully. Include env toggles per tier (e.g. `AI_DISABLE_CLAUDE`, `AI_DISABLE_GEMINI`). |
| P3 | **Force-fail test harness** — a script that points each tier URL at a dead port in turn and asserts the next tier answers, per variant | new `/opt/ai-chain/verify_chain.py` | Sonnet | Reusable per-proxy acceptance gate (Phase 2). |

**Gate G0:** P1+P2+P3 done and self-tested before touching any consumer proxy.

### Phase 0 — DONE in dev (2026-08-06)
- **P2 shared helper** `/opt/ai-chain/ai_chain.{py,mjs}` — built, both-language selftests PASS live vs
  claude-proxy (`mode:claude-oauth`), clean `need_api_permission` fall-through. **Independent Opus
  review: PASS, no must-fix.** Correct behaviors verified: free-first OpenRouter (inverted from the
  paid-first reference lists), no-silent-success (prose OK, empty falls through), Bearer only on
  claude `/generate` not `/vision`, real base64 vision block, py/JS parity, P3-naming compatible.
  **Should-fix (tracked, non-blocking):** (1) `model` param dropped on the T3 OpenRouter tier — use or
  drop it; (2) add JS selftest Case-3 (gemini fall-through) for test parity; (3) **retarget gemini text
  to `:8789` + Bearer** once the name-collision decision is made.
- **P3 verify harness** `/opt/ai-chain/verify_chain.py` — built, module-mode cascade self-validated
  PASS/PASS/PASS. Auto-discovers `<TIER>_..._URL` globals (helper is compatible).
- **P1 gemini text endpoint** — `$0` OAuth text mechanism validated (needs `GOOGLE_CLOUD_PROJECT`
  bypass); code staged on the Python app + backed up, **NOT deployed**. **Superseded finding:** the
  live Gemini text endpoint already exists on the **Node** `gemini-proxy` (`:8789/generate`) — see
  ADR-132 "Phase 0 findings" addendum. Gemini **vision** tier is not live (open decision).

**Gemini keystone RESOLVED 2026-08-06 (dev):** name collision sidestepped by adding `/vision` to the
live Node `gemini-proxy` (`:8789`); it now serves text+vision $0 OAuth. `ai_chain.py` rewired + tested
(both tiers -> `mode:gemini-oauth`). `ai_chain.mjs` Gemini-rewire still TODO (Node-proxy waves only).
Integration mechanics for the waves (each consumer proxy needs): (1) `ai_chain` importable in the
container (bind-mount `/opt/ai-chain`), (2) env `GEMINI_PROXY_TOKEN` + `CLAUDE_PROXY_TOKEN` +
`OPENROUTER_API_KEY` present (compose `environment:` + `.env`), (3) recreate (safe — `traefik-net` is a
`root_default` alias). OR, for a prod-shared file where a full refactor is riskier, an ADR-132-permitted
**additive inline Gemini insert** (mirror `ocr-proxy`'s `call_claude_vision` -> add `call_gemini_vision`
hitting `:8789/vision`).

**Next:** Wave 1 (tour-ai, ai-finance paid-SPOFs) — establish the integration pattern on the first, replicate.

---

## Phase 1 — Inventory (source of truth for Phase 2)

| ID | Proxy / file | Kind | Current chain | Target delta | Prod-shared? | Model |
|---|---|---|---|---|---|---|
| A1 | tour-ai-proxy `/opt/tour-ai-proxy/tour_ai_proxy.py` | text | **Anthropic REST only** | full T1→T5 (biggest gap) | check | Sonnet |
| A2 | ai-finance-proxy `/opt/ai-finance-proxy/ai_finance_proxy.py` | vision | **Anthropic REST only** | full T1→T4 (Claude+Gemini vision) | check | Sonnet |
| A3 | ocr-proxy `/opt/ocr-proxy/ocr_proxy.py` | vision | Claude /vision → OpenRouter (2026-08-06) | **insert T2 Gemini /analyze** between Claude & OpenRouter | shared (RentVeh+expense) | Sonnet |
| A4 | hub-media-proxy `/opt/hub-media-proxy/hub_media_proxy.py` `/suggest` | vision | Claude /vision only | add T2 Gemini + T3 OpenRouter | shared (ImageBinding) | Sonnet |
| A5 | health-ocr `/var/www/Others/health/server/ocr.js` | vision | claude-p → **generativelanguage(static)** → OpenRouter → Anthropic | swap static Gemini → gemini-proxy OAuth (ADR-114 fix); reorder to T1→T4 | check | Sonnet |
| A6 | excel-ai-sidecar `/opt/excel-ai-sidecar/server.mjs` | text | claude-p → **generativelanguage(static)** → OpenRouter → Anthropic | swap static Gemini → gemini-proxy `/generate` OAuth; reorder | check | Sonnet |
| A7 | counter-ai `/opt/counter-ai/counter_ai.py` | text | Claude OAuth → OpenRouter → Anthropic | insert T2 Gemini text | check | Haiku/Sonnet |
| A8 | print-ai-proxy `/opt/print-ai-proxy/print_ai_proxy.py` | text | Claude OAuth → OpenRouter → Anthropic | insert T2 Gemini text | check | Haiku/Sonnet |
| A9 | blogsmith `/var/www/Others/Automation/blogsmith/blogsmith_ai.py` | text | Claude OAuth → OpenRouter → Anthropic | insert T2 Gemini text | check | Haiku/Sonnet |
| A10 | sitecap `/var/www/Others/Automation/sitecap/ai-pipeline.js` | text | claude-p → OpenRouter → Anthropic | insert T2 Gemini text | check | Haiku/Sonnet |
| A11 | dispatch-ai `/opt/dispatch-ai/dispatch_ai.py` (address) | text | Claude OAuth → OpenRouter → Anthropic → Ollama | insert T2 Gemini text (keep Ollama T5) | **PROD-shared (manual dispatch)** | Sonnet |
| A12 | dispatch-ai (docket OCR) | vision | Ollama llava | ADR-062 §1 self-host exempt — OPTIONAL add T1/T2 vision above Ollama | PROD-shared | Sonnet |
| A13 | videosmith `/var/www/Others/Automation/videosmith/videosmith_app.py` | text | Anthropic + OpenRouter | add T1 Claude + T2 Gemini above | check | Sonnet |

**Excluded (correctly):** graphify-gemini-bridge (already gemini-proxy + Ollama; domain-specific — confirm order only). CC-reconciliation (ADR-056 copy-paste, out of scope). "Prod-shared? check" = confirm dev vs prod container before editing; treat unknown as prod-gated.

---

## Phase 2 — Per-proxy migration (each chunk is independent; pipeline-able)

Per chunk, the executing agent does exactly:
1. `cp <file> <file>.bak-YYYYMMDD-adr132`.
2. Refactor the AI call to use the shared helper (`chain_text`/`chain_vision`) OR inline the T2 Gemini tier if a full refactor is out of scope for a prod-shared file (additive, lower risk).
3. Preserve ADR-062 §2–§6 behavior (review gate, `need_api_permission` 503, response shape) — do not regress.
4. Deploy: `docker restart <container>` (NOT compose up). For `/var/www/Others/*` Node apps, restart their container likewise.
5. **Acceptance (mandatory, real-input):** run `verify_chain.py` against the live endpoint — force-fail T1, confirm T2 answers; force-fail T1+T2, confirm T3; confirm a normal call reports `mode: claude-oauth`. Log actual latency per tier.
6. Log result back into this doc's Phase-2 tracker.

**Rollout order (risk-first):**
- **Wave 1 (paid-only SPOFs):** A1 tour-ai, A2 ai-finance — highest value, currently paid + no fallback.
- **Wave 2 (ADR-114 key violations):** A5 health-ocr, A6 excel-ai — stop static Gemini keys.
- **Wave 3 (vision fleet):** A3 ocr-proxy, A4 hub-media, A12 dispatch docket (optional).
- **Wave 4 (text fleet):** A7 counter, A8 print, A9 blogsmith, A10 sitecap, A13 videosmith.
- **Wave 5 (prod-shared, hkl-gated, last):** A11 dispatch address — only after Waves 1-4 prove the helper in dev.

**Phase-2 tracker:**
- **Wave 1 DONE 2026-08-06 (dev+prod, hkl-authorized):** both paid-SPOFs migrated off direct
  `api.anthropic.com` (no-fallback) onto the shared `ai_chain` full chain.
  - **tour-ai-proxy** — /analyze, /route, /replan → `chain_text`; added `need_api_permission` 503
    (had none). Cascade verified: claude 6.7s / gemini 14s / openrouter 16.3s all PASS; normal call
    `claude-oauth` 11.3s; gemini-forced `gemini-oauth` ok. Backup `.bak-20260806-adr132w1`.
  - **ai-finance-proxy** — POST `/` (image|text) → `chain_vision`/`chain_text`; removed the old
    hard "API key not configured" 500 gate; added `need_api_permission` 503. Gemini vision extracted a
    synthetic UPI receipt correctly; normal `claude-oauth` 18.3s. Backup `.bak-20260806-adr132w1`.
  - Integration pattern established (reuse for later waves): bind-mount `/opt/ai-chain:/opt/ai-chain:ro`
    into the service, add env `GEMINI_PROXY_TOKEN`+`CLAUDE_PROXY_TOKEN`(+`OPENROUTER_API_KEY`) via compose
    `environment:`+`.env`, `docker compose up -d <svc>` (recreate needed for new mount/env; safe —
    `traefik-net`=`root_default` alias). `docker restart` is NOT enough when adding mounts/env.
  - **⚠ Wave-1 LESSON (fold into every remaining wave):** do NOT forward a proxy's Claude-specific
    resolved model id (e.g. `claude-sonnet-4-6`) into `ai_chain.chain_text/chain_vision`'s `model=` param
    — it leaks to the Gemini tier on fall-through and 500s it (silent skip of the free tier). **Pass
    `model=None`** so each tier uses its own default. (Ties to the P2-review should-fix: the shared `model`
    param has no per-tier map; treat it as provider-agnostic-or-None until that's added.)

**Remaining:** Wave 2 (health-ocr, excel-ai — ADR-114 static-key fixes; Node → needs `ai_chain.mjs`
Gemini rewire first), Wave 3 (ocr-proxy insert Gemini tier, hub-media), Wave 4 (counter/print/blogsmith/
sitecap/videosmith), Wave 5 (dispatch, most-shared).

---

## Verification & reversibility

- **Every proxy keeps per-tier env toggles** (P2) so any latency-sensitive path can drop back to a
  faster paid tier without a code change (ocr-proxy's `OCR_USE_CLAUDE` is the precedent).
- **Backups** (`.bak-YYYYMMDD-adr132`) per file; revert = restore + `docker restart`.
- **Force-fail acceptance** per proxy (not just "returns 200") — an all-fell-through-to-paid regression
  is silent otherwise (the exact trap that made ocr-proxy's test worth doing).
- **Latency budget:** note per-proxy p50; if an interactive path exceeds ~20s on OAuth tiers, flag to
  hkl whether to keep OAuth-primary there or toggle to paid-primary for that one path.

## Non-goals / open decisions for hkl

- **No prod cutover** without explicit hkl go-ahead per target (dispatch-ai especially).
- **Gemini billed-Vertex caveat** (ADR-132): if Vertex spend appears materially under Gemini-CLI quota
  exhaustion, revisit whether T2 stays ahead of T3 free OpenRouter models.
- Whether to also retire the standalone OC AI Proxy (`:4999` + `intercept.js`) in favor of the shared
  helper, or keep it for transparent Anthropic-format Node interception — decide after Wave 4.
