Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
169 commits
Select commit Hold shift + click to select a range
a8d7913
K0: exercise the kubeai backend against a real cluster
Erotemic Sep 24, 2026
711048b
K1: front the kubeai backend with the LiteLLM gateway
Erotemic Sep 24, 2026
b7bc3e8
Remove the unused profile-era KubeAI renderer
Erotemic Sep 24, 2026
e84c131
K2+K4: strict pod residency and shared crash diagnosis for kubeai
Erotemic Sep 24, 2026
791f4c8
K5: one served-name rule, shared GPU count, env on kubeai
Erotemic Sep 24, 2026
bbbe610
Journal: backend unification K0-K2, K4, K5
Erotemic Sep 24, 2026
ec124e4
Start branch for 0.7.2
Erotemic Sep 24, 2026
2de4a61
Merge branch 'main' into dev/0.7.2
Erotemic Sep 24, 2026
748e2ba
Merge branch 'main' into dev/backend-unification
Erotemic Sep 24, 2026
2c51376
G-A/B: move naming rules and the gateway's module code out of compose.py
Erotemic Sep 24, 2026
ba93ad9
G-C: a Gateway class owns the front door's state
Erotemic Sep 24, 2026
260ada6
Merge branch 'main' into dev/0.7.2
Erotemic Sep 24, 2026
4d849eb
Merge branch 'main' into dev/backend-unification
Erotemic Sep 24, 2026
96ffaf5
G-D: render_compose's front door becomes gateway.render_front_door
Erotemic Sep 24, 2026
f010ccf
Record the gateway extraction; drop two dead names in the controller
Erotemic Sep 24, 2026
0101a2d
Lesson: size-only test files should be sparse and cleaned up
Erotemic Sep 24, 2026
9e2061c
Add RTX 8000 Qwen profiling round 3
Erotemic Sep 24, 2026
32ef461
Merge branch 'dev/0.7.2' of github.com:AIQ-Kitware/infer_stack into d…
Erotemic Sep 24, 2026
af2e8fc
Make room for leased demand: idle keep-warm models give way on KubeAI
Erotemic Sep 24, 2026
08e61d5
Serialize every use of the shared ledger connection
Erotemic Sep 24, 2026
f03f6e9
TUI: stop repainting unchanged panes; batch log drawing; report stalls
Erotemic Sep 24, 2026
e96ad1c
Merge branch 'main' into dev/0.7.2
Erotemic Sep 24, 2026
13d8667
Merge branch 'main' into dev/backend-unification
Erotemic Sep 24, 2026
1fdfee8
Merge branch 'main' into dev/0.7.2
Erotemic Sep 24, 2026
8120605
Merge branch 'main' into dev/backend-unification
Erotemic Sep 24, 2026
da71846
Changelog: the TUI stops repainting what did not change
Erotemic Sep 24, 2026
4feff29
TUI: an activity line for running actions; faster launch and quit
Erotemic Sep 24, 2026
a3d18c1
Switch the CLI from scriptconfig to kwconf
Erotemic Sep 24, 2026
4def7e7
Remove the uv exclude-newer cutoff
Erotemic Sep 25, 2026
271d645
TUI: one Clean up key in the footer; gc --forget; log CLI equivalents
Erotemic Sep 25, 2026
d672bfa
TUI: shorten the four pane descriptions
Erotemic Sep 25, 2026
3fd3a37
TUI: show endpoints|models and leases|deployments as tabs
Erotemic Sep 25, 2026
fe88f74
Docs: Compose/KubeAI parity matrix and a roadmap to close the gaps
Erotemic Sep 26, 2026
d6a19c4
P1a: KubeAI acquires go through admission, as Compose's do
Erotemic Sep 26, 2026
d3ee494
Docs: a work queue for backend parity, ending in a UX audit loop
Erotemic Sep 26, 2026
184289c
P1b: one acquire path; the pre-admission branch is gone
Erotemic Sep 26, 2026
64f1e59
Queue: record the P1b commit
Erotemic Sep 26, 2026
c2d393b
P2: ps, logs, status and the TUI read the backend, on either backend
Erotemic Sep 26, 2026
46b49b7
Queue: record the P2 commit
Erotemic Sep 26, 2026
7a322c1
P3: the gateway in front of a cluster is the compose gateway
Erotemic Sep 26, 2026
487cab5
Queue: record the P3 commit
Erotemic Sep 26, 2026
1a4e4b2
P6: a parity suite, one test per row the matrix marks same
Erotemic Sep 26, 2026
c430caf
Queue: record the P6 commit
Erotemic Sep 26, 2026
94c263d
P4: KubeAI picks a resource profile by GPU size
Erotemic Sep 26, 2026
08dccd9
Queue: record the P4 commit
Erotemic Sep 26, 2026
49e70dd
P5: the gateway can run inside the cluster
Erotemic Sep 26, 2026
daf2476
Queue: record the P5 commit
Erotemic Sep 26, 2026
76c84aa
UX: "free a GPU first" only when a free GPU would have helped
Erotemic Sep 26, 2026
7fe76b3
Plan doc: a generic model name in the naming example
Erotemic Sep 26, 2026
e1c68f3
Docs: the README and two docs describe the current CLI
Erotemic Sep 26, 2026
00dbed1
Docs: mark the pre-leasing recipes as such; drop dead Makefile targets
Erotemic Sep 26, 2026
9eba980
UX: `logs --no-color` works
Erotemic Sep 26, 2026
ce48cc4
UX: Open WebUI runs as its data directory's owner
Erotemic Sep 26, 2026
977fa4e
UX: status says "pending" while a change is being applied
Erotemic Sep 26, 2026
6629c72
UX: the TUI picks up catalog edits made outside it
Erotemic Sep 26, 2026
0fc3f5d
UX: help and narration say what is true on both backends
Erotemic Sep 26, 2026
cc2907d
UX: failures name their cause, not a traceback
Erotemic Sep 26, 2026
fdc57c8
UX: an unreachable cluster says why, in one line
Erotemic Sep 26, 2026
be0b1fa
UX: the TUI works at 80x24 with the runtime pane open
Erotemic Sep 26, 2026
8bd07c8
UX: the TUI's API tab never shows the master key
Erotemic Sep 26, 2026
224501a
UX: the TUI says why a runtime command failed, and shows engine colors
Erotemic Sep 26, 2026
a62e5ae
UX: the README's first run works on a host with no GPU
Erotemic Sep 26, 2026
a8e66e9
Docs: the KubeAI profile example requests a GPU, as the README warns …
Erotemic Sep 26, 2026
0afbbaa
UX: the TUI's "Clean up" is "Clear finished"
Erotemic Sep 26, 2026
817d771
UX: `test` and `env` work on kubeai; readable TTLs; clean pipes
Erotemic Sep 26, 2026
aff378e
UX: kubeai narration and doctor say what they mean; an audit script
Erotemic Sep 26, 2026
96fc442
UX: kubeai's doctor checks the gateway's Docker too
Erotemic Sep 26, 2026
9696fa0
Docs: the manual describes the TUI's runtime pane and API tab as they…
Erotemic Sep 26, 2026
9bd3154
ps and the TUI show instance start times in local time
Erotemic Sep 26, 2026
4c77237
TUI: one gateway URL authority, tab keys, a sidebar that uses the width
Erotemic Sep 26, 2026
052fb36
TUI: the runtime pane grows when the terminal does
Erotemic Sep 26, 2026
91d36a2
A released stop deployment is reclaimed, not missing
Erotemic Sep 26, 2026
6495f5c
release: a summary that reads as a sentence
Erotemic Sep 26, 2026
d88a50c
queue: the abbreviation finding is decided, not open
Erotemic Sep 26, 2026
d4872ae
release/renew: a missing env-file is a message, not a traceback
Erotemic Sep 26, 2026
1692fdb
dev: the second k3s node reclaims a fixed 1Gi under disk pressure
Erotemic Sep 26, 2026
548486e
ps: GPUS says cpu or all, as leases does
Erotemic Sep 26, 2026
ff38cd3
queue: the UX audit passes (pass 8 found nothing new)
Erotemic Sep 26, 2026
7833668
queue: handover for the operator; the full kubeai e2e passes
Erotemic Sep 26, 2026
376784b
Decide /dev/shm and the pre-leasing recipes (queue 8a, 8b)
Erotemic Sep 26, 2026
1f58cc6
queue: reopen with review part 1 (replica residency, backend protocol…
Erotemic Sep 26, 2026
4555fcd
queue: add review part 2 (items 14-22); roadmap status is review hard…
Erotemic Sep 26, 2026
35be92c
Residency asks about deployments; one backend protocol; replica poiso…
Erotemic Sep 26, 2026
ee7bd7f
Reclaim only for capacity, only a victim that could help
Erotemic Sep 26, 2026
2face98
Keep the approved render until its publication completes
Erotemic Sep 26, 2026
8e4d9ed
Secret rotation restores the old key on any failure before apply
Erotemic Sep 26, 2026
b1e5a41
Retire routes before their upstreams go
Erotemic Sep 26, 2026
2473e95
Coalescing prefers what costs least: LIVE, then resident IDLE
Erotemic Sep 26, 2026
b2d003a
routes seed refuses to redefine an alias; one route-registry API
Erotemic Sep 26, 2026
9fa0116
A preview writes no secrets
Erotemic Sep 26, 2026
7e53162
Public seams instead of private reaches: lock, profile, marker
Erotemic Sep 26, 2026
e9e1cf9
Docs follow the hardened semantics; the replacement-gap poison case
Erotemic Sep 26, 2026
de7dea8
queue: the replacement-gap test fails on the old code too
Erotemic Sep 26, 2026
2d8785b
dev: journal and a lesson for the review hardening
Erotemic Sep 26, 2026
b1040f1
docs: date the review hardening 2026-09-26, the day it happened
Erotemic Sep 26, 2026
b1f5f62
queue: items 11-22 done; roadmap status: review hardening done
Erotemic Sep 26, 2026
0da932b
queue: reopen 11, 14, 15, 16, 21 and add 23 from the re-review
Erotemic Sep 26, 2026
4b8783b
KubeAI settles its host gateway after an interrupted apply
Erotemic Sep 26, 2026
4476573
Rotation keeps the old key when the apply cleanly misses the runtime
Erotemic Sep 27, 2026
768d924
An explicit re-approval is durable across its own partial apply
Erotemic Sep 27, 2026
b8b3542
Replicas: judged one by one, and serving is not residency
Erotemic Sep 27, 2026
55c1f96
Reclaim fails closed on scheduling it does not model
Erotemic Sep 27, 2026
30a62bb
A race-time routes seed conflict leaves no pending publication
Erotemic Sep 27, 2026
122f1b4
tests: the publish_change spy passes preflight through
Erotemic Sep 27, 2026
2f9adc4
Backend hooks: required state, common operations, typed capabilities
Erotemic Sep 27, 2026
abcd03d
queue: re-review items closed; roadmap and parity rows follow
Erotemic Sep 27, 2026
fde855e
dev: journal for the re-review pass
Erotemic Sep 27, 2026
4d54d60
queue: final closure items 24-27 (rotation x dynamic routing first)
Erotemic Sep 27, 2026
45e0e1c
Rotation completes under a running dynamic-routing gateway
Erotemic Sep 27, 2026
3abb2ff
The in-process backends keep last_planned_digest; the guard fails closed
Erotemic Sep 27, 2026
cfe6512
Docs say what leases health reports; the seed --replace race is recorded
Erotemic Sep 27, 2026
5337815
secrets rotate waits for the old key's rejection behind a rollout
Erotemic Sep 27, 2026
e8e9d42
queue: campaign 2 (external endpoints, access above leasing), queued …
Erotemic Sep 27, 2026
79a96e6
roadmap: review hardening closed (queue 11-27)
Erotemic Sep 27, 2026
0082538
queue: log everything still open outside the numbered items
Erotemic Sep 27, 2026
7e0e0b3
queue: campaign 2 design review folded in (item 28 decisions first)
Erotemic Sep 27, 2026
58df839
paths flags state directories another user (root) wrote
Erotemic Sep 27, 2026
fd11041
Campaign 2 design: published endpoints, derived routes, one access tr…
Erotemic Sep 27, 2026
bc390ad
ResolvedEndpoint: what an endpoint means, above its lease request
Erotemic Sep 27, 2026
131dd26
External endpoints in the catalog: an external: target, not an engine
Erotemic Sep 27, 2026
cbfc1da
One terminology: endpoint alias, upstream model, deployment id
Erotemic Sep 27, 2026
862c178
Published endpoint definitions outlive quiescence
Erotemic Sep 27, 2026
e697ccd
One GatewayRoute; gateway routes derived from the published catalogs
Erotemic Sep 27, 2026
2ac9a47
External keys by name on both gateways; a real-LiteLLM external e2e
Erotemic Sep 27, 2026
93823c6
Access above leasing: `infer-stack access`, typed connection info
Erotemic Sep 27, 2026
3562bcf
Show external endpoints as external: status, TUI catalog
Erotemic Sep 27, 2026
19fc8fd
External e2e covers access; bundles survive a member's redefinition
Erotemic Sep 27, 2026
501688e
Document external endpoints and access; audit covers access
Erotemic Sep 27, 2026
8797fdd
routes seed --replace is compare-and-swap; CLI decisions use typed me…
Erotemic Sep 27, 2026
09b72fa
External e2e runs on KubeAI's gateway too
Erotemic Sep 27, 2026
21ade9a
known-limitations: the in-cluster gateway routes only
Erotemic Sep 27, 2026
2af9c70
queue: campaign 2 review items 42-50
Erotemic Sep 27, 2026
df62475
Published endpoints commit with publication intent, never before
Erotemic Sep 27, 2026
852753b
Access is ready only once its routes are published
Erotemic Sep 27, 2026
7df1352
Missing keys refuse every publication; FrontDoorControl is the whole …
Erotemic Sep 27, 2026
5fc6e6c
Pinning protects every resident alias, not only catalog-defined ones
Erotemic Sep 27, 2026
7d158fe
routes list says it is desired state; api_base parsed strictly; no pr…
Erotemic Sep 27, 2026
93285f5
Journal and queue: campaign 2 review round closed
Erotemic Sep 27, 2026
7a01637
Add Kubernetes integration setup CLI
Erotemic Sep 28, 2026
2608d62
Keep Kubernetes integration distribution neutral
Erotemic Sep 28, 2026
81415c6
Add reversible Kubernetes Compose node handoff
Erotemic Sep 28, 2026
882758c
Add Kubernetes readiness setup and safe backend recovery transitions
Erotemic Oct 1, 2026
bc47baa
Secure K3s provisioning and correct Kubernetes readiness
Erotemic Oct 2, 2026
aad587a
Add efficient Kubernetes TUI monitoring and controls
Erotemic Oct 2, 2026
aee7d97
Respect KubeAI Model name limits with stable hashed shortening
Erotemic Oct 2, 2026
4a2c80b
Refresh GPU runtime canaries and clarify worker join readiness
Erotemic Oct 2, 2026
2dc083f
Separate KubeAI declaration, replica readiness and generation evidence
Erotemic Oct 2, 2026
51fcad9
Expose only supported KubeAI endpoint launch controls
Erotemic Oct 2, 2026
a3e08fd
Preserve Kubernetes generation evidence across polling and late respo…
Erotemic Oct 2, 2026
34e2c59
Add private K3s onboarding and named GPU worker acceptance
Erotemic Oct 2, 2026
97244dc
Document private worker onboarding and scoped GPU acceptance
Erotemic Oct 2, 2026
2c44ed3
Fix GPU holder scan races
Erotemic Oct 2, 2026
8cd0228
Merge branch 'dev/backend-unification' of github.com:AIQ-Kitware/infe…
Erotemic Oct 2, 2026
e77e675
Merge pull request #12 from AIQ-Kitware/dev/backend-unification
Erotemic Oct 2, 2026
132a1a8
Generalize HyperQwen GPU-class suggestions
Erotemic Oct 2, 2026
d6b3d21
Add notes
Erotemic Oct 3, 2026
2ba288c
Advertise the effective vLLM context window in LiteLLM route metadata
Erotemic Oct 3, 2026
6dfe393
Fix LiteLLM context metadata edge cases
Erotemic Oct 3, 2026
c4584d5
Document distributed infer-stack control plane plan
Erotemic Oct 3, 2026
6c841be
Link control-plane HA plan from known limitations
Erotemic Oct 3, 2026
eecaf66
Journal distributed control-plane planning
Erotemic Oct 3, 2026
df8f854
Merge pull request #14 from Erotemic/docs/control-plane-server-ha-plan
Erotemic Oct 3, 2026
4d195dd
Bump default vLLM image to v0.30.0-cu129
Erotemic Oct 3, 2026
edbf649
refactor: consolidate control-plane authorities
Erotemic Oct 3, 2026
60797d4
Fix K3s installer timeout
Erotemic Oct 7, 2026
56a0ab3
Update HyperQwen image and roomy GPU suggestions
Erotemic Oct 7, 2026
68f4b39
Record two-node KubeAI bring-up and harden worker cleanup
Erotemic Oct 7, 2026
318679d
Fix full-environment type checking
Erotemic Oct 7, 2026
3ba2d6f
Fix type checking diagnostics
Erotemic Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ jobs:
- name: Typecheck
run: |-
python -m pip install ty
pip install --prefer-binary -e ".[tests]"
# Install optional modules whose concrete types are part of the source
# tree. Otherwise unresolved-import=ignore turns them into Unknown and
# CI misses diagnostics that appear in a full local development env.
pip install --prefer-binary -e ".[tests,tui]" huggingface-hub ruamel.yaml
ty check ./infer_stack
build_and_test_sdist:
##
Expand Down
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ generated/*
!generated/kubeai/
generated/kubeai/*
!generated/kubeai/.gitignore
!examples/
!examples/single-node/
!examples/single-node/config.yaml
!examples/single-node/models.yaml

# rsync target for e2e harness reports (developer artifacts, not source)
e2e-report-*/
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ new entry; keep diffs small.

If a lesson seems important, but a MWE is not possible, label the lesson as
SPECULATIVE.

## Commit attribution

Every agent-authored commit must carry a `Co-authored-by:` trailer naming the exact model (and weights/config variant, when known) that produced it, per its own runtime identification — never copied from earlier commits; if undeterminable, say so.
377 changes: 376 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

28 changes: 6 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,34 +1,18 @@
PYTHON ?= python
EXAMPLE ?= single-node

init:
$(PYTHON) manage.py init

render:
$(PYTHON) manage.py render

deploy:
$(PYTHON) manage.py deploy
# Optional extra Helm values; the common case infers GPU profiles.
VALUES ?=

status:
$(PYTHON) manage.py status

smoke-test:
$(PYTHON) manage.py smoke-test

example-single-node:
cp examples/single-node/config.yaml ./config.yaml
cp examples/single-node/models.yaml ./models.yaml
@echo "Copied single-node example into repo root. Edit hostname/model ids as needed."
render:
$(PYTHON) manage.py render

bootstrap-k3s:
bash scripts/bootstrap_k3s.sh
$(PYTHON) manage.py kube bootstrap --provider=k3s --apply

install-kubeai:
bash scripts/install_kubeai.sh generated/kubeai/kubeai-values.yaml kubeai

bootstrap-single-node: example-single-node bootstrap-k3s
@echo "Single-node example copied and K3s bootstrapped. Next: python manage.py render && python manage.py deploy"
$(PYTHON) manage.py kube install --apply $(if $(VALUES),--values "$(VALUES)",)

port-forward-kubeai:
kubectl -n kubeai port-forward svc/kubeai 8000:80
1,393 changes: 497 additions & 896 deletions README.md

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions dev/aivm-pi-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# aivm × pi bridge: design notes (temporary home)

Status: **design only, not implemented.** This document codifies the bridging
logic that makes a user's aivm VM able to run `pi` against this stack's
LiteLLM gateway, so that whoever implements the script (possibly a stronger
agent) does not have to rediscover the ground truth. If this ever graduates
to a real feature, move it to `docs/` and pair it with an ADR; until then it
lives here, next to the temporary script it describes
(`dev/aivm_pi_bridge.sh` or `.py`, to be created).

## Purpose and ownership split

After `infer-stack acquire <model>` on a host, the user wants `pi` on a
managed aivm VM to work against that host's gateway with no manual
configuration. The tools each own one half of the truth:

- **infer-stack** knows the *endpoint*: the LiteLLM host port, the master
key, the gateway health, and the acquired model ids.
- **aivm** knows the *VM network*: which bridge network a VM is on, its
gateway IP, how to reach into the guest (`aivm ssh`), and the guest's
firewall exceptions (`firewall.allow_tcp_endpoints`).
- **pi** is a passive consumer: it needs a provider registration, a
credential, and a default model in its per-user config.

The bridge is a small script that composes the two tools' *public* surfaces.
Deliberately not: a first-class `infer-stack aivm` subcommand (feature creep
for a niche pairing — the stack must not know what aivm is), and not an
aivm-side hook that knows about LiteLLM (aivm must not know what
infer-stack is). Promotion criterion: a second consumer (another agent
client, multi-VM fleets, or a second target platform) is what would justify
a generic "clients/connectors" abstraction in infer-stack.

## Ground truth (verified 2026-10-02)

### The gateway (infer-stack side)

- Compose service `litellm`: image `ghcr.io/berriai/litellm:v1.82.3-stable`
(`infer_stack/config.py::PINNED_IMAGES`), container port 4000, host port
14042 (`DEFAULT_PORTS['litellm']`, user-configurable).
- The published port is a plain `f'{host_port}:4000'` mapping
(`infer_stack/leasing/gateway.py`), so Docker binds **0.0.0.0** on the
host (confirmed live: `0.0.0.0:14042->4000/tcp`).
- The master key is generated with an `sk-` prefix and stored in the
compose project env file: `API_KEY_ENV = 'LITELLM_MASTER_KEY'`,
`set_master_key()` / `master_key()` / `rotate_master_key()` in
`gateway.py`. The rendered LiteLLM config references it as
`os.environ/LITELLM_MASTER_KEY`.
- `Gateway.urls()` advertises `http://127.0.0.1:14042/v1` —
host-local only. A VM client needs the *host's bridge-side IP* instead
(the IP aivm calls the gateway IP).
- There is **no TLS/HTTPS anywhere** in the stack today. An optional nginx
reverse proxy exists (ports 80/443, `DEFAULT_PORTS['reverse_proxy_*']`)
and is the natural TLS-termination point if we ever want one.

### The client (pi side)

The provider is the npm extension **`pi-provider-litellm` v3.3.0**
("LiteLLM proxy provider extension for Pi", requires Pi 0.83+; MCP parts
need 0.99+). Verified against a live VM (`~/.pi/agent/`):

- **Install**: `pi install npm:pi-provider-litellm` — the *personal*
(global) install, writing the package declaration to
`~/.pi/agent/settings.json`. Note: `-l`/`--local` means the *project*
`.pi/settings.json`, which loads only after project trust is granted —
the wrong scope for a VM-wide setup. (The TODO in aivm's pi installer
currently says `pi install -l …`; it should drop the `-l`.)
- **Credential/endpoint config surfaces** (any one works; the first is what
a live, working VM shows):
1. `~/.pi/agent/auth.json` (mode 0600):
`{"litellm": {"type": "api_key", "key": "sk-…", "env": {"LITELLM_BASE_URL": "http://<gw>:<port>"}}}`
— the exact shape the extension's own `/login` persists.
2. `~/.pi/agent/settings.json`:
`{"litellm": {"providers": {"litellm": {"baseUrl": "http://<gw>:<port>", "apiKey": "…", "allowInsecureHttp": true}}}}`
(`apiKey` may be `$ENV` / `!command` / literal).
3. Env: `LITELLM_BASE_URL` + `LITELLM_API_KEY` (lowest precedence).
- **`allowInsecureHttp`** (default `false`; loopback is exempt) is the
current plain-HTTP workaround for non-loopback `http://` base URLs.
There is **no CA-cert / self-signed option** in the extension's documented
fields; trusting a self-signed cert would have to go through Node's
`NODE_EXTRA_CA_CERTS` (unverified against pi's Node 22) or an upstream
feature request.
- **Model discovery**: `/model/info` (admin) first, falling back to
`/v1/models`, then `/health` + per-route probes. Results persist to
`~/.pi/agent/models-store.json` (`litellmDiscoveryVersion` marker), so
the guest keeps a working catalog even if the extension is later absent.
`/litellm-refresh` re-discovers on demand. The extension also registers
LiteLLM MCP tools and Skills Gateway injection *by default* — fine for a
self-owned proxy, but the design should mention the trust note.
- **Default model/provider**: `settings.json` top-level
`defaultProvider` / `defaultModel` (e.g. `litellm` /
`qwen3.8-27b-dbirks-hyperqwen`).

### aivm side

- The machine store (host, `/var/lib/aivm` for fresh 0.6 installs) holds
`[[networks]]` records: `[networks.network]` (`name`, `bridge`,
`gateway_ip`, …) and `[networks.firewall]` (`enabled`, `block_cidrs`,
`allow_tcp_ports`, `allow_tcp_endpoints`, …). `[[vms]]` records carry
`network_name`, so the VM→network mapping is explicit
(`aivm/config_store/models.py::VMEntry`).
- `firewall.allow_tcp_endpoints` takes `IPv4:PORT` strings and generates:
an input-hook accept for traffic addressed to that host, and a
forward-hook accept on the **pre-DNAT** conntrack original tuple
(`aivm/firewall.py::_nft_script`). Because of that, one entry
`<gateway_ip>:<litellm_host_port>` covers the Docker-published port even
though Docker DNATs it to `<container_ip>:4000` (the pre-v0.6.0 failure
that made people add the *container* port to `allow_tcp_ports`; see
aivm `dev/journals/gpt56.md`, fix commit 42c8b47).
- Programmatic mutation is first-class:
`aivm.config_store.parse.parse_store_toml` →
`aivm.config_store.mutate.upsert_network` → render/write. Applying is
`aivm vm update --yes <vm>` (reconciles the managed nftables table live,
policy-fingerprint driven, no VM restart) or `aivm firewall apply`.
- Guest access is `aivm ssh <vm>` (managed access identity; verify the
exact non-interactive command form before scripting it — the CLI treats
it as a foreground session workflow). Pi itself is provisioned by
`aivm vm provision pi` (managed install under `~/.pi/agent`, official
installer, Node bootstrap).

### Concrete observed instance

This design was discovered from a live VM: default route `10.77.0.1`
(gateway), VM address `10.77.0.119`, `settings.json` with
`baseUrl: http://10.77.0.1:14042`, `allowInsecureHttp: true`, and 86
discovered models in `models-store.json` — i.e. the manual version of this
bridge already works; the script just automates and idempotently repeats it.

## The bridge algorithm

Runs on the **host**. Inputs: target VM name, `--model` (optional; default:
a sensible choice from the acquired endpoints), `--dry-run`, `--yes`.
Fail closed at each stage; never echo the master key.

**Host side**

1. Read infer-stack state: litellm enabled, host port, master key (env
file in the generated dir), and probe the gateway
(`/health` or `/v1/models` with the key). Abort with a clear message if
the gateway is down.
2. Read the aivm machine store; find the target VM's `network_name`; take
that network's `gateway_ip`.
3. Compute `endpoint = "<gateway_ip>:<litellm_host_port>"`; merge it into
that network's `firewall.allow_tcp_endpoints` (deduplicated; leave
`allow_tcp_ports` alone). Write the store back through aivm's own
parse/mutate/render round-trip so formatting, schema version, and
sibling records are preserved.
4. Apply: `aivm vm update --yes <vm>`; confirm the endpoint shows up
(`aivm firewall status` or `nft list`), and optionally TCP-probe the
endpoint from the guest.

**Guest side** (via `aivm ssh` or the managed ssh key)

5. Verify pi is provisioned (managed install present); if not, direct the
user to `aivm vm provision pi` (its installer already carries the TODO
for the plugin step).
6. `pi install npm:pi-provider-litellm` (global; idempotent).
7. Merge `~/.pi/agent/auth.json` (keep 0600): `litellm = {type: api_key,
key: <master key>, env: {LITELLM_BASE_URL: http://<gw>:<port>}}`.
Re-running after `rotate_master_key` is what resyncs the credential.
8. Merge `~/.pi/agent/settings.json`: `defaultProvider: "litellm"` and
`defaultModel: <id>` — JSON-merge only; never rewrite unrelated keys,
and skip the merge when values already match (idempotence).
9. End-to-end verification: one-shot non-interactive `pi` print of a trivial
completion through the gateway. Report success/failure with the
concrete commands used (auditability: the script logs what it ran).

**Shape**: ~150 lines of Python (aivm and infer-stack are both importable
on the host; prefer their Python APIs over parsing their output) or a shell
script if importability turns out to be an assumption. Secrets: write files
directly, log only redacted forms.

## HTTPS: options and recommendation

- **Option A — keep plain HTTP (status quo).** Inside the aivm sandbox this
is defensible: the firewall endpoint is scoped to one VM network, every
other private-range destination is blocked, and the traffic is between the
user's own VM and their own host. Residual risk is only the *host-side*
exposure of the 0.0.0.0 publish to whatever LAN the host sits on. Cheap
mitigation without TLS: publish the port bound to the bridge IP instead of
0.0.0.0 (compose `ports: ["10.77.0.1:14042:4000"]` — Docker supports
`IP:port:port`), so the gateway is reachable only from VM networks.
- **Option B — self-signed TLS terminated at the stack's nginx.** The
optional reverse proxy (443) generates a self-signed cert (SAN = the
bridge IP(s)), terminates TLS, and proxies to `litellm:4000`. The guest
trusts the cert via `NODE_EXTRA_CA_CERTS=<ca.pem>` on pi's Node runtime.
**Unverified**: whether pi's Node 22 build honors `NODE_EXTRA_CA_CERTS`
for the `fetch` used by the provider extension (undici historically lagged
on this). Alternatively request a `caCert` field upstream in
pi-provider-litellm.
- **Option C — native TLS in the LiteLLM container.** Check whether the
pinned image passes uvicorn `ssl_certfile`/`ssl_keyfile`; if it does, this
is simpler than B (no proxy hop) but gives the least control over cert
lifecycle.

**Recommendation**: ship the bridge with Option A (+ the interface-scoped
bind if easy) and treat B as a follow-up once the Node trust-store behavior
is confirmed with a 10-minute experiment on one VM. Do not block the bridge
on TLS: the aivm firewall already does the actual confinement.

## Open questions / verification checklist

- Exact non-interactive form of `aivm ssh <vm> <cmd>` (does it need a TTY?
does `--yes` cover the prompts?) — the script may fall back to plain
`ssh` using aivm's managed identity.
- Fix the aivm pi-installer TODO: `pi install -l …` → `pi install …`
(global, not project).
- `NODE_EXTRA_CA_CERTS` behavior with pi's bundled Node 22 (only for B).
- Docker `IP:port:port` publish on the target host (Option A mitigation).
- Multi-VM on one network: one endpoint entry covers *all* VMs on that
bridge (the entry is network-scoped, not VM-scoped) — say so in output.
- KubeAI backend is out of scope for the bridge (no compose publish;
ingress is a different story).
- What the script does when the user has *other* pi providers configured
(default: only touch `default*` when `--make-default` is passed? decide
during implementation — the observed live setup did set them).

## Confident / at risk

- **Confident**: the endpoint math (`gateway_ip:litellm_port`, one entry,
pre-DNAT matching), the pi config shapes (verified against a working
VM), and the aivm mutation/apply path (first-class APIs, live-reconciling
update). The manual procedure demonstrably works today.
- **At risk**: non-interactive `aivm ssh` scripting, `NODE_EXTRA_CA_CERTS`
(Option B), and the store write-back preserving every corner of a user's
hand-edited machine store (mitigate by going through aivm's own
parse/render and by `--dry-run` showing the exact store diff first).
2 changes: 1 addition & 1 deletion dev/e2e_tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Then rsync the printed results dir back; I review `report.md`.
### Before `--gpu` (one-time, so timeouts reflect logic not 10GB pulls)

```bash
docker pull vllm/vllm-openai:v0.19.1
docker pull vllm/vllm-openai:v0.30.0-cu129
docker pull ghcr.io/berriai/litellm:v1.82.3-stable
docker pull ollama/ollama:latest
```
Expand Down
38 changes: 38 additions & 0 deletions dev/e2e_tests/kubeai-cpu-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# KubeAI on a GPU-less machine, for exercising the kubeai backend for real.
# The chart's `cpu` profile runs vLLM's CPU image (needs AVX-512). Sized for
# a sub-1B model; raise the requests for anything larger.
resourceProfiles:
cpu:
imageName: "cpu"
requests:
cpu: 8
memory: "16Gi"
# Half the guest's 120 cores: only one such Model fits at a time, which is
# how dev/kubeai_e2e.sh's make-room check forces a scheduling conflict.
cpu-half:
imageName: "cpu"
requests:
cpu: 64
memory: "16Gi"
# Two "GPU sizes" on CPU, for the sizing check (E2E_SIZED=1 in
# dev/kubeai_e2e.sh): each selects the nodes labelled with one fake GPU
# product, as a real profile selects nvidia.com/gpu.product. `sized-80g`
# tolerates dev/k3s_agent_container.sh's taint, so it lands on that node.
sized-24g:
imageName: "cpu"
requests:
cpu: 8
memory: "16Gi"
nodeSelector:
nvidia.com/gpu.product: "FAKE-24G"
sized-80g:
imageName: "cpu"
requests:
cpu: 8
memory: "16Gi"
nodeSelector:
nvidia.com/gpu.product: "FAKE-80G"
tolerations:
- key: infer-stack.test/simulated
operator: Exists
effect: NoSchedule
Loading
Loading