# Open WebUI Model Routing Diagnostics

When a user complains that Open WebUI shows a different model than expected (e.g., dropdown says gemma-26b-200k but response comes from qwen36-35b-mtp), trace through these layers.

## Layer 1: Upstream endpoints

Check which backends Open WebUI is connected to:

```bash
docker exec open-webui env | grep -E 'OLLAMA|OPENAI|LLAMA|ENDPOINT|URL|MODEL|HERMES'
```

Key env vars:
- `OPENAI_API_BASE_URL` — primary OpenAI-compatible endpoint (e.g., llama-swap at `:9292/v1`)
- `HERMES_API_URL` — Hermes gateway (if set, but may not actually be referenced in code)

## Layer 2: Stored backend config

Open WebUI's `config` table stores the connection details:

```python
# /tmp/webui.db → config table (JSON blob in 'data' column)
rows = conn.execute('SELECT * FROM config').fetchall()
for r in rows:
    data = json.loads(r[1])
    # Check: openai.api_configs.{idx}.model_ids — hardcoded model list
    # Check: openai.api_base_urls — actual upstream URLs
```

When `model_ids` is explicitly set in `api_configs`, Open WebUI **does not fetch `/v1/models`** from that upstream — it uses ONLY the hardcoded IDs. This means model visibility is controlled by the config, not the upstream.

## Layer 3: Per-message model binding

The `chat_message` table stores the actual model used for EACH message:

```sql
SELECT id, chat_id, role, model_id, created_at
FROM chat_message
WHERE chat_id = '<chat-id>'
ORDER BY created_at ASC;
```

- User messages: `model_id` is NULL
- Assistant messages: `model_id` is the exact model that generated that response

If all assistant messages use `qwen36-35b-mtp` but the chat metadata says `gemma-26b-200k`, the model was either switched mid-chat or the request was routed incorrectly.

## Layer 4: Chat metadata vs. message tree

The `chat` table stores the full chat as JSON. Key fields in the JSON:

```python
chat_json = json.loads(chat_row['chat'])
chat_json['models']          # Current model(s) shown in dropdown — NOT reliable for history
chat_json['history']['messages']  # Dict of {msg_id: {role, models, childrenIds}}
chat_json['history']['currentId'] # Current leaf in the message tree
```

Each message in the history tree carries its own `models` array — that's the model that was selected when that message was SENT. A user switching the dropdown mid-chat creates a fork:

```
user (model=['gemma-26b-200k'])
  └── assistant (model_id=gemma-26b-200k)
        └── user (model=['qwen36-35b-mtp'])  ← user switched here
              └── assistant (model_id=qwen36-35b-mtp)
                    └── user (model=['qwen36-35b-mtp'])
                          └── assistant (model_id=qwen36-35b-mtp)
```

The chat-level `models` array reflects the **currently selected** model, NOT the model that produced existing messages. When the user changes the dropdown and continues the same chat, new messages fork from the current leaf with the new model.

## Layer 5: llama-swap request logs

Confirm what Open WebUI is actually requesting:

```bash
journalctl -u llama-swap --no-pager -n 100 | grep "POST /v1/chat/completions"
```

The client IP tells you the source:
- `172.21.0.x` = Docker bridge (Open WebUI container)
- `127.0.0.1` = local process (curl, hermes-cli)
- `::1` = localhost IPv6

Check for:
- `200` status — succeeded
- `500` with "upstream command exited prematurely" — model crashed during request
- Error connecting to backend port — model wasn't loaded yet

## Layer 6: Thinking toggle + reasoning tags interaction

Open WebUI has a "Thinking" toggle in the chat UI. When ON, the middleware processes reasoning tags from model output:

```python
# middleware.py
DEFAULT_REASONING_TAGS = [
    ('<think>', '</think>'),
    ('<thinking>', '</thinking>'),
    ('<reason>', '</reason>'),
    ...]
```

**Key: this is rendering, not generating.** The middleware converts tags it finds in the model's output into `<details type="reasoning">` blocks displayed as "Thought for X seconds". The tags come from the MODEL, not from Open WebUI.

Qwen models (qwen36, qwen3-coder-next, etc.) natively emit `<thinking>` tags in their output even when llama.cpp has `--reasoning off`. The flag controls llama.cpp's server-side reasoning feature, NOT the model's training-driven output tokens.

**llama-swap's `sendLoadingState: true`** makes this worse: when loading a model, llama-swap injects loading-status jokes into the streaming response's reasoning field, which Open WebUI renders as "Thought for X seconds". The model then sees its own previous "loading" text in the chat history and continues riffing on it in subsequent thinking blocks.

### Fix for "thinking stuck" / wrong model

1. **Start a new chat** with the correct model (don't continue an old one)
2. If a chat has history from model A but the dropdown says model B, the visible messages are FROM model A — the dropdown only affects the NEXT message
3. **Turn off the Thinking toggle** in the chat settings if you don't want reasoning tags rendered
4. **Set `sendLoadingState: false`** in llama-swap config to stop loading-state injection interfering with reasoning display
