feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix - #324

Closed
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime
Closed

feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fix#324
bussyjd wants to merge 5 commits into
mainfrom
feat/litellm-zero-downtime

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #322 (x402 verifier TLS) and #323 (Obol facilitator default) into a single PR with LiteLLM zero-downtime deployment. Three improvements:

1. x402 verifier TLS fix

The distroless x402-verifier image ships without a CA store, causing TLS verification of external HTTPS facilitators to fail. populateCABundle() reads the host's CA bundle and patches it into the ca-certificates ConfigMap in the x402 namespace.

2. Obol-operated facilitator default

Switches the default facilitator URL from facilitator.x402.rs to x402.gcp.obol.tech via a single DefaultFacilitatorURL constant. All references consolidated.

3. LiteLLM zero-downtime config

  • Hot-add models: hotAddModels() calls /model/new API via kubectl exec — no restart needed when only models change (no API key update)
  • Rolling deploy: 2 replicas, maxUnavailable: 0, PodDisruptionBudget (minAvailable: 1), preStop hook with 10s drain
  • Graceful fallback: hot-add failure falls back to rolling restart automatically

Test coverage

  • model_hotadd_test.go (NEW): ConfigureLiteLLM branching (API key -> restart, empty -> hot-add), payload construction, multiple entries, mock server /model/new acceptance
  • setup_ca_test.go (NEW): CA bundle patch JSON structure, no-bundle skip, empty-file skip
  • verifier_test.go (UPDATED): Description field assertion in 402 response body
  • Existing tests updated for new facilitator URL across config, watcher, sell, and setup packages

Supersedes

Test plan

  • go build ./... passes
  • go test ./internal/model/ passes (including new hotAddModels tests)
  • go test ./internal/x402/ passes (including CA bundle + Description tests)
  • CI green

bussyjd added 3 commits April 8, 2026 17:31
Two fixes validated with real Base Sepolia x402 payments between
two DGX Spark nodes running Nemotron 120B inference.
1. **CA certificate bundle**: The x402-verifier runs in a distroless
container with no CA store. TLS verification of the public
facilitator (facilitator.x402.rs) fails with "x509: certificate
signed by unknown authority". Fix: `obol sell pricing` now reads
the host CA bundle and patches it into the `ca-certificates`
ConfigMap mounted by the verifier.
2. **Missing Description field**: The facilitator rejects verify
requests that lack a `description` field in PaymentRequirement
with "invalid_format". Fix: populate Description from the route
pattern when building the payment requirement.
## Validated testnet flow
### Alice (seller)
```
obolup.sh # bootstrap dependencies
obol stack init && obol stack up
obol model setup custom --name nemotron-120b \
--endpoint http://host.k3d.internal:8000/v1 \
--model "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4"
obol sell pricing --wallet 0xC0De...97E --chain base-sepolia
obol sell http nemotron \
--wallet 0xC0De...97E --chain base-sepolia \
--per-request 0.001 --namespace llm \
--upstream litellm --port 4000 \
--health-path /health/readiness \
--register --register-name "Nemotron 120B on DGX Spark"
obol tunnel restart
```
### Bob (buyer)
```
# 1. Discover
curl $TUNNEL/.well-known/agent-registration.json
# → name: "Nemotron 120B on DGX Spark", x402Support: true
# 2. Probe
curl -X POST $TUNNEL/services/nemotron/v1/chat/completions
# → 402: payTo=0xC0De...97E, amount=1000, network=base-sepolia
# 3. Sign EIP-712 TransferWithAuthorization + pay
python3 bob_buy.py
# → 200: "The meaning of life is to discover and pursue purpose"
```
### On-chain receipts (Base Sepolia)
| Tx | Description |
|----|-------------|
| 0xd769953b...c231ec0 | x402 settlement: Bob→Alice 0.001 USDC via ERC-3009 |
Balance change: Alice +0.001 USDC, Bob -0.001 USDC.
Facilitator: https://facilitator.x402.rs (real public settlement).
Replace the third-party facilitator.x402.rs with the Obol-operated
facilitator at x402.gcp.obol.tech. This gives us control over
uptime, chain support, and monitoring (Grafana dashboards already
deployed in obol-infrastructure).
Introduces DefaultFacilitatorURL constant in internal/x402 and
updates all references: CLI flag default, config loader, standalone
inference gateway, and deployment store.
Companion PR in obol-infrastructure adds Base Sepolia (84532) to
the facilitator's chain config alongside Base Mainnet (8453).
Address #321 — LiteLLM reliability improvements:
1. Hot-add models via /model/new API instead of restarting the
deployment. ConfigMap still patched for persistence. Restart
only triggered when API keys change (Secret mount requires it).
2. Scale to 2 replicas with RollingUpdate (maxUnavailable: 0,
maxSurge: 1) so a new pod is ready before any old pod terminates.
3. PodDisruptionBudget (minAvailable: 1) prevents both replicas
from being down simultaneously during voluntary disruptions.
4. preStop hook (sleep 10) gives EndpointSlice time to deregister
the terminating pod before SIGTERM — prevents in-flight request
drops during rolling updates.
5. Reloader annotation on litellm-secrets — Stakater Reloader
triggers rolling restart on API key rotation, no manual restart.
6. terminationGracePeriodSeconds: 60 — long inference requests
(e.g. Nemotron 120B at 30s+) have time to complete.
Comment threadinternal/model/model.go Fixed
…uoting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: JeanDaniel Bussy <SilverSurfer972@gmail.com>
- model_hotadd_test.go: test ConfigureLiteLLM branching (API key →
restart vs empty → hot-add), payload construction, multiple entries,
and mock server acceptance of /model/new requests
- setup_ca_test.go: test CA bundle patch JSON structure, no-bundle skip,
and empty-file skip
- verifier_test.go: assert Description field ("Payment required for ...")
is present in 402 response body
@bussyjdbussyjd changed the title feat: LiteLLM zero-downtime config and hot-reload (#321)feat: LiteLLM zero-downtime, Obol facilitator default, x402 TLS fixApr 8, 2026
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Superseded by the validated integration branch \ and the \ prerelease cut from it. The release-candidate branch now carries the tested sell → discover → buy → settle path, updated docs/skills, and the final x402/buy-side fixes.

@bussyjdbussyjd closed this Apr 9, 2026
@OisinKyne
OisinKyne deleted the feat/litellm-zero-downtime branch April 23, 2026 19:26
Sign up for freeto 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.

2 participants

@bussyjd@github-advanced-security