activeagent 0.6.3 → 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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +240 -2
  3. data/README.md +15 -24
  4. data/lib/active_agent/base.rb +389 -39
  5. data/lib/active_agent/concerns/callbacks.rb +251 -0
  6. data/lib/active_agent/concerns/observers.rb +147 -0
  7. data/lib/active_agent/concerns/parameterized.rb +292 -0
  8. data/lib/active_agent/concerns/provider.rb +120 -0
  9. data/lib/active_agent/concerns/queueing.rb +36 -0
  10. data/lib/active_agent/concerns/rescue.rb +64 -0
  11. data/lib/active_agent/concerns/streaming.rb +282 -0
  12. data/lib/active_agent/concerns/tooling.rb +23 -0
  13. data/lib/active_agent/concerns/view.rb +150 -0
  14. data/lib/active_agent/configuration.rb +442 -20
  15. data/lib/active_agent/generation.rb +141 -47
  16. data/lib/active_agent/providers/_base_provider.rb +420 -0
  17. data/lib/active_agent/providers/anthropic/_types.rb +63 -0
  18. data/lib/active_agent/providers/anthropic/options.rb +53 -0
  19. data/lib/active_agent/providers/anthropic/request.rb +163 -0
  20. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  21. data/lib/active_agent/providers/anthropic_provider.rb +254 -0
  22. data/lib/active_agent/providers/common/messages/_types.rb +160 -0
  23. data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
  24. data/lib/active_agent/providers/common/messages/base.rb +17 -0
  25. data/lib/active_agent/providers/common/messages/system.rb +20 -0
  26. data/lib/active_agent/providers/common/messages/tool.rb +21 -0
  27. data/lib/active_agent/providers/common/messages/user.rb +20 -0
  28. data/lib/active_agent/providers/common/model.rb +361 -0
  29. data/lib/active_agent/providers/common/response.rb +13 -0
  30. data/lib/active_agent/providers/common/responses/_types.rb +51 -0
  31. data/lib/active_agent/providers/common/responses/base.rb +199 -0
  32. data/lib/active_agent/providers/common/responses/embed.rb +33 -0
  33. data/lib/active_agent/providers/common/responses/format.rb +31 -0
  34. data/lib/active_agent/providers/common/responses/message.rb +3 -0
  35. data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
  36. data/lib/active_agent/providers/common/usage.rb +385 -0
  37. data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
  38. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  39. data/lib/active_agent/providers/concerns/previewable.rb +150 -0
  40. data/lib/active_agent/providers/log_subscriber.rb +178 -0
  41. data/lib/active_agent/providers/mock/_types.rb +77 -0
  42. data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
  43. data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
  44. data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
  45. data/lib/active_agent/providers/mock/messages/base.rb +63 -0
  46. data/lib/active_agent/providers/mock/messages/user.rb +18 -0
  47. data/lib/active_agent/providers/mock/options.rb +30 -0
  48. data/lib/active_agent/providers/mock/request.rb +38 -0
  49. data/lib/active_agent/providers/mock_provider.rb +311 -0
  50. data/lib/active_agent/providers/ollama/_types.rb +5 -0
  51. data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
  52. data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
  53. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  54. data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
  55. data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
  56. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  57. data/lib/active_agent/providers/ollama/options.rb +27 -0
  58. data/lib/active_agent/providers/ollama_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/_base.rb +59 -0
  60. data/lib/active_agent/providers/open_ai/_types.rb +5 -0
  61. data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
  62. data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
  63. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  64. data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
  65. data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
  66. data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
  67. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  68. data/lib/active_agent/providers/open_ai/options.rb +74 -0
  69. data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
  70. data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
  71. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  72. data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
  73. data/lib/active_agent/providers/open_ai_provider.rb +94 -0
  74. data/lib/active_agent/providers/open_router/_types.rb +71 -0
  75. data/lib/active_agent/providers/open_router/options.rb +141 -0
  76. data/lib/active_agent/providers/open_router/request.rb +249 -0
  77. data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
  78. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
  79. data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
  80. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
  81. data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
  82. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
  83. data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
  84. data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
  85. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
  86. data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
  87. data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
  88. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
  89. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
  90. data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
  91. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  92. data/lib/active_agent/providers/open_router_provider.rb +62 -0
  93. data/lib/active_agent/providers/openai_provider.rb +2 -0
  94. data/lib/active_agent/providers/openrouter_provider.rb +2 -0
  95. data/lib/active_agent/railtie.rb +8 -6
  96. data/lib/active_agent/schema_generator.rb +333 -166
  97. data/lib/active_agent/version.rb +1 -1
  98. data/lib/active_agent.rb +112 -36
  99. data/lib/generators/active_agent/agent/USAGE +78 -0
  100. data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
  101. data/lib/generators/active_agent/install/USAGE +25 -0
  102. data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
  103. data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
  104. data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
  105. data/lib/generators/erb/agent_generator.rb +31 -16
  106. data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
  107. data/lib/generators/erb/templates/instructions.md.tt +3 -0
  108. data/lib/generators/erb/templates/instructions.text.tt +1 -0
  109. data/lib/generators/erb/templates/message.md.erb.tt +5 -0
  110. data/lib/generators/erb/templates/schema.json.tt +10 -0
  111. data/lib/generators/test_unit/agent_generator.rb +1 -1
  112. data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
  113. metadata +182 -71
  114. data/lib/active_agent/action_prompt/action.rb +0 -13
  115. data/lib/active_agent/action_prompt/base.rb +0 -623
  116. data/lib/active_agent/action_prompt/message.rb +0 -126
  117. data/lib/active_agent/action_prompt/prompt.rb +0 -136
  118. data/lib/active_agent/action_prompt.rb +0 -19
  119. data/lib/active_agent/callbacks.rb +0 -33
  120. data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
  121. data/lib/active_agent/generation_provider/base.rb +0 -55
  122. data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
  123. data/lib/active_agent/generation_provider/error_handling.rb +0 -167
  124. data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
  125. data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
  126. data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
  127. data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
  128. data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
  129. data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
  130. data/lib/active_agent/generation_provider/response.rb +0 -75
  131. data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
  132. data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
  133. data/lib/active_agent/generation_provider/tool_management.rb +0 -142
  134. data/lib/active_agent/generation_provider.rb +0 -67
  135. data/lib/active_agent/log_subscriber.rb +0 -44
  136. data/lib/active_agent/parameterized.rb +0 -75
  137. data/lib/active_agent/prompt_helper.rb +0 -19
  138. data/lib/active_agent/queued_generation.rb +0 -12
  139. data/lib/active_agent/rescuable.rb +0 -34
  140. data/lib/active_agent/sanitizers.rb +0 -40
  141. data/lib/active_agent/streaming.rb +0 -34
  142. data/lib/active_agent/test_case.rb +0 -125
  143. data/lib/generators/USAGE +0 -47
  144. data/lib/generators/active_agent/USAGE +0 -56
  145. data/lib/generators/erb/install_generator.rb +0 -44
  146. data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
  147. data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
  148. data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
  149. data/lib/generators/erb/templates/view.html.erb.tt +0 -5
  150. data/lib/generators/erb/templates/view.json.erb.tt +0 -16
  151. /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
  152. /data/lib/generators/erb/templates/{view.text.erb.tt → message.text.erb.tt} +0 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "json"
5
+ require_relative "transforms"
6
+ require_relative "requests/_types"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ module OpenRouter
11
+ # Wraps OpenAI gem's CompletionCreateParams with OpenRouter-specific extensions
12
+ #
13
+ # Delegates to OpenAI::Models::Chat::CompletionCreateParams for OpenAI-compatible
14
+ # parameters while adding support for OpenRouter-specific features like plugins,
15
+ # provider preferences, model fallbacks, and extended sampling parameters.
16
+ #
17
+ # OpenRouter-specific parameters:
18
+ # - plugins: Array of plugin configurations (e.g., file-parser for PDFs)
19
+ # - provider: ProviderPreferences object with require_parameters, data_collection, etc.
20
+ # - transforms: Array of transformation strings
21
+ # - models: Array of model strings for fallback routing
22
+ # - route: Routing strategy (default: "fallback")
23
+ # - top_k, min_p, top_a, repetition_penalty: Extended sampling parameters
24
+ #
25
+ # @example Basic usage
26
+ # request = Request.new(
27
+ # model: "openai/gpt-4",
28
+ # messages: [{role: "user", content: "Hello"}]
29
+ # )
30
+ #
31
+ # @example With OpenRouter-specific features
32
+ # request = Request.new(
33
+ # model: "openai/gpt-4",
34
+ # messages: [{role: "user", content: "Hello"}],
35
+ # models: ["anthropic/claude-3", "openai/gpt-4"],
36
+ # provider: {require_parameters: true}
37
+ # )
38
+ class Request < SimpleDelegator
39
+ # Default parameter values
40
+ DEFAULTS = {
41
+ frequency_penalty: 0,
42
+ logprobs: false,
43
+ n: 1,
44
+ presence_penalty: 0,
45
+ temperature: 1,
46
+ top_p: 1,
47
+ route: "fallback",
48
+ models: [],
49
+ transforms: []
50
+ }.freeze
51
+
52
+ # @return [Boolean, nil]
53
+ attr_reader :stream
54
+
55
+ # @return [Hash] OpenRouter-specific parameters
56
+ attr_reader :openrouter_params
57
+
58
+ # Creates a new OpenRouter request
59
+ #
60
+ # @param params [Hash] request parameters
61
+ # @option params [String] :model model identifier (default: "openrouter/auto")
62
+ # @option params [Array, String, Hash] :messages required conversation messages
63
+ # @option params [Array] :plugins plugin configurations
64
+ # @option params [Hash] :provider provider preferences
65
+ # @option params [Array<String>] :transforms transformation strings
66
+ # @option params [Array<String>] :models fallback model list
67
+ # @option params [String] :route routing strategy
68
+ # @option params [Integer] :top_k sampling parameter
69
+ # @option params [Float] :min_p minimum probability sampling
70
+ # @option params [Float] :top_a top-a sampling
71
+ # @option params [Float] :repetition_penalty repetition penalty
72
+ # @raise [ArgumentError] when parameters are invalid
73
+ def initialize(**params)
74
+ # Step 1: Extract stream flag
75
+ @stream = params[:stream]
76
+
77
+ # Step 2: Apply defaults
78
+ params = apply_defaults(params)
79
+
80
+ # Step 3: Normalize parameters and split into OpenAI vs OpenRouter-specific
81
+ # This handles response_format special logic for structured output
82
+ openai_params, @openrouter_params = Transforms.normalize_params(params)
83
+
84
+ # Step 4: Create gem model with OpenAI-compatible params
85
+ gem_model = ::OpenAI::Models::Chat::CompletionCreateParams.new(**openai_params)
86
+
87
+ # Step 5: Delegate to the gem model
88
+ super(gem_model)
89
+ rescue ArgumentError => e
90
+ raise ArgumentError, "Invalid OpenRouter request parameters: #{e.message}"
91
+ end
92
+
93
+ # Serializes request for API submission
94
+ #
95
+ # Merges OpenAI-compatible parameters with OpenRouter-specific extensions.
96
+ #
97
+ # @return [Hash] cleaned request hash
98
+ def serialize
99
+ # Get OpenAI params from gem model
100
+ openai_hash = Transforms.gem_to_hash(__getobj__)
101
+
102
+ # Merge with OpenRouter-specific params
103
+ Transforms.cleanup_serialized_request(openai_hash, @openrouter_params, DEFAULTS, __getobj__)
104
+ end
105
+
106
+ # @return [Array<Hash>, nil]
107
+ def messages
108
+ __getobj__.instance_variable_get(:@data)[:messages]
109
+ end
110
+
111
+ # Sets messages with normalization
112
+ #
113
+ # Merges new messages with existing ones for compatibility.
114
+ #
115
+ # @param value [Array, String, Hash]
116
+ # @return [void]
117
+ def messages=(value)
118
+ normalized_value = Transforms.normalize_messages(value)
119
+ current_messages = messages || []
120
+
121
+ # Merge behavior for OpenRouter compatibility
122
+ merged = current_messages | Array(normalized_value)
123
+ __getobj__.instance_variable_get(:@data)[:messages] = merged
124
+ end
125
+
126
+ # Alias for messages (common format compatibility)
127
+ #
128
+ # @return [Array<Hash>, nil]
129
+ def message
130
+ messages
131
+ end
132
+
133
+ # @param value [Array, String, Hash]
134
+ # @return [void]
135
+ def message=(value)
136
+ self.messages = value
137
+ end
138
+
139
+ # Sets instructions as developer messages
140
+ #
141
+ # Prepends developer messages to the messages array.
142
+ #
143
+ # @param values [Array<String>, String]
144
+ # @return [void]
145
+ def instructions=(*values)
146
+ instructions_messages = OpenAI::Chat::Transforms.normalize_instructions(values.flatten)
147
+ current_messages = messages || []
148
+ self.messages = instructions_messages + current_messages
149
+ end
150
+
151
+ # Accessor for OpenRouter-specific provider preferences
152
+ #
153
+ # @return [Hash, nil]
154
+ def provider
155
+ @openrouter_params[:provider]
156
+ end
157
+
158
+ # Sets provider preferences
159
+ #
160
+ # @param value [Hash]
161
+ # @return [void]
162
+ def provider=(value)
163
+ @openrouter_params[:provider] = value
164
+ end
165
+
166
+ # Accessor for OpenRouter plugins
167
+ #
168
+ # @return [Array, nil]
169
+ def plugins
170
+ @openrouter_params[:plugins]
171
+ end
172
+
173
+ # Sets plugins
174
+ #
175
+ # @param value [Array]
176
+ # @return [void]
177
+ def plugins=(value)
178
+ @openrouter_params[:plugins] = value
179
+ end
180
+
181
+ # Accessor for OpenRouter transforms
182
+ #
183
+ # @return [Array]
184
+ def transforms
185
+ @openrouter_params[:transforms] || []
186
+ end
187
+
188
+ # Sets transforms
189
+ #
190
+ # @param value [Array]
191
+ # @return [void]
192
+ def transforms=(value)
193
+ @openrouter_params[:transforms] = value
194
+ end
195
+
196
+ # Accessor for fallback models
197
+ #
198
+ # @return [Array]
199
+ def models
200
+ @openrouter_params[:models] || []
201
+ end
202
+
203
+ # Sets fallback models
204
+ #
205
+ # @param value [Array]
206
+ # @return [void]
207
+ def models=(value)
208
+ @openrouter_params[:models] = value
209
+ end
210
+
211
+ # Alias for backwards compatibility
212
+ alias_method :fallback_models, :models
213
+ alias_method :fallback_models=, :models=
214
+
215
+ # Accessor for routing strategy
216
+ #
217
+ # @return [String]
218
+ def route
219
+ @openrouter_params[:route] || DEFAULTS[:route]
220
+ end
221
+
222
+ # Sets routing strategy
223
+ #
224
+ # @param value [String]
225
+ # @return [void]
226
+ def route=(value)
227
+ @openrouter_params[:route] = value
228
+ end
229
+
230
+ private
231
+
232
+ # @api private
233
+ # @param params [Hash]
234
+ # @return [Hash]
235
+ def apply_defaults(params)
236
+ # Set default model if not provided
237
+ params[:model] ||= "openrouter/auto"
238
+
239
+ # Apply other defaults
240
+ DEFAULTS.each do |key, value|
241
+ params[key] = value unless params.key?(key)
242
+ end
243
+
244
+ params
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "messages/_types"
4
+ require_relative "provider_preferences/_types"
5
+ require_relative "plugins/_types"
6
+
7
+ require_relative "prediction"
8
+ require_relative "provider_preferences"
9
+ require_relative "response_format"
10
+ require_relative "plugin"
11
+
12
+ module ActiveAgent
13
+ module Providers
14
+ module OpenRouter
15
+ module Requests
16
+ # Type for MaxPrice
17
+ class MaxPriceType < ActiveModel::Type::Value
18
+ def cast(value)
19
+ case value
20
+ when MaxPrice
21
+ value
22
+ when Hash
23
+ MaxPrice.new(**value.deep_symbolize_keys)
24
+ when nil
25
+ nil
26
+ else
27
+ raise ArgumentError, "Cannot cast #{value.class} to MaxPrice"
28
+ end
29
+ end
30
+
31
+ def serialize(value)
32
+ case value
33
+ when MaxPrice
34
+ value.serialize
35
+ when Hash
36
+ value
37
+ when nil
38
+ nil
39
+ else
40
+ raise ArgumentError, "Cannot serialize #{value.class}"
41
+ end
42
+ end
43
+
44
+ def deserialize(value)
45
+ cast(value)
46
+ end
47
+ end
48
+
49
+ # Type for Prediction
50
+ class PredictionType < ActiveModel::Type::Value
51
+ def cast(value)
52
+ case value
53
+ when Prediction
54
+ value
55
+ when Hash
56
+ Prediction.new(**value.deep_symbolize_keys)
57
+ when nil
58
+ nil
59
+ else
60
+ raise ArgumentError, "Cannot cast #{value.class} to Prediction"
61
+ end
62
+ end
63
+
64
+ def serialize(value)
65
+ case value
66
+ when Prediction
67
+ value.serialize
68
+ when Hash
69
+ value
70
+ when nil
71
+ nil
72
+ else
73
+ raise ArgumentError, "Cannot serialize #{value.class}"
74
+ end
75
+ end
76
+
77
+ def deserialize(value)
78
+ cast(value)
79
+ end
80
+ end
81
+
82
+ # Type for ProviderPreferences
83
+ class ProviderPreferencesType < ActiveModel::Type::Value
84
+ def cast(value)
85
+ case value
86
+ when ProviderPreferences
87
+ value
88
+ when Hash
89
+ ProviderPreferences.new(**value.deep_symbolize_keys)
90
+ when nil
91
+ nil
92
+ else
93
+ raise ArgumentError, "Cannot cast #{value.class} to ProviderPreferences"
94
+ end
95
+ end
96
+
97
+ def serialize(value)
98
+ case value
99
+ when ProviderPreferences
100
+ value.serialize
101
+ when Hash
102
+ value
103
+ when nil
104
+ nil
105
+ else
106
+ raise ArgumentError, "Cannot serialize #{value.class}"
107
+ end
108
+ end
109
+
110
+ def deserialize(value)
111
+ cast(value)
112
+ end
113
+ end
114
+
115
+ # Type for ResponseFormat
116
+ class ResponseFormatType < ActiveModel::Type::Value
117
+ def cast(value)
118
+ case value
119
+ when ResponseFormat
120
+ value
121
+ when Hash
122
+ ResponseFormat.new(**value.deep_symbolize_keys)
123
+ when nil
124
+ nil
125
+ else
126
+ raise ArgumentError, "Cannot cast #{value.class} to ResponseFormat"
127
+ end
128
+ end
129
+
130
+ def serialize(value)
131
+ case value
132
+ when ResponseFormat
133
+ value.serialize
134
+ when Hash
135
+ value
136
+ when nil
137
+ nil
138
+ else
139
+ raise ArgumentError, "Cannot serialize #{value.class}"
140
+ end
141
+ end
142
+
143
+ def deserialize(value)
144
+ cast(value)
145
+ end
146
+ end
147
+
148
+ # Type for Plugins (array of Plugin objects)
149
+ class PluginsType < ActiveModel::Type::Value
150
+ def cast(value)
151
+ case value
152
+ when Array
153
+ value.map do |item|
154
+ case item
155
+ when Plugin
156
+ item
157
+ when Hash
158
+ Plugin.new(**item.deep_symbolize_keys)
159
+ else
160
+ raise ArgumentError, "Cannot cast #{item.class} to Plugin"
161
+ end
162
+ end
163
+ when nil
164
+ nil
165
+ else
166
+ raise ArgumentError, "Cannot cast #{value.class} to Array of Plugins"
167
+ end
168
+ end
169
+
170
+ def serialize(value)
171
+ case value
172
+ when Array
173
+ value.map do |item|
174
+ case item
175
+ when Plugin
176
+ item.serialize
177
+ when Hash
178
+ item
179
+ else
180
+ raise ArgumentError, "Cannot serialize #{item.class}"
181
+ end
182
+ end
183
+ when nil
184
+ nil
185
+ else
186
+ raise ArgumentError, "Cannot serialize #{value.class}"
187
+ end
188
+ end
189
+
190
+ def deserialize(value)
191
+ cast(value)
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../transforms"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module OpenRouter
8
+ module Requests
9
+ module Messages
10
+ # ActiveModel type for casting and normalizing messages
11
+ #
12
+ # Delegates to OpenRouter transforms which use OpenAI's message normalization.
13
+ class MessagesType < ActiveModel::Type::Value
14
+ # Casts value to normalized messages array
15
+ #
16
+ # @param value [Array, String, Hash, nil]
17
+ # @return [Array, nil]
18
+ def cast(value)
19
+ return nil if value.nil?
20
+ Transforms.normalize_messages(value)
21
+ end
22
+
23
+ # Serializes messages to hash array
24
+ #
25
+ # @param value [Array, nil]
26
+ # @return [Array, nil]
27
+ def serialize(value)
28
+ return nil if value.nil?
29
+
30
+ # If already serialized as hashes, return as-is
31
+ return value if value.is_a?(Array) && value.all? { |m| m.is_a?(Hash) }
32
+
33
+ # Otherwise convert gem objects to hashes
34
+ value.map { |msg| Transforms.gem_to_hash(msg) }
35
+ end
36
+
37
+ # @param value [Object]
38
+ # @return [Array, nil]
39
+ def deserialize(value)
40
+ cast(value)
41
+ end
42
+ end
43
+
44
+ # Kept for backwards compatibility but delegates to MessagesType
45
+ class MessageType < MessagesType
46
+ def cast(value)
47
+ # Single message - wrap in array then unwrap
48
+ result = super(value.is_a?(Array) ? value : [ value ])
49
+ result&.first
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/open_ai/chat/requests/messages/content/_types"
4
+ require_relative "file"
5
+
6
+ module ActiveAgent
7
+ module Providers
8
+ module OpenRouter
9
+ module Requests
10
+ module Messages
11
+ module Content
12
+ # Type for handling content (string or array of content parts) in OpenRouter.
13
+ #
14
+ # Extends OpenAI's ContentsType to use OpenRouter's ContentType which
15
+ # handles files differently (preserves data URI prefix).
16
+ class ContentsType < OpenAI::Chat::Requests::Messages::Content::ContentsType
17
+ def initialize
18
+ super
19
+ @content_type = ContentType.new
20
+ end
21
+ end
22
+
23
+ # Type for individual content items in OpenRouter.
24
+ #
25
+ # Uses OpenRouter's File class for file content which preserves
26
+ # the data URI prefix that OpenAI strips.
27
+ class ContentType < ActiveModel::Type::Value
28
+ def cast(value)
29
+ case value
30
+ when OpenAI::Chat::Requests::Messages::Content::Base
31
+ value
32
+ when Hash
33
+ hash = value.deep_symbolize_keys
34
+ type = hash[:type]&.to_sym
35
+
36
+ case type
37
+ when :text
38
+ OpenAI::Chat::Requests::Messages::Content::Text.new(**hash)
39
+ when :image_url
40
+ OpenAI::Chat::Requests::Messages::Content::Image.new(**hash)
41
+ when :input_audio
42
+ OpenAI::Chat::Requests::Messages::Content::Audio.new(**hash)
43
+ when :file
44
+ # Use OpenRouter's File class instead of OpenAI's
45
+ File.new(**hash)
46
+ when :refusal
47
+ OpenAI::Chat::Requests::Messages::Content::Refusal.new(**hash)
48
+ when nil
49
+ # When type is nil, check for specific content keys to infer type
50
+ if hash.key?(:text)
51
+ OpenAI::Chat::Requests::Messages::Content::Text.new(**hash)
52
+ elsif hash.key?(:image)
53
+ OpenAI::Chat::Requests::Messages::Content::Image.new(**hash.merge(image_url: hash.delete(:image)))
54
+ elsif hash.key?(:document)
55
+ # Use OpenRouter's File class for document content
56
+ File.new(**hash.merge(file: hash.delete(:document)))
57
+ else
58
+ raise ArgumentError, "Cannot determine content type from hash keys: #{hash.keys.inspect}"
59
+ end
60
+ else
61
+ raise ArgumentError, "Unknown content type: #{type.inspect}"
62
+ end
63
+ when String
64
+ # Plain text string becomes text content
65
+ OpenAI::Chat::Requests::Messages::Content::Text.new(text: value)
66
+ when nil
67
+ nil
68
+ else
69
+ raise ArgumentError, "Cannot cast #{value.class} to Content (expected Base, Hash, String, or nil)"
70
+ end
71
+ end
72
+
73
+ def serialize(value)
74
+ case value
75
+ when OpenAI::Chat::Requests::Messages::Content::Base
76
+ value.serialize
77
+ when Hash
78
+ value
79
+ when String
80
+ { type: "text", text: value }
81
+ when nil
82
+ nil
83
+ else
84
+ raise ArgumentError, "Cannot serialize #{value.class} as Content"
85
+ end
86
+ end
87
+
88
+ def deserialize(value)
89
+ cast(value)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/open_ai/chat/requests/messages/content/base"
4
+ require_relative "files/_types"
5
+
6
+ module ActiveAgent
7
+ module Providers
8
+ module OpenRouter
9
+ module Requests
10
+ module Messages
11
+ module Content
12
+ # File content part for OpenRouter messages
13
+ #
14
+ # Represents a file attachment in a message. Unlike OpenAI which strips
15
+ # the data URI prefix, OpenRouter preserves it in the file_data field.
16
+ #
17
+ # @example PDF file attachment
18
+ # file = File.new(
19
+ # file: {
20
+ # file_data: 'data:application/pdf;base64,JVBERi0...',
21
+ # filename: 'document.pdf'
22
+ # }
23
+ # )
24
+ #
25
+ # @see Files::Details
26
+ # @see https://openrouter.ai/docs/file-uploads OpenRouter File Uploads
27
+ class File < OpenAI::Chat::Requests::Messages::Content::Base
28
+ # @!attribute type
29
+ # @return [String] always "file"
30
+ attribute :type, :string, as: "file"
31
+
32
+ # @!attribute file
33
+ # @return [Files::Details] file details with data URI
34
+ attribute :file, Files::DetailsType.new
35
+
36
+ validates :file, presence: true
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "details"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module OpenRouter
8
+ module Requests
9
+ module Messages
10
+ module Content
11
+ module Files
12
+ # Type for the nested file object in OpenRouter.
13
+ #
14
+ # Uses OpenRouter's Details class which preserves the data URI prefix.
15
+ class DetailsType < ActiveModel::Type::Value
16
+ def cast(value)
17
+ case value
18
+ when Details
19
+ value
20
+ when Hash
21
+ Details.new(**value.deep_symbolize_keys)
22
+ when String
23
+ # Accept both data URIs and plain base64, but preserve the format
24
+ if value.start_with?("data:")
25
+ Details.new(file_data: value)
26
+ elsif value.match?(%r{\Ahttps?://})
27
+ raise ArgumentError, "HTTP/S URLs are not supported. Use a base64 data URI instead"
28
+ else
29
+ Details.new(file_data: value)
30
+ end
31
+ when nil
32
+ nil
33
+ else
34
+ raise ArgumentError, "Cannot cast #{value.class} to File::Details"
35
+ end
36
+ end
37
+
38
+ def serialize(value)
39
+ case value
40
+ when Details
41
+ value.serialize
42
+ when Hash
43
+ value
44
+ when nil
45
+ nil
46
+ else
47
+ raise ArgumentError, "Cannot serialize #{value.class}"
48
+ end
49
+ end
50
+
51
+ def deserialize(value)
52
+ cast(value)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end