Tool calling
Give models functions to call, run them in your code, and send the results back.
Tool calling lets the model ask your code to do something: look up a CVE, run a scan against your lab, read a file. You describe the tools, the model decides when to call one and with which arguments, you run it, and you send the result back. It works on every model and every protocol, with the request shapes you already know.
OpenAI tools
Describe each tool with a name, a description, and a JSON Schema for its arguments. tool_choice is "auto" by default; set it to "required" to force a call, "none" to forbid one, or name a specific function.
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://xfield.cloud.brutal.ai/api/v1", api_key=os.environ["XFIELD_API_KEY"])
tools = [
{
"type": "function",
"function": {
"name": "lookup_cve",
"description": "Get the summary, CVSS score, and affected versions for a CVE id.",
"parameters": {
"type": "object",
"properties": {"cve_id": {"type": "string", "description": "For example CVE-2024-3094"}},
"required": ["cve_id"],
},
},
}
]
def lookup_cve(cve_id: str) -> dict:
# Call your own vulnerability database here.
return {"id": cve_id, "cvss": 10.0, "summary": "Backdoor in xz/liblzma 5.6.0 and 5.6.1"}
messages = [{"role": "user", "content": "Is CVE-2024-3094 something I should patch today?"}]
# 1. The model decides to call the tool.
response = client.chat.completions.create(
model="deepseek-v4.1-flash-1017-uncensored", messages=messages, tools=tools, tool_choice="auto"
)
assistant = response.choices[0].message
messages.append(assistant)
# 2. Run each call and send the results back.
for call in assistant.tool_calls or []:
args = json.loads(call.function.arguments)
result = lookup_cve(**args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
# 3. The model answers with the results in hand.
final = client.chat.completions.create(model="deepseek-v4.1-flash-1017-uncensored", messages=messages, tools=tools)
print(final.choices[0].message.content)Appending the assistant message as-is keeps its tool_calls and its reasoning in the history, which the model needs to pick up where it left off. See Reasoning.
The model may call several tools in one turn. Run them all and send one tool message per call before you ask for the next response. Set parallel_tool_calls: false if you want one call at a time.
Anthropic tools
On /messages, tools use Anthropic's shape: input_schema instead of parameters, tool_use blocks in the response, and tool_result blocks in your reply.
import os
import anthropic
client = anthropic.Anthropic(base_url="https://xfield.cloud.brutal.ai/api", api_key=os.environ["XFIELD_API_KEY"])
tools = [
{
"name": "lookup_cve",
"description": "Get the summary, CVSS score, and affected versions for a CVE id.",
"input_schema": {
"type": "object",
"properties": {"cve_id": {"type": "string"}},
"required": ["cve_id"],
},
}
]
messages = [{"role": "user", "content": "Is CVE-2024-3094 something I should patch today?"}]
response = client.messages.create(
model="deepseek-v4.1-flash-1017-uncensored", max_tokens=4000, tools=tools, messages=messages
)
if response.stop_reason == "tool_use":
call = next(block for block in response.content if block.type == "tool_use")
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": call.id,
"content": '{"cvss": 10.0, "summary": "Backdoor in xz/liblzma 5.6.0 and 5.6.1"}',
}],
})
response = client.messages.create(
model="deepseek-v4.1-flash-1017-uncensored", max_tokens=4000, tools=tools, messages=messages
)
print(response.content[-1].text)tool_choice works too: {"type": "auto"}, {"type": "any"}, or {"type": "tool", "name": "lookup_cve"}.
Tips
- Keep tool definitions stable. Prompt caching is automatic and works on the start of the request. Put the system prompt and tools first, and do not reorder or reword them between calls. In long agent runs, this is the biggest single lever on cost and latency.
- Write descriptions for the model. Say what the tool does, when to use it, and what the arguments look like. A good description beats a clever name.
- Validate arguments. Arguments arrive as JSON text that matches your schema almost always, not always. Parse defensively and return a clear error message as the tool result when they do not fit; the model will usually correct itself.
- Return compact results. Large tool outputs fill the context quickly. Trim logs and scan output to what matters.
- Coding agents already do this. Claude Code, Codex, OpenCode, Cline, and Aider manage the tool loop for you. Point them at XField and they work as usual. See Claude Code setup.
- Streaming works with tools. Tool call arguments stream as deltas, in
delta.tool_callson Chat Completions andinput_json_deltaevents on Messages.