Class: KairosMcp::Daemon::DaemonLlmCaller

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/daemon/daemon_llm_caller.rb

Overview

DaemonLlmCaller โ€” thin Anthropic API wrapper for daemon LLM phases.

Design (Phase 4 v0.3 ยง1.3):

Interface contract (LlmPhaseFunctions expects):
  caller.call(messages:, system:, max_tokens:, **) โ†’ Hash
  Return: { content: String, input_tokens: Int, output_tokens: Int,
            attempts: Int }

Usage tracking contract:
  `attempts` reports TOTAL HTTP requests made (including retries).
  `input_tokens` / `output_tokens` are from the SUCCESSFUL response only.
  UsageAccumulator records `attempts` as llm_calls (not 1).

Shutdown contract:
  Caller injects `stop_requested:` proc. DaemonLlmCaller checks it
  between retry sleeps and before each HTTP call.

Defined Under Namespace

Classes: ConfigError, LlmCallError, ShutdownRequested

Constant Summary collapse

API_URL =
'https://api.anthropic.com/v1/messages'
API_VERSION =
'2023-06-01'
DEFAULT_MODEL =
'claude-sonnet-4-6-20260514'
DEFAULT_TIMEOUT =

short for SIGTERM compliance (<10s)

5
MAX_RETRIES =
2
MAX_RETRY_SLEEP =

fallback cap when no Retry-After header

30

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, model: DEFAULT_MODEL, timeout: DEFAULT_TIMEOUT, stop_requested: -> { false }, heartbeat_callback: nil, logger: nil) ⇒ DaemonLlmCaller

Returns a new instance of DaemonLlmCaller.

Parameters:

  • api_key (String)

    Anthropic API key

  • model (String) (defaults to: DEFAULT_MODEL)

    model ID (dated)

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    HTTP read timeout in seconds

  • stop_requested (Proc) (defaults to: -> { false })

    returns true when daemon is shutting down

  • heartbeat_callback (Proc, nil) (defaults to: nil)

    called every 0.5s during retry sleep

  • logger (#info, #warn, #error, nil) (defaults to: nil)


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/kairos_mcp/daemon/daemon_llm_caller.rb', line 54

def initialize(
  api_key:,
  model: DEFAULT_MODEL,
  timeout: DEFAULT_TIMEOUT,
  stop_requested: -> { false },
  heartbeat_callback: nil,
  logger: nil
)
  @api_key            = api_key
  @model              = model
  @timeout            = timeout
  @stop_requested     = stop_requested
  @heartbeat_callback = heartbeat_callback
  @logger             = logger
  validate_config!
end

Instance Method Details

#call(messages:, system:, max_tokens:) ⇒ Hash

Returns { content:, input_tokens:, output_tokens:, attempts: }.

Parameters:

  • messages (Array<Hash>)

    Anthropic message format

  • system (String)

    system prompt

  • max_tokens (Integer)

Returns:

  • (Hash)

    { content:, input_tokens:, output_tokens:, attempts: }

Raises:



77
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/kairos_mcp/daemon/daemon_llm_caller.rb', line 77

def call(messages:, system:, max_tokens:, **)
  attempts = 0
  last_error = nil

  (1 + MAX_RETRIES).times do |i|
    check_shutdown!

    attempts += 1
    call_start = monotonic_now

    begin
      response = http_post(
        model: @model,
        max_tokens: max_tokens,
        system: system,
        messages: messages
      )

      log_call(:info, attempts, call_start, response)

      return {
        content:      extract_text(response),
        input_tokens:  response.dig('usage', 'input_tokens') || 0,
        output_tokens: response.dig('usage', 'output_tokens') || 0,
        attempts:      attempts
      }

    rescue LlmCallError => e
      last_error = e
      log(:warn, "LLM call failed: HTTP #{e.http_code} (attempt #{i + 1}/#{1 + MAX_RETRIES})")

      if e.retryable && i < MAX_RETRIES
        sleep_time = compute_backoff(i, e)
        log(:info, "Retrying after #{sleep_time}s")
        interruptible_sleep(sleep_time)
      else
        break
      end
    end
  end

  # Attach total attempt count to the error for budget tracking
  if last_error
    last_error.instance_variable_set(:@attempts, attempts) unless last_error.attempts > 0
    raise last_error
  end
  raise LlmCallError.new('unknown error', attempts: attempts)
end

#verify!Object

Startup probe: verify API key is valid.

Raises:

  • (ConfigError)

    if key is invalid or API unreachable



128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/kairos_mcp/daemon/daemon_llm_caller.rb', line 128

def verify!
  call(
    messages: [{ role: 'user', content: 'ping' }],
    system: 'Reply with exactly "pong".',
    max_tokens: 8
  )
  true
rescue LlmCallError => e
  raise ConfigError, "API key verification failed: #{e.message}"
rescue ShutdownRequested
  # Shutdown during verify is fine โ€” don't mask as ConfigError
  raise
end