activeagent 1.0.0.rc1 → 1.0.1

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/lib/active_agent/providers/_base_provider.rb +94 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  6. data/lib/active_agent/providers/anthropic/request.rb +157 -78
  7. data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
  8. data/lib/active_agent/providers/anthropic_provider.rb +159 -59
  9. data/lib/active_agent/providers/common/messages/_types.rb +46 -3
  10. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  11. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  12. data/lib/active_agent/providers/common/usage.rb +385 -0
  13. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  14. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  15. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  16. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  17. data/lib/active_agent/providers/mock_provider.rb +23 -23
  18. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  19. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  20. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  21. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  22. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  23. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  24. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  25. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  26. data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
  27. data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
  28. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  29. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  30. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  31. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  32. data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
  33. data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
  34. data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
  35. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  36. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  37. data/lib/active_agent/providers/open_router/options.rb +49 -1
  38. data/lib/active_agent/providers/open_router/request.rb +252 -66
  39. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  40. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  41. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  42. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  43. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  44. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  45. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  46. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  47. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  48. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  49. data/lib/active_agent/providers/open_router/transforms.rb +164 -0
  50. data/lib/active_agent/providers/open_router_provider.rb +23 -0
  51. data/lib/active_agent/version.rb +1 -1
  52. metadata +17 -160
  53. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  54. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  55. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  56. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  57. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  58. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  59. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  60. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  61. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  62. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  78. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  79. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  80. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  81. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  82. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  83. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  84. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  85. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  86. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  87. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  88. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  91. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  92. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  93. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  94. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  95. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  96. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  97. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  98. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  99. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  100. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  101. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  102. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  116. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  117. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  118. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  119. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  120. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  121. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  122. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  123. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  124. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  125. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  126. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  127. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  128. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  129. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  130. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  131. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  162. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  163. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  166. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  167. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  168. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  170. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  171. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  172. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  173. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  174. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  175. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  184. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  185. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  186. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  187. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  188. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  189. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  190. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  191. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Anthropic
8
+ # Transforms between convenient input formats and Anthropic API structures.
9
+ #
10
+ # Provides bidirectional transformations:
11
+ # - Expand: shortcuts → API format (string → content blocks, consecutive messages → grouped)
12
+ # - Compress: API format → shortcuts (single content blocks → strings for efficiency)
13
+ module Transforms
14
+ class << self
15
+ # Converts gem model object to hash via JSON round-trip.
16
+ #
17
+ # This ensures proper nested serialization and symbolic keys.
18
+ #
19
+ # @param gem_object [Object] any object responding to .to_json
20
+ # @return [Hash]
21
+ def gem_to_hash(gem_object)
22
+ JSON.parse(gem_object.to_json, symbolize_names: true)
23
+ end
24
+
25
+ # @param params [Hash]
26
+ # @return [Hash]
27
+ def normalize_params(params)
28
+ params = params.dup
29
+ params[:messages] = normalize_messages(params[:messages]) if params[:messages]
30
+ params[:system] = normalize_system(params[:system]) if params[:system]
31
+ params[:tools] = normalize_tools(params[:tools]) if params[:tools]
32
+ params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice]
33
+
34
+ # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format)
35
+ if params[:mcps]
36
+ params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps))
37
+ elsif params[:mcp_servers]
38
+ params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers])
39
+ end
40
+
41
+ params
42
+ end
43
+
44
+ # Normalizes tools from common format to Anthropic format.
45
+ #
46
+ # Accepts both `parameters` and `input_schema` keys, converting to Anthropic's `input_schema`.
47
+ #
48
+ # @param tools [Array<Hash>]
49
+ # @return [Array<Hash>]
50
+ def normalize_tools(tools)
51
+ return tools unless tools.is_a?(Array)
52
+
53
+ tools.map do |tool|
54
+ tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool
55
+
56
+ # If already in Anthropic format (has input_schema), return as-is
57
+ next tool_hash if tool_hash[:input_schema]
58
+
59
+ # Convert common format with 'parameters' to Anthropic format with 'input_schema'
60
+ if tool_hash[:parameters]
61
+ tool_hash = tool_hash.dup
62
+ tool_hash[:input_schema] = tool_hash.delete(:parameters)
63
+ end
64
+
65
+ tool_hash
66
+ end
67
+ end
68
+
69
+ # Normalizes MCP servers from common format to Anthropic format.
70
+ #
71
+ # Common format:
72
+ # {name: "stripe", url: "https://...", authorization: "token"}
73
+ # Anthropic format:
74
+ # {type: "url", name: "stripe", url: "https://...", authorization_token: "token"}
75
+ #
76
+ # @param mcp_servers [Array<Hash>]
77
+ # @return [Array<Hash>]
78
+ def normalize_mcp_servers(mcp_servers)
79
+ return mcp_servers unless mcp_servers.is_a?(Array)
80
+
81
+ mcp_servers.map do |server|
82
+ server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server
83
+
84
+ # If already in Anthropic native format (has type: "url"), return as-is
85
+ # Check for absence of common format 'authorization' field OR presence of native 'authorization_token'
86
+ if server_hash[:type] == "url" && (server_hash[:authorization_token] || !server_hash[:authorization])
87
+ next server_hash
88
+ end
89
+
90
+ # Convert common format to Anthropic format
91
+ result = {
92
+ type: "url",
93
+ name: server_hash[:name],
94
+ url: server_hash[:url]
95
+ }
96
+
97
+ # Map 'authorization' to 'authorization_token'
98
+ if server_hash[:authorization]
99
+ result[:authorization_token] = server_hash[:authorization]
100
+ elsif server_hash[:authorization_token]
101
+ result[:authorization_token] = server_hash[:authorization_token]
102
+ end
103
+
104
+ result.compact
105
+ end
106
+ end
107
+
108
+ # Normalizes tool_choice from common format to Anthropic gem model objects.
109
+ #
110
+ # The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto,
111
+ # ToolChoiceAny, ToolChoiceTool, etc.), not a plain hash.
112
+ #
113
+ # Maps:
114
+ # - "required" → ToolChoiceAny (force tool use)
115
+ # - "auto" → ToolChoiceAuto (let model decide)
116
+ # - { name: "..." } → ToolChoiceTool with name
117
+ #
118
+ # @param tool_choice [String, Hash, Object]
119
+ # @return [Object] Anthropic gem model object
120
+ def normalize_tool_choice(tool_choice)
121
+ # If already a gem model object, return as-is
122
+ return tool_choice if tool_choice.is_a?(::Anthropic::Models::ToolChoiceAuto) ||
123
+ tool_choice.is_a?(::Anthropic::Models::ToolChoiceAny) ||
124
+ tool_choice.is_a?(::Anthropic::Models::ToolChoiceTool) ||
125
+ tool_choice.is_a?(::Anthropic::Models::ToolChoiceNone)
126
+
127
+ case tool_choice
128
+ when "required"
129
+ # Create ToolChoiceAny model for forcing tool use
130
+ ::Anthropic::Models::ToolChoiceAny.new(type: :any)
131
+ when "auto"
132
+ # Create ToolChoiceAuto model for letting model decide
133
+ ::Anthropic::Models::ToolChoiceAuto.new(type: :auto)
134
+ when Hash
135
+ choice_hash = tool_choice.deep_symbolize_keys
136
+
137
+ # If has type field, create appropriate model
138
+ if choice_hash[:type]
139
+ case choice_hash[:type].to_sym
140
+ when :any
141
+ ::Anthropic::Models::ToolChoiceAny.new(**choice_hash)
142
+ when :auto
143
+ ::Anthropic::Models::ToolChoiceAuto.new(**choice_hash)
144
+ when :tool
145
+ ::Anthropic::Models::ToolChoiceTool.new(**choice_hash)
146
+ when :none
147
+ ::Anthropic::Models::ToolChoiceNone.new(**choice_hash)
148
+ else
149
+ choice_hash
150
+ end
151
+ # Convert { name: "..." } to ToolChoiceTool
152
+ elsif choice_hash[:name]
153
+ ::Anthropic::Models::ToolChoiceTool.new(type: :tool, name: choice_hash[:name])
154
+ else
155
+ choice_hash
156
+ end
157
+ else
158
+ tool_choice
159
+ end
160
+ end
161
+
162
+ # Merges consecutive same-role messages into single messages with multiple content blocks.
163
+ #
164
+ # Required by Anthropic API - consecutive messages with the same role must be combined.
165
+ #
166
+ # @param messages [Array<Hash>]
167
+ # @return [Array<Hash>]
168
+ def normalize_messages(messages)
169
+ return messages unless messages.is_a?(Array)
170
+
171
+ grouped = []
172
+
173
+ messages.each do |msg|
174
+ msg_hash = msg.is_a?(Hash) ? msg.deep_symbolize_keys : { role: :user, content: msg }
175
+
176
+ # Extract role
177
+ role = msg_hash[:role]&.to_sym || :user
178
+
179
+ # Determine content
180
+ if msg_hash.key?(:content)
181
+ # Has explicit content key
182
+ content = normalize_content(msg_hash[:content])
183
+ elsif msg_hash.key?(:role) && msg_hash.keys.size > 1
184
+ # Has role + other keys (e.g., {role: "assistant", text: "..."})
185
+ # Treat everything except :role as content
186
+ content = normalize_content(msg_hash.except(:role))
187
+ elsif !msg_hash.key?(:role)
188
+ # No role or content - treat entire hash as content
189
+ content = normalize_content(msg_hash)
190
+ else
191
+ # Only has role, no content
192
+ content = []
193
+ end
194
+
195
+ if grouped.empty? || grouped.last[:role] != role
196
+ grouped << { role: role, content: content }
197
+ else
198
+ # Merge content from consecutive same-role messages
199
+ grouped.last[:content] += content
200
+ end
201
+ end
202
+
203
+ grouped
204
+ end
205
+
206
+ # Converts system content shortcuts to API format.
207
+ #
208
+ # Handles string, hash, or array inputs. Strings pass through unchanged
209
+ # since Anthropic accepts both string and structured formats.
210
+ #
211
+ # @param system [String, Array, Hash]
212
+ # @return [String, Array]
213
+ def normalize_system(system)
214
+ case system
215
+ when String
216
+ # Keep strings as-is - Anthropic accepts both string and array
217
+ system
218
+ when Array
219
+ # Normalize array of system blocks
220
+ system.map { |block| normalize_system_block(block) }
221
+ when Hash
222
+ # Single hash becomes array with one block
223
+ [ normalize_system_block(system) ]
224
+ else
225
+ system
226
+ end
227
+ end
228
+
229
+ # @param block [String, Hash]
230
+ # @return [Hash]
231
+ def normalize_system_block(block)
232
+ case block
233
+ when String
234
+ { type: "text", text: block }
235
+ when Hash
236
+ hash = block.deep_symbolize_keys
237
+ # Add type if missing and can be inferred
238
+ hash[:type] ||= "text" if hash[:text]
239
+ hash
240
+ else
241
+ block
242
+ end
243
+ end
244
+
245
+ # Expands content shortcuts into structured content block arrays.
246
+ #
247
+ # Handles multiple input formats:
248
+ # - String → `[{type: "text", text: "..."}]`
249
+ # - Hash with multiple keys → separate blocks per content type
250
+ # - Array → normalized items
251
+ #
252
+ # @param content [String, Array, Hash]
253
+ # @return [Array<Hash>]
254
+ def normalize_content(content)
255
+ case content
256
+ when String
257
+ # String → array with single text block
258
+ [ { type: "text", text: content } ]
259
+ when Array
260
+ # Normalize each item in the array
261
+ content.flat_map { |item| normalize_content_item(item) }
262
+ when Hash
263
+ # Check if hash has multiple content keys (text, image, document)
264
+ # If so, expand into separate content blocks
265
+ hash = content.deep_symbolize_keys
266
+ content_keys = [ :text, :image, :document ]
267
+ found_keys = content_keys & hash.keys
268
+
269
+ if found_keys.size > 1
270
+ # Multiple content types - expand into array
271
+ found_keys.flat_map { |key| normalize_content_item({ key => hash[key] }) }
272
+ else
273
+ # Single content item
274
+ [ normalize_content_item(content) ]
275
+ end
276
+ when nil
277
+ []
278
+ else
279
+ # Pass through other types (might be gem objects already)
280
+ [ content ]
281
+ end
282
+ end
283
+
284
+ # Infers content block type from hash keys or converts string to text block.
285
+ #
286
+ # @param item [String, Hash]
287
+ # @return [Hash]
288
+ def normalize_content_item(item)
289
+ case item
290
+ when String
291
+ { type: "text", text: item }
292
+ when Hash
293
+ hash = item.deep_symbolize_keys
294
+
295
+ # If type is specified, return as-is
296
+ return hash if hash[:type]
297
+
298
+ # Type inference based on keys
299
+ if hash[:text]
300
+ { type: "text" }.merge(hash)
301
+ elsif hash[:image]
302
+ # Normalize image source format
303
+ source = normalize_source(hash[:image])
304
+ { type: "image", source: source }.merge(hash.except(:image))
305
+ elsif hash[:document]
306
+ # Normalize document source format
307
+ source = normalize_source(hash[:document])
308
+ { type: "document", source: source }.merge(hash.except(:document))
309
+ elsif hash[:tool_use_id]
310
+ # Tool result content
311
+ { type: "tool_result" }.merge(hash)
312
+ elsif hash[:id] && hash[:name] && hash[:input]
313
+ # Tool use content
314
+ { type: "tool_use" }.merge(hash)
315
+ else
316
+ # Unknown format - return as-is and let gem validate
317
+ hash
318
+ end
319
+ else
320
+ # Pass through (might be gem object)
321
+ item
322
+ end
323
+ end
324
+
325
+ # Converts image/document source shortcuts to API structure.
326
+ #
327
+ # Handles multiple formats:
328
+ # - Regular URL → `{type: "url", url: "..."}`
329
+ # - Data URI → `{type: "base64", media_type: "...", data: "..."}`
330
+ # - Hash with base64 → `{type: "base64", media_type: "...", data: "..."}`
331
+ #
332
+ # @param source [String, Hash]
333
+ # @return [Hash]
334
+ def normalize_source(source)
335
+ case source
336
+ when String
337
+ # Check if it's a data URI (e.g., "data:image/png;base64,...")
338
+ if source.start_with?("data:")
339
+ parse_data_uri(source)
340
+ else
341
+ # Regular URL → wrap in url source type
342
+ { type: "url", url: source }
343
+ end
344
+ when Hash
345
+ hash = source.deep_symbolize_keys
346
+ # Already has type → return as-is
347
+ return hash if hash[:type]
348
+
349
+ # Has base64 data → add type
350
+ if hash[:data] && hash[:media_type]
351
+ { type: "base64" }.merge(hash)
352
+ else
353
+ # Unknown format → return as-is
354
+ hash
355
+ end
356
+ else
357
+ source
358
+ end
359
+ end
360
+
361
+ # Extracts media type and data from data URI.
362
+ #
363
+ # Expected format: `data:[<media type>][;base64],<data>`
364
+ #
365
+ # @param data_uri [String] e.g., "data:image/png;base64,iVBORw0..."
366
+ # @return [Hash] `{type: "base64", media_type: "...", data: "..."}`
367
+ def parse_data_uri(data_uri)
368
+ # Extract media type and data from data URI
369
+ # Format: data:[<media type>][;base64],<data>
370
+ match = data_uri.match(%r{\Adata:([^;,]+)(?:;base64)?,(.+)\z})
371
+
372
+ if match
373
+ {
374
+ type: "base64",
375
+ media_type: match[1],
376
+ data: match[2]
377
+ }
378
+ else
379
+ # Invalid data URI - return as URL fallback
380
+ { type: "url", url: data_uri }
381
+ end
382
+ end
383
+
384
+ # Converts single-element content arrays back to string shorthand.
385
+ #
386
+ # Reduces payload size by reversing the expansion done by normalize methods.
387
+ #
388
+ # @param hash [Hash]
389
+ # @return [Hash]
390
+ def compress_content(hash)
391
+ return hash unless hash.is_a?(Hash)
392
+
393
+ # Compress message content
394
+ hash[:messages]&.each do |msg|
395
+ compress_message_content!(msg)
396
+ end
397
+
398
+ # Compress system content
399
+ if hash[:system].is_a?(Array)
400
+ hash[:system] = compress_system_content(hash[:system])
401
+ end
402
+
403
+ hash
404
+ end
405
+
406
+ # Converts single text block arrays to string shorthand.
407
+ #
408
+ # `[{type: "text", text: "hello"}]` → `"hello"`
409
+ #
410
+ # @param msg [Hash] message with :content key
411
+ # @return [void]
412
+ def compress_message_content!(msg)
413
+ content = msg[:content]
414
+ return unless content.is_a?(Array)
415
+
416
+ # Single text block → string shorthand
417
+ if content.one? && content.first.is_a?(Hash) && content.first[:type] == "text"
418
+ msg[:content] = content.first[:text]
419
+ end
420
+ end
421
+
422
+ # Cleans up serialized request for API submission.
423
+ #
424
+ # Removes response-only fields, applies content compression,
425
+ # removes provider-internal fields, and removes default values.
426
+ # Note: max_tokens is kept even if it matches default as Anthropic API requires it.
427
+ #
428
+ # @param hash [Hash] serialized request
429
+ # @param defaults [Hash] default values to remove
430
+ # @param gem_object [Object] original gem object (unused but for consistency)
431
+ # @return [Hash] cleaned request hash
432
+ def cleanup_serialized_request(hash, defaults, gem_object = nil)
433
+ # Remove response-only fields from messages
434
+ if hash[:messages]
435
+ hash[:messages].each do |msg|
436
+ msg.delete(:id)
437
+ msg.delete(:model)
438
+ msg.delete(:stop_reason)
439
+ msg.delete(:stop_sequence)
440
+ msg.delete(:type)
441
+ msg.delete(:usage)
442
+ end
443
+ end
444
+
445
+ # Apply content compression for API efficiency
446
+ compress_content(hash)
447
+
448
+ # Remove provider-internal fields and empty arrays
449
+ hash.delete(:stop_sequences) if hash[:stop_sequences] == []
450
+ hash.delete(:mcp_servers) if hash[:mcp_servers] == []
451
+ hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice
452
+
453
+ # Remove default values (except max_tokens which is required by API)
454
+ defaults.each do |key, value|
455
+ next if key == :max_tokens # Anthropic API requires max_tokens
456
+ hash.delete(key) if hash[key] == value
457
+ end
458
+
459
+ hash
460
+ end
461
+
462
+ private
463
+
464
+ # Converts single text block to string.
465
+ #
466
+ # @param system [Array]
467
+ # @return [String, Array]
468
+ def compress_system_content(system)
469
+ return system unless system.is_a?(Array)
470
+
471
+ # Single text block → string shorthand
472
+ if system.one? && system.first.is_a?(Hash) && system.first[:type] == "text"
473
+ system.first[:text]
474
+ else
475
+ system
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
481
+ end
482
+ end