> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

> **Themelio compatibility reference.** This page describes the retained Themelio implementation or its historical design. Check the [parallel-operation guide](/compatibility/index.md) before applying it to the current platform.

# 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.md)

**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`            | `<issuer>/.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.md)).
:::

### 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 `<prefix_id>/<model_id>` internally, but the
root actor explicitly creates separate model records named `<resource_backend_id>.<model_id>`
with group-scoped access grants.

## Model naming and ACL

Models in Open WebUI are created by the root actor with the naming convention:

```
<resource_backend_id>.<model_name>
```

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": "<group_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/<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.md)).
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.

<!-- Additional sections (conversation history retention, admin UI workflows, model alias migration, staging/production differences) are intentionally deferred to a later session. -->

<details class="page-source"><summary>Page source</summary>

<a href="/source/docs/content/dd/0011-human-web-ui-and-agent-client-access.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
