diff --git a/docs/architecture/chat.md b/docs/architecture/chat.md index b55f54ca1e..918538f8c6 100644 --- a/docs/architecture/chat.md +++ b/docs/architecture/chat.md @@ -9,7 +9,7 @@ A chat thread belongs to a workspace. Each user message retrieves its own contex | Method | Path | Does | |---|---|---| -| `POST` | `/workspaces/{workspace_id}/chat/threads` | open a thread, with an optional `source_scope`, `mode`, `basic` or `agentic`, and `remember`, which makes the mode the model's default ([Agent threads](#agent-threads)); the title defaults to "New chat" | +| `POST` | `/workspaces/{workspace_id}/chat/threads` | open a thread, with an optional `source_scope` and `mode`, `basic` or `agentic` ([Agent threads](#agent-threads)); the title defaults to "New chat" | | `GET` | `/workspaces/{workspace_id}/chat/threads` | list, newest first, each with `running` while a reply is being generated for it, and `run_state`, `{state, position}`: `queued` with its place in line, `running`, or `needs-approval` while an agent turn waits on the user; `null` when nothing runs | | `PATCH` | `/chat/threads/{thread_id}` | rename, 1 to 200 characters | | `DELETE` | `/chat/threads/{thread_id}` | delete; a running reply is stopped first, waiting up to 5 seconds, then its messages cascade, its artifacts stay with the thread cleared, and its images' folder goes after the commit | @@ -262,12 +262,12 @@ Interface strings are `dictation__`, and a server code is `dic - The composer follows assistant-ui's keyboard behaviour. - While the turn waits, the thinking header's `role="status"` region says "Thinking" or the reading quarter. A second region (`reply-announcer.tsx`) stays silent until it says "Reply finished" once, for a reply it watched run to completion. It is keyed by message, because messages render by index and an instance would otherwise watch one thread's reply and announce another's. Tokens are never fed to it, since a polite region read on every delta would interrupt without pause; a reply loaded from history, a turn that ended with no answer, and one that failed or was stopped after text arrived say nothing; its alert, or the stop, already does. A stopped reply is marked `incomplete` with reason `cancelled` by the runtime ([`use-chat-runtime.ts`](../../surfsense_local/frontend/src/features/chat/use-chat-runtime.ts)), since assistant-ui would otherwise call it complete once the run ends. A queued reply's header region says "Waiting for another reply" with its place. The Chats button's count is in its accessible name ("Chats, 2 replies being written"), and an unread row carries "New reply" as screen-reader text. - The conversation has a heading, the thread's title, hidden visually because the visible title is the rename button. Focus does not move to it: on every thread switch the composer keeps the focus assistant-ui gives it, so someone can switch and type, and the composer is described by the heading (`aria-describedby`), so a screen reader names the conversation it writes into. A new chat has no heading until it is a thread. -- The startup loader and the typed-in thread title respect `prefers-reduced-motion`. +- The startup fade and the typed-in thread title follow Settings › General's Reduce motion, which matches the system by default ([overview](overview.md#frontend)). - The mic carries `aria-pressed` and a name that follows its state: "Start voice input", "Stop voice input", or "Set up voice input"; its tooltip is visual only. While voice input runs, a `role="status"` line in the strip says "Starting the microphone…", "Listening", "Loading the speech recognition model…", "Transcribing…" or the 30-second warning; a problem is a `role="alert"`, the two limits a `role="status"`. Under `prefers-reduced-motion` the level bars give way to the still dot beside them. ## Agent threads -How the agent runs is in [agent](agent.md). A thread opened with `mode: "agentic"` is the agent's, and one opened with `mode: "basic"` the chat's; with no `mode` it takes the selected model's default ([model capabilities](model-capabilities.md#the-modes), [agent](agent.md#which-threads-get-it)). Agentic asked for and kept out by a technical gate is refused with `409`, `detail.code` `no_model`, `agent_not_installed`, `tool_calls_unsupported` or `window_below_floor`, and no thread is made. Opening an agent thread starts opencode if it is not running, waits until it serves SurfSense's configuration, and opens an opencode session under the thread's title; the session's id and the engine, `agent_engine` `opencode`, are stored on the thread, and `ThreadRead` reports `uses_agent`, true when the thread has an engine. When opencode does not become ready, a thread that asked for Agentic is removed and the request answers `503`, `detail.code` `agent_unavailable`; one that asked for nothing opens as a chat. A mode sent with `remember: true`, which the composer sends only for a mode the user picked, is remembered as the model's default. Which engine runs a thread's turns, and how it moves when the selected model changes, is in [agent](agent.md#two-engines). +How the agent runs is in [agent](agent.md). A thread opened with `mode: "agentic"` is the agent's, and one opened with `mode: "basic"` the chat's; with no `mode` it takes the selected model's default ([model capabilities](model-capabilities.md#the-modes), [agent](agent.md#which-threads-get-it)). Agentic asked for and kept out by a technical gate is refused with `409`, `detail.code` `no_model`, `agent_not_installed`, `tool_calls_unsupported` or `window_below_floor`, and no thread is made. Opening an agent thread starts opencode if it is not running, waits until it serves SurfSense's configuration, and opens an opencode session under the thread's title; the session's id and the engine, `agent_engine` `opencode`, are stored on the thread, and `ThreadRead` reports `uses_agent`, true when the thread has an engine. When opencode does not become ready, a thread that asked for Agentic is removed and the request answers `503`, `detail.code` `agent_unavailable`; one that asked for nothing opens as a chat. Opening a thread never changes the model's default; the composer's switch saves a pick itself ([model capabilities](model-capabilities.md#the-modes)). Which engine runs a thread's turns, and how it moves when the selected model changes, is in [agent](agent.md#two-engines). The same routes then reach the agent ([`modules/agent/agent_threads/`](../../surfsense_local/backend/modules/agent/agent_threads/)): diff --git a/docs/architecture/chatgpt-subscription.md b/docs/architecture/chatgpt-subscription.md index 803c2b9e98..bb66ce7c70 100644 --- a/docs/architecture/chatgpt-subscription.md +++ b/docs/architecture/chatgpt-subscription.md @@ -83,7 +83,7 @@ The OpenAI catalog records the plan's models as calling tools, so Agentic mode i ## Frontend -Every model list reads `serves` rather than deciding: the Settings sections, their empty states and onboarding's server steps leave a ChatGPT connection out of every section but chat. A new ChatGPT connection is made only through **Use your ChatGPT plan**, below. The connection form lists **ChatGPT subscription** only while editing a ChatGPT connection, so a server's provider list never offers it. Editing one hides the URL and key fields, shows the account, and offers **Sign in again**, with **Open the sign-in page again** and Cancel while it waits, **Sign out**, and **Save changes** for a new name. Its provider cannot be changed. The model groups show the account's email in place of the URL. +Every model list reads `serves` rather than deciding: the Settings sections and their empty states leave a ChatGPT connection out of every section but chat. A new ChatGPT connection is made only through **Use your ChatGPT plan**, below. The connection form lists **ChatGPT subscription** only while editing a ChatGPT connection, so a server's provider list never offers it. Editing one hides the URL and key fields, shows the account, and offers **Sign in again**, with **Open the sign-in page again** and Cancel while it waits, **Sign out**, and **Save changes** for a new name. Its provider cannot be changed. The model groups show the account's email in place of the URL. **Use your ChatGPT plan** sits below **Use a server** on Settings' **Add model** page and on onboarding's text step, wherever the sign-in's `serves` includes the slot ([`chatgpt-option.tsx`](../../surfsense_local/frontend/src/features/models/remote/connections/chatgpt/chatgpt-option.tsx)). One click signs in as **ChatGPT subscription**, with the dialog's progress in the row and Cancel in place of the button, then opens the new connection's models as connecting a server does. A plan already connected reads "Connected as" its email and offers **Show models** instead. diff --git a/docs/architecture/connections.md b/docs/architecture/connections.md index 5d74043454..b8392965b3 100644 --- a/docs/architecture/connections.md +++ b/docs/architecture/connections.md @@ -180,7 +180,7 @@ Keys are protected by envelope encryption ([ADR 0018](../adr/0018-keychain-envel - Each model section in Settings shows, as a group under the local models, every connection whose `serves` includes that section's slot. `serves` comes from one backend rule ([`serves.py`](../../surfsense_local/backend/modules/llm/connections/serves.py)): every slot but `speech_to_text` for a key connection, `text_gen` alone for a ChatGPT one. The section's empty state counts only those connections. A group loads its models only when opened, so a slow or failed endpoint does not hold up the others, and lists only those whose `selectable_for` includes that section's slot; the model in use, if it comes from that server, stays shown above the list whether the group is open or closed. Open, its list scrolls inside its own capped-height area rather than lengthening the page. A model's row shows its type chips, and a Vision chip when `reads_images` is true. A model is assigned after an optional test; an exact ID the listing lacks can be typed in. A new connection is added from **Use a server** on the section's **Add model** page. Adding and editing a connection happen in a dialog over the page. Saving a new connection returns to the list with its group open on its models, since choosing one is why it was added; saving an edit only refreshes. - Edit and Disconnect sit on each group. Edit opens the same form in that dialog. A connection can fill every slot but `speech_to_text`, so Disconnect names each model it will clear, Chat, Image or both, whichever section it is disconnected from. -- Onboarding's model steps use the same groups and the same dialog; with nothing connected yet, their **Connect** opens the dialog directly, and saving opens the server page on the new connection's models. +- Onboarding's chat step uses the same groups and the same dialog; with nothing connected yet, its **Connect** opens the dialog directly, and saving opens the server page on the new connection's models. - The connection form picks a provider from the remote manifest, through `GET /llm/catalog/remote`, or "Local or custom server". A ready provider fills its URL, which stays editable so a proxy in front of it still works; a provider that needs account details asks for each field and builds the URL from its template; a provider that needs a URL leaves it to the user; an unreachable one is listed, disabled, with its reason. A provider that takes no key, a loopback server, hides the key field. "Local or custom server" leaves the URL to the user, since its port is whatever its owner set, with `http://localhost:11434/v1` as placeholder text only; a loopback server the manifest lists, such as LM Studio, fills the manifest's URL like any ready provider. The save sends `catalog_provider`. ## Known gaps diff --git a/docs/architecture/local-models/catalog.md b/docs/architecture/local-models/catalog.md index 9ecdf8932e..52c5974441 100644 --- a/docs/architecture/local-models/catalog.md +++ b/docs/architecture/local-models/catalog.md @@ -506,7 +506,7 @@ counted rather than decoded `POST /llm/installs` takes `{"catalog_id": "...", "select": true}`, and optionally the `model_type` that `select` fills, the engine's first type when -absent, so onboarding's editing step installs straight into `image_edit`. Nothing +absent, so a speech model's download installs straight into `speech_to_text`. Nothing else: no repo, file, URL, path or quantization, so the renderer cannot name an arbitrary download. A curated build's id is minted once per process and keyed on its repo and weights path; a searched build's is a ticket holding the whole build diff --git a/docs/architecture/local-models/selection.md b/docs/architecture/local-models/selection.md index 36e49a8174..f2c1fe5347 100644 --- a/docs/architecture/local-models/selection.md +++ b/docs/architecture/local-models/selection.md @@ -202,13 +202,15 @@ route. Only the onboarding page's last step does, once a chat model is persisted. Once the marker exists the app never shows onboarding again, and a missing selection is fixed from Settings' Chat section. -The onboarding page opens on a welcome screen, then six steps: chat, image, image editing, audio, video and search model. The welcome is not counted as a step, but it is part of onboarding and gated by the same marker, so it is never shown again once onboarding is done. +The onboarding page opens on a welcome screen, then the privacy choices ([usage statistics](../usage-statistics.md#consent)), then two model steps: chat and search model. The welcome is not counted as a step, but it is part of onboarding and gated by the same marker, so it is never shown again once onboarding is done. Image, image editing, audio and video models are not asked for: they are chosen in Settings, and the feature that needs one says so. The embedding step comes last and is not a slot, but it is the same component -as the model steps below, with one more entry in their tables +as the chat step below, with its own entry in their tables ([`model-step/`](../../../surfsense_local/frontend/src/features/onboarding/model-step/)). -What a step does on Use is its own hook: a slot's saves the selection, and the -embedding step's ([`use-embedding-step.ts`](../../../surfsense_local/frontend/src/features/onboarding/model-step/kinds/use-embedding-step.ts)) +What a step does on Use is its own hook: the chat step's +([`use-chat-step.ts`](../../../surfsense_local/frontend/src/features/onboarding/model-step/kinds/use-chat-step.ts)) +saves the selection, and the embedding step's +([`use-embedding-step.ts`](../../../surfsense_local/frontend/src/features/onboarding/model-step/kinds/use-embedding-step.ts)) only marks a choice, In use until another is used, bge-small by default and again if the chosen one is deleted. Its downloads install with `select: false`; a Hugging Face pick is labelled not tested by SurfSense; the bundled bge-small has @@ -220,21 +222,17 @@ Finish sends the choice with the call that ends onboarding; Skip and finish, or Finish with the choice untouched, sends none, which means bge-small ([embedding](../embedding.md)). -The model steps are one component for any slot +The model steps are one component ([`frontend/src/features/onboarding/model-step/`](../../../surfsense_local/frontend/src/features/onboarding/model-step/)), -built on the same hooks as Settings but with its own screens. Each lists every -model this computer can run at once: in the chat step, models downloaded from Hugging Face first, then the curated list with its starred row first, with +built on the same hooks as Settings but with its own screens. The chat step lists every +model this computer can run at once: models downloaded from Hugging Face first, then the curated list with its starred row first, with Download, Use and Delete as in Settings, and no Delete on a model the app ships; a download's progress shows under its -row and never moves the page. The chat step also offers Settings' Hugging Face -search, closed until asked for; the image, image editing, audio and video steps have none, since -sd.cpp's and audio.cpp's models are the few the catalog ships. The image editing step lists first the model chosen for images earlier when it edits too, so FLUX.2 klein is one Use away, and its downloads fill `image_edit`. A server sits one line below the list and names -any connected earlier. Once the slot has a model, the footer names it beside -Continue. Onboarding installs with `select: true`, so a download is also the -choice; Settings installs with `select: false`. The chat step's Continue is -enabled only once a chat model is selected, local or from a server. The image, -image editing, audio and video steps are optional, and each enables its Continue or Finish only once -its slot has a model. Each step's Skip and Continue move on to the next, image -to image editing to audio to video. The last step's buttons read Skip and finish and Finish, and both post the marker. +row and never moves the page. It also offers Settings' Hugging Face +search, closed until asked for. A server sits one line below the list and names +any connected earlier. Once a chat model is chosen, the footer names it beside +Continue. The chat step installs with `select: true`, so a download is also the +choice; Settings installs with `select: false`. Its Continue is +enabled only once a chat model is selected, local or from a server, and it has no Skip. The embedding step's buttons read Skip and finish and Finish, and both post the marker. ## Resolution: local and remote diff --git a/docs/architecture/model-capabilities.md b/docs/architecture/model-capabilities.md index 5b819c7e2b..9e06d0b3c1 100644 --- a/docs/architecture/model-capabilities.md +++ b/docs/architecture/model-capabilities.md @@ -50,7 +50,7 @@ A chat is Basic (Q&A), the chat engine's retrieval answers with Studio beside it - **`agentic_allowed`**, and when not, **`blocked`**: a technical gate only, never a score ([ADR 0044](../adr/0044-a-score-sets-the-default-mode-never-whether-agentic-is-allowed.md)). `agent_not_installed` when no opencode is staged, except on a Claude plan, whose agent is the user's Claude Code; `tool_calls_unsupported` when the remote catalog states `tool_call: false`, or llama-server does not report `supports_tool_calls` for a local model's template; `window_below_floor` when the stated window is under 32,768 tokens ([`agent_gate.py`](../../surfsense_local/backend/modules/llm/capability/agent_gate.py)). A catalog that says nothing, and a runtime that cannot be read, allow it. A local model's tool calls and window are read when a chat starts, since reading them would load the model, so the selection read allows it until then; a chat refused by one of these gates disables Agentic for that model in the composer until the app restarts. - **`default_mode`**: the mode the user last chose for a new chat on this model; else Agentic for `agent` and `agent_limited`, which hold only on a remote host; else Basic. A blocked Agentic defaults to Basic. - **`reason`**, what the switch says beside Agentic: `measured_pass` or `measured_near` with `passed` and `counted`; `measured_below`, a low scorer, with the same; `assumed`; `local_copy`, a copy on the user's own machine or network of a model whose row passed on a remote host, with that `host`; `untested`. An assumed row says `assumed` on a local copy too, since it was run nowhere, and the capability's own `reason` is `assumed` there rather than `measured_elsewhere`. -- **`remembered_mode`**, under `chat_modes` in the text selection's `settings`, one entry per provider, connection and model name ([`modes/remembered.py`](../../surfsense_local/backend/modules/llm/capability/modes/remembered.py)), written when a chat is opened with a mode the user chose (`remember: true`). A default sent back unchosen is not written, so a later score or the developer switch turned off still decides. The entries stay when the slot takes another model, so a model's choice comes back with it. The `agent_trial` the modes replaced reads as its model's Agentic, kept as such when the slot changes, and the first write drops it. +- **`remembered_mode`**, under `chat_modes` in the text selection's `settings`, one entry per provider, connection and model name ([`modes/remembered.py`](../../surfsense_local/backend/modules/llm/capability/modes/remembered.py)), written when the user picks a mode in the composer's switch (`PUT /llm/selection/text_gen/mode`). Opening a chat writes nothing, so a default the user never touched follows a later score, or the developer switch turned off. A saved Agentic that a local model's runtime then refuses by a gate is dropped, so the model's own default holds again. The entries stay when the slot takes another model, so a model's choice comes back with it. The `agent_trial` the modes replaced reads as its model's Agentic, kept as such when the slot changes, and the first write drops it. `SURFSENSE_LOCAL_AGENT_UNTESTED_MODELS=1` makes Agentic the default for every model the user has not picked a mode for and lifts the window floor; a stated no on tool calls, or no opencode, still keeps Agentic out. @@ -58,11 +58,12 @@ A chat is Basic (Q&A), the chat engine's retrieval answers with Studio beside it - `SelectionRead.capability` on `GET` and `PUT /llm/selection/text_gen` (null for other slots): `level`, `label_key` (the ICU select branch), `reason` (`code`, `values`), `note`, `measured` (`key`, `suite`, `assumed`, `suite_version`, `measured_on`, `provider`, `host`, `reads_images`, `passed`, `counted`, `provisional`, or null) and `modes` (`agentic_allowed`, `blocked`, `default_mode`: `basic` or `agentic`, `reason` (`code`, `values`), `remembered_mode`), and `studio`: `full` or `simple`, the Studio path below. - `capability_level` on each row of `GET /llm/connections/{id}/models` and `GET /llm/providers/llamacpp/models` that can fill the chat slot. -- `POST /workspaces/{id}/chat/threads` takes `mode` and `remember` ([chat](chat.md#agent-threads)). +- `PUT /llm/selection/text_gen/mode` with `{"mode": "basic" | "agentic", "provider", "connection_id", "name"}` saves the pick as the selected model's `remembered_mode` and returns its `modes`; `404` with no chat model chosen, `409` with `detail.code` `model_changed` when the model named is no longer the selected one, so a pick is never saved onto another model, and `409` with the gate's `detail.code` for Agentic on a model a gate keeps out ([`modes/router.py`](../../surfsense_local/backend/modules/llm/capability/modes/router.py)). +- `POST /workspaces/{id}/chat/threads` takes `mode` ([chat](chat.md#agent-threads)). In dev builds only, the model picker tags each chat model by how it did in the Agentic tests: "Agentic", "Agentic, may need nudges", "Low Agentic score" or "Not tested"; production shows no tag. In every build, Settings › Models shows **What this model can do** under the chat model in use ([`model-capability.tsx`](../../surfsense_local/frontend/src/features/models/capability/model-capability.tsx)), closed until opened, so a rating never reads as a verdict beside the model's name. Opened, it says in plain words what Chat offers (both modes and how well, or Basic only and why), what Studio makes (`studio`: full designed pages and Office files, or simple ones and what a stronger model adds), and, last and small, where the rating came from: the cases passed and the day, "From a short test" while the list is provisional, or why it is untested. The row's English note is not shown. -The composer's switch ([`mode-picker.tsx`](../../surfsense_local/frontend/src/features/chat/modes/mode-picker.tsx)) is a menu of "Basic (Q&A)" and "Agentic", each with one line on what it does. On a new chat it starts at the model's `default_mode` and follows the model when it changes; a pick holds for that model for the session, in a context above the open workspace. The new thread's `mode` is always sent, with `remember: true` only when the user picked it, so only a pick becomes the model's default. An Agentic that did not pass carries a tag ("Not tested", "Low score", "May need nudges", "Local copy", "Expected to pass"); the `reason`, or a gate, shows in a tooltip beside the Agentic row on hover or focus and is read with the row, in both menus: "Passed 2 of 8 Agentic tests, so it may stop early or make mistakes.", "Not tested with this model yet, so it may stop early or make mistakes.", "Passed on its full-size version. A local copy may do worse.", or that a flagship is assumed. Nothing is said under the composer. A gated Agentic is disabled with its reason, such as "This model can’t use tools." In an open chat, under "This chat’s mode", the switch lists the same two rows with the chat's mode checked; the other is marked "New chat", and picking it picks that mode and opens a new chat. A refused Agentic chat is worded by its code in a toast, and the question and images go back into the composer. A refusal by a gate the selection could not say, such as a local model's tool calls, disables Agentic with its reason for that model for the session ([`new-chat-mode.ts`](../../surfsense_local/frontend/src/features/chat/modes/new-chat-mode.ts)), so the next chat starts in Basic (Q&A) rather than asking again. The agent reads no attached image yet, so an Agentic chat, new or open, takes none ([chat](chat.md)). +The composer's switch ([`mode-picker.tsx`](../../surfsense_local/frontend/src/features/chat/modes/mode-picker.tsx)) is a menu of "Basic (Q&A)" and "Agentic", each with one line on what it does. On a new chat it starts at the model's `default_mode` and follows the model when it changes; a pick shows at once, from a context above the open workspace, and is saved as that model's default, so a refresh or a restart keeps it ([`save-mode-pick.ts`](../../surfsense_local/frontend/src/features/chat/modes/save-mode-pick.ts)). When the save fails, the pick holds for the session and a toast says so. The new thread's `mode` is always sent. An Agentic that did not pass carries a tag ("Not tested", "Low score", "May need nudges", "Local copy", "Expected to pass"); the `reason`, or a gate, shows in a tooltip beside the Agentic row on hover or focus and is read with the row, in both menus: "Passed 2 of 8 Agentic tests, so it may stop early or make mistakes.", "Not tested with this model yet, so it may stop early or make mistakes.", "Passed on its full-size version. A local copy may do worse.", or that a flagship is assumed. Nothing is said under the composer. A gated Agentic is disabled with its reason, such as "This model can’t use tools." In an open chat, under "This chat’s mode", the switch lists the same two rows with the chat's mode checked; the other is marked "New chat", and picking it picks that mode and opens a new chat. A refused Agentic chat is worded by its code in a toast, and the question and images go back into the composer. A refusal by a gate the selection could not say, such as a local model's tool calls, disables Agentic with its reason for that model for the session ([`new-chat-mode.ts`](../../surfsense_local/frontend/src/features/chat/modes/new-chat-mode.ts)), so the next chat starts in Basic (Q&A) rather than asking again. The agent reads no attached image yet, so an Agentic chat, new or open, takes none ([chat](chat.md)). ## Known gaps @@ -70,3 +71,5 @@ The composer's switch ([`mode-picker.tsx`](../../surfsense_local/frontend/src/fe - No row is measured on this computer, so every local model is `not_measured`; llama.cpp runs need a `served: local` row. - The matrix runner writes per-run `result.json` files, not the ladder input; each column is mapped by hand, triage labels included. - The sweep's rows come from a committed input, [`capability_list/sweep/openrouter-screen-v1.json`](../../surfsense_local/backend/scripts/capability_list/sweep/openrouter-screen-v1.json): the screen of 7 and 8 Oct 2026 with the 18 re-run models' verdicts in place of their first ones. Its two runs are beside it under `sweep/runs/`, each a `sweep-results.json` and its `attempts.jsonl`; their run folders, about 840 MB of transcripts and files, stay out of the repository. `catalog_rows.py` cannot fold a re-run into a screen, so that input was put together by hand, and no test pins the sweep's rows to it as one pins the ladder's ([#2221](https://github.com/MODSetter/SurfSense/issues/2221)). The 11 models the sweep left unresolved have no row ([#2222](https://github.com/MODSetter/SurfSense/issues/2222)). +- A local model's tool calls and window are unknown until a chat starts, so Agentic stays selectable on one that cannot run it until its first Agentic chat is refused. A saved Agentic on such a model also survives a refresh or a restart until then. `GET /props?model=&autoload=0` could tell sooner: at llama.cpp b11050 it never loads a model and returned the facts once the model was loaded, adding 5 to 7 ms to a selection read on one 8 GB Mac, with or without replies generating. While the model is not loaded it answers `400`, `500` or `503`, so anything but `200` has to read as unknown. Not built; other hardware is not measured. +- A server the user connects themselves, such as Ollama or LM Studio, takes the remote path, so the catalog usually says nothing about its models and Agentic stays selectable. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 14ae231806..53d45d7fd3 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -110,6 +110,8 @@ The rules that keep the processes out of each other's way: - API calls go through `request()` in [`lib/api.ts`](../../surfsense_local/frontend/src/lib/api.ts), except the Studio viewers, which fetch an artifact's file bytes directly; an ``, `