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
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
begin
|
|
2
|
-
gem "ruby-openai", ">= 8.1.0"
|
|
3
|
-
require "openai"
|
|
4
|
-
rescue LoadError
|
|
5
|
-
raise LoadError, "The 'ruby-openai >= 8.1.0' gem is required for OpenAIProvider. Please add it to your Gemfile and run `bundle install`."
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
require "active_agent/action_prompt/action"
|
|
9
|
-
require_relative "base"
|
|
10
|
-
require_relative "response"
|
|
11
|
-
require_relative "responses_adapter"
|
|
12
|
-
require_relative "stream_processing"
|
|
13
|
-
require_relative "message_formatting"
|
|
14
|
-
require_relative "tool_management"
|
|
15
|
-
|
|
16
|
-
module ActiveAgent
|
|
17
|
-
module GenerationProvider
|
|
18
|
-
class OpenAIProvider < Base
|
|
19
|
-
include StreamProcessing
|
|
20
|
-
include MessageFormatting
|
|
21
|
-
include ToolManagement
|
|
22
|
-
def initialize(config)
|
|
23
|
-
super
|
|
24
|
-
@host = config["host"] || nil
|
|
25
|
-
@api_type = config["api_type"] || nil
|
|
26
|
-
@access_token ||= config["api_key"] || config["access_token"] || OpenAI.configuration.access_token || ENV["OPENAI_ACCESS_TOKEN"]
|
|
27
|
-
@organization_id = config["organization_id"] || OpenAI.configuration.organization_id || ENV["OPENAI_ORGANIZATION_ID"]
|
|
28
|
-
@admin_token = config["admin_token"] || OpenAI.configuration.admin_token || ENV["OPENAI_ADMIN_TOKEN"]
|
|
29
|
-
@client = OpenAI::Client.new(
|
|
30
|
-
access_token: @access_token,
|
|
31
|
-
uri_base: @host,
|
|
32
|
-
organization_id: @organization_id,
|
|
33
|
-
admin_token: @admin_token,
|
|
34
|
-
api_type: @api_type,
|
|
35
|
-
log_errors: Rails.env.development?
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
@model_name = config["model"] || "gpt-4o-mini"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def generate(prompt)
|
|
42
|
-
@prompt = prompt
|
|
43
|
-
|
|
44
|
-
with_error_handling do
|
|
45
|
-
if @prompt.multimodal? || @prompt.content_type == "multipart/mixed"
|
|
46
|
-
responses_prompt(parameters: responses_parameters)
|
|
47
|
-
else
|
|
48
|
-
chat_prompt(parameters: prompt_parameters)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def embed(prompt)
|
|
54
|
-
@prompt = prompt
|
|
55
|
-
|
|
56
|
-
with_error_handling do
|
|
57
|
-
embeddings_prompt(parameters: embeddings_parameters)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
protected
|
|
62
|
-
|
|
63
|
-
# Override from StreamProcessing module
|
|
64
|
-
def process_stream_chunk(chunk, message, agent_stream)
|
|
65
|
-
new_content = chunk.dig("choices", 0, "delta", "content")
|
|
66
|
-
if new_content && !new_content.blank?
|
|
67
|
-
message.generation_id = chunk.dig("id")
|
|
68
|
-
message.content += new_content
|
|
69
|
-
# Call agent_stream directly without the block to avoid double execution
|
|
70
|
-
agent_stream&.call(message, new_content, false, prompt.action_name)
|
|
71
|
-
elsif chunk.dig("choices", 0, "delta", "tool_calls") && chunk.dig("choices", 0, "delta", "role")
|
|
72
|
-
message = handle_message(chunk.dig("choices", 0, "delta"))
|
|
73
|
-
prompt.messages << message
|
|
74
|
-
@response = ActiveAgent::GenerationProvider::Response.new(
|
|
75
|
-
prompt:,
|
|
76
|
-
message:,
|
|
77
|
-
raw_response: chunk,
|
|
78
|
-
raw_request: @streaming_request_params
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if chunk.dig("choices", 0, "finish_reason")
|
|
83
|
-
finalize_stream(message, agent_stream)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Override from MessageFormatting module to handle OpenAI image format
|
|
88
|
-
def format_image_content(message)
|
|
89
|
-
[ {
|
|
90
|
-
type: "image_url",
|
|
91
|
-
image_url: { url: message.content }
|
|
92
|
-
} ]
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
# Override from ParameterBuilder to add web_search_options for Chat API
|
|
98
|
-
def build_provider_parameters
|
|
99
|
-
params = {}
|
|
100
|
-
|
|
101
|
-
# Check if we're using a model that supports web_search_options in Chat API
|
|
102
|
-
if chat_api_web_search_model? && @prompt.options[:web_search]
|
|
103
|
-
params[:web_search_options] = build_web_search_options(@prompt.options[:web_search])
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
params
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def chat_api_web_search_model?
|
|
110
|
-
model = @prompt.options[:model] || @model_name
|
|
111
|
-
[ "gpt-4o-search-preview", "gpt-4o-mini-search-preview" ].include?(model)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def build_web_search_options(web_search_config)
|
|
115
|
-
options = {}
|
|
116
|
-
|
|
117
|
-
if web_search_config.is_a?(Hash)
|
|
118
|
-
options[:search_context_size] = web_search_config[:search_context_size] if web_search_config[:search_context_size]
|
|
119
|
-
|
|
120
|
-
if web_search_config[:user_location]
|
|
121
|
-
options[:user_location] = {
|
|
122
|
-
type: "approximate",
|
|
123
|
-
approximate: web_search_config[:user_location]
|
|
124
|
-
}
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
options
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def chat_response(response, request_params = nil)
|
|
132
|
-
return @response if prompt.options[:stream]
|
|
133
|
-
message_json = response.dig("choices", 0, "message")
|
|
134
|
-
message_json["id"] = response.dig("id") if message_json["id"].blank?
|
|
135
|
-
message = handle_message(message_json)
|
|
136
|
-
|
|
137
|
-
update_context(prompt: prompt, message: message, response: response)
|
|
138
|
-
|
|
139
|
-
@response = ActiveAgent::GenerationProvider::Response.new(
|
|
140
|
-
prompt: prompt,
|
|
141
|
-
message: message,
|
|
142
|
-
raw_response: response,
|
|
143
|
-
raw_request: request_params
|
|
144
|
-
)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def responses_response(response, request_params = nil)
|
|
148
|
-
message_json = response["output"].find { |output_item| output_item["type"] == "message" }
|
|
149
|
-
message_json["id"] = response.dig("id") if message_json["id"].blank?
|
|
150
|
-
|
|
151
|
-
message = ActiveAgent::ActionPrompt::Message.new(
|
|
152
|
-
generate_id: message_json["id"],
|
|
153
|
-
content: message_json["content"].first["text"],
|
|
154
|
-
role: message_json["role"].intern,
|
|
155
|
-
action_requested: message_json["finish_reason"] == "tool_calls",
|
|
156
|
-
raw_actions: message_json["tool_calls"] || [],
|
|
157
|
-
content_type: prompt.output_schema.present? ? "application/json" : "text/plain"
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
@response = ActiveAgent::GenerationProvider::Response.new(
|
|
161
|
-
prompt: prompt,
|
|
162
|
-
message: message,
|
|
163
|
-
raw_response: response,
|
|
164
|
-
raw_request: request_params
|
|
165
|
-
)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def handle_message(message_json)
|
|
169
|
-
ActiveAgent::ActionPrompt::Message.new(
|
|
170
|
-
generation_id: message_json["id"],
|
|
171
|
-
content: message_json["content"],
|
|
172
|
-
role: message_json["role"].intern,
|
|
173
|
-
action_requested: message_json["finish_reason"] == "tool_calls",
|
|
174
|
-
raw_actions: message_json["tool_calls"] || [],
|
|
175
|
-
requested_actions: handle_actions(message_json["tool_calls"]),
|
|
176
|
-
content_type: prompt.output_schema.present? ? "application/json" : "text/plain"
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# handle_actions is now provided by ToolManagement module
|
|
181
|
-
|
|
182
|
-
def chat_prompt(parameters: prompt_parameters)
|
|
183
|
-
if prompt.options[:stream] || config["stream"]
|
|
184
|
-
parameters[:stream] = provider_stream
|
|
185
|
-
@streaming_request_params = parameters
|
|
186
|
-
end
|
|
187
|
-
chat_response(@client.chat(parameters: parameters), parameters)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def responses_prompt(parameters: responses_parameters)
|
|
191
|
-
# parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
|
|
192
|
-
responses_response(@client.responses.create(parameters: parameters), parameters)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def responses_parameters(model: @prompt.options[:model] || @model_name, messages: @prompt.messages, temperature: @prompt.options[:temperature] || @config["temperature"] || 0.7, tools: @prompt.actions, structured_output: @prompt.output_schema)
|
|
196
|
-
# Build tools array, combining action tools with built-in tools
|
|
197
|
-
tools_array = build_tools_for_responses(tools)
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
model: model,
|
|
201
|
-
input: ActiveAgent::GenerationProvider::ResponsesAdapter.new(@prompt).input,
|
|
202
|
-
tools: tools_array.presence,
|
|
203
|
-
text: structured_output
|
|
204
|
-
}.compact
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def build_tools_for_responses(action_tools)
|
|
208
|
-
tools = []
|
|
209
|
-
|
|
210
|
-
# Start with action tools (user-defined functions) if any
|
|
211
|
-
tools.concat(action_tools) if action_tools.present?
|
|
212
|
-
|
|
213
|
-
# Add built-in tools if specified in options[:tools]
|
|
214
|
-
if @prompt.options[:tools].present?
|
|
215
|
-
built_in_tools = @prompt.options[:tools]
|
|
216
|
-
built_in_tools = [ built_in_tools ] unless built_in_tools.is_a?(Array)
|
|
217
|
-
|
|
218
|
-
built_in_tools.each do |tool|
|
|
219
|
-
next unless tool.is_a?(Hash)
|
|
220
|
-
|
|
221
|
-
case tool[:type]
|
|
222
|
-
when "web_search_preview", "web_search"
|
|
223
|
-
web_search_tool = { type: "web_search_preview" }
|
|
224
|
-
web_search_tool[:search_context_size] = tool[:search_context_size] if tool[:search_context_size]
|
|
225
|
-
web_search_tool[:user_location] = tool[:user_location] if tool[:user_location]
|
|
226
|
-
tools << web_search_tool
|
|
227
|
-
|
|
228
|
-
when "image_generation"
|
|
229
|
-
image_gen_tool = { type: "image_generation" }
|
|
230
|
-
image_gen_tool[:size] = tool[:size] if tool[:size]
|
|
231
|
-
image_gen_tool[:quality] = tool[:quality] if tool[:quality]
|
|
232
|
-
image_gen_tool[:format] = tool[:format] if tool[:format]
|
|
233
|
-
image_gen_tool[:compression] = tool[:compression] if tool[:compression]
|
|
234
|
-
image_gen_tool[:background] = tool[:background] if tool[:background]
|
|
235
|
-
image_gen_tool[:partial_images] = tool[:partial_images] if tool[:partial_images]
|
|
236
|
-
tools << image_gen_tool
|
|
237
|
-
|
|
238
|
-
when "mcp"
|
|
239
|
-
mcp_tool = { type: "mcp" }
|
|
240
|
-
mcp_tool[:server_label] = tool[:server_label] if tool[:server_label]
|
|
241
|
-
mcp_tool[:server_description] = tool[:server_description] if tool[:server_description]
|
|
242
|
-
mcp_tool[:server_url] = tool[:server_url] if tool[:server_url]
|
|
243
|
-
mcp_tool[:connector_id] = tool[:connector_id] if tool[:connector_id]
|
|
244
|
-
mcp_tool[:authorization] = tool[:authorization] if tool[:authorization]
|
|
245
|
-
mcp_tool[:require_approval] = tool[:require_approval] if tool[:require_approval]
|
|
246
|
-
mcp_tool[:allowed_tools] = tool[:allowed_tools] if tool[:allowed_tools]
|
|
247
|
-
tools << mcp_tool
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
tools
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def embeddings_parameters(input: prompt.message.content, model: "text-embedding-3-large")
|
|
256
|
-
{
|
|
257
|
-
model: model,
|
|
258
|
-
input: input
|
|
259
|
-
}
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def embeddings_response(response, request_params = nil)
|
|
263
|
-
message = ActiveAgent::ActionPrompt::Message.new(content: response.dig("data", 0, "embedding"), role: "assistant")
|
|
264
|
-
|
|
265
|
-
@response = ActiveAgent::GenerationProvider::Response.new(
|
|
266
|
-
prompt: prompt,
|
|
267
|
-
message: message,
|
|
268
|
-
raw_response: response,
|
|
269
|
-
raw_request: request_params
|
|
270
|
-
)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def embeddings_prompt(parameters:)
|
|
274
|
-
params = embeddings_parameters
|
|
275
|
-
embeddings_response(@client.embeddings(parameters: params), params)
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
end
|
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
require "openai"
|
|
2
|
-
require_relative "open_ai_provider"
|
|
3
|
-
|
|
4
|
-
module ActiveAgent
|
|
5
|
-
module GenerationProvider
|
|
6
|
-
class OpenRouterProvider < OpenAIProvider
|
|
7
|
-
def initialize(config)
|
|
8
|
-
@config = config
|
|
9
|
-
@access_token = config["api_key"] || config["access_token"] ||
|
|
10
|
-
ENV["OPENROUTER_API_KEY"] || ENV["OPENROUTER_ACCESS_TOKEN"]
|
|
11
|
-
@model_name = config["model"]
|
|
12
|
-
|
|
13
|
-
# OpenRouter-specific configuration
|
|
14
|
-
@app_name = config["app_name"] || default_app_name
|
|
15
|
-
@site_url = config["site_url"] || default_site_url
|
|
16
|
-
@enable_fallbacks = config["enable_fallbacks"] != false
|
|
17
|
-
@fallback_models = config["fallback_models"] || []
|
|
18
|
-
@transforms = config["transforms"] || []
|
|
19
|
-
@provider_preferences = config["provider"] || {}
|
|
20
|
-
@track_costs = config["track_costs"] != false
|
|
21
|
-
@route = config["route"] || "fallback"
|
|
22
|
-
|
|
23
|
-
# Data collection preference (allow, deny, or specific provider list)
|
|
24
|
-
@data_collection = config["data_collection"] || @provider_preferences["data_collection"] || "allow"
|
|
25
|
-
|
|
26
|
-
# Require parameters preference (defaults to false)
|
|
27
|
-
@require_parameters = config["require_parameters"] || @provider_preferences["require_parameters"] || false
|
|
28
|
-
|
|
29
|
-
# Additional OpenRouter provider routing options
|
|
30
|
-
@only_providers = config["only"] || @provider_preferences["only"]
|
|
31
|
-
@ignore_providers = config["ignore"] || @provider_preferences["ignore"]
|
|
32
|
-
@quantizations = config["quantizations"] || @provider_preferences["quantizations"]
|
|
33
|
-
@sort_preference = config["sort"] || @provider_preferences["sort"]
|
|
34
|
-
@max_price = config["max_price"] || @provider_preferences["max_price"]
|
|
35
|
-
|
|
36
|
-
# Initialize OpenAI client with OpenRouter base URL
|
|
37
|
-
@client = OpenAI::Client.new(
|
|
38
|
-
uri_base: "https://openrouter.ai/api/v1",
|
|
39
|
-
access_token: @access_token,
|
|
40
|
-
log_errors: Rails.env.development?,
|
|
41
|
-
extra_headers: openrouter_headers
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def generate(prompt)
|
|
46
|
-
@prompt = prompt
|
|
47
|
-
|
|
48
|
-
with_error_handling do
|
|
49
|
-
parameters = build_openrouter_parameters
|
|
50
|
-
response = execute_with_fallback(parameters)
|
|
51
|
-
process_openrouter_response(response)
|
|
52
|
-
end
|
|
53
|
-
rescue => e
|
|
54
|
-
handle_openrouter_error(e)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
protected
|
|
58
|
-
|
|
59
|
-
def build_provider_parameters
|
|
60
|
-
# Start with base OpenAI parameters
|
|
61
|
-
params = super
|
|
62
|
-
|
|
63
|
-
# Add OpenRouter-specific parameters
|
|
64
|
-
add_openrouter_params(params)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def format_content_item(item)
|
|
68
|
-
# Handle OpenRouter-specific content formats
|
|
69
|
-
if item.is_a?(Hash)
|
|
70
|
-
case item[:type] || item["type"]
|
|
71
|
-
when "file"
|
|
72
|
-
# Convert file type to image_url for OpenRouter PDF support
|
|
73
|
-
file_data = item.dig(:file, :file_data) || item.dig("file", "file_data")
|
|
74
|
-
if file_data
|
|
75
|
-
{
|
|
76
|
-
type: "image_url",
|
|
77
|
-
image_url: {
|
|
78
|
-
url: file_data
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else
|
|
82
|
-
item
|
|
83
|
-
end
|
|
84
|
-
else
|
|
85
|
-
# Use default formatting for other types
|
|
86
|
-
super
|
|
87
|
-
end
|
|
88
|
-
else
|
|
89
|
-
super
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def default_app_name
|
|
96
|
-
if defined?(Rails) && Rails.application
|
|
97
|
-
Rails.application.class.name.split("::").first
|
|
98
|
-
else
|
|
99
|
-
"ActiveAgent"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def default_site_url
|
|
104
|
-
# First check ActiveAgent config
|
|
105
|
-
return config["default_url_options"]["host"] if config.dig("default_url_options", "host")
|
|
106
|
-
|
|
107
|
-
# Then check Rails routes default_url_options
|
|
108
|
-
if defined?(Rails) && Rails.application&.routes&.default_url_options&.any?
|
|
109
|
-
host = Rails.application.routes.default_url_options[:host]
|
|
110
|
-
port = Rails.application.routes.default_url_options[:port]
|
|
111
|
-
protocol = Rails.application.routes.default_url_options[:protocol] || "https"
|
|
112
|
-
|
|
113
|
-
if host
|
|
114
|
-
url = "#{protocol}://#{host}"
|
|
115
|
-
url += ":#{port}" if port && port != 80 && port != 443
|
|
116
|
-
return url
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Finally check ActionMailer config as fallback
|
|
121
|
-
if defined?(Rails) && Rails.application&.config&.action_mailer&.default_url_options
|
|
122
|
-
options = Rails.application.config.action_mailer.default_url_options
|
|
123
|
-
host = options[:host]
|
|
124
|
-
port = options[:port]
|
|
125
|
-
protocol = options[:protocol] || "https"
|
|
126
|
-
|
|
127
|
-
if host
|
|
128
|
-
url = "#{protocol}://#{host}"
|
|
129
|
-
url += ":#{port}" if port && port != 80 && port != 443
|
|
130
|
-
return url
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
nil
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def openrouter_headers
|
|
138
|
-
headers = {}
|
|
139
|
-
headers["HTTP-Referer"] = @site_url if @site_url
|
|
140
|
-
headers["X-Title"] = @app_name if @app_name
|
|
141
|
-
headers
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def build_openrouter_parameters
|
|
145
|
-
parameters = prompt_parameters
|
|
146
|
-
|
|
147
|
-
# Handle multiple models for fallback
|
|
148
|
-
if @fallback_models.present?
|
|
149
|
-
parameters[:models] = [ @model_name ] + @fallback_models
|
|
150
|
-
parameters[:route] = @route
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Add transforms if specified
|
|
154
|
-
parameters[:transforms] = @transforms if @transforms.present?
|
|
155
|
-
|
|
156
|
-
# Add provider preferences (always include if we have data_collection or other settings)
|
|
157
|
-
# Check both configured and runtime data_collection/require_parameters values
|
|
158
|
-
runtime_data_collection = prompt&.options&.key?(:data_collection)
|
|
159
|
-
runtime_require_parameters = prompt&.options&.key?(:require_parameters)
|
|
160
|
-
runtime_provider_options = prompt&.options&.keys&.any? { |k| [ :only, :ignore, :quantizations, :sort, :max_price ].include?(k) }
|
|
161
|
-
|
|
162
|
-
if @provider_preferences.present? || @data_collection != "allow" || @require_parameters != false ||
|
|
163
|
-
@only_providers.present? || @ignore_providers.present? || @quantizations.present? ||
|
|
164
|
-
@sort_preference.present? || @max_price.present? ||
|
|
165
|
-
runtime_data_collection || runtime_require_parameters || runtime_provider_options
|
|
166
|
-
parameters[:provider] = build_provider_preferences
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Add plugins (e.g., for PDF processing)
|
|
170
|
-
|
|
171
|
-
parameters[:plugins] = prompt.options[:plugins] if prompt.options[:plugins].present?
|
|
172
|
-
parameters[:models] = prompt.options[:fallback_models] if prompt.options[:enable_fallbacks] && prompt.options[:fallback_models].present?
|
|
173
|
-
parameters
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def build_provider_preferences
|
|
177
|
-
prefs = {}
|
|
178
|
-
prefs[:order] = @provider_preferences["order"] if @provider_preferences["order"]
|
|
179
|
-
|
|
180
|
-
# Require parameters can be overridden at runtime
|
|
181
|
-
require_parameters = prompt.options[:require_parameters] if prompt&.options&.key?(:require_parameters)
|
|
182
|
-
require_parameters = @require_parameters if require_parameters.nil?
|
|
183
|
-
prefs[:require_parameters] = require_parameters if require_parameters != false
|
|
184
|
-
|
|
185
|
-
prefs[:allow_fallbacks] = @enable_fallbacks
|
|
186
|
-
|
|
187
|
-
# Data collection can be:
|
|
188
|
-
# - "allow" (default): Allow all providers to collect data
|
|
189
|
-
# - "deny": Deny all providers from collecting data
|
|
190
|
-
# - Array of provider names: Only allow these providers to collect data
|
|
191
|
-
# Check prompt options first (runtime override), then fall back to configured value
|
|
192
|
-
data_collection = prompt.options[:data_collection] if prompt&.options&.key?(:data_collection)
|
|
193
|
-
data_collection ||= @data_collection
|
|
194
|
-
prefs[:data_collection] = data_collection
|
|
195
|
-
|
|
196
|
-
# Additional OpenRouter provider routing options - check runtime overrides first
|
|
197
|
-
only_providers = prompt.options[:only] if prompt&.options&.key?(:only)
|
|
198
|
-
only_providers ||= @only_providers
|
|
199
|
-
prefs[:only] = only_providers if only_providers.present?
|
|
200
|
-
|
|
201
|
-
ignore_providers = prompt.options[:ignore] if prompt&.options&.key?(:ignore)
|
|
202
|
-
ignore_providers ||= @ignore_providers
|
|
203
|
-
prefs[:ignore] = ignore_providers if ignore_providers.present?
|
|
204
|
-
|
|
205
|
-
quantizations = prompt.options[:quantizations] if prompt&.options&.key?(:quantizations)
|
|
206
|
-
quantizations ||= @quantizations
|
|
207
|
-
prefs[:quantizations] = quantizations if quantizations.present?
|
|
208
|
-
|
|
209
|
-
sort_preference = prompt.options[:sort] if prompt&.options&.key?(:sort)
|
|
210
|
-
sort_preference ||= @sort_preference
|
|
211
|
-
prefs[:sort] = sort_preference if sort_preference.present?
|
|
212
|
-
|
|
213
|
-
max_price = prompt.options[:max_price] if prompt&.options&.key?(:max_price)
|
|
214
|
-
max_price ||= @max_price
|
|
215
|
-
prefs[:max_price] = max_price if max_price.present?
|
|
216
|
-
|
|
217
|
-
prefs.compact
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def add_openrouter_params(params)
|
|
221
|
-
# Add OpenRouter-specific routing parameters
|
|
222
|
-
if @enable_fallbacks && @fallback_models.present?
|
|
223
|
-
params[:models] = [ @model_name ] + @fallback_models
|
|
224
|
-
params[:route] = @route
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Add transforms
|
|
228
|
-
params[:transforms] = @transforms if @transforms.present?
|
|
229
|
-
|
|
230
|
-
# Add provider configuration (always include if we have data_collection or other settings)
|
|
231
|
-
# Check both configured and runtime data_collection/require_parameters values
|
|
232
|
-
runtime_data_collection = prompt&.options&.key?(:data_collection)
|
|
233
|
-
runtime_require_parameters = prompt&.options&.key?(:require_parameters)
|
|
234
|
-
runtime_provider_options = prompt&.options&.keys&.any? { |k| [ :only, :ignore, :quantizations, :sort, :max_price ].include?(k) }
|
|
235
|
-
|
|
236
|
-
if @provider_preferences.present? || @data_collection != "allow" || @require_parameters != false ||
|
|
237
|
-
@only_providers.present? || @ignore_providers.present? || @quantizations.present? ||
|
|
238
|
-
@sort_preference.present? || @max_price.present? ||
|
|
239
|
-
runtime_data_collection || runtime_require_parameters || runtime_provider_options
|
|
240
|
-
params[:provider] = build_provider_preferences
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Add plugins (e.g., for PDF processing)
|
|
244
|
-
if prompt.options[:plugins].present?
|
|
245
|
-
params[:plugins] = prompt.options[:plugins]
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
params
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def execute_with_fallback(parameters)
|
|
252
|
-
parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
|
|
253
|
-
|
|
254
|
-
response = @client.chat(parameters: parameters)
|
|
255
|
-
# Log if fallback was used
|
|
256
|
-
if response.respond_to?(:headers) && response.headers["x-model"] != @model_name
|
|
257
|
-
Rails.logger.info "[OpenRouter] Fallback model used: #{response.headers['x-model']}" if defined?(Rails)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
response
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def process_openrouter_response(response)
|
|
264
|
-
# Process as normal OpenAI response first
|
|
265
|
-
if prompt.options[:stream]
|
|
266
|
-
return @response
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# Extract standard response
|
|
270
|
-
message_json = response.dig("choices", 0, "message")
|
|
271
|
-
message_json["id"] = response.dig("id") if message_json && message_json["id"].blank?
|
|
272
|
-
message = handle_message(message_json) if message_json
|
|
273
|
-
|
|
274
|
-
update_context(prompt: prompt, message: message, response: response) if message
|
|
275
|
-
# Create response with OpenRouter metadata
|
|
276
|
-
@response = ActiveAgent::GenerationProvider::Response.new(
|
|
277
|
-
prompt: prompt,
|
|
278
|
-
message: message,
|
|
279
|
-
raw_response: response
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# OpenRouter includes provider and model info directly in the response body
|
|
283
|
-
if response["provider"] || response["model"]
|
|
284
|
-
@response.metadata = {
|
|
285
|
-
provider: response["provider"],
|
|
286
|
-
model_used: response["model"],
|
|
287
|
-
fallback_used: response["model"] != @model_name
|
|
288
|
-
}.compact
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# Track costs if enabled
|
|
292
|
-
track_usage(response) if @track_costs && response["usage"]
|
|
293
|
-
|
|
294
|
-
@response
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def add_openrouter_metadata(response, headers)
|
|
298
|
-
return unless response.respond_to?(:metadata=)
|
|
299
|
-
|
|
300
|
-
response.metadata = {
|
|
301
|
-
provider: headers["x-provider"],
|
|
302
|
-
model_used: headers["x-model"],
|
|
303
|
-
trace_id: headers["x-trace-id"],
|
|
304
|
-
fallback_used: headers["x-model"] != @model_name,
|
|
305
|
-
ratelimit: {
|
|
306
|
-
requests_limit: headers["x-ratelimit-requests-limit"],
|
|
307
|
-
requests_remaining: headers["x-ratelimit-requests-remaining"],
|
|
308
|
-
requests_reset: headers["x-ratelimit-requests-reset"],
|
|
309
|
-
tokens_limit: headers["x-ratelimit-tokens-limit"],
|
|
310
|
-
tokens_remaining: headers["x-ratelimit-tokens-remaining"],
|
|
311
|
-
tokens_reset: headers["x-ratelimit-tokens-reset"]
|
|
312
|
-
}.compact
|
|
313
|
-
}.compact
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def track_usage(response)
|
|
317
|
-
return nil unless @track_costs
|
|
318
|
-
return nil unless response["usage"]
|
|
319
|
-
|
|
320
|
-
usage = response["usage"]
|
|
321
|
-
model = response.dig("model") || @model_name
|
|
322
|
-
|
|
323
|
-
# Calculate costs (simplified - would need actual pricing data)
|
|
324
|
-
cost_info = {
|
|
325
|
-
model: model,
|
|
326
|
-
prompt_tokens: usage["prompt_tokens"],
|
|
327
|
-
completion_tokens: usage["completion_tokens"],
|
|
328
|
-
total_tokens: usage["total_tokens"]
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
# Log usage information
|
|
332
|
-
if defined?(Rails)
|
|
333
|
-
Rails.logger.info "[OpenRouter] Usage: #{cost_info.to_json}"
|
|
334
|
-
|
|
335
|
-
# Store in cache if available
|
|
336
|
-
if Rails.cache
|
|
337
|
-
cache_key = "openrouter:usage:#{Date.current}"
|
|
338
|
-
Rails.cache.increment("#{cache_key}:tokens", usage["total_tokens"])
|
|
339
|
-
Rails.cache.increment("#{cache_key}:requests")
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
cost_info
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def handle_openrouter_error(error)
|
|
347
|
-
error_message = error.message || error.to_s
|
|
348
|
-
|
|
349
|
-
case error_message
|
|
350
|
-
when /rate limit/i
|
|
351
|
-
handle_rate_limit_error(error)
|
|
352
|
-
when /insufficient credits|payment required/i
|
|
353
|
-
handle_insufficient_credits(error)
|
|
354
|
-
when /no available provider/i
|
|
355
|
-
handle_no_provider_error(error)
|
|
356
|
-
when /timeout/i
|
|
357
|
-
handle_timeout_error(error)
|
|
358
|
-
else
|
|
359
|
-
# Fall back to parent error handling
|
|
360
|
-
raise GenerationProviderError, error, error.backtrace
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def handle_rate_limit_error(error)
|
|
365
|
-
Rails.logger.error "[OpenRouter] Rate limit exceeded: #{error.message}" if defined?(Rails)
|
|
366
|
-
raise GenerationProviderError, "OpenRouter rate limit exceeded. Please retry later."
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def handle_insufficient_credits(error)
|
|
370
|
-
Rails.logger.error "[OpenRouter] Insufficient credits: #{error.message}" if defined?(Rails)
|
|
371
|
-
raise GenerationProviderError, "OpenRouter account has insufficient credits."
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def handle_no_provider_error(error)
|
|
375
|
-
Rails.logger.error "[OpenRouter] No available provider: #{error.message}" if defined?(Rails)
|
|
376
|
-
raise GenerationProviderError, "No available provider for the requested model."
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
def handle_timeout_error(error)
|
|
380
|
-
Rails.logger.error "[OpenRouter] Request timeout: #{error.message}" if defined?(Rails)
|
|
381
|
-
raise GenerationProviderError, "OpenRouter request timed out."
|
|
382
|
-
end
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
end
|