Novita silently ignores tool_choice
high · inference · probed live 2026-07-26 · affects Novita
tool_choice is the Chat Completions field an application uses to constrain tool
calling: "required" guarantees at least one tool call, and naming a function
forces a call to exactly that function. Novita accepts both constraints and simply ignores
them. Only "none" is honored — the one sign the field is read at all; whether it
is validated like DeepInfra's is untested.
What was measured
tool_choice: "required" on a prose-inviting prompt
(download):
{
"model": "google/gemma-4-31b-it",
"messages": [
{
"role": "user",
"content": "Just say hello."
}
],
"tools": [
{
"type": "function",
"function": {
"name": "log_event",
"description": "Logs an event message.",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
],
"tool_choice": "required",
"chat_template_kwargs": {
"enable_thinking": false
},
"temperature": 0,
"max_tokens": 200
}
Received — HTTP 200:
{
"id": "3c134565d28fa4229d6f1b203a3cab71",
"object": "chat.completion",
"created": 1785075405,
"model": "google/gemma-4-31b-it",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello!" },
"finish_reason": "tool_calls"
}
],
"usage": { "prompt_tokens": 58, "completion_tokens": 3, "total_tokens": 61 }
}
Prose, no tool_calls key at all — under
finish_reason: "tool_calls".
Forcing get_time by name on a weather prompt
(download):
{
"model": "google/gemma-4-31b-it",
"messages": [
{
"role": "user",
"content": "What's the weather in Paris right now?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "get_news",
"description": "Latest news on a topic.",
"parameters": {
"type": "object",
"properties": {
"topic": {
"type": "string"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "get_time",
"description": "Current time in a timezone.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string"
}
}
}
}
}
],
"tool_choice": {
"type": "function",
"function": {
"name": "get_time"
}
},
"chat_template_kwargs": {
"enable_thinking": false
},
"temperature": 0,
"max_tokens": 200
}
Received — HTTP 200, a call to get_weather
instead:
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "chatcmpl-tool-a81f9268ff103aec",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\": \"Paris\"}" }
}
]
},
"finish_reason": "tool_calls"
}
The failure signature — wrong tool, same arguments, same self-contradicting finish_reason — is byte-for-byte the DeepInfra signature (that finding).
How this breaks the contract — and why it matters
200s that satisfy neither guarantee, detectable only by comparing the returned tool name against the one you forced. Workflows that depend on forced tool calls — structured extraction, guaranteed tool loops — silently receive prose or a call with different semantics.
Run it yourself
curl "https://api.novita.ai/openai/chat/completions" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d @tool-choice-required.novita.json
Expected: a log_event call, then (with the second body) a get_time
call. Received: prose, then get_weather.