@openai/agents adds reasoning.effort and text.verbosity by matching your model name
The SDK chooses reasoning.effort and text.verbosity from how your model name is spelled, not from what the model supports. Measured on the wire on 0.18.0, including what it does to a custom gateway deployment name.
Updated on September 18, 2026
On this page
Quick answer (September 2026). @openai/agents decides whether to add reasoning.effort and text.verbosity to your request by pattern-matching the model name string, not by asking the API what the model supports. The vendor documents the case where you set no model at all. It does not document the rule that fires when you do set one. Measured on the wire against SDK 0.18.0: the match is anchored and case-sensitive, it catches any name shaped like gpt-N-anything where N is 5 or higher (including your own gateway deployment names), and the SDK's own exported isGpt5Default() helper now disagrees with it.
What is already written down
Credit first, because part of this is documented and you should read the primary sources rather than take my word for any of it.
The OpenAI Agents SDK docs state the no-model default. The models page says, verbatim:
OpenAI Agents SDK documentation, models page: "When an Agent does not specify a model, the Agents SDK uses gpt-5.6-luna with reasoning.effort="none" and verbosity="low" by default for cost-sensitive, high-volume agent workflows."
That is accurate and it is the case most people hit. The same page then says applications needing frontier capability "can explicitly set model="gpt-5.6-sol" and choose model_settings that are appropriate for the workload", which reads as though setting a model hands the settings decision back to you. It does not. The defaults still apply, chosen from the name you typed.
A reported bug covers one edge of the same machinery. Issue 1849, filed 8 September 2026, reported that the Runner silently stripped explicitly configured reasoning settings for gpt-6-astra. It was fixed the same day by PR 1850, whose description is worth quoting because it explains a behaviour you will otherwise find baffling:
PR 1850, describing its own scope: "Existing GPT-5 defaults, public helper semantics, known chat-alias exclusions, and legacy cleanup remain intact."
"Public helper semantics remain intact" is the sentence that matters. It means the exported helpers were deliberately left on the old rule while the injection moved to a new one. Section five measures what that costs you.
None of those sources describe the matching rule itself, its case sensitivity, or what it does to a custom model name. That is what the rest of this covers.
capture what the SDK actually sends
Do not infer this from source reading. Point the SDK at a local server and read the bytes. You need no API key and no billing, because the request never leaves your machine.
mkdir openai-agents-capture && cd openai-agents-capture
npm init -y
npm install --save-exact @openai/agents@0.18.0
Save this as capture.js:
const http = require('http');
const { Agent, run, setDefaultOpenAIKey } = require('@openai/agents');
const captured = [];
const server = http.createServer(function (req, res) {
let body = '';
req.on('data', function (chunk) { body += chunk; });
req.on('end', function () {
captured.push(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'capture-stop' } }));
});
});
server.listen(0, async function () {
const port = server.address().port;
process.env.OPENAI_BASE_URL = 'http://127.0.0.1:' + port + '/v1';
setDefaultOpenAIKey('sk-capture-not-a-real-key');
const modelName = process.argv[2];
const agent = new Agent({ name: 'probe', instructions: 'hi', model: modelName });
try {
await run(agent, 'ping', { maxTurns: 1 });
} catch (err) {
// The fake server returns an error on purpose. We only want the request.
}
server.close();
for (const raw of captured) {
const sent = JSON.parse(raw);
console.log(modelName.padEnd(24)
+ ' reasoning=' + JSON.stringify(sent.reasoning)
+ ' text=' + JSON.stringify(sent.text));
}
});
Run it against a model name:
node capture.js gpt-5.6
The server captures the outgoing Responses request body before anything is sent upstream. This is the same shape of harness used to bisect a behaviour change across SDK versions: capture the real artefact, do not reason about what the code probably does.
the measured matrix
Seven model names, same harness, SDK 0.18.0:
Scroll to see more
model passed to new Agent | reasoning sent | text sent |
|---|---|---|
gpt-5.6 | {"effort":"none"} | {"verbosity":"low"} |
GPT-5.6 | not sent | not sent |
gpt-6-astra | {"effort":"low"} | {"verbosity":"low"} |
gpt-5-chat-latest | not sent | not sent |
gpt-4o | not sent | not sent |
llama-3.3-70b | not sent | not sent |
gpt-5-mycompany-proxy | not sent | {"verbosity":"low"} |
Three of those rows are surprising. Read the last one twice.
the rule, exactly
The decision lives in getDefaultModelSettings in @openai/agents-core. On 0.18.0 it works like this.
A model name is in the reasoning-settings family when it matches an anchored pattern: the literal gpt-, then a major version number, then optionally a dot and a minor version, then either a hyphen or the end of the string. The major version must be 5 or higher. Four name shapes are excluded outright: gpt-5-chat-latest, gpt-5.1-chat-latest, gpt-5.2-chat-latest and gpt-5.3-chat-latest, because those chat aliases reject reasoning.effort.
If the name is in the family, one of two things happens:
- The name matches a registered default effort exactly, so you get both
reasoning.effortandtext.verbosity. There are sixteen such patterns on 0.18.0, coveringgpt-5throughgpt-5.6-lunaplusgpt-6-astra. - The name is in the family but has no registered effort, so you get
text.verbosityalone and no reasoning field.
You can check any name without sending a request:
const { getDefaultModelSettings } = require('@openai/agents');
const names = [
'gpt-5', 'gpt-5-mycompany-proxy', 'gpt-12-bar',
'gpt-4-proxy', 'gpt-5x-proxy', 'gpt5-proxy',
'my-gpt-5-proxy', 'gpt-5_proxy'
];
for (const name of names) {
console.log(name.padEnd(24), JSON.stringify(getDefaultModelSettings(name)));
}
Measured output:
gpt-5 {"reasoning":{"effort":"low"},"text":{"verbosity":"low"}}
gpt-5-mycompany-proxy {"text":{"verbosity":"low"}}
gpt-12-bar {"text":{"verbosity":"low"}}
gpt-4-proxy {}
gpt-5x-proxy {}
gpt5-proxy {}
my-gpt-5-proxy {}
gpt-5_proxy {}
gpt-12-bar is in the family. gpt-5x-proxy is not, because the character after the 5 is a letter rather than a hyphen, a dot or the end of the string. gpt-5_proxy is not, because an underscore is not a hyphen. This is a string test, so its edges are string edges.
three consequences the docs do not cover
The model option is case-sensitive. The environment variable is not.
OPENAI_DEFAULT_MODEL is lowercased when it is read. The model option on an Agent is passed through as you typed it. So the same string behaves differently depending on which door it comes in through:
OPENAI_DEFAULT_MODEL=GPT-5.6-LUNA node capture.js
resolves to gpt-5.6-luna and gets reasoning.effort plus text.verbosity, while
node capture.js GPT-5.6-LUNA
gets neither. Nothing warns you. If you read model names out of a config file, a spreadsheet or an environment-specific .env that a colleague capitalised, you have a silent behaviour difference between two deployments that both "use the same model".
Your gateway deployment name is matched too
This is the one worth acting on. If you run @openai/agents against a proxy, a gateway, a self-hosted server or an Azure-style deployment, the model string is whatever you named that deployment. Name it gpt-5-mycompany-proxy, or gpt-5-turbo-internal, or gpt-5-nano-eu-west, and the SDK adds text.verbosity to every request, because the name matches a pattern that has nothing to do with the model actually serving the request.
A gateway that passes unknown fields through will not care. A gateway that validates its request schema strictly will reject the call, and the field it rejects is one you never wrote. That is a genuinely hard failure to trace, because the parameter does not appear anywhere in your code.
Name the deployment something outside the pattern and the injection stops. mycompany-gpt-5-proxy and gpt5-proxy both fall outside it, as the output above shows.
isGpt5Default() no longer answers the question you are asking
@openai/agents exports isGpt5Default(). The obvious reading is "will the SDK apply GPT-5-family settings to my default model". On 0.18.0 that reading is wrong:
const { getDefaultModel, isGpt5Default, getDefaultModelSettings } = require('@openai/agents');
console.log('model: ', getDefaultModel());
console.log('helper: ', isGpt5Default());
console.log('injected:', JSON.stringify(getDefaultModelSettings()));
With OPENAI_DEFAULT_MODEL=gpt-6-astra, measured:
model: gpt-6-astra
helper: false
injected: {"reasoning":{"effort":"low"},"text":{"verbosity":"low"}}
The helper says no. The SDK injects anyway. This is not an oversight, and it is not a bug you should report: it is the "public helper semantics remain intact" clause of PR 1850 doing exactly what it says. The helper kept its old rule, a prefix test for gpt-5, while the injection moved to the version-aware rule. They agree on every gpt-5 name and diverge on everything from gpt-6 upward.
Use getDefaultModelSettings() when you want to know what will actually be sent. Treat isGpt5Default() as a narrower question than its name suggests.
what 0.17.2 changed, and what it did not
It is tempting to attribute all of this to the September fix. One version-by-version check says otherwise. Install each version into its own directory, because installing a second version into the same directory can prune the first:
for v in 0.14.0 0.16.0 0.17.1 0.17.2 0.18.0; do
mkdir -p "v$v" && cd "v$v"
npm init -y
npm install --save-exact "@openai/agents@$v"
cd ..
done
Then run the same getDefaultModelSettings check in each. Measured:
Scroll to see more
| version | gpt-6-astra | gpt-5-mycompany-proxy |
|---|---|---|
| 0.14.0 | nothing | {"text":{"verbosity":"low"}} |
| 0.16.0 | nothing | {"text":{"verbosity":"low"}} |
| 0.17.1 | nothing | {"text":{"verbosity":"low"}} |
| 0.17.2 | {"reasoning":{"effort":"low"},"text":{"verbosity":"low"}} | {"text":{"verbosity":"low"}} |
| 0.18.0 | {"reasoning":{"effort":"low"},"text":{"verbosity":"low"}} | {"text":{"verbosity":"low"}} |
So 0.17.2 extended the family upward to gpt-6 and beyond, which is what issue 1849 asked for. The custom-name catch is older than that and is unchanged across every version tested. If you were about to write a changelog note blaming the September release for your gateway problem, that would have been wrong. The behaviour has been there at least since 0.14.0.
turning it off
Pass modelSettings and the defaults stop being applied. An empty object is enough:
const agent = new Agent({
name: 'probe',
model: 'gpt-5.6',
modelSettings: {}
});
To reproduce the full comparison, add a modelSettings key to the agent config inside capture.js and re-run it for each case. Measured that way, for gpt-5.6 on 0.18.0:
no modelSettings reasoning={"effort":"none"} text={"verbosity":"low"}
modelSettings: {} reasoning=undefined text=undefined
explicit reasoning medium reasoning={"effort":"medium"} text=undefined
The third row is the post-0.17.2 behaviour that issue 1849 asked for: an explicit providerData.reasoning is now carried through rather than stripped. It also suppresses the verbosity default, because supplying any modelSettings opts you out of the whole block.
So the rule for production is short. If you care what reasoning and text are set to, set them. If you pass nothing, the SDK will choose for you based on how your model name is spelled.
A note on the general shape
This is not an argument against defaults. reasoning.effort of none on a high-volume agent is a reasonable choice and OpenAI say why they picked it. The problem is the selector. Deriving behaviour from a pattern match on a free-text identifier works as long as every name in the wild is one the vendor minted. The moment users name their own deployments, the pattern starts matching strings it was never designed for.
Anthropic's SDK reaches a similar surface from the other direction: its subagent definitions forward tool configuration to the CLI verbatim and unvalidated, so a field the types forbid still travels. Different failure, same root: a configuration value crossing a boundary where nothing checks whether it means what the far side thinks it means.
If you are choosing between frameworks on this axis rather than on features, the comparison worth reading is what you give up by letting a vendor SDK own the loop.
How to check your own setup in one command
node -e "const {getDefaultModel,getDefaultModelSettings}=require('@openai/agents');console.log(getDefaultModel(),JSON.stringify(getDefaultModelSettings()))"
Run it with the environment your service actually runs with. Whatever that prints is what every agent without explicit modelSettings will send.
Written by
Ren OkabeRen builds agent infrastructure and writes copy-paste tutorials for engineers shipping LLM tool-use systems.
Frequently asked questions
Why does @openai/agents send reasoning.effort when I never set it?
Because the SDK applies default model settings chosen by pattern-matching your model name string. On 0.18.0, a name shaped like gpt-N or gpt-N.M followed by a hyphen or the end of the string, where N is 5 or higher, is treated as being in the GPT-5-and-newer settings family. If that exact name also has a registered default effort, you get reasoning.effort and text.verbosity. Pass any modelSettings object, even an empty one, and the defaults stop being applied.
Does the model option in @openai/agents care about capitalisation?
Yes, and the environment variable does not, which is the asymmetry to watch. OPENAI_DEFAULT_MODEL is lowercased when it is read, so GPT-5.6-LUNA resolves to gpt-5.6-luna and receives the default settings. The model option on an Agent is passed through exactly as typed, so GPT-5.6 matches nothing and receives no defaults. Measured on SDK 0.18.0.
Why does my custom gateway model name get text.verbosity added?
Because the match is on the name string, not on the model behind it. A deployment named gpt-5-mycompany-proxy, gpt-5-turbo-internal or gpt-5-nano-eu-west matches the family pattern, so the SDK adds text.verbosity of low even though nothing about that deployment is a GPT-5 model. A gateway that validates its request schema strictly will reject a field you never wrote. Renaming the deployment outside the pattern, for example mycompany-gpt-5-proxy or gpt5-proxy, stops the injection.
Is isGpt5Default() a reliable check for whether default settings will be applied?
Not on 0.18.0. With OPENAI_DEFAULT_MODEL set to gpt-6-astra the helper returns false while the SDK still injects reasoning.effort of low and text.verbosity of low. This is deliberate: PR 1850 states that public helper semantics remain intact, so the helper kept a prefix test for gpt-5 while the injection moved to a version-aware rule. Call getDefaultModelSettings() instead if you want to know what will actually be sent.
Did the September 2026 release introduce this behaviour?
Only half of it. Installing 0.14.0, 0.16.0, 0.17.1, 0.17.2 and 0.18.0 each into its own directory and running the same check shows that 0.17.2 extended the family upward to gpt-6 and beyond, which is what issue 1849 asked for. The custom-name catch, where gpt-5-mycompany-proxy receives text.verbosity, is unchanged across every version tested and goes back at least to 0.14.0.
How do I see what my own service is sending without an API key?
Point OPENAI_BASE_URL at a local HTTP server that captures the request body and returns an error, then run one agent turn. The request never leaves your machine, so no key and no billing are needed. For a settings-only check, run node with a one-liner that prints getDefaultModel() and getDefaultModelSettings() using the same environment your service runs with.
Related tutorials
Bisecting a Claude Agent SDK behaviour change: why grepping the bundle dates it wrong
A grep bisect tells you when today's code landed, not when the behaviour changed. Here is the argv-capture harness that dates it properly, and the worked example where grepping made a maintainer look wrong and running the versions proved him right.
Claude Agent SDK: tools vs allowedTools, and what a subagent definition silently accepts
allowedTools auto-approves, tools restricts. That half is documented. What is not: an AgentDefinition has no allowedTools field, and the SDK forwards one verbatim to the CLI anyway, unvalidated.
Claude Agent SDK vs LangGraph (2026): When Each Wins
A runnable comparison of the Claude Agent SDK and LangGraph in 2026, with the same agent built in both and an honest decision matrix for when each wins.