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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +240 -2
- data/README.md +15 -24
- data/lib/active_agent/base.rb +389 -39
- data/lib/active_agent/concerns/callbacks.rb +251 -0
- data/lib/active_agent/concerns/observers.rb +147 -0
- data/lib/active_agent/concerns/parameterized.rb +292 -0
- data/lib/active_agent/concerns/provider.rb +120 -0
- data/lib/active_agent/concerns/queueing.rb +36 -0
- data/lib/active_agent/concerns/rescue.rb +64 -0
- data/lib/active_agent/concerns/streaming.rb +282 -0
- data/lib/active_agent/concerns/tooling.rb +23 -0
- data/lib/active_agent/concerns/view.rb +150 -0
- data/lib/active_agent/configuration.rb +442 -20
- data/lib/active_agent/generation.rb +141 -47
- data/lib/active_agent/providers/_base_provider.rb +420 -0
- data/lib/active_agent/providers/anthropic/_types.rb +63 -0
- data/lib/active_agent/providers/anthropic/options.rb +53 -0
- data/lib/active_agent/providers/anthropic/request.rb +163 -0
- data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
- data/lib/active_agent/providers/anthropic_provider.rb +254 -0
- data/lib/active_agent/providers/common/messages/_types.rb +160 -0
- data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
- data/lib/active_agent/providers/common/messages/base.rb +17 -0
- data/lib/active_agent/providers/common/messages/system.rb +20 -0
- data/lib/active_agent/providers/common/messages/tool.rb +21 -0
- data/lib/active_agent/providers/common/messages/user.rb +20 -0
- data/lib/active_agent/providers/common/model.rb +361 -0
- data/lib/active_agent/providers/common/response.rb +13 -0
- data/lib/active_agent/providers/common/responses/_types.rb +51 -0
- data/lib/active_agent/providers/common/responses/base.rb +199 -0
- data/lib/active_agent/providers/common/responses/embed.rb +33 -0
- data/lib/active_agent/providers/common/responses/format.rb +31 -0
- data/lib/active_agent/providers/common/responses/message.rb +3 -0
- data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/concerns/previewable.rb +150 -0
- data/lib/active_agent/providers/log_subscriber.rb +178 -0
- data/lib/active_agent/providers/mock/_types.rb +77 -0
- data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
- data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
- data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
- data/lib/active_agent/providers/mock/messages/base.rb +63 -0
- data/lib/active_agent/providers/mock/messages/user.rb +18 -0
- data/lib/active_agent/providers/mock/options.rb +30 -0
- data/lib/active_agent/providers/mock/request.rb +38 -0
- data/lib/active_agent/providers/mock_provider.rb +311 -0
- data/lib/active_agent/providers/ollama/_types.rb +5 -0
- data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama/options.rb +27 -0
- data/lib/active_agent/providers/ollama_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/_base.rb +59 -0
- data/lib/active_agent/providers/open_ai/_types.rb +5 -0
- data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/options.rb +74 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
- data/lib/active_agent/providers/open_ai_provider.rb +94 -0
- data/lib/active_agent/providers/open_router/_types.rb +71 -0
- data/lib/active_agent/providers/open_router/options.rb +141 -0
- data/lib/active_agent/providers/open_router/request.rb +249 -0
- data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
- data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
- data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
- data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
- data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
- data/lib/active_agent/providers/open_router/transforms.rb +134 -0
- data/lib/active_agent/providers/open_router_provider.rb +62 -0
- data/lib/active_agent/providers/openai_provider.rb +2 -0
- data/lib/active_agent/providers/openrouter_provider.rb +2 -0
- data/lib/active_agent/railtie.rb +8 -6
- data/lib/active_agent/schema_generator.rb +333 -166
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +112 -36
- data/lib/generators/active_agent/agent/USAGE +78 -0
- data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
- data/lib/generators/active_agent/install/USAGE +25 -0
- data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
- data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
- data/lib/generators/erb/agent_generator.rb +31 -16
- data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
- data/lib/generators/erb/templates/instructions.md.tt +3 -0
- data/lib/generators/erb/templates/instructions.text.tt +1 -0
- data/lib/generators/erb/templates/message.md.erb.tt +5 -0
- data/lib/generators/erb/templates/schema.json.tt +10 -0
- data/lib/generators/test_unit/agent_generator.rb +1 -1
- data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
- metadata +182 -71
- data/lib/active_agent/action_prompt/action.rb +0 -13
- data/lib/active_agent/action_prompt/base.rb +0 -623
- data/lib/active_agent/action_prompt/message.rb +0 -126
- data/lib/active_agent/action_prompt/prompt.rb +0 -136
- data/lib/active_agent/action_prompt.rb +0 -19
- data/lib/active_agent/callbacks.rb +0 -33
- data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
- data/lib/active_agent/generation_provider/base.rb +0 -55
- data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
- data/lib/active_agent/generation_provider/error_handling.rb +0 -167
- data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
- data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
- data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
- data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
- data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
- data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
- data/lib/active_agent/generation_provider/response.rb +0 -75
- data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
- data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
- data/lib/active_agent/generation_provider/tool_management.rb +0 -142
- data/lib/active_agent/generation_provider.rb +0 -67
- data/lib/active_agent/log_subscriber.rb +0 -44
- data/lib/active_agent/parameterized.rb +0 -75
- data/lib/active_agent/prompt_helper.rb +0 -19
- data/lib/active_agent/queued_generation.rb +0 -12
- data/lib/active_agent/rescuable.rb +0 -34
- data/lib/active_agent/sanitizers.rb +0 -40
- data/lib/active_agent/streaming.rb +0 -34
- data/lib/active_agent/test_case.rb +0 -125
- data/lib/generators/USAGE +0 -47
- data/lib/generators/active_agent/USAGE +0 -56
- data/lib/generators/erb/install_generator.rb +0 -44
- data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
- data/lib/generators/erb/templates/view.html.erb.tt +0 -5
- data/lib/generators/erb/templates/view.json.erb.tt +0 -16
- /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
- /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
|