Repository navigation
sync: rebase flex onto upstream open-webui v0.11.3 - #33
Conversation
…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
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.
…ery-access@flexion.us
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.
…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.
….11.3-20260902-204900
…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.
…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
Alembic ordering: v0.11.x now chains onto the fork's duplicate-email repairPushed -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 What it prevents
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 The sequencing decision, and its cost
So: repoint only.
|
| 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_headpasses against the merged state — I ran the actual test file, not a reimplementation. Reverting just the repoint makes it fail withexpected 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).
flexand 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 headsruns used a stubbedopen_webuimodule, 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.signingkeyis configured andgpg.formatissshwithcommit.gpgsign=false; I did not pick a key.flexhas 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
|
#35 merged ( Re-verified mergeability of this PR against the new
The only remaining blocker is the one required review. Requested from @bdruth. |
📸 Post-deploy smoke test — v0.11.3-flex.2 on prodTaken 2026-09-13, right after ℹ️ About panel — v0.11.3 confirmed + live Google OAuth loginSettings → 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 renderingOrange 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 lineupThe ⚫ xAI Grok-4.6 — chat response
Screenshots hosted on the |
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).




Note
#35 merged 2026-09-11.
flex0001_dup_email_repairnow exists onflex, and GitHub confirms this PR ismergeable: MERGEABLEagainst 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 (bdruthrequested).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'sgh pr createstep failed — #34 fixes that, still open.auths.pytoken_exchangeOAuth 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:#155724A 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
flexchangesnpm run build/docker buildon your own machine📋 What to actually review — the 11 shared-source files
backend/open_webui/utils/oauth.pybackend/open_webui/routers/models.py.github/workflows/*.yml(.disabled)×4backend/open_webui/config.pybackend/open_webui/routers/tasks.pysrc/lib/components/chat/Navbar.svelte.github/workflows/upstream-sync.ymlbackend/open_webui/utils/task.py🔧 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:
Provider icons lost —
backend/open_webui/routers/models.py+ the entirestatic/static/providers/asset directory. The/model/profile/imageendpoint 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).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 theglm-*form), fixed Moonshot/Kimi matching (moonshotai./kimi-aliases — the icon asset already existed, it just wasn't wired up), added aclaude-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 —
flexhad drifted 24 commits ahead; branch reconciledAfter rounds 1–2,
flexmoved ~24 commits ahead of where this branch was cut, includingflex's own new upstream-sync CI automation and aconfig.pychange — GitHub started reporting this PR asCONFLICTING.origin/flexwas 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 localv0.11.3git tag that had been silently repointed toflex's own tip) concluded the opposite of the truth. After re-fetching real tags:flexis still on v0.9.5 for this file; this branch'sconfig.pyis already byte-identical to genuine upstream v0.11.3, includingflex's one relevant change (readingOPENAI_API_CONFIGSfrom 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.test_oauth_google_groups.pywas also repaired (written against a since-removedAppConfig/auth_manager_configpattern and an olderGroupsAPI) 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 forwardflex's CI workflow updates, caught one duplicate-function false-conflict inmodels.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 onflex. Rather than strip it back out (which would just regress it again next merge), added the one import it needed to avoid aNameError. 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.flex0001_dup_email_repair, chained ontoa0b1c2d3e4f53c9b0ca343fdrepointed onto that revisionVerified against the real Alembic resolver (1.19.2), reading all 59 revisions:
alembic heads['d4c1a8e37b62']— one headKeyError: 'flex0001_dup_email_repair'['d4c1a8e37b62', 'flex0001_dup_email_repair']→CommandError: Multiple head revisions are presentThat last state is the dangerous one — a boot-time crash loop, with an error that names nothing useful.
Requirements before merging
flexbefore this PR. Merged 2026-09-11 (3459d519). Confirmed via the compare API: merge-base withflexisd290077(the commit right before fix(migrations): repair duplicate user emails as a restore guard #35),flex0001_dup_email_repair.pydoesn't exist on this branch at all, so it comes in cleanly fromflex's side with zero conflict.FLEXION FORK EDITcomment on3c9b0ca343fd'sdown_revisionsurvives future upstream syncs.upstream-sync.ymlrebases, so a conflict-resolver restoring upstream's'a0b1c2d3e4f5'silently reintroduces the two-head crash with nothing to catch it.test_single_headstays in the suite. The only automated tripwire — there is no CI onflex:backend.yamlandfrontend.yamlfire only onmain/dev, andstatusCheckRollupon both PRs is empty.BLOCKEDonREVIEW_REQUIREDwith zero reviews — not a failing check. Requested frombdruth(approved fix(migrations): repair duplicate user emails as a restore guard #35). The requirement isn't visible in repo rulesets orCODEOWNERS(rules/branches/flexreturns[]), so it is an org-level policy invisible to this token.🖼️ Consequence for the image already in ECR
v0.11.3-flex.1was pushed on 2026-09-03;flex0001_dup_email_repairwas 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
flexthat 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
auths.py'stoken_exchangeendpoint (pre-existing, untouched here) doesn't thread Google Groups info or the admin exemption the way the main OAuth callback now does.user.role != "admin"inoauth.py) has no automated test and isn't indocs/oauth-google-groups.md. Verified by code reading only — highest-value item for a human to specifically exercise.Dockerfile's commented-outNODE_OPTIONSmemory bump caused repeatedvite buildOOMs during local verification. Not changed here; worth revisiting before this hits CI.