Cherry Studio AI Core (2): Tool-Call Reliability and Provider Options
5 Jan 2026
5 min read
This is part 2 of the series. I walk through the tool-call reliability work I led and the provider config surface I had to stabilize to make tool usage observable and sane.
Key PRs I authored in this part:
- #10028 (tool-call refactor and MCP approval flow)
- #10068 (provider options + built-in tools)
- #10269 (includeUsage tracking)
1. Tool-call state flow and MCP approvals
Tool invocation used to be a weak link: prompt-to-tool mapping, approval confirmation, and error states were inconsistent across providers and across the renderer. In PR #10028 I refactored the flow into a single state machine (pending → invoking → done/error) and made MCP approval confirmations first-class.
The key shift is ownership. The runtime owns the flow, and the renderer simply consumes canonical events.
Tool state is derived from AI SDK stream events. The adapter converts those events into chunk types, and the handler maps them into tool responses:
case 'tool-call':
this.toolCallHandler.handleToolCall(chunk)
break
case 'tool-error':
this.toolCallHandler.handleToolError(chunk)
break
case 'tool-result':
this.toolCallHandler.handleToolResult(chunk)
breakFile: src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts
This matters because the renderer no longer guesses the state; it receives canonical tool events from the stream.
From there, ToolCallChunkHandler decides how to classify each tool call (provider-executed, builtin, or MCP) and emits a pending response that the UI can render consistently.
2. How tool approval actually works
Approval is handled as part of the tool lifecycle, not as a UI-only decision. The flow is:
- AI SDK emits a
tool-callevent. AiSdkToChunkAdapterconverts it intoChunkType.MCP_TOOL_PENDING.ToolCallChunkHandlerbuilds a pending tool response and pushes it downstream.- The renderer creates a tool block in pending state and binds it to tool permissions.
- Once the tool completes (or is rejected/cancelled), the block is finalized and the permission entry is cleared.
You can see the permission cleanup in the tool callbacks:
if (toolResponse?.id) {
dispatch(toolPermissionsActions.removeByToolCallId({ toolCallId: toolResponse.id }))
}File: src/renderer/src/services/messageStreaming/callbacks/toolCallbacks.ts
Here is the approval flow as a diagram:
flowchart TD A[AI SDK tool call event] --> B[AiSdkToChunkAdapter] B --> C[ChunkType MCP_TOOL_PENDING] C --> D[ToolCallChunkHandler builds pending response] D --> E[Renderer creates tool block] E --> F[Tool permission approval] F --> G[Tool executes] G --> H[Tool result or error] H --> I[Finalize block and clear permission]
3. Prompt-based tool calling (and how it fits the architecture)
Some providers do not support native function calling. For those, I implemented a prompt-based tool flow inside the plugin system. The idea is simple: inject a system prompt that teaches the model an XML tool-call format, then parse the model output and execute tools through the same runtime pipeline.
I chose the plugin path for two reasons:
- It lets prompt tools reuse the same lifecycle (transformParams → transformStream → tool execution → recursive call) without special cases in runtime.
- It keeps the “prompt tooling” logic interchangeable with native tool calling in the rest of the system.
The plugin builds a system prompt with tool definitions and examples:
return DEFAULT_SYSTEM_PROMPT
.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES)
.replace('{{ AVAILABLE_TOOLS }}', availableTools)
.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '')File: packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts
Once the model emits <tool_use> blocks, the plugin parses them into structured tool calls and executes them inside transformStream. It then simulates the AI SDK event flow by emitting tool-call/tool-result events, sending a step-finish with tool-calls, and running a recursive call with tool results.
That recursive call is the key integration point: prompt-based tools still go through the same runtime path, and the rest of the system only sees standard tool events and chunk types. In practice, this means the UI does not need to care whether a tool call was native or prompt-based.
This is why the implementation lives in plugins/: it keeps runtime clean and makes tool-call behavior portable across providers.
Here is the prompt-tool execution flow I use to reason about it:
flowchart TD A[Model outputs tool_use tags] --> B[promptToolUsePlugin] B --> C[TagExtractor parses tool_use] C --> D[ToolExecutor executeTools] D --> E[Emit tool call and tool result] E --> F[finish-step tool-calls] F --> G[buildRecursiveParams] G --> H[recursiveCall] H --> I[PluginEngine executes again] I --> J[AiSdkToChunkAdapter] J --> K[UI blocks update] H -.-> B
4. Provider options (AI SDK level, brief)
Provider options are mainly AI SDK provider options. In this series they are not the core story, so I only keep them as a brief note: the goal is to avoid inventing a parallel option system and keep execution aligned with the SDK.
5. Built-in tools (plugin-level injection)
My mental model is: built-in tools are abstracted as plugins so they can be added on demand and managed in one place. This matters because not every provider supports the same built-in tools. Web search is the best example — it is a provider-defined capability, not a universal tool.
So the injection happens at the plugin layer (via PluginBuilder + middleware config), not via provider options. That way, tool availability is scoped to the runtime pipeline, and only providers that actually support a built-in capability will see it.
The net effect is that tool calls are still uniform at the UI level, while capability differences are handled upstream in the plugin layer.
Takeaways
- Tool calls became explicit stream events with stable state transitions.
- Tool approval is part of the lifecycle, not a UI-only add-on.
- Provider options and built-in tool support moved into the aiCore pipeline.
Next up: response normalization and separating provider-defined tools from prompt tools.