Module: Legion::LLM::DaemonClient

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/daemon_client.rb

Constant Summary collapse

HEALTH_CACHE_TTL =
30
DEFAULT_TIMEOUT =
60

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns true if the daemon is reachable and healthy. Returns false immediately if daemon_url is nil. Caches a positive health check for HEALTH_CACHE_TTL seconds. An unhealthy result is not cached — rechecks on every call.

Returns:

  • (Boolean)


23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/legion/llm/daemon_client.rb', line 23

def available?
  return false if daemon_url.nil?

  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)

  return true if @healthy == true && @health_checked_at && (now - @health_checked_at) < HEALTH_CACHE_TTL

  result = check_health
  if result
    @healthy           = true
    @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
  end
  result
end

.chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil) ⇒ Object

POSTs a chat request to the daemon REST API. Returns a status hash based on the HTTP response code.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/legion/llm/daemon_client.rb', line 40

def chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil)
  request_id ||= SecureRandom.uuid

  body = {
    message:         message,
    request_id:      request_id,
    context:         context,
    tier_preference: tier_preference
  }
  body[:model]    = model    if model
  body[:provider] = provider if provider

  response = http_post('/api/llm/chat', body)
  interpret_response(response)
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.daemon_client.chat', request_id: request_id)
  mark_unhealthy
  { status: :unavailable, error: e.message }
end

.check_healthObject

GETs /api/health. Returns true on 200, false otherwise. Updates @healthy and @health_checked_at.



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/legion/llm/daemon_client.rb', line 78

def check_health
  response = http_get('/api/health')
  healthy = response.code == '200'
  @healthy           = healthy
  @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
  log.info("Daemon health check result=#{healthy ? 'healthy' : 'unhealthy'} url=#{daemon_url}")
  healthy
rescue StandardError => e
  handle_exception(e, level: :warn)
  mark_unhealthy
  false
end

.daemon_urlObject

Returns the daemon URL from settings, cached after first read. Returns nil if settings are unavailable or the key is missing.



62
63
64
65
66
# File 'lib/legion/llm/daemon_client.rb', line 62

def daemon_url
  return @daemon_url if defined?(@daemon_url)

  @daemon_url = fetch_daemon_url
end

.http_get(path) ⇒ Object

Builds and sends a GET request. Returns Net::HTTPResponse.



99
100
101
102
103
104
105
106
107
# File 'lib/legion/llm/daemon_client.rb', line 99

def http_get(path)
  uri     = URI.parse("#{daemon_url}#{path}")
  http    = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 2
  http.read_timeout = 2
  request = Net::HTTP::Get.new(uri.request_uri)
  request['Content-Type'] = 'application/json'
  http.request(request)
end

.http_post(path, body, timeout: DEFAULT_TIMEOUT) ⇒ Object

Builds and sends a POST request with a JSON body. Returns Net::HTTPResponse. The optional timeout: keyword overrides the default read timeout.



131
132
133
134
135
136
137
138
139
140
# File 'lib/legion/llm/daemon_client.rb', line 131

def http_post(path, body, timeout: DEFAULT_TIMEOUT)
  uri     = URI.parse("#{daemon_url}#{path}")
  http    = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 5
  http.read_timeout = timeout
  request = Net::HTTP::Post.new(uri.request_uri)
  request['Content-Type'] = 'application/json'
  request.body = ::JSON.dump(body)
  http.request(request)
end

.inference(messages:, tools: [], model: nil, provider: nil, caller: nil, conversation_id: nil, timeout: 120) ⇒ Object

POSTs a conversation-level inference request to the daemon REST API. Accepts a full messages array and optional tool schemas. Returns a status hash with structured inference fields on success.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/legion/llm/daemon_client.rb', line 112

def inference(messages:, tools: [], model: nil, provider: nil, caller: nil, conversation_id: nil,
              timeout: 120)
  body = { messages: messages, tools: tools }
  body[:model]           = model           if model
  body[:provider]        = provider        if provider
  body[:caller]          = caller          if caller
  body[:conversation_id] = conversation_id if conversation_id

  response = http_post('/api/llm/inference', body, timeout: timeout)
  interpret_inference_response(response)
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.daemon_client.inference', conversation_id: conversation_id)
  mark_unhealthy
  { status: :unavailable, error: e.message }
end

.interpret_response(response) ⇒ Object

Maps an HTTP response to a status hash. Follows the Legion API format: { data: … } for success, { error: … } for failure.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/legion/llm/daemon_client.rb', line 145

def interpret_response(response)
  code   = response.code.to_i
  parsed = safe_parse(response.body)

  case code
  when 200
    { status: :immediate, body: parsed.fetch(:data, parsed) }
  when 201
    { status: :created,   body: parsed.fetch(:data, parsed) }
  when 202
    data = parsed.fetch(:data, {})
    { status: :accepted, request_id: data[:request_id], poll_key: data[:poll_key] }
  when 403
    log.warn("Daemon returned 403 Denied url=#{daemon_url}")
    { status: :denied, error: parsed.fetch(:error, parsed) }
  when 429
    retry_after = extract_retry_after(response, parsed)
    log.warn("Daemon returned 429 RateLimited url=#{daemon_url} retry_after=#{retry_after}")
    { status: :rate_limited, retry_after: retry_after }
  when 503
    { status: :unavailable }
  else
    { status: :error, code: code, body: parsed }
  end
end

.mark_unhealthyObject

Marks the daemon as unhealthy and records the timestamp.



92
93
94
95
96
# File 'lib/legion/llm/daemon_client.rb', line 92

def mark_unhealthy
  log.warn("Daemon marked unhealthy url=#{daemon_url}")
  @healthy           = false
  @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
end

.reset!Object

Clears all cached state. Returns self for chaining.



69
70
71
72
73
74
# File 'lib/legion/llm/daemon_client.rb', line 69

def reset!
  remove_instance_variable(:@daemon_url) if defined?(@daemon_url)
  @healthy           = nil
  @health_checked_at = nil
  self
end