Class: Mana::Backends::Anthropic

Inherits:
Base
  • Object
show all
Defined in:
lib/mana/backends/anthropic.rb

Overview

Native Anthropic Claude backend.

Sends requests directly to the Anthropic Messages API (/v1/messages). No format conversion needed — Mana's internal format matches Anthropic's.

Constant Summary

Constants inherited from Base

Base::ANTHROPIC_PATTERN

Instance Method Summary collapse

Methods inherited from Base

for, #initialize

Constructor Details

This class inherits a constructor from Mana::Backends::Base

Instance Method Details

#chat(system:, messages:, tools:, model:, max_tokens: 4096) ⇒ Object

Non-streaming request. Returns content blocks directly since our internal format already matches Anthropic's — no normalization needed (unlike OpenAI).



12
13
14
15
16
17
18
19
20
21
22
# File 'lib/mana/backends/anthropic.rb', line 12

def chat(system:, messages:, tools:, model:, max_tokens: 4096)
  uri = URI("#{@config.effective_base_url}/v1/messages")
  parsed = http_post(uri, { model:, max_tokens:, system:, tools:, messages: }, {
    "x-api-key" => @config.api_key,
    "anthropic-version" => "2023-06-01"
  })
  {
    content: parsed[:content] || [],
    usage: parsed[:usage]
  }
end

#chat_stream(system:, messages:, tools:, model:, max_tokens: 4096, &on_event) ⇒ Object

Streaming variant — yields :text_delta, text: "..." events. Returns the complete content blocks array (same format as chat).



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/mana/backends/anthropic.rb', line 26

def chat_stream(system:, messages:, tools:, model:, max_tokens: 4096, &on_event)
  uri = URI("#{@config.effective_base_url}/v1/messages")
  content_blocks = []
  current_block = nil
  usage = {}

  # Anthropic streams SSE events that incrementally build content blocks.
  # We reassemble them into the same format that chat() returns.
  http_post_stream(uri, {
    model:, max_tokens:, system:, tools:, messages:, stream: true
  }, {
    "x-api-key" => @config.api_key,
    "anthropic-version" => "2023-06-01"
  }) do |event|
    case event[:type]
    when "message_start"
      usage[:input_tokens] = event.dig(:message, :usage, :input_tokens)
    when "message_delta"
      usage[:output_tokens] = event.dig(:usage, :output_tokens)
    when "content_block_start"
      current_block = event[:content_block].dup
      # Tool input arrives as JSON fragments — accumulate as a string, parse on stop
      current_block[:input] = +"" if current_block[:type] == "tool_use"
    when "content_block_delta"
      delta = event[:delta]
      if delta[:type] == "text_delta"
        current_block[:text] = (current_block[:text] || +"") << delta[:text]
        on_event&.call(type: :text_delta, text: delta[:text])
      elsif delta[:type] == "input_json_delta"
        current_block[:input] << delta[:partial_json]
      end
    when "content_block_stop"
      # Parse the accumulated JSON string into a Ruby hash for tool_use blocks
      if current_block && current_block[:type] == "tool_use"
        current_block[:input] = begin
          JSON.parse(current_block[:input], symbolize_names: true)
        rescue JSON::ParserError
          {}
        end
      end
      content_blocks << current_block if current_block
      current_block = nil
    end
  end

  {
    content: content_blocks,
    usage: usage.empty? ? nil : usage
  }
end