feat: add tool/function calling support - #52
Merged
Merged
Conversation
Adds OpenAI-style function tools, Anthropic tools, and Gemini function declarations across all four API surfaces (/v1/chat/completions, /v1/responses, /v1/messages, /v1beta/models/:model:generateContent), both streaming and non-streaming. OpenCode's own agent loop always executes tools itself, server-side, and has no concept of a 'client-executed' tool call to hand off to a caller. To bridge that gap: - When a request includes tools, the proxy dynamically registers a small local MCP server (mcp-tool-bridge.js) whose tool list is exactly the caller's declared tool schemas, reused from a small fixed-size pool of slot names (OpenCode's server API has no endpoint to deregister an MCP server once added). - Only those tools are enabled for that one prompt call via the existing tools enable/disable map; every built-in OpenCode tool stays disabled, same as before. - As soon as the model proposes calling one of the bridge tools, the full call (name + arguments) is already present on OpenCode's event stream (ToolStatePending includes the parsed input even before execution starts) - the proxy captures it and immediately aborts the session before the bridge's no-op tools/call handler would ever be consulted, then translates the call into the caller's expected tool_calls / tool_use / functionCall shape instead of a text answer. Also extends the OpenAI/Anthropic/Gemini/Responses message normalizers to render prior tool_calls/tool_use/functionCall and their results/tool_result/functionResponse as descriptive text when replaying conversation history, so multi-turn tool use works end-to-end even though sessions are stateless per-request. - index.js: parseOpenAITools/parseAnthropicTools/parseGeminiTools + applyOpenAIToolChoice/applyAnthropicToolChoice/applyGeminiToolChoice, sanitizeToolName, tool bridge pool + registerToolBridge, unified runAgentTurn (event-driven turn execution shared by executePrompt and executePromptStreaming when tools are present), tool-call branches in all four response builders and SSE emitters. - mcp-tool-bridge.js: minimal MCP stdio JSON-RPC server exposing caller-supplied tool schemas; tools/call is a harmless no-op since the proxy aborts the session before it would ever be consulted. - index.test.js: unit tests for the new parse/tool_choice helpers and history round-tripping, plus end-to-end tool-calling tests for all four API formats (stream + non-stream). - README.md: documents the new tools/tool_choice/toolConfig request fields, how the bridge mechanism works, its current limitations, and the new OPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZE env var. Testing: - npm test — 138 passed (116 existing + 22 new) - npm run lint — clean
Closed
…ity keywords - Move the Tool calling section up (right after Configuration) and add a runnable curl request/response example, instead of burying it near the bottom after How it works. - Add a Contents section now that the README has grown to 10+ sections. - Call out tool calling in the top-level tagline, architecture diagram, supported-formats table, and Why section (coding agents are now a first-class use case, not just chat clients). - Note in Install that copying just index.js doesn't get you tool calling (needs mcp-tool-bridge.js alongside it) - use the npm plugin instead. - package.json: mention tool/function calling in the description and add tool-calling/function-calling/tools/mcp/model-context-protocol/ coding-agent/ai-agent/agentic keywords for npm search discoverability.
This was referenced Jul 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #50. Adds OpenAI-style function tools, Anthropic tools, and Gemini function declarations across all four API surfaces (
/v1/chat/completions,/v1/responses,/v1/messages,/v1beta/models/:model:generateContent), both streaming and non-streaming.The problem
OpenCode's own agent loop always executes tools itself, server-side, and has no concept of a "client-executed" tool call — the exact thing coding-agent clients need (they run their own file/shell/etc. tools locally and expect the model to propose a call, then wait for an externally-supplied result).
How it works
tools, the proxy dynamically registers a small local MCP server (mcp-tool-bridge.js) whose tool list is exactly the caller's declared tool schemas — reused from a small fixed-size pool of slot names, since OpenCode's server API (POST /mcp) has no endpoint to deregister an MCP server once added.{[id]: boolean}); every built-in OpenCode tool stays disabled, same as before.ToolStatePendingincludes the fully-parsedinputeven before execution starts. The proxy captures it and immediately callssession.abort()before the bridge's harmless no-optools/callhandler would ever be consulted.tool_calls(OpenAI Chat Completions), afunction_calloutput item (OpenAI Responses API),tool_use(Anthropic), or afunctionCallpart (Gemini).tool_calls/toolrole,tool_use/tool_result,function_call/function_call_output,functionCall/functionResponse) as descriptive text when replaying conversation history, so multi-turn tool use works even though sessions are stateless per-request (this proxy always replays full history into a fresh session).Changes
index.js:parseOpenAITools/parseAnthropicTools/parseGeminiTools+applyOpenAIToolChoice/applyAnthropicToolChoice/applyGeminiToolChoice— parse each API'stools/tool_choice/toolConfiginto a canonical{name, description, parameters}list.sanitizeToolName, a small bridge-slot pool (acquireBridgeSlot/releaseBridgeSlot), andregisterToolBridge— dynamic MCP registration.runAgentTurn— unifies the event-driven turn execution shared byexecutePromptandexecutePromptStreamingwhen tools are present (both now accept an optionalcallerToolsparam; behavior is 100% unchanged when omitted/empty).createChatCompletionResponse,createResponsesApiResponse,createAnthropicResponse,createGeminiResponse) and their SSE/NDJSON streaming emitters.normalizeMessages,normalizeResponseInput,normalizeAnthropicMessages,normalizeGeminiContents) extended to round-trip tool calls/results in history.mcp-tool-bridge.js(new): minimal MCP stdio JSON-RPC server exposing caller-supplied tool schemas.tools/callis a harmless no-op stub since the proxy always aborts the session before it would be consulted.index.test.js: unit tests for the new parsing/tool_choice helpers and history round-tripping, plus end-to-end tool-calling tests (stream + non-stream) for all four API formats.README.md: documents the newtools/tool_choice/toolConfigrequest fields, how the bridge mechanism works, current limitations, and the newOPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZEenv var.Limitations (documented in README)
node, sonodemust be onPATHwherever OpenCode runs.Testing
npm test— 138 passed (116 existing + 22 new)npm run lint— cleanmcp-tool-bridge.js's JSON-RPC handling (initialize,tools/list,tools/call) by piping requests to it directly.Notes
I don't have a live OpenCode instance handy to run a full end-to-end integration test against the real server, so tests use mock
clientobjects matching the exact shapes documented in OpenCode's server API reference and its generated SDK client (session.abort,mcp.add,mcp.disconnect,ToolPart/ToolStatePending). Would appreciate a maintainer or anyone with a live setup giving this a try against a real OpenCode server before merging.