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,200 @@
1
+ require_relative "_base"
2
+ require_relative "responses/_types"
3
+ require_relative "responses/transforms"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module OpenAI
8
+ # Provider implementation for OpenAI's Responses API
9
+ #
10
+ # Uses the responses endpoint for improved streaming and structured function
11
+ # calling compared to the chat completions endpoint.
12
+ #
13
+ # @see Base
14
+ # @see https://platform.openai.com/docs/api-reference/responses
15
+ class ResponsesProvider < Base
16
+ # @return [Class]
17
+ def self.options_klass
18
+ Options
19
+ end
20
+
21
+ # @return [Responses::RequestType]
22
+ def self.prompt_request_type
23
+ Responses::RequestType.new
24
+ end
25
+
26
+ protected
27
+
28
+ # @return [Object] OpenAI client's responses endpoint
29
+ def api_prompt_executer
30
+ client.responses
31
+ end
32
+
33
+ # @see BaseProvider#api_response_normalize
34
+ # @param api_response [OpenAI::Models::Responses::Response]
35
+ # @return [Hash] normalized response hash
36
+ def api_response_normalize(api_response)
37
+ return api_response unless api_response
38
+
39
+ Responses::Transforms.gem_to_hash(api_response)
40
+ end
41
+
42
+ # Processes streaming response chunks from the Responses API
43
+ #
44
+ # Event types handled:
45
+ # - `:"response.created"`, `:"response.in_progress"` - response lifecycle
46
+ # - `:"response.output_item.added"` - message or function call added
47
+ # - `:"response.content_part.added"` - content part started
48
+ # - `:"response.output_text.delta"` - incremental text updates
49
+ # - `:"response.output_text.done"` - complete text
50
+ # - `:"response.function_call_arguments.delta"` - function argument updates
51
+ # - `:"response.function_call_arguments.done"` - complete function arguments
52
+ # - `:"response.content_part.done"` - content part completed
53
+ # - `:"response.output_item.done"` - message or function call completed
54
+ # - `:"response.completed"` - response finished
55
+ #
56
+ # @param api_response_event [Hash] streaming chunk with :type key
57
+ # @return [void]
58
+ # @see Base#process_stream_chunk
59
+ def process_stream_chunk(api_response_event)
60
+ instrument("stream_chunk.active_agent", chunk_type: api_response_event.type)
61
+
62
+ case api_response_event.type
63
+ # Response Created
64
+ when :"response.created", :"response.in_progress"
65
+ broadcast_stream_open
66
+
67
+ # -> Message Created
68
+ when :"response.output_item.added"
69
+ process_stream_output_item_added(api_response_event)
70
+
71
+ # -> -> Content Part Create
72
+ when :"response.content_part.added"
73
+
74
+ # -> -> -> Content Text Append
75
+ when :"response.output_text.delta"
76
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
77
+ message[:content] += api_response_event.delta
78
+ broadcast_stream_update(message, api_response_event.delta)
79
+
80
+ # -> -> -> Content Text Completed [Full Text]
81
+ when :"response.output_text.done"
82
+ message = message_stack.find { _1[:id] == api_response_event.item_id }
83
+ message[:content] = api_response_event.text
84
+ broadcast_stream_update(message, nil) # Don't double send content
85
+
86
+ # -> -> -> Content Function Call Append
87
+ when :"response.function_call_arguments.delta", :"response.function_call_arguments.done"
88
+ # No-Op: Wait for FC to Land
89
+
90
+ # -> -> Content Part Completed [Full Part]
91
+ when :"response.content_part.done"
92
+
93
+ # -> Message Completed
94
+ when :"response.output_item.done"
95
+ process_stream_output_item_done(api_response_event)
96
+
97
+ # Response Completed
98
+ when :"response.completed"
99
+ # Once we are finished, close out and run tooling callbacks (Recursive)
100
+ process_prompt_finished
101
+ else
102
+ raise "Unexpected Response Chunk Type: #{api_response_event.type}"
103
+ end
104
+ end
105
+
106
+ # Processes output item added events from streaming response
107
+ #
108
+ # Handles message and function_call item types. For messages, adds to stack
109
+ # with empty content. For function calls, waits for completion event.
110
+ #
111
+ # Required because API returns empty array instead of empty string for
112
+ # initial message content due to serialization bug.
113
+ #
114
+ # @param api_response_event [Hash] response chunk with :item key
115
+ # @return [void]
116
+ def process_stream_output_item_added(api_response_event)
117
+ case api_response_event.item.type
118
+ when :message
119
+ # PATCH: API returns an empty array instead of empty string due to a bug in their serialization
120
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item).compact_blank
121
+ message_stack << { content: "" }.merge(item_hash)
122
+ when :function_call
123
+ # No-Op: Wait for FC to Land (-> response.output_item.done)
124
+ else
125
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
126
+ end
127
+ end
128
+
129
+ # Processes output item completion events from streaming response
130
+ #
131
+ # For function calls, adds completed item to message stack.
132
+ # For messages, no action needed as content already updated via delta events.
133
+ #
134
+ # @param api_response_event [Hash] response chunk with completed :item
135
+ # @return [void]
136
+ def process_stream_output_item_done(api_response_event)
137
+ case api_response_event.item.type
138
+ when :message
139
+ # No-Op: Message Up to Date
140
+ when :function_call
141
+ item_hash = Responses::Transforms.gem_to_hash(api_response_event.item)
142
+ message_stack << item_hash
143
+ else
144
+ raise "Unexpected Item Type: #{api_response_event.item.type}"
145
+ end
146
+ end
147
+
148
+ # Executes function calls and creates output messages for conversation continuation
149
+ #
150
+ # @param api_function_calls [Array<Hash>] function calls with :call_id and :name keys
151
+ # @return [void]
152
+ # @see Base#process_function_calls
153
+ def process_function_calls(api_function_calls)
154
+ api_function_calls.each do |api_function_call|
155
+ output = instrument("tool_call.active_agent", tool_name: api_function_call[:name]) do
156
+ process_tool_call_function(api_function_call).to_json
157
+ end
158
+
159
+ # Create native gem input item for function call output
160
+ message = ::OpenAI::Models::Responses::ResponseInputItem::FunctionCallOutput.new(
161
+ call_id: api_function_call[:call_id],
162
+ output:
163
+ )
164
+
165
+ # Convert to hash for message_stack
166
+ message_stack.push(Responses::Transforms.gem_to_hash(message))
167
+ end
168
+ end
169
+
170
+ # Converts OpenAI gem response object to hash for storage.
171
+ #
172
+ # @param api_response [OpenAI::Models::Responses::Response]
173
+ # @return [Common::PromptResponse, nil]
174
+ def process_prompt_finished(api_response = nil)
175
+ # Convert gem object to hash so that raw_response["usage"] works
176
+ api_response_hash = api_response ? Responses::Transforms.gem_to_hash(api_response) : nil
177
+ super(api_response_hash)
178
+ end
179
+
180
+ # Extracts messages from completed API response.
181
+ #
182
+ # @param api_response [Hash] converted response hash
183
+ # @return [Array, nil] output array from response.output or nil
184
+ def process_prompt_finished_extract_messages(api_response)
185
+ return unless api_response
186
+
187
+ # Response is already a hash from process_prompt_finished
188
+ api_response[:output]
189
+ end
190
+
191
+ # Extracts function calls from message stack.
192
+ #
193
+ # @return [Array<Hash>] function call objects with type "function_call"
194
+ def process_prompt_finished_extract_function_calls
195
+ message_stack.select { _1[:type] == "function_call" }
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "open_ai/_base"
6
+ require_relative "open_ai/chat_provider"
7
+ require_relative "open_ai/responses_provider"
8
+ require_relative "open_ai/embedding/_types"
9
+
10
+ module ActiveAgent
11
+ module Providers
12
+ # Router for OpenAI's API versions based on supported features.
13
+ #
14
+ # This provider acts as a thin wrapper that routes requests between different versions
15
+ # of OpenAI's API (Chat API and Responses API) depending on the features used in the prompt.
16
+ # It automatically selects the appropriate API version based on:
17
+ # - Explicit API version specification (+:api_version+ option)
18
+ # - Presence of audio content in the request
19
+ #
20
+ # @example Basic usage
21
+ # provider = ActiveAgent::Providers::OpenAIProvider.new(...)
22
+ # result = provider.generate
23
+ #
24
+ # @see https://platform.openai.com/docs/guides/migrate-to-responses
25
+ class OpenAIProvider < OpenAI::Base
26
+ # Returns the embedding request type for OpenAI.
27
+ #
28
+ # @return [ActiveModel::Type::Value] The OpenAI embedding request type
29
+ def self.embed_request_type
30
+ OpenAI::Embedding::RequestType.new
31
+ end
32
+
33
+ attr_internal :api_version
34
+ attr_internal :raw_options
35
+
36
+ # Initializes the OpenAI provider router.
37
+ #
38
+ # Since this layer is just routing based on API version, we want to wait
39
+ # to cast values into their types.
40
+ #
41
+ # @param kwargs [Hash] Configuration options for the provider
42
+ # @option kwargs [Symbol] :service The service name to validate
43
+ # @option kwargs [Symbol] :api_version The OpenAI API version to use (:chat or :responses)
44
+ def initialize(kwargs = {})
45
+ # For Routing Prompt APIs
46
+ self.api_version = kwargs.delete(:api_version)
47
+ self.raw_options = kwargs.deep_dup
48
+
49
+ super
50
+ end
51
+
52
+ # Generates a response by routing to the appropriate OpenAI API version.
53
+ #
54
+ # This method determines which API version to use based on the prompt context:
55
+ # - Uses Chat API if +api_version: :chat+ is specified or audio is present
56
+ # - Uses Responses API otherwise (default)
57
+ #
58
+ # @return [Object] The generation result from the selected API provider
59
+ #
60
+ # @see https://platform.openai.com/docs/guides/migrate-to-responses
61
+ def prompt
62
+ if api_version == :chat || context[:audio].present?
63
+ OpenAI::ChatProvider.new(raw_options).prompt
64
+ else # api_version == :responses || true
65
+ OpenAI::ResponsesProvider.new(raw_options).prompt
66
+ end
67
+ end
68
+
69
+ # Generates a preview by routing to the appropriate OpenAI API version.
70
+ #
71
+ # Routes to Chat API or Responses API using the same logic as {#prompt}.
72
+ #
73
+ # @return [String] markdown-formatted preview
74
+ # @see #prompt
75
+ def preview
76
+ if api_version == :chat || context[:audio].present?
77
+ OpenAI::ChatProvider.new(raw_options).preview
78
+ else # api_version == :responses || true
79
+ OpenAI::ResponsesProvider.new(raw_options).preview
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ # Executes an embedding request via OpenAI's API.
86
+ #
87
+ # @param parameters [Hash] The embedding request parameters
88
+ # @return [Object] The embedding response from OpenAI
89
+ def api_embed_execute(parameters)
90
+ client.embeddings.create(**parameters).as_json.deep_symbolize_keys
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "requests/_types"
4
+
5
+ require_relative "request"
6
+ require_relative "options"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ module OpenRouter
11
+ # ActiveModel type for casting and serializing OpenRouter requests
12
+ #
13
+ # Handles conversion between hashes, Request objects, and serialized
14
+ # request hashes for the OpenRouter API.
15
+ #
16
+ # @example Type casting
17
+ # type = RequestType.new
18
+ # request = type.cast({ model: "openai/gpt-4", messages: "Hello" })
19
+ # # => #<Request ...>
20
+ #
21
+ # @example Serialization
22
+ # serialized = type.serialize(request)
23
+ # # => { model: "openai/gpt-4", messages: [...] }
24
+ class RequestType < ActiveModel::Type::Value
25
+ # Casts value to Request object
26
+ #
27
+ # @param value [Request, Hash, nil]
28
+ # @return [Request, nil]
29
+ # @raise [ArgumentError] if value cannot be cast
30
+ def cast(value)
31
+ case value
32
+ when Request
33
+ value
34
+ when Hash
35
+ Request.new(**value.deep_symbolize_keys)
36
+ when nil
37
+ nil
38
+ else
39
+ raise ArgumentError, "Cannot cast #{value.class} to Request"
40
+ end
41
+ end
42
+
43
+ # Serializes Request to hash for API submission
44
+ #
45
+ # @param value [Request, Hash, nil]
46
+ # @return [Hash, nil]
47
+ # @raise [ArgumentError] if value cannot be serialized
48
+ def serialize(value)
49
+ case value
50
+ when Request
51
+ value.serialize
52
+ when Hash
53
+ value
54
+ when nil
55
+ nil
56
+ else
57
+ raise ArgumentError, "Cannot serialize #{value.class}"
58
+ end
59
+ end
60
+
61
+ # Deserializes value from storage
62
+ #
63
+ # @param value [Object]
64
+ # @return [Request, nil]
65
+ def deserialize(value)
66
+ cast(value)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../open_ai/options"
4
+ require_relative "requests/response_format"
5
+ require_relative "requests/prediction"
6
+ require_relative "requests/provider_preferences"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ module OpenRouter
11
+ # Configuration options for OpenRouter provider
12
+ #
13
+ # Extends OpenAI::Options with OpenRouter-specific settings including
14
+ # HTTP-Referer and X-Title headers for app identification and ranking.
15
+ #
16
+ # @example Basic configuration
17
+ # options = Options.new(
18
+ # api_key: 'sk-or-v1-...',
19
+ # app_name: 'MyApp',
20
+ # site_url: 'https://myapp.com'
21
+ # )
22
+ #
23
+ # @example Rails auto-configuration
24
+ # # Automatically resolves app_name from Rails.application
25
+ # # and site_url from routes.default_url_options
26
+ # options = Options.new(api_key: ENV['OPENROUTER_API_KEY'])
27
+ #
28
+ # @see https://openrouter.ai/docs/api-keys OpenRouter API Keys
29
+ # @see https://openrouter.ai/docs/rankings OpenRouter App Rankings
30
+ class Options < ActiveAgent::Providers::OpenAI::Options
31
+ # @!attribute base_url
32
+ # @return [String] API endpoint (default: "https://openrouter.ai/api/v1")
33
+ attribute :base_url, :string, as: "https://openrouter.ai/api/v1"
34
+
35
+ # @!attribute app_name
36
+ # @return [String] application name for X-Title header (default: "ActiveAgent" or Rails app name)
37
+ attribute :app_name, :string, fallback: "ActiveAgent"
38
+
39
+ # @!attribute site_url
40
+ # @return [String] site URL for HTTP-Referer header (default: "https://activeagents.ai/" or Rails URL)
41
+ attribute :site_url, :string, fallback: "https://activeagents.ai/"
42
+
43
+ # Creates new OpenRouter options with auto-resolution
44
+ #
45
+ # Automatically resolves app_name from Rails application name and
46
+ # site_url from Rails routes/ActionMailer default_url_options.
47
+ #
48
+ # @param kwargs [Hash] configuration options
49
+ # @option kwargs [String] :api_key OpenRouter API key
50
+ # @option kwargs [String] :app_name application name for rankings
51
+ # @option kwargs [String] :site_url site URL for rankings
52
+ # @return [Options]
53
+ def initialize(kwargs = {})
54
+ kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
55
+
56
+ super(**deep_compact(kwargs.merge(
57
+ app_name: kwargs[:app_name] || resolve_app_name(kwargs),
58
+ site_url: kwargs[:site_url] || resolve_site_url(kwargs),
59
+ )))
60
+ end
61
+
62
+ # Serializes options for API requests
63
+ #
64
+ # Excludes app_name and site_url as they're sent via headers.
65
+ #
66
+ # @return [Hash] serialized options
67
+ def serialize
68
+ super.except(:app_name, :site_url)
69
+ end
70
+
71
+ # Returns extra headers for OpenRouter API
72
+ #
73
+ # Maps app_name and site_url to OpenRouter's required headers:
74
+ # - HTTP-Referer: site_url
75
+ # - X-Title: app_name
76
+ #
77
+ # @return [Hash] headers hash
78
+ def extra_headers
79
+ deep_compact(
80
+ "http-referer" => site_url.presence,
81
+ "x-title" => app_name.presence
82
+ )
83
+ end
84
+
85
+ private
86
+
87
+ def resolve_api_key(kwargs)
88
+ kwargs["api_key"] ||
89
+ ENV["OPENROUTER_API_KEY"] ||
90
+ ENV["OPEN_ROUTER_API_KEY"] ||
91
+ ENV["OPENROUTER_ACCESS_TOKEN"] ||
92
+ ENV["OPEN_ROUTER_ACCESS_TOKEN"]
93
+ end
94
+
95
+ # Not Used as Part of Open Router
96
+ def resolve_organization_id(kwargs) = nil
97
+ def resolve_project_id(kwargs) = nil
98
+
99
+ def resolve_app_name(kwargs)
100
+ if defined?(Rails) && Rails.application
101
+ Rails.application.class.name.split("::").first
102
+ end
103
+ end
104
+
105
+ def resolve_site_url(kwargs)
106
+ # First check ActiveAgent kwargs
107
+ return kwargs[:default_url_options][:host] if kwargs.dig(:default_url_options, :host)
108
+
109
+ # Then check Rails routes default_url_options
110
+ if defined?(Rails) && Rails.application&.routes&.default_url_options&.any?
111
+ host = Rails.application.routes.default_url_options[:host]
112
+ port = Rails.application.routes.default_url_options[:port]
113
+ protocol = Rails.application.routes.default_url_options[:protocol] || "https"
114
+
115
+ if host
116
+ url = "#{protocol}://#{host}"
117
+ url += ":#{port}" if port && port != 80 && port != 443
118
+ return url
119
+ end
120
+ end
121
+
122
+ # Finally check ActionMailer options as fallback
123
+ if defined?(Rails) && Rails.application&.config&.action_mailer&.default_url_options
124
+ options = Rails.application.config.action_mailer.default_url_options
125
+ host = options[:host]
126
+ port = options[:port]
127
+ protocol = options[:protocol] || "https"
128
+
129
+ if host
130
+ url = "#{protocol}://#{host}"
131
+ url += ":#{port}" if port && port != 80 && port != 443
132
+ return url
133
+ end
134
+ end
135
+
136
+ nil
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end