activeagent 1.0.0.rc1 → 1.0.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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/lib/active_agent/providers/_base_provider.rb +92 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/request.rb +135 -81
  6. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  7. data/lib/active_agent/providers/anthropic_provider.rb +96 -53
  8. data/lib/active_agent/providers/common/messages/_types.rb +37 -1
  9. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  10. data/lib/active_agent/providers/common/usage.rb +385 -0
  11. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  12. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  13. data/lib/active_agent/providers/mock_provider.rb +23 -23
  14. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  15. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  16. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  17. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  18. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  19. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  20. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  21. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  22. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  23. data/lib/active_agent/providers/open_ai/chat_provider.rb +57 -36
  24. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  25. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  26. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  27. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  28. data/lib/active_agent/providers/open_ai/responses/request.rb +100 -134
  29. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  30. data/lib/active_agent/providers/open_ai/responses_provider.rb +77 -30
  31. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  32. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  33. data/lib/active_agent/providers/open_router/options.rb +49 -1
  34. data/lib/active_agent/providers/open_router/request.rb +232 -66
  35. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  36. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  37. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  38. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  39. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  40. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  41. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  42. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  43. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  44. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  45. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  46. data/lib/active_agent/providers/open_router_provider.rb +9 -0
  47. data/lib/active_agent/version.rb +1 -1
  48. metadata +15 -159
  49. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  50. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  51. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  52. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  53. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  54. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  55. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  56. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  57. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  58. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  59. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  60. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  61. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  62. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  78. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  79. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  80. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  81. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  82. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  83. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  84. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  85. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  86. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  87. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  88. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  91. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  92. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  93. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  94. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  95. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  96. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  97. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  98. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  99. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  100. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  101. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  102. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  116. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  117. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  118. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  119. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  120. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  121. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  122. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  123. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  124. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  125. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  126. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  127. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  128. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  129. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  130. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  131. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  162. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  163. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  166. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  167. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  168. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  170. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  171. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  172. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  173. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  174. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  175. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  184. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  185. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  186. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  187. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -0,0 +1,364 @@
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 Chat
9
+ # Provides transformation methods for normalizing chat parameters
10
+ # to OpenAI gem's native format
11
+ #
12
+ # Handles message normalization, shorthand formats, instructions mapping,
13
+ # and response format conversion for the Chat Completions 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 all request parameters for OpenAI Chat API
25
+ #
26
+ # Handles instructions mapping to developer messages, message normalization,
27
+ # and response_format conversion. This is the main entry point for parameter
28
+ # transformation.
29
+ #
30
+ # @param params [Hash]
31
+ # @return [Hash] normalized parameters
32
+ def normalize_params(params)
33
+ params = params.dup
34
+
35
+ # Map common format 'instructions' to developer messages
36
+ if params.key?(:instructions)
37
+ instructions_messages = normalize_instructions(params.delete(:instructions))
38
+ params[:messages] = instructions_messages + Array(params[:messages] || [])
39
+ end
40
+
41
+ # Normalize messages for gem compatibility
42
+ params[:messages] = normalize_messages(params[:messages]) if params[:messages]
43
+
44
+ # Normalize response_format if present
45
+ params[:response_format] = normalize_response_format(params[:response_format]) if params[:response_format]
46
+
47
+ params
48
+ end
49
+
50
+ # Normalizes messages to OpenAI Chat API format using gem message classes
51
+ #
52
+ # Handles various input formats:
53
+ # - `"text"` → UserMessageParam
54
+ # - `[{role: "user", content: "..."}]` → array of message params
55
+ # - Merges consecutive same-role messages into single message
56
+ #
57
+ # @param messages [Array, String, Hash, nil]
58
+ # @return [Array<OpenAI::Models::Chat::ChatCompletionMessageParam>, nil]
59
+ def normalize_messages(messages)
60
+ case messages
61
+ when String
62
+ [ create_message_param("user", messages) ]
63
+ when Hash
64
+ [ normalize_message(messages) ]
65
+ when Array
66
+ grouped = []
67
+
68
+ messages.each do |msg|
69
+ normalized = normalize_message(msg)
70
+
71
+ if grouped.empty? || grouped.last.role != normalized.role
72
+ grouped << normalized
73
+ else
74
+ # Merge consecutive same-role messages
75
+ merged_content = merge_content(grouped.last.content, normalized.content)
76
+ grouped[-1] = create_message_param(grouped.last.role, merged_content)
77
+ end
78
+ end
79
+
80
+ grouped
81
+ when nil
82
+ nil
83
+ else
84
+ raise ArgumentError, "Cannot normalize #{messages.class} to messages array"
85
+ end
86
+ end
87
+
88
+ # Normalizes a single message to proper gem message param class
89
+ #
90
+ # Handles shorthand formats:
91
+ # - `"text"` → user message
92
+ # - `{text: "..."}` → user message
93
+ # - `{role: "system", text: "..."}` → system message
94
+ # - `{image: "url"}` → user message with image content part
95
+ # - `{text: "...", image: "url"}` → user message with text and image parts
96
+ #
97
+ # @param message [String, Hash, OpenAI::Models::Chat::ChatCompletionMessageParam]
98
+ # @return [OpenAI::Models::Chat::ChatCompletionMessageParam]
99
+ def normalize_message(message)
100
+ case message
101
+ when String
102
+ create_message_param("user", message)
103
+ when ::OpenAI::Models::Chat::ChatCompletionMessageParam
104
+ # Already a gem message param - pass through
105
+ message
106
+ when Hash
107
+ msg_hash = message.deep_symbolize_keys
108
+ role = msg_hash[:role]&.to_s || "user"
109
+
110
+ # Handle shorthand formats
111
+ content = if msg_hash.key?(:content)
112
+ # Standard format with explicit content
113
+ msg_hash[:content]
114
+ elsif msg_hash.key?(:text) && msg_hash.key?(:image)
115
+ # Shorthand with both text and image: { text: "...", image: "url" }
116
+ [
117
+ { type: "text", text: msg_hash[:text] },
118
+ { type: "image_url", image_url: { url: msg_hash[:image] } }
119
+ ]
120
+ elsif msg_hash.key?(:image)
121
+ # Shorthand with only image: { image: "url" }
122
+ # Text comes from adjacent prompt arguments
123
+ [ { type: "image_url", image_url: { url: msg_hash[:image] } } ]
124
+ elsif msg_hash.key?(:text)
125
+ # Shorthand: { text: "..." } or { role: "...", text: "..." }
126
+ msg_hash[:text]
127
+ else
128
+ # No content specified
129
+ nil
130
+ end
131
+
132
+ # Create appropriate message param based on role and content
133
+ extra_params = msg_hash.except(:role, :content, :text, :image)
134
+ create_message_param(role, content, extra_params)
135
+ else
136
+ raise ArgumentError, "Cannot normalize #{message.class} to message"
137
+ end
138
+ end
139
+
140
+ # Creates the appropriate gem message param class for the given role
141
+ #
142
+ # @param role [String] message role (developer, system, user, assistant, tool, function)
143
+ # @param content [String, Array, Hash, nil]
144
+ # @param extra_params [Hash] additional parameters (tool_call_id, name, etc.)
145
+ # @return [OpenAI::Models::Chat::ChatCompletionMessageParam]
146
+ # @raise [ArgumentError] when role is unknown
147
+ def create_message_param(role, content, extra_params = {})
148
+ params = { role: role }
149
+ params[:content] = normalize_content(content) if content
150
+ params.merge!(extra_params)
151
+
152
+ case role.to_s
153
+ when "developer"
154
+ ::OpenAI::Models::Chat::ChatCompletionDeveloperMessageParam.new(**params)
155
+ when "system"
156
+ ::OpenAI::Models::Chat::ChatCompletionSystemMessageParam.new(**params)
157
+ when "user"
158
+ ::OpenAI::Models::Chat::ChatCompletionUserMessageParam.new(**params)
159
+ when "assistant"
160
+ ::OpenAI::Models::Chat::ChatCompletionAssistantMessageParam.new(**params)
161
+ when "tool"
162
+ ::OpenAI::Models::Chat::ChatCompletionToolMessageParam.new(**params)
163
+ when "function"
164
+ ::OpenAI::Models::Chat::ChatCompletionFunctionMessageParam.new(**params)
165
+ else
166
+ raise ArgumentError, "Unknown message role: #{role}"
167
+ end
168
+ end
169
+
170
+ # Normalizes message content to Chat API format
171
+ #
172
+ # @param content [String, Array, Hash, nil]
173
+ # @return [String, Array, nil]
174
+ # @raise [ArgumentError] when content type is invalid
175
+ def normalize_content(content)
176
+ case content
177
+ when String
178
+ content
179
+ when Array
180
+ content.map { |part| normalize_content_part(part) }
181
+ when Hash
182
+ # Single content part as hash - wrap in array
183
+ [ normalize_content_part(content) ]
184
+ when nil
185
+ nil
186
+ else
187
+ raise ArgumentError, "Cannot normalize #{content.class} to content"
188
+ end
189
+ end
190
+
191
+ # Normalizes a single content part
192
+ #
193
+ # Converts strings to proper content part format with type and text keys.
194
+ #
195
+ # @param part [Hash, String]
196
+ # @return [Hash] content part with symbolized keys
197
+ # @raise [ArgumentError] when part type is invalid
198
+ def normalize_content_part(part)
199
+ case part
200
+ when Hash
201
+ part.deep_symbolize_keys
202
+ when String
203
+ { type: "text", text: part }
204
+ else
205
+ raise ArgumentError, "Cannot normalize #{part.class} to content part"
206
+ end
207
+ end
208
+
209
+ # Merges two content values for consecutive same-role messages
210
+ #
211
+ # Preserves multiple text parts and mixed content as array structure
212
+ # rather than concatenating strings.
213
+ #
214
+ # @param content1 [String, Array, nil]
215
+ # @param content2 [String, Array, nil]
216
+ # @return [Array] merged content parts
217
+ def merge_content(content1, content2)
218
+ # Convert to arrays for consistent handling
219
+ arr1 = content_to_array(content1)
220
+ arr2 = content_to_array(content2)
221
+
222
+ merged = arr1 + arr2
223
+
224
+ # Keep as array of content parts - don't simplify to string
225
+ # This preserves multiple text parts and mixed content
226
+ merged
227
+ end
228
+
229
+ # Converts content to array format for merging
230
+ #
231
+ # @param content [String, Array, nil]
232
+ # @return [Array<Hash>] content parts with type and text keys
233
+ def content_to_array(content)
234
+ case content
235
+ when String
236
+ [ { type: "text", text: content } ]
237
+ when Array
238
+ content.map { |part| part.is_a?(String) ? { type: "text", text: part } : part }
239
+ when nil
240
+ []
241
+ else
242
+ [ content ]
243
+ end
244
+ end
245
+
246
+ # Simplifies messages for cleaner API requests
247
+ #
248
+ # Converts gem message objects to hashes and simplifies content:
249
+ # - Single text content arrays → strings
250
+ # - Empty content arrays → removed
251
+ #
252
+ # @param messages [Array]
253
+ # @return [Array<Hash>]
254
+ def simplify_messages(messages)
255
+ return messages unless messages.is_a?(Array)
256
+
257
+ messages.map do |msg|
258
+ # Convert to hash if it's a gem object
259
+ simplified = msg.is_a?(Hash) ? msg.dup : gem_to_hash(msg)
260
+
261
+ # Simplify content if it's a single text part
262
+ if simplified[:content].is_a?(Array) && simplified[:content].size == 1
263
+ part = simplified[:content][0]
264
+ if part.is_a?(Hash) && part[:type] == "text" && part.keys.sort == [ :text, :type ]
265
+ simplified[:content] = part[:text]
266
+ end
267
+ end
268
+
269
+ # Remove empty content arrays
270
+ simplified.delete(:content) if simplified[:content] == []
271
+
272
+ simplified
273
+ end
274
+ end
275
+
276
+ # Normalizes response_format to OpenAI Chat API format
277
+ #
278
+ # @param format [Hash, Symbol, String]
279
+ # @return [Hash] normalized response format
280
+ def normalize_response_format(format)
281
+ case format
282
+ when Hash
283
+ format_hash = format.deep_symbolize_keys
284
+
285
+ if format_hash[:type] == "json_schema" || format_hash[:type] == :json_schema
286
+ # json_schema format
287
+ {
288
+ type: "json_schema",
289
+ json_schema: {
290
+ name: format_hash[:name] || format_hash[:json_schema]&.dig(:name),
291
+ schema: format_hash[:schema] || format_hash[:json_schema]&.dig(:schema),
292
+ strict: format_hash[:strict] || format_hash[:json_schema]&.dig(:strict)
293
+ }.compact
294
+ }
295
+ elsif format_hash[:type]
296
+ # Other type formats (json_object, text, etc.)
297
+ { type: format_hash[:type].to_s }
298
+ else
299
+ # Pass through (already properly structured or complex)
300
+ format_hash
301
+ end
302
+ when Symbol, String
303
+ # Simple string type
304
+ { type: format.to_s }
305
+ else
306
+ format
307
+ end
308
+ end
309
+
310
+ # Normalizes instructions to developer message format
311
+ #
312
+ # Converts instructions into developer messages with proper content structure.
313
+ # Multiple instructions become content parts in a single developer message
314
+ # rather than separate messages.
315
+ #
316
+ # @param instructions [Array<String>, String]
317
+ # @return [Array<Hash>] developer messages
318
+ def normalize_instructions(instructions)
319
+ instructions_array = Array(instructions)
320
+
321
+ # Convert multiple instructions into content parts for a single developer message
322
+ if instructions_array.size > 1
323
+ content_parts = instructions_array.map do |instruction|
324
+ { type: "text", text: instruction }
325
+ end
326
+ [ { role: "developer", content: content_parts } ]
327
+ else
328
+ instructions_array.map do |instruction|
329
+ { role: "developer", content: instruction }
330
+ end
331
+ end
332
+ end
333
+
334
+ # Cleans up serialized hash for API request
335
+ #
336
+ # Removes default values, simplifies messages, and handles special cases
337
+ # like web_search_options (which requires empty hash to enable).
338
+ #
339
+ # @param hash [Hash] serialized request hash
340
+ # @param defaults [Hash] default values to remove
341
+ # @param gem_object [Object] original gem object for checking values
342
+ # @return [Hash] cleaned request hash
343
+ def cleanup_serialized_request(hash, defaults, gem_object)
344
+ # Remove default values that shouldn't be in the request body
345
+ defaults.each do |key, value|
346
+ hash.delete(key) if hash[key] == value
347
+ end
348
+
349
+ # Simplify messages for cleaner API requests
350
+ hash[:messages] = simplify_messages(hash[:messages]) if hash[:messages]
351
+
352
+ # Add web_search_options if present (defaults to empty hash to enable feature)
353
+ if gem_object.instance_variable_get(:@data)[:web_search_options]
354
+ hash[:web_search_options] ||= {}
355
+ end
356
+
357
+ hash
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
@@ -4,11 +4,10 @@ require_relative "chat/_types"
4
4
  module ActiveAgent
5
5
  module Providers
6
6
  module OpenAI
7
- # Provider implementation for OpenAI's Chat Completion API.
7
+ # Provider implementation for OpenAI's Chat Completions API
8
8
  #
9
9
  # Handles chat-based interactions including streaming responses,
10
- # function/tool calling, and message management. Uses OpenAI's
11
- # chat completions endpoint for generating responses.
10
+ # function/tool calling, and message management.
12
11
  #
13
12
  # @see Base
14
13
  # @see https://platform.openai.com/docs/api-reference/chat
@@ -31,23 +30,32 @@ module ActiveAgent
31
30
  client.chat.completions
32
31
  end
33
32
 
34
- # Processes streaming response chunks from OpenAI's chat API.
33
+ # @see BaseProvider#api_response_normalize
34
+ # @param api_response [OpenAI::Models::ChatCompletion]
35
+ # @return [Hash] normalized response hash
36
+ def api_response_normalize(api_response)
37
+ return api_response unless api_response
38
+
39
+ Chat::Transforms.gem_to_hash(api_response)
40
+ end
41
+
42
+ # Processes streaming response chunks from OpenAI's chat API
35
43
  #
36
44
  # Handles message deltas, content updates, and completion detection.
37
45
  # Manages the message stack and broadcasts streaming updates.
38
46
  #
39
- # Handles multiple event types:
40
- # - `:chunk` - Message content and tool call deltas
41
- # - `:"content.delta"` - Incremental content updates
42
- # - `:"content.done"` - Complete content delivery
43
- # - `:"tool_calls.function.arguments.delta"` - Tool argument deltas
44
- # - `:"tool_calls.function.arguments.done"` - Complete tool arguments
47
+ # Event types handled:
48
+ # - `:chunk` - message content and tool call deltas
49
+ # - `:"content.delta"` - incremental content updates
50
+ # - `:"content.done"` - complete content delivery
51
+ # - `:"tool_calls.function.arguments.delta"` - tool argument deltas
52
+ # - `:"tool_calls.function.arguments.done"` - complete tool arguments
45
53
  #
46
54
  # @param api_response_event [OpenAI::Helpers::Streaming::ChatChunkEvent]
47
55
  # @return [void]
48
56
  # @see Base#process_stream_chunk
49
57
  def process_stream_chunk(api_response_event)
50
- instrument("stream_chunk_processing.provider.active_agent")
58
+ instrument("stream_chunk.active_agent")
51
59
 
52
60
  # Called Multiple Times: [Chunk<T>, T]<Content, ToolsCall>
53
61
  case api_response_event.type
@@ -62,11 +70,6 @@ module ActiveAgent
62
70
  if api_message.delta.content
63
71
  broadcast_stream_update(message_stack.last, api_message.delta.content)
64
72
  end
65
-
66
- # If this is the last api_chunk to be processed
67
- return unless api_message.finish_reason
68
-
69
- instrument("stream_finished.provider.active_agent", finish_reason: api_message.finish_reason)
70
73
  when :"content.delta"
71
74
  # Returns the deltas, without context
72
75
  # => {type: :"content.delta", delta: "", snapshot: "", parsed: nil}
@@ -86,42 +89,60 @@ module ActiveAgent
86
89
  end
87
90
  end
88
91
 
89
- # Processes function/tool calls from the API response.
92
+ # Processes function/tool calls from the API response
90
93
  #
91
94
  # Executes each tool call and creates tool response messages
92
95
  # for the next iteration of the conversation.
93
96
  #
94
- # @param api_function_calls [Array<Hash>] function call objects with :type, :id, and :function keys
97
+ # @param api_function_calls [Array<Hash>] function calls with :type, :id, and :function keys
95
98
  # @return [void]
96
99
  # @see Base#process_function_calls
97
100
  def process_function_calls(api_function_calls)
98
101
  api_function_calls.each do |api_function_call|
99
- content = case api_function_call[:type]
100
- when "function"
101
- instrument("tool_execution.provider.active_agent", tool_name: api_function_call.dig(:function, :name))
102
- process_tool_call_function(api_function_call[:function])
103
- else
104
- fail "Unexpected Tool Call Type: #{api_function_call[:type]}"
102
+ content = instrument("tool_call.active_agent", tool_name: api_function_call.dig(:function, :name)) do
103
+ case api_function_call[:type]
104
+ when "function"
105
+ process_tool_call_function(api_function_call[:function])
106
+ else
107
+ fail "Unexpected Tool Call Type: #{api_function_call[:type]}"
108
+ end
105
109
  end
106
110
 
107
- message = Chat::Requests::Messages::Tool.new(
111
+ # Create tool message using gem's message param class
112
+ message = ::OpenAI::Models::Chat::ChatCompletionToolMessageParam.new(
113
+ role: "tool",
108
114
  tool_call_id: api_function_call[:id],
109
115
  content: content.to_json
110
116
  )
111
117
 
112
- message_stack.push(message.serialize)
118
+ # Serialize and push to message stack
119
+ message_hash = Chat::Transforms.gem_to_hash(message)
120
+ message_stack.push(message_hash)
113
121
  end
114
122
  end
115
123
 
116
124
  # Extracts messages from the completed API response.
125
+ # Converts OpenAI gem response object to hash for storage.
117
126
  #
118
- # @param api_response [Hash]
127
+ # @param api_response [OpenAI::Models::Chat::ChatCompletion]
128
+ # @return [Common::PromptResponse, nil]
129
+ def process_prompt_finished(api_response = nil)
130
+ # Convert gem object to hash so that raw_response["usage"] works
131
+ api_response_hash = api_response ? Chat::Transforms.gem_to_hash(api_response) : nil
132
+ super(api_response_hash)
133
+ end
134
+
135
+ # Extracts messages from completed API response.
136
+ #
137
+ # @param api_response [Hash] converted response hash
119
138
  # @return [Array<Hash>, nil] single-element array with message or nil if no message
120
139
  # @see Base#process_prompt_finished_extract_messages
121
140
  def process_prompt_finished_extract_messages(api_response)
122
- api_message = api_response&.dig(:choices, 0, :message)
141
+ return unless api_response
142
+
143
+ api_message = api_response[:choices][0][:message]
123
144
 
124
- [ api_message ] if api_message
145
+ [ api_message ]
125
146
  end
126
147
 
127
148
  # Extracts function calls from the last message in the stack.
@@ -132,20 +153,20 @@ module ActiveAgent
132
153
  message_stack.last[:tool_calls]
133
154
  end
134
155
 
135
- # Merges streaming delta into a message.
156
+ # Merges streaming delta into a message
136
157
  #
137
158
  # Separated from hash_merge_delta to allow Ollama to override role handling.
138
159
  #
139
160
  # @param message [Hash]
140
161
  # @param delta [Hash]
141
- # @return [Hash] the merged message
162
+ # @return [Hash] merged message
142
163
  def message_merge_delta(message, delta)
143
164
  hash_merge_delta(message, delta)
144
165
  end
145
166
 
146
167
  private
147
168
 
148
- # Finds an existing message by ID or creates a new one.
169
+ # Finds an existing message by index or creates a new one
149
170
  #
150
171
  # @param id [Integer]
151
172
  # @return [Hash] found or newly created message
@@ -157,14 +178,14 @@ module ActiveAgent
157
178
  message_stack.last
158
179
  end
159
180
 
160
- # Recursively merges delta changes into a hash structure.
181
+ # Recursively merges delta changes into a hash structure
161
182
  #
162
- # Handles the complex delta merging needed for OpenAI's streaming API,
163
- # including arrays with indexed items and string concatenation.
183
+ # Handles complex delta merging for OpenAI's streaming API, including
184
+ # arrays with indexed items and string concatenation.
164
185
  #
165
186
  # @param hash [Hash] target hash to merge into
166
187
  # @param delta [Hash] delta changes to apply
167
- # @return [Hash] the merged hash
188
+ # @return [Hash] merged hash
168
189
  def hash_merge_delta(hash, delta)
169
190
  delta.each do |key, value|
170
191
  case hash[key]
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "requests/_types"
4
3
  require_relative "request"
5
4
 
6
5
  module ActiveAgent
7
6
  module Providers
8
7
  module OpenAI
9
8
  module Embedding
10
- # Type for Request model
9
+ # ActiveModel type for casting and serializing embedding requests
11
10
  class RequestType < ActiveModel::Type::Value
11
+ # Casts value to Request object
12
+ #
13
+ # @param value [Request, Hash, nil]
14
+ # @return [Request, nil]
15
+ # @raise [ArgumentError] when value cannot be cast
12
16
  def cast(value)
13
17
  case value
14
18
  when Request
@@ -22,6 +26,11 @@ module ActiveAgent
22
26
  end
23
27
  end
24
28
 
29
+ # Serializes Request to hash for API submission
30
+ #
31
+ # @param value [Request, Hash, nil]
32
+ # @return [Hash, nil]
33
+ # @raise [ArgumentError] when value cannot be serialized
25
34
  def serialize(value)
26
35
  case value
27
36
  when Request
@@ -35,6 +44,8 @@ module ActiveAgent
35
44
  end
36
45
  end
37
46
 
47
+ # @param value [Object]
48
+ # @return [Request, nil]
38
49
  def deserialize(value)
39
50
  cast(value)
40
51
  end