The essential problem: OpenAI has completely damaged and ruined the API documentation for the Chat Completions endpoint. There used to be a toggle on each page that was easy to miss, but that is gone. The responses endpoint can only be called “foisted on you”.
What Documentation has: This “input” parameter is only used by Responses
# 3. Execute the function logic for get_horoscope
result = {"horoscope": get_horoscope(function_call_arguments["sign"])}
# 4. Provide function call results to the model
input_list.append({
"type": "function_call_output",
"call_id": function_call.call_id,
"output": json.dumps(result),
})
Then, OpenAI treats how functions work internally like some kind of secret, not disclosing that to the AI, there are only named “tools” from the start, and that “functions” is one type of tool where you can place your own descriptions of multiple code paths your app can respond with, and all the other “tools” are for internal use only.
Here is a complete demonstration, with a foundation written in 2023 with the introduction of “tools” as a parameter replacing “functions”. “tools” now is a container for functions, but with nothing else you can send on Chat Completions. You can either call tools with a question that stimulates parallel tool calls, or run with a built-in return demonstration.
Copy this into a code editor IDE to get a better view. Run in a Jupyter notebook or REPL environment, and all the variables will be globals you can inspect after it runs.
"""
demo: assembling a multi-step function-call
walkthrough into one runnable Python file.
This file shows how to:
- declare tool specs (functions the model may call)
- prepare the chat messages (system/user)
- see how the AI emits a tool call to a function
- uses *parallel tool calls*
- optionally replay a prior assistant message
and the subsequent tool returns (controlled by the
global RETURN_A_TOOL_RESPONSE flag) and see a final answer.
- perform a chat completion call that uses
function/tool parameters and message construction.
- demonstrate how to parse a "with_raw_response"
response and extract headers, choices, usage, and
tool_calls. (headers have rate limits, etc)
"""
import json
from openai import OpenAI
# Toggle this to include prior assistant/tool
# responses in the outgoing messages.
RETURN_A_TOOL_RESPONSE = False
# Initialize the official client instance.
client = OpenAI() # automatic OPENAI_API_KEY retrieval from env var
"""
Tool specification section:
We're enumerating tools (with functions) the assistant may
call. Each entry follows the function-calling JSON
schema: name, description, and JSON schema for
parameters. Description helps the model decide when and how
to use the tool.
"""
toolspec = []
toolspec.extend(
[
{
"type": "function",
"function": {
"name": "get_weather_forecast",
"description": (
"Get weather forecast. AI can make multiple"
" tool calls in one response."
),
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. 'Seattle, WA'.",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": (
"Temperature unit to return. 'celsius' or"
" 'fahrenheit'."
),
},
"time_period": {
"type": "number",
"description": "Length in days or portions.",
},
},
# required keys must match properties above
"required": ["location", "format", "time_period"],
},
},
}
]
)
"""
Base messages construction:
We always include a 'system' instruction and a
'user' request. This forms the starting point for the
assistant's reasoning about whether to call functions.
"""
base_messages = [
{
"role": "system",
"content": "You are a helpful AI assistant.",
},
{
"role": "user",
"content": (
"How hot will it be today in Seattle? And in Miami?"
" Use multi-tool to get both at the same time."
),
},
]
# Assemble the parameter dictionary template with model.
params = {
"model": "gpt-5-mini",
# 'tools' is the function schema list that the model
# may call during completion (function-calling).
"tools": toolspec,
# We'll provide 'messages' below after conditional
# augmentation depending on RETURN_A_TOOL_RESPONSE.
"messages": None, # Added at end
"max_completion_tokens": 5000, # must be big
"reasoning_effort": "low",
}
"""
Optional replay of assistant and tool outputs:
This shows also adding a past assistant function call
and the return result of parallel functions. Also note
the pattern of the AI also writing to the user. If the
global flag is False, we send only the user prompt.
If True, we append the assistant "tool_calls" object
and two 'tool' role messages representing tool returns.
"""
# Start with a shallow copy of base messages.
outgoing_messages = list(base_messages)
if RETURN_A_TOOL_RESPONSE:
# The assistant previously emitted a message that
# invoked two function calls (multi-tool).
assistant_emitted = {
"role": "assistant",
"content": "Let me look up the weather in those cities "
"for you...",
# 'tool_calls' is a demo field used to show the
# assistant's function call history.
"tool_calls": [
{
"id": "call_rygjilssMBx8JQGUgEo7QqeY",
"type": "function",
"function": {
"name": "get_weather_forecast",
# arguments serialized as a JSON string
"arguments": (
"{\"location\": \"Seattle\", "
"\"format\": \"fahrenheit\", "
"\"time_period\": 1}"
),
},
},
{
"id": "call_pI6vxWtSMU5puVBHNm5nJhw3",
"type": "function",
"function": {
"name": "get_weather_forecast",
"arguments": (
"{\"location\": \"Miami\", "
"\"format\": \"fahrenheit\", "
"\"time_period\": 1}"
),
},
},
],
}
outgoing_messages.append(assistant_emitted)
# Simulated tool return messages. Each has role 'tool'
# and a 'tool_call_id' linking it to the call above.
tool_return_seattle = {
"role": "tool",
"tool_call_id": "call_rygjilssMBx8JQGUgEo7QqeY",
"content": (
"Seattle 2022-12-15 forecast: high 62, low 42, "
"partly cloudy\n"
),
}
tool_return_miami = {
"role": "tool",
"tool_call_id": "call_pI6vxWtSMU5puVBHNm5nJhw3",
"content": "Miami 2022-12-15 forecast: high 77, low 66, sunny\n",
}
outgoing_messages.append(tool_return_seattle)
outgoing_messages.append(tool_return_miami)
# Finalize the messages into params for the API call.
params["messages"] = outgoing_messages
"""
Execute the chat completion call:
We use SDK's 'with_raw_response' to show how to extract
response headers alongside the JSON payload.
Wrapped in try/except to handle network or API errors.
Streaming and iterating over stream chunks not in demo.
"""
c = None
try:
c = client.chat.completions.with_raw_response.create(
**params
)
except Exception as e:
print(f"Error: {e}")
# If we received a raw response, parse out common fields.
if c:
# Extract headers into globals for demo purposes.
try:
headers_dict = c.headers.items().mapping.copy()
except Exception:
# Fallback if headers structure differs.
try:
headers_dict = dict(c.headers.items())
except Exception:
headers_dict = {}
for key, value in headers_dict.items():
variable_name = f'headers_{key.replace("-", "_")}'
globals()[variable_name] = value
# This line demonstrates that headers_* globals exist.
# (It will raise if the header key is missing.)
# remains = headers_x_ratelimit_remaining_tokens
# Parse the JSON body from the raw response bytes.
try:
api_return_dict = json.loads(c.content.decode())
except Exception:
api_return_dict = {}
# Extract typical fields demonstratively (if present).
api_choice = {}
if api_return_dict.get("choices"):
api_choice = api_return_dict.get("choices")[0]
api_finish_str = api_choice.get("finish_reason")
usage_dict = api_return_dict.get("usage")
api_message_dict = api_choice.get("message", {})
api_message_str = api_message_dict.get("content")
api_tools_list = api_message_dict.get("tool_calls")
# Print the assistant's message if present.
if api_message_str:
print(api_message_str)
# If the model included 'tool_calls', pretty-print them.
if api_tools_list:
for tool_item in api_tools_list:
print(json.dumps(tool_item, indent=2))
"""
Example AI output (for demonstration):
Here are the weather forecasts for today:
- Seattle: High 62°F, Low 42°F, Partly Cloudy
- Miami: High 77°F, Low 66°F, Sunny
"""