A correct message in response to a tool call cannot validate as ChatCompletionMessage

In response to a tool call we need to create the following message:

{
      "tool_call_id": tool_call.id,
      "role": "tool",
      "name": function_name,
      "content": function_response,
}

This is copied from https://platform.openai.com/docs/guides/function-calling under Example invoking multiple function calls in one response and this seems to work.
But this structure does not validate as a ChatCompletionMessage:

from openai.types.chat.chat_completion import ChatCompletionMessage

message_dict = {
     "tool_call_id": 'tool_call.id',
    "role": "tool",
    "name": 'function_name',
    "content": 'function_response',
}

message = ChatCompletionMessage(**message_dict)

Output:

Traceback (most recent call last):
  File "/home/zby/gpt/answerbot/tmp/ChatCompletionMessage_example.py", line 10, in <module>
    message = ChatCompletionMessage(**message_dict)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zby/gpt/answerbot/.venv/lib/python3.11/site-packages/pydantic/main.py", line 175, in __init__
    self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 1 validation error for ChatCompletionMessage
role
  Input should be 'assistant' [type=literal_error, input_value='tool', input_type=str]
    For further information visit https://errors.pydantic.dev/2.7/v/literal_error

If I change the role to ‘assistant’ - then it seems to work.

That is because you aren’t using it in the same manner as is presented in the example.

This is invalid,

message = ChatCompletionMessage(**message_dict)

This is what the documentation does,

tool_calls = response_message.tool_calls
    # Step 2: check if the model wanted to call a function
    if tool_calls:
        # Step 3: call the function
        # Note: the JSON response may not always be valid; be sure to handle errors
        available_functions = {
            "get_current_weather": get_current_weather,
        }  # only one function in this example, but you can have multiple
        messages.append(response_message)  # extend conversation with assistant's reply
        # Step 4: send the info for each function call and function response to the model
        for tool_call in tool_calls:
            function_name = tool_call.function.name
            function_to_call = available_functions[function_name]
            function_args = json.loads(tool_call.function.arguments)
            function_response = function_to_call(
                location=function_args.get("location"),
                unit=function_args.get("unit"),
            )
            messages.append(
                {
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": function_response,
                }
            )  # extend conversation with function response
        second_response = client.chat.completions.create(
            model="gpt-3.5-turbo-0125",
            messages=messages,
        )  # get a new response from the model where it can see the function response
        return second_response

You need to use the .append method to add the function response message to the existing messages.

That uses dictionary to parameter unpacking, a perfectly valid method of passing a JSON-like Python object into a function as parameters.

It uses one of “types” from the Python library, something I haven’t messed with, because a python list is just fine.

However, the input to the API chat.competions function is ALL parameters of the API call, not just the messages. Things like the model name, temperature, the tool specification, etc. Of which the messages (a list) is just one more thing that must be present.

I use that convention in my big demo of returning the assistant message with a tool call and the paired tool call return with matching IDs.

What I wrote is a linear example with fixed strings to be as demonstrative as possible. You must have the parsing that looks for multiple parallel tools, and adaptively provide a list of all of them in the return. That’s the purpose of the “for tool_call” in the code sample above.

Also, the endpoint will give you an error if you don’t have both “assistant” and “tool”, with matching IDs and length.

Sorry @_j, I’ll be more clear.

This is invalid,

message = ChatCompletionMessage(**message_dict)

With respect to the OpenAI Python module given the content which the OP has demonstrated is contained within the message_dict.

Specifically, using a role of tool.

Is there anything else you want to be needlessly pedantic about right now?

I can be Pydantic.

print(json.dumps(typ.chat.chat_completion.ChatCompletionMessage.model_json_schema(), indent=1))`

{
 "$defs": {
  "ChatCompletionMessageToolCall": {
   "additionalProperties": true,
   "properties": {
    "id": {
     "title": "Id",
     "type": "string"
    },
    "function": {
     "$ref": "#/$defs/Function"
    },
    "type": {
     "const": "function",
     "title": "Type"
    }
   },
   "required": [
    "id",
    "function",
    "type"
   ],
   "title": "ChatCompletionMessageToolCall",
   "type": "object"
  },
  "Function": {
   "additionalProperties": true,
   "properties": {
    "arguments": {
     "title": "Arguments",
     "type": "string"
    },
    "name": {
     "title": "Name",
     "type": "string"
    }
   },
   "required": [
    "arguments",
    "name"
   ],
   "title": "Function",
   "type": "object"
  },
  "FunctionCall": {
   "additionalProperties": true,
   "properties": {
    "arguments": {
     "title": "Arguments",
     "type": "string"
    },
    "name": {
     "title": "Name",
     "type": "string"
    }
   },
   "required": [
    "arguments",
    "name"
   ],
   "title": "FunctionCall",
   "type": "object"
  }
 },
 "additionalProperties": true,
 "properties": {
  "content": {
   "anyOf": [
    {
     "type": "string"
    },
    {
     "type": "null"
    }
   ],
   "default": null,
   "title": "Content"
  },
  "role": {
   "const": "assistant",
   "title": "Role"
  },
  "function_call": {
   "anyOf": [
    {
     "$ref": "#/$defs/FunctionCall"
    },
    {
     "type": "null"
    }
   ],
   "default": null
  },
  "tool_calls": {
   "anyOf": [
    {
     "items": {
      "$ref": "#/$defs/ChatCompletionMessageToolCall"
     },
     "type": "array"
    },
    {
     "type": "null"
    }
   ],
   "default": null,
   "title": "Tool Calls"
  }
 },
 "required": [
  "role"
 ],
 "title": "ChatCompletionMessage",
 "type": "object"
}

From types, this object class appears to be for capturing the response from the API. That’s the reason anything but “assistant” doesn’t validate. Accompanying this level in the typing hierarchy are schemas such as “ChatCompletionTokenLogprob”, which is certainly not an input to the API.

Conclusion: don’t mess with their types without documentation. If of unknown purpose, they could be hit with unanticipated changes. Then, just one more thing to patch when you go to get OFF the OpenAI platform…

The code from the example works. I wanted to make it more robust and use and object for the message instead of a raw dictionary. I found a class that seems to be designed to contain the message data structure. But alas not really - as was mentioned somewhere else in this thread - ChatCompletionMessage is meant to be a message from the server and as a message created by other means it works only sometimes. This is rather surprising.

And to answer some other points in this thread:

  1. I did not try to create a ChatCompletion which has fields for model name, temperature etc. ChatCompletionMessage is a different class.
  2. I still wanted to use messages.append, I just wanted to change the parameter of that call from a raw dictionary to an object.