Repository navigation
feat(ai): freeform tools - #59963
feat(ai): freeform tools#59963
Conversation
There was a problem hiding this comment.
Looks sound to me. The new tests and the full packages/ai suite pass, and tsc is clean. A small script of my own showed a freeform tool going out as a custom tool (with a custom tool choice) on Azure Responses, as a normal JSON tool on Anthropic, and being rejected at build time when the parameters aren't exactly one required string. I had no credentials, so I couldn't test against a live model.
Public API (additions in @opencode/ai, backwards compatible): a Freeform schema and type export; an optional freeform option on AI.tool, Tool.make and ToolDefinition; a ToolDefinition.freeformField helper; a FreeformOption type. Tools without freeform are sent exactly as before. The Open Responses helpers whose signatures changed (lowerTools, lowerToolChoice, allowedToolChoice) aren't exported from the package.
No blocking concerns.
Adds OpenAI-style custom ("freeform") tools to
@opencode/ai. A tool with exactly one required string parameter can setfreeform, and the model writes that parameter as raw text instead of JSON args:Same on
Tool.make. The one-string-field rule is checked at the type level and again when the tool is built. Execution, tool input events, results and history all keep the normal{ code }object; freeform is only a wire encoding, carried onToolDefinition.freeform.Who encodes it: OpenAI Responses and Chat Completions, so everything that reuses them gets it with no per-provider code: Azure, Bedrock Mantle, Vercel, OpenRouter, Cloudflare AI Gateway, DigitalOcean, Venice, OpenAI-compatible Chat/Responses, plus the xAI/Meta/Alibaba Responses and the other Chat-based providers. Anthropic, Gemini, Bedrock Converse etc. have no freeform code and send the normal JSON tool.
No fallback: the caller decides. A tool with
freeformalways goes out as a custom tool on those two protocols; if the model doesn't support it you get the provider's error. Core will add or omitfreeformper model (from models.dev) in a later PR.Structure: one shared module both protocols use, and thin wire mapping in each:
{"code":"…, closed with"}) through the existing tool-stream code, so consumers see the same events as a JSON tool.fc_vsctc_);call_idstays.Live matrix (
ai.llm.loopwith an executing tool, all streamed over HTTP SSE;textused a multi-line input with quotes, backslashes, a tab and emoji, and the tool got it byte-for-byte):/openai/v1)Every ✅ means the custom call came back, the tool ran with the right input, and the model read the result. I didn't test Cloudflare Chat because the account's billing balance was too low.
Switching history between models also works: custom-call history replays to Claude as JSON
tool_use, back to OpenAI Responses as custom calls, and to the same tool withoutfreeformas a function call.Visual
Added by the opencode bot (AI-generated).
Public API: adds an optional
freeformoption toAI.tool/Tool.make/ToolDefinitionand aFreeformexport in@opencode/ai(backwards compatible)