feat: OpenAI tool calling + array/object content fix#1
Merged
Conversation
OpenAI Chat Completions `content` can be either a string or a content-parts
array (e.g. `[{ type: "text", text: "..." }, { type: "image_url", ... }]`).
`messagesToPrompt` previously interpolated `msg.content` directly into template
literals, so the array form stringified via `Array#toString` -> each element's
default `Object#toString` -> `"[object Object]"` separated by commas.
Downstream clients that send structured content (e.g. OpenClaw's inbound
message pipeline) end up feeding Claude CLI prompts like
`[object Object],[object Object]`, and the model naturally responds about
"receiving [object Object] messages".
Introduce a `contentToString` helper and thread it through `messagesToPrompt`:
- strings pass through unchanged
- array parts: extract `text` for text parts, `"[image]"` placeholder for
`image_url` parts (Claude CLI --print mode is text-only), JSON.stringify
for unknown shapes so no info is silently dropped
- null/undefined -> empty string (and filtered out before join)
- bare objects -> JSON.stringify as a defensive fallback
Also widen `OpenAIChatMessage.content` to the union type permitted by the
OpenAI spec, so TypeScript consumers stop having to narrow manually.
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.
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
Ports two upstream PRs from
joesobo/claude-max-api-proxyonto this fork, merged so both fixes coexist.messagesToPrompt(fixes the[object Object]bug when clients send OpenAI multimodal-stylecontent: [{type:"text",...}]).Encoder (
src/adapter/openai-to-cli.ts)tools: tool list, JSON-Schema params, emission rules,tool_choicepolicy.tool_choice→ auto / none / required / specific function mapped to explicit policy text.tool_callsreplayed as<tool_call>…</tool_call>; role=toolrendered as<tool_result id="…">…</tool_result>for multi-turn re-hydration.contentToStringfrom PR#3 retained and now called on every role branch (system/user/assistant) so array/multimodal content keeps working alongside the tool flow.Decoder (
src/adapter/cli-to-openai.ts)extractToolCallsscans Claude's text for<tool_call>blocks, parses inner JSON, returns OpenAItool_calls+ cleaned text. Robust to missingid/arguments, malformed JSON, and unterminated blocks.cliResultToOpenai/cliToOpenaiChunkacceptextractToolsflag;finish_reasonflips to"tool_calls"when any call is parsed.Routes (
src/server/routes.ts)hasToolsthrough to the decoder.tool_callsin the final chunk.[Timing rid=…]) + opt-inDEBUG=1prompt dump for diagnosing new agent frameworks.Types (
src/types/openai.ts)Full alignment with OpenAI Chat Completions v1 spec:
OpenAIChatMessageunion (system | user | assistant | tool).OpenAITool,OpenAIToolChoice,OpenAIToolCall,OpenAIToolCallDelta,OpenAIFinishReason(incl."tool_calls").content,OpenAIToolMessage.tool_call_id.OpenAIContentPartretained from PR#3; system/user/assistantcontentwidened tostring | OpenAIContentPart[](assistant also| null).Tests
14 new cases in
src/adapter/tool-calling.test.ts:hasToolsflag, protocol injection,tool_choicevariants (auto/none/required/specific), multi-turn tool role rendering.Test plan
npm install && npm run build— clean, no type errors.npm test— 14/14 pass.contentToString).Notes
package-lock.jsonpicked up a name re-sync (upstream rename fromclaude-code-cli-provider→claude-max-api-proxy) duringnpm install.