Skip to content

feat: extract shared gateway client into packages/llm (#93) - #320

Closed
ckrough wants to merge 1 commit into
mainfrom
issue/93
Closed

feat: extract shared gateway client into packages/llm (#93)#320
ckrough wants to merge 1 commit into
mainfrom
issue/93

Conversation

@ckrough

@ckrough ckrough commented Jul 27, 2026

Copy link
Copy Markdown
Member

Extracts build_gateway_client out of retriever into a new shared package packages/llm (dist evermore-llm, import evermore_llm), decoupled from retriever's Settings by a structural GatewayConfig Protocol. ADR 0028 consolidated all outbound model traffic onto one gateway and deferred this promotion until packages/ existed; it does now.

Zero behavior change: the moved body differs from the deleted services/retriever/src/retriever/infrastructure/llm/gateway_client.py only by the settings -> config parameter rename. The if scope is None: token-resolution branch is byte-identical. Retriever's gateway and safety tests changed by exactly one import line each, bodies untouched (function inventory 9 = 9).

GatewayScope moves to the package and retriever imports it back, leaving one definition rather than two. The scope boundary is documented in ADR 0034 and packages/llm/README.md: shared model-call infra belongs here; service-specific wiring (provider selection, fallback chains, retry and circuit-breaker policy, DI wiring) stays in the service, and cross-service domain contracts go to packages/schema.

On acceptance criterion 3 (petdata and biowriter import the helper): no cross-service copy or cross-import exists to eliminate. petdata makes no gateway calls and carries no openai dependency; biowriter is not scaffolded as a Python service. Wiring either now would add an unused dependency. Retriever ships as the sole consumer, biowriter wiring is deferred to #64, and the documented boundary is what prevents a future copy.

Pipeline phases

Phase Note
🟡 Plan / design triage Adopted the pre-written worktree plan (digest verified, base_sha ancestral); design pass skipped at complexity score 2
🟢 Implement Package, retriever wiring, and docs landed across three implementer spawns; tests written first
🟢 Test-validation 0 blocking, 2 advisory; both advisories fixed in-PR
🟢 Review-gate APPROVE (1 cycle), 5 advisory findings, 3 fixed in-PR
🟢 Simplify NO-OP
🟢 Gates test + lint exit 0
🟡 Security review 0 blocking; 1 medium accepted risk (pre-existing fail-open, relocated unchanged), 1 low fixed, 1 low filed as #319
🟢 PR opened Non-draft, closes #93

Test results

Both commands from .daedalus/config.json, exit 0.

test: (cd services/retriever && uv sync && uv run python -m pytest tests/ --ignore=tests/integration) && (cd apps/stacker && npm ci && npm run build)

retriever: 385 passed, 10 deselected, coverage 89.44% (floor 80%). stacker: build ✔ done (adapter-cloudflare). TEST_GATE_EXIT=0

lint: retriever ruff check + ruff format --check + mypy --strict, then petdata ruff + mypy + bandit, then stacker npm run check

retriever: ruff clean, 105 files formatted, mypy strict clean (67 source files). petdata: ruff clean, 56 files formatted, mypy clean (25 source files), bandit clean. stacker: svelte-check 4414 files, 0 errors 0 warnings. LINT_GATE_EXIT=0

Package-local gates (packages/llm): ruff format clean, ruff check clean, mypy --strict src/ clean (2 source files), pytest 10 passed.

Mutation testing

Five mutations to build_gateway_client, each applied then reverted. After strengthening the test fake (review-gate finding), all five are caught by the package suite alone:

Mutation Caught
collapse the if scope is None: branch ✅ 3 failures
always send default_headers
drop the api_key=token or "unused" placeholder
hardcode the auth header name
swallow the base_url ValueError

Eval scores

No evals configured in this repo.

Follow-ups

Closes #93

Move `build_gateway_client` out of retriever into a new shared package
(dist `evermore-llm`, import `evermore_llm`), so petdata and biowriter can
import the one gateway constructor instead of copying it. ADR 0028
consolidated all outbound model traffic onto one gateway and deferred this
promotion until `packages/` existed; it does now.

Shared code cannot import a service's config, so the package defines a
structural `GatewayConfig` Protocol naming the four members the constructor
reads. Retriever's `Settings` satisfies it with no change to `Settings`, and
mypy strict checks that at the call sites. `GatewayScope` moves to the package
and retriever imports it back, leaving one definition rather than two.

Zero behavior change: the moved body differs only by the `settings` -> `config`
parameter rename. Retriever's gateway and safety tests changed by one import
line each, bodies untouched.

Closes #93
@ckrough ckrough closed this Jul 27, 2026
@ckrough
ckrough deleted the issue/93 branch July 27, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create packages/llm (shared gateway client)

1 participant