The role declared sso.enabled false behind three suppressions, so the checks were switched off rather than satisfied. LiteLLM authenticates its own admin UI instead of sitting behind an oauth2 proxy, so the declaration is flavor oidc and the client id, the three Keycloak endpoints, the proxy base URL and the admin id all render together under one flag: a half-set group would let the gateway fall back to its own form with nothing complaining. The variants pin both states so the matrix runs with and without SSO.
The proof has to measure the departure, not the arrival: a gateway ignoring the client id answers /sso/key/generate with its own username and password form and never redirects, so the spec asserts the round trip to Keycloak and back. The guest spec became SSO-aware, since its canonical-domain and password-field assertions only hold where no SSO is configured.
Two more gateway defects: the key warm-up ran even where no model was served, falling back to openrouter/auto so the gateway then answered every prompt with 400, and it is now gated on a served-model expression mirroring the model selection itself. A lint requires every declared svc-ai backend to have a model_list branch, because a backend without a route is an alias nobody serves. probe_auth.py proves revocation rather than creation: it mints a throwaway key, revokes it, and fails unless a real consumer key survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four roles gained a canonical domain without a tor service block. Declare it
the way 84b22b37a8 did for n8n: bond 0.5, enabled on 'svc-net-tor' in
group_names, shared, with the variant pins alongside.
The nocheck directives carry the two exemptions: the service has no persona
surface for the playwright flag, and the sso-variant-tor combination requires
the onion in every variant, so the false polarity has no variant left to
live in.
hermes also moves its local http port from 8089 to 8092, because 8089
belongs to web-app-socialhome. That is a separate concern riding along in the
same file; splitting it would need interactive staging.
This is merge work rather than authored here: it follows the established
pattern and the port collision is real, but none of it has been deployed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The LiteLLM gateway ships its own admin UI, but svc-ai-litellm stays
headless: the UI is optional and public, the gateway is neither. Putting
both in one role would either make the UI mandatory or teach the gateway
domain logic it has no business knowing.
So this role runs no container at all. It loads sys-stk-front-proxy and
points domain, http_port and proxy_internal_port at svc-ai-litellm's
service entry. The third variable is what makes swarm work: compose
reaches the gateway through its published host port, swarm renders the
vhost against the internal port and routes over the overlay.
networks.local.force_bridge is set because a role without a container has
no overlay for openresty to join; the upstream resolves to
host.docker.internal and the gateway's published port instead.
provides: litellm_ui renames the registry entry, because
get_entity_name(web-app-litellm) collides with the key svc-ai-litellm
already owns and a duplicate aborts every service resolution repo-wide.
The coupling is one-way: the UI role pulls the gateway, the gateway knows
nothing about the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>