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,228 @@
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
+ # Simplifies input for cleaner API requests
25
+ #
26
+ # Unwraps single-element arrays:
27
+ # - `["text"]` → `"text"`
28
+ # - `[{type: "input_text", text: "..."}]` → `"..."`
29
+ # - `[{role: "user", content: "..."}]` → `"..."`
30
+ #
31
+ # @param input [Array, String, Hash]
32
+ # @return [String, Array, Hash]
33
+ def simplify_input(input)
34
+ return input unless input.is_a?(Array)
35
+
36
+ # Single string element - unwrap it
37
+ if input.size == 1 && input[0].is_a?(String)
38
+ return input[0]
39
+ end
40
+
41
+ # Single content object {type: "input_text", text: "..."} - unwrap to string
42
+ if input.size == 1 &&
43
+ input[0].is_a?(Hash) &&
44
+ input[0][:type] == "input_text" &&
45
+ input[0][:text].is_a?(String) &&
46
+ input[0].keys.sort == [ :text, :type ]
47
+ return input[0][:text]
48
+ end
49
+
50
+ # Single message with string content - simplify to string
51
+ if input.size == 1 &&
52
+ input[0].is_a?(Hash) &&
53
+ input[0][:role] == "user" &&
54
+ input[0][:content].is_a?(String)
55
+ return input[0][:content]
56
+ end
57
+
58
+ input
59
+ end
60
+
61
+ # Normalizes response_format to OpenAI Responses API text parameter
62
+ #
63
+ # Maps common response_format structures to Responses API format.
64
+ # Returns ResponseTextConfig object to preserve proper nesting.
65
+ #
66
+ # @param format [Hash, Symbol, String]
67
+ # @return [OpenAI::Models::Responses::ResponseTextConfig]
68
+ def normalize_response_format(format)
69
+ text_hash = case format
70
+ when Hash
71
+ if format[:type] == "json_schema" || format[:type] == :json_schema
72
+ # json_schema format: map to Responses API structure
73
+ {
74
+ format: {
75
+ type: "json_schema",
76
+ name: format[:name] || format[:json_schema]&.dig(:name),
77
+ schema: format[:schema] || format[:json_schema]&.dig(:schema),
78
+ strict: format[:strict] || format[:json_schema]&.dig(:strict)
79
+ }.compact
80
+ }
81
+ elsif format[:type] == "json_object" || format[:type] == :json_object
82
+ # json_object format
83
+ { format: { type: "json_object" } }
84
+ elsif format[:type]
85
+ # Other simple type formats (text, etc.) - wrap in format key
86
+ { format: { type: format[:type].to_s } }
87
+ else
88
+ # Pass through other hash formats (already has format key or complex structure)
89
+ format
90
+ end
91
+ when Symbol, String
92
+ # Simple format types
93
+ { format: { type: format.to_s } }
94
+ else
95
+ format
96
+ end
97
+
98
+ # Convert hash to ResponseTextConfig object to preserve nesting
99
+ ::OpenAI::Models::Responses::ResponseTextConfig.new(**text_hash)
100
+ end
101
+
102
+ # Normalizes input/messages to gem-compatible format
103
+ #
104
+ # Handles various input formats:
105
+ # - `"text"` → string (passthrough)
106
+ # - `{role: "user", content: "..."}` → wrapped in array
107
+ # - `[{text: "..."}, {image: "url"}]` → wrapped as user message with content array
108
+ # - `["msg1", "msg2"]` → array of user messages
109
+ #
110
+ # @param input [String, Hash, Array, Object]
111
+ # @return [String, Array<Hash>]
112
+ def normalize_input(input)
113
+ # String inputs pass through unchanged
114
+ return input if input.is_a?(String)
115
+
116
+ # Single hash should be wrapped in an array
117
+ if input.is_a?(Hash)
118
+ return [ normalize_message(input) ]
119
+ end
120
+
121
+ # Handle arrays
122
+ return input unless input.respond_to?(:map)
123
+
124
+ # Check if this is an array of content items (strings or text/image/document hashes)
125
+ # Content items don't have a :role key (messages do)
126
+ # BUT NOT a single string (which should have been caught above)
127
+ all_content_items = input.size > 1 && input.all? do |item|
128
+ if item.is_a?(String)
129
+ true
130
+ elsif item.is_a?(Hash)
131
+ # If it has a role, it's a message, not a content item
132
+ !item.key?(:role) && (item.key?(:text) || item.key?(:image) || item.key?(:document))
133
+ else
134
+ false
135
+ end
136
+ end
137
+
138
+ if all_content_items
139
+ # These are multiple content items, wrap in a user message
140
+ content = input.map { |item| normalize_message(item) }
141
+ return [ { role: "user", content: content } ]
142
+ end
143
+
144
+ # Otherwise treat as array of messages
145
+ input.map { |item| normalize_message(item, context: :input) }
146
+ end
147
+
148
+ # Normalizes a single message to hash format
149
+ #
150
+ # Handles shorthand formats:
151
+ # - `{text: "..."}` → user message
152
+ # - `{image: "url"}` → input_image content part
153
+ # - `{document: "url"}` → input_file content part
154
+ #
155
+ # @param message [Hash, String, Object]
156
+ # @param context [Symbol] :input for messages, :content for content parts
157
+ # @return [Hash, String]
158
+ def normalize_message(message, context: :content)
159
+ # If it's our custom model object, serialize it
160
+ if message.respond_to?(:serialize)
161
+ message.serialize
162
+ elsif message.is_a?(Hash)
163
+ # If it has a role, it's a message - convert :text to :content
164
+ if message.key?(:role)
165
+ normalized = message.dup
166
+ if normalized.key?(:text) && !normalized.key?(:content)
167
+ normalized[:content] = normalized.delete(:text)
168
+ end
169
+ return normalized
170
+ end
171
+
172
+ # Expand shorthand formats to full structures for content items
173
+ if message.key?(:image)
174
+ { type: "input_image", image_url: message[:image] }
175
+ elsif message.key?(:document)
176
+ document_value = message[:document]
177
+ if document_value.start_with?("data:")
178
+ { type: "input_file", filename: "document.pdf", file_data: document_value }
179
+ else
180
+ { type: "input_file", file_url: document_value }
181
+ end
182
+ elsif message.key?(:text) && message.size == 1
183
+ # Single :text key without :role - treat as user message
184
+ { role: "user", content: message[:text] }
185
+ elsif message.key?(:text)
186
+ # Bare text content item with other keys
187
+ { type: "input_text", text: message[:text] }
188
+ else
189
+ message
190
+ end
191
+ elsif message.is_a?(String)
192
+ # Context matters: in input array, strings become messages; in content array, they become input_text
193
+ if context == :input
194
+ { role: "user", content: message }
195
+ else
196
+ { type: "input_text", text: message }
197
+ end
198
+ else
199
+ # Pass through anything else
200
+ message
201
+ end
202
+ end
203
+
204
+ # Cleans up serialized request for API submission
205
+ #
206
+ # Removes default values and simplifies input where possible.
207
+ #
208
+ # @param hash [Hash] serialized request
209
+ # @param defaults [Hash] default values to remove
210
+ # @param gem_object [Object] original gem object
211
+ # @return [Hash] cleaned request hash
212
+ def cleanup_serialized_request(hash, defaults, gem_object)
213
+ # Remove default values that shouldn't be in the request body
214
+ defaults.each do |key, value|
215
+ hash.delete(key) if hash[key] == value
216
+ end
217
+
218
+ # Simplify input when possible for cleaner API requests
219
+ hash[:input] = simplify_input(hash[:input]) if hash[:input]
220
+
221
+ hash
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -1,13 +1,14 @@
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
@@ -29,17 +30,34 @@ module ActiveAgent
29
30
  client.responses
30
31
  end
31
32
 
32
- # Processes streaming response chunks from the Responses API.
33
+ # @see BaseProvider#api_response_normalize
34
+ # @param api_response [OpenAI::Models::Responses::Response]
35
+ # @return [Hash] normalized response hash
36
+ def api_response_normalize(api_response)
37
+ return api_response unless api_response
38
+
39
+ Responses::Transforms.gem_to_hash(api_response)
40
+ end
41
+
42
+ # Processes streaming response chunks from the Responses API
33
43
  #
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.
44
+ # Event types handled:
45
+ # - `:"response.created"`, `:"response.in_progress"` - response lifecycle
46
+ # - `:"response.output_item.added"` - message or function call added
47
+ # - `:"response.content_part.added"` - content part started
48
+ # - `:"response.output_text.delta"` - incremental text updates
49
+ # - `:"response.output_text.done"` - complete text
50
+ # - `:"response.function_call_arguments.delta"` - function argument updates
51
+ # - `:"response.function_call_arguments.done"` - complete function arguments
52
+ # - `:"response.content_part.done"` - content part completed
53
+ # - `:"response.output_item.done"` - message or function call completed
54
+ # - `:"response.completed"` - response finished
38
55
  #
39
- # @param api_response_event [Hash] streaming response chunk with :type key
56
+ # @param api_response_event [Hash] streaming chunk with :type key
40
57
  # @return [void]
58
+ # @see Base#process_stream_chunk
41
59
  def process_stream_chunk(api_response_event)
42
- instrument("stream_chunk_processing.provider.active_agent", chunk_type: api_response_event.type)
60
+ instrument("stream_chunk.active_agent", chunk_type: api_response_event.type)
43
61
 
44
62
  case api_response_event.type
45
63
  # Response Created
@@ -55,14 +73,14 @@ module ActiveAgent
55
73
 
56
74
  # -> -> -> Content Text Append
57
75
  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])
76
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
77
+ message[:content] += api_response_event.delta
78
+ broadcast_stream_update(message, api_response_event.delta)
61
79
 
62
80
  # -> -> -> Content Text Completed [Full Text]
63
81
  when :"response.output_text.done"
64
- message = message_stack.find { _1[:id] == api_response_event[:item_id] }
65
- message[:content] = api_response_event[:text]
82
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
83
+ message[:content] = api_response_event.text
66
84
  broadcast_stream_update(message, nil) # Don't double send content
67
85
 
68
86
  # -> -> -> Content Function Call Append
@@ -81,11 +99,17 @@ module ActiveAgent
81
99
  # Once we are finished, close out and run tooling callbacks (Recursive)
82
100
  process_prompt_finished
83
101
  else
84
- fail "Unexpected Response Chunk Type: #{type}"
102
+ raise "Unexpected Response Chunk Type: #{api_response_event.type}"
85
103
  end
86
104
  end
87
105
 
88
- # Processes output item added events from streaming response.
106
+ # Processes output item added events from streaming response
107
+ #
108
+ # Handles message and function_call item types. For messages, adds to stack
109
+ # with empty content. For function calls, waits for completion event.
110
+ #
111
+ # Required because API returns empty array instead of empty string for
112
+ # initial message content due to serialization bug.
89
113
  #
90
114
  # @param api_response_event [Hash] response chunk with :item key
91
115
  # @return [void]
@@ -93,15 +117,19 @@ module ActiveAgent
93
117
  case api_response_event.item.type
94
118
  when :message
95
119
  # 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)
120
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item).compact_blank
121
+ message_stack << { content: "" }.merge(item_hash)
97
122
  when :function_call
98
123
  # No-Op: Wait for FC to Land (-> response.output_item.done)
99
124
  else
100
- fail "Unexpected Item Type: #{api_response_event.item.type}"
125
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
101
126
  end
102
127
  end
103
128
 
104
- # Processes output item completion events from streaming response.
129
+ # Processes output item completion events from streaming response
130
+ #
131
+ # For function calls, adds completed item to message stack.
132
+ # For messages, no action needed as content already updated via delta events.
105
133
  #
106
134
  # @param api_response_event [Hash] response chunk with completed :item
107
135
  # @return [void]
@@ -110,35 +138,54 @@ module ActiveAgent
110
138
  when :message
111
139
  # No-Op: Message Up to Date
112
140
  when :function_call
113
- message_stack << api_response_event.item
141
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item)
142
+ message_stack << item_hash
114
143
  else
115
- fail "Unexpected Item Type: #{api_response_event.item.type}"
144
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
116
145
  end
117
146
  end
118
147
 
119
- # Executes function calls and creates output messages for conversation continuation.
148
+ # Executes function calls and creates output messages for conversation continuation
120
149
  #
121
150
  # @param api_function_calls [Array<Hash>] function calls with :call_id and :name keys
122
151
  # @return [void]
152
+ # @see Base#process_function_calls
123
153
  def process_function_calls(api_function_calls)
124
154
  api_function_calls.each do |api_function_call|
125
- instrument("tool_execution.provider.active_agent", tool_name: api_function_call[:name])
155
+ output = instrument("tool_call.active_agent", tool_name: api_function_call[:name]) do
156
+ process_tool_call_function(api_function_call).to_json
157
+ end
126
158
 
127
- message = Responses::Requests::Inputs::FunctionCallOutput.new(
159
+ # Create native gem input item for function call output
160
+ message = ::OpenAI::Models::Responses::ResponseInputItem::FunctionCallOutput.new(
128
161
  call_id: api_function_call[:call_id],
129
- output: process_tool_call_function(api_function_call).to_json
162
+ output:
130
163
  )
131
164
 
132
- message_stack.push(message.serialize)
165
+ # Convert to hash for message_stack
166
+ message_stack.push(Responses::Transforms.gem_to_hash(message))
133
167
  end
134
168
  end
135
169
 
170
+ # Converts OpenAI gem response object to hash for storage.
171
+ #
172
+ # @param api_response [OpenAI::Models::Responses::Response]
173
+ # @return [Common::PromptResponse, nil]
174
+ def process_prompt_finished(api_response = nil)
175
+ # Convert gem object to hash so that raw_response["usage"] works
176
+ api_response_hash = api_response ? Responses::Transforms.gem_to_hash(api_response) : nil
177
+ super(api_response_hash)
178
+ end
179
+
136
180
  # Extracts messages from completed API response.
137
181
  #
138
- # @param api_response [Hash] completed API response
139
- # @return [Array, nil] output array from response[:output] or nil
182
+ # @param api_response [Hash] converted response hash
183
+ # @return [Array, nil] output array from response.output or nil
140
184
  def process_prompt_finished_extract_messages(api_response)
141
- api_response&.dig(:output)
185
+ return unless api_response
186
+
187
+ # Response is already a hash from process_prompt_finished
188
+ api_response[:output]
142
189
  end
143
190
 
144
191
  # 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
@@ -8,11 +8,48 @@ require_relative "requests/provider_preferences"
8
8
  module ActiveAgent
9
9
  module Providers
10
10
  module OpenRouter
11
+ # Configuration options for OpenRouter provider
12
+ #
13
+ # Extends OpenAI::Options with OpenRouter-specific settings including
14
+ # HTTP-Referer and X-Title headers for app identification and ranking.
15
+ #
16
+ # @example Basic configuration
17
+ # options = Options.new(
18
+ # api_key: 'sk-or-v1-...',
19
+ # app_name: 'MyApp',
20
+ # site_url: 'https://myapp.com'
21
+ # )
22
+ #
23
+ # @example Rails auto-configuration
24
+ # # Automatically resolves app_name from Rails.application
25
+ # # and site_url from routes.default_url_options
26
+ # options = Options.new(api_key: ENV['OPENROUTER_API_KEY'])
27
+ #
28
+ # @see https://openrouter.ai/docs/api-keys OpenRouter API Keys
29
+ # @see https://openrouter.ai/docs/rankings OpenRouter App Rankings
11
30
  class Options < ActiveAgent::Providers::OpenAI::Options
31
+ # @!attribute base_url
32
+ # @return [String] API endpoint (default: "https://openrouter.ai/api/v1")
12
33
  attribute :base_url, :string, as: "https://openrouter.ai/api/v1"
34
+
35
+ # @!attribute app_name
36
+ # @return [String] application name for X-Title header (default: "ActiveAgent" or Rails app name)
13
37
  attribute :app_name, :string, fallback: "ActiveAgent"
38
+
39
+ # @!attribute site_url
40
+ # @return [String] site URL for HTTP-Referer header (default: "https://activeagents.ai/" or Rails URL)
14
41
  attribute :site_url, :string, fallback: "https://activeagents.ai/"
15
42
 
43
+ # Creates new OpenRouter options with auto-resolution
44
+ #
45
+ # Automatically resolves app_name from Rails application name and
46
+ # site_url from Rails routes/ActionMailer default_url_options.
47
+ #
48
+ # @param kwargs [Hash] configuration options
49
+ # @option kwargs [String] :api_key OpenRouter API key
50
+ # @option kwargs [String] :app_name application name for rankings
51
+ # @option kwargs [String] :site_url site URL for rankings
52
+ # @return [Options]
16
53
  def initialize(kwargs = {})
17
54
  kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
18
55
 
@@ -22,11 +59,22 @@ module ActiveAgent
22
59
  )))
23
60
  end
24
61
 
62
+ # Serializes options for API requests
63
+ #
64
+ # Excludes app_name and site_url as they're sent via headers.
65
+ #
66
+ # @return [Hash] serialized options
25
67
  def serialize
26
68
  super.except(:app_name, :site_url)
27
69
  end
28
70
 
29
- # We fallback to ActiveAgent but allow empty strings to unset
71
+ # Returns extra headers for OpenRouter API
72
+ #
73
+ # Maps app_name and site_url to OpenRouter's required headers:
74
+ # - HTTP-Referer: site_url
75
+ # - X-Title: app_name
76
+ #
77
+ # @return [Hash] headers hash
30
78
  def extra_headers
31
79
  deep_compact(
32
80
  "http-referer" => site_url.presence,