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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -1
- data/lib/active_agent/providers/_base_provider.rb +94 -82
- data/lib/active_agent/providers/anthropic/_types.rb +2 -2
- data/lib/active_agent/providers/anthropic/options.rb +4 -6
- data/lib/active_agent/providers/anthropic/request.rb +157 -78
- data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
- data/lib/active_agent/providers/anthropic_provider.rb +159 -59
- data/lib/active_agent/providers/common/messages/_types.rb +46 -3
- data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
- data/lib/active_agent/providers/common/responses/base.rb +118 -70
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/concerns/previewable.rb +39 -5
- data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
- data/lib/active_agent/providers/log_subscriber.rb +64 -246
- data/lib/active_agent/providers/mock_provider.rb +23 -23
- data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama_provider.rb +0 -1
- data/lib/active_agent/providers/open_ai/_base.rb +3 -2
- data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
- data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
- data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
- data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
- data/lib/active_agent/providers/open_ai_provider.rb +0 -3
- data/lib/active_agent/providers/open_router/_types.rb +27 -1
- data/lib/active_agent/providers/open_router/options.rb +49 -1
- data/lib/active_agent/providers/open_router/request.rb +252 -66
- data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
- data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
- data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
- data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
- data/lib/active_agent/providers/open_router/transforms.rb +164 -0
- data/lib/active_agent/providers/open_router_provider.rb +23 -0
- data/lib/active_agent/version.rb +1 -1
- metadata +17 -160
- data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
- data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
- data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
- data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
- data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
- data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
- data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
- data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
- data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
- data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
- data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
- data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
- data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
- data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
- data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
- data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
- data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
- data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
- data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
- data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
- data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
- data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
- data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
- data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
- data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
- data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
- data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
- data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
- data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
- data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
- data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
- data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
- data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
- data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
- data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
- data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
- data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
- data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
- data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
- data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
- data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
- data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
- data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
- data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
- data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
- data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
- data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
- data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
- data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
- data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
- data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
- 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
|