Streamed Chat Completions turns report response_id '__fake_id__' instead of None, and it leaks into previous_response_id and compaction
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 77/100
Hướng nghiên cứu
Bắt đầu với phần tạo phản hồi dạng streaming trong src/agents/models/openai_chatcompletions.py và cách ChatCmplStreamHandler phát ra response.completed; so sánh với luồng phản hồi không streaming. Kiểm tra các bài kiểm thử về ID phản hồi của Chat Completions dạng streaming và bổ sung kiểm thử cho hành vi với None, bao gồm cách xử lý ID phản hồi trước đó hoặc việc nén ở các bước downstream. Hoàn thành khi Chat Completions dạng streaming báo cáo response_id=None và không chuyển ID giữ chỗ xuống các bước downstream.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Describe the bug
When an agent runs on OpenAIChatCompletionsModel with Runner.run_streamed, the turn's ModelResponse.response_id is the SDK's own placeholder "__fake_id__". The same agent with Runner.run gets response_id=None. So result.last_response_id and result.raw_responses[i].response_id come back as "__fake_id__" only in streaming mode.
The rest of the runner treats None as "this provider has no server response id", so the placeholder slips past those checks. I hit two follow-on effects.
-
With
auto_previous_response_id=True(or an explicitprevious_response_id), a handoff from a Responses agent to a Chat Completions agent and back replaces the realresp_...id in the chain with"__fake_id__". The next Responses request sendsprevious_response_id="__fake_id__". In non-streamed mode the tracker keepsresp_A1. -
With
OpenAIResponsesCompactionSession, post-write compaction calls/responses/compactwithprevious_response_id="__fake_id__", an id the server never issued. In the repro the mocked 400 for that request comes out of the streamed run. In non-streamed mode compaction is skipped because the id isNone, and the run succeeds.
I know previous_response_id and compaction are documented as Responses features, so 1 and 2 are mixed setups. The last_response_id difference between streamed and non-streamed runs shows up with a plain Chat Completions agent though.
From reading the code, LiteLLM and AnyLLM streaming build the same placeholder response and go through the same path. I only ran the OpenAI Chat Completions model.
Debug information
- Agents SDK version: main at cd26bbb (the same line is in v0.24.0)
- Related library versions (optional, e.g.
any-llm,litellm, orpydantic): openai 3.29.0 - Python version: 3.14.4
- Operating system: macOS
- Model and model provider:
OpenAIChatCompletionsModel(LiteLLM and AnyLLM models look affected by code reading) - Does the issue reproduce with the latest Agents SDK release? I ran it on main. v0.24.0 has the same code.
- Does the issue occur consistently or intermittently? Consistently
Output of the script below. The two warnings at the top come from the handoff case and are expected.
OpenAIChatCompletionsModel does not support server-managed conversation state (previous_response_id). Chat Completions requires callers to pass the full conversation history; use a Responses API model for previous_response_id or a conversation-capable model for conversation_id. Ignoring unsupported server-managed conversation state; enable strict feature validation to raise an error instead.
OpenAIChatCompletionsModel does not support server-managed conversation state (previous_response_id). Chat Completions requires callers to pass the full conversation history; use a Responses API model for previous_response_id or a conversation-capable model for conversation_id. Ignoring unsupported server-managed conversation state; enable strict feature validation to raise an error instead.
case_last_response_id
run last_response_id=None
run_streamed last_response_id='__fake_id__'
case_compaction
run ok final_output='hi', compact previous_response_id sent=[]
run_streamed raised BadRequestError, compact previous_response_id sent=['__fake_id__']
case_handoff_chain
run second Responses request previous_response_id='resp_A1'
run_streamed second Responses request previous_response_id='__fake_id__'
Repro steps
Self-contained, no API key needed. All HTTP goes to an httpx2.MockTransport (the HTTP client the current openai package uses). The compaction mock answers with a 400 invalid_request_error, standing in for the real endpoint rejecting an id it never issued.
import asyncio
import json
import httpx2 # the HTTP library the current openai package is built on
from openai import AsyncOpenAI
from agents import Agent, Runner, SQLiteSession, set_tracing_disabled
from agents.memory import OpenAIResponsesCompactionSession
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from agents.models.openai_responses import OpenAIResponsesModel
set_tracing_disabled(True)
def mock_client(replies, seen):
def handler(request):
body = json.loads(request.content or b"{}")
seen.append((request.url.path, body))
status, ctype, content = replies.pop(0)(body)
return httpx2.Response(status, headers={"content-type": ctype}, content=content)
transport = httpx2.MockTransport(handler)
return AsyncOpenAI(
api_key="test",
base_url="http://test/v1",
http_client=httpx2.AsyncClient(transport=transport),
max_retries=0,
)
def sse(events, named=False):
out = ""
for e in events:
if named:
out += f"event: {e['type']}\n"
out += f"data: {json.dumps(e)}\n\n"
if not named:
out += "data: [DONE]\n\n"
return out.encode()
def chat_reply(message, finish, streamed):
if not streamed:
body = {"id": "chatcmpl-1", "object": "chat.completion", "created": 1, "model": "m",
"choices": [{"index": 0, "message": message, "finish_reason": finish}]}
return lambda b: (200, "application/json", json.dumps(body).encode())
delta = dict(message)
if "tool_calls" in delta:
delta["tool_calls"] = [{"index": 0, **tc} for tc in delta["tool_calls"]]
chunks = [
{"id": "chatcmpl-1", "object": "chat.completion.chunk", "created": 1, "model": "m",
"choices": [{"index": 0, "delta": delta, "finish_reason": None}]},
{"id": "chatcmpl-1", "object": "chat.completion.chunk", "created": 1, "model": "m",
"choices": [{"index": 0, "delta": {}, "finish_reason": finish}]},
]
return lambda b: (200, "text/event-stream", sse(chunks))
def responses_reply(output, rid, streamed):
r = {"id": rid, "object": "response", "created_at": 0, "model": "m", "status": "completed",
"output": output, "tool_choice": "auto", "tools": [], "parallel_tool_calls": True}
if not streamed:
return lambda b: (200, "application/json", json.dumps(r).encode())
events = [{"type": "response.created", "sequence_number": 0,
"response": {**r, "status": "in_progress", "output": []}}]
for i, item in enumerate(output):
events.append({"type": "response.output_item.done", "sequence_number": i + 1,
"output_index": i, "item": item})
events.append({"type": "response.completed", "sequence_number": len(output) + 1, "response": r})
return lambda b: (200, "text/event-stream", sse(events, named=True))
async def run(agent, streamed, **kwargs):
if streamed:
result = Runner.run_streamed(agent, "hi", **kwargs)
async for _ in result.stream_events():
pass
return result
return await Runner.run(agent, "hi", **kwargs)
HI = {"role": "assistant", "content": "hi"}
async def case_last_response_id(streamed):
client = mock_client([chat_reply(HI, "stop", streamed)], [])
agent = Agent(name="a", model=OpenAIChatCompletionsModel(model="m", openai_client=client))
result = await run(agent, streamed)
return f"last_response_id={result.last_response_id!r}"
async def case_compaction(streamed):
def compact_400(body):
err = {"error": {"message": f"Invalid 'previous_response_id': "
f"'{body.get('previous_response_id')}'.",
"type": "invalid_request_error", "param": "previous_response_id"}}
return 400, "application/json", json.dumps(err).encode()
compact_seen = []
session = OpenAIResponsesCompactionSession(
"s", SQLiteSession("s"), client=mock_client([compact_400], compact_seen),
model="gpt-4.1", should_trigger_compaction=lambda ctx: True,
)
client = mock_client([chat_reply(HI, "stop", streamed)], [])
agent = Agent(name="a", model=OpenAIChatCompletionsModel(model="m", openai_client=client))
try:
result = await run(agent, streamed, session=session)
out = f"ok final_output={result.final_output!r}"
except Exception as e:
out = f"raised {type(e).__name__}"
sent = [b.get("previous_response_id") for _, b in compact_seen]
return f"{out}, compact previous_response_id sent={sent}"
async def case_handoff_chain(streamed):
to_b = {"type": "function_call", "id": "fc_1", "call_id": "call_1",
"name": "transfer_to_b", "arguments": "{}", "status": "completed"}
done = {"type": "message", "id": "msg_1", "role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": "done", "annotations": []}]}
to_a = {"role": "assistant", "content": None, "tool_calls": [
{"id": "call_2", "type": "function",
"function": {"name": "transfer_to_a", "arguments": "{}"}}]}
a_seen = []
a_client = mock_client([responses_reply([to_b], "resp_A1", streamed),
responses_reply([done], "resp_A2", streamed)], a_seen)
b_client = mock_client([chat_reply(to_a, "tool_calls", streamed)], [])
a = Agent(name="a", model=OpenAIResponsesModel(model="m", openai_client=a_client))
b = Agent(name="b", model=OpenAIChatCompletionsModel(model="m", openai_client=b_client))
a.handoffs = [b]
b.handoffs = [a]
await run(a, streamed, auto_previous_response_id=True)
return f"second Responses request previous_response_id={a_seen[1][1].get('previous_response_id')!r}"
async def main():
for case in (case_last_response_id, case_compaction, case_handoff_chain):
print(case.__name__)
for streamed in (False, True):
label = " run_streamed" if streamed else " run "
print(label, await case(streamed))
asyncio.run(main())
Expected behavior
A streamed Chat Completions turn should report response_id=None, the same as the non-streamed turn. Then last_response_id is None, a previous_response_id chain keeps the last real resp_... id, and compaction is not attempted with a made-up id.
Root cause
For streaming, the Chat Completions model builds a placeholder Response(id=FAKE_RESPONSES_ID, ...) (openai_chatcompletions.py#L770, same in litellm_model.py#L669 and any_llm_model.py#L982), and ChatCmplStreamHandler copies it into the response.completed event (chatcmpl_stream_handler.py#L1349).
run_single_turn_streamed then passes that id straight through (run_loop.py#L2694).
final_response = ModelResponse(
output=terminal_response.output,
usage=usage,
response_id=terminal_response.id,
...
The non-streamed path returns response_id=None (openai_chatcompletions.py#L381, litellm_model.py#L375). The downstream code only checks for None, in the conversation tracker (oai_conversation.py#L386-L392 and the resume path at L223-L228) and in the compaction gate (session_persistence.py#L730). So the placeholder gets treated as a real server id.
The SDK already ignores FAKE_RESPONSES_ID for item ids (_normalize_server_item_id in oai_conversation.py#L37), just not for response ids.
Suggested fix
Map the placeholder to None where the streamed ModelResponse is built.
response_id=(
None if terminal_response.id == FAKE_RESPONSES_ID else terminal_response.id
),
That covers all three adapters in one place.
- Ngôn ngữ chính
- Python
- Star
- 30.4k
- Fork
- 4.9k
- Merge trung bình
- 15 giờ 42 phút
- Pull request đã merge (30 ngày)
- 124
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của openai/openai-agents-python
-
fix(typing): explicitly specialize ToolContext[Any] in ToolFunctionWithToolContextCó thể làm lại được @AbarnaaSree đã nhận 12 ngày trước và không có pull request nào đang mở. Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
openai/openai-agents-python#5387 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
openai/openai-agents-python#5453 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 53/100
openai/openai-agents-python#5450 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
function_tool rejects valid arguments when a parameter uses Pydantic strict mode (Enum, date, UUID)Có thể đã có người làm @Lahari25 đã nhận 1 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 65/100
openai/openai-agents-python#5448 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 65/100
openai/openai-agents-python#5447 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của openai/openai-agents-python
Issue tương tự
-
Admin member of a research group cannot see the HX2 allocation until they've been added to itĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
Maintainer thường phản hồi trong vòng 2 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
developmentseed/titiler-cmr#278 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 2 ngày