activeagent 1.0.0.rc1 → 1.0.1

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/lib/active_agent/providers/_base_provider.rb +94 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  6. data/lib/active_agent/providers/anthropic/request.rb +157 -78
  7. data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
  8. data/lib/active_agent/providers/anthropic_provider.rb +159 -59
  9. data/lib/active_agent/providers/common/messages/_types.rb +46 -3
  10. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  11. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  12. data/lib/active_agent/providers/common/usage.rb +385 -0
  13. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  14. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  15. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  16. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  17. data/lib/active_agent/providers/mock_provider.rb +23 -23
  18. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  19. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  20. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  21. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  22. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  23. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  24. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  25. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  26. data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
  27. data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
  28. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  29. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  30. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  31. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  32. data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
  33. data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
  34. data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
  35. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  36. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  37. data/lib/active_agent/providers/open_router/options.rb +49 -1
  38. data/lib/active_agent/providers/open_router/request.rb +252 -66
  39. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  40. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  41. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  42. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  43. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  44. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  45. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  46. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  47. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  48. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  49. data/lib/active_agent/providers/open_router/transforms.rb +164 -0
  50. data/lib/active_agent/providers/open_router_provider.rb +23 -0
  51. data/lib/active_agent/version.rb +1 -1
  52. metadata +17 -160
  53. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  54. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  55. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  56. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  57. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  58. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  59. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  60. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  61. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  62. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  78. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  79. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  80. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  81. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  82. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  83. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  84. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  85. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  86. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  87. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  88. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  91. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  92. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  93. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  94. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  95. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  96. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  97. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  98. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  99. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  100. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  101. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  102. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  116. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  117. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  118. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  119. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  120. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  121. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  122. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  123. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  124. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  125. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  126. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  127. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  128. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  129. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  130. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  131. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  162. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  163. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  166. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  167. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  168. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  170. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  171. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  172. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  173. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  174. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  175. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  184. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  185. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  186. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  187. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  188. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  189. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  190. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  191. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module OpenAI
8
+ module Responses
9
+ # Provides transformation methods for normalizing response parameters
10
+ # to OpenAI gem's native format
11
+ #
12
+ # Handles input normalization, message conversion, and response format
13
+ # transformation for the Responses API.
14
+ module Transforms
15
+ class << self
16
+ # Converts gem model object to hash via JSON round-trip
17
+ #
18
+ # @param gem_object [Object]
19
+ # @return [Hash] with symbolized keys
20
+ def gem_to_hash(gem_object)
21
+ JSON.parse(gem_object.to_json, symbolize_names: true)
22
+ end
23
+
24
+ # Normalizes tools from common format to OpenAI Responses API format.
25
+ #
26
+ # Accepts tools in multiple formats:
27
+ # - Common format: `{name: "...", description: "...", parameters: {...}}`
28
+ # - Nested format: `{type: "function", function: {name: "...", ...}}`
29
+ # - Responses format: `{type: "function", name: "...", parameters: {...}}`
30
+ #
31
+ # Always outputs flat Responses API format.
32
+ #
33
+ # @param tools [Array<Hash>]
34
+ # @return [Array<Hash>]
35
+ def normalize_tools(tools)
36
+ return tools unless tools.is_a?(Array)
37
+
38
+ tools.map do |tool|
39
+ tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool
40
+
41
+ # If already in Responses format (flat with type, name, parameters), return as-is
42
+ if tool_hash[:type] == "function" && tool_hash[:name]
43
+ next tool_hash
44
+ end
45
+
46
+ # If in nested Chat API format, flatten it
47
+ if tool_hash[:type] == "function" && tool_hash[:function]
48
+ func = tool_hash[:function]
49
+ next {
50
+ type: "function",
51
+ name: func[:name],
52
+ description: func[:description],
53
+ parameters: func[:parameters] || func[:input_schema]
54
+ }.compact
55
+ end
56
+
57
+ # If in common format (no type field), convert to Responses format
58
+ if tool_hash[:name] && !tool_hash[:type]
59
+ next {
60
+ type: "function",
61
+ name: tool_hash[:name],
62
+ description: tool_hash[:description],
63
+ parameters: tool_hash[:parameters] || tool_hash[:input_schema]
64
+ }.compact
65
+ end
66
+
67
+ # Pass through other formats
68
+ tool_hash
69
+ end
70
+ end
71
+
72
+ # Normalizes MCP servers from common format to OpenAI Responses API format.
73
+ #
74
+ # Common format:
75
+ # {name: "stripe", url: "https://...", authorization: "token"}
76
+ # OpenAI format:
77
+ # {type: "mcp", server_label: "stripe", server_url: "https://...", authorization: "token"}
78
+ #
79
+ # @param mcp_servers [Array<Hash>]
80
+ # @return [Array<Hash>]
81
+ def normalize_mcp_servers(mcp_servers)
82
+ return mcp_servers unless mcp_servers.is_a?(Array)
83
+
84
+ mcp_servers.map do |server|
85
+ server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server
86
+
87
+ # If already in OpenAI format (has type: "mcp" and server_label), return as-is
88
+ if server_hash[:type] == "mcp" && server_hash[:server_label]
89
+ next server_hash
90
+ end
91
+
92
+ # Convert common format to OpenAI format
93
+ result = {
94
+ type: "mcp",
95
+ server_label: server_hash[:name] || server_hash[:server_label],
96
+ server_url: server_hash[:url] || server_hash[:server_url]
97
+ }
98
+
99
+ # Keep authorization field (OpenAI uses 'authorization', not 'authorization_token')
100
+ if server_hash[:authorization]
101
+ result[:authorization] = server_hash[:authorization]
102
+ end
103
+
104
+ result.compact
105
+ end
106
+ end
107
+
108
+ # Normalizes tool_choice from common format to OpenAI Responses API format.
109
+ #
110
+ # Responses API uses flat format for specific tool choice, unlike Chat API's nested format.
111
+ # Must return gem model objects for proper serialization.
112
+ #
113
+ # Maps:
114
+ # - "required" → :required symbol (force tool use)
115
+ # - "auto" → :auto symbol (let model decide)
116
+ # - { name: "..." } → ToolChoiceFunction model object
117
+ #
118
+ # @param tool_choice [String, Hash, Object]
119
+ # @return [Symbol, Object] Symbol or gem model object
120
+ def normalize_tool_choice(tool_choice)
121
+ # If already a gem model object, return as-is
122
+ return tool_choice if tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceFunction) ||
123
+ tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceAllowed) ||
124
+ tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceTypes) ||
125
+ tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceMcp) ||
126
+ tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceCustom)
127
+
128
+ case tool_choice
129
+ when "required"
130
+ :required # Return as symbol
131
+ when "auto"
132
+ :auto # Return as symbol
133
+ when "none"
134
+ :none # Return as symbol
135
+ when Hash
136
+ choice_hash = tool_choice.deep_symbolize_keys
137
+
138
+ # If already in proper format with type, try to create gem model
139
+ if choice_hash[:type] == "function" && choice_hash[:name]
140
+ # Create ToolChoiceFunction gem model object
141
+ ::OpenAI::Models::Responses::ToolChoiceFunction.new(
142
+ type: :function,
143
+ name: choice_hash[:name]
144
+ )
145
+ # Convert { name: "..." } to ToolChoiceFunction model
146
+ elsif choice_hash[:name] && !choice_hash[:type]
147
+ ::OpenAI::Models::Responses::ToolChoiceFunction.new(
148
+ type: :function,
149
+ name: choice_hash[:name]
150
+ )
151
+ else
152
+ choice_hash
153
+ end
154
+ else
155
+ tool_choice
156
+ end
157
+ end
158
+
159
+ # Simplifies input for cleaner API requests
160
+ #
161
+ # Unwraps single-element arrays:
162
+ # - `["text"]` → `"text"`
163
+ # - `[{type: "input_text", text: "..."}]` → `"..."`
164
+ # - `[{role: "user", content: "..."}]` → `"..."`
165
+ #
166
+ # @param input [Array, String, Hash]
167
+ # @return [String, Array, Hash]
168
+ def simplify_input(input)
169
+ return input unless input.is_a?(Array)
170
+
171
+ # Single string element - unwrap it
172
+ if input.size == 1 && input[0].is_a?(String)
173
+ return input[0]
174
+ end
175
+
176
+ # Single content object {type: "input_text", text: "..."} - unwrap to string
177
+ if input.size == 1 &&
178
+ input[0].is_a?(Hash) &&
179
+ input[0][:type] == "input_text" &&
180
+ input[0][:text].is_a?(String) &&
181
+ input[0].keys.sort == [ :text, :type ]
182
+ return input[0][:text]
183
+ end
184
+
185
+ # Single message with string content - simplify to string
186
+ if input.size == 1 &&
187
+ input[0].is_a?(Hash) &&
188
+ input[0][:role] == "user" &&
189
+ input[0][:content].is_a?(String)
190
+ return input[0][:content]
191
+ end
192
+
193
+ input
194
+ end
195
+
196
+ # Normalizes response_format to OpenAI Responses API text parameter
197
+ #
198
+ # Maps common response_format structures to Responses API format.
199
+ # Returns ResponseTextConfig object to preserve proper nesting.
200
+ #
201
+ # @param format [Hash, Symbol, String]
202
+ # @return [OpenAI::Models::Responses::ResponseTextConfig]
203
+ def normalize_response_format(format)
204
+ text_hash = case format
205
+ when Hash
206
+ if format[:type] == "json_schema" || format[:type] == :json_schema
207
+ # json_schema format: map to Responses API structure
208
+ {
209
+ format: {
210
+ type: "json_schema",
211
+ name: format[:name] || format[:json_schema]&.dig(:name),
212
+ schema: format[:schema] || format[:json_schema]&.dig(:schema),
213
+ strict: format[:strict] || format[:json_schema]&.dig(:strict)
214
+ }.compact
215
+ }
216
+ elsif format[:type] == "json_object" || format[:type] == :json_object
217
+ # json_object format
218
+ { format: { type: "json_object" } }
219
+ elsif format[:type]
220
+ # Other simple type formats (text, etc.) - wrap in format key
221
+ { format: { type: format[:type].to_s } }
222
+ else
223
+ # Pass through other hash formats (already has format key or complex structure)
224
+ format
225
+ end
226
+ when Symbol, String
227
+ # Simple format types
228
+ { format: { type: format.to_s } }
229
+ else
230
+ format
231
+ end
232
+
233
+ # Convert hash to ResponseTextConfig object to preserve nesting
234
+ ::OpenAI::Models::Responses::ResponseTextConfig.new(**text_hash)
235
+ end
236
+
237
+ # Normalizes input/messages to gem-compatible format
238
+ #
239
+ # Handles various input formats:
240
+ # - `"text"` → string (passthrough)
241
+ # - `{role: "user", content: "..."}` → wrapped in array
242
+ # - `[{text: "..."}, {image: "url"}]` → wrapped as user message with content array
243
+ # - `["msg1", "msg2"]` → array of user messages
244
+ #
245
+ # @param input [String, Hash, Array, Object]
246
+ # @return [String, Array<Hash>]
247
+ def normalize_input(input)
248
+ # String inputs pass through unchanged
249
+ return input if input.is_a?(String)
250
+
251
+ # Single hash should be wrapped in an array
252
+ if input.is_a?(Hash)
253
+ return [ normalize_message(input) ]
254
+ end
255
+
256
+ # Handle arrays
257
+ return input unless input.respond_to?(:map)
258
+
259
+ # Check if this is an array of content items (strings or text/image/document hashes)
260
+ # Content items don't have a :role key (messages do)
261
+ # BUT NOT a single string (which should have been caught above)
262
+ all_content_items = input.size > 1 && input.all? do |item|
263
+ if item.is_a?(String)
264
+ true
265
+ elsif item.is_a?(Hash)
266
+ # If it has a role, it's a message, not a content item
267
+ !item.key?(:role) && (item.key?(:text) || item.key?(:image) || item.key?(:document))
268
+ else
269
+ false
270
+ end
271
+ end
272
+
273
+ if all_content_items
274
+ # These are multiple content items, wrap in a user message
275
+ content = input.map { |item| normalize_message(item) }
276
+ return [ { role: "user", content: content } ]
277
+ end
278
+
279
+ # Otherwise treat as array of messages
280
+ input.map { |item| normalize_message(item, context: :input) }
281
+ end
282
+
283
+ # Normalizes a single message to hash format
284
+ #
285
+ # Handles shorthand formats:
286
+ # - `{text: "..."}` → user message
287
+ # - `{image: "url"}` → input_image content part
288
+ # - `{document: "url"}` → input_file content part
289
+ #
290
+ # @param message [Hash, String, Object]
291
+ # @param context [Symbol] :input for messages, :content for content parts
292
+ # @return [Hash, String]
293
+ def normalize_message(message, context: :content)
294
+ # If it's our custom model object, serialize it
295
+ if message.respond_to?(:serialize)
296
+ message.serialize
297
+ elsif message.is_a?(Hash)
298
+ # If it has a role, it's a message - convert :text to :content
299
+ if message.key?(:role)
300
+ normalized = message.dup
301
+ if normalized.key?(:text) && !normalized.key?(:content)
302
+ normalized[:content] = normalized.delete(:text)
303
+ end
304
+ return normalized
305
+ end
306
+
307
+ # Expand shorthand formats to full structures for content items
308
+ if message.key?(:image)
309
+ { type: "input_image", image_url: message[:image] }
310
+ elsif message.key?(:document)
311
+ document_value = message[:document]
312
+ if document_value.start_with?("data:")
313
+ { type: "input_file", filename: "document.pdf", file_data: document_value }
314
+ else
315
+ { type: "input_file", file_url: document_value }
316
+ end
317
+ elsif message.key?(:text) && message.size == 1
318
+ # Single :text key without :role - treat as user message
319
+ { role: "user", content: message[:text] }
320
+ elsif message.key?(:text)
321
+ # Bare text content item with other keys
322
+ { type: "input_text", text: message[:text] }
323
+ else
324
+ message
325
+ end
326
+ elsif message.is_a?(String)
327
+ # Context matters: in input array, strings become messages; in content array, they become input_text
328
+ if context == :input
329
+ { role: "user", content: message }
330
+ else
331
+ { type: "input_text", text: message }
332
+ end
333
+ else
334
+ # Pass through anything else
335
+ message
336
+ end
337
+ end
338
+
339
+ # Cleans up serialized request for API submission
340
+ #
341
+ # Removes default values and simplifies input where possible.
342
+ #
343
+ # @param hash [Hash] serialized request
344
+ # @param defaults [Hash] default values to remove
345
+ # @param gem_object [Object] original gem object
346
+ # @return [Hash] cleaned request hash
347
+ def cleanup_serialized_request(hash, defaults, gem_object)
348
+ # Remove default values that shouldn't be in the request body
349
+ defaults.each do |key, value|
350
+ hash.delete(key) if hash[key] == value
351
+ end
352
+
353
+ # Simplify input when possible for cleaner API requests
354
+ hash[:input] = simplify_input(hash[:input]) if hash[:input]
355
+
356
+ hash
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -1,17 +1,20 @@
1
1
  require_relative "_base"
2
2
  require_relative "responses/_types"
3
+ require_relative "responses/transforms"
3
4
 
4
5
  module ActiveAgent
5
6
  module Providers
6
7
  module OpenAI
7
- # Handles OpenAI's Responses API with improved streaming and structured function calling.
8
+ # Provider implementation for OpenAI's Responses API
8
9
  #
9
- # Uses the newer responses endpoint instead of the chat completions endpoint
10
- # for more reliable streaming and better structured interactions with function calls.
10
+ # Uses the responses endpoint for improved streaming and structured function
11
+ # calling compared to the chat completions endpoint.
11
12
  #
12
13
  # @see Base
13
14
  # @see https://platform.openai.com/docs/api-reference/responses
14
15
  class ResponsesProvider < Base
16
+ include ToolChoiceClearing
17
+
15
18
  # @return [Class]
16
19
  def self.options_klass
17
20
  Options
@@ -24,22 +27,75 @@ module ActiveAgent
24
27
 
25
28
  protected
26
29
 
30
+ # @see BaseProvider#prepare_prompt_request
31
+ # @return [Request]
32
+ def prepare_prompt_request
33
+ prepare_prompt_request_tools
34
+
35
+ super
36
+ end
37
+
38
+ # Extracts function names from Responses API function_call items.
39
+ #
40
+ # @return [Array<String>]
41
+ def extract_used_function_names
42
+ message_stack
43
+ .select { |item| item[:type] == "function_call" }
44
+ .map { |item| item[:name] }
45
+ .compact
46
+ end
47
+
48
+ # Returns true if tool_choice == :required.
49
+ #
50
+ # @return [Boolean]
51
+ def tool_choice_forces_required?
52
+ request.tool_choice == :required
53
+ end
54
+
55
+ # Returns [true, name] if tool_choice is a ToolChoiceFunction model object.
56
+ #
57
+ # @return [Array<Boolean, String|nil>]
58
+ def tool_choice_forces_specific?
59
+ if request.tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceFunction)
60
+ [ true, request.tool_choice.name ]
61
+ else
62
+ [ false, nil ]
63
+ end
64
+ end
65
+
27
66
  # @return [Object] OpenAI client's responses endpoint
28
67
  def api_prompt_executer
29
68
  client.responses
30
69
  end
31
70
 
32
- # Processes streaming response chunks from the Responses API.
71
+ # @see BaseProvider#api_response_normalize
72
+ # @param api_response [OpenAI::Models::Responses::Response]
73
+ # @return [Hash] normalized response hash
74
+ def api_response_normalize(api_response)
75
+ return api_response unless api_response
76
+
77
+ Responses::Transforms.gem_to_hash(api_response)
78
+ end
79
+
80
+ # Processes streaming response chunks from the Responses API
33
81
  #
34
- # Handles event types: response.created, response.output_item.added,
35
- # response.output_text.delta, response.function_call_arguments.delta,
36
- # and response.completed. Updates message stack and broadcasts streaming
37
- # updates to listeners.
82
+ # Event types handled:
83
+ # - `:"response.created"`, `:"response.in_progress"` - response lifecycle
84
+ # - `:"response.output_item.added"` - message or function call added
85
+ # - `:"response.content_part.added"` - content part started
86
+ # - `:"response.output_text.delta"` - incremental text updates
87
+ # - `:"response.output_text.done"` - complete text
88
+ # - `:"response.function_call_arguments.delta"` - function argument updates
89
+ # - `:"response.function_call_arguments.done"` - complete function arguments
90
+ # - `:"response.content_part.done"` - content part completed
91
+ # - `:"response.output_item.done"` - message or function call completed
92
+ # - `:"response.completed"` - response finished
38
93
  #
39
- # @param api_response_event [Hash] streaming response chunk with :type key
94
+ # @param api_response_event [Hash] streaming chunk with :type key
40
95
  # @return [void]
96
+ # @see Base#process_stream_chunk
41
97
  def process_stream_chunk(api_response_event)
42
- instrument("stream_chunk_processing.provider.active_agent", chunk_type: api_response_event.type)
98
+ instrument("stream_chunk.active_agent", chunk_type: api_response_event.type)
43
99
 
44
100
  case api_response_event.type
45
101
  # Response Created
@@ -55,14 +111,14 @@ module ActiveAgent
55
111
 
56
112
  # -> -> -> Content Text Append
57
113
  when :"response.output_text.delta"
58
- message = message_stack.find { _1[:id] == api_response_event[:item_id] }
59
- message[:content] += api_response_event[:delta]
60
- broadcast_stream_update(message, api_response_event[:delta])
114
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
115
+ message[:content] += api_response_event.delta
116
+ broadcast_stream_update(message, api_response_event.delta)
61
117
 
62
118
  # -> -> -> Content Text Completed [Full Text]
63
119
  when :"response.output_text.done"
64
- message = message_stack.find { _1[:id] == api_response_event[:item_id] }
65
- message[:content] = api_response_event[:text]
120
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
121
+ message[:content] = api_response_event.text
66
122
  broadcast_stream_update(message, nil) # Don't double send content
67
123
 
68
124
  # -> -> -> Content Function Call Append
@@ -81,11 +137,17 @@ module ActiveAgent
81
137
  # Once we are finished, close out and run tooling callbacks (Recursive)
82
138
  process_prompt_finished
83
139
  else
84
- fail "Unexpected Response Chunk Type: #{type}"
140
+ raise "Unexpected Response Chunk Type: #{api_response_event.type}"
85
141
  end
86
142
  end
87
143
 
88
- # Processes output item added events from streaming response.
144
+ # Processes output item added events from streaming response
145
+ #
146
+ # Handles message and function_call item types. For messages, adds to stack
147
+ # with empty content. For function calls, waits for completion event.
148
+ #
149
+ # Required because API returns empty array instead of empty string for
150
+ # initial message content due to serialization bug.
89
151
  #
90
152
  # @param api_response_event [Hash] response chunk with :item key
91
153
  # @return [void]
@@ -93,15 +155,19 @@ module ActiveAgent
93
155
  case api_response_event.item.type
94
156
  when :message
95
157
  # PATCH: API returns an empty array instead of empty string due to a bug in their serialization
96
- message_stack << { content: "" }.merge(api_response_event.item.to_h.compact_blank)
158
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item).compact_blank
159
+ message_stack << { content: "" }.merge(item_hash)
97
160
  when :function_call
98
161
  # No-Op: Wait for FC to Land (-> response.output_item.done)
99
162
  else
100
- fail "Unexpected Item Type: #{api_response_event.item.type}"
163
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
101
164
  end
102
165
  end
103
166
 
104
- # Processes output item completion events from streaming response.
167
+ # Processes output item completion events from streaming response
168
+ #
169
+ # For function calls, adds completed item to message stack.
170
+ # For messages, no action needed as content already updated via delta events.
105
171
  #
106
172
  # @param api_response_event [Hash] response chunk with completed :item
107
173
  # @return [void]
@@ -110,35 +176,54 @@ module ActiveAgent
110
176
  when :message
111
177
  # No-Op: Message Up to Date
112
178
  when :function_call
113
- message_stack << api_response_event.item
179
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item)
180
+ message_stack << item_hash
114
181
  else
115
- fail "Unexpected Item Type: #{api_response_event.item.type}"
182
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
116
183
  end
117
184
  end
118
185
 
119
- # Executes function calls and creates output messages for conversation continuation.
186
+ # Executes function calls and creates output messages for conversation continuation
120
187
  #
121
188
  # @param api_function_calls [Array<Hash>] function calls with :call_id and :name keys
122
189
  # @return [void]
190
+ # @see Base#process_function_calls
123
191
  def process_function_calls(api_function_calls)
124
192
  api_function_calls.each do |api_function_call|
125
- instrument("tool_execution.provider.active_agent", tool_name: api_function_call[:name])
193
+ output = instrument("tool_call.active_agent", tool_name: api_function_call[:name]) do
194
+ process_tool_call_function(api_function_call).to_json
195
+ end
126
196
 
127
- message = Responses::Requests::Inputs::FunctionCallOutput.new(
197
+ # Create native gem input item for function call output
198
+ message = ::OpenAI::Models::Responses::ResponseInputItem::FunctionCallOutput.new(
128
199
  call_id: api_function_call[:call_id],
129
- output: process_tool_call_function(api_function_call).to_json
200
+ output:
130
201
  )
131
202
 
132
- message_stack.push(message.serialize)
203
+ # Convert to hash for message_stack
204
+ message_stack.push(Responses::Transforms.gem_to_hash(message))
133
205
  end
134
206
  end
135
207
 
208
+ # Converts OpenAI gem response object to hash for storage.
209
+ #
210
+ # @param api_response [OpenAI::Models::Responses::Response]
211
+ # @return [Common::PromptResponse, nil]
212
+ def process_prompt_finished(api_response = nil)
213
+ # Convert gem object to hash so that raw_response["usage"] works
214
+ api_response_hash = api_response ? Responses::Transforms.gem_to_hash(api_response) : nil
215
+ super(api_response_hash)
216
+ end
217
+
136
218
  # Extracts messages from completed API response.
137
219
  #
138
- # @param api_response [Hash] completed API response
139
- # @return [Array, nil] output array from response[:output] or nil
220
+ # @param api_response [Hash] converted response hash
221
+ # @return [Array, nil] output array from response.output or nil
140
222
  def process_prompt_finished_extract_messages(api_response)
141
- api_response&.dig(:output)
223
+ return unless api_response
224
+
225
+ # Response is already a hash from process_prompt_finished
226
+ api_response[:output]
142
227
  end
143
228
 
144
229
  # Extracts function calls from message stack.
@@ -60,10 +60,8 @@ module ActiveAgent
60
60
  # @see https://platform.openai.com/docs/guides/migrate-to-responses
61
61
  def prompt
62
62
  if api_version == :chat || context[:audio].present?
63
- instrument("api_routing.provider.active_agent", api_type: :chat, api_version: api_version, has_audio: context[:audio].present?)
64
63
  OpenAI::ChatProvider.new(raw_options).prompt
65
64
  else # api_version == :responses || true
66
- instrument("api_routing.provider.active_agent", api_type: :responses, api_version: api_version)
67
65
  OpenAI::ResponsesProvider.new(raw_options).prompt
68
66
  end
69
67
  end
@@ -89,7 +87,6 @@ module ActiveAgent
89
87
  # @param parameters [Hash] The embedding request parameters
90
88
  # @return [Object] The embedding response from OpenAI
91
89
  def api_embed_execute(parameters)
92
- instrument("embeddings_request.provider.active_agent")
93
90
  client.embeddings.create(**parameters).as_json.deep_symbolize_keys
94
91
  end
95
92
  end
@@ -8,8 +8,25 @@ require_relative "options"
8
8
  module ActiveAgent
9
9
  module Providers
10
10
  module OpenRouter
11
- # Type for Request model
11
+ # ActiveModel type for casting and serializing OpenRouter requests
12
+ #
13
+ # Handles conversion between hashes, Request objects, and serialized
14
+ # request hashes for the OpenRouter API.
15
+ #
16
+ # @example Type casting
17
+ # type = RequestType.new
18
+ # request = type.cast({ model: "openai/gpt-4", messages: "Hello" })
19
+ # # => #<Request ...>
20
+ #
21
+ # @example Serialization
22
+ # serialized = type.serialize(request)
23
+ # # => { model: "openai/gpt-4", messages: [...] }
12
24
  class RequestType < ActiveModel::Type::Value
25
+ # Casts value to Request object
26
+ #
27
+ # @param value [Request, Hash, nil]
28
+ # @return [Request, nil]
29
+ # @raise [ArgumentError] if value cannot be cast
13
30
  def cast(value)
14
31
  case value
15
32
  when Request
@@ -23,6 +40,11 @@ module ActiveAgent
23
40
  end
24
41
  end
25
42
 
43
+ # Serializes Request to hash for API submission
44
+ #
45
+ # @param value [Request, Hash, nil]
46
+ # @return [Hash, nil]
47
+ # @raise [ArgumentError] if value cannot be serialized
26
48
  def serialize(value)
27
49
  case value
28
50
  when Request
@@ -36,6 +58,10 @@ module ActiveAgent
36
58
  end
37
59
  end
38
60
 
61
+ # Deserializes value from storage
62
+ #
63
+ # @param value [Object]
64
+ # @return [Request, nil]
39
65
  def deserialize(value)
40
66
  cast(value)
41
67
  end