Skip to content

Commit 46594bc

Browse files
committed
fix(openai): filter duplicate function_call items when using previous_response_id
When using the Responses API with previous_response_id, the delta input_chat_ctx includes function_call items from the previous LLM response. The server already knows about these from the referenced response, so sending them again causes each tool call to appear twice in the API logs. Filter out function_call items from the serialized input when previous_response_id is set, keeping only function_call_output items which are genuinely new. Fixes #5136
1 parent d4079b6 commit 46594bc

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/responses/llm.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ async def _run_impl(self) -> None:
381381
self._response_completed = False
382382
chat_ctx, _ = self._chat_ctx.to_provider_format(format="openai.responses")
383383

384+
# When using previous_response_id, the server already has the
385+
# function_call items from that response. Sending them again
386+
# causes each tool call to appear twice in the API logs.
387+
if "previous_response_id" in self._extra_kwargs:
388+
chat_ctx = [
389+
item
390+
for item in chat_ctx
391+
if not (isinstance(item, dict) and item.get("type") == "function_call")
392+
]
393+
384394
self._tool_ctx = llm.ToolContext(self.tools)
385395
tool_schemas = cast(
386396
list[ToolParam],

tests/test_chat_ctx.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,3 +658,47 @@ def test_instructions_as_modality():
658658
turn2_ctx = turn1_ctx.copy()
659659
apply_instructions_modality(turn2_ctx, modality="audio")
660660
assert str(turn2_ctx.items[0].content[0]) == "audio instructions"
661+
662+
663+
def test_responses_chat_ctx_excludes_function_calls_with_previous_response_id():
664+
"""When previous_response_id is set, function_call items from the previous
665+
response are already known to the server. Sending them again causes
666+
duplicated tool calls in the API logs (issue #5136)."""
667+
chat_ctx = ChatContext()
668+
chat_ctx.add_message(role="user", content="What is the weather?")
669+
chat_ctx.insert(
670+
FunctionCall(
671+
call_id="call_abc",
672+
name="get_weather",
673+
arguments='{"city": "SF"}',
674+
)
675+
)
676+
chat_ctx.insert(
677+
FunctionCallOutput(
678+
call_id="call_abc",
679+
name="get_weather",
680+
output='{"temp": 72}',
681+
is_error=False,
682+
)
683+
)
684+
685+
items, _ = chat_ctx.to_provider_format(format="openai.responses")
686+
687+
# Unfiltered output should contain a function_call item
688+
fc_items = [i for i in items if isinstance(i, dict) and i.get("type") == "function_call"]
689+
fco_items = [
690+
i for i in items if isinstance(i, dict) and i.get("type") == "function_call_output"
691+
]
692+
assert len(fc_items) == 1, "expected one function_call item in unfiltered output"
693+
assert len(fco_items) == 1, "expected one function_call_output item"
694+
695+
# Simulate the filter applied in LLMStream._run_impl when
696+
# previous_response_id is present
697+
filtered = [i for i in items if not (isinstance(i, dict) and i.get("type") == "function_call")]
698+
699+
fc_after = [i for i in filtered if isinstance(i, dict) and i.get("type") == "function_call"]
700+
fco_after = [
701+
i for i in filtered if isinstance(i, dict) and i.get("type") == "function_call_output"
702+
]
703+
assert len(fc_after) == 0, "function_call items should be removed"
704+
assert len(fco_after) == 1, "function_call_output items should be preserved"

0 commit comments

Comments
 (0)