# Novita silently ignores tool_choice

high · inference · probed live 2026-07-26 · affects Novita ·
HTML version: [https://inferencecanary.com/novita-tool-choice-ignored](https://inferencecanary.com/novita-tool-choice-ignored)

`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
([https://inferencecanary.com/assets/repro/tool-choice-required.novita.json](https://inferencecanary.com/assets/repro/tool-choice-required.novita.json)):

```json
{
  "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**:

```json
{
  "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
([https://inferencecanary.com/assets/repro/tool-choice-specific.novita.json](https://inferencecanary.com/assets/repro/tool-choice-specific.novita.json)):

```json
{
  "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:

```json
{
  "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](https://inferencecanary.com/deepinfra-tool-choice-unenforced.md)).

## 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`.
