GPT-6 Astra function calling is the engine of AI agents: the model decides which tool to invoke and what arguments to pass; your runtime executes the side effect; the result feeds back into the model for continued reasoning. When this loop is correct, Astra can autonomously query APIs, read databases, and execute custom logic. When it is wrong, the loop deadlocks, tool permissions exceed what was intended, or the entire agent is locked to Chat Completions and cannot reach hosted tools. This article does not repeat what Function Calling is — that belongs in the protocol comparison — and does not repeat how to obtain an API key — that is the API tutorial. This article covers the three core tool patterns you need to go from "it runs" to "it is safely deployable."
Step 1: Define a tool schema
A tool schema is a JSON object that tells the model what the tool is called, what it does, and what parameters it accepts. Astra supports type: function; parameters use standard JSON Schema. The example below defines a tool that calls a weather HTTP API:
import json
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city via HTTP API.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. Tokyo, London, Shanghai",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default celsius.",
},
},
"required": ["city"],
"additionalProperties": False,
},
}
]
A few authoring guidelines. The description should say what the tool does, not just what it is named. The model reads this field to decide whether to invoke the tool. List all required fields under required; optional fields go in properties but not in required. Setting additionalProperties: false reduces the chance of the model passing unexpected keys. Multiple tools go in the same list; the model picks one or more based on the conversation. When the model calls multiple tools in a single turn, your loop must handle parallel tool results before continuing.
Step 2: The full tool loop
The tool loop follows a fixed pattern: send request → check output for function_call items → execute → append results to input → send again, repeating until no new function_call items appear. The loop must be a while True. Assuming a single round is a common bug because the model is allowed to call tools across multiple turns before producing a final text answer.
import json
import requests
from openai import OpenAI
client = OpenAI()
def call_weather_api(city: str, unit: str = "celsius") -> dict:
"""Replace with your real HTTP endpoint."""
url = "https://api.example.com/weather"
r = requests.get(url, params={"city": city, "unit": unit}, timeout=5)
r.raise_for_status()
return r.json() # e.g. {"city": "Tokyo", "temp": 22, "condition": "sunny"}
# Initial request — model may call tools
response = client.responses.create(
model="gpt-6-astra",
input="What is the weather in Tokyo right now?",
tools=tools,
reasoning={"effort": "low"},
)
# Tool loop: keep going until model stops calling tools
while True:
tool_calls = [o for o in response.output if o.type == "function_call"]
if not tool_calls:
break # model is done — final answer is in response.output_text
tool_results = []
for call in tool_calls:
args = json.loads(call.arguments)
if call.name == "get_weather":
result = call_weather_api(**args)
else:
result = {"error": f"Unknown tool: {call.name}"}
tool_results.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result, ensure_ascii=False),
})
# Append history + results and continue
response = client.responses.create(
model="gpt-6-astra",
input=response.output + tool_results,
tools=tools,
reasoning={"effort": "low"},
)
print(response.output_text)
Several implementation details matter. First, response.output is a list that may contain both function_call items and message items. Filter for type == "function_call" only. Second, when continuing the conversation, pass response.output + tool_results as input. Sending only tool_results loses the conversation history, and the model cannot understand what question it was answering. Third, call_id in each result must match the call_id in the corresponding tool call; the platform validates this pairing. Fourth, serialize tool return values with json.dumps to a string. Do not pass raw Python dicts as the output field.
name field in model output against a known allowlist before dispatching. Validate argument types. For HTTP tools, add a timeout and a domain allowlist. For database tools, execute only pre-defined SQL statements with parameterized values — never concatenate model output into a query string. Any tool that writes to disk or runs shell commands belongs on a remote, disposable Mac rather than a shared desktop.
Step 3: A read-only database tool
A safe database tool rests on three properties: read-only credentials (no INSERT, UPDATE, DELETE), parameterized queries (no SQL injection), and a SQL allowlist (the model cannot construct arbitrary queries). The example below uses SQLite, but the pattern applies to PostgreSQL and MySQL: store allowed query names and their fixed SQL strings in a dict; the tool executes only what is in that dict and accepts only the parameters the caller provides.
import sqlite3
from typing import Any
# Only these parameterized queries can be executed — no raw user SQL
ALLOWED_QUERIES: dict[str, str] = {
"get_order": "SELECT id, status, total, created_at FROM orders WHERE id = ?",
"list_orders": "SELECT id, status, total FROM orders WHERE user_id = ? LIMIT 20",
}
def query_db(query_name: str, params: list[Any]) -> list[dict]:
"""Execute an allowlisted, parameterized read-only query.
Never accepts raw SQL strings from the model.
"""
sql = ALLOWED_QUERIES.get(query_name)
if sql is None:
raise ValueError(f"Query '{query_name}' not in allowlist")
con = sqlite3.connect("orders.db", check_same_thread=False)
con.row_factory = sqlite3.Row
try:
rows = con.execute(sql, params).fetchall()
return [dict(r) for r in rows]
finally:
con.close()
# Schema exposed to the model
db_tool = {
"type": "function",
"name": "query_db",
"description": "Query the orders DB. Only read-only allowlisted queries.",
"parameters": {
"type": "object",
"properties": {
"query_name": {
"type": "string",
"enum": list(ALLOWED_QUERIES.keys()),
"description": "Name of the allowlisted query",
},
"params": {
"type": "array",
"items": {"type": ["string", "number"]},
"description": "Positional parameters for the query",
},
},
"required": ["query_name", "params"],
},
}
In production you will also need a connection pool (do not connect and close per tool call), a query timeout (either via the database driver's timeout argument or a database-level setting), a hard row-limit baked into every SQL statement (the model should not be able to retrieve 100,000 rows), and masking of sensitive columns (phone numbers and email addresses before they enter the model's context). The tool result sent back to the model should be a summary or a small slice of data, not a raw table dump. Every row returned costs input tokens on the next request.
Function tools versus hosted tools
Astra supports both tool types. The selection logic is different for each, and confusing them leads to either unnecessary development overhead or hitting the wrong execution boundary.
| Dimension | Function tools (type: function) | Hosted tools (OpenAI-operated) |
|---|---|---|
| Execution location | Your runtime (local machine or remote Mac) | OpenAI infrastructure |
| Best for | Private APIs, internal databases, custom business logic | Web search, file search, code interpreter, shell, computer use |
| Development effort | You write the schema, the execution function, and the loop | Name the tool in the tools list; OpenAI handles execution |
| Billing | Tokens only (input + output) | Tokens plus per-call tool fees (search per call, computer use per hour) |
| Isolation requirement | Write-capable tools need an isolated execution environment | OpenAI handles sandbox isolation; you still govern data access |
| Capability boundary | Arbitrary custom logic; constrained by runtime permissions | Governed by OpenAI policy; cannot reach private networks |
The two types can be mixed in the same request. You can pass function tool schemas alongside a hosted web search tool in the same tools list; the model decides which to call based on the conversation. When mixing, note that hosted tool results flow back into the conversation automatically. You do not submit function_call_output for them. Your tool loop only needs to handle the function_call items for the function tools you defined.
Decision matrix
| If you need to | Use this | Why |
|---|---|---|
| Call an internal REST API | Function tool | Private networks are not reachable from OpenAI infrastructure |
| Query an internal database (read-only) | Function tool + SQL allowlist | Private data; you control access credentials and row-level permissions |
| Search the public web for live information | Hosted web_search_preview | OpenAI crawls and ranks; no infrastructure maintenance on your side |
| Let the model run Python code | Hosted code_interpreter | Executes in an OpenAI sandbox; does not consume your compute |
| Let the model drive a desktop or browser | Hosted computer_use + isolated Mac | Computer Use needs a visible desktop; isolation prevents accidental writes |
| Execute arbitrary shell commands (writes disk) | Function tool + remote Mac isolation | Write permissions must live on a disposable host, not a shared desktop |
| Mix web search with private API calls | Hosted web_search + function tool combined | Both types can coexist in a single request's tools list |
Recommended stacks
A — Solo developer or prototype: local machine, function tools targeting HTTP APIs (no disk writes), schema written and loop verified with call_id logging. Enable hosted tools (web_search is the most common first addition) only after the function tool loop runs reliably. Add a second tool only after the single-tool loop is stable. Do not attempt parallel tool calls until the sequential loop is correct.
B — Small team or product: function tools mounted behind internal services with token authentication; database tools using read-only credentials, a connection pool, and query timeouts; hosted tools enabled per task type (web_search for research, code_interpreter for data analysis). Tool execution logic covered by unit tests; tool return values schema-validated before feeding the model. Write-capable scenarios isolated on a remote Mac node.
C — Enterprise or high-security deployment: all function tool calls proxied through an API gateway with audit logging; database tools pointing to a read-only replica with row-level security enforced at the database layer; computer use sessions on disposable isolated Mac nodes that are destroyed at session end; hosted tool data-retention policy matched to account agreement. Account and delivery details live in the help center; monthly node cost is on the Mac mini pricing page.
Common pitfalls
- Running the tool loop only once: using
ifinstead ofwhilemeans the model's first tool call ends the loop. The final text answer never arrives. - Submitting results without the conversation history: passing only
tool_resultsasinputinstead ofresponse.output + tool_resultsloses context. The model does not know what it was asked, and usually errors or answers something unrelated. - Passing model output directly into SQL or shell: even when the model is well-behaved, prompt injection through user data can construct dangerous statements. An allowlist with parameterized queries eliminates the class of attack.
- No timeout on tool HTTP calls: a downstream service that hangs will freeze the entire agent session indefinitely. Every external call needs a
timeoutand the loop needs a maximum iteration count. - Returning large payloads from tools: tool results enter the model context. Returning 10,000 rows or a full file dumps into the prompt and can trigger the 272K whole-request billing uplift. Aggregate and truncate at the tool layer; send a summary, not a raw dump.
Action plan: 7 steps
- Confirm the plain-text Responses path works first (see the API tutorial). Tools build on this; skipping it adds confusion.
- Write the first function tool schema. Make the description a verb phrase that says what the tool does. List required fields in
required. SetadditionalProperties: false. - Implement the tool execution function. Add a
timeoutfor HTTP tools. For database tools, use the allowlist and parameterized query pattern from this article. - Send a Responses request with the
toolsparameter. Printresponse.outputto confirm afunction_callitem appears. - Implement the
while Trueloop. Submit results asfunction_call_outputitems with matchingcall_ids, passing full history as input. Exit when no newfunction_callitems appear. - Decide whether to add hosted tools (web_search, code_interpreter). If so, include them in the same
toolslist. Remember that hosted tool results are injected automatically; you do not need to submit them. - Move any tool that writes to disk, executes shell commands, or drives a browser to an isolated remote Mac. Destroy the workspace when the session ends.
FAQ
Do function tools require the Responses API?
Yes. Tool calls, hosted tools, and async tool loops require the Responses API. Chat Completions is not the primary path for Astra function calling; a hosted tool on a Completions request returns HTTP 400.
How many tools can I define per request?
There is no strict small limit, but more tools consume more prompt tokens. Pass only the tools relevant to the current task rather than loading the entire tool library on every request.
How do I prevent SQL injection in a database tool?
Combine parameterized queries using ? placeholders with a fixed SQL allowlist. The tool executes only pre-defined statements; the caller supplies the parameter values. Never concatenate model output directly into a SQL string.
When should I use a function tool versus a hosted tool?
Private APIs, internal databases, and custom logic use function tools. Web search, code interpretation, and computer use are hosted tools operated by OpenAI. Both types can coexist in the same request's tools list.
Can I stream the final answer after tool calls?
Yes. Tool call events are structured; after submitting results the final text answer can be streamed by setting stream=True. Verify the non-streaming loop works correctly before enabling streaming.
Conclusion
GPT-6 Astra Function Calling has three foundations: a schema with a clear description and a complete parameters block; a while loop that submits full history with each tool result; and a database tool locked to a SQL allowlist with parameterized queries, with write-capable tools on an isolated Mac. Choosing the right tool type — function tool for private systems, hosted tool for OpenAI-operated capabilities — moves an agent from prototype to production-ready. Remote Mac nodes are listed on the rental page and pricing page; account issues go to the help center.
Further reading
- How to call the GPT-6 Astra API: Python from key to first request →
- What is Function Calling: OpenAI, Gemini, Claude compared →
- What is GPT-6 Astra: release, pricing, and agents →
Tools touching real APIs? Run them on an isolated Cloud Mac
Calling a public HTTP API locally is fine. Anything that writes disk, edits a repo, or opens a browser should not share your daily laptop. Remote Mac nodes isolate a workspace per session so Astra function tools can go from working to safely deployable.