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"}(+ optionalnamespace,caller,async). - Input
custom_tool_call_output:{"type":"custom_tool_call_output","call_id":"call_…","output":"<string>" | [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 |
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.