system HF Staff commited on
Commit
dfca7c9
ยท
verified ยท
1 Parent(s): 765119d

Sync from felladrin/MiniSearch@fd8be33

Browse files
.env.example CHANGED
@@ -26,8 +26,14 @@ INTERNAL_OPENAI_COMPATIBLE_API_NAME="Internal API"
26
  # "internal" -> $INTERNAL_OPENAI_COMPATIBLE_API_NAME
27
  DEFAULT_INFERENCE_TYPE="browser"
28
 
29
- # Whether a text search that failed on SearXNG is retried against a second search provider (`search.parallel.ai`). Only "true" or "1" turn it on.
30
  SEARCH_FALLBACK_ENABLED="false"
31
 
32
- # The API key for that second search provider. Optional; sending one raises its free-tier rate limits.
33
- SEARCH_FALLBACK_API_KEY=""
 
 
 
 
 
 
 
26
  # "internal" -> $INTERNAL_OPENAI_COMPATIBLE_API_NAME
27
  DEFAULT_INFERENCE_TYPE="browser"
28
 
29
+ # Whether a text search that failed on SearXNG is retried against the second search providers (search.parallel.ai and You.com, tried one after the other in random order until one returns results). Only "true" or "1" turn it on.
30
  SEARCH_FALLBACK_ENABLED="false"
31
 
32
+ # Comma-separated list of the second search providers to try: "parallel", "youcom", or both. Empty means all of them. With the fallback enabled, an unknown name stops the server at start.
33
+ SEARCH_FALLBACK_PROVIDERS=""
34
+
35
+ # The API key for Parallel (search.parallel.ai). Optional; without it the keyless tier is used, which Parallel describes as for exploration and light use.
36
+ SEARCH_FALLBACK_PARALLEL_API_KEY=""
37
+
38
+ # The API key for You.com. Optional; without it the keyless "profile=free" endpoint is used, which allows 100 queries per day.
39
+ SEARCH_FALLBACK_YOUCOM_API_KEY=""
agents.md CHANGED
@@ -154,7 +154,7 @@ Need to:
154
  - `server/dictationModelServerHook.ts` - `/dictation-models/` endpoint: serves the pinned speech-to-text model files from the instance
155
  - `server/cacheServerHook.ts` - Cache-Control headers (preview server only)
156
  - `server/webSearchService.ts` - SearXNG integration with circuit breaker and retry logic
157
- - `server/fallbackSearchService.ts` - Second text-search source for when SearXNG fails and `SEARCH_FALLBACK_ENABLED` is on
158
  - `server/pageContentService.ts` - Reads result pages and extracts query-relevant passages
159
  - `server/pageReadHostBreaker.ts` - Per-host circuit breaker for page reads: skips a host that refused the last three reads, then probes it once
160
  - `server/utils/publicUrl.ts` - Blocks private and reserved addresses before a server-side fetch
 
154
  - `server/dictationModelServerHook.ts` - `/dictation-models/` endpoint: serves the pinned speech-to-text model files from the instance
155
  - `server/cacheServerHook.ts` - Cache-Control headers (preview server only)
156
  - `server/webSearchService.ts` - SearXNG integration with circuit breaker and retry logic
157
+ - `server/fallbackSearchService.ts` - Fallback text-search cascade over Parallel and You.com for when SearXNG fails and `SEARCH_FALLBACK_ENABLED` is on, with a per-provider pause after a 429
158
  - `server/pageContentService.ts` - Reads result pages and extracts query-relevant passages
159
  - `server/pageReadHostBreaker.ts` - Per-host circuit breaker for page reads: skips a host that refused the last three reads, then probes it once
160
  - `server/utils/publicUrl.ts` - Blocks private and reserved addresses before a server-side fetch
changelog.md CHANGED
@@ -3,6 +3,11 @@
3
  Notable changes, grouped by date, most recent first.
4
 
5
 
 
 
 
 
 
6
  ## 2026-10-08
7
 
8
  - `/status` now reports `authorization.distinctRejectedTokens`, the number of distinct dead tokens the server has cached and keeps refusing. A steady stream of invalid-token rejections with a value of 1 there is one client replaying one stale token, not many old search links, which the existing counters could not tell apart.
 
3
  Notable changes, grouped by date, most recent first.
4
 
5
 
6
+ ## 2026-10-10
7
+
8
+ - The text-search fallback now cascades over Parallel and You.com instead of asking a single provider. On each search the configured providers are tried in a random order, one after the other, until one returns results, under the same 15-second budget, which is split across the providers still untried and not paused so a hanging one cannot starve the rest. A provider that answers HTTP 429 is paused until its `Retry-After` and skipped meanwhile, as You.com's acceptable use policy asks. A single search can now reach more than one provider; each sees the query text and this server's IP address, never the user's. `SEARCH_FALLBACK_PROVIDERS` picks the providers (all of them when empty), and `SEARCH_FALLBACK_PARALLEL_API_KEY` and `SEARCH_FALLBACK_YOUCOM_API_KEY` hold the optional keys. `SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are removed: if either is still set while the fallback is enabled, the server stops at start with an error naming the replacement, and so does a provider key with a control character inside it. `/status` gained per-provider counters under `searches.fallbackProviders`.
9
+
10
+
11
  ## 2026-10-08
12
 
13
  - `/status` now reports `authorization.distinctRejectedTokens`, the number of distinct dead tokens the server has cached and keeps refusing. A steady stream of invalid-token rejections with a value of 1 there is one client replaying one stale token, not many old search links, which the existing counters could not tell apart.
docs/api.md CHANGED
@@ -175,16 +175,23 @@ Failures:
175
  | `500` | `{"error":"Internal server error"}` | Anything else |
176
 
177
  With `SEARCH_FALLBACK_ENABLED` on (see `docs/configuration.md`), a text search
178
- whose SearXNG attempt failed is retried against a second provider before this
179
- endpoint gives up. That attempt covers the whole exchange under one 15-second
180
- budget, and it is made only after SearXNG has failed: never while SearXNG
181
- answers, and never on an image search. It is skipped when less than two seconds
182
- of the search's own 25-second deadline remain, since an answer that late cannot
183
- reach the client before its 30-second timeout, and that skip answers the `502`
184
- above and counts as a fallback that failed. If the fallback answers with nothing
185
- usable, this endpoint responds `200` with `[]`, so the client shows the
186
- "No results found" alert rather than the "Text search unavailable" one; only a
187
- fallback that fails itself falls through to the same `502`.
 
 
 
 
 
 
 
188
 
189
  ### `GET /search/images`
190
 
@@ -360,9 +367,18 @@ counts searches answered with fallback results, and `searchesFailedOnFallback`
360
  counts the other attempts: those the fallback itself failed, and those where
361
  SearXNG's retries had left so little of the 25-second search deadline that the
362
  fallback was never asked. A rising `searchesFailedOnFallback` is therefore not
363
- by itself a sign that the fallback provider is failing. Both are plain counts,
364
  and neither carries a query.
365
 
 
 
 
 
 
 
 
 
 
366
  Nothing in the response is per-user: queries, URLs and client addresses are
367
  never recorded, only aggregate outcomes
368
  (see the privacy section of `docs/security.md`).
 
175
  | `500` | `{"error":"Internal server error"}` | Anything else |
176
 
177
  With `SEARCH_FALLBACK_ENABLED` on (see `docs/configuration.md`), a text search
178
+ whose SearXNG attempt failed is retried against the configured fallback
179
+ providers before this endpoint gives up. They are tried one after the other, in
180
+ a random order on every search, until one returns results; a provider that
181
+ answers with nothing usable or fails hands the search to the next. The whole
182
+ cascade runs under one 15-second budget, or what is left of the search's own
183
+ deadline when that is shorter, and each attempt gets the remaining budget
184
+ divided by the number of providers still untried and not paused. A provider
185
+ paused after a `429` is skipped without a request. The cascade runs only after SearXNG has
186
+ failed: never while SearXNG answers, and never on an image search. It is
187
+ skipped when less than two seconds of the search's own 25-second deadline
188
+ remain, since an answer that late cannot reach the client before its 30-second
189
+ timeout, and that skip answers the `502` above and counts as a fallback that
190
+ failed. If at least one provider answers with nothing usable and none returns
191
+ results, this endpoint responds `200` with `[]`, so the client shows the
192
+ "No results found" alert rather than the "Text search unavailable" one; only
193
+ when every provider tried fails, or none could be tried at all, does the
194
+ search fall through to the same `502`.
195
 
196
  ### `GET /search/images`
197
 
 
367
  counts the other attempts: those the fallback itself failed, and those where
368
  SearXNG's retries had left so little of the 25-second search deadline that the
369
  fallback was never asked. A rising `searchesFailedOnFallback` is therefore not
370
+ by itself a sign that the fallback providers are failing. Both are plain counts,
371
  and neither carries a query.
372
 
373
+ `searches.fallbackProviders` breaks the cascade down per provider name. Each
374
+ entry counts the attempts that provider `served` with results, answered
375
+ `empty`, or `failed`, the subset of those failures that were a `429`
376
+ (`rateLimited`, also counted in `failed`), and the searches that skipped it
377
+ while it was paused (`skippedWhilePaused`); `pausedForMs` is
378
+ the time left on its current pause, or `0` when it is not paused. The names are
379
+ configured infrastructure, not anything a user typed, and no field carries a
380
+ query or a provider's error text.
381
+
382
  Nothing in the response is per-user: queries, URLs and client addresses are
383
  never recorded, only aggregate outcomes
384
  (see the privacy section of `docs/security.md`).
docs/configuration.md CHANGED
@@ -91,12 +91,23 @@ These defaults are provided by `docker-compose.yml`/`docker-compose.production.y
91
 
92
  ### Search Fallback
93
 
94
- When SearXNG fails on a text search, the server can retry that search against a second provider, sending the query to `search.parallel.ai`. That provider documents the keyless tier as free for light use with lower rate limits, and recommends an API key for production use. `docker-compose.yml` passes only `HOST`, `PORT`, `BASIC_SSL` and `HMR_PORT` into the development container, so these two variables are not carried by the compose `environment` list; set them in `.env`, which `vite.config.ts` loads, inside the container as well as outside it. `vite.config.ts` reads `.env.example` after `.env`, so anything still set there acts as a default: the real `SEARCH_FALLBACK_API_KEY` belongs in `.env`, never in `.env.example`, which is committed.
95
 
96
  | Variable | Default | Description |
97
  |----------|---------|-------------|
98
- | `SEARCH_FALLBACK_ENABLED` | `false` | Serve text results from the fallback when SearXNG has failed (an HTTP error, a network error, every engine unresponsive, or an open circuit), under one 15-second budget for the whole exchange. Only `true` or `1` turns it on, and changing it takes a restart, since a process keeps the environment it was started with. Image searches never use it, and a fallback that returns nothing usable is still an HTTP 200 with no results rather than an outage |
99
- | `SEARCH_FALLBACK_API_KEY` | `''` | Optional API key for that provider, sent as a Bearer token. It raises the provider's free-tier rate limits; without it the fallback still works |
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  ## Application Settings
102
 
 
91
 
92
  ### Search Fallback
93
 
94
+ When SearXNG fails on a text search, the server can retry that search against second providers: `search.parallel.ai` (Parallel) and You.com's MCP endpoint (`https://api.you.com/mcp`). Every configured provider takes part. On each search they are put in a random order, and each is tried in turn until one returns results; a provider that answers with nothing usable, or fails, hands the search to the next one. The whole cascade shares one 15-second budget, and each attempt gets the remaining budget divided by the number of providers still untried and not paused, so a provider that hangs cannot use up the time the others need. A single search can therefore reach more than one provider, one after the other. `docker-compose.yml` passes only `HOST`, `PORT`, `BASIC_SSL` and `HMR_PORT` into the development container, so these variables are not carried by the compose `environment` list; set them in `.env`, which `vite.config.ts` loads, inside the container as well as outside it. `vite.config.ts` reads `.env.example` after `.env`, so anything still set there acts as a default: the real API keys belong in `.env`, never in `.env.example`, which is committed.
95
 
96
  | Variable | Default | Description |
97
  |----------|---------|-------------|
98
+ | `SEARCH_FALLBACK_ENABLED` | `false` | Serve text results from the fallback providers when SearXNG has failed (an HTTP error, a network error, every engine unresponsive, or an open circuit), under one 15-second budget for the whole cascade. Only `true` or `1` turns it on, and changing it takes a restart, since a process keeps the environment it was started with. Image searches never use it, and a fallback that returns nothing usable is still an HTTP 200 with no results rather than an outage |
99
+ | `SEARCH_FALLBACK_PROVIDERS` | `''` (all providers) | Comma-separated list of the providers to try: `parallel`, `youcom`, or both. Names are trimmed and lowercased, and empty items are ignored. Unset or empty means every built-in provider. With `SEARCH_FALLBACK_ENABLED` on, an unknown name, or a list with no names at all (such as `,`), stops the server at start with an error that names the valid values |
100
+ | `SEARCH_FALLBACK_PARALLEL_API_KEY` | `''` | Optional API key for Parallel, sent as a Bearer token. Without it the fallback calls Parallel's keyless tier. With `SEARCH_FALLBACK_ENABLED` on, a key with a control character inside it (such as a pasted line break) stops the server at start, since the HTTP client would quote it in its error |
101
+ | `SEARCH_FALLBACK_YOUCOM_API_KEY` | `''` | Optional API key for You.com, sent as a Bearer token. Without it the fallback calls the keyless `https://api.you.com/mcp?profile=free` endpoint. A key with a control character inside it stops the server at start, like the Parallel key |
102
+
103
+ `SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are gone. If either is still set to a non-empty value while `SEARCH_FALLBACK_ENABLED` is on, the server stops at start with an error that names the variable to use instead, so an upgrade cannot silently drop a key: move the key to `SEARCH_FALLBACK_PARALLEL_API_KEY` or `SEARCH_FALLBACK_YOUCOM_API_KEY`, and the provider choice to `SEARCH_FALLBACK_PROVIDERS`.
104
+
105
+ Each provider has its own terms, and each operator is responsible for following them:
106
+
107
+ - **Parallel**: Parallel describes its keyless tier as for exploration and light use, and asks for an account and an API key for production use ([docs.parallel.ai](https://docs.parallel.ai/integrations/mcp/search-mcp)).
108
+ - **You.com**: the keyless `profile=free` endpoint allows 100 queries per day ([you.com](https://you.com/docs/build-with-agents/mcp-server)). Its API acceptable use policy asks integrations to honor 429 responses ([you.com](https://you.com/acceptable-use-policy-may-2026)).
109
+
110
+ That is why a provider that answers HTTP 429 is paused: the cascade skips it, without a network call, until its `Retry-After` has passed (60 seconds when the header is missing or invalid, and never more than an hour or less than a second). A public instance should set a key for each provider.
111
 
112
  ## Application Settings
113
 
docs/failure-injection.md CHANGED
@@ -37,6 +37,9 @@ needed.
37
  | SearXNG is down while the fallback is switched off | HTTP 502; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
38
  | SearXNG is down on an image search while the fallback is on | HTTP 502; the fallback is text-only, so it is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
39
  | SearXNG answers while the fallback is on | HTTP 200 from SearXNG's results; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
 
 
 
40
  | Thumbnail host never answers | Request aborted after the timeout, `/thumbnail` answers HTTP 502 and the tile shows the host name | `server/thumbnailEndpointServerHook.test.ts` |
41
  | DNS lookup for a thumbnail never settles | Bounded by the same deadline as the fetch, `/thumbnail` answers HTTP 502 | `server/thumbnailEndpointServerHook.test.ts` |
42
  | Thumbnail URL resolves into a private range | Refused before any request, `/thumbnail` answers HTTP 403 | `server/thumbnailEndpointServerHook.test.ts` |
 
37
  | SearXNG is down while the fallback is switched off | HTTP 502; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
38
  | SearXNG is down on an image search while the fallback is on | HTTP 502; the fallback is text-only, so it is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
39
  | SearXNG answers while the fallback is on | HTTP 200 from SearXNG's results; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โ€บ graceful degradation โ€บ text search fallback |
40
+ | The first fallback provider fails and the next one answers | The cascade moves on to the next provider within what is left of the shared budget, and that provider's results are served; the failure is logged with the provider name and a status code, never the query | `server/fallbackSearchService.test.ts` โ€บ the cascade |
41
+ | A fallback provider answers HTTP 429 | That provider is paused until its `Retry-After` (60 s when missing or invalid, at most an hour), the next provider serves the search, and later searches skip the paused one without a request until the pause ends | `server/fallbackSearchService.test.ts` โ€บ pausing a rate-limited provider |
42
+ | Every fallback provider is paused | No provider is called, the cascade throws, and `/search/text` answers HTTP 502 as for a fallback that failed | `server/fallbackSearchService.test.ts` โ€บ pausing a rate-limited provider |
43
  | Thumbnail host never answers | Request aborted after the timeout, `/thumbnail` answers HTTP 502 and the tile shows the host name | `server/thumbnailEndpointServerHook.test.ts` |
44
  | DNS lookup for a thumbnail never settles | Bounded by the same deadline as the fetch, `/thumbnail` answers HTTP 502 | `server/thumbnailEndpointServerHook.test.ts` |
45
  | Thumbnail URL resolves into a private range | Refused before any request, `/thumbnail` answers HTTP 403 | `server/thumbnailEndpointServerHook.test.ts` |
docs/overview.md CHANGED
@@ -248,7 +248,7 @@ The `/status` endpoint returns a JSON object:
248
  | `searchesWithUnresponsiveEngines` | number | Searches, text and image together, that came back with zero results and unresponsive engines, whether the retries were spent or an all-suspended set failed fast; one per search, not per attempt |
249
  | `searchesWithAllResultsDiscarded` | number | Text searches whose results were all dropped during processing |
250
  | `searchesServedByFallback` | number | Text searches answered by the fallback source after SearXNG failed |
251
- | `searchesFailedOnFallback` | number | Fallback searches that failed, plus searches where SearXNG's own retries had left too little of the search deadline for the fallback to be asked at all. A rising count is therefore not by itself a sign that the fallback provider is failing |
252
  | `biEncoderServiceStatus` | string | `"healthy"` or `"unhealthy"` |
253
  | `rerankerServiceStatus` | string | `"healthy"` or `"unhealthy"` |
254
  | `webSearchServiceStatus` | string | `"healthy"`, `"degraded"` or `"unhealthy"`, see below |
@@ -422,6 +422,7 @@ way `pageReads` does:
422
  | `searches.degradedSearchTypes` | array | Nothing; which of `text` and `images` is currently flagged, so a `degraded` verdict says which engine pool it is about. Empty when the half-open circuit is what triggered the verdict, which `circuitState` in the same object names |
423
  | `searches.circuitOpens` | number | `failureThreshold` and `resetTimeout` on the SearXNG breaker: each opening is a minute of serving no searches at all |
424
  | `searches.unresponsiveEngines` | object | Nothing; `failures` and `lastFailure` per engine name, which is how one engine timing out is told apart from every engine being CAPTCHA-blocked. `lastFailure` is `blocked` (a CAPTCHA, a rate limit or an access denial), `timeout` or `other`, never SearXNG's own wording, which is free-form text from an upstream engine and does not belong on an endpoint that needs no token. Counted on the searches `searchesWithUnresponsiveEngines` counts, so a search that recovered on a retry is not in it |
 
425
  | `reranker.reranks` | number | Nothing; the denominator for the rest |
426
  | `reranker.averageMs` | number | Whether reranking or SearXNG is what users wait for, and so whether to rerank a shortlist instead of all 30 results. Over `reranks`, covering the filtering and sorting as well as the model |
427
  | `reranker.keptRate` | number | `kStandardDeviationFactor`: near 100% means the filter is not filtering. Text and image reranks are pooled, and the two run through different paths, so this moves with the traffic mix as well as with the threshold |
 
248
  | `searchesWithUnresponsiveEngines` | number | Searches, text and image together, that came back with zero results and unresponsive engines, whether the retries were spent or an all-suspended set failed fast; one per search, not per attempt |
249
  | `searchesWithAllResultsDiscarded` | number | Text searches whose results were all dropped during processing |
250
  | `searchesServedByFallback` | number | Text searches answered by the fallback source after SearXNG failed |
251
+ | `searchesFailedOnFallback` | number | Fallback searches that failed, plus searches where SearXNG's own retries had left too little of the search deadline for the fallback to be asked at all. A rising count is therefore not by itself a sign that the fallback providers are failing |
252
  | `biEncoderServiceStatus` | string | `"healthy"` or `"unhealthy"` |
253
  | `rerankerServiceStatus` | string | `"healthy"` or `"unhealthy"` |
254
  | `webSearchServiceStatus` | string | `"healthy"`, `"degraded"` or `"unhealthy"`, see below |
 
422
  | `searches.degradedSearchTypes` | array | Nothing; which of `text` and `images` is currently flagged, so a `degraded` verdict says which engine pool it is about. Empty when the half-open circuit is what triggered the verdict, which `circuitState` in the same object names |
423
  | `searches.circuitOpens` | number | `failureThreshold` and `resetTimeout` on the SearXNG breaker: each opening is a minute of serving no searches at all |
424
  | `searches.unresponsiveEngines` | object | Nothing; `failures` and `lastFailure` per engine name, which is how one engine timing out is told apart from every engine being CAPTCHA-blocked. `lastFailure` is `blocked` (a CAPTCHA, a rate limit or an access denial), `timeout` or `other`, never SearXNG's own wording, which is free-form text from an upstream engine and does not belong on an endpoint that needs no token. Counted on the searches `searchesWithUnresponsiveEngines` counts, so a search that recovered on a retry is not in it |
425
+ | `searches.fallbackProviders` | object | Whether each fallback provider earns its place, and whether one needs an API key: per provider name, `served`, `empty`, `failed`, `rateLimited` (answered `429`, and also counted in `failed`) and `skippedWhilePaused` counts, plus `pausedForMs`, the time left on its current `429` pause (`0` when not paused). A steady `rateLimited` count on a keyless provider is the cue to set its key (see `docs/configuration.md`). Provider names are configured infrastructure, not user input |
426
  | `reranker.reranks` | number | Nothing; the denominator for the rest |
427
  | `reranker.averageMs` | number | Whether reranking or SearXNG is what users wait for, and so whether to rerank a shortlist instead of all 30 results. Over `reranks`, covering the filtering and sorting as well as the model |
428
  | `reranker.keptRate` | number | `kStandardDeviationFactor`: near 100% means the filter is not filtering. Text and image reranks are pooled, and the two run through different paths, so this moves with the traffic mix as well as with the threshold |
docs/security.md CHANGED
@@ -44,8 +44,8 @@ Every HTTP request from client to backend carries a `token` query parameter for
44
 
45
  - **Local-First Storage**: All data stored in IndexedDB, no cloud sync
46
  - **No Tracking**: No telemetry, analytics, or user tracking
47
- - **SearXNG Integration**: All web searches routed through privacy-focused metasearch, which holds for every search as long as `SEARCH_FALLBACK_ENABLED` is off (the default); with it on, a text search that found SearXNG failed goes to the fallback provider instead, and an image search never does
48
- - **The Fallback Sends The Query To A Third Party When It Is On**: With `SEARCH_FALLBACK_ENABLED` on (off by default), a text query whose SearXNG search failed is sent to `search.parallel.ai` (Parallel), which sees the query text and the server's IP address. The user's IP address is never sent, because the request leaves from the server rather than from the browser. The query is still never written to the server log; what `/status` publishes for this feature is the two counts `searchesServedByFallback` and `searchesFailedOnFallback`, neither of which carries a query
49
  - **Browser-Only Mode Still Downloads Models From HuggingFace**: The instance sends no telemetry and no analytics. But browser-only mode is not fully offline: both wllama and the local text-to-speech engine fetch their models from `huggingface.co` (served from `us.aws.cdn.hf.co`), so that host sees the user's IP. The text-to-speech engine's full host list is in the Reading Answers Aloud bullet below.
50
  - **Page Reading Is On By Default, And Reversible**: `enablePageContentFetch` ships on and the user can turn it off under AI Settings; nothing is read until AI responses are enabled, and the server (never the browser) requests the top result pages, so those sites see the instance and no user cookies or IP
51
  - **Search And Page Reading Leave No Query In The Log**: The search query is never written to the server log, and neither is the URL of a page read for grounding; how often searches come back empty, fail on unresponsive engines, are fully discarded, are served by the fallback or lose the fallback too, and how page reads ended, are counted instead and reported on `/status` (`searchesWithoutResults`, `searchesWithUnresponsiveEngines`, `searchesWithAllResultsDiscarded`, `searchesServedByFallback`, `searchesFailedOnFallback`, `pageReads`). `searches.unresponsiveEngines` names the engines behind those failures, which is configured infrastructure rather than anything a user typed, the same basis on which `inference.byModel` names model ids. What each failed with is published as one of `blocked`, `timeout` or `other`, never SearXNG's own wording: `/status` needs no token, and an upstream engine is free to build its error text out of the request URL, and so out of the query. The raw string stays in the server log and in the thrown error the search endpoint catches; the 502 it answers with carries a fixed message, so the string never leaves the server. The page reader's per-host circuit breaker (`server/pageReadHostBreaker.ts`) holds the hosts it has boxed in memory, since it needs them to know which host to skip; a page-read host comes from the search results, downstream of the query, so unlike an engine name it is never published: `/status` reports how many reads were skipped and how often a host was boxed, as counts only. Anything in front of the instance keeps its own access log, where `?q=` appears in full
@@ -96,7 +96,7 @@ The full trust model lives in [`.github/SECURITY.md`](../.github/SECURITY.md) ("
96
  | `server/pageReadsSinceLastRestart.ts` | In-memory aggregate counters for pages read for grounding (outcomes, durations, passage ratios), reported on `/status`; records no query, URL, host, or per-read timestamp |
97
  | `server/pageReadHostBreaker.ts` | Per-host circuit breaker for page reads: a host that refused the last three reads is skipped for five minutes, then probed with one read; holds host names in memory only, reset on restart, and reports counts alone |
98
  | `server/searchEndpointServerHook.ts` | Proxies text/image search to SearXNG after token verification (via `handleTokenVerification`), and serves fallback text results when SearXNG has failed and `SEARCH_FALLBACK_ENABLED` is on |
99
- | `server/fallbackSearchService.ts` | Second text-search source, consulted only after SearXNG has failed: reads `SEARCH_FALLBACK_ENABLED` and `SEARCH_FALLBACK_API_KEY` at call time, runs the JSON-RPC exchange against `search.parallel.ai` under one 15-second budget, and maps the reply into the tuples the SearXNG path produces; records no query |
100
  | `server/verifyTokenAndRateLimit.ts` | Verifies the Argon2 token hash and enforces rate limiting (10 requests per 10 seconds, shared by the search, page-content and inference endpoints; a separate 60-per-10-seconds budget for `/thumbnail`) |
101
  | `server/handleTokenVerification.ts` | Middleware bridge that calls `verifyTokenAndRateLimit` and writes 400/401/429 error responses for the search, page-content, thumbnail and inference endpoints |
102
  | `server/configEndpointServerHook.ts` | Serves the non-secret runtime config at `/api/config`, including whether access keys are enabled |
@@ -171,7 +171,7 @@ pin and keeps the old rebinding residual.
171
  ## Threat Model
172
 
173
  - **Local Environment**: Assumes trusted local execution
174
- - **Network Requests**: All external requests go through SearXNG proxy, except a text search whose SearXNG search failed, which goes to `search.parallel.ai`, and only when `SEARCH_FALLBACK_ENABLED` is on (off by default)
175
  - **AI Models**: Models run locally or through trusted providers
176
  - **Data Exfiltration**: Prevented by local-first architecture
177
 
 
44
 
45
  - **Local-First Storage**: All data stored in IndexedDB, no cloud sync
46
  - **No Tracking**: No telemetry, analytics, or user tracking
47
+ - **SearXNG Integration**: All web searches routed through privacy-focused metasearch, which holds for every search as long as `SEARCH_FALLBACK_ENABLED` is off (the default); with it on, a text search that found SearXNG failed goes to the fallback providers instead, and an image search never does
48
+ - **The Fallback Sends The Query To A Third Party When It Is On**: With `SEARCH_FALLBACK_ENABLED` on (off by default), a text query whose SearXNG search failed is sent to the providers `SEARCH_FALLBACK_PROVIDERS` lists (all of them when it is empty): `search.parallel.ai` (Parallel) and You.com's MCP endpoint (`https://api.you.com/mcp`). They are tried one after the other, in a random order, until one returns results, so a single search can reach more than one provider. Each provider that is tried sees the query text and the server's IP address. The user's IP address is never sent, because the request leaves from the server rather than from the browser. The query is still never written to the server log, and neither is any provider's error text; what `/status` publishes for this feature is the two counts `searchesServedByFallback` and `searchesFailedOnFallback` and the per-provider counters under `searches.fallbackProviders`, none of which carries a query. The provider names there are configured infrastructure, not anything a user typed
49
  - **Browser-Only Mode Still Downloads Models From HuggingFace**: The instance sends no telemetry and no analytics. But browser-only mode is not fully offline: both wllama and the local text-to-speech engine fetch their models from `huggingface.co` (served from `us.aws.cdn.hf.co`), so that host sees the user's IP. The text-to-speech engine's full host list is in the Reading Answers Aloud bullet below.
50
  - **Page Reading Is On By Default, And Reversible**: `enablePageContentFetch` ships on and the user can turn it off under AI Settings; nothing is read until AI responses are enabled, and the server (never the browser) requests the top result pages, so those sites see the instance and no user cookies or IP
51
  - **Search And Page Reading Leave No Query In The Log**: The search query is never written to the server log, and neither is the URL of a page read for grounding; how often searches come back empty, fail on unresponsive engines, are fully discarded, are served by the fallback or lose the fallback too, and how page reads ended, are counted instead and reported on `/status` (`searchesWithoutResults`, `searchesWithUnresponsiveEngines`, `searchesWithAllResultsDiscarded`, `searchesServedByFallback`, `searchesFailedOnFallback`, `pageReads`). `searches.unresponsiveEngines` names the engines behind those failures, which is configured infrastructure rather than anything a user typed, the same basis on which `inference.byModel` names model ids. What each failed with is published as one of `blocked`, `timeout` or `other`, never SearXNG's own wording: `/status` needs no token, and an upstream engine is free to build its error text out of the request URL, and so out of the query. The raw string stays in the server log and in the thrown error the search endpoint catches; the 502 it answers with carries a fixed message, so the string never leaves the server. The page reader's per-host circuit breaker (`server/pageReadHostBreaker.ts`) holds the hosts it has boxed in memory, since it needs them to know which host to skip; a page-read host comes from the search results, downstream of the query, so unlike an engine name it is never published: `/status` reports how many reads were skipped and how often a host was boxed, as counts only. Anything in front of the instance keeps its own access log, where `?q=` appears in full
 
96
  | `server/pageReadsSinceLastRestart.ts` | In-memory aggregate counters for pages read for grounding (outcomes, durations, passage ratios), reported on `/status`; records no query, URL, host, or per-read timestamp |
97
  | `server/pageReadHostBreaker.ts` | Per-host circuit breaker for page reads: a host that refused the last three reads is skipped for five minutes, then probed with one read; holds host names in memory only, reset on restart, and reports counts alone |
98
  | `server/searchEndpointServerHook.ts` | Proxies text/image search to SearXNG after token verification (via `handleTokenVerification`), and serves fallback text results when SearXNG has failed and `SEARCH_FALLBACK_ENABLED` is on |
99
+ | `server/fallbackSearchService.ts` | Second text-search source, consulted only after SearXNG has failed: reads `SEARCH_FALLBACK_ENABLED`, `SEARCH_FALLBACK_PROVIDERS` and the per-provider keys at call time, and refuses to start on an unknown provider name or a removed variable; runs the JSON-RPC exchange against `search.parallel.ai` and You.com's MCP endpoint in a random order per search, one after the other until one returns results, under one shared 15-second budget; pauses a provider that answered 429 until its `Retry-After`; and maps the reply into the tuples the SearXNG path produces; records no query |
100
  | `server/verifyTokenAndRateLimit.ts` | Verifies the Argon2 token hash and enforces rate limiting (10 requests per 10 seconds, shared by the search, page-content and inference endpoints; a separate 60-per-10-seconds budget for `/thumbnail`) |
101
  | `server/handleTokenVerification.ts` | Middleware bridge that calls `verifyTokenAndRateLimit` and writes 400/401/429 error responses for the search, page-content, thumbnail and inference endpoints |
102
  | `server/configEndpointServerHook.ts` | Serves the non-secret runtime config at `/api/config`, including whether access keys are enabled |
 
171
  ## Threat Model
172
 
173
  - **Local Environment**: Assumes trusted local execution
174
+ - **Network Requests**: All external requests go through SearXNG proxy, except a text search whose SearXNG search failed, which goes to the fallback providers `SEARCH_FALLBACK_PROVIDERS` lists (`search.parallel.ai` and You.com's MCP endpoint, one after the other until one returns results, so possibly more than one per search), and only when `SEARCH_FALLBACK_ENABLED` is on (off by default)
175
  - **AI Models**: Models run locally or through trusted providers
176
  - **Data Exfiltration**: Prevented by local-first architecture
177
 
package-lock.json CHANGED
@@ -95,13 +95,13 @@
95
  "license": "MIT"
96
  },
97
  "node_modules/@ai-sdk/gateway": {
98
- "version": "4.0.103",
99
- "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.103.tgz",
100
- "integrity": "sha512-nnGTUHzPRQ14jhv9CL4OQrzcRBIFTBMIPKGIi9Z9u/4GtwSRxa/nxUX6np2Iwp5phOzO0a0njj/j9Jolf5AEaA==",
101
  "license": "Apache-2.0",
102
  "dependencies": {
103
- "@ai-sdk/provider": "4.0.21",
104
- "@ai-sdk/provider-utils": "5.0.53",
105
  "@vercel/oidc": "3.2.0"
106
  },
107
  "engines": {
@@ -111,14 +111,54 @@
111
  "zod": "^3.25.76 || ^4.1.8"
112
  }
113
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  "node_modules/@ai-sdk/openai-compatible": {
115
- "version": "3.0.62",
116
- "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.62.tgz",
117
- "integrity": "sha512-IAz1xv8zot4KLGa8kN7UfAA6H5+ARzAgAKLsoKxHPBzZuOc2E3ohaELg0x/JR9HVTbMbiftb0dPyCb8cT2KBRg==",
118
  "license": "Apache-2.0",
119
  "dependencies": {
120
- "@ai-sdk/provider": "4.0.21",
121
- "@ai-sdk/provider-utils": "5.0.53"
122
  },
123
  "engines": {
124
  "node": ">=22"
@@ -127,10 +167,10 @@
127
  "zod": "^3.25.76 || ^4.1.8"
128
  }
129
  },
130
- "node_modules/@ai-sdk/provider": {
131
- "version": "4.0.21",
132
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.21.tgz",
133
- "integrity": "sha512-UpbC9C1oht8dhfKPbVXSLRZS3DI8uk8n5v2uMjBPNIYGsC2kL045ywq7D8Hh9KgnH9rP5W/GxQdYtWScRouqlA==",
134
  "license": "Apache-2.0",
135
  "dependencies": {
136
  "json-schema": "^0.4.0"
@@ -139,13 +179,13 @@
139
  "node": ">=22"
140
  }
141
  },
142
- "node_modules/@ai-sdk/provider-utils": {
143
- "version": "5.0.53",
144
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.53.tgz",
145
- "integrity": "sha512-VVe6UDd0y0/B4TfauuKKzcppqvhTmSluwwUVT4WG2/8h7gFbyFm7PM4UGM+ZXe3/RI6bv+Vl1cw/7aiBoQqdxg==",
146
  "license": "Apache-2.0",
147
  "dependencies": {
148
- "@ai-sdk/provider": "4.0.21",
149
  "@standard-schema/spec": "^1.1.0",
150
  "@workflow/serde": "4.1.0",
151
  "eventsource-parser": "^3.0.8",
@@ -158,7 +198,7 @@
158
  "zod": "^3.25.76 || ^4.1.8"
159
  }
160
  },
161
- "node_modules/@ai-sdk/provider-utils/node_modules/undici": {
162
  "version": "7.30.0",
163
  "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
164
  "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
@@ -785,13 +825,13 @@
785
  "license": "MIT"
786
  },
787
  "node_modules/@mantine/carousel": {
788
- "version": "9.7.0",
789
- "resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-9.7.0.tgz",
790
- "integrity": "sha512-L/6IPhltQrDj+TXJUzse73O5rMrHETVciIfDeMnpe3Kl65TrsfbI304wl7SjqnPumwIRHjimfJaY7EIMer6pZg==",
791
  "license": "MIT",
792
  "peerDependencies": {
793
- "@mantine/core": "9.7.0",
794
- "@mantine/hooks": "9.7.0",
795
  "embla-carousel": ">=8.0.0",
796
  "embla-carousel-react": ">=8.0.0",
797
  "react": "^19.2.0",
@@ -799,24 +839,24 @@
799
  }
800
  },
801
  "node_modules/@mantine/code-highlight": {
802
- "version": "9.7.0",
803
- "resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-9.7.0.tgz",
804
- "integrity": "sha512-zVSe6MkNW2pkhXsZdja/klNWNfXJXG0Re5O7Bd9z/ubLkK3e0xa4daMH2AwOuyZSf3btSFqv1hY1dJPIGC2BMA==",
805
  "license": "MIT",
806
  "dependencies": {
807
  "clsx": "^2.1.1"
808
  },
809
  "peerDependencies": {
810
- "@mantine/core": "9.7.0",
811
- "@mantine/hooks": "9.7.0",
812
  "react": "^19.2.0",
813
  "react-dom": "^19.2.0"
814
  }
815
  },
816
  "node_modules/@mantine/core": {
817
- "version": "9.7.0",
818
- "resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.7.0.tgz",
819
- "integrity": "sha512-WhCvfXXpYMKV6py05wt0Vlxthtxe4YOzQiVhrrCvWKYDmvhKXgOTDzRdjkGHNji4r+7f7d3nz2XpwbzaZ47R/w==",
820
  "license": "MIT",
821
  "dependencies": {
822
  "@floating-ui/react": "^0.27.20",
@@ -826,15 +866,15 @@
826
  "type-fest": "^5.10.0"
827
  },
828
  "peerDependencies": {
829
- "@mantine/hooks": "9.7.0",
830
  "react": "^19.2.0",
831
  "react-dom": "^19.2.0"
832
  }
833
  },
834
  "node_modules/@mantine/form": {
835
- "version": "9.7.0",
836
- "resolved": "https://registry.npmjs.org/@mantine/form/-/form-9.7.0.tgz",
837
- "integrity": "sha512-v7GuFaKzM5TRpxh0LmlwM3S59yB2PXQjmn7IdbKQpvW+72tKJZwKnbEFJbYfTehBS73MKxqO+MGctHLUc1Yz2g==",
838
  "license": "MIT",
839
  "dependencies": {
840
  "@standard-schema/spec": "^1.1.0",
@@ -846,34 +886,34 @@
846
  }
847
  },
848
  "node_modules/@mantine/hooks": {
849
- "version": "9.7.0",
850
- "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.7.0.tgz",
851
- "integrity": "sha512-3a1w90f1OliaClGRlnPiviJtKxyMiKdVyM8Pd2h6se+LOm9/bgGI+pgoGw/tgv++BSQ4frqv+X7NBlqcY/S7KQ==",
852
  "license": "MIT",
853
  "peerDependencies": {
854
  "react": "^19.2.0"
855
  }
856
  },
857
  "node_modules/@mantine/notifications": {
858
- "version": "9.7.0",
859
- "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-9.7.0.tgz",
860
- "integrity": "sha512-0WUBbZpgDOO86YsGjV3zBYge+jkjIC9VLojSah4g0ZaYzKnh1cANRc0RF1OwSosABKaDZKZ1FAvlAhP8hq2ApQ==",
861
  "license": "MIT",
862
  "dependencies": {
863
- "@mantine/store": "9.7.0",
864
  "react-transition-group": "4.4.5"
865
  },
866
  "peerDependencies": {
867
- "@mantine/core": "9.7.0",
868
- "@mantine/hooks": "9.7.0",
869
  "react": "^19.2.0",
870
  "react-dom": "^19.2.0"
871
  }
872
  },
873
  "node_modules/@mantine/store": {
874
- "version": "9.7.0",
875
- "resolved": "https://registry.npmjs.org/@mantine/store/-/store-9.7.0.tgz",
876
- "integrity": "sha512-r1eCDANMSITib3NvYU82JBXVivT7wEDvHeTyRJdO+slqH+K9PY5PIIeasGIocQNXyi9HQph6rz+e3eou5abRMw==",
877
  "license": "MIT",
878
  "peerDependencies": {
879
  "react": "^19.2.0"
@@ -2988,14 +3028,14 @@
2988
  }
2989
  },
2990
  "node_modules/ai": {
2991
- "version": "7.0.127",
2992
- "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.127.tgz",
2993
- "integrity": "sha512-JNsNPk4ZvRGEJuqdVo2lYFkm7uAuf3p/RwKFWn1Wh/+IK3U1RyFWw2GXoFnhzqNfxEuj7XJB4WpIUhl1sHToWg==",
2994
  "license": "Apache-2.0",
2995
  "dependencies": {
2996
- "@ai-sdk/gateway": "4.0.103",
2997
- "@ai-sdk/provider": "4.0.21",
2998
- "@ai-sdk/provider-utils": "5.0.53"
2999
  },
3000
  "engines": {
3001
  "node": ">=22"
@@ -3004,6 +3044,46 @@
3004
  "zod": "^3.25.76 || ^4.1.8"
3005
  }
3006
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3007
  "node_modules/ansi-regex": {
3008
  "version": "5.0.1",
3009
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3617,9 +3697,9 @@
3617
  }
3618
  },
3619
  "node_modules/dotenv": {
3620
- "version": "18.0.5",
3621
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.5.tgz",
3622
- "integrity": "sha512-aBrGvt6KhjxbEnatqMWOMidftwsrCiCvqh1yNIGB9QyF0J+VcWkKDMsvYOEm7ZS82oiV+BT1sTOtIlAmUlTpwQ==",
3623
  "license": "BSD-2-Clause",
3624
  "bin": {
3625
  "dotenv": "dist/index.cjs"
@@ -4623,9 +4703,9 @@
4623
  }
4624
  },
4625
  "node_modules/knip": {
4626
- "version": "6.39.0",
4627
- "resolved": "https://registry.npmjs.org/knip/-/knip-6.39.0.tgz",
4628
- "integrity": "sha512-Y7g6Ve1ATMox1u1v+gx3z9LTlvzVLTJISm/LVn9TgBU4qc7OoC63AWGO8qCFPVDDdaqlYdXkDDvvO1METBfTRw==",
4629
  "dev": true,
4630
  "funding": [
4631
  {
@@ -7827,9 +7907,9 @@
7827
  }
7828
  },
7829
  "node_modules/vite": {
7830
- "version": "8.3.2",
7831
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz",
7832
- "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==",
7833
  "license": "MIT",
7834
  "dependencies": {
7835
  "lightningcss": "^1.33.0",
 
95
  "license": "MIT"
96
  },
97
  "node_modules/@ai-sdk/gateway": {
98
+ "version": "4.0.106",
99
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.106.tgz",
100
+ "integrity": "sha512-NprSdFzyVnbEp2SDWtIgsCFIUHFCehOD9S6FQkF/dLrCmnke8w4SOK3KcmUUiEbwtVzlUF8prkaELp+ohcQaQw==",
101
  "license": "Apache-2.0",
102
  "dependencies": {
103
+ "@ai-sdk/provider": "4.0.24",
104
+ "@ai-sdk/provider-utils": "5.0.56",
105
  "@vercel/oidc": "3.2.0"
106
  },
107
  "engines": {
 
111
  "zod": "^3.25.76 || ^4.1.8"
112
  }
113
  },
114
+ "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": {
115
+ "version": "4.0.24",
116
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.24.tgz",
117
+ "integrity": "sha512-iztFqJTdxNN0ZzqZAAnVPMhRMaL1bfQFa3GIPAffWQ0+dP2FXIkyMD8kHeEI7EpB/C6TR45cqOkrS7YZZ1sATA==",
118
+ "license": "Apache-2.0",
119
+ "dependencies": {
120
+ "json-schema": "^0.4.0"
121
+ },
122
+ "engines": {
123
+ "node": ">=22"
124
+ }
125
+ },
126
+ "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": {
127
+ "version": "5.0.56",
128
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.56.tgz",
129
+ "integrity": "sha512-F7/U1YpVXEfSxRHbxScm0PVdr9lN6GaUWHnt8ws2UjuGeuxWHqCkV25dGpHAkpDHkuIXoC+aak/OqSylKSJmHw==",
130
+ "license": "Apache-2.0",
131
+ "dependencies": {
132
+ "@ai-sdk/provider": "4.0.24",
133
+ "@standard-schema/spec": "^1.1.0",
134
+ "@workflow/serde": "4.1.0",
135
+ "eventsource-parser": "^3.0.8",
136
+ "undici": "^7.29.0"
137
+ },
138
+ "engines": {
139
+ "node": ">=22"
140
+ },
141
+ "peerDependencies": {
142
+ "zod": "^3.25.76 || ^4.1.8"
143
+ }
144
+ },
145
+ "node_modules/@ai-sdk/gateway/node_modules/undici": {
146
+ "version": "7.30.0",
147
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
148
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
149
+ "license": "MIT",
150
+ "engines": {
151
+ "node": ">=20.18.1"
152
+ }
153
+ },
154
  "node_modules/@ai-sdk/openai-compatible": {
155
+ "version": "3.0.65",
156
+ "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.65.tgz",
157
+ "integrity": "sha512-TBfQk1hf9hOioOwdeDvuXdbmFDp+uLvYf5lfu/gCKVFy6rzjpEqTekEln2ebYonrnq2lIglsuj9S9IovaGhDZA==",
158
  "license": "Apache-2.0",
159
  "dependencies": {
160
+ "@ai-sdk/provider": "4.0.24",
161
+ "@ai-sdk/provider-utils": "5.0.56"
162
  },
163
  "engines": {
164
  "node": ">=22"
 
167
  "zod": "^3.25.76 || ^4.1.8"
168
  }
169
  },
170
+ "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider": {
171
+ "version": "4.0.24",
172
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.24.tgz",
173
+ "integrity": "sha512-iztFqJTdxNN0ZzqZAAnVPMhRMaL1bfQFa3GIPAffWQ0+dP2FXIkyMD8kHeEI7EpB/C6TR45cqOkrS7YZZ1sATA==",
174
  "license": "Apache-2.0",
175
  "dependencies": {
176
  "json-schema": "^0.4.0"
 
179
  "node": ">=22"
180
  }
181
  },
182
+ "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": {
183
+ "version": "5.0.56",
184
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.56.tgz",
185
+ "integrity": "sha512-F7/U1YpVXEfSxRHbxScm0PVdr9lN6GaUWHnt8ws2UjuGeuxWHqCkV25dGpHAkpDHkuIXoC+aak/OqSylKSJmHw==",
186
  "license": "Apache-2.0",
187
  "dependencies": {
188
+ "@ai-sdk/provider": "4.0.24",
189
  "@standard-schema/spec": "^1.1.0",
190
  "@workflow/serde": "4.1.0",
191
  "eventsource-parser": "^3.0.8",
 
198
  "zod": "^3.25.76 || ^4.1.8"
199
  }
200
  },
201
+ "node_modules/@ai-sdk/openai-compatible/node_modules/undici": {
202
  "version": "7.30.0",
203
  "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
204
  "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
 
825
  "license": "MIT"
826
  },
827
  "node_modules/@mantine/carousel": {
828
+ "version": "9.7.1",
829
+ "resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-9.7.1.tgz",
830
+ "integrity": "sha512-lEa4BjqsrCUp4xJ4pJyaOCvcXGyqRfSYUumUvY/Q1KHrkqTdReiZCdURcsX+OHjqrju1KruddWH/0qNzAYJ9Ew==",
831
  "license": "MIT",
832
  "peerDependencies": {
833
+ "@mantine/core": "9.7.1",
834
+ "@mantine/hooks": "9.7.1",
835
  "embla-carousel": ">=8.0.0",
836
  "embla-carousel-react": ">=8.0.0",
837
  "react": "^19.2.0",
 
839
  }
840
  },
841
  "node_modules/@mantine/code-highlight": {
842
+ "version": "9.7.1",
843
+ "resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-9.7.1.tgz",
844
+ "integrity": "sha512-iKr+gUauwvW5yrdB6rSNAeKCZqwuzXpb5pwopgymCB9JJs/UI7mfR5vyRkR5NkDgZOUHJU4O1QqzzG3bkroWPA==",
845
  "license": "MIT",
846
  "dependencies": {
847
  "clsx": "^2.1.1"
848
  },
849
  "peerDependencies": {
850
+ "@mantine/core": "9.7.1",
851
+ "@mantine/hooks": "9.7.1",
852
  "react": "^19.2.0",
853
  "react-dom": "^19.2.0"
854
  }
855
  },
856
  "node_modules/@mantine/core": {
857
+ "version": "9.7.1",
858
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.7.1.tgz",
859
+ "integrity": "sha512-SU1xklOfdEDzpPhw35ti9rF4QRE9HUongCe37C+xCO2Ea8MLJkAzreCrMxb8qcCij+gnF7SQbtTCBa1r+pKxYA==",
860
  "license": "MIT",
861
  "dependencies": {
862
  "@floating-ui/react": "^0.27.20",
 
866
  "type-fest": "^5.10.0"
867
  },
868
  "peerDependencies": {
869
+ "@mantine/hooks": "9.7.1",
870
  "react": "^19.2.0",
871
  "react-dom": "^19.2.0"
872
  }
873
  },
874
  "node_modules/@mantine/form": {
875
+ "version": "9.7.1",
876
+ "resolved": "https://registry.npmjs.org/@mantine/form/-/form-9.7.1.tgz",
877
+ "integrity": "sha512-fVjWYdiMBw21z/bqUkFghyV1MsANHoqCZaXji0k4KJaRCHh7cD3qhBuLIVhc2hUCKXQ3cHiJTHLz3ayoE/mRZg==",
878
  "license": "MIT",
879
  "dependencies": {
880
  "@standard-schema/spec": "^1.1.0",
 
886
  }
887
  },
888
  "node_modules/@mantine/hooks": {
889
+ "version": "9.7.1",
890
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.7.1.tgz",
891
+ "integrity": "sha512-C1xD4A3bfB87nUsLHr42WqOabnK4G+fCyEf8FN14EEiZAcKjdcWJbK63qxhIXFKuHCc3acLbpm2ts9UqmLASTw==",
892
  "license": "MIT",
893
  "peerDependencies": {
894
  "react": "^19.2.0"
895
  }
896
  },
897
  "node_modules/@mantine/notifications": {
898
+ "version": "9.7.1",
899
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-9.7.1.tgz",
900
+ "integrity": "sha512-gmN+rTrjwYFJqaIqDQtQfoTJ/OR0wSxqxIihOHD5/S/sYUycVi9wDqK4tub5y8x0HeQH4kNkehPMN4l1Uk0A6A==",
901
  "license": "MIT",
902
  "dependencies": {
903
+ "@mantine/store": "9.7.1",
904
  "react-transition-group": "4.4.5"
905
  },
906
  "peerDependencies": {
907
+ "@mantine/core": "9.7.1",
908
+ "@mantine/hooks": "9.7.1",
909
  "react": "^19.2.0",
910
  "react-dom": "^19.2.0"
911
  }
912
  },
913
  "node_modules/@mantine/store": {
914
+ "version": "9.7.1",
915
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-9.7.1.tgz",
916
+ "integrity": "sha512-CYCQHWt7zFrUe2kVQmFAWnQaZbiDUoN2VeMJBI/vf1XybGj+itTEB6cnvoX4C8yOrln3sXe4BIoIf46pUhnEhA==",
917
  "license": "MIT",
918
  "peerDependencies": {
919
  "react": "^19.2.0"
 
3028
  }
3029
  },
3030
  "node_modules/ai": {
3031
+ "version": "7.0.130",
3032
+ "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.130.tgz",
3033
+ "integrity": "sha512-QkBkCogl46jXUg0SrSsdqQlOI2ZVMCjfnbBQS8t3gE21U/SO17oNMVFyjmcC9z20wOoYUA+2O2vsso0maoS7dw==",
3034
  "license": "Apache-2.0",
3035
  "dependencies": {
3036
+ "@ai-sdk/gateway": "4.0.106",
3037
+ "@ai-sdk/provider": "4.0.24",
3038
+ "@ai-sdk/provider-utils": "5.0.56"
3039
  },
3040
  "engines": {
3041
  "node": ">=22"
 
3044
  "zod": "^3.25.76 || ^4.1.8"
3045
  }
3046
  },
3047
+ "node_modules/ai/node_modules/@ai-sdk/provider": {
3048
+ "version": "4.0.24",
3049
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.24.tgz",
3050
+ "integrity": "sha512-iztFqJTdxNN0ZzqZAAnVPMhRMaL1bfQFa3GIPAffWQ0+dP2FXIkyMD8kHeEI7EpB/C6TR45cqOkrS7YZZ1sATA==",
3051
+ "license": "Apache-2.0",
3052
+ "dependencies": {
3053
+ "json-schema": "^0.4.0"
3054
+ },
3055
+ "engines": {
3056
+ "node": ">=22"
3057
+ }
3058
+ },
3059
+ "node_modules/ai/node_modules/@ai-sdk/provider-utils": {
3060
+ "version": "5.0.56",
3061
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.56.tgz",
3062
+ "integrity": "sha512-F7/U1YpVXEfSxRHbxScm0PVdr9lN6GaUWHnt8ws2UjuGeuxWHqCkV25dGpHAkpDHkuIXoC+aak/OqSylKSJmHw==",
3063
+ "license": "Apache-2.0",
3064
+ "dependencies": {
3065
+ "@ai-sdk/provider": "4.0.24",
3066
+ "@standard-schema/spec": "^1.1.0",
3067
+ "@workflow/serde": "4.1.0",
3068
+ "eventsource-parser": "^3.0.8",
3069
+ "undici": "^7.29.0"
3070
+ },
3071
+ "engines": {
3072
+ "node": ">=22"
3073
+ },
3074
+ "peerDependencies": {
3075
+ "zod": "^3.25.76 || ^4.1.8"
3076
+ }
3077
+ },
3078
+ "node_modules/ai/node_modules/undici": {
3079
+ "version": "7.30.0",
3080
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
3081
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
3082
+ "license": "MIT",
3083
+ "engines": {
3084
+ "node": ">=20.18.1"
3085
+ }
3086
+ },
3087
  "node_modules/ansi-regex": {
3088
  "version": "5.0.1",
3089
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
 
3697
  }
3698
  },
3699
  "node_modules/dotenv": {
3700
+ "version": "18.0.6",
3701
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.6.tgz",
3702
+ "integrity": "sha512-ZMVmohV6CvAJtj++pZNyggluUt6hbRdQAxc2+Z635RL6/AG/ruXv5lHnybUFoxMPL/yrTL/tsF4paZu2l7RA6Q==",
3703
  "license": "BSD-2-Clause",
3704
  "bin": {
3705
  "dotenv": "dist/index.cjs"
 
4703
  }
4704
  },
4705
  "node_modules/knip": {
4706
+ "version": "6.40.0",
4707
+ "resolved": "https://registry.npmjs.org/knip/-/knip-6.40.0.tgz",
4708
+ "integrity": "sha512-5Lp/ANJu3AkW5f+J+2QG14z7xKZGi/ybb5MU1EWjeGdt6MpuIaLpLFiyBi0ZKryTiuTHiJqo/WKJRaWYimvavw==",
4709
  "dev": true,
4710
  "funding": [
4711
  {
 
7907
  }
7908
  },
7909
  "node_modules/vite": {
7910
+ "version": "8.3.3",
7911
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz",
7912
+ "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==",
7913
  "license": "MIT",
7914
  "dependencies": {
7915
  "lightningcss": "^1.33.0",