Commit Graph

17512 Commits

Author SHA1 Message Date
G30 866e8582d1 ci: auto-label bug reports created outside the issue form (#27256)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:07:25 -04:00
Timothy Jaeryang Baek c3c70d4a7c refac 2026-07-23 22:56:02 -04:00
Timothy Jaeryang Baek 2ef6c76f51 refac 2026-07-23 22:52:23 -04:00
Timothy Jaeryang Baek 43e7eefa95 refac 2026-07-23 22:45:54 -04:00
Timothy Jaeryang Baek 40320c1136 refac 2026-07-23 22:44:04 -04:00
Timothy Jaeryang Baek 75a8a0046b refac 2026-07-23 22:36:03 -04:00
Timothy Jaeryang Baek 8d2fee5d45 refac 2026-07-23 22:35:54 -04:00
Jiwon Lee 9d020edf0f fix: make native date/time picker icons visible in dark mode (#27275) 2026-07-23 22:23:46 -04:00
Timothy Jaeryang Baek 021c4c7a2e refac 2026-07-23 22:20:50 -04:00
G30 3132f11e55 feat: add setting to toggle keyboard shortcuts on/off (#27300) 2026-07-23 22:14:56 -04:00
G30 4f823774ab fix: migrate deprecated authlib.jose import to joserfc in oauth utils (#27310) 2026-07-23 21:54:59 -04:00
G30 4fca375ad4 chore: remove unused ArchivedChatsModal component (#27308) 2026-07-23 21:54:45 -04:00
Classic298 acf586c006 fix: resolve the web loader parser per URL instead of locking in the first one (#27367)
SafeWebBaseLoader._unpack_fetch_results assigned the resolved parser to the parser parameter itself, so the None check only ran for the first URL. In a mixed batch every later document was parsed with whatever the first URL happened to select: an .xml feed first meant all following HTML pages went through the xml parser (broken text extraction), and an HTML page first meant .xml URLs were parsed as HTML. Web search regularly fetches mixed batches, so this silently degraded extraction quality depending on result order.

The parser is now resolved per URL; an explicitly passed parser still applies to the whole batch as before. Verified with mixed xml/html batches in both orders and with an explicit parser override.
2026-07-23 21:35:27 -04:00
Classic298 656a848043 perf: halve Redis round trips on model resolution and socket pools (#27225)
When WEBSOCKET_MANAGER=redis, app.state.MODELS and the socket session/
usage pools are Redis-backed dicts, so every membership test and
getitem is a network round trip:

- generate_chat_completion checked `model_id not in models` (HEXISTS)
  and then read `models[model_id]` (HGET) on every chat completion.
  A single .get() now serves both, with the same not-found error.
- The direct-connection branch spread the pool with `{**MODELS, ...}`,
  which iterates keys() then fetches each value — HKEYS plus one HGET
  per model. dict(MODELS.items()) issues a single HGETALL instead.
- get_user_ids_from_room called SESSION_POOL.get(sid) twice per
  session (once to filter, once for the value); the usage handler
  checked membership then fetched the same key. Both now do one
  lookup.

In non-Redis mode these are plain dicts and behavior is identical.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 21:32:01 -04:00
Timothy Jaeryang Baek 429f2df50c refac 2026-07-23 21:29:33 -04:00
Classic298 484fb61743 perf: make streamed content accumulation genuinely linear (#27359)
The streaming handler's content accumulator is a closure cell (declared nonlocal in stream_body_handler), and CPython's in-place string append optimization only applies to plain local variables (STORE_FAST), never to cell variables (STORE_DEREF). The content += value form introduced in #27231 therefore still allocates and copies the full accumulated string on every delta, exactly like the f-string it replaced; whether that copy is cheap or expensive is up to the allocator, and measurements swing accordingly (6 to 83 ms of pure copying for a 400 KB response on Python 3.12, against 0.3 ms for this patch).

Accumulate the deltas in a list instead and join once at the single read site (publish_chat_finished_event at stream end). List append is amortized O(1) with no dependence on reference counts, bytecode specialization or allocator behaviour, so accumulation is O(n) by construction. The non-str fallback keeps the previous f-string coercion semantics.

Verified end to end against a mock SSE upstream: a streamed chat with a think-tag block plus 40 content deltas produces output items, message text, reasoning text and usage identical to current dev, with no errors in the server log.
2026-07-23 18:17:41 -05:00
Timothy Jaeryang Baek 9a49b271aa refac 2026-07-23 19:17:19 -04:00
Timothy Jaeryang Baek f2dd88285a refac 2026-07-23 19:15:32 -04:00
Timothy Jaeryang Baek 858e9236df refac 2026-07-23 19:12:47 -04:00
Classic298 d0f759ce40 fix: capture uncompressed response bodies in audit logs (#27369)
CompressMiddleware was registered before AuditLoggingMiddleware. Starlette prepends on add_middleware, so the audit layer ended up outside compression and, at the REQUEST_RESPONSE level, recorded the zstd/brotli/gzip bytes of every response, decoded with errors='replace'. Any client that sent Accept-Encoding (i.e. every browser) therefore produced audit entries whose response_object was unreadable mojibake.

Registering the audit middleware before the compression middleware places it inside compression, so it observes the response body exactly as the route produced it while the client still receives the compressed stream.

Verified with a stacked ASGI harness: in the old order the captured body is not parseable; in the new order the captured body round-trips as the original JSON and the client response stays compressed.
2026-07-23 18:09:40 -05:00
Classic298 3d45947053 fix: report sub-second timings in the X-Process-Time header (#27368)
The header value was truncated with int(), so every request faster than one second reported X-Process-Time: 0 and the header carried no information for exactly the requests it is meant to describe. Emit fractional seconds with microsecond precision instead, matching the pre-ASGI-refactor behavior where the raw float was sent.
2026-07-23 18:09:23 -05:00
Classic298 e0918ddb40 perf: stop the audit middleware from re-running the whole auth pipeline (#27373)
With audit logging enabled, every audited request authenticated twice. The route dependency resolved the user once, and then _log_audit_entry called get_current_user again in the request's finally block: a second JWT decode, two more Redis revocation lookups, a second user row fetch with pydantic validation and, crucially, a second fire-and-forget last-active write transaction per request.

get_current_user now stashes the resolved user on the scope-backed request state (the same mechanism the auth middleware already uses for request.state.token), and the audit middleware reuses it, falling back to the old resolution only when no user was stashed (e.g. routes without an auth dependency). While in the file, the audit path patterns are compiled once in the constructor instead of per request, and the always-log endpoint set is a class attribute instead of a per-call literal; both are fixed for the process lifetime.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| audit auth resolution, CPU floor (JWT decode + user validate only) | 16.7 us | 0.24 us |
| extra work per audited request | 2 Redis GETs + 1 user SELECT + 1 last-active write | none |

The before column understates the saving: it excludes the Redis and DB round trips listed in the second row, which dominate in real deployments.

Functionally verified with a stacked ASGI harness: when the route resolves a user the audit entry carries that user and the auth pipeline is not invoked again; without a stashed user the fallback path still resolves and logs correctly; the skip matrix (exclusions, whitelist mode, always-log auth endpoints, unauthenticated and non-audited methods) is unchanged.
2026-07-23 18:09:04 -05:00
Classic298 699d512e2f perf: drop redundant session.refresh calls after commit across the model layer (#27381)
Both session factories run with expire_on_commit=False, so ORM objects keep their attribute values after commit. Every session.refresh issued right after a commit therefore re-SELECTed a row whose values the session already held, including full chat JSON blobs and user settings, purely to overwrite identical data. Fifty such calls existed across the model layer, covering nearly every write path in the app (chat inserts, title updates, pin/archive toggles, user role and settings updates, tool, prompt, function, model, file, tag, feedback, memory, automation and grant writes).

All fifty are removed. The only refreshes with an actual job were the two update-then-reload paths in tools and skills, where a Core UPDATE statement bypasses the identity map; those now use session.get(..., populate_existing=True), which guarantees a fresh row in one SELECT whether or not the row was already present in the session (the previous code issued get plus refresh, two SELECTs, on the default configuration).

Benchmark (real SQLite DB, per write):

| write path | before | after |
| --- | --- | --- |
| chat title update, ~600 KB chat blob | 2.08 ms | 1.24 ms |
| user role update, small row | 1.21 ms | 0.68 ms |

On Postgres each removed refresh is additionally a network round trip. The chat-blob case also skips re-parsing the entire JSON document per write.

Functionally verified against a fresh database: user insert, role and settings updates, chat insert (including the server-default meta column, which is always provided client-side), title update and pin toggle, tool insert and the Core-update reload path, tag insert and the prompt insert flow that pins version_id after history creation all return correct values and persist correctly.
2026-07-23 18:08:00 -05:00
Classic298 6b655689cc perf: cut repeated per-model work out of model list assembly
get_all_models runs on every models refresh and, without the base-models cache (off by default), on every /api/models request. Several of its costs multiplied by the model count for no reason:

- The active action and filter id sets were derived from get_functions_by_type, which loads full function rows including plugin source and validates them, only for the ids and is_global flags. A generalized column-only query now returns (id, is_global) tuples; the existing filter-specific helper delegates to it.
- Action priorities were computed inside the per-model sort key, constructing a pydantic Valves object per action per model; with global actions in every model's list that was models x actions constructions per refresh. Priorities are now memoized per action.
- Global action and filter item dicts were rebuilt per model from the same modules. The item lists are now built once per function and shallow-copied per model, keeping per-model dicts independent exactly as before (nested values were already shared).
- Deactivated base-model overrides were dropped with models.remove, a linear scan and shift per removal; removals are now collected and filtered out in one identity-based pass, preserving list.remove's exact object semantics.
- RedisDict.set fingerprinted the payload by serializing the already-serialized mapping a second time plus a sha256; a direct dict comparison against the last written mapping has the same skip semantics without re-serializing anything.
- /api/models did tag normalization and profile-image stripping for every model before access filtering discarded the invisible ones, and always evaluated a json.dumps debug f-string; the work now runs only on visible models and the debug line is gated on the log level. The duplicate-id dedup keeps its position before filtering so the effective-model semantics are unchanged.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| model-cache fingerprint, 200 models | 45 us | 1.4 us |
| action priority Valves builds, 200 models x 4 global actions | 0.37 ms (800 builds) | 0.002 ms (4 builds) |
| function-table payload for id sets | full rows incl. source | (id, is_global) tuples |

Functionally verified: the column-only id query matches the full-row query for actions and filters including inactive exclusion, and the fingerprint skip logic writes on first set, skips identical payloads, updates plus deletes stale keys on change and clears on empty, against a scripted fake Redis.
2026-07-23 19:05:32 -04:00
Classic298 310ae91302 perf: make tools defer_content real and batch tool and knowledge access filters (#27387)
Tools.get_tools(defer_content=True) contained the literal dead statement "stmt = stmt": the deferral was a no-op, so every tools listing loaded the full Python source of every tool (five caller sites pass defer_content=True expecting the optimization: the tools list endpoints and the user and group permission overviews). The listing now selects every column except content, and ToolModel.content becomes optional to represent deferred rows; router projections are content-less response models, so nothing downstream reads the source on these paths.

On top of that, get_tools_by_user_id issued one grant query per non-owned tool and Knowledges.get_knowledge_bases_by_user_id did the same per knowledge base (the latter also sits inside per-file access checks). Both now resolve grants for all non-owned rows in a single get_accessible_resource_ids call, the same batch helper the model listing already uses.

Benchmark (real SQLite DB):

| metric | before | after |
| --- | --- | --- |
| tools listing, 33 tools x ~200 KB source | 5.13 ms | 3.18 ms |
| grant queries per accessible-tools call, N non-owned tools | N | 1 |
| grant queries per accessible-KBs call, N non-owned KBs | N | 1 |

The listing row scales with source size; on Postgres the deferral additionally avoids shipping every tool's source over the wire per listing, and each removed grant query was a real round trip.

Functionally verified: deferred listings match full listings field for field with content None, grants included and router projections working; access filtering returns exactly owned plus granted tools and knowledge bases and nothing for strangers; full (non-deferred) reads still carry the source.
2026-07-23 17:51:07 -05:00
Classic298 5b518cbe43 perf: list knowledge base file metadata without loading extracted file contents (#27386)
Knowledges.get_file_metadatas_by_id fetched full File rows, whose data column carries the entire extracted text of each document, validated each into a FileModel and then threw everything except id, hash, meta and the timestamps away. The function backs every knowledge base detail view and runs again after every file add or remove (eight call sites in the knowledge router), so rendering a filename list for a 50-file knowledge base parsed tens of megabytes of JSON per request.

The listing now selects exactly the five columns the response needs, joined through KnowledgeFile, mirroring the column-only helper that already existed in the files model for id-based lookups.

Benchmark (real SQLite DB, 50 files with ~200 KB extracted text each):

| metric | before | after |
| --- | --- | --- |
| KB file metadata listing | 14.2 ms | 1.20 ms |

The gap widens linearly with file size and count since extracted contents no longer get read, parsed or validated at all.

Functionally verified: output matches the old implementation field for field on all 50 files and an unknown knowledge base still returns an empty list.
2026-07-23 17:50:55 -05:00
Classic298 d67bc4ffcd perf: batch the file access check queries (#27383)
has_access_to_file runs for every non-owner file GET, per RAG file check and per shared-chat or model-attached file. Its final step called Models.get_models_by_user_id, which issued one grant query per non-owned workspace model, so a single file check on an instance with M workspace models cost M grant queries plus a group query, with the deny path always paying full price. Its collection_name step listed every knowledge base the user can access (itself one grant query per knowledge base) just to scan the list for one id. And get_accessible_folder_files repeated the whole pipeline per folder entry, refetching the caller's group memberships every time.

Three changes, all using parameters and helpers that already exist:
- Models.get_models_by_user_id resolves grants for all non-owned models in one get_accessible_resource_ids call and accepts prefetched user_group_ids.
- The collection_name check fetches the one referenced knowledge base and performs a single owner-or-grant check with the already-resolved group ids, preserving the write-requires-owner guard exactly (including its short-circuit before any grant query).
- get_accessible_folder_files resolves group ids once and threads them through every per-entry check.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| filter loop CPU, 300 workspace models (queries stubbed) | 47 us | 19 us |
| grant queries per file-access check, M workspace models | M | 1 |
| group membership queries per folder listing, F files | F | 1 |

The stubbed CPU row understates the win: each removed query in the other two rows was a real database round trip.

Functionally verified with stubbed accessors: owned plus granted models are returned with owned ids excluded from the batch query; model-attached file access resolves through the batched path; the collection_name path does one KB fetch and one grant check with no full listing; a missing KB falls through; write access via a KB still requires the KB owner to own the file and short-circuits before the grant query; folder listings fetch groups exactly once.
2026-07-23 17:50:08 -05:00
Classic298 d5f099a5d4 perf: cut per-request database session overhead (#27385)
Two independent sources of fixed per-request cost:

The async SQLite engine was created with pool_pre_ping=True. A pre-ping guards against server connections dropped by timeouts or restarts, which cannot happen to a local SQLite file; each ping still costs a hop into the aiosqlite worker thread plus a SELECT 1 on every connection checkout, and with session sharing off a single request checks out a connection for every model-layer call it makes. The Postgres engines keep their pre-ping, where it is actually protective.

CommitSessionMiddleware unconditionally ran ScopedSession.commit() plus remove() after every HTTP request. The scoped registry instantiates a session on first access, so on the vast majority of requests (which never touch the sync session, per the middleware's own docstring) this built a Session, opened and committed an empty transaction and tore everything down for nothing. The middleware now checks ScopedSession.registry.has() first: requests that used the sync session are committed and removed exactly as before, on success and on the rollback path alike, and idle requests skip the machinery entirely.

Benchmark (real SQLite database):

| metric | before | after |
| --- | --- | --- |
| user row fetch incl. session + connection checkout | 681 us | 514 us |
| idle-request sync session work (create + empty commit + teardown) | 12.3 us | 0.26 us |

The first row saves per model-layer call, not per request: a request making five DB calls saves the checkout ping five times.

Functionally verified: normal reads and writes work with pre-ping off; an idle request through the middleware leaves no sync session behind; a request that uses the sync session still gets committed and removed.
2026-07-23 17:49:48 -05:00
Classic298 dd514ee20b fix: persist upstream streaming error lines by awaiting the message upsert (#27365)
The branch that normalizes plain JSON error lines from streaming upstreams (lines without the SSE data: prefix) called Chats.upsert_message_to_chat_by_id_and_message_id without await. The coroutine was never executed, so the error was never written to the chat and Python emitted a "coroutine was never awaited" RuntimeWarning instead. The frontend still received the error event, but after a reload the message showed no trace of the failure.

The parallel error-persist branch further down the same handler already awaits the call; this aligns the two.
2026-07-23 17:49:18 -05:00
Timothy Jaeryang Baek e769f9ff4f refac 2026-07-23 17:53:13 -04:00
Timothy Jaeryang Baek ec56022bc1 refac 2026-07-23 16:35:48 -04:00
Timothy Jaeryang Baek 892dc03151 refac 2026-07-23 16:35:01 -04:00
G30 e62e4eb9fe fix(ui): contain archived chats and analytics content within the settings modal (#27306) 2026-07-23 13:34:35 -05:00
Timothy Jaeryang Baek 132a29fd1c refac 2026-07-23 14:29:30 -04:00
Timothy Jaeryang Baek c8f2e09fdc refac 2026-07-23 14:29:27 -04:00
Timothy Jaeryang Baek 25faa19941 refac 2026-07-23 14:27:55 -04:00
Classic298 e64acf1c0a perf: batch and deduplicate per-request DB reads in the chat middleware (#27223)
Several spots in the chat pipeline issued sequential single-key config
SELECTs, or fetched the same key twice back-to-back, on every request:

- chat_completion_tools_handler: task model default/external and the
  tools prompt template were four sequential Config round trips (the
  template was fetched twice). One batched Config.get_many now serves
  all of them.
- chat_completion_files_handler: the six RAG settings (top_k,
  top_k_reranker, relevance_threshold, hybrid_bm25_weight,
  enable_hybrid_search, full_context) were six sequential round trips
  inside the retrieval call. Batched into one get_many.
- Voice and code-interpreter prompt templates were each fetched twice
  within one conditional; fetch once and reuse. The code-interpreter
  engine was likewise fetched twice per execution.
- Skill resolution fetched the accessible-skills list, kept only the
  ids, then re-fetched each mentioned skill by id (N+1). Reuse the
  rows from the access query.

Value semantics are identical: get_many applies the same defaults as
the individual gets, and the pre-existing truthiness/empty-string
checks on templates are preserved exactly.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:21:29 -05:00
Juan Calderon-Perez ca2d7c9deb feat: add LDAP group synchronization support (#27263)
* feat: expose LDAP group sync settings in admin config

LDAP group synchronization was already wired into the login flow but its
settings (group management, auto-creation, and the group attribute) could
only be set via environment variables. OAuth, by contrast, exposes its
group-mapping settings through the admin config API and UI.

Bring LDAP to parity:
- Add enable_group_management, enable_group_creation and
  attribute_for_groups to LdapServerConfig and LDAP_SERVER_CONFIG_KEYS so
  the /admin/config/ldap/server endpoint reads and persists them.
- Add a "Group Mapping / Auto-Create Groups / Group Attribute" section to
  the LDAP admin settings UI, mirroring the OAuth group-mapping controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: harden LDAP group sync config and login flow

Address review findings on the LDAP group-sync settings:

- ldap_auth: move the auto-create-groups call inside the try/except that
  wraps group sync, so a group-creation error is logged instead of
  bubbling to the broad handler and failing the whole login.
- update_ldap_server: reject saving with group management enabled but an
  empty group attribute, which would otherwise make sync silently no-op
  (mirrors the existing required-field validation).
- Authentication.svelte: merge the LDAP server config response into the
  client defaults instead of replacing the object, so any key an older
  backend omits keeps its default value.

Note: the empty-directory-groups behavior was reviewed and already
matches OAuth (both skip removal when no groups are returned), so it was
left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: default blank LDAP group attribute to memberOf before save

The Group Attribute field advertises "Default to memberOf", but the
backend now rejects an empty group attribute when group management is
enabled. Fall back to the memberOf default client-side when the field is
left blank, so the advertised default holds and the save isn't rejected.
The backend validation remains as defense-in-depth for direct API calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: initialize LDAP port default as null instead of empty string

The backend LdapServerConfig types port as `int | None`, but the frontend
initialized it to an empty string. If a save carried that default (e.g.
when the backend response omits port under version skew), Pydantic would
reject the empty string. `null` matches the model and is also what the
type="number" input yields when the field is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: parse LDAP group DNs correctly instead of splitting on commas

Group CN extraction split the DN on raw commas and sliced off "CN=",
which mangles any group whose name contains an escaped separator (e.g.
"CN=Sales\, EMEA,OU=...") into a truncated, wrong name that then fails to
match the intended Open WebUI group. Use ldap3's parse_dn to split the DN
respecting RFC 4514 escaping, and unescape the resulting value so the CN
matches what an administrator sees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* chore: address review feedback on _unescape_ldap_dn_value

Trim the docstring and rename the loop index to a more descriptive name
(i -> pos) per review feedback on the group DN unescaping helper. No
behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:20:55 -05:00
Timothy Jaeryang Baek 0f82f40b70 refac 2026-07-23 13:49:49 -04:00
Timothy Jaeryang Baek ca11bd90a7 chore: format 2026-07-23 13:41:16 -04:00
Timothy Jaeryang Baek 93bd05271c refac 2026-07-23 13:40:30 -04:00
Timothy Jaeryang Baek 3026ac64a2 refac 2026-07-23 13:36:35 -04:00
Classic298 dc4b828852 fix: correct async import hook that disables the code-interpreter module blocklist (#27245)
The middleware code-interpreter path defines `restricted_import` as `async def` and assigns it to `builtins.__import__`, which Python's import machinery calls synchronously. Calling an async function returns a coroutine without running its body, so the blocklist check never executes and `_real_import` is never called. When `CODE_INTERPRETER_BLOCKED_MODULES` is set, blocked modules are therefore not blocked, and every subsequent import inside the interpreter binds a dangling coroutine instead of the module, breaking legitimate imports as well.

Define the hook as a regular `def`, matching the working implementation in `tools/builtin.py`. A blocked top-level import now raises `ImportError`, and all other imports pass through to the real importer.
2026-07-23 12:33:44 -05:00
andrep2222 66bf96c62d Forward user info headers to Mistral OCR API (#27253)
Mirrors the ENABLE_FORWARD_USER_INFO_HEADERS pattern already used by
the audio/TTS and external document loader integrations, so the
Mistral OCR backend can identify the requesting user the same way.

Co-authored-by: andrep <vpham@aut.ac.nz>
2026-07-23 12:33:19 -05:00
Timothy Jaeryang Baek 49abfbdd15 refac 2026-07-23 13:18:04 -04:00
Classic298 da7097565c perf: deduplicate repeated config fetches in Ollama request handlers (#27226)
The per-request Ollama handlers (chat, generate, embed, embeddings,
and the OpenAI-compat completions/chat-completions/messages/responses
endpoints) fetched 'ollama.api_configs' up to three times and
'ollama.base_urls' separately within a single request — the .get()
default-argument pattern made the second api_configs fetch
unconditional, and get_api_key() triggered a third. Up to four
sequential SELECTs per request collapse to one.

A new get_ollama_connection_config() helper fetches base_urls and
api_configs together in one batched Config.get_many where both are
needed; handlers that only need api_configs fetch it once into a
local. Admin operations (pull/push/copy/delete) and the TTL-cached
model-list path are deliberately left untouched.

Resolution semantics (str(idx) key first, url-key legacy fallback,
same defaults) are unchanged.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:48:55 -04:00
Timothy Jaeryang Baek 85664f650c refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-23 12:48:23 -04:00
Timothy Jaeryang Baek f9107edeeb refac 2026-07-23 12:48:14 -04:00
Classic298 8ace4f0a8a perf: stop recomputing static work in per-response ASGI middlewares (#27229)
SecurityHeadersMiddleware called set_security_headers() on every
response — 14 os.environ.get lookups plus a regex validation per
configured header, for values that are static for the process
lifetime. Compute the header list once at construction; when no
security env vars are set, skip wrapping send entirely.

RedirectMiddleware decoded and parse_qs'd the query string of every
GET, though it only acts on /watch?v= and ?shared= URLs. Add a cheap
path/substring precheck first; a false positive just falls through to
the previous full parse, so no redirect behavior changes.

Verified byte-identical responses (status, Location, header values)
against the previous implementations across redirect, passthrough,
and no-env cases.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:29:01 -04:00
Timothy Jaeryang Baek 1513ddaf58 refac 2026-07-23 12:25:17 -04:00