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

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.

Parameters:

  • hash (Hash)

    serialized request

  • defaults (Hash)

    default values to remove

  • gem_object (Object) (defaults to: nil)

    original gem object (unused but for consistency)

Returns:

  • (Hash)

    cleaned request hash



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 432

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(: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.

Parameters:

  • hash (Hash)

Returns:

  • (Hash)


390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 390

def compress_content(hash)
  return hash unless hash.is_a?(Hash)

  # Compress message content
  hash[:messages]&.each do |msg|
    compress_message_content!(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.

‘[“text”, text: “hello”]` → `“hello”`

Parameters:

  • msg (Hash)

    message with :content key



412
413
414
415
416
417
418
419
420
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 412

def compress_message_content!(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.

Parameters:

  • gem_object (Object)

    any object responding to .to_json

Returns:

  • (Hash)


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

.normalize_content(content) ⇒ Array<Hash>

Expands content shortcuts into structured content block arrays.

Handles multiple input formats:

  • String → ‘[“text”, text: “…”]`

  • Hash with multiple keys → separate blocks per content type

  • Array → normalized items

Parameters:

  • content (String, Array, Hash)

Returns:

  • (Array<Hash>)


254
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
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 254

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.

Parameters:

  • item (String, Hash)

Returns:

  • (Hash)


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 288

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"}

Parameters:

  • mcp_servers (Array<Hash>)

Returns:

  • (Array<Hash>)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 78

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.

Parameters:

  • messages (Array<Hash>)

Returns:

  • (Array<Hash>)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 168

def normalize_messages(messages)
  return messages unless messages.is_a?(Array)

  grouped = []

  messages.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

Parameters:

  • params (Hash)

Returns:

  • (Hash)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 27

def normalize_params(params)
  params = params.dup
  params[:messages] = normalize_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]

  # 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_source(source) ⇒ Hash

Converts image/document source shortcuts to API structure.

Handles multiple formats:

  • Regular URL → ‘“url”, url: “…”`

  • Data URI → ‘“base64”, media_type: “…”, data: “…”`

  • Hash with base64 → ‘“base64”, media_type: “…”, data: “…”`

Parameters:

  • source (String, Hash)

Returns:

  • (Hash)


334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 334

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.

Parameters:

  • system (String, Array, Hash)

Returns:

  • (String, Array)


213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 213

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

Parameters:

  • block (String, Hash)

Returns:

  • (Hash)


231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 231

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

Parameters:

  • tool_choice (String, Hash, Object)

Returns:

  • (Object)

    Anthropic gem model object



120
121
122
123
124
125
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
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 120

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`.

Parameters:

  • tools (Array<Hash>)

Returns:

  • (Array<Hash>)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 50

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>],<data>`

Parameters:

  • data_uri (String)

    e.g., “data:image/png;base64,iVBORw0…”

Returns:

  • (Hash)

    ‘“base64”, media_type: “…”, data: “…”`



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/active_agent/providers/anthropic/transforms.rb', line 367

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