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,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/open_ai/chat/requests/messages/content/files/details"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenRouter
|
|
8
|
+
module Requests
|
|
9
|
+
module Messages
|
|
10
|
+
module Content
|
|
11
|
+
module Files
|
|
12
|
+
# File details for OpenRouter file attachments
|
|
13
|
+
#
|
|
14
|
+
# Represents the nested file object within a file content part.
|
|
15
|
+
# Unlike OpenAI which strips the data URI prefix (e.g., data:application/pdf;base64,),
|
|
16
|
+
# OpenRouter requires it to be present in the file_data field.
|
|
17
|
+
#
|
|
18
|
+
# @example With data URI
|
|
19
|
+
# details = Details.new(
|
|
20
|
+
# file_data: 'data:application/pdf;base64,JVBERi0xLjQK...',
|
|
21
|
+
# filename: 'report.pdf'
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @see Content::File
|
|
25
|
+
class Details < OpenAI::Chat::Requests::Messages::Content::Files::Details
|
|
26
|
+
# @!attribute file_data
|
|
27
|
+
# @return [String] file data with data URI prefix intact
|
|
28
|
+
# Format: "data:<mime-type>;base64,<base64-data>"
|
|
29
|
+
attribute :file_data, :string
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
module Requests
|
|
7
|
+
# Plugin configuration for OpenRouter requests
|
|
8
|
+
#
|
|
9
|
+
# OpenRouter supports plugins that enhance model capabilities.
|
|
10
|
+
# Currently supports the file-parser plugin for processing PDF documents.
|
|
11
|
+
#
|
|
12
|
+
# @example File parser plugin with PDF text extraction
|
|
13
|
+
# plugin = Plugin.new(
|
|
14
|
+
# id: 'file-parser',
|
|
15
|
+
# pdf: { engine: 'pdf-text' }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @example File parser plugin with OCR
|
|
19
|
+
# plugin = Plugin.new(
|
|
20
|
+
# id: 'file-parser',
|
|
21
|
+
# pdf: { engine: 'mistral-ocr' }
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @see https://openrouter.ai/docs/plugins OpenRouter Plugins
|
|
25
|
+
# @see Plugins::PdfConfig
|
|
26
|
+
class Plugin < Common::BaseModel
|
|
27
|
+
# @!attribute id
|
|
28
|
+
# @return [String] plugin identifier (currently only 'file-parser' is supported)
|
|
29
|
+
attribute :id, :string
|
|
30
|
+
|
|
31
|
+
# @!attribute pdf
|
|
32
|
+
# @return [Plugins::PdfConfig, nil] PDF processing configuration
|
|
33
|
+
attribute :pdf, Plugins::PdfConfigType.new
|
|
34
|
+
|
|
35
|
+
validates :id, presence: true
|
|
36
|
+
validates :id, inclusion: { in: %w[file-parser] }, allow_nil: false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pdf_config"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenRouter
|
|
8
|
+
module Requests
|
|
9
|
+
module Plugins
|
|
10
|
+
# Type for PdfConfig
|
|
11
|
+
class PdfConfigType < ActiveModel::Type::Value
|
|
12
|
+
def cast(value)
|
|
13
|
+
case value
|
|
14
|
+
when PdfConfig
|
|
15
|
+
value
|
|
16
|
+
when Hash
|
|
17
|
+
PdfConfig.new(**value.deep_symbolize_keys)
|
|
18
|
+
when nil
|
|
19
|
+
nil
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "Cannot cast #{value.class} to PdfConfig"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def serialize(value)
|
|
26
|
+
case value
|
|
27
|
+
when PdfConfig
|
|
28
|
+
value.serialize
|
|
29
|
+
when Hash
|
|
30
|
+
value
|
|
31
|
+
when nil
|
|
32
|
+
nil
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deserialize(value)
|
|
39
|
+
cast(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
module Requests
|
|
7
|
+
module Plugins
|
|
8
|
+
# PDF processing configuration for file-parser plugin
|
|
9
|
+
#
|
|
10
|
+
# OpenRouter provides multiple PDF processing engines with different
|
|
11
|
+
# capabilities and costs:
|
|
12
|
+
#
|
|
13
|
+
# - **mistral-ocr**: Best for scanned documents or PDFs with images
|
|
14
|
+
# - Cost: $2 per 1,000 pages
|
|
15
|
+
# - Use when: Document is image-heavy or has poor text extraction
|
|
16
|
+
#
|
|
17
|
+
# - **pdf-text**: Best for well-structured PDFs with clear text content
|
|
18
|
+
# - Cost: Free
|
|
19
|
+
# - Use when: Document has clean, extractable text
|
|
20
|
+
#
|
|
21
|
+
# - **native**: Use model's native file processing capabilities
|
|
22
|
+
# - Cost: Charged as input tokens
|
|
23
|
+
# - Use when: Model supports native file input
|
|
24
|
+
#
|
|
25
|
+
# If no engine is specified, OpenRouter defaults to the model's native
|
|
26
|
+
# file processing if available, otherwise uses mistral-ocr.
|
|
27
|
+
#
|
|
28
|
+
# @example Text extraction (free)
|
|
29
|
+
# pdf_config = PdfConfig.new(engine: 'pdf-text')
|
|
30
|
+
#
|
|
31
|
+
# @example OCR for scanned documents
|
|
32
|
+
# pdf_config = PdfConfig.new(engine: 'mistral-ocr')
|
|
33
|
+
#
|
|
34
|
+
# @example Use model's native processing
|
|
35
|
+
# pdf_config = PdfConfig.new(engine: 'native')
|
|
36
|
+
#
|
|
37
|
+
# @see https://openrouter.ai/docs/plugins#file-parser OpenRouter File Parser Plugin
|
|
38
|
+
# @see Plugin
|
|
39
|
+
class PdfConfig < Common::BaseModel
|
|
40
|
+
# @!attribute engine
|
|
41
|
+
# @return [String, nil] PDF processing engine
|
|
42
|
+
# Options: 'mistral-ocr', 'pdf-text', 'native'
|
|
43
|
+
attribute :engine, :string
|
|
44
|
+
|
|
45
|
+
validates :engine, inclusion: { in: %w[mistral-ocr pdf-text native] }, allow_nil: true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
module Requests
|
|
7
|
+
# Prediction configuration for prefilling responses
|
|
8
|
+
#
|
|
9
|
+
# Allows prefilling the start of the model's response. When provided,
|
|
10
|
+
# the model continues from this predicted content.
|
|
11
|
+
#
|
|
12
|
+
# @example Content prediction
|
|
13
|
+
# prediction = Prediction.new(
|
|
14
|
+
# type: 'content',
|
|
15
|
+
# content: 'Once upon a time'
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @see https://platform.openai.com/docs/api-reference/chat/create#chat-create-prediction
|
|
19
|
+
class Prediction < Common::BaseModel
|
|
20
|
+
# @!attribute type
|
|
21
|
+
# @return [String] prediction type (currently only 'content' is supported)
|
|
22
|
+
attribute :type, :string
|
|
23
|
+
|
|
24
|
+
# @!attribute content
|
|
25
|
+
# @return [String] predicted content to prefill the response
|
|
26
|
+
attribute :content, :string
|
|
27
|
+
|
|
28
|
+
validates :type, inclusion: { in: %w[content] }, allow_nil: true
|
|
29
|
+
validates :content, presence: true, if: -> { type.present? }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "max_price"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenRouter
|
|
8
|
+
module Requests
|
|
9
|
+
# Type for MaxPrice
|
|
10
|
+
class MaxPriceType < ActiveModel::Type::Value
|
|
11
|
+
def cast(value)
|
|
12
|
+
case value
|
|
13
|
+
when MaxPrice
|
|
14
|
+
value
|
|
15
|
+
when Hash
|
|
16
|
+
MaxPrice.new(**value.deep_symbolize_keys)
|
|
17
|
+
when nil
|
|
18
|
+
nil
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "Cannot cast #{value.class} to MaxPrice"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serialize(value)
|
|
25
|
+
case value
|
|
26
|
+
when MaxPrice
|
|
27
|
+
value.serialize
|
|
28
|
+
when Hash
|
|
29
|
+
value
|
|
30
|
+
when nil
|
|
31
|
+
nil
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deserialize(value)
|
|
38
|
+
cast(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
module Requests
|
|
7
|
+
# Maximum price configuration for provider routing
|
|
8
|
+
#
|
|
9
|
+
# Specifies maximum acceptable prices (in USD per million tokens or per
|
|
10
|
+
# operation) for filtering providers. OpenRouter will only route to
|
|
11
|
+
# providers within these price constraints.
|
|
12
|
+
#
|
|
13
|
+
# @example Setting prompt and completion limits
|
|
14
|
+
# max_price = MaxPrice.new(
|
|
15
|
+
# prompt: 0.01, # $0.01 per million input tokens
|
|
16
|
+
# completion: 0.03 # $0.03 per million output tokens
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example Setting all constraints
|
|
20
|
+
# max_price = MaxPrice.new(
|
|
21
|
+
# prompt: 0.01,
|
|
22
|
+
# completion: 0.03,
|
|
23
|
+
# image: 0.001,
|
|
24
|
+
# audio: 0.002,
|
|
25
|
+
# request: 0.0001
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @see https://openrouter.ai/docs/provider-routing OpenRouter Provider Routing
|
|
29
|
+
# @see ProviderPreferences
|
|
30
|
+
class MaxPrice < Common::BaseModel
|
|
31
|
+
# @!attribute prompt
|
|
32
|
+
# @return [Float, nil] maximum price per million prompt tokens (input)
|
|
33
|
+
attribute :prompt, :float
|
|
34
|
+
|
|
35
|
+
# @!attribute completion
|
|
36
|
+
# @return [Float, nil] maximum price per million completion tokens (output)
|
|
37
|
+
attribute :completion, :float
|
|
38
|
+
|
|
39
|
+
# @!attribute image
|
|
40
|
+
# @return [Float, nil] maximum price per image operation
|
|
41
|
+
attribute :image, :float
|
|
42
|
+
|
|
43
|
+
# @!attribute audio
|
|
44
|
+
# @return [Float, nil] maximum price per audio operation
|
|
45
|
+
attribute :audio, :float
|
|
46
|
+
|
|
47
|
+
# @!attribute request
|
|
48
|
+
# @return [Float, nil] maximum price per request
|
|
49
|
+
attribute :request, :float
|
|
50
|
+
|
|
51
|
+
validates :prompt, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
52
|
+
validates :completion, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
53
|
+
validates :image, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
54
|
+
validates :audio, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
55
|
+
validates :request, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
56
|
+
|
|
57
|
+
# Backwards Compatibility
|
|
58
|
+
alias_attribute :prompt_tokens, :prompt
|
|
59
|
+
alias_attribute :completion_tokens, :completion
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "provider_preferences/_types"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenRouter
|
|
8
|
+
module Requests
|
|
9
|
+
# Provider preferences for routing requests to specific providers
|
|
10
|
+
#
|
|
11
|
+
# Controls how OpenRouter selects and routes requests to underlying model
|
|
12
|
+
# providers. Enables filtering by parameters, cost constraints, privacy
|
|
13
|
+
# settings, and provider-specific preferences.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic provider routing
|
|
16
|
+
# prefs = ProviderPreferences.new(
|
|
17
|
+
# require_parameters: true,
|
|
18
|
+
# allow_fallbacks: false
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Privacy-focused routing
|
|
22
|
+
# prefs = ProviderPreferences.new(
|
|
23
|
+
# data_collection: 'deny',
|
|
24
|
+
# zdr: true
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @example Cost-optimized routing
|
|
28
|
+
# prefs = ProviderPreferences.new(
|
|
29
|
+
# sort: 'price',
|
|
30
|
+
# max_price: { prompt: 0.01, completion: 0.03 }
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# @example Provider ordering
|
|
34
|
+
# prefs = ProviderPreferences.new(
|
|
35
|
+
# order: ['OpenAI', 'Anthropic'],
|
|
36
|
+
# ignore: ['Together']
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# @see https://openrouter.ai/docs/provider-routing OpenRouter Provider Routing
|
|
40
|
+
# @see MaxPrice
|
|
41
|
+
class ProviderPreferences < Common::BaseModel
|
|
42
|
+
# @!attribute allow_fallbacks
|
|
43
|
+
# @return [Boolean, nil] whether to allow backup providers when primary is unavailable
|
|
44
|
+
# - true: (default) use next best provider when primary unavailable
|
|
45
|
+
# - false: only use primary/custom provider, return error if unavailable
|
|
46
|
+
attribute :allow_fallbacks, :boolean
|
|
47
|
+
|
|
48
|
+
# @!attribute require_parameters
|
|
49
|
+
# @return [Boolean, nil] whether to filter to providers supporting all parameters
|
|
50
|
+
# - true: only use providers that support all provided parameters
|
|
51
|
+
# - false: providers receive only the parameters they support
|
|
52
|
+
attribute :require_parameters, :boolean
|
|
53
|
+
|
|
54
|
+
# @!attribute data_collection
|
|
55
|
+
# @return [String, nil] data collection preference
|
|
56
|
+
# - 'allow': (default) allow providers which store/train on user data
|
|
57
|
+
# - 'deny': only use providers that don't collect user data
|
|
58
|
+
attribute :data_collection, :string
|
|
59
|
+
|
|
60
|
+
# @!attribute zdr
|
|
61
|
+
# @return [Boolean, nil] zero data retention mode (stricter privacy)
|
|
62
|
+
attribute :zdr, :boolean
|
|
63
|
+
|
|
64
|
+
# @!attribute order
|
|
65
|
+
# @return [Array<String>] ordered list of provider slugs to try in sequence
|
|
66
|
+
attribute :order, default: -> { [] }
|
|
67
|
+
|
|
68
|
+
# @!attribute only
|
|
69
|
+
# @return [Array<String>] allowlist of provider slugs (merged with account settings)
|
|
70
|
+
attribute :only, default: -> { [] }
|
|
71
|
+
|
|
72
|
+
# @!attribute ignore
|
|
73
|
+
# @return [Array<String>] blocklist of provider slugs (merged with account settings)
|
|
74
|
+
attribute :ignore, default: -> { [] }
|
|
75
|
+
|
|
76
|
+
# @!attribute quantizations
|
|
77
|
+
# @return [Array<String>] quantization levels to filter providers by
|
|
78
|
+
# Options: int4, int8, fp4, fp6, fp8, fp16, bf16, fp32, unknown
|
|
79
|
+
attribute :quantizations, default: -> { [] }
|
|
80
|
+
|
|
81
|
+
# @!attribute sort
|
|
82
|
+
# @return [String, nil] sorting strategy when order not specified
|
|
83
|
+
# Options: price, throughput, latency
|
|
84
|
+
# Note: disables load balancing when set
|
|
85
|
+
attribute :sort, :string
|
|
86
|
+
|
|
87
|
+
# @!attribute max_price
|
|
88
|
+
# @return [MaxPrice, nil] maximum price constraints per token/operation
|
|
89
|
+
attribute :max_price, MaxPriceType.new
|
|
90
|
+
|
|
91
|
+
# Validations matching the schema
|
|
92
|
+
validates :data_collection, inclusion: { in: %w[deny allow] }, allow_nil: true
|
|
93
|
+
validates :sort, inclusion: { in: %w[price throughput latency] }, allow_nil: true
|
|
94
|
+
validates :quantizations, inclusion: {
|
|
95
|
+
in: [ %w[int4 int8 fp4 fp6 fp8 fp16 bf16 fp32 unknown].freeze ],
|
|
96
|
+
message: "must contain valid quantization levels"
|
|
97
|
+
}, allow_nil: true, if: -> { quantizations.is_a?(Array) && quantizations.any? }
|
|
98
|
+
|
|
99
|
+
# Backwards Compatibility
|
|
100
|
+
alias_attribute :enable_fallbacks, :allow_fallbacks
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
module Requests
|
|
7
|
+
# Response format configuration for structured output
|
|
8
|
+
#
|
|
9
|
+
# Enables JSON-formatted responses using OpenAI's structured output format.
|
|
10
|
+
# When using structured output, OpenRouter automatically sets
|
|
11
|
+
# provider.require_parameters=true to route to compatible models.
|
|
12
|
+
#
|
|
13
|
+
# @example JSON object format
|
|
14
|
+
# format = ResponseFormat.new(type: 'json_object')
|
|
15
|
+
#
|
|
16
|
+
# @example JSON schema format
|
|
17
|
+
# format = ResponseFormat.new(
|
|
18
|
+
# type: 'json_schema',
|
|
19
|
+
# json_schema: {
|
|
20
|
+
# name: 'user_profile',
|
|
21
|
+
# description: 'A user profile',
|
|
22
|
+
# schema: { type: 'object', properties: { name: { type: 'string' } } },
|
|
23
|
+
# strict: true
|
|
24
|
+
# }
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @see https://openrouter.ai/docs/structured-outputs OpenRouter Structured Outputs
|
|
28
|
+
# @see https://platform.openai.com/docs/guides/structured-outputs OpenAI Structured Outputs
|
|
29
|
+
class ResponseFormat < Common::BaseModel
|
|
30
|
+
# @!attribute type
|
|
31
|
+
# @return [String] response format type ('json_object' or 'json_schema')
|
|
32
|
+
attribute :type, :string
|
|
33
|
+
|
|
34
|
+
# @!attribute json_schema
|
|
35
|
+
# @return [Hash, nil] JSON schema configuration (required when type is 'json_schema')
|
|
36
|
+
# @option json_schema [String] :name schema name (max 64 chars, alphanumeric/underscore/dash)
|
|
37
|
+
# @option json_schema [String] :description schema description
|
|
38
|
+
# @option json_schema [Hash] :schema JSON schema definition
|
|
39
|
+
# @option json_schema [Boolean] :strict whether to enforce strict schema adherence
|
|
40
|
+
attribute :json_schema # Hash with name, description, schema, strict
|
|
41
|
+
|
|
42
|
+
validates :type, inclusion: { in: %w[json_object json_schema] }, allow_nil: true
|
|
43
|
+
|
|
44
|
+
# Validate that json_schema is present when type is json_schema
|
|
45
|
+
validate :validate_json_schema_presence
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_json_schema_presence
|
|
50
|
+
if type == "json_schema" && json_schema.blank?
|
|
51
|
+
errors.add(:json_schema, "must be present when type is 'json_schema'")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if json_schema.present? && json_schema.is_a?(Hash)
|
|
55
|
+
validate_json_schema_structure
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_json_schema_structure
|
|
60
|
+
unless json_schema[:name].present?
|
|
61
|
+
errors.add(:json_schema, "must include 'name' field")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if json_schema[:name].present? && json_schema[:name].length > 64
|
|
65
|
+
errors.add(:json_schema, "name must be 64 characters or less")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Name must match pattern: a-z, A-Z, 0-9, underscores and dashes
|
|
69
|
+
if json_schema[:name].present? && json_schema[:name] !~ /^[a-zA-Z0-9_-]+$/
|
|
70
|
+
errors.add(:json_schema, "name must contain only a-z, A-Z, 0-9, underscores and dashes")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/keys"
|
|
4
|
+
require_relative "../open_ai/chat/transforms"
|
|
5
|
+
|
|
6
|
+
module ActiveAgent
|
|
7
|
+
module Providers
|
|
8
|
+
module OpenRouter
|
|
9
|
+
# Provides transformation methods for normalizing OpenRouter parameters
|
|
10
|
+
# to work with OpenAI gem's native format plus OpenRouter extensions
|
|
11
|
+
#
|
|
12
|
+
# Leverages OpenAI::Chat::Transforms for base message normalization while
|
|
13
|
+
# adding handling for OpenRouter-specific parameters like plugins, provider
|
|
14
|
+
# preferences, and model fallbacks.
|
|
15
|
+
module Transforms
|
|
16
|
+
class << self
|
|
17
|
+
# Converts gem model object to hash via JSON round-trip
|
|
18
|
+
#
|
|
19
|
+
# @param gem_object [Object]
|
|
20
|
+
# @return [Hash] with symbolized keys
|
|
21
|
+
def gem_to_hash(gem_object)
|
|
22
|
+
OpenAI::Chat::Transforms.gem_to_hash(gem_object)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Normalizes all request parameters for OpenRouter API
|
|
26
|
+
#
|
|
27
|
+
# Handles both OpenAI-compatible parameters and OpenRouter-specific extensions.
|
|
28
|
+
# OpenRouter-specific params (plugins, provider, transforms, models, route) are
|
|
29
|
+
# extracted and returned separately for manual serialization.
|
|
30
|
+
#
|
|
31
|
+
# @param params [Hash]
|
|
32
|
+
# @return [Array<Hash, Hash>] tuple of [openai_params, openrouter_params]
|
|
33
|
+
def normalize_params(params)
|
|
34
|
+
params = params.dup
|
|
35
|
+
|
|
36
|
+
# Extract OpenRouter-specific parameters
|
|
37
|
+
openrouter_params = {}
|
|
38
|
+
openrouter_params[:plugins] = params.delete(:plugins) if params.key?(:plugins)
|
|
39
|
+
openrouter_params[:provider] = params.delete(:provider) if params.key?(:provider)
|
|
40
|
+
openrouter_params[:transforms] = params.delete(:transforms) if params.key?(:transforms)
|
|
41
|
+
openrouter_params[:models] = params.delete(:models) if params.key?(:models)
|
|
42
|
+
openrouter_params[:route] = params.delete(:route) if params.key?(:route)
|
|
43
|
+
|
|
44
|
+
# Extract OpenRouter-specific sampling parameters not in OpenAI
|
|
45
|
+
openrouter_params[:top_k] = params.delete(:top_k) if params.key?(:top_k)
|
|
46
|
+
openrouter_params[:min_p] = params.delete(:min_p) if params.key?(:min_p)
|
|
47
|
+
openrouter_params[:top_a] = params.delete(:top_a) if params.key?(:top_a)
|
|
48
|
+
openrouter_params[:repetition_penalty] = params.delete(:repetition_penalty) if params.key?(:repetition_penalty)
|
|
49
|
+
|
|
50
|
+
# Handle response_format special logic for OpenRouter
|
|
51
|
+
# OpenRouter requires provider.require_parameters=true for structured output
|
|
52
|
+
if params[:response_format]
|
|
53
|
+
response_format = params[:response_format]
|
|
54
|
+
response_format_hash = response_format.is_a?(Hash) ? response_format : { type: response_format }
|
|
55
|
+
|
|
56
|
+
if %i[json_object json_schema].include?(response_format_hash[:type].to_sym)
|
|
57
|
+
openrouter_params[:provider] ||= {}
|
|
58
|
+
openrouter_params[:provider][:require_parameters] = true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Use OpenAI transforms for the base parameters
|
|
63
|
+
openai_params = OpenAI::Chat::Transforms.normalize_params(params)
|
|
64
|
+
|
|
65
|
+
[ openai_params, openrouter_params ]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Normalizes messages using OpenAI transforms
|
|
69
|
+
#
|
|
70
|
+
# @param messages [Array, String, Hash, nil]
|
|
71
|
+
# @return [Array<OpenAI::Models::Chat::ChatCompletionMessageParam>, nil]
|
|
72
|
+
def normalize_messages(messages)
|
|
73
|
+
OpenAI::Chat::Transforms.normalize_messages(messages)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalizes response_format for OpenRouter
|
|
77
|
+
#
|
|
78
|
+
# Delegates to OpenAI transforms. The special handling for structured output
|
|
79
|
+
# (setting provider.require_parameters=true) is handled in normalize_params.
|
|
80
|
+
#
|
|
81
|
+
# @param format [Hash, Symbol, String]
|
|
82
|
+
# @return [Hash]
|
|
83
|
+
def normalize_response_format(format)
|
|
84
|
+
OpenAI::Chat::Transforms.normalize_response_format(format)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Cleans up serialized request for API submission
|
|
88
|
+
#
|
|
89
|
+
# Merges OpenAI-compatible params with OpenRouter-specific params.
|
|
90
|
+
#
|
|
91
|
+
# @param openai_hash [Hash] serialized OpenAI request
|
|
92
|
+
# @param openrouter_params [Hash] OpenRouter-specific parameters
|
|
93
|
+
# @param defaults [Hash] default values to remove
|
|
94
|
+
# @param gem_object [Object] original gem object
|
|
95
|
+
# @return [Hash] cleaned and merged request hash
|
|
96
|
+
def cleanup_serialized_request(openai_hash, openrouter_params, defaults, gem_object)
|
|
97
|
+
# Start with OpenAI cleanup
|
|
98
|
+
cleaned = OpenAI::Chat::Transforms.cleanup_serialized_request(openai_hash, defaults, gem_object)
|
|
99
|
+
|
|
100
|
+
# Merge OpenRouter-specific params, but skip default values
|
|
101
|
+
openrouter_params.each do |key, value|
|
|
102
|
+
# Skip if value is nil, empty, or matches the default
|
|
103
|
+
next if value.nil?
|
|
104
|
+
next if value.respond_to?(:empty?) && value.empty?
|
|
105
|
+
next if defaults.key?(key) && defaults[key] == value
|
|
106
|
+
|
|
107
|
+
cleaned[key] = serialize_openrouter_param(key, value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
cleaned
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Serializes OpenRouter-specific parameters
|
|
114
|
+
#
|
|
115
|
+
# @param key [Symbol]
|
|
116
|
+
# @param value [Object]
|
|
117
|
+
# @return [Object] serialized value
|
|
118
|
+
def serialize_openrouter_param(key, value)
|
|
119
|
+
case key
|
|
120
|
+
when :provider
|
|
121
|
+
# Serialize provider preferences object
|
|
122
|
+
value.respond_to?(:serialize) ? value.serialize : value
|
|
123
|
+
when :plugins
|
|
124
|
+
# Serialize plugins array
|
|
125
|
+
value.respond_to?(:map) ? value.map { |p| p.respond_to?(:serialize) ? p.serialize : p } : value
|
|
126
|
+
else
|
|
127
|
+
value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|