--- title: "DD-0011: Chat UI and Client Access — Implementation" --- # DD-0011: Chat UI and Client Access — Implementation **Related ADR:** [ADR-0011: Chat UI and Client Access for Shared LLM Services](/adr/0011-human-web-ui-and-agent-client-access) **Status:** Draft **Date:** 2026-06-30 **Authors:** TBD ## Implementation branches | Branch | Key content | |---|---| | `local/llm-access-openwebui` | `open-webui-deploy` Ansible role; `llm-access-root-actor` connection/group/ACL management | | `local/llm-access-waldur-seed` | Local Waldur AI offering seed fixtures | All branches are stacked on top of earlier `local/*` branches and diverge from `origin/main` by approximately 110 commits total. :::warning Tested on a local Minikube cluster in **CPU mode only**. Full SSO login against the production AAI (GRNET MyAccessID) and multi-project isolation at scale require staging/production validation. ::: ## Open WebUI deployment Open WebUI is deployed by the `open-webui-deploy` Ansible role into the `open-webui` namespace using the official Helm chart. | Attribute | Value | |---|---| | Helm chart | `open-webui/open-webui` v14.11.0 | | Image | `ghcr.io/open-webui/open-webui:0.9.6` | | Namespace | `open-webui` | | Service | NodePort (port 80 / containerPort 8080) | | Persistence | 5 Gi PVC (`ReadWriteOnce`) for conversation history (SQLite) | Disabled features (set via Helm values): | Feature | Setting | |---|---| | Ollama backend | `ollama.enabled: false` | | Pipelines | `pipelines.enabled: false` | | Tika (file parsing) | `tika.enabled: false` | | Terminals | `terminals.enabled: false` | | Direct connections | `ENABLE_DIRECT_CONNECTIONS: false` | | Ollama API | `ENABLE_OLLAMA_API: false` | Open WebUI's **default** OpenAI connection (`openaiBaseApiUrl`) is set to LiteLLM's internal Service URL with a disabled key (`disabled-until-project-connection`). Per-project connections are provisioned separately by `llm-access-root-actor` (see below). ## OIDC / SSO configuration | Helm/env variable | Value | |---|---| | `sso.enabled` | `true` when Keycloak is present or `shared_oauth2_issuer` is set | | `sso.oidc.providerUrl` | `/.well-known/openid-configuration` | | `sso.oidc.providerName` | `"Keycloak"` | | `sso.oidc.scopes` | `"openid email profile"` | | `sso.groupManagement.groupsClaim` | `"project_ids"` (local shim only — see note) | | `ENABLE_OAUTH_SIGNUP` | `true` | | `DEFAULT_USER_ROLE` | `"user"` | | `ENABLE_OAUTH_GROUP_CREATION` | `true` | :::note The `project_ids` OIDC groups claim is a **local development shim** only. Keycloak is configured with a custom mapper that injects `project_ids` from seeded group memberships to exercise the SSO login flow end-to-end in the local cluster. In staging and production, the external Keycloak / MyAccessID realm does not carry project group data. Group membership is set exclusively by `llm-access-root-actor` via the membership sync loop (see [DD-0020](/dd/0020-llm-access-membership-source-of-truth)). ::: ### Local CA trust (local mode only) In local mode, an init container merges the local SSL proxy CA into the Open WebUI Python `certifi` bundle so that OIDC back-channel calls succeed despite the self-signed certificate. This init container runs only when `local_mode=true`; it is not present in staging/production. ## Per-project connection model Open WebUI stores its OpenAI-compatible backend connections in three parallel arrays in its internal config (retrieved via `GET /openai/config` and updated via `POST /openai/config/update`): ``` OPENAI_API_BASE_URLS: [url_0, url_1, ...] OPENAI_API_KEYS: [key_0, key_1, ...] OPENAI_API_CONFIGS: {"0": {config_0}, "1": {config_1}, ...} ``` One entry is maintained per Waldur project resource by `llm-access-root-actor`: | Field | Value | |---|---| | `base_url` | `http://litellm.litellm.svc.cluster.local:4000/v1` | | `api_key` | project-scoped LiteLLM virtual key (from Kubernetes Secret) | | `config.enable` | `true` (active resource) or `false` (terminated resource) | | `config.prefix_id` | `resource_backend_id` | | `config.model_ids` | list of allowed model names (from offering `available_models`) | When a resource is terminated, `llm-access-root-actor` sets `config.enable: false` and clears the `api_key` field; the entry is not removed (prevents index shift in the arrays). ### What the `prefix_id` controls Open WebUI uses `prefix_id` + `model_ids` to generate per-connection model entries. The resulting model IDs visible to users are `/` internally, but the root actor explicitly creates separate model records named `.` with group-scoped access grants. ## Model naming and ACL Models in Open WebUI are created by the root actor with the naming convention: ``` . ``` For example, project `buffermusic` with model `qwen2.5-0.5b` → model ID `buffermusic.qwen2.5-0.5b`. Each model has access grants set via `PUT /api/v1/models/model`: ```json { "access_grants": [ { "principal_type": "group", "principal_id": "", "permission": "read" } ] } ``` Where `group_id` is the Open WebUI group corresponding to the project (see below). Users who are not members of the group cannot see or use the model. When a resource is terminated or its model allowlist changes: - Removed models have their `access_grants` cleared (no group restriction → effectively hidden from all non-admin users; the root actor does not delete model records). - New models in the allowlist get the project group ACL applied. ## Group management One Open WebUI group per Waldur project resource: | Field | Value | |---|---| | Group `name` | `resource_backend_id` (e.g., `buffermusic`) | | Group `description` | Waldur project name (e.g., `"Buffer Music Project"`) | The group is created idempotently via `POST /api/v1/groups/create` if it does not exist; otherwise `POST /api/v1/groups/id//update` updates the description if it changed. Group membership is managed separately by the `membership_sync` agent (see [DD-0020](/dd/0020-llm-access-membership-source-of-truth)). The group creation and model ACL wiring happen during `project_create` / `project_update`; user additions/removals happen on every membership sync cycle (every 5 minutes). ## Request routing (UI path) When a human user chats in Open WebUI: ``` Browser → Open WebUI backend (session auth) → select connection where config.prefix_id matches the model's prefix → POST /v1/chat/completions to LiteLLM api_key = per-project virtual key → LiteLLM: check team budget, route to vLLM alias Service → LiteLLM: record spend in PostgreSQL ``` Open WebUI does not have direct access to the inference namespace or vLLM endpoints. All routing goes through LiteLLM. Budget enforcement is identical to the API path. ## API client access API clients (OpenAI SDK, Claude Code, Codex CLI) authenticate to LiteLLM directly using the per-project virtual key. The key is distributed via the Waldur offering: - Virtual keys **do not expire automatically** — there is no TTL. A key remains valid until explicitly rotated. - The `rotate_project_key` endpoint on the root actor (`service.py: rotate_project_key`) generates a brand-new LiteLLM virtual key via `POST /key/generate`, persists it to the project's Kubernetes Secret (bumping a `rotation_version` counter), updates the Open WebUI connection to use the new key, and then deletes the **previous** key from LiteLLM. Rotation is therefore immediate and destructive to the old key: any client still configured with the previous key starts failing right after rotation completes. - The Waldur offering exposes a "view API key" action that triggers this endpoint. Rotation is on-demand only — there is no automatic/periodic rotation schedule. - API clients configure their base URL to the LiteLLM external endpoint and use the key. For Codex CLI clients requiring `POST /v1/responses`: a separate `responses-proxy` service (from `local/codex-responses-api` branch) translates `/v1/responses` to Chat Completions. This is outside the LiteLLM gateway and the Open WebUI stack.