openai.BadRequestError: 400 - {'error': {'message': "Invalid 'messages[1].tool_calls': empty array. Expected at least one item, got empty.", 'type': 'invalid_request_error', 'param': 'messages[1].tool_calls', 'code': 'empty_array'}}

Hi,
When I execute the following code

import openai

client = openai.OpenAI()

messages = [
    {"role": "user", "content": "What color is the sky?"},
]

completion = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=messages,
)

print(completion.choices[0].message)
messages.append(completion.choices[0].message)
messages.append(
    {"role": "user", "content": "What color is the ocean?"},
)

completion = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
)

print(completion.choices[0].message)

It results in:

openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid 'messages[1].tool_calls': empty array. Expected an array with minimum length 1, but got an empty array instead.", 'type': 'invalid_request_error', 'param': 'messages[1].tool_calls', 'code': 'empty_array'}}

It was raised in `client.beta.chat.completions.parse` returns `tool_calls` with empty array, which is invalid for message history · Issue #2061 · openai/openai-python · GitHub but it seems to be an API-specific issue, so I am raising this issue here as well.

While there is a difference between the object produced between beta…parse() and .create(), you must also consider that this cannot be sent back to the AI as an unaltered message.

>>>completion.choices[0].message.model_dump()

{
  'content': 'Blue!!!',
  'refusal': None,
  'role': 'assistant',
  'audio': None,
  'function_call': None,
  'tool_calls': [],
  'parsed': None
}
  • If you had specified a response_format BaseModel, then you would be getting back parsed that cannot be sent to chat completions;
  • If the AI denied you, you may get back refusal and no content, which cannot be sent to chat completions;
  • If the AI model spoke 'audio': '(base64)", then this cannot be sent back to chat completions.

etc.

if not completion.choices[0].message.tool_calls:
    completion.choices[0].message.tool_calls = None

would fix up that one key. Or a “keep what’s valid”:

history_message = {
    k: v for k, v in vars(completion.choices[0].message).items()
    if k in ['content', 'role', 'function_call', 'tool_calls']
    and v not in [None, []]
}

Which would still fail if you don’t copy a “refusal” into empty “content”.