Skip to content

feat: add tool/function calling support - #52

Merged
KochC merged 2 commits into
devfrom
feat/tool-support
Jul 4, 2026
Merged

feat: add tool/function calling support#52
KochC merged 2 commits into
devfrom
feat/tool-support

Conversation

@KochC

@KochC KochC commented Jul 4, 2026

Copy link
Copy Markdown
Owner

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

  1. 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, since OpenCode's server API (POST /mcp) has no endpoint to deregister an MCP server once added.
  2. Only those tools are enabled for that one prompt call via the existing tools enable/disable map ({[id]: boolean}); every built-in OpenCode tool stays disabled, same as before.
  3. 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 fully-parsed input even before execution starts. The proxy captures it and immediately calls session.abort() before the bridge's harmless no-op tools/call handler would ever be consulted.
  4. The captured call is translated into the caller's expected shape: tool_calls (OpenAI Chat Completions), a function_call output item (OpenAI Responses API), tool_use (Anthropic), or a functionCall part (Gemini).
  5. The message normalizers for all four formats now render prior tool calls/results (tool_calls/tool role, 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's tools/tool_choice/toolConfig into a canonical {name, description, parameters} list.
    • sanitizeToolName, a small bridge-slot pool (acquireBridgeSlot/releaseBridgeSlot), and registerToolBridge — dynamic MCP registration.
    • runAgentTurn — unifies the event-driven turn execution shared by executePrompt and executePromptStreaming when tools are present (both now accept an optional callerTools param; behavior is 100% unchanged when omitted/empty).
    • Tool-call branches added to all four response builders (createChatCompletionResponse, createResponsesApiResponse, createAnthropicResponse, createGeminiResponse) and their SSE/NDJSON streaming emitters.
    • Message normalizers (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/call is 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 new tools/tool_choice/toolConfig request fields, how the bridge mechanism works, current limitations, and the new OPENCODE_LLM_PROXY_TOOL_BRIDGE_POOL_SIZE env var.

Limitations (documented in README)

  • One tool call per turn — no parallel/simultaneous tool calls yet.
  • Bridge servers are reused from a fixed pool (default 8 slots, configurable) rather than registered fresh per request, since OpenCode has no MCP deregistration endpoint.
  • The bridge process is spawned with node, so node must be on PATH wherever OpenCode runs.

Testing

  • npm test — 138 passed (116 existing + 22 new)
  • npm run lint — clean
  • Manually smoke-tested mcp-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 client objects 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.

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
@KochC KochC mentioned this pull request Jul 4, 2026
…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.
@KochC
KochC merged commit d01cf20 into dev Jul 4, 2026
3 checks passed
@KochC
KochC deleted the feat/tool-support branch July 4, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant