fix(llm): zero-downtime LiteLLM config operations — hot-path Reloader stance + x402-buyer split (#321)#707
Open
bussyjd wants to merge 3 commits into
Open
fix(llm): zero-downtime LiteLLM config operations — hot-path Reloader stance + x402-buyer split (#321)#707bussyjd wants to merge 3 commits into
bussyjd wants to merge 3 commits into
Conversation
Phase 1 of the zero-downtime work for #321: - obol model prefer no longer rolls the LiteLLM deployment. model_list order is an obol convention (Rank reads the ConfigMap); LiteLLM's router does not use it, so the restart was pure downtime. - RestartLiteLLM now fails loudly when the rollout does not converge within 90s instead of warning and reporting success (#321 item 3). - ReconcileRecorded's ConfigMap-only branch restarts explicitly now that Reloader no longer watches litellm-config. - New drift safety net: obol model status compares the ConfigMap model_list against the live router /v1/models (CheckRouterDrift / DiffRouterModels) and reports missing/extra entries, replacing the Reloader annotation as the guard against silently-failed hot calls. Claude-Session: https://claude.ai/code/session_01YLuXwN1A4tG3xmEMKvAzeD
…rollouts Phase 2 of #321. The buyer's RWO consumed-auth PVC forced the litellm Deployment onto replicas:1 + Recreate, turning every rollout (Reloader secret rotation, image bump, config reload) into a full inference gap — and the Reloader annotation on litellm-config triggered such a rollout on every model add/remove/prefer and first-time purchase, defeating the hot /model/new path shipped in #320. - x402-buyer becomes its own Deployment (1 replica, Recreate, keeps the PVC and single-writer auth semantics) + ClusterIP Service. Buyer CM changes still hot-reload via /admin/reload; only buyer image bumps briefly gap paid/* routes. - litellm is now stateless: RollingUpdate maxSurge:1 maxUnavailable:0 — a new pod must be Ready before the old one terminates. Reloader watches litellm-secrets only; litellm-config changes are hot-applied. - Paid-route api_base moves to http://x402-buyer.llm.svc.cluster.local:8402/v1. The controller migrates legacy 127.0.0.1:8402 entries at startup and on per-purchase reconcile (upgraded-in-place clusters), hot-syncing the live router. - Controller buyer probes (reload/remove/status) and buy.py target the x402-buyer pods; flow scripts reach the buyer via its Service. - PodMonitor keeps its historical name but targets the buyer pods. NetworkPolicy for :8402 is deliberately deferred: agent namespaces have no common label and buy.py legitimately reads /status from them; the Service changes addressing, not exposure (pod IP was already cluster-reachable). Claude-Session: https://claude.ai/code/session_01YLuXwN1A4tG3xmEMKvAzeD
CLAUDE.md and obol-stack-dev references: buyer is a standalone Deployment + Service (port-forward svc/x402-buyer), paid routes point at the buyer Service with the mandatory /v1 suffix, and Reloader watches litellm-secrets only — litellm-config changes are hot-applied with drift surfaced by obol model status (#321). Claude-Session: https://claude.ai/code/session_01YLuXwN1A4tG3xmEMKvAzeD
This was referenced Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LiteLLM zero-downtime config operations (#321 phases 1+2)
Problem
The hot
/model/newpath shipped in #320 was being defeated by two later changes:2eed34badded a Reloader annotation onlitellm-config. Every model add/remove/prefer and every first-time purchase patches that ConfigMap (persistence-first ordering), so Reloader rolled the pod seconds after the hot-add succeeded.5c9a879moved the buyer's consumed-auth state to an RWO PVC inside the litellm pod, forcingreplicas: 1+strategy: Recreate— every rollout became a full inference gap.Net effect: every config operation took the inference gateway down, which is exactly what #321 set out to fix (and what the latest issue comment asks about).
Phase 1 — stop self-inflicted restarts
litellm-secretsonly.litellm-configchanges are hot-applied via/model/new+/model/deleteby both the CLI and the serviceoffer-controller; the ConfigMap is persistence for the next pod start.obol model preferno longer restarts LiteLLM (order is an obol convention read from the ConfigMap byRank(); LiteLLM's router ignores it).RestartLiteLLMfails loudly on rollout timeout instead of warning and reporting success (LiteLLM reliability: hot-reload, zero-downtime restarts, and single-replica fragility #321 item 3).obol model statusnow compares the ConfigMap model_list against the live router/v1/modelsand reports missing/extra entries (internal/model/drift.go), so a silently-failed hot call is visible instead of masked by a restart.ReconcileRecorded's ConfigMap-only branch restarts explicitly now that Reloader won't.Phase 2 — make the remaining restarts gapless
x402-buyer.llm.svc:8402.RollingUpdate maxSurge: 1, maxUnavailable: 0. Secret rotation (via Reloader) and image bumps surge a new pod to Ready before the old one terminates — zero inference gap. Stillreplicas: 1steady-state (no extra laptop footprint).api_base→http://x402-buyer.llm.svc.cluster.local:8402/v1(chart wildcard + controller-written entries).127.0.0.1:8402entries at startup and on per-purchase reconcile (ConfigMap rewrite + hot delete/re-add). Those entries are dead upstreams on upgraded clusters until migrated, so the delete/re-add window is not a regression./admin/reload,/admin/remove,/status) and agentbuy.pytargetapp=x402-buyerpods; flow scripts reach the buyer via its Service.litellm-x402-buyer) but targets the buyer pods.Deliberately deferred
hermes-obol-agent,openclaw-<id>, agent CRs) share no common label andbuy.pylegitimately reads/statusfrom them. The Service changes addressing, not exposure (the pod IP was already cluster-reachable). Follow-up: label agent namespaces, then restrict./model/updatewith in-memory key) — unnecessary now that key rotation rolls gaplessly; would only remove the surge itself.Review boundaries / invariants
x402-buyermust have exactly one writer of consumed-auth state: buyer Deployment isreplicas: 1+Recreate, PVC unchanged.TestBuyerStatePVC).litellm-config(pinned byTestLLMTemplate_IncludesPaidRouteAndBuyerSidecar).addLiteLLMModelEntry)./v1suffix on buyer api_base is load-bearing (CLAUDE.md pitfall 6; pinned byTestBuyerAPIBase).Tests
internal/model/drift_test.go— table-drivenDiffRouterModels(wildcards, missing, extra).internal/serviceoffercontroller/purchase_migration_test.go— legacy api_base migration (startup + per-purchase + no-op + missing-CM),buyerAPIBase.internal/stack/stack_test.go(annotation stance, service api_base),internal/embed/embed_buyer_state_test.go(buyer Deployment owns PVC, litellm stateless + RollingUpdate params).go test ./...green.Local validation (fresh k3d cluster,
OBOL_DEVELOPMENT=true, dev-built CLI + controller image)An in-cluster probe hit
litellm.llm.svc:4000/health/readinesstwice a second for the whole session: 1253 samples, 0 failures across all of the following.litellmpod is single-container,RollingUpdate {maxSurge:1, maxUnavailable:0}, annotations carrysecret.reloader.stakater.com/reloadonly;x402-buyerruns as its own 1-replica Recreate Deployment with the PVC, plus ClusterIP Service.obol model prefer qwen3.5→ ConfigMap reordered, same pod, restartCount 0.obol model remove nomic-embed-text→ hot-delete, same pod;obol model statusdrift check stayed clean (live router really dropped it).obol model statusprintmissing from router: drift-test-ghostwith the fix hint; restoring the CM cleared it.litellm-secretstriggered a Reloader surge rollout — old and new pods briefly coexisted, buyer pod untouched, zero probe failures through the replacement./healthz→ok,/status→{}.paid/zdt-testentry with the legacy127.0.0.1:8402/v1api_base, restarted the controller → logbuyer-migrate: rewrote 1 legacy buyer api_base entries in llm/litellm-config, CM rewritten to the Service URL, entry hot-added.paid/zdt-testthrough LiteLLM returned the buyer's own application error (no purchased upstream mapped for requested model) — LiteLLM → x402-buyer Service connectivity confirmed; only an actual purchase is absent.obol model remove paid/zdt-test(live + config), final drift check clean.Not exercised live: a real paid buy (needs funded wallet + seller; covered by release-gate flows 06/08/11/13/14, which this PR updates for the Service address).