OpenAI Chat Completions function calling with a compatible gateway
Use the Chat Completions tools shape, not the Responses one. This guide covers field layout, follow-up turns and what a 401 probe does not prove.
What Chat Completions tool calling does
The model does not run your code. It returns structured tool_calls; your application validates arguments, runs an allowlisted function, and sends a role: tool message back. Treat model-produced arguments as untrusted input.
| Step | Chat Completions field | Your application |
|---|---|---|
| 1. Define | tools[].type = function and tools[].function | Expose only approved functions |
| 2. Request | choices[0].message.tool_calls[] | Read id, function.name, function.arguments |
| 3. Execute | No external action is performed by the model | Authorize, validate, run and record the call |
| 4. Continue | Append assistant message + role: tool with matching tool_call_id | Ask for the final answer |
Verify the route, key and model first
Check GET /v1/models, then use an exact model ID returned to your key. On August 4, 2026, production probes against MonkeyAI showed:
- Unauthenticated
POST /v1/chat/completionswith a valid tools body returned401 API_KEY_REQUIRED. - Invalid Bearer token returned
401 INVALID_API_KEY. - Unauthenticated
GET /v1/modelsreturned401 API_KEY_REQUIRED.
Those results only prove route and authentication reachability. They do not prove that a specific model supports tools, strict mode or parallel calls.
curl -sS https://monkeytheme.com/v1/models \ -H "Authorization: Bearer ***"
Send a minimal Chat Completions tools definition
In Chat Completions, function fields sit under tools[].function. OpenAI OpenAPI names this shape ChatCompletionTool. Name length is limited to 64 characters and should use letters, numbers, underscores or dashes.
curl -sS https://monkeytheme.com/v1/chat/completions \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID_FROM_LIST",
"messages": [
{"role": "user", "content": "What is the weather in Paris?"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Return current weather for a city.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
"additionalProperties": false
}
}
}],
"tool_choice": "auto"
}'With strict mode, list every property in required and set additionalProperties to false on each object. OpenAI notes that Chat Completions remains non-strict by default unless you set strict: true.
Parse tool_calls and return tool results
When the model chooses tools, the assistant message usually has empty or null content, finish_reason is often tool_calls, and message.tool_calls carries one or more calls. Append that assistant message, then one tool message per call ID.
import json
from openai import OpenAI
client = OpenAI(
api_key="YOUR_KEY",
base_url="https://monkeytheme.com/v1",
)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Return current weather for a city.",
"strict": True,
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
},
}]
messages = [{"role": "user", "content": "What is the weather in Paris?"}]
first = client.chat.completions.create(
model="MODEL_ID_FROM_LIST",
messages=messages,
tools=tools,
tool_choice="auto",
)
assistant = first.choices[0].message
messages.append(assistant)
for call in assistant.tool_calls or []:
args = json.loads(call.function.arguments)
result = dispatch_approved_function(call.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(
model="MODEL_ID_FROM_LIST",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)dispatch_approved_function is application code, not an SDK helper. OpenAI also warns that function.arguments is a JSON string and may be invalid or invent fields outside your schema, so validate before execution.
tool_choice and parallel_tool_calls
| Control | Chat Completions value | Effect |
|---|---|---|
| Auto | "tool_choice": "auto" | Default when tools are present: zero, one or more tools |
| Required | "tool_choice": "required" | Model must call one or more tools |
| None | "tool_choice": "none" | No tool call; text only |
| Named tool | {"type":"function","function":{"name":"get_weather"}} | Force that function |
| Parallel off | "parallel_tool_calls": false | At most one tool call in the turn |
Do not copy the Responses-only named shape {"type":"function","name":"..."} into Chat Completions. The named Chat Completions choice nests the name under function.
Do not mix Chat Completions and Responses shapes
| Detail | Chat Completions | Responses |
|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses |
| Function definition | tools[].function.{name,description,parameters,strict} | Function fields sit directly on the tools item |
| Returned call | choices[0].message.tool_calls[] | output[] items with type function_call |
| Correlation field | tool_call_id on the tool message | call_id on function_call_output |
| Result message | {"role":"tool", ...} | function_call_output input item |
If a client or SDK example targets Responses, rewrite the schema before pointing it at /v1/chat/completions.
Put safety checks around the executor
- Allowlist function names; never map a model-supplied name to arbitrary code.
- Parse JSON, validate again server-side and reject unknown fields.
- Apply the current user's authorization to every resource ID.
- Require confirmation for destructive or externally visible actions.
- Use idempotency keys for writes and keep an audit record of the tool call ID.
- Return bounded, redacted output. Do not feed secrets back into the model context.
Sources and related guides
FAQ
Does the model execute my function?
No. The model returns structured tool_calls. Your application decides whether to run them and sends role: tool results back.
Does a 401 response prove tool calling works?
No. It only proves the request reached authentication. Use a real key and an exact model ID to test tools.
Where do Chat Completions function fields go?
Under tools[].function. Do not place name/description/parameters at the tools item root the way Responses does.
How do I correlate a tool result?
Copy message.tool_calls[].id into the next role: tool message as tool_call_id. Missing or mismatched IDs break the follow-up turn.
Can one assistant message contain several tool calls?
Yes. OpenAI allows zero, one or multiple calls unless you set parallel_tool_calls to false. Iterate every call.