> ## Documentation Index
> Fetch the complete documentation index at: https://warpllm.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool calling

> Let a model call your functions, and feed the results back into the conversation.

Tool calling uses the OpenAI shape: you send `tools`, the model answers with
`tool_calls`, you run them and send the results back as `tool` messages.

<Note>
  Whether a given model calls tools well is the provider's business, not
  warpllm's. warpllm forwards `tools` and `tool_choice` and returns whatever
  comes back. Check the provider's own docs before assuming a model supports
  them.
</Note>

## The exchange

<Steps>
  <Step title="Send tool definitions with the request">
    ```json theme={null}
    {
      "model": "openai/gpt-5-nano",
      "messages": [{ "role": "user", "content": "What's the weather in Oslo?" }],
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "get_weather",
            "description": "Current weather for a city.",
            "parameters": {
              "type": "object",
              "properties": { "city": { "type": "string" } },
              "required": ["city"]
            },
            "strict": true
          }
        }
      ]
    }
    ```
  </Step>

  <Step title="The model answers with tool_calls">
    `finish_reason` is `tool_calls`, and `message.content` is `null`.

    ```json theme={null}
    {
      "choices": [
        {
          "finish_reason": "tool_calls",
          "message": {
            "role": "assistant",
            "content": null,
            "tool_calls": [
              {
                "id": "call_abc",
                "type": "function",
                "function": {
                  "name": "get_weather",
                  "arguments": "{\"city\":\"Oslo\"}"
                }
              }
            ]
          }
        }
      ]
    }
    ```

    <Warning>
      `arguments` is a **JSON string**, not an object. Parse it, and treat a
      parse failure as a normal outcome — models emit malformed JSON.
    </Warning>
  </Step>

  <Step title="Append the assistant turn and one tool message per call">
    Each `tool` message carries the `tool_call_id` it answers.

    ```json theme={null}
    { "role": "tool", "tool_call_id": "call_abc", "content": "{\"tempC\": 4}" }
    ```
  </Step>

  <Step title="Call again with the extended message list">
    The model now has the results and produces its final answer. Loop until
    `finish_reason` is no longer `tool_calls`.
  </Step>
</Steps>

## A complete loop

<CodeGroup>
  ```python Python theme={null}
  import json

  from warpllm import WarpLLM

  client = WarpLLM()

  TOOLS = [
      {
          "type": "function",
          "function": {
              "name": "get_weather",
              "description": "Current weather for a city.",
              "parameters": {
                  "type": "object",
                  "properties": {"city": {"type": "string"}},
                  "required": ["city"],
              },
          },
      }
  ]

  IMPLEMENTATIONS = {"get_weather": lambda city: {"tempC": 4, "city": city}}

  messages = [{"role": "user", "content": "What's the weather in Oslo?"}]

  while True:
      completion = client.chat_completions({
          "model": "openai/gpt-5-nano",
          "messages": messages,
          "tools": TOOLS,
      })
      message = completion["choices"][0]["message"]
      messages.append(message)

      calls = message.get("tool_calls")
      if not calls:
          print(message["content"])
          break

      for call in calls:
          fn = call["function"]
          result = IMPLEMENTATIONS[fn["name"]](**json.loads(fn["arguments"]))
          messages.append({
              "role": "tool",
              "tool_call_id": call["id"],
              "content": json.dumps(result),
          })
  ```

  ```ts TypeScript theme={null}
  import { WarpLLM } from '@warpllm/warpllm'

  const client = new WarpLLM()

  const tools = [
    {
      type: 'function',
      function: {
        name: 'get_weather',
        description: 'Current weather for a city.',
        parameters: {
          type: 'object',
          properties: { city: { type: 'string' } },
          required: ['city'],
        },
      },
    },
  ]

  const implementations = {
    get_weather: ({ city }: { city: string }) => ({ tempC: 4, city }),
  }

  const messages: any[] = [{ role: 'user', content: "What's the weather in Oslo?" }]

  for (;;) {
    const completion = await client.chatCompletions({
      model: 'openai/gpt-5-nano',
      messages,
      tools,
    })
    const message = completion.choices[0].message
    messages.push(message)

    const calls = message.tool_calls
    if (!calls?.length) {
      console.log(message.content)
      break
    }

    for (const call of calls) {
      if (call.type !== 'function') continue
      const result = implementations[call.function.name](JSON.parse(call.function.arguments))
      messages.push({
        role: 'tool',
        tool_call_id: call.id,
        content: JSON.stringify(result),
      })
    }
  }
  ```
</CodeGroup>

## Forcing a tool

`tool_choice` controls whether the model may, must, or must not call one.

| Value | Effect |
| - | - |
| `"auto"` | The model decides. The default when `tools` is present |
| `"none"` | No tool calls; answer in text |
| `"required"` | The model must call some tool |
| `{ "type": "function", "function": { "name": "get_weather" } }` | It must call that one |

## Tool calls in a stream

Streamed tool calls arrive as fragments, and `arguments` is built up across
chunks. Reassemble by `index`.

```python theme={null}
calls: dict[int, dict] = {}

for chunk in stream:
    for fragment in chunk["choices"][0]["delta"].get("tool_calls", []):
        call = calls.setdefault(
            fragment["index"], {"id": "", "name": "", "arguments": ""}
        )
        call["id"] += fragment.get("id", "")
        call["name"] += fragment.get("function", {}).get("name", "")
        call["arguments"] += fragment.get("function", {}).get("arguments", "")

# Only now is each `arguments` a complete JSON string.
```

<Warning>
  Do not parse `arguments` until the stream ends. A fragment is a slice of a
  JSON string, and a slice of valid JSON is rarely valid JSON.
</Warning>

## Custom tool calls

Alongside `type: "function"`, the response types model `type: "custom"` calls,
which carry `custom: { name, input }` — a raw string rather than JSON
arguments. Branch on `type` before reaching for `function`.

## Next

<CardGroup cols={2}>
  <Card title="Structured outputs" icon="brackets-curly" href="/guides/structured-outputs">
    When you want a shaped answer rather than a function call.
  </Card>

  <Card title="Streaming" icon="water" href="/guides/streaming">
    The chunk shape these fragments arrive in.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.