Skip to content

Responses API: sending a client-authored assistant turn requires either a fake message id or a # type: ignore #3544

Description

@adubovik

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

Conversation histories don't always come back from the API. Assistant turns can be replayed
from storage, edited before being sent again, produced by other code, or carried over from
somewhere else. A turn like that has no OpenAI message id, because no API call ever produced
one. I'd like to pass it in input and have the request types support that.

The only version the types accept makes me fill in an id and a status I don't have:

conversation: ResponseInputParam = [
    {"role": "user", "content": [{"type": "input_text", "text": "2+3=?"}]},
    {
        "type": "message",
        "id": "msg_fake_msg_id",      # <-- made up
        "status": "completed",        # <-- made up
        "role": "assistant",
        "content": [
            {"type": "output_text", "text": "5", "annotations": []},
            {"type": "output_text", "text": "something else?", "annotations": []},
        ],
    },
    {"role": "user", "content": [{"type": "input_text", "text": "what's the square of the result?"}]},
]

The id is the part that worries me, since ids plausibly take part in prompt caching. To be
safe about it I'd have to invent a scheme that derives the same synthetic id for the same
conversation prefix every time and keep that stable across versions. That's real work whose
only purpose is keeping the type checker quiet.

There are two other ways to write the same turn. Neither needs an id, and they fail in
opposite directions.

The first one just leaves id and status out, and the API is fine with it: the request goes
through and comes back with a normal answer (checked against the live endpoint, openai 2.48.0).
Only the types object, which suggests those two fields are the SDK's requirement rather than
the service's:

    {
        "type": "message",
        "role": "assistant",
        "content": [
            {"type": "output_text", "text": "5", "annotations": []},  # type: ignore
            {"type": "output_text", "text": "something else?", "annotations": []},  # type: ignore
        ],
    },

Writing # type: ignore (or cast()) to send a payload the API accepts happily defeats the
point of having typed requests in the first place.

The second one type-checks with nothing suppressed, then fails on the server:

    {
        "role": "assistant",
        "content": [
            {"type": "input_text", "text": "5"},
            {"type": "input_text", "text": "something else?"},
        ],
    },
{"error": {"message": "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
           "type": "invalid_request_error", "param": "input[1].content[0]", "code": "invalid_value"}}

So out of the three, the one that both type-checks and works is the one with made-up values in
it.

Expected behavior

Some way to express a client-authored assistant turn that type-checks and doesn't require
inventing field values.

Related issues

The id requirement also came up in #2501, and #3015 proposes relaxing it, though status is
required as well so the workarounds above would still be needed. #2323 looks adjacent.

To Reproduce

Pass each of the three snippets above to client.responses.create(model=..., input=conversation)
with a type checker enabled. In order: the first needs the made-up id and status, the second
needs # type: ignore, the third type-checks and then returns the 400 above.

Code snippets

.env:

OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5

app.py:

import os

from dotenv import load_dotenv
from openai import OpenAI
from openai.types.responses import ResponseInputParam

load_dotenv()

client = OpenAI()


def create_conversation(option: int) -> ResponseInputParam:
    if option == 1:
        return [
            {
                "role": "user",
                "content": [{"type": "input_text", "text": "2+3=?"}],
            },
            {
                "type": "message",
                "id": "msg_fake_msg_id",
                "status": "completed",
                "role": "assistant",
                "content": [
                    {"type": "output_text", "text": "5", "annotations": []},
                    {
                        "type": "output_text",
                        "text": "something else?",
                        "annotations": [],
                    },
                ],
            },
            {
                "role": "user",
                "content": [
                    {"type": "input_text", "text": "what's the square of the result?"}
                ],
            },
        ]

    if option == 2:
        return [
            {
                "role": "user",
                "content": [{"type": "input_text", "text": "2+3=?"}],
            },
            {
                "type": "message",
                "role": "assistant",
                "content": [
                    {"type": "output_text", "text": "5", "annotations": []},  # type: ignore
                    {"type": "output_text", "text": "something else?", "annotations": []},  # type: ignore
                ],
            },
            {
                "role": "user",
                "content": [
                    {"type": "input_text", "text": "what's the square of the result?"}
                ],
            },
        ]

    return [
        {
            "role": "user",
            "content": [{"type": "input_text", "text": "2+3=?"}],
        },
        {
            "role": "assistant",
            "content": [
                {"type": "input_text", "text": "5"},
                {"type": "input_text", "text": "something else?"},
            ],
        },
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "what's the square of the result?"}
            ],
        },
    ]


for i in [1, 2, 3]:
    response = client.responses.create(
        model=os.environ["OPENAI_MODEL"],
        input=create_conversation(i),
    )
    print(response.output_text)

OS

macOS 14.6

Python version

3.11.13

Library version

openai 2.48.0 (checked with pyright 1.1.411)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions