> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milliseconds.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milliseconds.ai/_mcp/server.

# Function calling

Send `tools` on `POST /v1/chat/completions` and decision-machine-1 returns one tool call.
Every request needs an API key. See [Authentication](/authentication).

The model does not reason about your tools. It runs two typed decisions:

#### Pick the tool

Each tool becomes one label: its `function.name` with its `function.description`. Classification over those labels picks the winning name.

#### Fill the arguments

Extraction then runs the chosen tool's `function.parameters` schema over the same text. Each property becomes a field to find.

Both steps read the user text only. The API joins `user` messages and discards `system`, `developer`, `assistant` and `tool` roles.

## What comes back

The response is an OpenAI chat completion with `finish_reason: "tool_calls"` and `content: null`.
The API returns exactly one tool call, ever. Ids are `call_` plus 24 hex characters.

**`curl`**

```bash title="curl"
curl -s https://api.milliseconds.ai/v1/chat/completions \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer test_sk-...' \
  -d '{
    "model": "decision-machine-1",
    "messages": [
      {"role": "user", "content": "Hi, I was charged twice for my May invoice. Order #A-4471. Please refund the duplicate."}
    ],
    "tools": [
      {"type": "function", "function": {
        "name": "issue_refund",
        "description": "The customer asks for money back for a duplicate or wrong charge.",
        "parameters": {"type": "object", "properties": {
          "order_id": {"type": "string", "description": "The order or invoice reference"},
          "reason": {"type": "string", "description": "Why the customer wants a refund"}
        }}
      }},
      {"type": "function", "function": {
        "name": "track_shipment",
        "description": "The customer asks where a package is or when it arrives.",
        "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}}
      }},
      {"type": "function", "function": {
        "name": "reset_password",
        "description": "The customer cannot sign in and needs a password reset.",
        "parameters": {"type": "object", "properties": {"email": {"type": "string"}}}
      }}
    ]
  }'
```

**`Response`**

```json title="Response"
{
  "id": "chatcmpl-17848228b0f14d5f93abe7b1",
  "object": "chat.completion",
  "created": 1789607270,
  "model": "decision-machine-1",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_5f5c75850cb14b76a357925d",
            "type": "function",
            "function": {
              "name": "issue_refund",
              "arguments": "{\"order_id\":\"A-4471\",\"reason\":null}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {"prompt_tokens": 22, "completion_tokens": 9, "total_tokens": 31}
}
```

`arguments` is a JSON string, as in the OpenAI API. Parse it before you use it.
A property with nothing to find in the text comes back `null`, like `reason` above. Handle nulls in your tool handler.

## The OpenAI SDKs

Point the official OpenAI client at `https://api.milliseconds.ai/v1`. Any `api_key` string works.

**`Python (openai)`**

```python title="Python (openai)"
import json
from openai import OpenAI

client = OpenAI(base_url="https://api.milliseconds.ai/v1", api_key="test_sk-...")

tools = [
    {"type": "function", "function": {
        "name": "issue_refund",
        "description": "The customer asks for money back for a duplicate or wrong charge.",
        "parameters": {"type": "object", "properties": {
            "order_id": {"type": "string", "description": "The order or invoice reference"},
            "reason": {"type": "string", "description": "Why the customer wants a refund"},
        }},
    }},
    {"type": "function", "function": {
        "name": "track_shipment",
        "description": "The customer asks where a package is or when it arrives.",
        "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}},
    }},
]

resp = client.chat.completions.create(
    model="decision-machine-1",
    messages=[{"role": "user", "content": "Charged twice for order A-4471, refund please."}],
    tools=tools,
)

call = resp.choices[0].message.tool_calls[0]
print(call.function.name, json.loads(call.function.arguments))
```

**`TypeScript (openai)`**

```typescript title="TypeScript (openai)"
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.milliseconds.ai/v1",
  apiKey: "test_sk-...",
});

const resp = await client.chat.completions.create({
  model: "decision-machine-1",
  messages: [{ role: "user", content: "Charged twice for order A-4471, refund please." }],
  tools: [
    {
      type: "function",
      function: {
        name: "issue_refund",
        description: "The customer asks for money back for a duplicate or wrong charge.",
        parameters: {
          type: "object",
          properties: {
            order_id: { type: "string", description: "The order or invoice reference" },
            reason: { type: "string", description: "Why the customer wants a refund" },
          },
        },
      },
    },
    {
      type: "function",
      function: {
        name: "track_shipment",
        description: "The customer asks where a package is or when it arrives.",
        parameters: { type: "object", properties: { order_id: { type: "string" } } },
      },
    },
  ],
});

const call = resp.choices[0].message.tool_calls![0];
console.log(call.function.name, JSON.parse(call.function.arguments));
```

## tool_choice rules

| `tool_choice`                                 | Behaviour                                                                                                                                       |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| absent, `"auto"`, `"required"`                | The same path. With one tool, the API uses it and runs no classification. With two or more, classification over the tool labels picks the name. |
| `{"type":"function","function":{"name":"…"}}` | The API forces that tool and runs no classification. An unknown name returns 400 `invalid_request`.                                             |
| `"none"`                                      | The API skips the tools path. The request falls through to `response_format`, or returns 400 `unsupported_request`.                             |

`auto` and `required` behave the same way, because the model always returns a tool call on this path.
A classification result that names no known tool falls back to `tools[0]`.

> **Warning**
>
> `tool_choice: "none"` without a `response_format.json_schema` returns 400:
>
> ```json
> {"error":{"code":"unsupported_request","message":"decision-machine-1 decides, it does not chat. Send response_format.json_schema for structured extraction, tools for function calling, or use the capability endpoints under /v1/decision-machine-1/."}}
> ```

### Forcing one tool

A named `tool_choice` skips the pick and runs extraction only. It overrides what the text is about.

**`curl`**

```bash title="curl"
curl -s https://api.milliseconds.ai/v1/chat/completions \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer test_sk-...' \
  -d '{
    "model": "decision-machine-1",
    "messages": [{"role": "user", "content": "Weather in Paris"}],
    "tools": [
      {"type": "function", "function": {"name": "get_weather",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}},
      {"type": "function", "function": {"name": "book_hotel",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}
    ],
    "tool_choice": {"type": "function", "function": {"name": "book_hotel"}}
  }'
```

**`Response`**

```json title="Response"
{
  "id": "chatcmpl-3a5eb2efc72b4c4fb6fea803",
  "object": "chat.completion",
  "created": 1789606543,
  "model": "decision-machine-1",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {"id":"call_72600ea87f66442bb15f701f","type":"function","function":{"name":"book_hotel","arguments":"{\"city\":\"Paris\"}"}}
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {"prompt_tokens":7,"completion_tokens":4,"total_tokens":11}
}
```

**`Unknown tool name -> 400`**

```json title="Unknown tool name -> 400"
{"error":{"code":"invalid_request","message":"tool_choice names an unknown tool."}}
```

## Tools with no parameters

A tool without `parameters`, or with no `properties`, makes no extraction call. `arguments` is the literal `"{}"`.
This call took 0.07 s, because no model ran.

**`Response`**

```json title="Response"
{"id":"chatcmpl-1083cb565f6345dd98ade359","object":"chat.completion","created":1789606543,"model":"decision-machine-1","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_69971c1a13854e7aaeedb156","type":"function","function":{"name":"do_it","arguments":"{}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}
```

## Write descriptions that describe the case

The tool pick is a classification over your descriptions. A description of the tool's mechanics classifies badly.
Describe the user situation that should trigger the tool.

| Weak                     | Strong                                                                |
| ------------------------ | --------------------------------------------------------------------- |
| `"Refund tool"`          | `"The customer asks for money back for a duplicate or wrong charge."` |
| `"Shipping API wrapper"` | `"The customer asks where a package is or when it arrives."`          |
| `"Auth helper"`          | `"The customer cannot sign in and needs a password reset."`           |

The same rule applies to `parameters`: give each property a `description` that says what to look for in the text.
The [writing rules for statements and labels](/concepts/writing-good-questions) apply here without change.

## Limits of this surface

> **Note**
>
> `tools` wins over `response_format` when you send both. The tools path runs first.

* One tool call per response. Parallel tool calls do not exist.
* No probability and no confidence. The classification scores stay inside the facade.
* No conversation. The API discards a follow-up `tool` message, like every non-user role.
* The parameters schema goes through the same extraction engine, so the [schema support matrix](/capabilities/extract) applies.
* `usage.prompt_tokens` reports the request's input tokens. See [Pricing](/reference/pricing).

Measured latency: 0.72 s for two tools (one classification call plus one extraction call), 0.40 s for a forced tool.
Fewer tools and a forced `tool_choice` both cut a call.

Call [classify](/capabilities/classify) and [extract](/capabilities/extract) directly when you want the label
probabilities, the scores for every tool, or a batch of texts in one request. The
[migration table](/openai/when-to-use-native) maps each chat usage to a capability.

## Next

* [Intent routing](/patterns/intent-routing) — the same routing decision with probabilities you can threshold.
* [Structured extraction](/openai/structured-extraction) — `response_format.json_schema` when you want data, not a tool.
* [Streaming and differences](/openai/streaming-and-differences) — what `stream: true` returns and what the facade ignores.