openai-agents 0.0.11__py3-none-any.whl → 0.0.13__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.

Potentially problematic release.


This version of openai-agents might be problematic. Click here for more details.

@@ -1,73 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
3
  import json
5
4
  import time
6
- from collections.abc import AsyncIterator, Iterable
7
- from dataclasses import dataclass, field
5
+ from collections.abc import AsyncIterator
8
6
  from typing import TYPE_CHECKING, Any, Literal, cast, overload
9
7
 
10
- from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream, NotGiven
8
+ from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream
11
9
  from openai.types import ChatModel
12
- from openai.types.chat import (
13
- ChatCompletion,
14
- ChatCompletionAssistantMessageParam,
15
- ChatCompletionChunk,
16
- ChatCompletionContentPartImageParam,
17
- ChatCompletionContentPartParam,
18
- ChatCompletionContentPartTextParam,
19
- ChatCompletionDeveloperMessageParam,
20
- ChatCompletionMessage,
21
- ChatCompletionMessageParam,
22
- ChatCompletionMessageToolCallParam,
23
- ChatCompletionSystemMessageParam,
24
- ChatCompletionToolChoiceOptionParam,
25
- ChatCompletionToolMessageParam,
26
- ChatCompletionUserMessageParam,
27
- )
28
- from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
29
- from openai.types.chat.completion_create_params import ResponseFormat
30
- from openai.types.completion_usage import CompletionUsage
31
- from openai.types.responses import (
32
- EasyInputMessageParam,
33
- Response,
34
- ResponseCompletedEvent,
35
- ResponseContentPartAddedEvent,
36
- ResponseContentPartDoneEvent,
37
- ResponseCreatedEvent,
38
- ResponseFileSearchToolCallParam,
39
- ResponseFunctionCallArgumentsDeltaEvent,
40
- ResponseFunctionToolCall,
41
- ResponseFunctionToolCallParam,
42
- ResponseInputContentParam,
43
- ResponseInputImageParam,
44
- ResponseInputTextParam,
45
- ResponseOutputItem,
46
- ResponseOutputItemAddedEvent,
47
- ResponseOutputItemDoneEvent,
48
- ResponseOutputMessage,
49
- ResponseOutputMessageParam,
50
- ResponseOutputRefusal,
51
- ResponseOutputText,
52
- ResponseRefusalDeltaEvent,
53
- ResponseTextDeltaEvent,
54
- ResponseUsage,
55
- )
56
- from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message
57
- from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
10
+ from openai.types.chat import ChatCompletion, ChatCompletionChunk
11
+ from openai.types.responses import Response
58
12
 
59
13
  from .. import _debug
60
- from ..agent_output import AgentOutputSchema
61
- from ..exceptions import AgentsException, UserError
14
+ from ..agent_output import AgentOutputSchemaBase
62
15
  from ..handoffs import Handoff
63
- from ..items import ModelResponse, TResponseInputItem, TResponseOutputItem, TResponseStreamEvent
16
+ from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent
64
17
  from ..logger import logger
65
- from ..tool import FunctionTool, Tool
18
+ from ..tool import Tool
66
19
  from ..tracing import generation_span
67
20
  from ..tracing.span_data import GenerationSpanData
68
21
  from ..tracing.spans import Span
69
22
  from ..usage import Usage
70
- from ..version import __version__
23
+ from .chatcmpl_converter import Converter
24
+ from .chatcmpl_helpers import HEADERS, ChatCmplHelpers
25
+ from .chatcmpl_stream_handler import ChatCmplStreamHandler
71
26
  from .fake_id import FAKE_RESPONSES_ID
72
27
  from .interface import Model, ModelTracing
73
28
 
@@ -75,18 +30,6 @@ if TYPE_CHECKING:
75
30
  from ..model_settings import ModelSettings
76
31
 
77
32
 
78
- _USER_AGENT = f"Agents/Python {__version__}"
79
- _HEADERS = {"User-Agent": _USER_AGENT}
80
-
81
-
82
- @dataclass
83
- class _StreamingState:
84
- started: bool = False
85
- text_content_index_and_output: tuple[int, ResponseOutputText] | None = None
86
- refusal_content_index_and_output: tuple[int, ResponseOutputRefusal] | None = None
87
- function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict)
88
-
89
-
90
33
  class OpenAIChatCompletionsModel(Model):
91
34
  def __init__(
92
35
  self,
@@ -105,15 +48,14 @@ class OpenAIChatCompletionsModel(Model):
105
48
  input: str | list[TResponseInputItem],
106
49
  model_settings: ModelSettings,
107
50
  tools: list[Tool],
108
- output_schema: AgentOutputSchema | None,
51
+ output_schema: AgentOutputSchemaBase | None,
109
52
  handoffs: list[Handoff],
110
53
  tracing: ModelTracing,
111
54
  previous_response_id: str | None,
112
55
  ) -> ModelResponse:
113
56
  with generation_span(
114
57
  model=str(self.model),
115
- model_config=dataclasses.asdict(model_settings)
116
- | {"base_url": str(self._client.base_url)},
58
+ model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)},
117
59
  disabled=tracing.is_disabled(),
118
60
  ) as span_generation:
119
61
  response = await self._fetch_response(
@@ -152,7 +94,7 @@ class OpenAIChatCompletionsModel(Model):
152
94
  "output_tokens": usage.output_tokens,
153
95
  }
154
96
 
155
- items = _Converter.message_to_output_items(response.choices[0].message)
97
+ items = Converter.message_to_output_items(response.choices[0].message)
156
98
 
157
99
  return ModelResponse(
158
100
  output=items,
@@ -166,7 +108,7 @@ class OpenAIChatCompletionsModel(Model):
166
108
  input: str | list[TResponseInputItem],
167
109
  model_settings: ModelSettings,
168
110
  tools: list[Tool],
169
- output_schema: AgentOutputSchema | None,
111
+ output_schema: AgentOutputSchemaBase | None,
170
112
  handoffs: list[Handoff],
171
113
  tracing: ModelTracing,
172
114
  *,
@@ -177,8 +119,7 @@ class OpenAIChatCompletionsModel(Model):
177
119
  """
178
120
  with generation_span(
179
121
  model=str(self.model),
180
- model_config=dataclasses.asdict(model_settings)
181
- | {"base_url": str(self._client.base_url)},
122
+ model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)},
182
123
  disabled=tracing.is_disabled(),
183
124
  ) as span_generation:
184
125
  response, stream = await self._fetch_response(
@@ -193,257 +134,20 @@ class OpenAIChatCompletionsModel(Model):
193
134
  stream=True,
194
135
  )
195
136
 
196
- usage: CompletionUsage | None = None
197
- state = _StreamingState()
198
-
199
- async for chunk in stream:
200
- if not state.started:
201
- state.started = True
202
- yield ResponseCreatedEvent(
203
- response=response,
204
- type="response.created",
205
- )
206
-
207
- # The usage is only available in the last chunk
208
- usage = chunk.usage
209
-
210
- if not chunk.choices or not chunk.choices[0].delta:
211
- continue
212
-
213
- delta = chunk.choices[0].delta
214
-
215
- # Handle text
216
- if delta.content:
217
- if not state.text_content_index_and_output:
218
- # Initialize a content tracker for streaming text
219
- state.text_content_index_and_output = (
220
- 0 if not state.refusal_content_index_and_output else 1,
221
- ResponseOutputText(
222
- text="",
223
- type="output_text",
224
- annotations=[],
225
- ),
226
- )
227
- # Start a new assistant message stream
228
- assistant_item = ResponseOutputMessage(
229
- id=FAKE_RESPONSES_ID,
230
- content=[],
231
- role="assistant",
232
- type="message",
233
- status="in_progress",
234
- )
235
- # Notify consumers of the start of a new output message + first content part
236
- yield ResponseOutputItemAddedEvent(
237
- item=assistant_item,
238
- output_index=0,
239
- type="response.output_item.added",
240
- )
241
- yield ResponseContentPartAddedEvent(
242
- content_index=state.text_content_index_and_output[0],
243
- item_id=FAKE_RESPONSES_ID,
244
- output_index=0,
245
- part=ResponseOutputText(
246
- text="",
247
- type="output_text",
248
- annotations=[],
249
- ),
250
- type="response.content_part.added",
251
- )
252
- # Emit the delta for this segment of content
253
- yield ResponseTextDeltaEvent(
254
- content_index=state.text_content_index_and_output[0],
255
- delta=delta.content,
256
- item_id=FAKE_RESPONSES_ID,
257
- output_index=0,
258
- type="response.output_text.delta",
259
- )
260
- # Accumulate the text into the response part
261
- state.text_content_index_and_output[1].text += delta.content
262
-
263
- # Handle refusals (model declines to answer)
264
- if delta.refusal:
265
- if not state.refusal_content_index_and_output:
266
- # Initialize a content tracker for streaming refusal text
267
- state.refusal_content_index_and_output = (
268
- 0 if not state.text_content_index_and_output else 1,
269
- ResponseOutputRefusal(refusal="", type="refusal"),
270
- )
271
- # Start a new assistant message if one doesn't exist yet (in-progress)
272
- assistant_item = ResponseOutputMessage(
273
- id=FAKE_RESPONSES_ID,
274
- content=[],
275
- role="assistant",
276
- type="message",
277
- status="in_progress",
278
- )
279
- # Notify downstream that assistant message + first content part are starting
280
- yield ResponseOutputItemAddedEvent(
281
- item=assistant_item,
282
- output_index=0,
283
- type="response.output_item.added",
284
- )
285
- yield ResponseContentPartAddedEvent(
286
- content_index=state.refusal_content_index_and_output[0],
287
- item_id=FAKE_RESPONSES_ID,
288
- output_index=0,
289
- part=ResponseOutputText(
290
- text="",
291
- type="output_text",
292
- annotations=[],
293
- ),
294
- type="response.content_part.added",
295
- )
296
- # Emit the delta for this segment of refusal
297
- yield ResponseRefusalDeltaEvent(
298
- content_index=state.refusal_content_index_and_output[0],
299
- delta=delta.refusal,
300
- item_id=FAKE_RESPONSES_ID,
301
- output_index=0,
302
- type="response.refusal.delta",
303
- )
304
- # Accumulate the refusal string in the output part
305
- state.refusal_content_index_and_output[1].refusal += delta.refusal
306
-
307
- # Handle tool calls
308
- # Because we don't know the name of the function until the end of the stream, we'll
309
- # save everything and yield events at the end
310
- if delta.tool_calls:
311
- for tc_delta in delta.tool_calls:
312
- if tc_delta.index not in state.function_calls:
313
- state.function_calls[tc_delta.index] = ResponseFunctionToolCall(
314
- id=FAKE_RESPONSES_ID,
315
- arguments="",
316
- name="",
317
- type="function_call",
318
- call_id="",
319
- )
320
- tc_function = tc_delta.function
321
-
322
- state.function_calls[tc_delta.index].arguments += (
323
- tc_function.arguments if tc_function else ""
324
- ) or ""
325
- state.function_calls[tc_delta.index].name += (
326
- tc_function.name if tc_function else ""
327
- ) or ""
328
- state.function_calls[tc_delta.index].call_id += tc_delta.id or ""
329
-
330
- function_call_starting_index = 0
331
- if state.text_content_index_and_output:
332
- function_call_starting_index += 1
333
- # Send end event for this content part
334
- yield ResponseContentPartDoneEvent(
335
- content_index=state.text_content_index_and_output[0],
336
- item_id=FAKE_RESPONSES_ID,
337
- output_index=0,
338
- part=state.text_content_index_and_output[1],
339
- type="response.content_part.done",
340
- )
341
-
342
- if state.refusal_content_index_and_output:
343
- function_call_starting_index += 1
344
- # Send end event for this content part
345
- yield ResponseContentPartDoneEvent(
346
- content_index=state.refusal_content_index_and_output[0],
347
- item_id=FAKE_RESPONSES_ID,
348
- output_index=0,
349
- part=state.refusal_content_index_and_output[1],
350
- type="response.content_part.done",
351
- )
352
-
353
- # Actually send events for the function calls
354
- for function_call in state.function_calls.values():
355
- # First, a ResponseOutputItemAdded for the function call
356
- yield ResponseOutputItemAddedEvent(
357
- item=ResponseFunctionToolCall(
358
- id=FAKE_RESPONSES_ID,
359
- call_id=function_call.call_id,
360
- arguments=function_call.arguments,
361
- name=function_call.name,
362
- type="function_call",
363
- ),
364
- output_index=function_call_starting_index,
365
- type="response.output_item.added",
366
- )
367
- # Then, yield the args
368
- yield ResponseFunctionCallArgumentsDeltaEvent(
369
- delta=function_call.arguments,
370
- item_id=FAKE_RESPONSES_ID,
371
- output_index=function_call_starting_index,
372
- type="response.function_call_arguments.delta",
373
- )
374
- # Finally, the ResponseOutputItemDone
375
- yield ResponseOutputItemDoneEvent(
376
- item=ResponseFunctionToolCall(
377
- id=FAKE_RESPONSES_ID,
378
- call_id=function_call.call_id,
379
- arguments=function_call.arguments,
380
- name=function_call.name,
381
- type="function_call",
382
- ),
383
- output_index=function_call_starting_index,
384
- type="response.output_item.done",
385
- )
137
+ final_response: Response | None = None
138
+ async for chunk in ChatCmplStreamHandler.handle_stream(response, stream):
139
+ yield chunk
386
140
 
387
- # Finally, send the Response completed event
388
- outputs: list[ResponseOutputItem] = []
389
- if state.text_content_index_and_output or state.refusal_content_index_and_output:
390
- assistant_msg = ResponseOutputMessage(
391
- id=FAKE_RESPONSES_ID,
392
- content=[],
393
- role="assistant",
394
- type="message",
395
- status="completed",
396
- )
397
- if state.text_content_index_and_output:
398
- assistant_msg.content.append(state.text_content_index_and_output[1])
399
- if state.refusal_content_index_and_output:
400
- assistant_msg.content.append(state.refusal_content_index_and_output[1])
401
- outputs.append(assistant_msg)
402
-
403
- # send a ResponseOutputItemDone for the assistant message
404
- yield ResponseOutputItemDoneEvent(
405
- item=assistant_msg,
406
- output_index=0,
407
- type="response.output_item.done",
408
- )
409
-
410
- for function_call in state.function_calls.values():
411
- outputs.append(function_call)
412
-
413
- final_response = response.model_copy()
414
- final_response.output = outputs
415
- final_response.usage = (
416
- ResponseUsage(
417
- input_tokens=usage.prompt_tokens,
418
- output_tokens=usage.completion_tokens,
419
- total_tokens=usage.total_tokens,
420
- output_tokens_details=OutputTokensDetails(
421
- reasoning_tokens=usage.completion_tokens_details.reasoning_tokens
422
- if usage.completion_tokens_details
423
- and usage.completion_tokens_details.reasoning_tokens
424
- else 0
425
- ),
426
- input_tokens_details=InputTokensDetails(
427
- cached_tokens=usage.prompt_tokens_details.cached_tokens
428
- if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens
429
- else 0
430
- ),
431
- )
432
- if usage
433
- else None
434
- )
141
+ if chunk.type == "response.completed":
142
+ final_response = chunk.response
435
143
 
436
- yield ResponseCompletedEvent(
437
- response=final_response,
438
- type="response.completed",
439
- )
440
- if tracing.include_data():
144
+ if tracing.include_data() and final_response:
441
145
  span_generation.span_data.output = [final_response.model_dump()]
442
146
 
443
- if usage:
147
+ if final_response and final_response.usage:
444
148
  span_generation.span_data.usage = {
445
- "input_tokens": usage.prompt_tokens,
446
- "output_tokens": usage.completion_tokens,
149
+ "input_tokens": final_response.usage.input_tokens,
150
+ "output_tokens": final_response.usage.output_tokens,
447
151
  }
448
152
 
449
153
  @overload
@@ -453,7 +157,7 @@ class OpenAIChatCompletionsModel(Model):
453
157
  input: str | list[TResponseInputItem],
454
158
  model_settings: ModelSettings,
455
159
  tools: list[Tool],
456
- output_schema: AgentOutputSchema | None,
160
+ output_schema: AgentOutputSchemaBase | None,
457
161
  handoffs: list[Handoff],
458
162
  span: Span[GenerationSpanData],
459
163
  tracing: ModelTracing,
@@ -467,7 +171,7 @@ class OpenAIChatCompletionsModel(Model):
467
171
  input: str | list[TResponseInputItem],
468
172
  model_settings: ModelSettings,
469
173
  tools: list[Tool],
470
- output_schema: AgentOutputSchema | None,
174
+ output_schema: AgentOutputSchemaBase | None,
471
175
  handoffs: list[Handoff],
472
176
  span: Span[GenerationSpanData],
473
177
  tracing: ModelTracing,
@@ -480,13 +184,13 @@ class OpenAIChatCompletionsModel(Model):
480
184
  input: str | list[TResponseInputItem],
481
185
  model_settings: ModelSettings,
482
186
  tools: list[Tool],
483
- output_schema: AgentOutputSchema | None,
187
+ output_schema: AgentOutputSchemaBase | None,
484
188
  handoffs: list[Handoff],
485
189
  span: Span[GenerationSpanData],
486
190
  tracing: ModelTracing,
487
191
  stream: bool = False,
488
192
  ) -> ChatCompletion | tuple[Response, AsyncStream[ChatCompletionChunk]]:
489
- converted_messages = _Converter.items_to_messages(input)
193
+ converted_messages = Converter.items_to_messages(input)
490
194
 
491
195
  if system_instructions:
492
196
  converted_messages.insert(
@@ -506,13 +210,13 @@ class OpenAIChatCompletionsModel(Model):
506
210
  if model_settings.parallel_tool_calls is False
507
211
  else NOT_GIVEN
508
212
  )
509
- tool_choice = _Converter.convert_tool_choice(model_settings.tool_choice)
510
- response_format = _Converter.convert_response_format(output_schema)
213
+ tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
214
+ response_format = Converter.convert_response_format(output_schema)
511
215
 
512
- converted_tools = [ToolConverter.to_openai(tool) for tool in tools] if tools else []
216
+ converted_tools = [Converter.tool_to_openai(tool) for tool in tools] if tools else []
513
217
 
514
218
  for handoff in handoffs:
515
- converted_tools.append(ToolConverter.convert_handoff_tool(handoff))
219
+ converted_tools.append(Converter.convert_handoff_tool(handoff))
516
220
 
517
221
  if _debug.DONT_LOG_MODEL_DATA:
518
222
  logger.debug("Calling LLM")
@@ -526,9 +230,9 @@ class OpenAIChatCompletionsModel(Model):
526
230
  )
527
231
 
528
232
  reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None
529
- store = _Converter.get_store_param(self._get_client(), model_settings)
233
+ store = ChatCmplHelpers.get_store_param(self._get_client(), model_settings)
530
234
 
531
- stream_options = _Converter.get_stream_options_param(
235
+ stream_options = ChatCmplHelpers.get_stream_options_param(
532
236
  self._get_client(), model_settings, stream=stream
533
237
  )
534
238
 
@@ -548,7 +252,7 @@ class OpenAIChatCompletionsModel(Model):
548
252
  stream_options=self._non_null_or_not_given(stream_options),
549
253
  store=self._non_null_or_not_given(store),
550
254
  reasoning_effort=self._non_null_or_not_given(reasoning_effort),
551
- extra_headers=_HEADERS,
255
+ extra_headers={ **HEADERS, **(model_settings.extra_headers or {}) },
552
256
  extra_query=model_settings.extra_query,
553
257
  extra_body=model_settings.extra_body,
554
258
  metadata=self._non_null_or_not_given(model_settings.metadata),
@@ -578,453 +282,3 @@ class OpenAIChatCompletionsModel(Model):
578
282
  if self._client is None:
579
283
  self._client = AsyncOpenAI()
580
284
  return self._client
581
-
582
-
583
- class _Converter:
584
- @classmethod
585
- def is_openai(cls, client: AsyncOpenAI):
586
- return str(client.base_url).startswith("https://api.openai.com")
587
-
588
- @classmethod
589
- def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> bool | None:
590
- # Match the behavior of Responses where store is True when not given
591
- default_store = True if cls.is_openai(client) else None
592
- return model_settings.store if model_settings.store is not None else default_store
593
-
594
- @classmethod
595
- def get_stream_options_param(
596
- cls, client: AsyncOpenAI, model_settings: ModelSettings, stream: bool
597
- ) -> dict[str, bool] | None:
598
- if not stream:
599
- return None
600
-
601
- default_include_usage = True if cls.is_openai(client) else None
602
- include_usage = (
603
- model_settings.include_usage
604
- if model_settings.include_usage is not None
605
- else default_include_usage
606
- )
607
- stream_options = {"include_usage": include_usage} if include_usage is not None else None
608
- return stream_options
609
-
610
- @classmethod
611
- def convert_tool_choice(
612
- cls, tool_choice: Literal["auto", "required", "none"] | str | None
613
- ) -> ChatCompletionToolChoiceOptionParam | NotGiven:
614
- if tool_choice is None:
615
- return NOT_GIVEN
616
- elif tool_choice == "auto":
617
- return "auto"
618
- elif tool_choice == "required":
619
- return "required"
620
- elif tool_choice == "none":
621
- return "none"
622
- else:
623
- return {
624
- "type": "function",
625
- "function": {
626
- "name": tool_choice,
627
- },
628
- }
629
-
630
- @classmethod
631
- def convert_response_format(
632
- cls, final_output_schema: AgentOutputSchema | None
633
- ) -> ResponseFormat | NotGiven:
634
- if not final_output_schema or final_output_schema.is_plain_text():
635
- return NOT_GIVEN
636
-
637
- return {
638
- "type": "json_schema",
639
- "json_schema": {
640
- "name": "final_output",
641
- "strict": final_output_schema.strict_json_schema,
642
- "schema": final_output_schema.json_schema(),
643
- },
644
- }
645
-
646
- @classmethod
647
- def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TResponseOutputItem]:
648
- items: list[TResponseOutputItem] = []
649
-
650
- message_item = ResponseOutputMessage(
651
- id=FAKE_RESPONSES_ID,
652
- content=[],
653
- role="assistant",
654
- type="message",
655
- status="completed",
656
- )
657
- if message.content:
658
- message_item.content.append(
659
- ResponseOutputText(text=message.content, type="output_text", annotations=[])
660
- )
661
- if message.refusal:
662
- message_item.content.append(
663
- ResponseOutputRefusal(refusal=message.refusal, type="refusal")
664
- )
665
- if message.audio:
666
- raise AgentsException("Audio is not currently supported")
667
-
668
- if message_item.content:
669
- items.append(message_item)
670
-
671
- if message.tool_calls:
672
- for tool_call in message.tool_calls:
673
- items.append(
674
- ResponseFunctionToolCall(
675
- id=FAKE_RESPONSES_ID,
676
- call_id=tool_call.id,
677
- arguments=tool_call.function.arguments,
678
- name=tool_call.function.name,
679
- type="function_call",
680
- )
681
- )
682
-
683
- return items
684
-
685
- @classmethod
686
- def maybe_easy_input_message(cls, item: Any) -> EasyInputMessageParam | None:
687
- if not isinstance(item, dict):
688
- return None
689
-
690
- keys = item.keys()
691
- # EasyInputMessageParam only has these two keys
692
- if keys != {"content", "role"}:
693
- return None
694
-
695
- role = item.get("role", None)
696
- if role not in ("user", "assistant", "system", "developer"):
697
- return None
698
-
699
- if "content" not in item:
700
- return None
701
-
702
- return cast(EasyInputMessageParam, item)
703
-
704
- @classmethod
705
- def maybe_input_message(cls, item: Any) -> Message | None:
706
- if (
707
- isinstance(item, dict)
708
- and item.get("type") == "message"
709
- and item.get("role")
710
- in (
711
- "user",
712
- "system",
713
- "developer",
714
- )
715
- ):
716
- return cast(Message, item)
717
-
718
- return None
719
-
720
- @classmethod
721
- def maybe_file_search_call(cls, item: Any) -> ResponseFileSearchToolCallParam | None:
722
- if isinstance(item, dict) and item.get("type") == "file_search_call":
723
- return cast(ResponseFileSearchToolCallParam, item)
724
- return None
725
-
726
- @classmethod
727
- def maybe_function_tool_call(cls, item: Any) -> ResponseFunctionToolCallParam | None:
728
- if isinstance(item, dict) and item.get("type") == "function_call":
729
- return cast(ResponseFunctionToolCallParam, item)
730
- return None
731
-
732
- @classmethod
733
- def maybe_function_tool_call_output(
734
- cls,
735
- item: Any,
736
- ) -> FunctionCallOutput | None:
737
- if isinstance(item, dict) and item.get("type") == "function_call_output":
738
- return cast(FunctionCallOutput, item)
739
- return None
740
-
741
- @classmethod
742
- def maybe_item_reference(cls, item: Any) -> ItemReference | None:
743
- if isinstance(item, dict) and item.get("type") == "item_reference":
744
- return cast(ItemReference, item)
745
- return None
746
-
747
- @classmethod
748
- def maybe_response_output_message(cls, item: Any) -> ResponseOutputMessageParam | None:
749
- # ResponseOutputMessage is only used for messages with role assistant
750
- if (
751
- isinstance(item, dict)
752
- and item.get("type") == "message"
753
- and item.get("role") == "assistant"
754
- ):
755
- return cast(ResponseOutputMessageParam, item)
756
- return None
757
-
758
- @classmethod
759
- def extract_text_content(
760
- cls, content: str | Iterable[ResponseInputContentParam]
761
- ) -> str | list[ChatCompletionContentPartTextParam]:
762
- all_content = cls.extract_all_content(content)
763
- if isinstance(all_content, str):
764
- return all_content
765
- out: list[ChatCompletionContentPartTextParam] = []
766
- for c in all_content:
767
- if c.get("type") == "text":
768
- out.append(cast(ChatCompletionContentPartTextParam, c))
769
- return out
770
-
771
- @classmethod
772
- def extract_all_content(
773
- cls, content: str | Iterable[ResponseInputContentParam]
774
- ) -> str | list[ChatCompletionContentPartParam]:
775
- if isinstance(content, str):
776
- return content
777
- out: list[ChatCompletionContentPartParam] = []
778
-
779
- for c in content:
780
- if isinstance(c, dict) and c.get("type") == "input_text":
781
- casted_text_param = cast(ResponseInputTextParam, c)
782
- out.append(
783
- ChatCompletionContentPartTextParam(
784
- type="text",
785
- text=casted_text_param["text"],
786
- )
787
- )
788
- elif isinstance(c, dict) and c.get("type") == "input_image":
789
- casted_image_param = cast(ResponseInputImageParam, c)
790
- if "image_url" not in casted_image_param or not casted_image_param["image_url"]:
791
- raise UserError(
792
- f"Only image URLs are supported for input_image {casted_image_param}"
793
- )
794
- out.append(
795
- ChatCompletionContentPartImageParam(
796
- type="image_url",
797
- image_url={
798
- "url": casted_image_param["image_url"],
799
- "detail": casted_image_param["detail"],
800
- },
801
- )
802
- )
803
- elif isinstance(c, dict) and c.get("type") == "input_file":
804
- raise UserError(f"File uploads are not supported for chat completions {c}")
805
- else:
806
- raise UserError(f"Unknown content: {c}")
807
- return out
808
-
809
- @classmethod
810
- def items_to_messages(
811
- cls,
812
- items: str | Iterable[TResponseInputItem],
813
- ) -> list[ChatCompletionMessageParam]:
814
- """
815
- Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
816
-
817
- Rules:
818
- - EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
819
- - EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam
820
- - EasyInputMessage or InputMessage (role=developer) => ChatCompletionDeveloperMessageParam
821
- - InputMessage (role=assistant) => Start or flush a ChatCompletionAssistantMessageParam
822
- - response_output_message => Also produces/flushes a ChatCompletionAssistantMessageParam
823
- - tool calls get attached to the *current* assistant message, or create one if none.
824
- - tool outputs => ChatCompletionToolMessageParam
825
- """
826
-
827
- if isinstance(items, str):
828
- return [
829
- ChatCompletionUserMessageParam(
830
- role="user",
831
- content=items,
832
- )
833
- ]
834
-
835
- result: list[ChatCompletionMessageParam] = []
836
- current_assistant_msg: ChatCompletionAssistantMessageParam | None = None
837
-
838
- def flush_assistant_message() -> None:
839
- nonlocal current_assistant_msg
840
- if current_assistant_msg is not None:
841
- # The API doesn't support empty arrays for tool_calls
842
- if not current_assistant_msg.get("tool_calls"):
843
- del current_assistant_msg["tool_calls"]
844
- result.append(current_assistant_msg)
845
- current_assistant_msg = None
846
-
847
- def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
848
- nonlocal current_assistant_msg
849
- if current_assistant_msg is None:
850
- current_assistant_msg = ChatCompletionAssistantMessageParam(role="assistant")
851
- current_assistant_msg["tool_calls"] = []
852
- return current_assistant_msg
853
-
854
- for item in items:
855
- # 1) Check easy input message
856
- if easy_msg := cls.maybe_easy_input_message(item):
857
- role = easy_msg["role"]
858
- content = easy_msg["content"]
859
-
860
- if role == "user":
861
- flush_assistant_message()
862
- msg_user: ChatCompletionUserMessageParam = {
863
- "role": "user",
864
- "content": cls.extract_all_content(content),
865
- }
866
- result.append(msg_user)
867
- elif role == "system":
868
- flush_assistant_message()
869
- msg_system: ChatCompletionSystemMessageParam = {
870
- "role": "system",
871
- "content": cls.extract_text_content(content),
872
- }
873
- result.append(msg_system)
874
- elif role == "developer":
875
- flush_assistant_message()
876
- msg_developer: ChatCompletionDeveloperMessageParam = {
877
- "role": "developer",
878
- "content": cls.extract_text_content(content),
879
- }
880
- result.append(msg_developer)
881
- elif role == "assistant":
882
- flush_assistant_message()
883
- msg_assistant: ChatCompletionAssistantMessageParam = {
884
- "role": "assistant",
885
- "content": cls.extract_text_content(content),
886
- }
887
- result.append(msg_assistant)
888
- else:
889
- raise UserError(f"Unexpected role in easy_input_message: {role}")
890
-
891
- # 2) Check input message
892
- elif in_msg := cls.maybe_input_message(item):
893
- role = in_msg["role"]
894
- content = in_msg["content"]
895
- flush_assistant_message()
896
-
897
- if role == "user":
898
- msg_user = {
899
- "role": "user",
900
- "content": cls.extract_all_content(content),
901
- }
902
- result.append(msg_user)
903
- elif role == "system":
904
- msg_system = {
905
- "role": "system",
906
- "content": cls.extract_text_content(content),
907
- }
908
- result.append(msg_system)
909
- elif role == "developer":
910
- msg_developer = {
911
- "role": "developer",
912
- "content": cls.extract_text_content(content),
913
- }
914
- result.append(msg_developer)
915
- else:
916
- raise UserError(f"Unexpected role in input_message: {role}")
917
-
918
- # 3) response output message => assistant
919
- elif resp_msg := cls.maybe_response_output_message(item):
920
- flush_assistant_message()
921
- new_asst = ChatCompletionAssistantMessageParam(role="assistant")
922
- contents = resp_msg["content"]
923
-
924
- text_segments = []
925
- for c in contents:
926
- if c["type"] == "output_text":
927
- text_segments.append(c["text"])
928
- elif c["type"] == "refusal":
929
- new_asst["refusal"] = c["refusal"]
930
- elif c["type"] == "output_audio":
931
- # Can't handle this, b/c chat completions expects an ID which we dont have
932
- raise UserError(
933
- f"Only audio IDs are supported for chat completions, but got: {c}"
934
- )
935
- else:
936
- raise UserError(f"Unknown content type in ResponseOutputMessage: {c}")
937
-
938
- if text_segments:
939
- combined = "\n".join(text_segments)
940
- new_asst["content"] = combined
941
-
942
- new_asst["tool_calls"] = []
943
- current_assistant_msg = new_asst
944
-
945
- # 4) function/file-search calls => attach to assistant
946
- elif file_search := cls.maybe_file_search_call(item):
947
- asst = ensure_assistant_message()
948
- tool_calls = list(asst.get("tool_calls", []))
949
- new_tool_call = ChatCompletionMessageToolCallParam(
950
- id=file_search["id"],
951
- type="function",
952
- function={
953
- "name": "file_search_call",
954
- "arguments": json.dumps(
955
- {
956
- "queries": file_search.get("queries", []),
957
- "status": file_search.get("status"),
958
- }
959
- ),
960
- },
961
- )
962
- tool_calls.append(new_tool_call)
963
- asst["tool_calls"] = tool_calls
964
-
965
- elif func_call := cls.maybe_function_tool_call(item):
966
- asst = ensure_assistant_message()
967
- tool_calls = list(asst.get("tool_calls", []))
968
- arguments = func_call["arguments"] if func_call["arguments"] else "{}"
969
- new_tool_call = ChatCompletionMessageToolCallParam(
970
- id=func_call["call_id"],
971
- type="function",
972
- function={
973
- "name": func_call["name"],
974
- "arguments": arguments,
975
- },
976
- )
977
- tool_calls.append(new_tool_call)
978
- asst["tool_calls"] = tool_calls
979
- # 5) function call output => tool message
980
- elif func_output := cls.maybe_function_tool_call_output(item):
981
- flush_assistant_message()
982
- msg: ChatCompletionToolMessageParam = {
983
- "role": "tool",
984
- "tool_call_id": func_output["call_id"],
985
- "content": func_output["output"],
986
- }
987
- result.append(msg)
988
-
989
- # 6) item reference => handle or raise
990
- elif item_ref := cls.maybe_item_reference(item):
991
- raise UserError(
992
- f"Encountered an item_reference, which is not supported: {item_ref}"
993
- )
994
-
995
- # 7) If we haven't recognized it => fail or ignore
996
- else:
997
- raise UserError(f"Unhandled item type or structure: {item}")
998
-
999
- flush_assistant_message()
1000
- return result
1001
-
1002
-
1003
- class ToolConverter:
1004
- @classmethod
1005
- def to_openai(cls, tool: Tool) -> ChatCompletionToolParam:
1006
- if isinstance(tool, FunctionTool):
1007
- return {
1008
- "type": "function",
1009
- "function": {
1010
- "name": tool.name,
1011
- "description": tool.description or "",
1012
- "parameters": tool.params_json_schema,
1013
- },
1014
- }
1015
-
1016
- raise UserError(
1017
- f"Hosted tools are not supported with the ChatCompletions API. Got tool type: "
1018
- f"{type(tool)}, tool: {tool}"
1019
- )
1020
-
1021
- @classmethod
1022
- def convert_handoff_tool(cls, handoff: Handoff[Any]) -> ChatCompletionToolParam:
1023
- return {
1024
- "type": "function",
1025
- "function": {
1026
- "name": handoff.tool_name,
1027
- "description": handoff.tool_description,
1028
- "parameters": handoff.input_json_schema,
1029
- },
1030
- }