Build the resilience proxy core (phase 1, aosh leg 04): num_ctx injection, queue, resilience, OpenAI surface #2

Closed
opened 2026-06-25 09:56:13 +00:00 by coilysiren · 2 comments
Owner

Goal

Build the resilience proxy core in agent-proxy per the locked aosh design, and prove the per-model num_ctx fix end to end against the live tower. This is aosh headless leg 04 - the design is locked, this is engineering to spec. Do not redesign. Read the legs.

Read first (source of truth - do not invent)

  • coilyco-bridge/agentic-os-hardware docs/plan/04-headless-proxy-build.md - the build leg, the authority for this issue.
  • docs/plan/02-reference-architecture.md - the locked architecture and the app/ layout.
  • docs/plan/01-reference-diagnosis.md - the measured 32k-truncation root cause this kills.
  • docs/plan/03-headless-tower-benchmark.md - the per-model safe num_ctx values (use them when they exist).
  • Tracking: coilysiren/inbox#118.

Dependency / sequencing

  • Live testing needs a tailnet route to the tower (kai-tower-3026 ollama over the tailnet, FQDN at SSM /coilysiren/kai-tower-3026/tailnet-fqdn). That route is coilyco-flight-deck/ward#330. Carry this on a tailnet-capable host with the network flag + --aws so curl http://$TOWER:11434/api/version works from inside the container. If the route is not yet available, build steps 1 and 3-6 still proceed against a stub ollama, but step 2's live proof is gated on ward#330.
  • Carry after agent-proxy#1 (doc reorg) merges, or rebase on latest main, to avoid README/FEATURES churn collisions. Keep docs/FEATURES.md current as features land (the trifecta).

Stack

Python 3.12, FastAPI, uvicorn, httpx, pydantic-settings, structlog, prometheus-client, opentelemetry-sdk plus the FastAPI and httpx instrumentations, sentry-sdk, tiktoken (or the model tokenizer) for token counting. Manage with uv, declared in pyproject.toml. Add .ward/ward.yaml so dev commands route through ward.

Build order (each step ends green and committed, per leg 04)

  1. Skeleton + obs first. Create the app/ tree from leg 02 (main.py queue.py upstream.py resilience.py models.py analysis.py obs.py config.py). Wire obs.py before any logic - structlog JSON, prometheus /metrics, OTel tracer, Sentry init from config.py. main.py exposes /healthz -> 200. Confirm /healthz and /metrics under uvicorn.
  2. Backend registry + num_ctx injection (upstream.py, models.py). A logical-model table maps name -> {backend_url, ollama_tag, num_ctx}. The httpx client forwards to the backend's native ollama /api/chat with options.num_ctx injected. Prove end to end against the tower: a 55k-token request to logical fast-think returns prompt_eval_count near the injected num_ctx, not 32767. This is the core win - verify before building further. (Live-test gated on the tailnet route above.)
  3. Queue + workers (queue.py). Bounded asyncio.Queue + worker pool. Route enqueues a job and awaits its future, a worker pops and dispatches via upstream.py. Backpressure -> 429 when full. Export llm_queue_depth.
  4. Resilience (resilience.py). Wrap dispatch with response validation (non-empty, tool-call JSON parses, no degenerate repetition), retries with backoff, the fallback chain (next backend in the logical model's list), per-backend circuit breaker with cooldown. Each path increments its metric (llm_retries_total, llm_fallbacks_total, llm_circuit_state).
  5. Analysis (analysis.py). Count prompt tokens. If a request exceeds the model's safe num_ctx, trim oldest non-system turns (or summarize) before forward and increment llm_truncation_avoided_total. Never pass an over-budget prompt to the model.
  6. OpenAI-compatible surface polish. /v1/chat/completions (streaming + non-streaming), /v1/completions, /v1/models listing the logical names. Shape responses to the OpenAI schema so harnesses need no special handling.

Per-model num_ctx source

Use the leg-03 safe values when they exist. Until then default fast-think (Qwen3-A3B) to 49152 (proven safe, leg 01) and others conservatively to 32768. Values live in config the deploy can override (a ConfigMap later).

Local validation

  • Reproduce the leg-01 truncation test through the proxy - confirm the 32k cliff is gone.
  • Point one harness at the local proxy (opencode baseURL -> http://127.0.0.1:8080/v1) and run a short agentic loop.
  • Run the leg-05 reliability harness against the local proxy for a baseline-vs-direct number.

Secrets and config

config.py reads from env and SSM, never hardcodes. Sentry DSN and any API-fallback keys come from SSM at boot. No secret in a tracked file. The tower FQDN resolves at runtime from /coilysiren/kai-tower-3026/tailnet-fqdn.

Guardrails

  • Do not deploy to kai-server here - that is the consult leg 09 (externally visible, the 2-pod decision, auth).
  • Keep the proxy stateless except the in-memory queue. No DB.
  • Commit each build step. Keep docs/FEATURES.md current.
  • This is phase 1 of the capability platform (per agent-proxy#1). Stay tightly the reliability proxy - the capability phases (tool injection, MCP credential passthrough, RAG, upskilling) are later, not here.

Done-condition

The proxy runs locally, injects per-model num_ctx, validates/retries/falls back, exposes metrics/traces/logs/errors, serves the OpenAI-compatible surface, and demonstrably removes the 32k truncation for a repointed harness against the live tower. Committed and pushed to main.

## Goal Build the resilience proxy core in `agent-proxy` per the **locked** aosh design, and prove the per-model `num_ctx` fix end to end against the live tower. This is aosh **headless leg 04** - the design is locked, this is engineering to spec. **Do not redesign. Read the legs.** ## Read first (source of truth - do not invent) - `coilyco-bridge/agentic-os-hardware` `docs/plan/04-headless-proxy-build.md` - the build leg, the authority for this issue. - `docs/plan/02-reference-architecture.md` - the locked architecture and the `app/` layout. - `docs/plan/01-reference-diagnosis.md` - the measured 32k-truncation root cause this kills. - `docs/plan/03-headless-tower-benchmark.md` - the per-model safe `num_ctx` values (use them when they exist). - Tracking: `coilysiren/inbox#118`. ## Dependency / sequencing - **Live testing needs a tailnet route to the tower** (`kai-tower-3026` ollama over the tailnet, FQDN at SSM `/coilysiren/kai-tower-3026/tailnet-fqdn`). That route is `coilyco-flight-deck/ward#330`. **Carry this on a tailnet-capable host with the network flag + `--aws`** so `curl http://$TOWER:11434/api/version` works from inside the container. If the route is not yet available, build steps 1 and 3-6 still proceed against a stub ollama, but step 2's live proof is gated on ward#330. - Carry **after** `agent-proxy#1` (doc reorg) merges, or rebase on latest `main`, to avoid README/FEATURES churn collisions. Keep `docs/FEATURES.md` current as features land (the trifecta). ## Stack Python 3.12, FastAPI, uvicorn, httpx, pydantic-settings, structlog, prometheus-client, opentelemetry-sdk plus the FastAPI and httpx instrumentations, sentry-sdk, tiktoken (or the model tokenizer) for token counting. Manage with `uv`, declared in `pyproject.toml`. Add `.ward/ward.yaml` so dev commands route through ward. ## Build order (each step ends green and committed, per leg 04) 1. **Skeleton + obs first.** Create the `app/` tree from leg 02 (`main.py` `queue.py` `upstream.py` `resilience.py` `models.py` `analysis.py` `obs.py` `config.py`). Wire `obs.py` before any logic - structlog JSON, prometheus `/metrics`, OTel tracer, Sentry init from `config.py`. `main.py` exposes `/healthz` -> 200. Confirm `/healthz` and `/metrics` under uvicorn. 2. **Backend registry + num_ctx injection (`upstream.py`, `models.py`).** A logical-model table maps name -> `{backend_url, ollama_tag, num_ctx}`. The httpx client forwards to the backend's native ollama `/api/chat` with `options.num_ctx` injected. **Prove end to end against the tower**: a 55k-token request to logical `fast-think` returns `prompt_eval_count` near the injected `num_ctx`, not 32767. This is the core win - verify before building further. (Live-test gated on the tailnet route above.) 3. **Queue + workers (`queue.py`).** Bounded `asyncio.Queue` + worker pool. Route enqueues a job and awaits its future, a worker pops and dispatches via `upstream.py`. Backpressure -> 429 when full. Export `llm_queue_depth`. 4. **Resilience (`resilience.py`).** Wrap dispatch with response validation (non-empty, tool-call JSON parses, no degenerate repetition), retries with backoff, the fallback chain (next backend in the logical model's list), per-backend circuit breaker with cooldown. Each path increments its metric (`llm_retries_total`, `llm_fallbacks_total`, `llm_circuit_state`). 5. **Analysis (`analysis.py`).** Count prompt tokens. If a request exceeds the model's safe `num_ctx`, trim oldest non-system turns (or summarize) before forward and increment `llm_truncation_avoided_total`. Never pass an over-budget prompt to the model. 6. **OpenAI-compatible surface polish.** `/v1/chat/completions` (streaming + non-streaming), `/v1/completions`, `/v1/models` listing the logical names. Shape responses to the OpenAI schema so harnesses need no special handling. ## Per-model num_ctx source Use the leg-03 safe values when they exist. Until then default `fast-think` (Qwen3-A3B) to **49152** (proven safe, leg 01) and others conservatively to **32768**. Values live in config the deploy can override (a ConfigMap later). ## Local validation - Reproduce the leg-01 truncation test **through the proxy** - confirm the 32k cliff is gone. - Point one harness at the local proxy (opencode `baseURL` -> `http://127.0.0.1:8080/v1`) and run a short agentic loop. - Run the leg-05 reliability harness against the local proxy for a baseline-vs-direct number. ## Secrets and config `config.py` reads from env and SSM, never hardcodes. Sentry DSN and any API-fallback keys come from SSM at boot. No secret in a tracked file. The tower FQDN resolves at runtime from `/coilysiren/kai-tower-3026/tailnet-fqdn`. ## Guardrails - **Do not deploy to kai-server here** - that is the consult leg 09 (externally visible, the 2-pod decision, auth). - Keep the proxy stateless except the in-memory queue. No DB. - Commit each build step. Keep `docs/FEATURES.md` current. - This is **phase 1** of the capability platform (per `agent-proxy#1`). Stay tightly the reliability proxy - the capability phases (tool injection, MCP credential passthrough, RAG, upskilling) are later, not here. ## Done-condition The proxy runs locally, injects per-model `num_ctx`, validates/retries/falls back, exposes metrics/traces/logs/errors, serves the OpenAI-compatible surface, and **demonstrably removes the 32k truncation for a repointed harness against the live tower**. Committed and pushed to `main`.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-agent-proxy-2 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-02T02:23:31Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-agent-proxy-2` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-02T02:23:31Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - resilience proxy core landed on main (num_ctx injection, queue, resilience, context guard, OpenAI surface); 32k cliff proven gone live (32767 -> 49151).

Honest retro: this one went smoothly because the design was genuinely locked - legs 01/02/04 left almost nothing to invent, so it was engineering to spec, and the tower happened to be reachable directly so the live proof wasn't gated after all. The core win landed on the first real try: direct tower /v1 caps at 32767, through the proxy fast-think keeps 49151.

What fought back was all downstream of one fact I under-weighted: fast-think (qwen3:30b-a3b) is a reasoning model. It puts its output in message.thinking and returns empty content until it finishes, so my first two proof runs 502'd - not a bug in num_ctx, but my own validator. That forced two real refinements, both surfaced by live testing rather than guessed: a legitimately short answer ("OK") must not read as truncation garbage (only non-word junk does), and a thinking-only response that ran out of budget did real work and must not be rerolled into a 502. I'm glad the leg says "verify before building further" - those only showed up against the real model. A third fix came from the reliability harness: the queue bound its asyncio.Queue at construction, which broke across event loops - now built in start().

Confidence: high on the core (proven live, 26 tests green, tool-calls round-trip as proper OpenAI JSON). Lower-confidence rough edges worth follow-ups: (1) the leg-05 reliability harness runs and scores but my context-growth config wasn't aggressive enough to reproduce the ~75% degradation in a few turns - tuning it into a real before/after is genuinely leg-05 work with its own SLO consult; (2) streaming skips full-content validation by design (a stream can't be revalidated) - documented, but worth revisiting; (3) the fallback chain ships only the tower primary by default since the sibling/CPU/API backends aren't addressable from here - the structure is there, config adds them.

Single-repo run: the agentic-os-hardware grant was read-only source-of-truth for leg 04, so I touched nothing there and there's no cross-repo push to verify. Did not deploy to kai-server (that's leg 09, out of scope).

WARD-OUTCOME: done - resilience proxy core landed on main (num_ctx injection, queue, resilience, context guard, OpenAI surface); 32k cliff proven gone live (32767 -> 49151). Honest retro: this one went smoothly because the design was genuinely locked - legs 01/02/04 left almost nothing to invent, so it was engineering to spec, and the tower happened to be reachable directly so the live proof wasn't gated after all. The core win landed on the first real try: direct tower `/v1` caps at 32767, through the proxy `fast-think` keeps 49151. What fought back was all downstream of one fact I under-weighted: `fast-think` (qwen3:30b-a3b) is a reasoning model. It puts its output in `message.thinking` and returns empty `content` until it finishes, so my first two proof runs 502'd - not a bug in num_ctx, but my own validator. That forced two real refinements, both surfaced by live testing rather than guessed: a legitimately short answer ("OK") must not read as truncation garbage (only non-word junk does), and a thinking-only response that ran out of budget did real work and must not be rerolled into a 502. I'm glad the leg says "verify before building further" - those only showed up against the real model. A third fix came from the reliability harness: the queue bound its `asyncio.Queue` at construction, which broke across event loops - now built in `start()`. Confidence: high on the core (proven live, 26 tests green, tool-calls round-trip as proper OpenAI JSON). Lower-confidence rough edges worth follow-ups: (1) the leg-05 reliability harness runs and scores but my context-growth config wasn't aggressive enough to reproduce the ~75% degradation in a few turns - tuning it into a real before/after is genuinely leg-05 work with its own SLO consult; (2) streaming skips full-content validation by design (a stream can't be revalidated) - documented, but worth revisiting; (3) the fallback chain ships only the tower primary by default since the sibling/CPU/API backends aren't addressable from here - the structure is there, config adds them. Single-repo run: the agentic-os-hardware grant was read-only source-of-truth for leg 04, so I touched nothing there and there's no cross-repo push to verify. Did not deploy to kai-server (that's leg 09, out of scope).
Sign in to join this conversation.
No description provided.