Module: ActiveAgent::Providers::Anthropic::Transforms
- Defined in:
- lib/active_agent/providers/anthropic/transforms.rb
Overview
Transforms between convenient input formats and Anthropic API structures.
Provides bidirectional transformations:
- Expand: shortcuts → API format (string → content blocks, consecutive messages → grouped)
- Compress: API format → shortcuts (single content blocks → strings for efficiency)
Class Method Summary collapse
-
.cleanup_serialized_request(hash, defaults, gem_object = nil) ⇒ Hash
Cleans up serialized request for API submission.
-
.compress_content(hash) ⇒ Hash
Converts single-element content arrays back to string shorthand.
-
.compress_message_content!(msg) ⇒ void
Converts single text block arrays to string shorthand.
-
.gem_to_hash(gem_object) ⇒ Hash
Converts gem model object to hash via JSON round-trip.
-
.inject_additional_properties(schema) ⇒ Hash
Recursively injects
additionalProperties: falseinto all object schemas. -
.normalize_content(content) ⇒ Array<Hash>
Expands content shortcuts into structured content block arrays.
-
.normalize_content_item(item) ⇒ Hash
Infers content block type from hash keys or converts string to text block.
-
.normalize_mcp_servers(mcp_servers) ⇒ Array<Hash>
Normalizes MCP servers from common format to Anthropic format.
-
.normalize_messages(messages) ⇒ Array<Hash>
Merges consecutive same-role messages into single messages with multiple content blocks.
- .normalize_params(params) ⇒ Hash
-
.normalize_response_format(format) ⇒ Hash
Normalizes response_format to Anthropic output_config structure.
-
.normalize_source(source) ⇒ Hash
Converts image/document source shortcuts to API structure.
-
.normalize_system(system) ⇒ String, Array
Converts system content shortcuts to API format.
- .normalize_system_block(block) ⇒ Hash
-
.normalize_tool_choice(tool_choice) ⇒ Object
Normalizes tool_choice from common format to Anthropic gem model objects.
-
.normalize_tools(tools) ⇒ Array<Hash>
Normalizes tools from common format to Anthropic format.
-
.parse_data_uri(data_uri) ⇒ Hash
Extracts media type and data from data URI.
Class Method Details
.cleanup_serialized_request(hash, defaults, gem_object = nil) ⇒ Hash
Cleans up serialized request for API submission.
Removes response-only fields, applies content compression, removes provider-internal fields, and removes default values. Note: max_tokens is kept even if it matches default as Anthropic API requires it.
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 519 def cleanup_serialized_request(hash, defaults, gem_object = nil) # Remove response-only fields from messages if hash[:messages] hash[:messages].each do |msg| msg.delete(:id) msg.delete(:model) msg.delete(:stop_reason) msg.delete(:stop_sequence) msg.delete(:stop_details) msg.delete(:type) msg.delete(:usage) end end # Apply content compression for API efficiency compress_content(hash) # Remove provider-internal fields and empty arrays hash.delete(:stop_sequences) if hash[:stop_sequences] == [] hash.delete(:mcp_servers) if hash[:mcp_servers] == [] hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice # Remove default values (except max_tokens which is required by API) defaults.each do |key, value| next if key == :max_tokens # Anthropic API requires max_tokens hash.delete(key) if hash[key] == value end hash end |
.compress_content(hash) ⇒ Hash
Converts single-element content arrays back to string shorthand.
Reduces payload size by reversing the expansion done by normalize methods.
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 477 def compress_content(hash) return hash unless hash.is_a?(Hash) # Compress message content hash[:messages]&.each do |msg| (msg) end # Compress system content if hash[:system].is_a?(Array) hash[:system] = compress_system_content(hash[:system]) end hash end |
.compress_message_content!(msg) ⇒ void
This method returns an undefined value.
Converts single text block arrays to string shorthand.
[{type: "text", text: "hello"}] → "hello"
499 500 501 502 503 504 505 506 507 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 499 def (msg) content = msg[:content] return unless content.is_a?(Array) # Single text block → string shorthand if content.one? && content.first.is_a?(Hash) && content.first[:type] == "text" msg[:content] = content.first[:text] end end |
.gem_to_hash(gem_object) ⇒ Hash
Converts gem model object to hash via JSON round-trip.
This ensures proper nested serialization and symbolic keys.
21 22 23 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 21 def gem_to_hash(gem_object) JSON.parse(gem_object.to_json, symbolize_names: true) end |
.inject_additional_properties(schema) ⇒ Hash
Recursively injects additionalProperties: false into all object schemas.
Anthropic's structured output API requires this field on all object types. Only inserts when not already explicitly set by the user.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 225 def inject_additional_properties(schema) return schema unless schema.is_a?(Hash) if schema[:type] == "object" && !schema.key?(:additionalProperties) schema[:additionalProperties] = false end # Recurse into nested schemas schema[:properties]&.each_value { |prop| inject_additional_properties(prop) } schema[:items]&.then { |items| inject_additional_properties(items) } schema[:anyOf]&.each { |s| inject_additional_properties(s) } schema[:oneOf]&.each { |s| inject_additional_properties(s) } schema[:allOf]&.each { |s| inject_additional_properties(s) } if schema[:definitions] schema[:definitions].each_value { |defn| inject_additional_properties(defn) } end if schema[:"$defs"] schema[:"$defs"].each_value { |defn| inject_additional_properties(defn) } end schema end |
.normalize_content(content) ⇒ Array<Hash>
Expands content shortcuts into structured content block arrays.
Handles multiple input formats:
- String →
[{type: "text", text: "..."}] - Hash with multiple keys → separate blocks per content type
- Array → normalized items
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 341 def normalize_content(content) case content when String # String → array with single text block [ { type: "text", text: content } ] when Array # Normalize each item in the array content.flat_map { |item| normalize_content_item(item) } when Hash # Check if hash has multiple content keys (text, image, document) # If so, expand into separate content blocks hash = content.deep_symbolize_keys content_keys = [ :text, :image, :document ] found_keys = content_keys & hash.keys if found_keys.size > 1 # Multiple content types - expand into array found_keys.flat_map { |key| normalize_content_item({ key => hash[key] }) } else # Single content item [ normalize_content_item(content) ] end when nil [] else # Pass through other types (might be gem objects already) [ content ] end end |
.normalize_content_item(item) ⇒ Hash
Infers content block type from hash keys or converts string to text block.
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 375 def normalize_content_item(item) case item when String { type: "text", text: item } when Hash hash = item.deep_symbolize_keys # If type is specified, return as-is return hash if hash[:type] # Type inference based on keys if hash[:text] { type: "text" }.merge(hash) elsif hash[:image] # Normalize image source format source = normalize_source(hash[:image]) { type: "image", source: source }.merge(hash.except(:image)) elsif hash[:document] # Normalize document source format source = normalize_source(hash[:document]) { type: "document", source: source }.merge(hash.except(:document)) elsif hash[:tool_use_id] # Tool result content { type: "tool_result" }.merge(hash) elsif hash[:id] && hash[:name] && hash[:input] # Tool use content { type: "tool_use" }.merge(hash) else # Unknown format - return as-is and let gem validate hash end else # Pass through (might be gem object) item end end |
.normalize_mcp_servers(mcp_servers) ⇒ Array<Hash>
Normalizes MCP servers from common format to Anthropic format.
Common format:
{name: "stripe", url: "https://...", authorization: "token"}
Anthropic format:
{type: "url", name: "stripe", url: "https://...", authorization_token: "token"}
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 84 def normalize_mcp_servers(mcp_servers) return mcp_servers unless mcp_servers.is_a?(Array) mcp_servers.map do |server| server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server # If already in Anthropic native format (has type: "url"), return as-is # Check for absence of common format 'authorization' field OR presence of native 'authorization_token' if server_hash[:type] == "url" && (server_hash[:authorization_token] || !server_hash[:authorization]) next server_hash end # Convert common format to Anthropic format result = { type: "url", name: server_hash[:name], url: server_hash[:url] } # Map 'authorization' to 'authorization_token' if server_hash[:authorization] result[:authorization_token] = server_hash[:authorization] elsif server_hash[:authorization_token] result[:authorization_token] = server_hash[:authorization_token] end result.compact end end |
.normalize_messages(messages) ⇒ Array<Hash>
Merges consecutive same-role messages into single messages with multiple content blocks.
Required by Anthropic API - consecutive messages with the same role must be combined.
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 255 def () return unless .is_a?(Array) grouped = [] .each do |msg| msg_hash = msg.is_a?(Hash) ? msg.deep_symbolize_keys : { role: :user, content: msg } # Extract role role = msg_hash[:role]&.to_sym || :user # Determine content if msg_hash.key?(:content) # Has explicit content key content = normalize_content(msg_hash[:content]) elsif msg_hash.key?(:role) && msg_hash.keys.size > 1 # Has role + other keys (e.g., {role: "assistant", text: "..."}) # Treat everything except :role as content content = normalize_content(msg_hash.except(:role)) elsif !msg_hash.key?(:role) # No role or content - treat entire hash as content content = normalize_content(msg_hash) else # Only has role, no content content = [] end if grouped.empty? || grouped.last[:role] != role grouped << { role: role, content: content } else # Merge content from consecutive same-role messages grouped.last[:content] += content end end grouped end |
.normalize_params(params) ⇒ Hash
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 27 def normalize_params(params) params = params.dup params[:messages] = (params[:messages]) if params[:messages] params[:system] = normalize_system(params[:system]) if params[:system] params[:tools] = normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] # Normalize json_schema response_format → output_config (Anthropic's native structured output field) if params[:response_format] output_config = normalize_response_format(params.delete(:response_format)) params[:output_config] = output_config if output_config end # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format) if params[:mcps] params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps)) elsif params[:mcp_servers] params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) end params end |
.normalize_response_format(format) ⇒ Hash
Normalizes response_format to Anthropic output_config structure.
Supported (ActiveAgent common format):
{ type: "json_schema", json_schema: { name: "...", schema: {...}, strict: true } }
→ { format: { type: "json_schema", schema: ... } }
Notes:
- Anthropic requires
additionalProperties: falseon all object schemas. This is auto-injected into any object schemas that don't have it set. - Anthropic does not use OpenAI's
nameorstrictfields in output_config.format. - json_object is not handled here; it remains prompt-emulated.
- text is not handled here; Anthropic returns plain text by default.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 183 def normalize_response_format(format) case format when Hash format_hash = format.deep_symbolize_keys if format_hash[:type].to_s == "json_schema" schema = format_hash[:json_schema]&.dig(:schema) if schema schema = inject_additional_properties(schema.deep_dup) end { format: { type: "json_schema", schema: schema }.compact } elsif format_hash[:type].to_s == "json_object" # json_object is not handled here; it remains prompt-emulated. nil elsif format_hash[:type].to_s == "text" # text is not handled here; it remains prompt-emulated. nil else # Pass through (already properly structured or Anthropic native format) format_hash end when Symbol, String # Bare :json_schema without a schema cannot use native output_config. # Anthropic requires a valid JSON Schema — return nil to fall back. nil else format end end |
.normalize_source(source) ⇒ Hash
Converts image/document source shortcuts to API structure.
Handles multiple formats:
- Regular URL →
{type: "url", url: "..."} - Data URI →
{type: "base64", media_type: "...", data: "..."} - Hash with base64 →
{type: "base64", media_type: "...", data: "..."}
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 421 def normalize_source(source) case source when String # Check if it's a data URI (e.g., "data:image/png;base64,...") if source.start_with?("data:") parse_data_uri(source) else # Regular URL → wrap in url source type { type: "url", url: source } end when Hash hash = source.deep_symbolize_keys # Already has type → return as-is return hash if hash[:type] # Has base64 data → add type if hash[:data] && hash[:media_type] { type: "base64" }.merge(hash) else # Unknown format → return as-is hash end else source end end |
.normalize_system(system) ⇒ String, Array
Converts system content shortcuts to API format.
Handles string, hash, or array inputs. Strings pass through unchanged since Anthropic accepts both string and structured formats.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 300 def normalize_system(system) case system when String # Keep strings as-is - Anthropic accepts both string and array system when Array # Normalize array of system blocks system.map { |block| normalize_system_block(block) } when Hash # Single hash becomes array with one block [ normalize_system_block(system) ] else system end end |
.normalize_system_block(block) ⇒ Hash
318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 318 def normalize_system_block(block) case block when String { type: "text", text: block } when Hash hash = block.deep_symbolize_keys # Add type if missing and can be inferred hash[:type] ||= "text" if hash[:text] hash else block end end |
.normalize_tool_choice(tool_choice) ⇒ Object
Normalizes tool_choice from common format to Anthropic gem model objects.
The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, etc.), not a plain hash.
Maps:
- "required" → ToolChoiceAny (force tool use)
- "auto" → ToolChoiceAuto (let model decide)
- { name: "..." } → ToolChoiceTool with name
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 126 def normalize_tool_choice(tool_choice) # If already a gem model object, return as-is return tool_choice if tool_choice.is_a?(::Anthropic::Models::ToolChoiceAuto) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceAny) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceTool) || tool_choice.is_a?(::Anthropic::Models::ToolChoiceNone) case tool_choice when "required" # Create ToolChoiceAny model for forcing tool use ::Anthropic::Models::ToolChoiceAny.new(type: :any) when "auto" # Create ToolChoiceAuto model for letting model decide ::Anthropic::Models::ToolChoiceAuto.new(type: :auto) when Hash choice_hash = tool_choice.deep_symbolize_keys # If has type field, create appropriate model if choice_hash[:type] case choice_hash[:type].to_sym when :any ::Anthropic::Models::ToolChoiceAny.new(**choice_hash) when :auto ::Anthropic::Models::ToolChoiceAuto.new(**choice_hash) when :tool ::Anthropic::Models::ToolChoiceTool.new(**choice_hash) when :none ::Anthropic::Models::ToolChoiceNone.new(**choice_hash) else choice_hash end # Convert { name: "..." } to ToolChoiceTool elsif choice_hash[:name] ::Anthropic::Models::ToolChoiceTool.new(type: :tool, name: choice_hash[:name]) else choice_hash end else tool_choice end end |
.normalize_tools(tools) ⇒ Array<Hash>
Normalizes tools from common format to Anthropic format.
Accepts both parameters and input_schema keys, converting to Anthropic's input_schema.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 56 def normalize_tools(tools) return tools unless tools.is_a?(Array) tools.map do |tool| tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool # If already in Anthropic format (has input_schema), return as-is next tool_hash if tool_hash[:input_schema] # Convert common format with 'parameters' to Anthropic format with 'input_schema' if tool_hash[:parameters] tool_hash = tool_hash.dup tool_hash[:input_schema] = tool_hash.delete(:parameters) end tool_hash end end |
.parse_data_uri(data_uri) ⇒ Hash
Extracts media type and data from data URI.
Expected format: data:[<media type>][;base64],<data>
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 |
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 454 def parse_data_uri(data_uri) # Extract media type and data from data URI # Format: data:[<media type>][;base64],<data> match = data_uri.match(%r{\Adata:([^;,]+)(?:;base64)?,(.+)\z}) if match { type: "base64", media_type: match[1], data: match[2] } else # Invalid data URI - return as URL fallback { type: "url", url: data_uri } end end |