Summary
web_search (Decopilot deep-research tool) uses the chat / thinking model's provider to call the deep-research model. When the two are configured on different providers — e.g. chat on OpenRouter and deep-research on Gemini — the call fails with 400 not a valid model ID because OpenRouter is asked to route a Gemini-only model id.
Reproduction
- Configure the "thinking" tier to a model on OpenRouter (e.g.
anthropic/claude-opus-4.7).
- Configure the "deep research" tier to a Gemini Deep Research model (e.g.
deep-research-max-preview-04-2026).
- Send a chat message in web-search mode.
Observed
The web_search tool errors instantly (~30–65 ms) with:
data: {"type":"tool-output-error", "errorText":"No output generated. Check the stream for errors."}
Server-side logs:
deep-research-max-preview-04-2026 is not a valid model ID
AI_APICallError (from @openrouter/ai-sdk-provider)
The model dispatches a recovery text response from its own training data instead.
Root cause
apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts:205-213 wires the tool with a single provider:
if (provider && models.deepResearch) {
tools.web_search = createWebSearchTool(writer, {
provider, // resolved for thinking model
deepResearchModelInfo: models.deepResearch, // might be a different provider's id
ctx, toolOutputMap, taskId,
});
}
Inside web-search.ts:67-69, 166:
const modelId = deepResearchModelInfo.id;
const asyncResearch = provider.asyncResearch;
const useAsyncResearch = asyncResearch?.canHandle(modelId) === true;
// ...
const model = provider.aiSdk.languageModel(deepResearchModelInfo.id);
provider is the chat-side provider, so:
provider.asyncResearch?.canHandle(geminiModelId) returns false (OpenRouter has no path for Gemini's Interactions API)
- Code falls into the synchronous branch →
provider.aiSdk.languageModel(geminiModelId) → OpenRouter POST → 400.
Fix sketch
The models.deepResearch info needs to carry its own provider (or a providerId the tool resolves), so createWebSearchTool is called with the deep-research model's actual provider instead of the chat-side one.
Concretely:
- Tier resolution that produces
models.deepResearch already knows which credential/provider that model came from — currently that info is dropped before reaching the tool.
- Plumb
deepResearchProvider through createWebSearchTool (alongside deepResearchModelInfo), and have the tool use it for both the asyncResearch.canHandle check and the aiSdk.languageModel(...) call.
- The existing
image tool path (createGenerateImageTool with imageModelInfo) likely has the same issue and should be audited.
Workarounds
- Set the deep-research tier to a model reachable through the chat model's provider (e.g.
perplexity/sonar-deep-research on OpenRouter), or
- Switch the chat tier to a Gemini model so both share the Gemini provider.
Out of scope
This is unrelated to #3353 (the JetStream / subscribe-model refactor) — the streaming layer surfaces the tool error correctly. This is a tool-registration / provider-routing issue.
Summary
web_search(Decopilot deep-research tool) uses the chat / thinking model's provider to call the deep-research model. When the two are configured on different providers — e.g. chat on OpenRouter and deep-research on Gemini — the call fails with400 not a valid model IDbecause OpenRouter is asked to route a Gemini-only model id.Reproduction
anthropic/claude-opus-4.7).deep-research-max-preview-04-2026).Observed
The
web_searchtool errors instantly (~30–65 ms) with:Server-side logs:
The model dispatches a recovery text response from its own training data instead.
Root cause
apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts:205-213wires the tool with a singleprovider:Inside
web-search.ts:67-69, 166:provideris the chat-side provider, so:provider.asyncResearch?.canHandle(geminiModelId)returnsfalse(OpenRouter has no path for Gemini's Interactions API)provider.aiSdk.languageModel(geminiModelId)→ OpenRouter POST → 400.Fix sketch
The
models.deepResearchinfo needs to carry its own provider (or aproviderIdthe tool resolves), socreateWebSearchToolis called with the deep-research model's actual provider instead of the chat-side one.Concretely:
models.deepResearchalready knows which credential/provider that model came from — currently that info is dropped before reaching the tool.deepResearchProviderthroughcreateWebSearchTool(alongsidedeepResearchModelInfo), and have the tool use it for both theasyncResearch.canHandlecheck and theaiSdk.languageModel(...)call.imagetool path (createGenerateImageToolwithimageModelInfo) likely has the same issue and should be audited.Workarounds
perplexity/sonar-deep-researchon OpenRouter), orOut of scope
This is unrelated to #3353 (the JetStream / subscribe-model refactor) — the streaming layer surfaces the tool error correctly. This is a tool-registration / provider-routing issue.