#308·gpt-oss

Duplicate call_id silently rebinds function_call_output to the wrong function in the rendered prompt

Author: AUTHENSORCreated Sep 5, 2026Updated Sep 5, 2026

Duplicate call_id silently rebinds function_call_output to the wrong function in the rendered prompt

Affected: openai/gpt-oss@7b58334 (gpt_oss/responses_api/api_server.py)

Summary

When a request input contains two function_call items that share one call_id but target different functions, the tool outputs are bound to whichever function was registered last under that id, regardless of which call each output actually answers. The server renders the conversation shown to the model with the wrong function attribution and drops the pairing entirely for the losing function. The binding is positional over a silently overwritten dict, not content bound, and no validation or error is raised for the duplicate id.

Code

In gpt_oss/responses_api/api_server.py inside the /v1/responses handler:

python
function_call_map = {}                    # line 1263
...
elif item.type == "function_call":        # line 1299
    function_call_map[item.call_id] = item    # line 1300, silent overwrite
    messages.append(... .with_recipient(f"functions.{item.name}") ...)
elif item.type == "function_call_output": # line 1306
    function_call = function_call_map.get(item.call_id, None)
    ...
    messages.append(
        Message.from_author_and_content(
            Author.new(Role.TOOL, f"functions.{function_call.name}"),  # line 1313
            item.output,
        )
        ...
    )

There is no uniqueness check on call_id and no ambiguity error, so the last function_call item registered under an id receives every output sent for that id.

Reproduction

Run poc/poc_1_call_binding.py (see poc/run_all.sh). It drives the real FastAPI app from create_api_server with the same stub-token approach the repo's own tests use, and captures the exact prompt tokens handed to the model.

  • Control arm (distinct ids): the prompt renders <|start|>functions.get_weather to=assistant<|channel|>commentary<|message|>WEATHER_RESULT 21C sunny and the matching delete_file result, correctly paired.

  • Attack arm B (shared id, outputs after both calls): the prompt renders the weather result under the delete_file recipient:

    <|start|>functions.delete_file to=assistant<|channel|>commentary<|message|>WEATHER_RESULT 21C sunny

    The get_weather call receives no result at all and delete_file receives two.

  • Attack arm C (identical call/output pairs, interleaved order): the same output renders under get_weather, purely because of its position relative to the duplicate registrations.

Evidence from a byte-identical double run: attack_B_misbinds_weather_to_delete_file: true, same_pairs_different_order_different_attribution: true. (byte-identical double run; verdicts: attack_B_misbinds_weather_to_delete_file: true, same_pairs_different_order_different_attribution: true).

Impact

A client that reuses call ids across different functions (per-turn counters such as call_0, merged histories from two assistant turns, or history replay combined with previous_response_id, which concatenates stored output items with newly supplied input items) gets a conversation in which tool results are attributed to the wrong tool. The model then reasons over a fabricated pairing, for example treating a destructive call as having returned the other tool's payload, with no error surfaced to either side.

Suggested fix

Reject or disambiguate duplicate call ids when building the input (ValueError/422, matching the existing behavior for an output with an unknown call id), or key the tool message on the call item the output immediately follows rather than a last-writer-wins map.