Duplicate call_id silently rebinds function_call_output to the wrong function in the rendered prompt
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:
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 sunnyand 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 sunnyThe 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.
Source: openai/gpt-oss