openai 0.97.0 → 0.99.0

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 (244) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +76 -1
  4. data/lib/openai/helpers/agents/session_stream.rb +60 -3
  5. data/lib/openai/helpers/beta/agents/artifacts.rb +72 -0
  6. data/lib/openai/helpers/beta/agents/creation_stream.rb +69 -0
  7. data/lib/openai/helpers/beta/agents/files.rb +236 -0
  8. data/lib/openai/helpers/beta/agents/function_tool.rb +65 -0
  9. data/lib/openai/helpers/beta/agents/model_adapter.rb +27 -0
  10. data/lib/openai/helpers/beta/agents/result.rb +170 -0
  11. data/lib/openai/helpers/beta/agents/typed_output.rb +191 -0
  12. data/lib/openai/helpers/live/websocket.rb +7 -1
  13. data/lib/openai/helpers/realtime/conversation_item_variant_resolver.rb +1 -1
  14. data/lib/openai/helpers/responses_websocket/connection.rb +7 -1
  15. data/lib/openai/helpers/responses_websocket/session.rb +5 -1
  16. data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +12 -2
  17. data/lib/openai/internal/transport/base_client.rb +18 -0
  18. data/lib/openai/internal/type/array_of.rb +1 -1
  19. data/lib/openai/internal/type/base_model.rb +6 -3
  20. data/lib/openai/internal/type/converter.rb +50 -3
  21. data/lib/openai/internal/type/hash_of.rb +1 -1
  22. data/lib/openai/internal/type/union.rb +6 -6
  23. data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +16 -3
  24. data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +16 -3
  25. data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +16 -3
  26. data/lib/openai/models/admin/organization/usage_completions_response.rb +16 -3
  27. data/lib/openai/models/admin/organization/usage_costs_response.rb +16 -3
  28. data/lib/openai/models/admin/organization/usage_embeddings_response.rb +16 -3
  29. data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +16 -3
  30. data/lib/openai/models/admin/organization/usage_images_response.rb +16 -3
  31. data/lib/openai/models/admin/organization/usage_moderations_response.rb +16 -3
  32. data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +16 -3
  33. data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +16 -3
  34. data/lib/openai/models/all_models.rb +42 -0
  35. data/lib/openai/models/audio/speech_create_params.rb +6 -0
  36. data/lib/openai/models/audio/voice.rb +75 -0
  37. data/lib/openai/models/audio/voice_create_params.rb +217 -0
  38. data/lib/openai/models/beta/agent_create_params.rb +0 -3
  39. data/lib/openai/models/beta/agent_update_params.rb +0 -3
  40. data/lib/openai/models/beta/agents/session_create_params.rb +0 -3
  41. data/lib/openai/models/beta/agents/session_update_params.rb +0 -3
  42. data/lib/openai/models/beta/agents/sessions/session_trace.rb +64 -0
  43. data/lib/openai/models/beta/agents/sessions/trace_list_params.rb +68 -0
  44. data/lib/openai/models/beta/agents/vaults/credential.rb +8 -4
  45. data/lib/openai/models/beta/beta_response.rb +220 -0
  46. data/lib/openai/models/beta/beta_response_output_text.rb +2 -2
  47. data/lib/openai/models/beta/beta_response_output_text_annotation_added_event.rb +2 -2
  48. data/lib/openai/models/beta/beta_responses_client_event.rb +220 -0
  49. data/lib/openai/models/beta/beta_tool.rb +3 -9
  50. data/lib/openai/models/beta/environment.rb +33 -1
  51. data/lib/openai/models/beta/environment_param.rb +24 -1
  52. data/lib/openai/models/beta/response_compact_params.rb +220 -0
  53. data/lib/openai/models/beta/response_create_params.rb +220 -0
  54. data/lib/openai/models/beta/session_turn_error.rb +3 -0
  55. data/lib/openai/models/chat/chat_completion_audio_param.rb +6 -3
  56. data/lib/openai/models/chat/chat_completion_content_part_image.rb +1 -0
  57. data/lib/openai/models/chat_model.rb +178 -0
  58. data/lib/openai/models/image.rb +14 -14
  59. data/lib/openai/models/image_create_variation_params.rb +7 -7
  60. data/lib/openai/models/image_edit_params.rb +76 -87
  61. data/lib/openai/models/image_generate_params.rb +51 -62
  62. data/lib/openai/models/image_model.rb +5 -3
  63. data/lib/openai/models/live/fork_server_event.rb +14 -3
  64. data/lib/openai/models/live/server_event.rb +14 -3
  65. data/lib/openai/models/realtime/realtime_audio_config_output.rb +9 -6
  66. data/lib/openai/models/realtime/realtime_response_create_audio_output.rb +9 -6
  67. data/lib/openai/models/realtime/translations/client_secret_create_params.rb +28 -0
  68. data/lib/openai/models/responses/response_compact_params.rb +220 -0
  69. data/lib/openai/models/responses/response_output_text.rb +2 -2
  70. data/lib/openai/models/responses/response_output_text_annotation_added_event.rb +2 -2
  71. data/lib/openai/models/responses/tool.rb +3 -9
  72. data/lib/openai/models/responses_model.rb +42 -0
  73. data/lib/openai/models/webhooks/agent_session_action_required_webhook_event.rb +109 -0
  74. data/lib/openai/models/webhooks/agent_session_created_webhook_event.rb +106 -0
  75. data/lib/openai/models/webhooks/agent_session_failed_webhook_event.rb +86 -0
  76. data/lib/openai/models/webhooks/agent_session_idle_webhook_event.rb +86 -0
  77. data/lib/openai/models/webhooks/agent_session_in_progress_webhook_event.rb +86 -0
  78. data/lib/openai/models/webhooks/unwrap_webhook_event.rb +18 -2
  79. data/lib/openai/models/webhooks/webhook_create_params.rb +5 -0
  80. data/lib/openai/models/webhooks/webhook_test_params.rb +5 -0
  81. data/lib/openai/models/webhooks/webhook_update_params.rb +5 -0
  82. data/lib/openai/resources/audio/speech.rb +2 -0
  83. data/lib/openai/resources/audio/voices.rb +53 -0
  84. data/lib/openai/resources/audio.rb +5 -0
  85. data/lib/openai/resources/beta/agents/environments/files.rb +31 -0
  86. data/lib/openai/resources/beta/agents/sessions/artifacts.rb +8 -0
  87. data/lib/openai/resources/beta/agents/sessions/events.rb +6 -5
  88. data/lib/openai/resources/beta/agents/sessions/traces.rb +58 -0
  89. data/lib/openai/resources/beta/agents/sessions.rb +18 -5
  90. data/lib/openai/resources/evals/runs.rb +1 -1
  91. data/lib/openai/resources/images.rb +71 -103
  92. data/lib/openai/resources/realtime/translations/client_secrets.rb +58 -0
  93. data/lib/openai/resources/realtime/translations.rb +20 -0
  94. data/lib/openai/resources/realtime.rb +4 -0
  95. data/lib/openai/resources/webhooks.rb +1 -1
  96. data/lib/openai/version.rb +1 -1
  97. data/lib/openai.rb +17 -1
  98. data/rbi/openai/helpers/agents.rbi +20 -1
  99. data/rbi/openai/helpers/beta/agents/artifacts.rbi +29 -0
  100. data/rbi/openai/helpers/beta/agents/files.rbi +65 -0
  101. data/rbi/openai/helpers/beta/agents/function_tool.rbi +40 -0
  102. data/rbi/openai/helpers/beta/agents/model_adapter.rbi +22 -0
  103. data/rbi/openai/helpers/beta/agents.rbi +146 -0
  104. data/rbi/openai/internal/transport/base_client.rbi +12 -0
  105. data/rbi/openai/internal/type/base_model.rbi +2 -2
  106. data/rbi/openai/internal/type/converter.rbi +21 -3
  107. data/rbi/openai/internal/type/union.rbi +6 -2
  108. data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +19 -2
  109. data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +19 -2
  110. data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +19 -2
  111. data/rbi/openai/models/admin/organization/usage_completions_response.rbi +19 -2
  112. data/rbi/openai/models/admin/organization/usage_costs_response.rbi +19 -2
  113. data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +19 -2
  114. data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +19 -2
  115. data/rbi/openai/models/admin/organization/usage_images_response.rbi +19 -2
  116. data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +19 -2
  117. data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +19 -2
  118. data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +19 -2
  119. data/rbi/openai/models/audio/speech_create_params.rbi +6 -0
  120. data/rbi/openai/models/audio/voice.rbi +111 -0
  121. data/rbi/openai/models/audio/voice_create_params.rbi +321 -0
  122. data/rbi/openai/models/beta/agent_create_params.rbi +0 -3
  123. data/rbi/openai/models/beta/agent_update_params.rbi +0 -3
  124. data/rbi/openai/models/beta/agents/session_create_params.rbi +0 -3
  125. data/rbi/openai/models/beta/agents/session_update_params.rbi +0 -3
  126. data/rbi/openai/models/beta/agents/sessions/session_trace.rbi +97 -0
  127. data/rbi/openai/models/beta/agents/sessions/trace_list_params.rbi +121 -0
  128. data/rbi/openai/models/beta/agents/vaults/credential.rbi +5 -5
  129. data/rbi/openai/models/beta/beta_response_output_text.rbi +2 -2
  130. data/rbi/openai/models/beta/beta_response_output_text_annotation_added_event.rbi +2 -2
  131. data/rbi/openai/models/beta/beta_tool.rbi +3 -9
  132. data/rbi/openai/models/beta/environment.rbi +29 -0
  133. data/rbi/openai/models/beta/environment_param.rbi +31 -0
  134. data/rbi/openai/models/beta/session_turn_error.rbi +3 -0
  135. data/rbi/openai/models/chat/chat_completion_audio_param.rbi +6 -3
  136. data/rbi/openai/models/chat/chat_completion_content_part_image.rbi +1 -0
  137. data/rbi/openai/models/image.rbi +14 -14
  138. data/rbi/openai/models/image_create_variation_params.rbi +6 -6
  139. data/rbi/openai/models/image_edit_params.rbi +61 -82
  140. data/rbi/openai/models/image_generate_params.rbi +30 -61
  141. data/rbi/openai/models/image_model.rbi +3 -3
  142. data/rbi/openai/models/live/fork_server_event.rbi +14 -2
  143. data/rbi/openai/models/live/server_event.rbi +14 -2
  144. data/rbi/openai/models/realtime/realtime_audio_config_output.rbi +9 -6
  145. data/rbi/openai/models/realtime/realtime_response_create_audio_output.rbi +9 -6
  146. data/rbi/openai/models/realtime/translations/client_secret_create_params.rbi +68 -0
  147. data/rbi/openai/models/responses/response_output_text.rbi +2 -2
  148. data/rbi/openai/models/responses/response_output_text_annotation_added_event.rbi +2 -2
  149. data/rbi/openai/models/responses/tool.rbi +3 -9
  150. data/rbi/openai/models/webhooks/agent_session_action_required_webhook_event.rbi +222 -0
  151. data/rbi/openai/models/webhooks/agent_session_created_webhook_event.rbi +202 -0
  152. data/rbi/openai/models/webhooks/agent_session_failed_webhook_event.rbi +151 -0
  153. data/rbi/openai/models/webhooks/agent_session_idle_webhook_event.rbi +151 -0
  154. data/rbi/openai/models/webhooks/agent_session_in_progress_webhook_event.rbi +151 -0
  155. data/rbi/openai/models/webhooks/unwrap_webhook_event.rbi +7 -1
  156. data/rbi/openai/models/webhooks/webhook_create_params.rbi +20 -0
  157. data/rbi/openai/models/webhooks/webhook_test_params.rbi +20 -0
  158. data/rbi/openai/models/webhooks/webhook_update_params.rbi +20 -0
  159. data/rbi/openai/resources/audio/speech.rbi +2 -0
  160. data/rbi/openai/resources/audio/voices.rbi +49 -0
  161. data/rbi/openai/resources/audio.rbi +4 -0
  162. data/rbi/openai/resources/beta/agents/environments/files.rbi +31 -0
  163. data/rbi/openai/resources/beta/agents/sessions/artifacts.rbi +8 -0
  164. data/rbi/openai/resources/beta/agents/sessions/events.rbi +6 -5
  165. data/rbi/openai/resources/beta/agents/sessions/traces.rbi +54 -0
  166. data/rbi/openai/resources/beta/agents/sessions.rbi +6 -1
  167. data/rbi/openai/resources/images.rbi +69 -103
  168. data/rbi/openai/resources/realtime/translations/client_secrets.rbi +54 -0
  169. data/rbi/openai/resources/realtime/translations.rbi +22 -0
  170. data/rbi/openai/resources/realtime.rbi +3 -0
  171. data/rbi/openai/resources/webhooks.rbi +5 -0
  172. data/sig/openai/helpers/agents.rbs +7 -0
  173. data/sig/openai/helpers/beta/agents/artifacts.rbs +20 -0
  174. data/sig/openai/helpers/beta/agents/files.rbs +41 -0
  175. data/sig/openai/helpers/beta/agents/function_tool.rbs +24 -0
  176. data/sig/openai/helpers/beta/agents/model_adapter.rbs +16 -0
  177. data/sig/openai/helpers/beta/agents.rbs +71 -0
  178. data/sig/openai/helpers/structured_output.rbs +12 -0
  179. data/sig/openai/internal/transport/base_client.rbs +8 -0
  180. data/sig/openai/internal/type/base_model.rbs +3 -1
  181. data/sig/openai/internal/type/converter.rbs +16 -2
  182. data/sig/openai/internal/type/union.rbs +2 -1
  183. data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +20 -3
  184. data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +20 -3
  185. data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +20 -3
  186. data/sig/openai/models/admin/organization/usage_completions_response.rbs +20 -3
  187. data/sig/openai/models/admin/organization/usage_costs_response.rbs +20 -3
  188. data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +20 -3
  189. data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +20 -3
  190. data/sig/openai/models/admin/organization/usage_images_response.rbs +20 -3
  191. data/sig/openai/models/admin/organization/usage_moderations_response.rbs +20 -3
  192. data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +20 -3
  193. data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +20 -3
  194. data/sig/openai/models/audio/speech_create_params.rbs +4 -0
  195. data/sig/openai/models/audio/voice.rbs +70 -0
  196. data/sig/openai/models/audio/voice_create_params.rbs +203 -0
  197. data/sig/openai/models/beta/agent_create_params.rbs +1 -5
  198. data/sig/openai/models/beta/agent_update_params.rbs +1 -5
  199. data/sig/openai/models/beta/agents/session_create_params.rbs +1 -5
  200. data/sig/openai/models/beta/agents/session_update_params.rbs +1 -5
  201. data/sig/openai/models/beta/agents/sessions/session_trace.rbs +65 -0
  202. data/sig/openai/models/beta/agents/sessions/trace_list_params.rbs +81 -0
  203. data/sig/openai/models/beta/agents/vaults/credential.rbs +5 -5
  204. data/sig/openai/models/beta/beta_response_output_text.rbs +2 -2
  205. data/sig/openai/models/beta/beta_response_output_text_annotation_added_event.rbs +2 -2
  206. data/sig/openai/models/beta/beta_tool.rbs +3 -9
  207. data/sig/openai/models/beta/environment.rbs +22 -0
  208. data/sig/openai/models/beta/environment_param.rbs +25 -0
  209. data/sig/openai/models/beta/session_turn_error.rbs +4 -0
  210. data/sig/openai/models/chat/chat_completion_audio_param.rbs +4 -2
  211. data/sig/openai/models/chat/chat_completion_content_part_image.rbs +2 -1
  212. data/sig/openai/models/image.rbs +21 -21
  213. data/sig/openai/models/image_create_variation_params.rbs +4 -4
  214. data/sig/openai/models/image_edit_params.rbs +46 -60
  215. data/sig/openai/models/image_generate_params.rbs +23 -45
  216. data/sig/openai/models/image_model.rbs +6 -6
  217. data/sig/openai/models/live/fork_server_event.rbs +17 -2
  218. data/sig/openai/models/live/server_event.rbs +17 -2
  219. data/sig/openai/models/realtime/realtime_audio_config_output.rbs +9 -6
  220. data/sig/openai/models/realtime/realtime_response_create_audio_output.rbs +9 -6
  221. data/sig/openai/models/realtime/translations/client_secret_create_params.rbs +39 -0
  222. data/sig/openai/models/responses/response_output_text.rbs +2 -2
  223. data/sig/openai/models/responses/response_output_text_annotation_added_event.rbs +2 -2
  224. data/sig/openai/models/responses/tool.rbs +3 -9
  225. data/sig/openai/models/webhooks/agent_session_action_required_webhook_event.rbs +114 -0
  226. data/sig/openai/models/webhooks/agent_session_created_webhook_event.rbs +115 -0
  227. data/sig/openai/models/webhooks/agent_session_failed_webhook_event.rbs +89 -0
  228. data/sig/openai/models/webhooks/agent_session_idle_webhook_event.rbs +89 -0
  229. data/sig/openai/models/webhooks/agent_session_in_progress_webhook_event.rbs +89 -0
  230. data/sig/openai/models/webhooks/unwrap_webhook_event.rbs +6 -1
  231. data/sig/openai/models/webhooks/webhook_create_params.rbs +10 -0
  232. data/sig/openai/models/webhooks/webhook_test_params.rbs +10 -0
  233. data/sig/openai/models/webhooks/webhook_update_params.rbs +10 -0
  234. data/sig/openai/resources/audio/voices.rbs +14 -0
  235. data/sig/openai/resources/audio.rbs +2 -0
  236. data/sig/openai/resources/beta/agents/environments/files.rbs +17 -0
  237. data/sig/openai/resources/beta/agents/sessions/artifacts.rbs +4 -0
  238. data/sig/openai/resources/beta/agents/sessions/traces.rbs +21 -0
  239. data/sig/openai/resources/beta/agents/sessions.rbs +4 -1
  240. data/sig/openai/resources/realtime/translations/client_secrets.rbs +17 -0
  241. data/sig/openai/resources/realtime/translations.rbs +11 -0
  242. data/sig/openai/resources/realtime.rbs +2 -0
  243. data/sig/openai/resources/webhooks.rbs +6 -1
  244. metadata +61 -1
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model_adapter"
4
+
5
+ module OpenAI
6
+ module Helpers
7
+ module Beta
8
+ module Agents
9
+ # Beta binding between an application's argument model and local callback.
10
+ class FunctionTool
11
+ # @return [String]
12
+ attr_reader :name
13
+
14
+ # @param name [String]
15
+ # @param arguments [Class<OpenAI::BaseModel>]
16
+ # @param description [String]
17
+ # @yieldparam arguments [OpenAI::BaseModel]
18
+ def initialize(name:, arguments:, description: "", &handler)
19
+ ModelAdapter.validate!(arguments, parameter: "arguments")
20
+
21
+ raise ArgumentError, "a callback is required" unless handler
22
+ raise ArgumentError, "name must be a nonempty string" unless name.is_a?(String) && !name.empty?
23
+
24
+ @name = name.dup.freeze
25
+ @arguments = arguments
26
+ @handler = handler
27
+ @definition = JSON
28
+ .generate(
29
+ type: :function,
30
+ name: @name,
31
+ parameters: arguments.to_json_schema,
32
+ description: description
33
+ )
34
+ .freeze
35
+ end
36
+
37
+ # Supply this definition in the agent's tools at creation.
38
+ # @return [Hash{Symbol=>Object}]
39
+ def definition = JSON.parse(@definition, symbolize_names: true)
40
+
41
+ # Pass these local bindings to sessions.stream(tool_handlers: ...).
42
+ # @return [Hash{String=>Proc}]
43
+ def handlers = {name => method(:call).to_proc}
44
+
45
+ # Parse using the SDK's BaseModel conventions, then invoke the callback.
46
+ # This is not JSON Schema validation; validate application constraints
47
+ # and authorization in the callback before performing side effects.
48
+ # @param arguments [Hash{String=>Object}, String]
49
+ # @return [Object]
50
+ def call(arguments)
51
+ values = JSON.parse(arguments.is_a?(String) ? arguments : JSON.generate(arguments), symbolize_names: true)
52
+ raise ArgumentError, "Tool arguments must be a JSON object" unless values.is_a?(Hash)
53
+ parsed = ModelAdapter.coerce(@arguments, values)
54
+ unless parsed
55
+ raise ArgumentError, "Tool arguments cannot be parsed into the argument model"
56
+ end
57
+
58
+ @handler.call(parsed)
59
+ end
60
+
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Beta
6
+ module Agents
7
+ # Shared native model checks; JSON and wire-schema policies stay with each caller.
8
+ # @api private
9
+ module ModelAdapter
10
+ def self.validate!(model, parameter:)
11
+ unless model.is_a?(Class) && model < OpenAI::BaseModel
12
+ raise ArgumentError, "#{parameter} must be an OpenAI::BaseModel subclass"
13
+ end
14
+ end
15
+
16
+ def self.coerce(model, value, memoize: false)
17
+ state = OpenAI::Internal::Type::Converter.new_coerce_state(memoize: memoize)
18
+ parsed = OpenAI::Internal::Type::Converter.coerce(model, value, state: state)
19
+ parsed if parsed.is_a?(model) && state[:exactness][:no].zero?
20
+ end
21
+ end
22
+
23
+ private_constant :ModelAdapter
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Beta
6
+ module Agents
7
+ # The final output of one completed root turn (beta).
8
+ # Messages contain final assistant answers, not the session transcript.
9
+ class TurnResult
10
+ attr_reader :turn, :messages
11
+
12
+ # @api private
13
+ def initialize(turn:, messages:)
14
+ @turn = turn
15
+ @messages = messages.freeze
16
+ end
17
+
18
+ def session_id = turn.session_id
19
+ def turn_id = turn.id
20
+
21
+ # Joins text without adding separators or making requests.
22
+ def output_text = messages.map(&:output_text).join
23
+
24
+ # A raw result has no parser attached.
25
+ def output_parsed = nil
26
+
27
+ # Parse this completed answer without changing the hosted session.
28
+ def parse(output_type:)
29
+ OutputParser.new(output_type).parse(self)
30
+ end
31
+ end
32
+
33
+ # Collection failed, or the observed turn did not complete successfully.
34
+ # A transport failure describes observation, not failure of hosted work.
35
+ class ResultError < OpenAI::Errors::Error
36
+ attr_reader :reason, :session_id, :turn, :messages, :required_actions
37
+
38
+ # @api private
39
+ def initialize(reason:, session_id:, turn:, messages:, required_actions: [])
40
+ @reason = reason
41
+ @session_id = session_id
42
+ @turn = turn
43
+ @messages = messages.freeze
44
+ @required_actions = required_actions.freeze
45
+ super("Unable to collect the agent turn result: #{reason}")
46
+ end
47
+
48
+ def turn_id = turn&.id
49
+ end
50
+
51
+ # @api private
52
+ class ResultCollector
53
+ def initialize(session_id: nil, handler_names: [])
54
+ @session_id = session_id
55
+ @handler_names = handler_names
56
+ @messages = {}
57
+ @required_actions = []
58
+ end
59
+
60
+ def enable
61
+ return if @enabled
62
+ if @started
63
+ raise ArgumentError, "Call with_result_collection before consuming events to collect a final result"
64
+ end
65
+
66
+ @enabled = true
67
+ end
68
+
69
+ def observe(event)
70
+ @started = true
71
+ return unless @enabled
72
+ return if @finished || !event.is_a?(OpenAI::Internal::Type::BaseModel)
73
+
74
+ case event.type
75
+ when :"agent.session.created"
76
+ @session_id ||= event.session.id.dup
77
+ when :"agent.session.turn.created"
78
+ if @turn.nil? && event.turn.subagent_id.nil? && (@session_id.nil? || event.session_id == @session_id)
79
+ @turn = copy(event.turn)
80
+ @session_id = @turn.session_id
81
+ end
82
+
83
+ when :"agent.session.turn.completed", :"agent.session.turn.failed", :"agent.session.turn.cancelled"
84
+ if @turn && event.turn_id == @turn.id
85
+ @turn = copy(event.turn)
86
+ @terminal = true
87
+ @required_actions = []
88
+ end
89
+
90
+ when :"agent.session.turn.item.done"
91
+ item = event.item
92
+ if @turn &&
93
+ item.is_a?(OpenAI::Models::Beta::AgentSessionAssistantMessage) &&
94
+ item.turn_id == @turn.id &&
95
+ item.role == :assistant &&
96
+ item.status == :completed &&
97
+ item.phase != :commentary
98
+ @messages[item.id] = [event.output_index, copy(item, OpenAI::Models::Beta::AgentSessionMessage)]
99
+ end
100
+
101
+ when :"agent.session.requires_action"
102
+ @required_actions = event.session.required_actions.filter_map do |action|
103
+ if action.is_a?(OpenAI::Models::Beta::AgentSession::RequiredAction::FunctionCall) &&
104
+ @handler_names.include?(action.name)
105
+ next
106
+ end
107
+
108
+ copy(action)
109
+ end
110
+
111
+ when :"agent.session.in_progress"
112
+ @required_actions = [] if event.session.id == @session_id
113
+ when :"agent.session.idle"
114
+ @finished = true if @terminal && event.session.id == @session_id
115
+ when :"agent.session.failed"
116
+ @session_id ||= event.session.id.dup
117
+ @failure = :failed
118
+ @finished = true
119
+ end
120
+ end
121
+
122
+ def stopped?
123
+ @finished || !@required_actions.empty? || (@terminal && @turn.status != :completed)
124
+ end
125
+
126
+ def observe_error(error)
127
+ @cause ||= error if @enabled && !@finished
128
+ end
129
+
130
+ def result
131
+ return @result if @result
132
+ raise @result_error if @result_error
133
+ raise error(:observation_error), cause: @cause if @cause
134
+ raise error(@failure) if @failure
135
+ raise error(@turn.status) if @terminal && @turn.status != :completed
136
+ raise error(:requires_action) unless @required_actions.empty?
137
+ raise error(:incomplete) unless @finished && @turn&.status == :completed
138
+
139
+ @result = TurnResult.new(turn: @turn, messages: take_messages)
140
+ end
141
+
142
+ private
143
+
144
+ def take_messages
145
+ messages = @messages.values.sort_by(&:first).map(&:last)
146
+ @messages.clear
147
+ messages
148
+ end
149
+
150
+ def error(reason)
151
+ @result_error = ResultError.new(
152
+ reason: reason,
153
+ session_id: @session_id,
154
+ turn: @turn,
155
+ messages: take_messages,
156
+ required_actions: @required_actions
157
+ )
158
+ end
159
+
160
+ def copy(value, model = value.class)
161
+ raw = JSON.parse(JSON.generate(value), symbolize_names: true)
162
+ OpenAI::Internal::Type::Converter.coerce(model, raw)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ require_relative "typed_output"
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model_adapter"
4
+
5
+ module OpenAI
6
+ module Helpers
7
+ module Beta
8
+ module Agents
9
+ # A completed turn with output hydrated into the requested Ruby model (beta).
10
+ class ParsedTurnResult < TurnResult
11
+ attr_reader :raw_result, :output_parsed
12
+
13
+ # @api private
14
+ def initialize(raw_result:, output_parsed:)
15
+ @raw_result = raw_result
16
+ @output_parsed = output_parsed
17
+ super(turn: raw_result.turn, messages: raw_result.messages)
18
+ end
19
+ end
20
+
21
+ # Hosted work completed, but its answer did not match the requested model.
22
+ class OutputParseError < OpenAI::Errors::Error
23
+ attr_reader :raw_result
24
+
25
+ # @api private
26
+ def initialize(raw_result:)
27
+ @raw_result = raw_result
28
+ super("The completed agent output does not match output_type")
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ class OutputParser
34
+ def initialize(model)
35
+ ModelAdapter.validate!(model, parameter: "output_type")
36
+
37
+ @model = model
38
+ end
39
+
40
+ def prepare_request(params)
41
+ agent = params.fetch(:agent, nil).to_h.transform_keys { _1.is_a?(String) ? _1.to_sym : _1 }
42
+ text = agent.fetch(:text, nil).to_h.transform_keys { _1.is_a?(String) ? _1.to_sym : _1 }
43
+ if text.key?(:format) || text.key?(:format_)
44
+ raise ArgumentError, "output_type cannot be combined with agent.text.format"
45
+ end
46
+
47
+ @schema = JSON.parse(JSON.generate(@model.to_json_schema, max_nesting: false), max_nesting: false)
48
+ normalize_references
49
+ params[:agent] = agent.merge(text: text.merge(format: {type: :json_schema, schema: @schema}))
50
+ end
51
+
52
+ def parse(result)
53
+ return @result if @result && @result.raw_result.equal?(result)
54
+ raise @error if @error && @error.raw_result.equal?(result)
55
+
56
+ first = nil
57
+ result.messages.each do |message|
58
+ message.content.each do |part|
59
+ next unless (part[:type] || (part["type"] if part.is_a?(Hash))).to_s == "output_text"
60
+ text = part[:text] || (part["text"] if part.is_a?(Hash))
61
+ value = JSON.parse(text, symbolize_names: true, max_nesting: false)
62
+ parsed = ModelAdapter.coerce(@model, value, memoize: true)
63
+ raise TypeError, "Output cannot be parsed into output_type" unless parsed
64
+ first ||= parsed
65
+ end
66
+ end
67
+
68
+ raise TypeError, "Output does not contain structured text" unless first
69
+
70
+ @result = ParsedTurnResult.new(raw_result: result, output_parsed: first)
71
+ rescue StandardError, SystemStackError => error
72
+ raise if error.equal?(@error)
73
+ @error = OutputParseError.new(raw_result: result)
74
+ raise @error, cause: nil
75
+ end
76
+
77
+ private
78
+
79
+ def resolve(ref)
80
+ return unless ref.is_a?(String) && ref.start_with?("#/")
81
+
82
+ URI::RFC2396_PARSER
83
+ .unescape(ref.delete_prefix("#/"))
84
+ .split("/", -1)
85
+ .reduce(@schema) { |node, token| node[token.gsub("~1", "/").gsub("~0", "~")] if node.is_a?(Hash) }
86
+ end
87
+
88
+ def normalize_references
89
+ return unless @schema.is_a?(Hash) && @schema["$defs"].is_a?(Hash)
90
+ return if @schema.key?("$ref") && !(@schema.keys - %w[$defs $ref]).empty?
91
+ definitions = @schema.fetch("$defs")
92
+ return if definitions.empty? || !definitions.values.all? { _1.is_a?(Hash) }
93
+
94
+ names = {}.compare_by_identity
95
+ definitions.each_value.with_index { |definition, index| names[definition] = "model_#{index}" }
96
+ nodes = schema_nodes
97
+ return unless references_covered?(nodes)
98
+ references = nodes.select { _1.key?("$ref") }
99
+ return if references.empty?
100
+ targets = references.map { resolve(_1["$ref"]) }
101
+ # Native Ruby pointers encode definition names; Agents looks up raw suffixes.
102
+ # Unfamiliar references keep their schema and definition names intact.
103
+ return unless targets.all? { names.key?(_1) }
104
+ root = resolve(@schema["$ref"])
105
+ expand_root = root.is_a?(Hash) && !root.key?("$defs") && (@schema.keys - %w[$defs $ref]).empty?
106
+
107
+ references.zip(targets).each do |node, target|
108
+ node["$ref"] = "#/$defs/#{names.fetch(target)}"
109
+ if (node.key?("description") || node.key?("title")) && !node.key?("anyOf")
110
+ node["anyOf"] = [{"$ref" => node.delete("$ref")}]
111
+ end
112
+ end
113
+
114
+ @schema["$defs"] = definitions.to_h { |_name, definition| [names.fetch(definition), definition] }
115
+ @schema = @schema.except("$ref").merge(root) if expand_root
116
+ end
117
+
118
+ def references_covered?(nodes)
119
+ known = nodes.each_with_object({}.compare_by_identity) { |node, seen| seen[node] = true }
120
+ pending = [@schema]
121
+ until pending.empty?
122
+ value = pending.pop
123
+ case value
124
+ when Hash
125
+ if known[value]
126
+ unless (value.keys &
127
+ %w[$id id $anchor $dynamicAnchor $dynamicRef $recursiveAnchor $recursiveRef discriminator])
128
+ .empty?
129
+ return false
130
+ end
131
+
132
+ pending.concat(
133
+ value.except("$ref", "title", "description", "default", "const", "enum", "examples").values
134
+ )
135
+ else
136
+ return false if %w[$ref $dynamicRef $recursiveRef].any? { value[_1].is_a?(String) }
137
+ pending.concat(value.values)
138
+ end
139
+
140
+ when Array
141
+ pending.concat(value)
142
+ when String
143
+ return false if value.start_with?("#/")
144
+ end
145
+ end
146
+
147
+ true
148
+ end
149
+
150
+ def schema_nodes
151
+ nodes = []
152
+ pending = [@schema]
153
+ until pending.empty?
154
+ node = pending.pop
155
+ next unless node.is_a?(Hash)
156
+ nodes << node
157
+ # Visit schema positions only: defaults, examples and enum values are data.
158
+ %w[properties patternProperties $defs definitions dependentSchemas dependencies].each do |key|
159
+ pending.concat(node[key].values.grep(Hash)) if node[key].is_a?(Hash)
160
+ end
161
+
162
+ %w[anyOf allOf oneOf prefixItems].each do |key|
163
+ pending.concat(node[key]) if node[key].is_a?(Array)
164
+ end
165
+
166
+ %w[
167
+ items
168
+ additionalProperties
169
+ additionalItems
170
+ contains
171
+ not
172
+ if
173
+ then
174
+ else
175
+ propertyNames
176
+ unevaluatedProperties
177
+ unevaluatedItems
178
+ ]
179
+ .each do |key|
180
+ pending.concat(node[key].is_a?(Array) ? node[key] : [node[key]]) if node.key?(key)
181
+ end
182
+ end
183
+
184
+ nodes
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -99,10 +99,16 @@ module OpenAI
99
99
  end
100
100
 
101
101
  private def write_text(text)
102
+ complete = false
102
103
  super
104
+ complete = true
105
+ nil
103
106
  rescue StandardError
104
- @poisoned = true
105
107
  raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
108
+ ensure
109
+ # Also retire a possibly buffered write when cancellation bypasses
110
+ # StandardError, before another sender or normal cleanup can flush it.
111
+ @poisoned = true unless complete
106
112
  end
107
113
 
108
114
  private def read_raw_message
@@ -7,7 +7,7 @@ module OpenAI
7
7
  # exceptions local to Realtime instead of weakening SDK-wide union behavior.
8
8
  module ConversationItem
9
9
  class << self
10
- private def resolve_variant(value)
10
+ private def resolve_variant(value, state: nil)
11
11
  return super unless value.is_a?(Hash)
12
12
 
13
13
  type = value.fetch(:type) { value.fetch("type", OpenAI::Internal::OMIT) }
@@ -177,10 +177,16 @@ module OpenAI
177
177
  end
178
178
 
179
179
  private def write_text(text)
180
+ complete = false
180
181
  super
182
+ complete = true
183
+ nil
181
184
  rescue StandardError
182
- @poisoned = true
183
185
  raise OpenAI::Errors::ResponsesSendError.new(url: @url), cause: nil
186
+ ensure
187
+ # Cancellation can bypass StandardError. Poison before another lane
188
+ # can send, so normal cleanup cannot flush an uncertain write.
189
+ @poisoned = true unless complete
184
190
  end
185
191
 
186
192
  private def with_read_lease
@@ -495,8 +495,12 @@ module OpenAI
495
495
  raise
496
496
  rescue OpenAI::Errors::ResponsesSendError => error
497
497
  @error = error
498
- stop_reader
499
498
  raise
499
+ ensure
500
+ if @connection.poisoned?
501
+ @error ||= OpenAI::Errors::ResponsesSendError.new(url: @connection.url)
502
+ stop_reader
503
+ end
500
504
  end
501
505
 
502
506
  # @api private
@@ -117,7 +117,10 @@ module OpenAI
117
117
  decoded = +"".b
118
118
  begin
119
119
  inflater.inflate(buffer + ::Protocol::WebSocket::Extension::Compression::Inflate::TRAILER) do |chunk|
120
- if inflater.total_out - start_out > @responses_decoded_limit
120
+ if inflater.total_out -
121
+ start_out > @responses_decoded_limit ||
122
+ decoded.bytesize +
123
+ chunk.bytesize > @responses_decoded_limit
121
124
  raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
122
125
  end
123
126
 
@@ -129,7 +132,14 @@ module OpenAI
129
132
  raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
130
133
  end
131
134
 
132
- decoded << inflater.flush_next_out
135
+ # Finished streams can put unused input in this buffer without
136
+ # increasing total_out. Count every byte before collecting it.
137
+ tail = inflater.flush_next_out
138
+ if decoded.bytesize + tail.bytesize > @responses_decoded_limit
139
+ raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
140
+ end
141
+
142
+ decoded << tail
133
143
  rescue ::Protocol::WebSocket::ProtocolError
134
144
  @responses_bounded_framer.abort
135
145
  inflater.close
@@ -823,6 +823,24 @@ module OpenAI
823
823
  end
824
824
  end
825
825
 
826
+ # @api private
827
+ # Consume binary response chunks without buffering a complete body.
828
+ def request_streaming_body(req, on_response = nil)
829
+ if req[:options].to_h[:include_raw_body]
830
+ raise ArgumentError, "include_raw_body is not supported for streaming downloads"
831
+ end
832
+
833
+ _url, response, log_context = perform_request(req)
834
+ finish_request(log_context, response) do
835
+ on_response&.call
836
+ response.body.each { |chunk| yield chunk }
837
+ nil
838
+ end
839
+
840
+ ensure
841
+ OpenAI::Internal::Util.close_fused!(response.body) if response
842
+ end
843
+
826
844
  # @api private
827
845
  #
828
846
  # @param req [Hash{Symbol=>Object}]
@@ -94,7 +94,7 @@ module OpenAI
94
94
  return value
95
95
  end
96
96
 
97
- target = item_type
97
+ target = OpenAI::Internal::Type::Converter.resolve_type(@item_type_fn, state: state)
98
98
  exactness[:yes] += 1
99
99
  previous_error = state.fetch(:error)
100
100
  last_error = nil
@@ -28,9 +28,12 @@ module OpenAI
28
28
  # @api private
29
29
  #
30
30
  # @return [Hash{Symbol=>Hash{Symbol=>Object}}]
31
- def fields
31
+ def fields(state: nil)
32
32
  known_fields.transform_values do |field|
33
- {**field.except(:type_fn), type: field.fetch(:type_fn).call}
33
+ {
34
+ **field.except(:type_fn),
35
+ type: OpenAI::Internal::Type::Converter.resolve_type(field.fetch(:type_fn), state: state)
36
+ }
34
37
  end
35
38
  end
36
39
 
@@ -297,7 +300,7 @@ module OpenAI
297
300
  data = instance.to_h
298
301
  viability = instance.instance_variable_get(:@coerced)
299
302
 
300
- fields.each do |name, field|
303
+ fields(state: state).each do |name, field|
301
304
  mode, required, target = field.fetch_values(:mode, :required, :type)
302
305
  api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const)
303
306
  src_name = state.fetch(:translate_names) ? api_name : name