Visitar URL original
sync: rebase flex onto upstream open-webui v0.11.3 by lgarceau768 · Pull Request #33 · flexion/open-webui · GitHub
Skip to content

sync: rebase flex onto upstream open-webui v0.11.3 - #33

Merged
lgarceau768 merged 1840 commits into
flexfrom
upstream-sync/v0.11.3-20260902-204900
Oct 8, 2026
Merged

lgarceau768 merged 1840 commits into
flexfrom
upstream-sync/v0.11.3-20260902-204900

Conversation

@lgarceau768

@lgarceau768 lgarceau768 commented Sep 2, 2026 •

Copy link
Copy Markdown

status merge order files changed commits additions deletions human review scope ci

Note

#35 merged 2026-09-11. flex0001_dup_email_repair now exists on flex, and GitHub confirms this PR is mergeable: MERGEABLE against the new tip — merge-base is the commit right before #35 landed, and #35's file was never touched on this branch, so it merges in clean. The only remaining blocker is the one required review (bdruth requested).

Tip

945 files, but only 11 need human eyes. 934 are a clean upstream rebase. The table in What to actually review is the job.

🎯 TL;DR

Rebases flex (v0.9.5) onto upstream open-webui v0.11.3. Opened manually because the sync workflow's gh pr create step failed — #34 fixes that, still open.

Item Detail
✅ Fixed Provider icons — dropped in the rebase, restored and expanded (Grok, GLM, Kimi/Moonshot, Claude)
✅ Fixed Google OAuth Groups reconciled onto v0.11.3 — including a bug that would have broken every Google login, caught before it shipped
✅ Verified Full app boot, frontend build, OAuth suite (7/7), icon spot-checks, chat smoke test
✅ Verified Live Google OAuth login, in the dev environment
🟡 Deferred Model Helper UI — code present and working, just has no button yet. Separate PR.
🟡 Deferred auths.py token_exchange OAuth gap; missing test/docs for the admin-group exemption

🔀 The failure mode this PR is really about

flowchart LR
    U["upstream v0.11.3"] --> R["automated rebase<br/>945 files<br/>ZERO conflict markers"]
    F["flex v0.9.5<br/>+ fork-only features"] --> R
    R --> OK["934 files<br/>clean"]
    R --> D["11 shared-source files<br/>resolved automatically"]
    D --> S["2 whole features<br/>SILENTLY DROPPED"]
    S --> X1["provider icons<br/>restored + expanded"]
    S --> X2["Google OAuth Groups<br/>restored + login bug fixed"]

    style S fill:#f8d7da,stroke:#721c24,stroke-width:2px
    style OK fill:#d4edda,stroke:#155724
Loading

A clean rebase with no conflict markers, but a whole feature quietly missing. This has now been caught twice. That is why the reviewer checklist asks for a manual diff rather than trusting the absence of <<<<<<<.

✅ Reviewer checklist

  • Do a real Google OAuth login test — the one thing that couldn't be verified locally
  • Diff the 11 files below for any other silently-dropped flex changes
  • npm run build / docker build on your own machine
  • Spot-check provider icons for your gateway's model ids
  • Land infra#583 and deploy to dev & main on current versions
  • Land infra#582 after infra#583, deploy, verify backups created
  • Land #34 before the next sync run

📋 What to actually review — the 11 shared-source files

File Status
backend/open_webui/utils/oauth.py 🔴 Feature regression + login-breaking bug found & fixed (round 3)
backend/open_webui/routers/models.py 🔴 Regression found & fixed (rounds 1–3)
.github/workflows/*.yml(.disabled) ×4 🟢 Reviewed, resolved (round 3)
backend/open_webui/config.py 🟢 Reviewed — no change needed (round 3)
backend/open_webui/routers/tasks.py 🟢 Reviewed, resolved (round 3)
src/lib/components/chat/Navbar.svelte 🟢 Reviewed, resolved (round 3)
.github/workflows/upstream-sync.yml ⚪ Not re-touched this round
backend/open_webui/utils/task.py ⚪ Not re-touched this round

🔧 Three rounds of fixes have landed on this branch

Rounds 1 & 2 — provider icons silently dropped

Two Flexion-only features were confirmed silently dropped by the conflict resolution. Both fixed directly on this branch, verified live against a real gateway (104 real model ids) in an isolated Docker container before pushing:

  1. Provider icons lost — backend/open_webui/routers/models.py + the entire static/static/providers/ asset directory. The /model/profile/image endpoint stopped resolving any provider logo and fell through to the generic favicon for every model. Restored the matching helpers (get_provider_from_model_id, get_provider_icon_path, _load_provider_config) and the provider PNGs, additive-only against upstream's rewritten endpoint (its MIME allowlist and arena-model lookup are untouched).

  2. Expanded provider coverage + a brand-icon fix, found while verifying the above against real gateway model ids: added xAI/Grok and Zhipu GLM (including the zai.glm-* id shape this deployment actually serves, not just the glm-* form), fixed Moonshot/Kimi matching (moonshotai. / kimi- aliases — the icon asset already existed, it just wasn't wired up), added a claude- alias for direct/non-Bedrock Anthropic ids, and swapped the Anthropic icon for the correct orange Claude mark.

Result: 102/104 live model ids resolve to a real provider icon. The 2 that don't are a Flexion custom model and the arena-model placeholder — neither should have one.

Round 3 — flex had drifted 24 commits ahead; branch reconciled

After rounds 1–2, flex moved ~24 commits ahead of where this branch was cut, including flex's own new upstream-sync CI automation and a config.py change — GitHub started reporting this PR as CONFLICTING. origin/flex was merged in and all 12 resulting conflicts resolved and verified locally before pushing.

config.py — false alarm, caught before merging. An initial analysis (based on a stale local v0.11.3 git tag that had been silently repointed to flex's own tip) concluded the opposite of the truth. After re-fetching real tags: flex is still on v0.9.5 for this file; this branch's config.py is already byte-identical to genuine upstream v0.11.3, including flex's one relevant change (reading OPENAI_API_CONFIGS from env as JSON — upstream already does this natively). Resolved to keep this branch's version unchanged.

oauth.py — the important one. flex's Google Groups OAuth feature (#12: Cloud Identity API group lookups, plus an admin-group-management exemption so an IdP can't silently strip an admin's configured groups) was threaded onto the real v0.11.3 base across all 7 conflicting regions, call-site by call-site.

Along the way: caught and fixed a bug that would have broken every Google OAuth login on this branch — flex's code read GOOGLE_OAUTH_SCOPE.value, but upstream v0.11.3 made this setting a plain string, so .value would AttributeError on every callback.

test_oauth_google_groups.py was also repaired (written against a since-removed AppConfig/auth_manager_config pattern and an older Groups API) and now passes 7/7.

Everything else — 4 disabled workflow files, publish-flex-image.yml, routers/models.py, routers/tasks.py, Navbar.svelte, the provider assets — resolved additively: kept this branch's already-tested icon work and the v0.11.3 model-selector relocation, brought forward flex's CI workflow updates, caught one duplicate-function false-conflict in models.py (both sides had defined the same provider-icon helpers — kept exactly one copy).

Model Helper note: flex's "help me pick a model" modal came through this merge via ordinary non-conflicting auto-merge, since it's real, existing code on flex. Rather than strip it back out (which would just regress it again next merge), added the one import it needed to avoid a NameError. It is fully dormant — no UI wiring triggers it — so nothing user-facing changes today.

Verification performed: full app import/boot in an isolated Docker container (proving config.py + oauth.py + models.py + tasks.py — three separately-resolved files — actually work together), frontend build (vite build, 6349 modules), OAuth test suite (7/7), provider-icon spot checks, and an email/password login + chat smoke test.


⚠️ Merge order — this PR is one half of a pair

# PR What it adds Must merge
1 #35 flex0001_dup_email_repair, chained onto a0b1c2d3e4f5 first — ✅ merged 2026-09-11
2 this PR (#33) The v0.11.3 chain, with 3c9b0ca343fd repointed onto that revision second — ⏳ awaiting review

Verified against the real Alembic resolver (1.19.2), reading all 59 revisions:

State alembic heads
✅ #35 then #33 ['d4c1a8e37b62'] — one head
❌ this PR merged without #35 KeyError: 'flex0001_dup_email_repair'
❌ #35 merged, repoint reverted ['d4c1a8e37b62', 'flex0001_dup_email_repair'] → CommandError: Multiple head revisions are present

That last state is the dangerous one — a boot-time crash loop, with an error that names nothing useful.

Requirements before merging

  • fix(migrations): repair duplicate user emails as a restore guard #35 merges to flex before this PR. Merged 2026-09-11 (3459d519). Confirmed via the compare API: merge-base with flex is d290077 (the commit right before fix(migrations): repair duplicate user emails as a restore guard #35), flex0001_dup_email_repair.py doesn't exist on this branch at all, so it comes in cleanly from flex's side with zero conflict.
  • The FLEXION FORK EDIT comment on 3c9b0ca343fd's down_revision survives future upstream syncs. upstream-sync.yml rebases, so a conflict-resolver restoring upstream's 'a0b1c2d3e4f5' silently reintroduces the two-head crash with nothing to catch it.
  • test_single_head stays in the suite. The only automated tripwire — there is no CI on flex: backend.yaml and frontend.yaml fire only on main/dev, and statusCheckRollup on both PRs is empty.
  • This PR needs one approving review. BLOCKED on REVIEW_REQUIRED with zero reviews — not a failing check. Requested from bdruth (approved fix(migrations): repair duplicate user emails as a restore guard #35). The requirement isn't visible in repo rulesets or CODEOWNERS (rules/branches/flex returns []), so it is an org-level policy invisible to this token.
🖼️ Consequence for the image already in ECR

v0.11.3-flex.1 was pushed on 2026-09-03; flex0001_dup_email_repair was written on 2026-09-10. So the image currently on dev, and the one infra#596 would put on prod, does not contain the repair migration.

Fine for the upgrade itself — both databases are already clean — but the restore guard does not take effect until a new image is built from a flex that includes it. Until then, restoring a pre-2026-09-03 backup still resurrects the eight duplicate accounts and re-blocks the upgrade.


🟡 Deferred, not blocking

Four follow-ups
  • Model Helper UI — the modal/endpoint exist on this branch but nothing triggers them yet. A restoration + cost-tier-display plan is drafted (an admin-settable Low/Medium/High badge, since no pricing data exists anywhere in this codebase today) but not implemented. Separate PR.
  • auths.py's token_exchange endpoint (pre-existing, untouched here) doesn't thread Google Groups info or the admin exemption the way the main OAuth callback now does.
  • The admin group-management exemption (user.role != "admin" in oauth.py) has no automated test and isn't in docs/oauth-google-groups.md. Verified by code reading only — highest-value item for a human to specifically exercise.
  • Dockerfile's commented-out NODE_OPTIONS memory bump caused repeated vite build OOMs during local verification. Not changed here; worth revisiting before this hits CI.

tjbck and others added 30 commits August 23, 2026 02:34
…ch (open-webui#28943)

The DuckDuckGo search path catches RatelimitException from the ddgs library. That exception is defined by the library but never raised anywhere in it, checked against the pinned 9.14.4 and against 9.11.3, so the handler could never run. The two fallbacks around it were dead for the same reason: ddgs.text() returns a non-empty list or raises, so None and an empty list are not outcomes it can produce.

Removing all three leaves one call and changes nothing observable. A refused or rate limited search already came out as a failed search, with the error shown to the user and the traceback in the log, and it still does.

The backend argument is now passed as backend or 'auto' rather than conditionally omitted, because 'auto' is the library's own default for that parameter, so every configured value including unset and empty resolves exactly as before. Verified by running the old and the new function side by side against a stubbed library covering normal results, the domain filter, all four backend settings and a failing search, with identical results in every case.
…f 400 (open-webui#28942)

Any failure during a web search comes back to the client as a bare HTTP 500 with nothing in it. The handler tries to build a 400 whose detail is the caught exception object itself, FastAPI cannot serialise that into a response body, so rendering the error response fails and the request falls through to the generic 500 handler. In chat this surfaces as a web search that fails with no explanation at all, and the most common trigger is simply selecting a search engine without configuring its API key.

This routes the failure through the standard error formatter, which is what the sibling handler for content loading failures in the same function already does. Web search failures now return 400 with a readable message, and the exception itself keeps going to the server log exactly as before.

Passing str(e) into the response was the other option and was rejected: the rest of the backend deliberately keeps provider exception text out of client responses and in the log, and provider exceptions here can carry request details that should not be echoed back.
…#28948)

The web search error message was a lambda with a passthrough branch that returned whatever it was handed. Since open-webui#28942 both call sites pass no arguments, so that branch is unreachable, and it is the trap that let a caller drop a raw exception object into an HTTP response body and turn an intended 400 into an unserialisable 500.

A plain string constant removes the trap and lines the message up with every other fixed message in that file. Behaviour is unchanged: the response detail comes out byte for byte identical, because the enum already overrides __str__ to render members as their value. Verified on Python 3.11 and 3.12, both producing the same string and the same JSON body.
…n-webui#27671)

Task lists in Notes serialized to markdown as `- [ ] [ ]` with the item text pushed onto a separate line after a blank line, so previewing or downloading a note produced a broken checklist, and checking an item left the second `[ ]` behind as plain text.

TipTap renders each task item as a checkbox inside a label plus a block-wrapped body. The GFM turndown plugin matches that checkbox and emits its own `[ ]`, which landed next to the marker the task item rule already writes, and the block wrapper left blank lines around the text that the old leading-whitespace strip could not remove.

Register a rule that drops the checkbox so the task item rule is the only source of the marker, and trim the block wrapper while indenting continuation lines so nested lists and code fences stay inside the item.

Fixes open-webui#26067
The ComfyUI workflow.json Edit drawer (CodeEditorModal -> Drawer) used
z-999, while the admin Settings dialog (Modal) uses z-9999. Since both
are appended to <body>, the drawer rendered behind the settings dialog
and appeared to open 'in the background' (open-webui#27647).

Add an optional zIndexClass prop to Drawer (default z-999, preserving
existing behaviour for all other callers) and pass z-99999 from
CodeEditorModal so the editor surfaces above any enclosing modal.
…ntent (open-webui#28840)

A stream filter function, or a provider that puts something other than a string in a delta, makes the streaming handler concatenate a string with a non-string. That raises TypeError, and the broad handler wrapped around the whole per-chunk block swallows it at debug level and moves on. The chunk's text never reaches the message the user sees, and nothing above debug level says why.

The content and reasoning fields are now coerced to text once, where they are read off the delta, ahead of every consumer. The coercion is guarded on truthiness, so falsy values such as an empty list still skip the block exactly as before, and the accumulated content receives byte for byte what it received previously.

Checked against 14 delta shapes covering strings, empty values, numbers, booleans, None, lists, dicts and a content array: the truthiness gate and the accumulated content are identical before and after.
…n-webui#28859)

`x in d` and `x in d.keys()` are identical for a plain dict, so the `.keys()` call builds a throwaway view and reads as if it were doing something. Both sites operate on a plain dict: `combined` in `merge_and_sort_query_results` is a local `dict()`, and `ui_settings` comes from `UserSettings.model_dump()` where `ui` is annotated `dict | None` and is already guarded against None on the preceding line.

No behaviour change, and no measurable speedup either, so this is a readability cleanup rather than a performance one.

Sites where `.keys()` is load-bearing are left alone: the `list(d.keys())` snapshots taken before mutating during iteration, and the places where `.keys()` is the iteration or comprehension source rather than a membership test.
75 entries in the French locale had an empty value. i18next falls back to the
key when a value is empty, so French users were shown raw English strings
across permission settings, empty states, form placeholders and error toasts.

- fill those 75 entries; no key is added, removed or reordered
- follow the conventions already present in the file: "Entrez ..." for input
  hints, "Échec de ..." for failures, Chat -> Conversation, Token left as is
- interpolation placeholders preserved; no existing translation modified
…open-webui#27663)

The timer scheduler polls once a second and cancels on every message send and chat open, the sidebar lists chats ordered by `updated_at`, and the folder badges count unread chats per folder. None of those could be served by an index, so each call read most of the `chat` table, and because `meta` sits after the chat payload column SQLite had to walk every row's overflow pages to get there. On a large history that stalls the sidebar, every chat switch and every send, and the idle poll alone burns about a quarter of a CPU core.

Timers now keep their due time in a dedicated `chat.timer_at` column behind a partial index, and the chat list, unread and unfinished-reply queries each get an index matching their filter and ordering. Existing pending timers are backfilled from their meta by the migration. Dropping the `internal` and `type` checks also makes a forked timer chat inert, where a fork used to copy `meta` verbatim and become a second claim target that could fire a duplicate timer.

Measured on SQLite, same rows returned:

| query | before | after |
|---|---|---|
| idle timer poll (2000 chats, 0.43 GB) | 170 ms | 0.04 ms |
| cancel on send and chat open (4000 chats, 377 MB) | 200 ms | 0.04 ms |
| sidebar chat list (15000 chats, 1.26 GB) | 157 ms | 1.8 ms |
| folder unread badges (15000 chats, 1.4 GB) | 54 ms | 0.2 ms |

PostgreSQL 17 serves all of them as index-only scans with no sort node. Exercised through fresh install, upgrade with seeded data, downgrade and re-upgrade on SQLite and PostgreSQL 17.

Fixes open-webui#27622
…streamed chunk (open-webui#28861)

Streamed responses are scanned for reasoning and code interpreter tags. To work out where the last complete tag ended, the scanner searched backwards from the start of the accumulated text on every chunk, once per tag set. Ordinary prose contains no angle bracket, so that search never stopped early and read the entire response back every time. The cost grows with the square of the response length, and this scanning is on unless a model turns it off.

The two positions are now carried forward as the text grows, so each chunk only scans the characters it added.

Measured on CPython 3.12, a 270 KB response streamed in 27000 chunks:

| response text | before | after |
|---|---|---|
| no newlines | 7690 ms | 40.6 ms |
| with newlines | 5695 ms | 41.7 ms |

The carried positions match a full rescan at every step of 36282 randomized replays, covering text with no markers, newlines only, dense markers, real tags and truncation part way through.
…ough (open-webui#27675)

* fix: use the pooled client timeout for the Anthropic Messages passthrough

The native `/api/v1/messages` passthrough still referenced `openai.AIOHTTP_CLIENT_TIMEOUT`, which stopped existing when `routers/openai.py` moved onto `session_pool.get_client_timeout()`. Every passthrough request therefore raised `AttributeError: module 'open_webui.routers.openai' has no attribute 'AIOHTTP_CLIENT_TIMEOUT'` before it was sent, and the surrounding handler turned that into a 502 "Open WebUI: Server Connection Error", so Anthropic-format clients such as Cline could not reach any model at all.

Use `get_client_timeout(stream=...)` like the OpenAI and Ollama proxies do, so the configured `AIOHTTP_CLIENT_TIMEOUT` applies and streaming requests additionally get the idle-read timeout.

Fixes open-webui#27595

* fix: authenticate native Anthropic requests with x-api-key

The Anthropic Messages passthrough and the token-count forwarding both build their upstream request through `get_anthropic_request_target`, which sends the connection key as `Authorization: Bearer <key>`. Anthropic's OpenAI-compatible `/chat/completions` endpoint accepts that, which is why the model works in the chat UI, but the native `/v1/messages` and `/v1/messages/count_tokens` endpoints do not: they require the key in `x-api-key` and reject a bearer token with 401 `Invalid bearer token` (and `jwt auth is not yet supported on count_tokens`). They also require an `anthropic-version` header, which was never sent.

For `api.anthropic.com` connections, send `anthropic-version` and move the key into `x-api-key`, dropping the bearer header. Connections using session, OAuth or Entra ID auth keep their token untouched, LiteLLM passthrough connections are unaffected, and admin-configured custom headers still win over both defaults.

Fixes open-webui#27695
tjbck and others added 7 commits August 31, 2026 10:47
Keeps the entries already on dev and merges in the two this branch carried, then
puts the section back into the shape the format asks for.

Added now holds the contrast accessibility mode gives the dropdown menus, their
submenus and the model picker, followed by the general improvements placeholder
and the translation entry in their reserved positions. The translation entry
moves out of Changed, takes the standard wording and drops its link, which that
entry never carries; Changed is left out, having nothing else in it.

Each entry is condensed to a single sentence, the backticked column name becomes
a quoted one, and the interface font entry gains the commit it came from, having
carried no reference at all. Fixed is ordered by reach, from the conversation
branches and the failed upgrade down to the font and the disconnect control.
Manual workflow (workflow_dispatch only) that builds the Flexion-customized
Docker image for linux/arm64 and pushes it to AWS ECR under the chosen
environment's repository (open-webui-dev or open-webui-prod) with the
caller-supplied version tag.

Design notes:
- ARM-native runner (ubuntu-24.04-arm) matches the Fargate ARM deploy
  target — no QEMU overhead.
- Only linux/arm64 is built. Multi-arch would double build time for no
  current benefit; can be extended later if x86 deploys are needed.
- AWS auth via OIDC: assumes GitHubActionsOpenWebUIDev for environment=dev
  and GitHubActionsOpenWebUIProd for environment=prod. The prod role's
  trust policy was extended in flexion/flexion-open-webui-infra#461 to
  accept tokens from this repo's flex branch.
- Tag overwrite guard: refuses to push if the tag already exists in ECR.
  Prevents accidental republishes that would mask source-of-truth
  provenance. Operator must delete the existing tag manually to retag.
- ECR tag = upstream release tag verbatim (e.g. v0.9.5, not v9.5) — the
  0. prefix is preserved end-to-end through to the CDK pin in the infra
  repo.
@lgarceau768 lgarceau768 self-assigned this Sep 2, 2026
@lgarceau768
lgarceau768 changed the base branch from main to flex September 2, 2026 20:54
flex-upstream-sync-bot added 4 commits September 2, 2026 22:16
…11.3 sync

The v0.9.5 -> v0.11.3 upstream sync's automated conflict resolution on
backend/open_webui/routers/models.py silently dropped Flexion's per-model
provider icon feature, and the entire static/static/providers/ asset
directory never made it onto the sync branch. As a result
GET /api/v1/models/model/profile/image always fell through to a 302
redirect to /static/favicon.png, so the model picker showed the generic
Open WebUI logo for every model instead of the provider logo.

Restored:
- static/static/providers/ (16 provider PNGs + providers.json) from flex.
  These are copied into STATIC_DIR at startup by config.py's
  FRONTEND_BUILD_DIR/static glob, so no build changes are needed.
- _load_provider_config(), get_provider_from_model_id() and
  get_provider_icon_path() in routers/models.py, verbatim from flex.
- The get_provider_icon_path() FileResponse fallback in
  get_model_profile_image(), in its original position just above the
  favicon redirect.

Upstream's v0.11.3 logic in that endpoint (arena-model lookup via
Config.get, the profile-image MIME allowlist, and the http/data:image/
static-redirect branches) is untouched.
…tching

Follow-up to the provider-icon restore. Four gaps in the icon matching,
all verified against the 104 model ids our OpenAI-compatible gateway
actually serves (102 now resolve; the 2 that don't are a Flexion custom
model and the built-in arena-model placeholder, neither of which should
have a provider icon).

- xAI / Grok: new provider key "xai" -> grok.png, plus a "grok-" alias so
  bare ids like 'grok-4-latest' resolve. The dotted Bedrock form
  'us.xai.grok-4-fast' resolves via the existing first-segment logic.
- GLM / ChatGLM: new provider key "glm" -> glm.png plus a "glm-" alias.
  The alias is REQUIRED, not cosmetic: 'glm-4.5' contains a literal dot
  in its version, so without the alias the dot-split fallback yields a
  bogus provider 'glm-4' and no icon. Also added "zai" -> glm.png, since
  the gateway serves GLM as 'zai.glm-4.7-flash' / 'zai.glm-5' rather than
  the bare form.
- Moonshot / Kimi: no new asset needed; wired the existing moonshot.png
  up via a "moonshotai." alias (the gateway's vendor segment is
  'moonshotai', not 'moonshot') and a "kimi-" alias for bare ids.
  'moonshotai.kimi-k2.5' previously fell through to the favicon.
- Anthropic: replaced anthropic.png with the orange Claude mark (it was a
  generic black Anthropic wordmark) and added a "claude-" alias so direct
  Anthropic API ids like 'claude-3-5-sonnet-20241022' — which have no
  'anthropic.' segment — resolve, closing the same gap the 'gpt-' alias
  closes for OpenAI.

New icons are sourced from lobehub/lobe-icons (simple-icons has no xAI or
GLM entry), normalized to 128x128 on white to match the existing set.
They are real PNGs rather than the JPEG-with-.png-extension the existing
16 assets use, which matches the endpoint's declared image/png.

providers.json gains _alias_note / _glm_note so the non-obvious aliases
are not deleted later as redundant.
….11.3-20260902-204900

Round 2: Grok/xAI + GLM providers, Kimi and Claude matching fixes,
orange Claude mark replacing the generic Anthropic wordmark.
@lgarceau768 lgarceau768 changed the title Upstream sync/v0.11.3 20260902 204900 sync: rebase flex onto upstream open-webui v0.11.3 Sep 3, 2026
flex-upstream-sync-bot and others added 4 commits September 3, 2026 12:07
…licts

Brings flex's Flexion-specific work forward onto the real upstream v0.11.3
base, resolving all 12 conflicted paths from the divergence between the
sync branch and flex (~24 commits ahead).

Resolutions:

- backend/open_webui/utils/oauth.py + test/test_oauth_google_groups.py:
  flex's Google Groups OAuth feature (PR #12) fully re-threaded onto the
  v0.11.3 base; 7/7 tests passing. Also fixes a latent bug that would have
  broken every Google login: v0.11.3 made GOOGLE_OAUTH_SCOPE a plain string
  rather than a PersistentConfig, so the old `.value` access would raise
  AttributeError.

- backend/open_webui/config.py: kept the v0.11.3 upstream file unchanged
  (zero content change). An earlier plan believed flex's copy was newer;
  that was a false premise caused by a stale local v0.11.3 git tag pointing
  at flex's own tip. After `git fetch upstream --force --tags`, HEAD's file
  is confirmed byte-identical to the real upstream v0.11.3 tag.

- .github/workflows/{build-release,docker-build,format-backend,
  format-build-frontend}.yml(.disabled) and publish-flex-image.yml: kept
  flex's versions — the deliberately-renamed inert CI files plus flex's
  newer GitHub Action version pins.

- static/static/providers/{providers.json,anthropic.png}: kept the sync
  branch's versions, which carry this session's already-tested provider-icon
  work (Grok, GLM, Kimi/Moonshot, orange Claude mark). flex only has the
  stale pre-fix copies.

- backend/open_webui/routers/models.py: diff-alignment false conflict.
  Both sides independently define _load_provider_config,
  get_provider_from_model_id and get_provider_icon_path at nearly the same
  offset; verified byte-identical and kept exactly one copy rather than
  concatenating (which would have produced duplicate defs). Import regions
  reconciled to HEAD's modern style — every name from flex's side confirmed
  already bound.

- backend/open_webui/routers/tasks.py: kept HEAD's modern import lines; all
  names from flex's block confirmed already present.

- src/lib/components/chat/Navbar.svelte: kept HEAD entirely. flex still has
  the pre-relocation model-selector + helper-button UI; moving the model
  selector into MessageInput.svelte is an accepted upstream v0.11.3 change.

Note on Model Helper: the feature exists only on flex, so this merge carries
it forward through non-conflicting regions (constants.py enum, tasks.py
endpoint + template, utils/task.py, apis/index.ts, ModelHelperModal.svelte).
The one missing import was added so the merged code is coherent — without it
tasks.py would raise NameError. Its UI entry point is absent (Navbar kept
HEAD's version), so the modal is not yet reachable; giving it a new home in
MessageInput.svelte remains the separate reintroduce-model-helper work.
The frontend build stage OOMs on our ARM CI runner ("FATAL ERROR:
Ineffective mark-compacts near heap limit" during `vite build`),
confirmed while publishing this branch to ECR for dev deployment.
Upstream ships this line commented out too (same in the real v0.11.3
tag) - just needed for our build environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Runner has 4 CPU/15.5GB RAM, plenty of headroom - the 4096MB V8
old-space ceiling was just too low for this bundle's actual memory
needs, not real system memory pressure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…b1c2d3e4f5

Repoints upstream's first v0.11.x revision, 3c9b0ca343fd, from
'a0b1c2d3e4f5' to 'flex0001_dup_email_repair'.

Without this, merging both this sync and #35 leaves TWO ALEMBIC HEADS.
'a0b1c2d3e4f5' is the fork's current migration head, and it would be the
parent of both upstream's 3c9b0ca343fd and the fork's new
flex0001_dup_email_repair. backend/open_webui/config.py:74 calls
command.upgrade(cfg, 'head'), which does not tolerate that:

  CommandError: Multiple head revisions are present for given argument
  'head'; please specify a specific target revision, '<branchname>@Head'
  to narrow to a specific head, or 'heads' for all heads

That is a boot-time crash loop, not a degraded start, and it appears only
after both PRs have landed - the point at which everyone believes the work
is done. Reproduced against the real Alembic resolver, not reasoned about.

Chaining the v0.11.x series onto the repair instead linearises the graph
and puts the repair ahead of all 16 v0.11.x revisions rather than merely
ahead of the unique-email index. A database restored from a pre-2026-09-03
backup is stamped at a0b1c2d3e4f5, so the repair becomes the first
revision applied - before f0bd01a18a3d_add_unique_normalized_user_email_index,
the revision those duplicate rows would otherwise block.

ORDERING REQUIREMENT: #35 must merge to flex before this PR does.
flex0001_dup_email_repair exists only on fix/duplicate-user-email-repair-migration.
Until it lands on flex, this branch's chain references a revision that is
not present and Alembic raises KeyError: 'flex0001_dup_email_repair'.
That failure is deliberate and is the safer of the two: it names the
missing revision, so it points at the change that is absent, whereas
"multiple head revisions are present" names nothing. The repair migration
is deliberately NOT copied onto this branch - doing so would land a
destructive user/auth row merge through a PR titled "upstream sync",
bypassing the review #35 exists to receive.

Verified: with flex0001 present, `alembic heads` -> ['d4c1a8e37b62'],
exactly one head, 59 revisions; #35's test_single_head passes. With the
repoint reverted, that same test fails with two heads.

Refs #35
Refs flexion/flexion-open-webui-infra#580

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLRr6UnzNNqtSQZPFKZjig
@lgarceau768

Copy link
Copy Markdown
Author

Alembic ordering: v0.11.x now chains onto the fork's duplicate-email repair

Pushed f22720578 — a one-file change to backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py:

-down_revision: Union[str, None] = 'a0b1c2d3e4f5'
+down_revision: Union[str, None] = 'flex0001_dup_email_repair'

plus a comment block above it explaining why the line diverges from upstream, and the matching Revises: line in the docstring.

What it prevents

a0b1c2d3e4f5 is flex's current migration head. #35 adds flex0001_dup_email_repair parented on it. Upstream's 3c9b0ca343fd — the first of the 16 v0.11.x revisions on this branch — also declares down_revision = 'a0b1c2d3e4f5'. Two children, one parent, two Alembic heads. backend/open_webui/config.py:74 calls command.upgrade(alembic_cfg, 'head'), which does not tolerate that:

CommandError: Multiple head revisions are present for given argument 'head';
please specify a specific target revision, '<branchname>@head' to narrow to a
specific head, or 'heads' for all heads

That is a boot-time crash loop, and it would appear only after both PRs merged — the moment everyone believes the work is finished. Merge order does not save you: without this change the end state has two heads either way.

Repointing also does something the narrower fix would not. It puts the repair ahead of all 16 v0.11.x revisions, not merely ahead of f0bd01a18a3d_add_unique_normalized_user_email_index. A database restored from a pre-2026-09-03 backup is stamped at a0b1c2d3e4f5, so the repair becomes the first revision applied.

The sequencing decision, and its cost

flex0001_dup_email_repair is not on this branch and not on flex — it exists only on #35's branch, which is an unmerged draft. I considered bringing the migration file onto this branch to make #33 self-consistent, and deliberately did not:

Copying it here would land a destructive user/auth row merge — deletes rows, folds unique-key collisions — into flex through a PR titled "sync: rebase flex onto upstream open-webui v0.11.3", reviewed as an upstream sync. #35 is still a draft precisely because that migration has not yet had its review. It would also put a 500-line data migration on two open PRs at once, where any review-driven change to #35 produces a divergent copy — and, as noted below, this repo has no CI on flex to catch the divergence.

So: repoint only.

⚠️ Required merge order — #35 before #33

Until #35 merges to flex, this branch's migration chain does not resolve. Alembic raises:

UserWarning: Revision flex0001_dup_email_repair referenced from
  flex0001_dup_email_repair -> 3c9b0ca343fd (head), add knowledge_directory table
  is not present
KeyError: 'flex0001_dup_email_repair'

I am not papering over that. It is a real broken intermediate state, and it is the better of the two failures: it names the missing revision, so the error points directly at the change that is absent. "Multiple head revisions are present" names nothing and gives a reader no thread to pull.

One correction to the usual assumption here: this will not show up as red CI. statusCheckRollup on this PR is empty, and no workflow targets flex — backend.yaml and frontend.yaml trigger only on main/dev. So the dependency is enforced by this comment, by the in-file comment block, and by #35's test_single_head once both are on flex — not by an automated gate on this PR.

Verified

Against the real Alembic resolver (1.19.2), not read off the files:

state alembic heads result
This branch as pushed (flex0001 absent) — KeyError: 'flex0001_dup_email_repair' — expected, see above
#35 merged, then #33 (flex0001 present) ['d4c1a8e37b62'] ✅ exactly one head, 59 revisions
Counterfactual: flex0001 present, repoint reverted ['d4c1a8e37b62', 'flex0001_dup_email_repair'] ❌ two heads; upgrade('head') raises CommandError
  • fix(migrations): repair duplicate user emails as a restore guard #35's test_single_head passes against the merged state — I ran the actual test file, not a reimplementation. Reverting just the repoint makes it fail with expected exactly one Alembic head, found ['d4c1a8e37b62', 'flex0001_dup_email_repair'], so it is a genuine tripwire rather than a tautology.
  • Graph checked by parsing all 58/59 revision files as well as by executing, since the chain cannot be replayed on either engine (known upstream problem, documented in fix(migrations): repair duplicate user emails as a restore guard #35).
  • flex and this branch each resolve to a single head today (a0b1c2d3e4f5 / d4c1a8e37b62), no dangling refs, no other branch points — so this is the only ordering collision the sync introduces.

Not confirmed

  • No migration was executed against any database. This is a graph-resolution change; upgrade() was resolved, never run. Nothing deployed, nothing merged, no AWS access.
  • The alembic heads runs used a stubbed open_webui module, because some revision files import the application at module scope. That affects module import only, not the revision graph.
  • The commit is unsigned. No user.signingkey is configured and gpg.format is ssh with commit.gpgsign=false; I did not pick a key. flex has no rulesets and no branch protection at all, so this does not block the merge.

Why this PR is BLOCKED (reported, not touched)

mergeable: MERGEABLE, mergeStateStatus: BLOCKED, reviewDecision: REVIEW_REQUIRED, zero reviews and zero review requests. It is not a failing check — statusCheckRollup is empty and no workflow targets flex.

The requirement is not visible at repo level: repos/flexion/open-webui/rulesets (including ?includes_parents=true) returns [], rules/branches/flex returns [], classic branch protection 404s, and there is no CODEOWNERS on flex. Since reviewDecision is populated at all, a review requirement does exist — most likely an org-level ruleset, which I could not enumerate (orgs/flexion/rulesets needs the admin:org scope). It needs one approving review. I did not attempt to unblock it and did not change any PR's draft/ready state.


🤖 Generated with Claude Code

https://claude.ai/code/session_01PLRr6UnzNNqtSQZPFKZjig

@lgarceau768

Copy link
Copy Markdown
Author

#35 merged (3459d519, 2026-09-11 21:12 UTC — approved by guruparan18 and bdruth).

Re-verified mergeability of this PR against the new flex tip:

  • mergeable: MERGEABLE, mergeStateStatus: BLOCKED (review only, not a conflict)
  • merge-base with flex is d290077 — the commit immediately before fix(migrations): repair duplicate user emails as a restore guard #35 landed
  • flex0001_dup_email_repair.py does not exist on this branch at all, so it merges in cleanly from flex's side with zero conflict, and 3c9b0ca343fd's down_revision now resolves

The only remaining blocker is the one required review. Requested from @bdruth.

@lgarceau768

lgarceau768 commented Sep 13, 2026 •

Copy link
Copy Markdown
Author

📸 Post-deploy smoke test — v0.11.3-flex.2 on prod

Taken 2026-09-13, right after v0.11.3-flex.2 (this branch's content, plus #35's restore guard) deployed to prod through flexion-open-webui-infra#596. Migration ran clean (17 revisions, exit 0), service healthy on the new image — three different providers in one session (the actual thing round 1–2 of this PR restored and expanded), the app's own About panel confirming the version, and a live Google OAuth login.

ℹ️ About panel — v0.11.3 confirmed + live Google OAuth login

About panel showing v0.11.3 and a Google OAuth 1Password prompt

Settings → About reads "Open WebUI Version: v0.11.3 (latest)" directly from the running app — independent confirmation of the upgrade, straight from the UI rather than from ECS/ECR metadata. The 1Password "Save in 1Password?" prompt in the corner is mid-flow from a real Google OAuth sign-in against the account this PR's Google Groups reconciliation (round 3) covers.

🟠 Claude Opus — chat response + code block rendering

Claude Opus debounce snippet

Orange Claude mark resolves correctly for a direct (non-Bedrock) Anthropic id — the icon fix from round 2 of this PR. Syntax highlighting, line numbers, and the collapse/save/copy toolbar all render on a real response.

🔍 Model picker — provider icons across the full lineup

Model picker showing provider icons

The @-search model picker with a live gateway's model list — the exact surface the provider-icon restoration in this PR targets. Each entry resolves to a real provider mark instead of falling back to the generic favicon.

⚫ xAI Grok-4.6 — chat response

Grok 4.6 joke response

us.xai.grok-4.6 (US) resolving to the xAI mark — one of the providers added in round 2 (previously fell through to the generic favicon before this PR).

Screenshots hosted on the assets/pr33-smoke-test-screenshots orphan branch — kept separate from this PR's diff on purpose, referenced here only.

lgarceau768 added a commit that referenced this pull request Sep 13, 2026
Confirms Open WebUI Version: v0.11.3 (latest) in the running app's own
About panel, and a live Google OAuth sign-in (1Password save prompt tied
to the Google-linked Flexion account).
@lgarceau768
lgarceau768 requested a review from msharp9 October 8, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.