Searched existing issues/PRs first. The strict items I found all concern the response_format / structured-output path (#3928, #4045, PR #3931, open #4422 "Generated JSON schema fails strict mode checks of Azure", commit a1b7d5e). I could not find any issue/PR about tool/function strict. This report is specifically about the tool-calling path.
Bug description
When building OpenAI tool/function definitions, strict is written inside the parameters JSON-Schema object (via FunctionParameters.Builder.putAdditionalProperty("strict", true)) and the function-level FunctionDefinition.Builder.strict(...) is never called.
Per the OpenAI/Azure API, strict is a field of the function object — a sibling of parameters, alongside name/description — not a JSON-Schema keyword. Placed inside parameters, strict is an unrecognized schema keyword and is ignored by the provider. The net effect is that strict mode is a silent no-op for tool calling: the framework hardcodes strict = true, but the model runs best-effort tool-argument generation with no schema enforcement. No error is raised, so the misbehavior is invisible.
This affects both spring-ai-openai (OpenAiChatModel) and spring-ai-openai-sdk (OpenAiSdkChatModel).
Relevant code (OpenAiChatModel#getChatCompletionTools, main):
parametersBuilder.putAdditionalProperty("strict", JsonValue.from(true)); // TODO allow non-strict
...
FunctionDefinition functionDefinition = FunctionDefinition.builder()
.name(toolDefinition.name())
.description(toolDefinition.description())
.parameters(parametersBuilder.build()) // strict ends up INSIDE parameters
.build(); // FunctionDefinition.Builder.strict(...) never called
The OpenAI Java SDK already exposes FunctionDefinition.Builder.strict(Boolean) — it is simply not used here.
Environment
- Spring AI: 2.0.0 — and the defect is still present on
main.
- Affected module(s):
spring-ai-openai (OpenAiChatModel#getChatCompletionTools); the same defect is in spring-ai-openai-sdk (OpenAiSdkChatModel).
- Provider: reproducible on both OpenAI and Azure OpenAI — the misplacement is provider-agnostic.
(Java/Spring Boot versions and vector store are not relevant here: this is a deterministic tool-schema serialization defect, independent of runtime or retrieval configuration.)
Steps to reproduce
- Register any
@Tool/ToolCallback and call OpenAiChatModel (or OpenAiSdkChatModel) with a prompt that triggers the tool.
- Capture the outgoing
/chat/completions request body.
- Inspect
tools[0].function.
Observed request (abbreviated):
{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": { "location": { "type": "string" } },
"strict": true // ← placed here; ignored by the API
}
}
}
]
}
There is no tools[0].function.strict. Because strict sits inside the JSON schema, the provider ignores it and the request is treated as non-strict. (This also explains why non-strict-compliant tool schemas — e.g. objects without additionalProperties:false/required — are accepted rather than rejected the way the response_format path rejects them in #4422.)
Expected behavior
strict should be set at the function level and omitted from the JSON schema:
{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"strict": true, // ← function-level, honored by the API
"parameters": {
"type": "object",
"properties": { "location": { "type": "string" } },
"additionalProperties": false,
"required": ["location"]
}
}
}
]
}
i.e. call FunctionDefinition.builder()....parameters(...).strict(true).build() instead of injecting "strict" into the parameters object. Ideally the value is configurable (per the existing // TODO allow non-strict) via OpenAiChatOptions, since strict imposes schema requirements (additionalProperties:false and all fields required at every level) that not every tool schema satisfies.
Minimal Complete Reproducible example
A test that captures the request body and asserts strict placement (fails on current behavior):
@Test
void toolStrictIsEmittedAtFunctionLevel_notInsideParameters() throws Exception {
try (var server = new MockWebServer()) {
server.enqueue(new MockResponse()
.setHeader("Content-Type", "application/json")
// minimal valid assistant response, no tool call needed to inspect the request
.setBody("""
{"id":"x","object":"chat.completion","created":0,"model":"gpt-4.1",
"choices":[{"index":0,"finish_reason":"stop",
"message":{"role":"assistant","content":"ok"}}]}"""));
server.start();
var api = OpenAiApi.builder()
.baseUrl(server.url("/").toString())
.apiKey(new SimpleApiKey("test"))
.build();
var chatModel = OpenAiChatModel.builder()
.openAiApi(api)
.defaultOptions(OpenAiChatOptions.builder().model("gpt-4.1").build())
.build();
ToolCallback tool = FunctionToolCallback
.builder("get_weather", (WeatherRequest req) -> "sunny")
.description("Get weather")
.inputType(WeatherRequest.class)
.build();
chatModel.call(new Prompt("What is the weather?",
ToolCallingChatOptions.builder().toolCallbacks(tool).build()));
var recorded = server.takeRequest();
var body = new ObjectMapper().readTree(recorded.getBody().readUtf8());
var function = body.at("/tools/0/function");
// Expected: strict at function level
assertThat(function.path("strict").asBoolean(false)).isTrue(); // FAILS today
// And NOT inside the parameters schema
assertThat(function.path("parameters").has("strict")).isFalse(); // FAILS today
}
}
record WeatherRequest(String location) {}
(Builder API names may need minor adjustment to the exact release; the assertion on tools[0].function.strict vs tools[0].function.parameters.strict is the point.)
Suggested fix
In OpenAiChatModel#getChatCompletionTools (and the OpenAiSdkChatModel equivalent): drop parametersBuilder.putAdditionalProperty("strict", ...) and instead call FunctionDefinition.Builder.strict(<configurable>). Expose the flag on OpenAiChatOptions (default configurable) to resolve the existing // TODO allow non-strict.
Bug description
When building OpenAI tool/function definitions,
strictis written inside theparametersJSON-Schema object (viaFunctionParameters.Builder.putAdditionalProperty("strict", true)) and the function-levelFunctionDefinition.Builder.strict(...)is never called.Per the OpenAI/Azure API,
strictis a field of the function object — a sibling ofparameters, alongsidename/description— not a JSON-Schema keyword. Placed insideparameters,strictis an unrecognized schema keyword and is ignored by the provider. The net effect is that strict mode is a silent no-op for tool calling: the framework hardcodesstrict = true, but the model runs best-effort tool-argument generation with no schema enforcement. No error is raised, so the misbehavior is invisible.This affects both
spring-ai-openai(OpenAiChatModel) andspring-ai-openai-sdk(OpenAiSdkChatModel).Relevant code (
OpenAiChatModel#getChatCompletionTools,main):The OpenAI Java SDK already exposes
FunctionDefinition.Builder.strict(Boolean)— it is simply not used here.Environment
main.spring-ai-openai(OpenAiChatModel#getChatCompletionTools); the same defect is inspring-ai-openai-sdk(OpenAiSdkChatModel).(Java/Spring Boot versions and vector store are not relevant here: this is a deterministic tool-schema serialization defect, independent of runtime or retrieval configuration.)
Steps to reproduce
@Tool/ToolCallbackand callOpenAiChatModel(orOpenAiSdkChatModel) with a prompt that triggers the tool./chat/completionsrequest body.tools[0].function.Observed request (abbreviated):
{ "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get weather", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "strict": true // ← placed here; ignored by the API } } } ] }There is no
tools[0].function.strict. Becausestrictsits inside the JSON schema, the provider ignores it and the request is treated as non-strict. (This also explains why non-strict-compliant tool schemas — e.g. objects withoutadditionalProperties:false/required— are accepted rather than rejected the way theresponse_formatpath rejects them in #4422.)Expected behavior
strictshould be set at the function level and omitted from the JSON schema:{ "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get weather", "strict": true, // ← function-level, honored by the API "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "additionalProperties": false, "required": ["location"] } } } ] }i.e. call
FunctionDefinition.builder()....parameters(...).strict(true).build()instead of injecting"strict"into the parameters object. Ideally the value is configurable (per the existing// TODO allow non-strict) viaOpenAiChatOptions, since strict imposes schema requirements (additionalProperties:falseand all fieldsrequiredat every level) that not every tool schema satisfies.Minimal Complete Reproducible example
A test that captures the request body and asserts
strictplacement (fails on current behavior):(Builder API names may need minor adjustment to the exact release; the assertion on
tools[0].function.strictvstools[0].function.parameters.strictis the point.)Suggested fix
In
OpenAiChatModel#getChatCompletionTools(and theOpenAiSdkChatModelequivalent): dropparametersBuilder.putAdditionalProperty("strict", ...)and instead callFunctionDefinition.Builder.strict(<configurable>). Expose the flag onOpenAiChatOptions(default configurable) to resolve the existing// TODO allow non-strict.