feat(websearch): add toggle to disable prefilling search keywords in …#14135
feat(websearch): add toggle to disable prefilling search keywords in …#14135karuboniru wants to merge 5 commits intoCherryHQ:v2from
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a user-facing setting to control whether Cherry Studio injects pre-extracted web-search queries into the builtin_web_search tool description, allowing models to decide queries independently when desired.
Changes:
- Added
prefillKeywordsto the persistedwebsearchRedux slice (defaulttrue) and exposed an action to update it. - Added a new “Prefill search keywords” toggle in Settings → Web Search → General with localized label/tooltip.
- Updated
builtin_web_searchtool description/schema text and the search orchestration plugin to respect the toggle.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/src/store/websearch.ts | Introduces persisted prefillKeywords state and setPrefillKeywords action. |
| src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx | Adds the new toggle UI and wires it to Redux. |
| src/renderer/src/i18n/locales/en-us.json | Adds English strings for the toggle label/tooltip. |
| src/renderer/src/i18n/locales/zh-cn.json | Adds Simplified Chinese strings for the toggle label/tooltip. |
| src/renderer/src/i18n/locales/zh-tw.json | Adds Traditional Chinese strings for the toggle label/tooltip. |
| src/renderer/src/aiCore/tools/WebSearchTool.ts | Makes the tool description/schema conditional on prefillKeywords. |
| src/renderer/src/aiCore/plugins/searchOrchestrationPlugin.ts | Passes prefillKeywords from Redux into the web search tool factory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d5ce867 to
0309b82
Compare
…tool description When an agent uses web search, Cherry Studio pre-extracts keywords and injects them into the builtin_web_search tool description. This commit adds a setting in Settings -> Web Search -> General to toggle this behavior. When disabled, the model receives a generic tool description and must independently decide what search query to use, which can be useful for models that perform better without pre-suggested queries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Karuboniru <yanqiyu01@gmail.com>
0309b82 to
5b3bdcf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx
Outdated
Show resolved
Hide resolved
…refill is disabled When the "Prefill search keywords" toggle is off the goal is to let the model decide on its own whether to search and what to search for. The previous implementation still ran the intent-analysis LLM call and silently fell back to the pre-extracted keywords inside execute() if the model supplied no additionalContext -- partially defeating the purpose. Changes: - onRequestStart: skip the web-search extraction call entirely when prefillKeywords is false (knowledge-base extraction is unaffected). - transformParams: when prefillKeywords is false, always register the builtin_web_search tool unconditionally so the model can reach for it; when true, keep the existing intent-gated behaviour. - WebSearchTool execute: when prefillKeywords is false and the model provides no additionalContext, treat the call as not_needed instead of falling back to the pre-extracted queries. - BasicSettings: prefix void on the setPrefillKeywords call to satisfy the no-floating-promises lint rule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Karuboniru <yanqiyu01@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Move preference key into generator sources: add chat.web_search.prefill_keywords to target-key-definitions.json and classification.json, then regenerate preferenceSchemas.ts and migration mappings so the key is durable. - Cache prefillKeywords per-request: read the preference once in onRequestStart, store in prefillKeywordsPerRequest map, reuse in transformParams and clean up in onRequestEnd -- both phases are now guaranteed to see the same value. - Validate non-empty queries before adding the web search tool in prefill mode: check that at least one query trims to a non-empty string in addition to the existing not_needed guard. - Clearer signal for missing query in model-driven mode: throw a descriptive Error when the model calls the tool without additionalContext so the AI SDK surfaces it as a tool error and the model can retry with an explicit query, instead of silently returning an empty no-search-needed result. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Karuboniru <yanqiyu01@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…cache leak - WebSearchTool: throw a clear error when getWebSearchProvider() returns undefined instead of using a non-null assertion, so a mis-configured or not-yet-loaded provider id surfaces with an actionable message rather than a cryptic crash. - WebSearchTool (model-driven mode): additionalContext is declared optional in the schema; calling the tool without a query now returns empty results (no search) consistent with the schema contract and PR description, instead of throwing. The provider guard above still throws for a genuinely broken provider, keeping the two failure modes distinct. - searchOrchestrationPlugin: move per-request cache cleanup (intentAnalysisResults, userMessages, prefillKeywordsPerRequest) from the try block into a finally block so the caches are always cleared even when storeConversationMemory throws. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Karuboniru <yanqiyu01@gmail.com>
In model-driven mode (prefillKeywords=false) the tool has no pre-extracted queries to fall back on, so a missing additionalContext is always an error. - Split inputSchema into two separate Zod objects: optional additionalContext in prefill mode (existing behaviour), required additionalContext in model-driven mode. The AI SDK now validates the presence of the query before execute is called, rather than relying solely on a runtime guard. - Keep a defensive throw inside execute for the model-driven branch so any blank/whitespace-only string (which passes Zod .string()) is still rejected with a clear message asking the model to retry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Karuboniru <yanqiyu01@gmail.com>
d691fcb to
39f5b8d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // In model-driven mode additionalContext is the sole source of the query — make it required | ||
| // so the AI SDK validates it before execute is called. In prefill mode it remains optional | ||
| // because the tool can fall back to the pre-extracted keywords. | ||
| inputSchema: prefillKeywords | ||
| ? z.object({ | ||
| additionalContext: z | ||
| .string() | ||
| .optional() | ||
| .describe('Optional additional context, keywords, or specific focus to enhance the search') | ||
| }) | ||
| : z.object({ | ||
| additionalContext: z.string().describe('Required: the search query or keywords to search the web') | ||
| }), |
There was a problem hiding this comment.
PR description says the schema keeps additionalContext optional in both modes and that when prefill is disabled a missing query is treated as “not needed”. In this implementation, additionalContext becomes required when prefillKeywords is false and an empty/blank value throws an error. Please either update the PR description to match, or adjust the schema/execute behavior to keep additionalContext optional and return an empty result when it’s absent (as described).
| if (!prefillKeywords) { | ||
| // Schema requires additionalContext in this mode; this guard is purely defensive. | ||
| const query = additionalContext?.trim() | ||
| if (!query) { | ||
| throw new Error('No search query provided. Please retry with a non-empty search query in additionalContext.') | ||
| } | ||
| const extractResults: ExtractResults = { websearch: { question: [query] } } | ||
| return webSearchService.processWebsearch(webSearchProvider, extractResults, requestId) | ||
| } |
There was a problem hiding this comment.
In model-driven mode (prefillKeywords === false), if the model passes the sentinel value "not_needed" as additionalContext, this will currently run a real web search for the literal string not_needed. Consider handling this the same way as prefill mode (returning an empty result) to avoid unnecessary requests and confusing outputs.
| if (!prefillKeywords) { | ||
| // Schema requires additionalContext in this mode; this guard is purely defensive. | ||
| const query = additionalContext?.trim() | ||
| if (!query) { | ||
| throw new Error('No search query provided. Please retry with a non-empty search query in additionalContext.') | ||
| } | ||
| const extractResults: ExtractResults = { websearch: { question: [query] } } | ||
| return webSearchService.processWebsearch(webSearchProvider, extractResults, requestId) |
There was a problem hiding this comment.
With prefillKeywords disabled, web intent analysis (including URL extraction) is skipped and the tool input only accepts a single additionalContext string. That makes the processWebsearch summarize-path (question[0] === 'summarize' + links[]) effectively unreachable in this mode, even if the user asks to summarize specific URLs. Consider extending the tool input to accept optional links (or parsing URLs from additionalContext) so summarization still works without the pre-extraction phase.
| // When prefillKeywords is disabled the model decides on its own when and what to search, | ||
| // so there is no point running the pre-extraction LLM call for web search. | ||
| // Read once and cache so transformParams sees the same value for this request. | ||
| const prefillKeywords = await preferenceService.get('chat.web_search.prefill_keywords') | ||
| prefillKeywordsPerRequest[context.requestId] = prefillKeywords | ||
| const shouldWebExtract = shouldWebSearch && prefillKeywords | ||
|
|
There was a problem hiding this comment.
prefillKeywords is fetched and cached even when shouldWebSearch is false (e.g., assistants with only knowledge-base search enabled). Since preferenceService.get can trigger an IPC call on a cold cache, consider only reading this preference when shouldWebSearch is true, and defaulting prefillKeywordsPerRequest[requestId] to true otherwise.
…tool description
When an agent uses web search, Cherry Studio pre-extracts keywords and injects them into the builtin_web_search tool description. This commit adds a setting in Settings -> Web Search -> General to toggle this behavior.
When disabled, the model receives a generic tool description and must independently decide what search query to use, which can be useful for models that perform better without pre-suggested queries.
What this PR does
Before this PR: When an agent with web search enabled processes a user query, Cherry Studio pre-analyzes the message, extracts search keywords, and injects them directly into the
builtin_web_searchtool description — e.g.:The model sees these prefilled queries and tends to use them as-is.
After this PR: A new toggle — Settings → Web Search → General → "Prefill search keywords" — lets users disable this behavior. When turned off:
builtin_web_searchtool is exposed unconditionally so the model can always reach for it.Fixes #
Why we need it and why it was done in this way
Modern LLMs are generally capable enough to decide on their own when to search and what to search for. Pre-extracting keywords with a separate LLM call adds extra token usage and latency before the main request even starts. Users who want to control searching behavior (e.g. force or suppress searches, steer the query style) can already do so more naturally through the system prompt. The prefill behavior remains the default for users who prefer it, but this toggle lets others opt out.
The following tradeoffs were made:
true(existing behavior preserved — no regression for current users).additionalContext, the search is treated as not needed (no silent fallback to pre-extracted queries). The tool description instructs the model to always supply its own query.additionalContextoptional in both modes; the description text guides the model's behavior rather than enforcing it at the schema level.The following alternatives were considered:
Breaking changes
None. The default value is
true, preserving existing behavior for all users.Special notes for your reviewer
Unlike the other settings in the
websearchRedux slice (which have not yet been migrated), this setting is stored directly inPreferenceService— the v2 SQLite-backed preference system — rather than Redux. The preference keychat.web_search.prefill_keywords(defaulttrue) is declared inpackages/shared/data/preference/preferenceSchemas.ts.In the renderer the toggle reads and writes via the
usePreference('chat.web_search.prefill_keywords')hook (no Redux dispatch). In the plugin layer,searchOrchestrationPlugin.tsreads it asynchronously viapreferenceService.get('chat.web_search.prefill_keywords'). The Reduxwebsearchslice (src/renderer/src/store/websearch.ts) is untouched.This means no data migration will be needed when the websearch slice is eventually migrated — the preference is already in SQLite.
Checklist
Release note