Cette page n'est pas encore traduite en Français. La version anglaise d'origine est affichée.
Tool calling
Letting the model call your functions
Tool calls (also called function calls) let a model ask for information or actions it does not have on its own. The model never calls anything itself — it replies with a tool_calls array naming a function and its arguments, you run that function locally, and you send the result back in a follow-up request. The model then folds the result into its final answer.
The request and response shapes are the OpenAI Chat Completions tool-calling format, unchanged. If your code already calls OpenAI or an OpenAI-compatible gateway this way, pointing it at https://openrouter.myip.co.kr/api/v1 and one of our model ids is the only change required.
Checking support before you call
supported_parameters in GET /models is the source of truth, and it also works as a query filter:
curl "https://openrouter.myip.co.kr/api/v1/models?supported_parameters=tools"To force the gateway to reject a request rather than silently drop to a model that cannot use tools, add require_parameters: true to provider{} — see Provider routing.
The three-step exchange
Step 1 — send the tools with the request
{
"model": "google/gemma-4-26b-a4b",
"messages": [
{ "role": "user", "content": "What's the weather in Busan right now?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g. Busan" }
},
"required": ["city"]
}
}
}
]
}If the model decides it needs the tool, it replies with finish_reason: "tool_calls" and a tool_calls array instead of message content:
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_8f2a",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Busan\"}" }
}
]
},
"finish_reason": "tool_calls",
"native_finish_reason": "tool_calls"
}
]
}Step 2 — run the tool yourself
We never execute anything. Parse function.arguments (it is a JSON string, not an object) and call your own code:
const args = JSON.parse(toolCall.function.arguments) as { city: string };
const result = await getWeather(args.city);Step 3 — send the result back
Append the assistant's tool-call message and a role: "tool" message carrying the result, then call the API again with the same tools array:
{
"model": "google/gemma-4-26b-a4b",
"messages": [
{ "role": "user", "content": "What's the weather in Busan right now?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{ "id": "call_8f2a", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Busan\"}" } }
]
},
{ "role": "tool", "tool_call_id": "call_8f2a", "content": "{\"tempC\":21,\"condition\":\"clear\"}" }
],
"tools": [ /* same tool definitions as step 1 */ ]
}The model's second response is a normal text message, with finish_reason: "stop".
Full example
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://openrouter.myip.co.kr/api/v1", api_key=os.environ["MYIP_API_KEY"])
MODEL = "google/gemma-4-26b-a4b"
def get_weather(city: str) -> dict:
return {"tempC": 21, "condition": "clear"} # replace with a real call
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "What's the weather in Busan right now?"}]
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message.model_dump())
for call in message.tool_calls or []:
args = json.loads(call.function.arguments)
result = get_weather(**args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
final = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
print(final.choices[0].message.content)tool_choice
{ "tool_choice": "auto" } // default: model decides
{ "tool_choice": "none" } // never call a tool
{ "tool_choice": "required" } // must call some tool
{ "tool_choice": { "type": "function", "function": { "name": "get_weather" } } } // force this onetool_choice is not in our routing-key list, so it is forwarded upstream exactly as sent; whether an engine honours every variant is a property of that engine, not of us.
parallel_tool_calls
Some engines can request several tools in the same turn. Set parallel_tool_calls: false to force one call at a time. Like every non-routing parameter, it is passed straight through — it has an effect only if the serving engine implements it.
Streaming with tools
Tool calls arrive incrementally across delta.tool_calls chunks, keyed by array index; you accumulate function.arguments as a string until the chunk with finish_reason: "tool_calls".
const toolCallsByIndex: Record<number, { id: string; name: string; args: string }> = {};
for await (const chunk of stream) {
for (const delta of chunk.choices[0]?.delta.tool_calls ?? []) {
const slot = (toolCallsByIndex[delta.index] ??= { id: '', name: '', args: '' });
if (delta.id) slot.id = delta.id;
if (delta.function?.name) slot.name = delta.function.name;
if (delta.function?.arguments) slot.args += delta.function.arguments;
}
if (chunk.choices[0]?.finish_reason === 'tool_calls') break;
}See Streaming for the general SSE shape, including the final usage chunk and how mid-stream errors are reported.
A minimal agent loop
Chaining tool calls until the model stops asking for one is the same loop regardless of how many tools you define:
def run(messages, tools, max_turns=10):
for _ in range(max_turns):
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message.model_dump())
if not message.tool_calls:
return message.content
for call in message.tool_calls:
result = TOOL_MAP[call.function.name](**json.loads(call.function.arguments))
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
raise RuntimeError("max_turns exceeded")Always cap the number of turns. A model that keeps asking for tools is not a hypothetical — it is the most common way an integration runs up an unexpected bill.
Good tool definitions
- Name tools for what they do, not vaguely:
get_weather_forecast, notweather. - Describe them like documentation. The model only knows what
descriptiontells it — mention units, formats, and edge cases ("City name, zip code, or 'lat,lng'"). - Keep schemas strict.
"additionalProperties": falseand arequiredlist reduce malformed arguments. - Design tools to compose. A
search_products→get_product_details→check_inventorychain reads naturally to a model that already understands each tool's purpose.
Errors specific to tool calling
| Situation | What you get |
|---|---|
tools sent to a model/candidate that does not support it | Depends on the engine: usually the field is silently ignored and the model answers as if it were not sent |
Malformed arguments JSON from the model | Not caught by us — validate before you JSON.parse it |
You forget to send tools again on the follow-up call | The model may ask for the same tool again, or answer without using the earlier result |
See Errors and debugging for the general error envelope.
Dernière mise à jour : 5 sept. 2026