feat: OpenAI-compatible function/tool calling#2
Open
haexhub wants to merge 6 commits into
Open
Conversation
added 6 commits
April 15, 2026 23:29
Translates standard OpenAI chat-completions tools in/out across the Claude
Code CLI boundary so clients like Hermes Agent and any other OpenAI-
compatible agent framework can use typed function calls through the proxy.
Encoder (src/adapter/openai-to-cli.ts):
* When request.tools is present, prepend a tool-use protocol description
to the prompt (tool names, JSON-Schema params, tool_choice semantics).
* Render assistant tool_calls and tool-role messages so Claude sees the
full round-trip context on multi-turn conversations.
* Expose TOOL_CALL_OPEN_TAG / TOOL_CALL_CLOSE_TAG for the decoder.
* openaiToCli() now returns hasTools so the caller knows to run the
extraction step on the response.
Decoder (src/adapter/cli-to-openai.ts):
* New extractToolCalls() scans response text for <tool_call>…</tool_call>
blocks, parses JSON, normalizes missing fields (generated id, empty
arguments), and returns OpenAI-format tool_calls plus clean text.
* cliResultToOpenai() / cliToOpenaiChunk() / createDoneChunk() now
optionally emit finish_reason="tool_calls" when tools were detected.
Types (src/types/openai.ts):
* Full OpenAI spec alignment: tool / tool_choice / tool_calls /
OpenAIToolMessage role, nullable assistant content, streaming deltas.
Server (src/server/routes.ts):
* Both streaming and non-streaming paths detect cliInput.hasTools and
route through the extractor. Streaming buffers content while tools
are active so tool_call JSON is only emitted once complete.
Tests (src/adapter/tool-calling.test.ts):
* 14 tests covering encoder, decoder, tool_choice variants, multi-turn,
malformed / unterminated blocks, missing name/args, id generation.
Claude was interpreting 'you have access to tools' literally (trying to
execute them) rather than as 'emit JSON requests'. Reword the prompt to:
- Explicitly separate emission from execution
- Forbid meta-commentary ("I'll call X")
- Fall back to plain text when no tool fits
- Avoid hallucinated tool names
nayrosk
referenced
this pull request
in nayrosk/claude-max-api-proxy
Apr 21, 2026
Apply upstream PR #2 on top of existing content-parts fix (PR joesobo#3). Encoder (openai-to-cli): - Inject Tool-Use Protocol into system prompt when request has tools. - Map tool_choice (auto/none/required/specific) to explicit policy text. - Render role=tool as <tool_result id="..."> and assistant tool_calls as <tool_call> blocks so multi-turn tool flows re-hydrate. - Preserve contentToString from PR joesobo#3 and apply it across all branches so array/multimodal content keeps working alongside the new flow. Decoder (cli-to-openai): - extractToolCalls scans Claude's text for <tool_call> blocks, returns OpenAI tool_calls plus cleaned text. Robust to missing id/args, malformed JSON, and unterminated blocks. - cliResultToOpenai / cliToOpenaiChunk accept extractTools flag and flip finish_reason to "tool_calls" when any call is parsed. Routes: pass hasTools through, buffer stream content when tools active (partial JSON unsafe to stream), emit parsed tool_calls in final chunk. Adds per-request timing logs + opt-in DEBUG=1 prompt dump. Types: full OpenAI v1 alignment — discriminated message union, tool / tool_choice / tool_call / delta, nullable assistant content, "tool_calls" finish reason. OpenAIContentPart kept and widened across system / user / assistant content. Tests: 14 new cases in src/adapter/tool-calling.test.ts cover encoder (hasTools, protocol injection, tool_choice variants, multi-turn) and decoder (single/multiple blocks, missing id/args, custom id, malformed, unterminated, plain-text passthrough). All pass.
4 tasks
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
Adds OpenAI-compatible function/tool calling across the proxy so agent
frameworks that speak the standard Chat Completions API (Hermes Agent,
Clawdbot in function-calling mode, LangChain OpenAI, etc.) can drive
Claude Code CLI with typed tools.
Previously `request.tools` was silently dropped and the CLI's stdout
was passed through as plain text. The LLM had no way to signal a tool
call, so agent frameworks got a confused text reply instead of a
structured `tool_calls` response.
How it works
No changes to the CLI invocation itself — Claude continues running in
`--print` mode. The protocol lives entirely in the adapter:
Request side (`openai-to-cli.ts`)
Protocol section to the prompt: tool definitions, JSON-Schema params,
and clear rules telling Claude to emit a single `<tool_call>…</tool_call>`
block with `{name, arguments}` when it wants to call a tool.
maps to explicit policy text.
blocks so multi-turn tool conversations re-hydrate correctly.
`<tool_call>` shape so Claude sees its own prior calls.
Response side (`cli-to-openai.ts`)
blocks, parses the inner JSON, and returns OpenAI-format `tool_calls`
plus clean text.
JSON is silently dropped so plain text still reaches the client.
Routes (`server/routes.ts`) now pass the `hasTools` flag through to
the decoder. Streaming buffers content until end-of-stream when tools
are active (partial JSON is ambiguous to stream incrementally).
Types alignment
`types/openai.ts` is extended to full OpenAI spec: `OpenAITool`,
`OpenAIToolChoice`, `OpenAIToolCall` (+ delta for streaming), nullable
assistant content, `tool` role message, and `"tool_calls"` as a finish
reason.
Tests
14 new tests in `src/adapter/tool-calling.test.ts` cover:
variants (`auto` / `none` / `required` / specific function),
multi-turn `tool` role rendering.
custom id, malformed JSON, missing name, unterminated block,
plain-text passthrough.
All pass with `pnpm build && pnpm test`.
Verified end-to-end
Tested with a Hermes Agent (NousResearch) instance talking to the proxy
as `custom:claude-proxy` with 46 registered tools including
`mcp_fwbg_*` tools via a FastMCP stdio server. Round-trip confirmed:
Hermes → proxy → Claude emits `<tool_call>` → decoder returns
`tool_calls` → Hermes executes MCP tool → result round-trips back →
Claude emits final text with the data.
Optional debug logging
`DEBUG=1` env var enables per-request logging: model, tool-count,
message-count, stream flag, tool names, and the full prompt text.
Useful when adding a new agent framework and diagnosing protocol
mismatches.
Backwards compatibility
Requests without `tools` take the original code path — only the
response types are relaxed to allow the new `tool_calls` field
(optional). No existing tests regressed.