Issue
Include "tools": [] in the exact same request to Responses vs Chat Completions - significantly different billing.
Expected:
- identical context window placements
- identical billing
- an empty tools list treated as inert and no context additions
Symptom:
- 96 tokens are being added on Chat Completions with an empty tools list with gpt 5 models such as gpt-5.6 and gpt-5.4-mini. Potentially 96 tokens of useless distracting injection.
- 12 tokens are being added on gpt-4.1, “input_tokens”: 26 → “prompt_tokens”: 38
This does not occur on Responses.
Comparative API calls:
- Responses -
ASSISTANT MESSAGE
helloUSAGE
{ "input_tokens": 25, "input_tokens_details": { "cache_write_tokens": 0, "cached_tokens": 0 }, "output_tokens": 5, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 30 }- Chat Completions -
ASSISTANT MESSAGE
helloUSAGE
{ "prompt_tokens": 121, "completion_tokens": 4, "total_tokens": 125, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }
Replication Python script
import json
import os
import httpx
def post_json(url, payload):
response = httpx.post(
url,
headers={
"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
},
json=payload,
timeout=120.0,
)
if response.is_error:
raise RuntimeError(
f"HTTP {response.status_code}\n"
f"{response.text}\n"
f"x-request-id: {response.headers.get('x-request-id')}"
)
return response.json()
def output_items(response):
"""Responses API extractor for just text events for user display"""
for item in response.get("output", []):
if item.get("type") == "reasoning":
for part in item.get("summary", []):
if part.get("type") == "summary_text":
yield "reasoning summary", part["text"]
elif item.get("type") == "message" and item.get("role") == "assistant":
for part in item.get("content", []):
if part.get("type") == "output_text":
yield "assistant message", part["text"]
elif part.get("type") == "refusal":
yield "assistant refusal", part["refusal"]
def chat_output_items(response):
"""Chat Completions API extractor for just text for user display"""
for choice in response.get("choices", []):
message = choice.get("message", {})
if message.get("role") == "assistant":
if message.get("refusal") is not None:
yield "assistant refusal", message["refusal"]
elif message.get("content") is not None:
yield "assistant message", message["content"]
MODEL = "gpt-5.6-luna"
DEVELOPER_TEXT = (
"Fulfill user needs in a friendly multi-turn chat setting."
)
USER_TEXT = "Say only hello"
responses_payload = {
"model": MODEL,
"input": [
{
"type": "message",
"role": "developer",
"content": [
{
"type": "input_text",
"text": DEVELOPER_TEXT,
},
],
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": USER_TEXT,
},
],
},
],
"store": False,
"tools": [],
}
chat_completions_payload = {
"model": MODEL,
"messages": [
{
"role": "developer",
"content": [
{
"type": "text",
"text": DEVELOPER_TEXT,
},
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": USER_TEXT,
},
],
},
],
"store": False,
"tools": [],
}
print("## - Responses - ##")
response = post_json(
"https://api.openai.com/v1/responses",
responses_payload,
)
for label, text in output_items(response):
print(f"\n{label.upper()}\n{text}")
print("\nUSAGE")
print("```\n" + json.dumps(response.get("usage", {}), indent=2) + "\n```")
print("## - Chat Completions - ##")
chat_response = post_json(
"https://api.openai.com/v1/chat/completions",
chat_completions_payload,
)
for label, text in chat_output_items(chat_response):
print(f"\n{label.upper()}\n{text}")
print("\nUSAGE")
print("```\n" + json.dumps(chat_response.get("usage", {}), indent=2) + "\n```")
Then and additionally: there is no need to block developer functions on gpt-5.4+ on Chat Completions. Functions can only be used with reasoning_effort:“none”, which degrades the output further. Because OpenAI couldn’t adapt “phase”?