Custom tools
Custom tools work in much the same way as JSON schema-driven function tools. But rather than providing the model explicit instructions on what input your tool requires, the model can pass an arbitrary string back to your tool as input. This is useful to avoid unnecessarily wrapping a response in JSON, or to apply a custom grammar to the response (more on this below).
The following code sample shows creating a custom tool that expects to receive a string of text containing Python code as a response.
Custom tool calling example
import OpenAI from "openai";
const client = new OpenAI();
const response = await client.responses.create({
model: "gpt-6-astra",
input: "Use the code_exec tool to print hello world to the console.",
tools: [
{
type: "custom",
name: "code_exec",
description: "Executes arbitrary Python code.",
},
],
});
console.log(response.output);from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-astra",
input="Use the code_exec tool to print hello world to the console.",
tools=[
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code.",
}
],
)
print(response.output)package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
tool := responses.ToolParamOfCustom("code_exec")
tool.OfCustom.Description = openai.String("Executes arbitrary Python code.")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the code_exec tool to print hello world to the console.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use code_exec to print hello world.")
.addTool(
CustomTool.builder()
.name("code_exec")
.description("Executes arbitrary Python code.")
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);require "openai"
client = OpenAI::Client.new
response = client.responses.create(
model: "gpt-6-astra",
input: "Use code_exec to print hello world.",
tools: [{
type: :custom,
name: "code_exec",
description: "Executes arbitrary Python code."
}]
)
puts(response.output)Just as before, the output array will contain a tool call generated by the model. Except this time, the tool call input is given as plain text.
[
{
"id": "rs_6890e972fa7c819ca8bc561526b989170694874912ae0ea6",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6890e975e86c819c9338825b3e1994810694874912ae0ea6",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_aGiFQkRWSWAIsMQ19fKqxUgb",
"input": "print(\"hello world\")",
"name": "code_exec"
}
]Context-free grammars
A context-free grammar (CFG) is a set of rules that define how to produce valid text in a given format. For custom tools, you can provide a CFG that will constrain the model's text input for a custom tool.
You can provide a custom CFG using the grammar parameter when configuring a custom tool. Currently, we support two forms of CFG syntax when defining grammars: lark and regex.
Lark CFG
Lark context free grammar example
import OpenAI from "openai";
const client = new OpenAI();
const grammar = `
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
`;
const response = await client.responses.create({
model: "gpt-6-astra",
input: "Use the math_exp tool to add four plus four.",
tools: [
{
type: "custom",
name: "math_exp",
description: "Creates valid mathematical expressions",
format: {
type: "grammar",
syntax: "lark",
definition: grammar,
},
},
],
});
console.log(response.output);from openai import OpenAI
client = OpenAI()
grammar = """
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
"""
response = client.responses.create(
model="gpt-6-astra",
input="Use the math_exp tool to add four plus four.",
tools=[
{
"type": "custom",
"name": "math_exp",
"description": "Creates valid mathematical expressions",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": grammar,
},
}
],
)
print(response.output)package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared"
)
func main() {
client := openai.NewClient()
grammar := `start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT`
tool := responses.ToolParamOfCustom("math_exp")
tool.OfCustom.Description = openai.String("Creates valid mathematical expressions")
tool.OfCustom.Format = shared.CustomToolInputFormatParamOfGrammar(grammar, "lark")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the math_exp tool to add four plus four.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.CustomToolInputFormat;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
String grammar =
"""
start: expr
expr: term (SP ADD SP term)*
term: INT
SP: " "
ADD: "+"
%import common.INT
""";
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use math_exp to add four plus four.")
.addTool(
CustomTool.builder()
.name("math_exp")
.description("Creates valid mathematical expressions.")
.format(
CustomToolInputFormat.Grammar.builder()
.syntax(CustomToolInputFormat.Grammar.Syntax.LARK)
.definition(grammar)
.build())
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);require "openai"
client = OpenAI::Client.new
grammar = <<~LARK
start: expr
expr: term (SP ADD SP term)*
term: INT
SP: " "
ADD: "+"
%import common.INT
LARK
response = client.responses.create(
model: "gpt-6-astra",
input: "Use math_exp to add four plus four.",
tools: [{
type: :custom,
name: "math_exp",
description: "Creates valid mathematical expressions.",
format: {type: :grammar, syntax: :lark, definition: grammar}
}]
)
puts(response.output)The output from the tool should then conform to the Lark CFG that you defined:
[
{
"id": "rs_6890ed2b6374819dbbff5353e6664ef103f4db9848be4829",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6890ed2f32e8819daa62bef772b8c15503f4db9848be4829",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_pmlLjmvG33KJdyVdC4MVdk5N",
"input": "4 + 4",
"name": "math_exp"
}
]Grammars are specified using a variation of Lark. Model sampling is constrained using LLGuidance. Some features of Lark are not supported:
- Lookarounds in lexer regexes
- Lazy modifiers (
*?,+?,??) in lexer regexes - Priorities of terminals
- Templates
- Imports (other than built-in
%importcommon) %declares
We recommend using the Lark IDE to experiment with custom grammars.
Limit grammar complexity
Limit your grammar to the rules and patterns your tool needs. The OpenAI API may return an error if the grammar is too complex, so you should ensure that your desired grammar is compatible before using it in the API.
Lark grammars can be tricky to perfect. While less complex grammars perform most reliably, complex grammars often require iteration on the grammar definition itself, the prompt, and the tool description to ensure that the model does not go out of distribution.
Correct versus incorrect patterns
Correct (single, bounded terminal):
start: SENTENCE
SENTENCE: /[A-Za-z, ]*(the hero|a dragon|an old man|the princess)[A-Za-z, ]*(fought|saved|found|lost)[A-Za-z, ]*(a treasure|the kingdom|a secret|his way)[A-Za-z, ]*\./Do NOT do this (splitting across rules/terminals). This attempts to let rules partition free text between terminals. The lexer will greedily match the free-text pieces and you'll lose control:
start: sentence
sentence: /[A-Za-z, ]+/ subject /[A-Za-z, ]+/ verb /[A-Za-z, ]+/ object /[A-Za-z, ]+/Lowercase rules don't influence how terminals are cut from the input—only terminal definitions do. When you need “free text between anchors,” make it one giant regex terminal so the lexer matches it exactly once with the structure you intend.
Terminals versus rules
Lark uses terminals for lexer tokens (by convention, UPPERCASE) and rules for parser productions (by convention, lowercase). The most practical way to stay within the supported subset and avoid surprises is to keep your grammar explicit and avoid unnecessary complexity, and to use terminals and rules with a clear separation of concerns.
The regex syntax used by terminals is the Rust regex crate syntax, not Python's re module.
Key ideas and best practices
Lexer runs before the parser
Terminals are matched by the lexer (greedily / longest match wins) before any CFG rule logic is applied. If you try to "shape" a terminal by splitting it across several rules, the lexer cannot be guided by those rules—only by terminal regexes.
Prefer one terminal when you're carving text out of freeform spans
If you need to recognize a pattern embedded in arbitrary text (for example, natural language with “anything” between anchors), express that as a single terminal. Do not try to interleave free‑text terminals with parser rules; the greedy lexer will not respect your intended boundaries and it is highly likely the model will go out of distribution.
Use rules to compose discrete tokens
Rules are ideal when you're combining explicitly delimited terminals (numbers, keywords, punctuation) into larger structures. They're not the right tool for constraining "the stuff in between" two terminals.
Keep terminals focused, bounded, and self-contained
Favor explicit character classes and bounded quantifiers (\{0,10\}, not unbounded * everywhere). If you need "any text up to a period," prefer something like /[^.\n]\{0,10\}*\./ rather than /.+\./ to avoid runaway growth.
Use rules to combine tokens, not to steer regex internals
Good rule usage example:
start: expr
NUMBER: /[0-9]+/
PLUS: "+"
MINUS: "-"
expr: term (("+"|"-") term)*
term: NUMBERTreat whitespace explicitly
Don't rely on open-ended %ignore directives. Using unbounded ignore directives may cause the grammar to be too complex and/or may cause the model to go out of distribution. Prefer threading explicit terminals wherever whitespace is allowed.
Troubleshooting
- If the API rejects the grammar because it is too complex, simplify the rules and terminals and remove unbounded
%ignores. - If custom tools are called with unexpected tokens, confirm terminals aren’t overlapping; check greedy lexer.
- When the model drifts "out‑of‑distribution" (shows up as the model producing excessively long or repetitive outputs, it is syntactically valid but is semantically wrong):
- Tighten the grammar.
- Iterate on the prompt (add few-shot examples) and tool description (explain the grammar and instruct the model to reason and conform to it).
- Experiment with a higher reasoning effort (e.g, bump from medium to high).
Regex CFG
Regex context free grammar example
import OpenAI from "openai";
const client = new OpenAI();
const grammar =
"^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\\s+(?P<day>\\d{1,2})(?:st|nd|rd|th)?\\s+(?P<year>\\d{4})\\s+at\\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$";
const response = await client.responses.create({
model: "gpt-6-astra",
input:
"Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
tools: [
{
type: "custom",
name: "timestamp",
description: "Saves a timestamp in date + time in 24-hr format.",
format: {
type: "grammar",
syntax: "regex",
definition: grammar,
},
},
],
});
console.log(response.output);from openai import OpenAI
client = OpenAI()
grammar = r"^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$"
response = client.responses.create(
model="gpt-6-astra",
input="Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
tools=[
{
"type": "custom",
"name": "timestamp",
"description": "Saves a timestamp in date + time in 24-hr format.",
"format": {
"type": "grammar",
"syntax": "regex",
"definition": grammar,
},
}
],
)
print(response.output)package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared"
)
func main() {
client := openai.NewClient()
grammar := `^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$`
tool := responses.ToolParamOfCustom("timestamp")
tool.OfCustom.Description = openai.String("Saves a timestamp in date and time format.")
tool.OfCustom.Format = shared.CustomToolInputFormatParamOfGrammar(grammar, "regex")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.CustomToolInputFormat;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
String grammar =
"^(January|February|March|April|May|June|July|August|September|October|November|December) "
+ "\\d{1,2}(st|nd|rd|th)? \\d{4} at (0?[1-9]|1[0-2])(AM|PM)$";
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use timestamp to save August 7th 2025 at 10AM.")
.addTool(
CustomTool.builder()
.name("timestamp")
.description("Saves a timestamp in date and time format.")
.format(
CustomToolInputFormat.Grammar.builder()
.syntax(CustomToolInputFormat.Grammar.Syntax.REGEX)
.definition(grammar)
.build())
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);require "openai"
client = OpenAI::Client.new
grammar = "^(January|February|March|April|May|June|July|August|September|October|November|December) \\d{1,2}(st|nd|rd|th)? \\d{4} at (0?[1-9]|1[0-2])(AM|PM)$"
response = client.responses.create(
model: "gpt-6-astra",
input: "Use timestamp to save August 7th 2025 at 10AM.",
tools: [{
type: :custom,
name: "timestamp",
description: "Saves a timestamp in date and time format.",
format: {type: :grammar, syntax: :regex, definition: grammar}
}]
)
puts(response.output)The output from the tool should then conform to the Regex CFG that you defined:
[
{
"id": "rs_6894f7a3dd4c81a1823a723a00bfa8710d7962f622d1c260",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6894f7ad7fb881a1bffa1f377393b1a40d7962f622d1c260",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_8m4XCnYvEmFlzHgDHbaOCFlK",
"input": "August 7th 2025 at 10AM",
"name": "timestamp"
}
]As with the Lark syntax, regexes use the Rust regex crate syntax, not Python's re module.
Some features of Regex are not supported:
- Lookarounds
- Lazy modifiers (
*?,+?,??)
Key ideas and best practices
Pattern must be on one line
If you need to match a newline in the input, use the escaped sequence \n. Do not use verbose/extended mode, which allows patterns to span multiple lines.
Provide the regex as a plain pattern string
Don't enclose the pattern in //.