# Custom tools (`type: "custom"`) — free-form text and CFG grammars **Status:** DOCUMENTED · LIVE_VERIFIED (2026-09-18, `gpt-5.4-nano`, regex grammar and default text format; streaming events captured) **Sources:** https://developers.openai.com/api/docs/guides/function-calling#custom-tools · openapi-master.yaml `CustomToolParam`, `CustomGrammarFormatParam`, `CustomToolCall`, `CustomToolCallOutput`, `CustomToolChatCompletions`, `ToolChoiceCustom` **Last verified:** 2026-09-18 ## Definition | Field | Type | Req. | Notes | |---|---|---|---| | `type` | `"custom"` | yes | | | `name` | string | yes | | | `description` | string | no | explain the grammar/expected text here | | `format` | `{type:"text"}` (default) \| `{type:"grammar", syntax:"lark"\|"regex", definition:string}` | no | grammar constrains sampling (LLGuidance); terminals/regex use **Rust regex** syntax | | `async`, `defer_loading`, `allowed_callers` | | no | same semantics as functions | Chat Completions shape: `{"type":"custom","custom":{"name","description","format":{"type":"grammar","grammar":{"syntax","definition"}}}}` (note the extra `grammar` wrapper); forced choice `{"type":"custom","custom":{"name":…}}`. ## Items - Output `custom_tool_call`: `{"id":"ctc_…","type":"custom_tool_call","status":"completed","call_id":"call_…","name":"answer_yes_no","input":"yes"}` (+ optional `namespace`, `caller`, `async`). - Input `custom_tool_call_output`: `{"type":"custom_tool_call_output","call_id":"call_…","output":"" | [content parts]}`. ## Streaming (live) `response.output_item.added` → `response.custom_tool_call_input.delta` (`delta`, `item_id`, `output_index`, `obfuscation`) → `response.custom_tool_call_input.done` (`input`) → `response.output_item.done`. ## Grammar guidance (docs) - Lark subset: terminals UPPERCASE (lexer, greedy longest-match), rules lowercase; keep "free text between anchors" as one bounded regex terminal; avoid open-ended `%ignore`; bounded quantifiers (`{0,10}`) over `*`. - Regex: Rust regex crate syntax, no verbose mode, escape newlines as `\n`; some features unsupported. - Too-complex grammars → API error; iterate on grammar + prompt + description; out-of-distribution symptom = long repetitive but syntactically valid output. ## Live evidence | Probe | Model | Status | Result | |---|---|---|---| | regex `^(yes|no)$`, `tool_choice {type:custom,name}` | gpt-5.4-nano | 200 | `input: "yes"` | | text format, `tool_choice: "required"` | gpt-5.4-nano | 200 | `custom_tool_call` | | stream | gpt-5.4-nano | 200 | 7 events incl. `custom_tool_call_input.delta/done` | Examples: `examples/openai/tools/custom-tools/` (sh/py/ts). Test: `test_custom_tool_regex_grammar`.