openai-agents 0.2.8__py3-none-any.whl → 0.6.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. agents/__init__.py +105 -4
  2. agents/_debug.py +15 -4
  3. agents/_run_impl.py +1203 -96
  4. agents/agent.py +164 -19
  5. agents/apply_diff.py +329 -0
  6. agents/editor.py +47 -0
  7. agents/exceptions.py +35 -0
  8. agents/extensions/experimental/__init__.py +6 -0
  9. agents/extensions/experimental/codex/__init__.py +92 -0
  10. agents/extensions/experimental/codex/codex.py +89 -0
  11. agents/extensions/experimental/codex/codex_options.py +35 -0
  12. agents/extensions/experimental/codex/codex_tool.py +1142 -0
  13. agents/extensions/experimental/codex/events.py +162 -0
  14. agents/extensions/experimental/codex/exec.py +263 -0
  15. agents/extensions/experimental/codex/items.py +245 -0
  16. agents/extensions/experimental/codex/output_schema_file.py +50 -0
  17. agents/extensions/experimental/codex/payloads.py +31 -0
  18. agents/extensions/experimental/codex/thread.py +214 -0
  19. agents/extensions/experimental/codex/thread_options.py +54 -0
  20. agents/extensions/experimental/codex/turn_options.py +36 -0
  21. agents/extensions/handoff_filters.py +13 -1
  22. agents/extensions/memory/__init__.py +120 -0
  23. agents/extensions/memory/advanced_sqlite_session.py +1285 -0
  24. agents/extensions/memory/async_sqlite_session.py +239 -0
  25. agents/extensions/memory/dapr_session.py +423 -0
  26. agents/extensions/memory/encrypt_session.py +185 -0
  27. agents/extensions/memory/redis_session.py +261 -0
  28. agents/extensions/memory/sqlalchemy_session.py +334 -0
  29. agents/extensions/models/litellm_model.py +449 -36
  30. agents/extensions/models/litellm_provider.py +3 -1
  31. agents/function_schema.py +47 -5
  32. agents/guardrail.py +16 -2
  33. agents/{handoffs.py → handoffs/__init__.py} +89 -47
  34. agents/handoffs/history.py +268 -0
  35. agents/items.py +237 -11
  36. agents/lifecycle.py +75 -14
  37. agents/mcp/server.py +280 -37
  38. agents/mcp/util.py +24 -3
  39. agents/memory/__init__.py +22 -2
  40. agents/memory/openai_conversations_session.py +91 -0
  41. agents/memory/openai_responses_compaction_session.py +249 -0
  42. agents/memory/session.py +19 -261
  43. agents/memory/sqlite_session.py +275 -0
  44. agents/memory/util.py +20 -0
  45. agents/model_settings.py +14 -3
  46. agents/models/__init__.py +13 -0
  47. agents/models/chatcmpl_converter.py +303 -50
  48. agents/models/chatcmpl_helpers.py +63 -0
  49. agents/models/chatcmpl_stream_handler.py +290 -68
  50. agents/models/default_models.py +58 -0
  51. agents/models/interface.py +4 -0
  52. agents/models/openai_chatcompletions.py +103 -49
  53. agents/models/openai_provider.py +10 -4
  54. agents/models/openai_responses.py +162 -46
  55. agents/realtime/__init__.py +4 -0
  56. agents/realtime/_util.py +14 -3
  57. agents/realtime/agent.py +7 -0
  58. agents/realtime/audio_formats.py +53 -0
  59. agents/realtime/config.py +78 -10
  60. agents/realtime/events.py +18 -0
  61. agents/realtime/handoffs.py +2 -2
  62. agents/realtime/items.py +17 -1
  63. agents/realtime/model.py +13 -0
  64. agents/realtime/model_events.py +12 -0
  65. agents/realtime/model_inputs.py +18 -1
  66. agents/realtime/openai_realtime.py +696 -150
  67. agents/realtime/session.py +243 -23
  68. agents/repl.py +7 -3
  69. agents/result.py +197 -38
  70. agents/run.py +949 -168
  71. agents/run_context.py +13 -2
  72. agents/stream_events.py +1 -0
  73. agents/strict_schema.py +14 -0
  74. agents/tool.py +413 -15
  75. agents/tool_context.py +22 -1
  76. agents/tool_guardrails.py +279 -0
  77. agents/tracing/__init__.py +2 -0
  78. agents/tracing/config.py +9 -0
  79. agents/tracing/create.py +4 -0
  80. agents/tracing/processor_interface.py +84 -11
  81. agents/tracing/processors.py +65 -54
  82. agents/tracing/provider.py +64 -7
  83. agents/tracing/spans.py +105 -0
  84. agents/tracing/traces.py +116 -16
  85. agents/usage.py +134 -12
  86. agents/util/_json.py +19 -1
  87. agents/util/_transforms.py +12 -2
  88. agents/voice/input.py +5 -4
  89. agents/voice/models/openai_stt.py +17 -9
  90. agents/voice/pipeline.py +2 -0
  91. agents/voice/pipeline_config.py +4 -0
  92. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
  93. openai_agents-0.6.8.dist-info/RECORD +134 -0
  94. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
  95. openai_agents-0.2.8.dist-info/RECORD +0 -103
  96. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import AsyncIterator
4
4
  from dataclasses import dataclass, field
5
+ from typing import Any
5
6
 
6
7
  from openai import AsyncStream
7
8
  from openai.types.chat import ChatCompletionChunk
@@ -28,14 +29,21 @@ from openai.types.responses import (
28
29
  ResponseTextDeltaEvent,
29
30
  ResponseUsage,
30
31
  )
31
- from openai.types.responses.response_reasoning_item import Summary
32
+ from openai.types.responses.response_reasoning_item import Content, Summary
32
33
  from openai.types.responses.response_reasoning_summary_part_added_event import (
33
34
  Part as AddedEventPart,
34
35
  )
35
36
  from openai.types.responses.response_reasoning_summary_part_done_event import Part as DoneEventPart
37
+ from openai.types.responses.response_reasoning_text_delta_event import (
38
+ ResponseReasoningTextDeltaEvent,
39
+ )
40
+ from openai.types.responses.response_reasoning_text_done_event import (
41
+ ResponseReasoningTextDoneEvent,
42
+ )
36
43
  from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
37
44
 
38
45
  from ..items import TResponseStreamEvent
46
+ from .chatcmpl_helpers import ChatCmplHelpers
39
47
  from .fake_id import FAKE_RESPONSES_ID
40
48
 
41
49
 
@@ -56,6 +64,11 @@ class StreamingState:
56
64
  # Fields for real-time function call streaming
57
65
  function_call_streaming: dict[int, bool] = field(default_factory=dict)
58
66
  function_call_output_idx: dict[int, int] = field(default_factory=dict)
67
+ # Store accumulated thinking text and signature for Anthropic compatibility
68
+ thinking_text: str = ""
69
+ thinking_signature: str | None = None
70
+ # Store provider data for all output items
71
+ provider_data: dict[str, Any] = field(default_factory=dict)
59
72
 
60
73
 
61
74
  class SequenceNumber:
@@ -74,7 +87,17 @@ class ChatCmplStreamHandler:
74
87
  cls,
75
88
  response: Response,
76
89
  stream: AsyncStream[ChatCompletionChunk],
90
+ model: str | None = None,
77
91
  ) -> AsyncIterator[TResponseStreamEvent]:
92
+ """
93
+ Handle a streaming chat completion response and yield response events.
94
+
95
+ Args:
96
+ response: The initial Response object to populate with streamed data
97
+ stream: The async stream of chat completion chunks from the model
98
+ model: The source model that is generating this stream. Used to handle
99
+ provider-specific stream processing.
100
+ """
78
101
  usage: CompletionUsage | None = None
79
102
  state = StreamingState()
80
103
  sequence_number = SequenceNumber()
@@ -88,31 +111,52 @@ class ChatCmplStreamHandler:
88
111
  )
89
112
 
90
113
  # This is always set by the OpenAI API, but not by others e.g. LiteLLM
91
- usage = chunk.usage if hasattr(chunk, "usage") else None
114
+ # Only update when chunk has usage data (not always in the last chunk)
115
+ if hasattr(chunk, "usage") and chunk.usage is not None:
116
+ usage = chunk.usage
92
117
 
93
118
  if not chunk.choices or not chunk.choices[0].delta:
94
119
  continue
95
120
 
96
- delta = chunk.choices[0].delta
121
+ # Build provider_data for non-OpenAI Responses API endpoints format
122
+ if model:
123
+ state.provider_data["model"] = model
124
+ elif hasattr(chunk, "model") and chunk.model:
125
+ state.provider_data["model"] = chunk.model
126
+
127
+ if hasattr(chunk, "id") and chunk.id:
128
+ state.provider_data["response_id"] = chunk.id
97
129
 
98
- # Handle reasoning content
130
+ delta = chunk.choices[0].delta
131
+ choice_logprobs = chunk.choices[0].logprobs
132
+
133
+ # Handle thinking blocks from Anthropic (for preserving signatures)
134
+ if hasattr(delta, "thinking_blocks") and delta.thinking_blocks:
135
+ for block in delta.thinking_blocks:
136
+ if isinstance(block, dict):
137
+ # Accumulate thinking text
138
+ thinking_text = block.get("thinking", "")
139
+ if thinking_text:
140
+ state.thinking_text += thinking_text
141
+ # Store signature if present
142
+ signature = block.get("signature")
143
+ if signature:
144
+ state.thinking_signature = signature
145
+
146
+ # Handle reasoning content for reasoning summaries
99
147
  if hasattr(delta, "reasoning_content"):
100
148
  reasoning_content = delta.reasoning_content
101
149
  if reasoning_content and not state.reasoning_content_index_and_output:
102
- state.reasoning_content_index_and_output = (
103
- 0,
104
- ResponseReasoningItem(
105
- id=FAKE_RESPONSES_ID,
106
- summary=[Summary(text="", type="summary_text")],
107
- type="reasoning",
108
- ),
150
+ reasoning_item = ResponseReasoningItem(
151
+ id=FAKE_RESPONSES_ID,
152
+ summary=[Summary(text="", type="summary_text")],
153
+ type="reasoning",
109
154
  )
155
+ if state.provider_data:
156
+ reasoning_item.provider_data = state.provider_data.copy() # type: ignore[attr-defined]
157
+ state.reasoning_content_index_and_output = (0, reasoning_item)
110
158
  yield ResponseOutputItemAddedEvent(
111
- item=ResponseReasoningItem(
112
- id=FAKE_RESPONSES_ID,
113
- summary=[Summary(text="", type="summary_text")],
114
- type="reasoning",
115
- ),
159
+ item=reasoning_item,
116
160
  output_index=0,
117
161
  type="response.output_item.added",
118
162
  sequence_number=sequence_number.get_and_increment(),
@@ -128,6 +172,12 @@ class ChatCmplStreamHandler:
128
172
  )
129
173
 
130
174
  if reasoning_content and state.reasoning_content_index_and_output:
175
+ # Ensure summary list has at least one element
176
+ if not state.reasoning_content_index_and_output[1].summary:
177
+ state.reasoning_content_index_and_output[1].summary = [
178
+ Summary(text="", type="summary_text")
179
+ ]
180
+
131
181
  yield ResponseReasoningSummaryTextDeltaEvent(
132
182
  delta=reasoning_content,
133
183
  item_id=FAKE_RESPONSES_ID,
@@ -138,10 +188,50 @@ class ChatCmplStreamHandler:
138
188
  )
139
189
 
140
190
  # Create a new summary with updated text
141
- current_summary = state.reasoning_content_index_and_output[1].summary[0]
142
- updated_text = current_summary.text + reasoning_content
143
- new_summary = Summary(text=updated_text, type="summary_text")
144
- state.reasoning_content_index_and_output[1].summary[0] = new_summary
191
+ current_content = state.reasoning_content_index_and_output[1].summary[0]
192
+ updated_text = current_content.text + reasoning_content
193
+ new_content = Summary(text=updated_text, type="summary_text")
194
+ state.reasoning_content_index_and_output[1].summary[0] = new_content
195
+
196
+ # Handle reasoning content from 3rd party platforms
197
+ if hasattr(delta, "reasoning"):
198
+ reasoning_text = delta.reasoning
199
+ if reasoning_text and not state.reasoning_content_index_and_output:
200
+ reasoning_item = ResponseReasoningItem(
201
+ id=FAKE_RESPONSES_ID,
202
+ summary=[],
203
+ content=[Content(text="", type="reasoning_text")],
204
+ type="reasoning",
205
+ )
206
+ if state.provider_data:
207
+ reasoning_item.provider_data = state.provider_data.copy() # type: ignore[attr-defined]
208
+ state.reasoning_content_index_and_output = (0, reasoning_item)
209
+ yield ResponseOutputItemAddedEvent(
210
+ item=reasoning_item,
211
+ output_index=0,
212
+ type="response.output_item.added",
213
+ sequence_number=sequence_number.get_and_increment(),
214
+ )
215
+
216
+ if reasoning_text and state.reasoning_content_index_and_output:
217
+ yield ResponseReasoningTextDeltaEvent(
218
+ delta=reasoning_text,
219
+ item_id=FAKE_RESPONSES_ID,
220
+ output_index=0,
221
+ content_index=0,
222
+ type="response.reasoning_text.delta",
223
+ sequence_number=sequence_number.get_and_increment(),
224
+ )
225
+
226
+ # Create a new summary with updated text
227
+ if not state.reasoning_content_index_and_output[1].content:
228
+ state.reasoning_content_index_and_output[1].content = [
229
+ Content(text="", type="reasoning_text")
230
+ ]
231
+ current_text = state.reasoning_content_index_and_output[1].content[0]
232
+ updated_text = current_text.text + reasoning_text
233
+ new_text_content = Content(text=updated_text, type="reasoning_text")
234
+ state.reasoning_content_index_and_output[1].content[0] = new_text_content
145
235
 
146
236
  # Handle regular content
147
237
  if delta.content is not None:
@@ -158,6 +248,7 @@ class ChatCmplStreamHandler:
158
248
  text="",
159
249
  type="output_text",
160
250
  annotations=[],
251
+ logprobs=[],
161
252
  ),
162
253
  )
163
254
  # Start a new assistant message stream
@@ -168,6 +259,8 @@ class ChatCmplStreamHandler:
168
259
  type="message",
169
260
  status="in_progress",
170
261
  )
262
+ if state.provider_data:
263
+ assistant_item.provider_data = state.provider_data.copy() # type: ignore[attr-defined]
171
264
  # Notify consumers of the start of a new output message + first content part
172
265
  yield ResponseOutputItemAddedEvent(
173
266
  item=assistant_item,
@@ -185,10 +278,20 @@ class ChatCmplStreamHandler:
185
278
  text="",
186
279
  type="output_text",
187
280
  annotations=[],
281
+ logprobs=[],
188
282
  ),
189
283
  type="response.content_part.added",
190
284
  sequence_number=sequence_number.get_and_increment(),
191
285
  )
286
+ delta_logprobs = (
287
+ ChatCmplHelpers.convert_logprobs_for_text_delta(
288
+ choice_logprobs.content if choice_logprobs else None
289
+ )
290
+ or []
291
+ )
292
+ output_logprobs = ChatCmplHelpers.convert_logprobs_for_output_text(
293
+ choice_logprobs.content if choice_logprobs else None
294
+ )
192
295
  # Emit the delta for this segment of content
193
296
  yield ResponseTextDeltaEvent(
194
297
  content_index=state.text_content_index_and_output[0],
@@ -198,10 +301,15 @@ class ChatCmplStreamHandler:
198
301
  is not None, # fixed 0 -> 0 or 1
199
302
  type="response.output_text.delta",
200
303
  sequence_number=sequence_number.get_and_increment(),
201
- logprobs=[],
304
+ logprobs=delta_logprobs,
202
305
  )
203
306
  # Accumulate the text into the response part
204
307
  state.text_content_index_and_output[1].text += delta.content
308
+ if output_logprobs:
309
+ existing_logprobs = state.text_content_index_and_output[1].logprobs or []
310
+ state.text_content_index_and_output[1].logprobs = (
311
+ existing_logprobs + output_logprobs
312
+ )
205
313
 
206
314
  # Handle refusals (model declines to answer)
207
315
  # This is always set by the OpenAI API, but not by others e.g. LiteLLM
@@ -225,6 +333,8 @@ class ChatCmplStreamHandler:
225
333
  type="message",
226
334
  status="in_progress",
227
335
  )
336
+ if state.provider_data:
337
+ assistant_item.provider_data = state.provider_data.copy() # type: ignore[attr-defined]
228
338
  # Notify downstream that assistant message + first content part are starting
229
339
  yield ResponseOutputItemAddedEvent(
230
340
  item=assistant_item,
@@ -236,12 +346,10 @@ class ChatCmplStreamHandler:
236
346
  yield ResponseContentPartAddedEvent(
237
347
  content_index=state.refusal_content_index_and_output[0],
238
348
  item_id=FAKE_RESPONSES_ID,
239
- output_index=state.reasoning_content_index_and_output
240
- is not None, # fixed 0 -> 0 or 1
241
- part=ResponseOutputText(
242
- text="",
243
- type="output_text",
244
- annotations=[],
349
+ output_index=(1 if state.reasoning_content_index_and_output else 0),
350
+ part=ResponseOutputRefusal(
351
+ refusal="",
352
+ type="refusal",
245
353
  ),
246
354
  type="response.content_part.added",
247
355
  sequence_number=sequence_number.get_and_increment(),
@@ -284,7 +392,60 @@ class ChatCmplStreamHandler:
284
392
  state.function_calls[tc_delta.index].name = tc_function.name
285
393
 
286
394
  if tc_delta.id:
287
- state.function_calls[tc_delta.index].call_id = tc_delta.id
395
+ # Clean up litellm's addition of __thought__ suffix to tool_call.id for
396
+ # Gemini models. See: https://github.com/BerriAI/litellm/pull/16895
397
+ # This suffix is redundant since we can get thought_signature from
398
+ # provider_specific_fields, and this hack causes validation errors when
399
+ # cross-model passing to other models.
400
+ tool_call_id = tc_delta.id
401
+ if model and "gemini" in model.lower() and "__thought__" in tool_call_id:
402
+ tool_call_id = tool_call_id.split("__thought__")[0]
403
+
404
+ state.function_calls[tc_delta.index].call_id = tool_call_id
405
+
406
+ # Initialize provider_data for this function call from state.provider_data
407
+ if not hasattr(state.function_calls[tc_delta.index], "provider_data"):
408
+ if state.provider_data:
409
+ state.function_calls[
410
+ tc_delta.index
411
+ ].provider_data = state.provider_data.copy() # type: ignore[attr-defined]
412
+
413
+ # Capture provider_specific_fields data from LiteLLM
414
+ if (
415
+ hasattr(tc_delta, "provider_specific_fields")
416
+ and tc_delta.provider_specific_fields
417
+ ):
418
+ # Handle Gemini thought_signatures
419
+ if model and "gemini" in model.lower():
420
+ provider_specific_fields = tc_delta.provider_specific_fields
421
+ if isinstance(provider_specific_fields, dict):
422
+ thought_sig = provider_specific_fields.get("thought_signature")
423
+ if thought_sig:
424
+ # Start with state.provider_data, then add thought_signature
425
+ func_provider_data = (
426
+ state.provider_data.copy() if state.provider_data else {}
427
+ )
428
+ func_provider_data["thought_signature"] = thought_sig
429
+ state.function_calls[
430
+ tc_delta.index
431
+ ].provider_data = func_provider_data # type: ignore[attr-defined]
432
+
433
+ # Capture extra_content data from Google's chatcmpl endpoint
434
+ if hasattr(tc_delta, "extra_content") and tc_delta.extra_content:
435
+ extra_content = tc_delta.extra_content
436
+ if isinstance(extra_content, dict):
437
+ google_fields = extra_content.get("google")
438
+ if google_fields and isinstance(google_fields, dict):
439
+ thought_sig = google_fields.get("thought_signature")
440
+ if thought_sig:
441
+ # Start with state.provider_data, then add thought_signature
442
+ func_provider_data = (
443
+ state.provider_data.copy() if state.provider_data else {}
444
+ )
445
+ func_provider_data["thought_signature"] = thought_sig
446
+ state.function_calls[
447
+ tc_delta.index
448
+ ].provider_data = func_provider_data # type: ignore[attr-defined]
288
449
 
289
450
  function_call = state.function_calls[tc_delta.index]
290
451
 
@@ -315,14 +476,28 @@ class ChatCmplStreamHandler:
315
476
  )
316
477
 
317
478
  # Send initial function call added event
479
+ func_call_item = ResponseFunctionToolCall(
480
+ id=FAKE_RESPONSES_ID,
481
+ call_id=function_call.call_id,
482
+ arguments="", # Start with empty arguments
483
+ name=function_call.name,
484
+ type="function_call",
485
+ )
486
+ # Merge provider_data from state and function_call (e.g. thought_signature)
487
+ if state.provider_data or (
488
+ hasattr(function_call, "provider_data") and function_call.provider_data
489
+ ):
490
+ merged_provider_data = (
491
+ state.provider_data.copy() if state.provider_data else {}
492
+ )
493
+ if (
494
+ hasattr(function_call, "provider_data")
495
+ and function_call.provider_data
496
+ ):
497
+ merged_provider_data.update(function_call.provider_data)
498
+ func_call_item.provider_data = merged_provider_data # type: ignore[attr-defined]
318
499
  yield ResponseOutputItemAddedEvent(
319
- item=ResponseFunctionToolCall(
320
- id=FAKE_RESPONSES_ID,
321
- call_id=function_call.call_id,
322
- arguments="", # Start with empty arguments
323
- name=function_call.name,
324
- type="function_call",
325
- ),
500
+ item=func_call_item,
326
501
  output_index=function_call_starting_index,
327
502
  type="response.output_item.added",
328
503
  sequence_number=sequence_number.get_and_increment(),
@@ -344,17 +519,30 @@ class ChatCmplStreamHandler:
344
519
  )
345
520
 
346
521
  if state.reasoning_content_index_and_output:
347
- yield ResponseReasoningSummaryPartDoneEvent(
348
- item_id=FAKE_RESPONSES_ID,
349
- output_index=0,
350
- summary_index=0,
351
- part=DoneEventPart(
352
- text=state.reasoning_content_index_and_output[1].summary[0].text,
353
- type="summary_text",
354
- ),
355
- type="response.reasoning_summary_part.done",
356
- sequence_number=sequence_number.get_and_increment(),
357
- )
522
+ if (
523
+ state.reasoning_content_index_and_output[1].summary
524
+ and len(state.reasoning_content_index_and_output[1].summary) > 0
525
+ ):
526
+ yield ResponseReasoningSummaryPartDoneEvent(
527
+ item_id=FAKE_RESPONSES_ID,
528
+ output_index=0,
529
+ summary_index=0,
530
+ part=DoneEventPart(
531
+ text=state.reasoning_content_index_and_output[1].summary[0].text,
532
+ type="summary_text",
533
+ ),
534
+ type="response.reasoning_summary_part.done",
535
+ sequence_number=sequence_number.get_and_increment(),
536
+ )
537
+ elif state.reasoning_content_index_and_output[1].content is not None:
538
+ yield ResponseReasoningTextDoneEvent(
539
+ item_id=FAKE_RESPONSES_ID,
540
+ output_index=0,
541
+ content_index=0,
542
+ text=state.reasoning_content_index_and_output[1].content[0].text,
543
+ type="response.reasoning_text.done",
544
+ sequence_number=sequence_number.get_and_increment(),
545
+ )
358
546
  yield ResponseOutputItemDoneEvent(
359
547
  item=state.reasoning_content_index_and_output[1],
360
548
  output_index=0,
@@ -397,14 +585,27 @@ class ChatCmplStreamHandler:
397
585
  if state.function_call_streaming.get(index, False):
398
586
  # Function call was streamed, just send the completion event
399
587
  output_index = state.function_call_output_idx[index]
588
+
589
+ # Build function call kwargs, include provider_data if present
590
+ func_call_kwargs: dict[str, Any] = {
591
+ "id": FAKE_RESPONSES_ID,
592
+ "call_id": function_call.call_id,
593
+ "arguments": function_call.arguments,
594
+ "name": function_call.name,
595
+ "type": "function_call",
596
+ }
597
+
598
+ # Merge provider_data from state and function_call (e.g. thought_signature)
599
+ if state.provider_data or (
600
+ hasattr(function_call, "provider_data") and function_call.provider_data
601
+ ):
602
+ merged_provider_data = state.provider_data.copy() if state.provider_data else {}
603
+ if hasattr(function_call, "provider_data") and function_call.provider_data:
604
+ merged_provider_data.update(function_call.provider_data)
605
+ func_call_kwargs["provider_data"] = merged_provider_data
606
+
400
607
  yield ResponseOutputItemDoneEvent(
401
- item=ResponseFunctionToolCall(
402
- id=FAKE_RESPONSES_ID,
403
- call_id=function_call.call_id,
404
- arguments=function_call.arguments,
405
- name=function_call.name,
406
- type="function_call",
407
- ),
608
+ item=ResponseFunctionToolCall(**func_call_kwargs),
408
609
  output_index=output_index,
409
610
  type="response.output_item.done",
410
611
  sequence_number=sequence_number.get_and_increment(),
@@ -425,15 +626,27 @@ class ChatCmplStreamHandler:
425
626
  1 for streaming in state.function_call_streaming.values() if streaming
426
627
  )
427
628
 
629
+ # Build function call kwargs, include provider_data if present
630
+ fallback_func_call_kwargs: dict[str, Any] = {
631
+ "id": FAKE_RESPONSES_ID,
632
+ "call_id": function_call.call_id,
633
+ "arguments": function_call.arguments,
634
+ "name": function_call.name,
635
+ "type": "function_call",
636
+ }
637
+
638
+ # Merge provider_data from state and function_call (e.g. thought_signature)
639
+ if state.provider_data or (
640
+ hasattr(function_call, "provider_data") and function_call.provider_data
641
+ ):
642
+ merged_provider_data = state.provider_data.copy() if state.provider_data else {}
643
+ if hasattr(function_call, "provider_data") and function_call.provider_data:
644
+ merged_provider_data.update(function_call.provider_data)
645
+ fallback_func_call_kwargs["provider_data"] = merged_provider_data
646
+
428
647
  # Send all events at once (backward compatibility)
429
648
  yield ResponseOutputItemAddedEvent(
430
- item=ResponseFunctionToolCall(
431
- id=FAKE_RESPONSES_ID,
432
- call_id=function_call.call_id,
433
- arguments=function_call.arguments,
434
- name=function_call.name,
435
- type="function_call",
436
- ),
649
+ item=ResponseFunctionToolCall(**fallback_func_call_kwargs),
437
650
  output_index=fallback_starting_index,
438
651
  type="response.output_item.added",
439
652
  sequence_number=sequence_number.get_and_increment(),
@@ -446,13 +659,7 @@ class ChatCmplStreamHandler:
446
659
  sequence_number=sequence_number.get_and_increment(),
447
660
  )
448
661
  yield ResponseOutputItemDoneEvent(
449
- item=ResponseFunctionToolCall(
450
- id=FAKE_RESPONSES_ID,
451
- call_id=function_call.call_id,
452
- arguments=function_call.arguments,
453
- name=function_call.name,
454
- type="function_call",
455
- ),
662
+ item=ResponseFunctionToolCall(**fallback_func_call_kwargs),
456
663
  output_index=fallback_starting_index,
457
664
  type="response.output_item.done",
458
665
  sequence_number=sequence_number.get_and_increment(),
@@ -463,7 +670,19 @@ class ChatCmplStreamHandler:
463
670
 
464
671
  # include Reasoning item if it exists
465
672
  if state.reasoning_content_index_and_output:
466
- outputs.append(state.reasoning_content_index_and_output[1])
673
+ reasoning_item = state.reasoning_content_index_and_output[1]
674
+ # Store thinking text in content and signature in encrypted_content
675
+ if state.thinking_text:
676
+ # Add thinking text as a Content object
677
+ if not reasoning_item.content:
678
+ reasoning_item.content = []
679
+ reasoning_item.content.append(
680
+ Content(text=state.thinking_text, type="reasoning_text")
681
+ )
682
+ # Store signature in encrypted_content
683
+ if state.thinking_signature:
684
+ reasoning_item.encrypted_content = state.thinking_signature
685
+ outputs.append(reasoning_item)
467
686
 
468
687
  # include text or refusal content if they exist
469
688
  if state.text_content_index_and_output or state.refusal_content_index_and_output:
@@ -474,6 +693,8 @@ class ChatCmplStreamHandler:
474
693
  type="message",
475
694
  status="completed",
476
695
  )
696
+ if state.provider_data:
697
+ assistant_msg.provider_data = state.provider_data.copy() # type: ignore[attr-defined]
477
698
  if state.text_content_index_and_output:
478
699
  assistant_msg.content.append(state.text_content_index_and_output[1])
479
700
  if state.refusal_content_index_and_output:
@@ -494,6 +715,7 @@ class ChatCmplStreamHandler:
494
715
 
495
716
  final_response = response.model_copy()
496
717
  final_response.output = outputs
718
+
497
719
  final_response.usage = (
498
720
  ResponseUsage(
499
721
  input_tokens=usage.prompt_tokens or 0,
@@ -0,0 +1,58 @@
1
+ import copy
2
+ import os
3
+ from typing import Optional
4
+
5
+ from openai.types.shared.reasoning import Reasoning
6
+
7
+ from agents.model_settings import ModelSettings
8
+
9
+ OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME = "OPENAI_DEFAULT_MODEL"
10
+
11
+ # discourage directly accessing this constant
12
+ # use the get_default_model and get_default_model_settings() functions instead
13
+ _GPT_5_DEFAULT_MODEL_SETTINGS: ModelSettings = ModelSettings(
14
+ # We chose "low" instead of "minimal" because some of the built-in tools
15
+ # (e.g., file search, image generation, etc.) do not support "minimal"
16
+ # If you want to use "minimal" reasoning effort, you can pass your own model settings
17
+ reasoning=Reasoning(effort="low"),
18
+ verbosity="low",
19
+ )
20
+
21
+
22
+ def gpt_5_reasoning_settings_required(model_name: str) -> bool:
23
+ """
24
+ Returns True if the model name is a GPT-5 model and reasoning settings are required.
25
+ """
26
+ if model_name.startswith("gpt-5-chat"):
27
+ # gpt-5-chat-latest does not require reasoning settings
28
+ return False
29
+ # matches any of gpt-5 models
30
+ return model_name.startswith("gpt-5")
31
+
32
+
33
+ def is_gpt_5_default() -> bool:
34
+ """
35
+ Returns True if the default model is a GPT-5 model.
36
+ This is used to determine if the default model settings are compatible with GPT-5 models.
37
+ If the default model is not a GPT-5 model, the model settings are compatible with other models.
38
+ """
39
+ return gpt_5_reasoning_settings_required(get_default_model())
40
+
41
+
42
+ def get_default_model() -> str:
43
+ """
44
+ Returns the default model name.
45
+ """
46
+ return os.getenv(OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME, "gpt-4.1").lower()
47
+
48
+
49
+ def get_default_model_settings(model: Optional[str] = None) -> ModelSettings:
50
+ """
51
+ Returns the default model settings.
52
+ If the default model is a GPT-5 model, returns the GPT-5 default model settings.
53
+ Otherwise, returns the legacy default model settings.
54
+ """
55
+ _model = model if model is not None else get_default_model()
56
+ if gpt_5_reasoning_settings_required(_model):
57
+ return copy.deepcopy(_GPT_5_DEFAULT_MODEL_SETTINGS)
58
+ return ModelSettings()
@@ -48,6 +48,7 @@ class Model(abc.ABC):
48
48
  tracing: ModelTracing,
49
49
  *,
50
50
  previous_response_id: str | None,
51
+ conversation_id: str | None,
51
52
  prompt: ResponsePromptParam | None,
52
53
  ) -> ModelResponse:
53
54
  """Get a response from the model.
@@ -62,6 +63,7 @@ class Model(abc.ABC):
62
63
  tracing: Tracing configuration.
63
64
  previous_response_id: the ID of the previous response. Generally not used by the model,
64
65
  except for the OpenAI Responses API.
66
+ conversation_id: The ID of the stored conversation, if any.
65
67
  prompt: The prompt config to use for the model.
66
68
 
67
69
  Returns:
@@ -81,6 +83,7 @@ class Model(abc.ABC):
81
83
  tracing: ModelTracing,
82
84
  *,
83
85
  previous_response_id: str | None,
86
+ conversation_id: str | None,
84
87
  prompt: ResponsePromptParam | None,
85
88
  ) -> AsyncIterator[TResponseStreamEvent]:
86
89
  """Stream a response from the model.
@@ -95,6 +98,7 @@ class Model(abc.ABC):
95
98
  tracing: Tracing configuration.
96
99
  previous_response_id: the ID of the previous response. Generally not used by the model,
97
100
  except for the OpenAI Responses API.
101
+ conversation_id: The ID of the stored conversation, if any.
98
102
  prompt: The prompt config to use for the model.
99
103
 
100
104
  Returns: