Web Search

How live web search works, what it sends, and how retrieved text is contained.

Breeze can search the live web, and the sources come back cited. Search runs through Tavily, falling back to a keyless provider when Tavily is unavailable.

This is the only feature that leaves your machine

Everything else in Breeze talks to localhost. Only the search query the model wrote and the pages it reads cross the boundary -- never your transcript.

It is on by default

The Web search toggle in the chat settings popover starts on, and the composer remembers it along with your other switches.

Leaving it on is cheap, because the flag does not decide anything on its own. A short acquire pass decides per message whether the web is actually needed, and the answer model is picked from what that pass did -- so "hi" is still answered by the small local model. See model selection.

How a search turn works

A turn runs in two phases: acquire, then answer.

Acquire

A short, non-streaming call on the tool-capable model whose only job is to decide whether the web is needed, and to run the tools if it is. It never writes the answer, so nothing from this pass reaches you.

Tools

Tool calls run in parallel. Each returns Evidence -- a title, a URL and text -- rather than raw JSON.

Answer

One streaming call. The results arrive as a numbered evidence block inside the user turn, not as tool-role messages, and the model cites them as [1], [2].

Why the evidence block is the interesting part

Putting results in the user turn instead of a tool message is what lets any model answer from them -- including the generative-UI model, which cannot call tools at all. That single change is why search and widgets now compose.

The tools

ToolWhat it does
web_searchTavily, falling back to a keyless DuckDuckGo HTML search when Tavily errors or runs out of credits.
fetch_urlReads one page, when you name a URL or a result needs its full text.

Both are defined in backend/tools.py in OpenAI function-calling format.

When Tavily runs out

Credit exhaustion arrives as an ordinary exception from the SDK -- and so does a bad key, a network blip or a timeout. All of them take the same fallback, so a dry Tavily quota degrades your answer rather than breaking your turn. If the fallback also fails, the model answers from its own knowledge.

Retrieved text is untrusted

Anything fetched from the open web is attacker-controlled: a page can contain text written specifically to be read by a model. Breeze treats it as data, in layers, and every retrieved byte passes the same choke point (backend/evidence.py) regardless of which tool fetched it.

Configuration

Tavily is optional. Without a key, web_search falls straight through to the keyless fallback.

backend/.env
TAVILY_API_KEY=your-tavily-key

Tracing

With Langfuse on, the acquire pass runs under tools.acquire and the answer under chat.stream_responses, so the decision and the answer are separate spans in one trace. See Langfuse.