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,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