Most backend integrations with LLMs start as a raw HTTP call before someone wraps it in a SDK. The following curl function calling tool use examples show the exact wire format for tool use against any OpenAI-compatible /v1/chat/completions endpoint, so you can debug, test fallbacks, or script agentic flows without language-specific libraries.
Step 1: Export credentials and choose a base URL
Set your API key and target endpoint in the shell. If you hit OpenAI directly, use its base URL. If you want one endpoint that fronts many providers, an OpenAI-compatible gateway like n4n.ai accepts this identical payload and will route to an available provider, honoring the same tool schemas and cache-control hints.
export OPENAI_API_KEY="sk-your-key"
export BASE_URL="https://api.openai.com/v1"
# Alternative: export BASE_URL="https://api.n4n.ai/v1"
Keep the key out of your process list by using env vars or a secrets file. Never paste secrets into shared curl snippets.
Step 2: Define a tool schema
Tool use requires a JSON Schema description of each function. The model uses this to decide whether to call the tool and what arguments to pass. Write the schema to tools.json:
[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current temperature for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City, state, or country"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}
}
]
The top-level tools array holds one or more objects. Each must declare type: "function" and a function block with name, description, and parameters. Strict schema validation happens server-side; missing required fields will cause the request to fail.
Step 3: Send the first chat completion request
Build the request body with jq so you can inject the tool schema cleanly. This avoids escaping headaches in bash.
jq -n --slurpfile tools tools.json '{
model: "gpt-4o-mini",
messages: [
{role: "user", content: "What is the weather in Paris?"}
],
tools: $tools,
tool_choice: "auto"
}' > payload.json
curl -s "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
--data @payload.json | tee response1.json
tool_choice: "auto" lets the model decide. To force a specific function, use {"type":"function","function":{"name":"get_weather"}}. The response is saved to response1.json for inspection.
Step 4: Parse the model’s tool call
When the model wants to call a tool, the assistant message contains a tool_calls array instead of content. Extract it:
jq '.choices[0].message.tool_calls' response1.json
Expected shape:
[
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"Paris\",\"unit\":\"celsius\"}"
}
}
]
Note arguments is a JSON string, not an object. Pull the name and arguments separately:
NAME=$(jq -r '.choices[0].message.tool_calls[0].function.name' response1.json)
ARGS=$(jq -r '.choices[0].message.tool_calls[0].function.arguments' response1.json)
echo "Model asked for $NAME with $ARGS"
If tool_calls is null, the model answered directly and you can read content.
Step 5: Execute the tool and return the result
Run your actual function server-side. Here is a minimal Python stub that mimics a weather API:
import json, sys
args = json.loads(sys.argv[1])
# In production, call your real service here
result = {"temperature": 21, "unit": args.get("unit", "celsius"), "location": args["location"]}
print(json.dumps(result))
Capture its output and build the follow-up request. You must echo back the assistant message (with tool_calls) and append a role: "tool" message referencing the tool_call_id.
RESULT=$(python3 fake_weather.py "$ARGS")
jq -n --slurpfile t1 response1.json --arg result "$RESULT" '{
model: "gpt-4o-mini",
messages: [
{role: "user", content: "What is the weather in Paris?"},
($t1[0].choices[0].message | {role, content, tool_calls}),
{
role: "tool",
tool_call_id: ($t1[0].choices[0].message.tool_calls[0].id),
content: $result
}
]
}' > payload2.json
curl -s "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
--data @payload2.json | jq '.choices[0].message'
The final assistant message should now incorporate the tool result, e.g., “The temperature in Paris is 21°C.”
Step 6: Handle parallel calls and forced selection
Models can emit multiple tool_calls in one turn. Loop over them in your executor:
jq -r '.choices[0].message.tool_calls[] | .id + " " + .function.name + " " + .function.arguments' response1.json \
| while read -r id name args; do
echo "Exec $name ($id): $args"
done
To guarantee a tool runs (useful for guarded workflows), set tool_choice explicitly in the first request:
{
"tool_choice": {
"type": "function",
"function": {"name": "get_weather"}
}
}
The model will then always return a get_weather call, even if the prompt is ambiguous.
Step 7: Verify success
Success means three things: the first response contained a valid tool_calls entry, your tool message was accepted without a 400, and the second response finish_reason is stop with natural language that uses the returned data.
# Check first response has tool call
jq -e '.choices[0].message.tool_calls | length > 0' response1.json && echo "TOOL CALLED OK"
# Check second response finished cleanly
jq -e '.choices[0].finish_reason == "stop"' payload2.response.json 2>/dev/null || \
curl -s "$BASE_URL/chat/completions" -H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" --data @payload2.json | jq '.choices[0].finish_reason'
If you see finish_reason: "tool_calls" on the second call, the model wants another round—chain the loop. For production, cap iterations to avoid infinite tool cascades.
Caveats when scripting tool use with curl
The content field of an assistant message that contains tool_calls is null in the OpenAI schema. Do not try to concatenate strings into it; preserve the exact tool_calls object from the API response when echoing back.
Tool IDs must match between the assistant message and the tool result. Mismatches cause a 400 with invalid_tool_call_id.
When using an OpenAI-compatible gateway, the same tools array works across providers, but individual model support for parallel calls or forced tool_choice varies. Test against the specific model you pin.
Streaming complicates parsing: tool_calls arrive as deltas with index fields. For curl debugging, non-streaming is simpler and sufficient to validate your schema and round-trip logic before adding a streaming client.
These curl function calling tool use examples cover the full round trip. Once the shape is confirmed, port the exact JSON to your language SDK or gateway middleware without guessing at field names.