Class: Legion::LLM::Call::LexLLMAdapter

Inherits:
Object
  • Object
show all
Includes:
Legion::Logging::Helper
Defined in:
lib/legion/llm/call/lex_llm_adapter.rb

Overview

Adapts a lex-llm provider class to legion-llm's native dispatch contract.

Constant Summary collapse

METADATA_KEYS =
%i[tier capabilities enabled].freeze
RESPONSES_PROVIDER_FAMILIES =

Only providers that natively expose /v1/responses (OpenAI API proper). All other providers (vLLM, Ollama, MLX, Anthropic, Bedrock, Gemini, Vertex, Azure Foundry) use /v1/chat/completions and must declare :responses in their instance capabilities explicitly.

%i[openai].freeze
CANONICAL_ROLES =

Canonical roles are exactly system/user/assistant/tool. Dialect roles (developer/critic/discriminator) fold to :system at this edge.

%i[system user assistant tool].freeze

Instance Method Summary collapse

Constructor Details

#initialize(provider_name, provider_class, instance_config: {}) ⇒ LexLLMAdapter

Returns a new instance of LexLLMAdapter.



22
23
24
25
26
27
28
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 22

def initialize(provider_name, provider_class, instance_config: {})
  @provider_name = provider_name.to_sym
  @provider_class = provider_class
  @instance_config = instance_config
  @capabilities = Array(instance_config[:capabilities] || instance_config['capabilities']).map(&:to_sym)
  @lex_llm_namespace = resolve_lex_llm_namespace
end

Instance Method Details

#chat(model:, messages:, **opts) ⇒ Object

0.8.0 provider funnel (08): positional canonical messages, and the return value is a Canonical::Response — the adapter returns it directly (with offering metadata merged), never a re-projected hash.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 33

def chat(model:, messages:, **opts)
  response = provider.chat(
    normalize_messages(messages, system: opts[:system]),
    model:      model,
    tools:      normalize_tools(opts[:tools]),
    params:     completion_params(opts),
    headers:    opts[:headers] || {},
    schema:     opts[:schema],
    thinking:   opts[:thinking],
    tool_prefs: opts[:tool_prefs]
  )

  (response, offering_metadata: opts[:offering_metadata])
end

#completion_params(opts) ⇒ Object

Canonical Params keys only (03 O03a): temperature folds into params at the adapter edge; the 0.8.0 funnel has no temperature kwarg.



73
74
75
76
77
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 73

def completion_params(opts)
  params = (opts[:params] || {}).dup
  params[:temperature] = opts[:temperature] if opts.key?(:temperature) && !opts[:temperature].nil?
  params.empty? ? nil : params
end

#count_tokens(model:, messages:) ⇒ Object



144
145
146
147
148
149
150
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 144

def count_tokens(model:, messages:, **)
  {
    result: provider.count_tokens(messages: normalize_messages(messages), model: model),
    model:  model,
    usage:  {}
  }
end

#discover_offerings(live: false, **filters) ⇒ Object

SSOT writer contract: every lex-llm-* provider actor discovers its catalog through adapter.discover_offerings (forwarded to the per-instance Provider's catalog method — same body as #offerings), then publishes the result into the shared inventory registry (lex-llm Inventory::Registry) on its own discovery cadence, reconciling write-time weights from current settings (lex-llm Inventory::WeightReconciler) before each publish. Without this funnel the actors have no catalog and the registry stays empty.



170
171
172
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 170

def discover_offerings(live: false, **filters)
  offerings(live: live, **filters)
end

#embed(model:, text:, dimensions: nil, **opts) ⇒ Object

0.8.0 embed artifact (05 §3 / O07): a documented Hash { text:, model:, embedding: Array<Float>, usage: Canonical::Usage }.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 107

def embed(model:, text:, dimensions: nil, **opts)
  response = provider.embed(
    text:       text,
    model:      model,
    dimensions: dimensions,
    params:     opts[:params] || {},
    headers:    opts[:headers] || {}
  )
  usage = response[:usage]
  input_tokens = usage.respond_to?(:input_tokens) ? usage.input_tokens.to_i : 0

  {
    result:   response[:embedding],
    model:    response[:model],
    usage:    { input_tokens: input_tokens, output_tokens: 0 },
    metadata: (offering_metadata: opts[:offering_metadata])
  }
end

#health(live: false) ⇒ Object



140
141
142
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 140

def health(live: false)
  provider.health(live: live)
end

#image(model:, prompt:, size:, with: nil, mask: nil, **opts) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 126

def image(model:, prompt:, size:, with: nil, mask: nil, **opts)
  response = call_image_provider(
    prompt:  prompt,
    model:   model,
    size:    size,
    with:    with,
    mask:    mask,
    params:  opts[:params] || {},
    headers: opts[:headers] || {}
  )

  image_response(response, model: model, offering_metadata: opts[:offering_metadata])
end

#offerings(live: false, **filters) ⇒ Object



152
153
154
155
156
157
158
159
160
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 152

def offerings(live: false, **filters)
  return [] unless provider.respond_to?(:discover_offerings)

  provider.discover_offerings(live: live, raise_on_unreachable: live, **filters)
rescue ArgumentError => e
  raise unless e.message.include?('raise_on_unreachable')

  provider.discover_offerings(live: live, **filters)
end

#responses(model:, body:, messages:, stream: false, **opts) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 79

def responses(model:, body:, messages:, stream: false, **opts, &)
  raise Legion::LLM::ProviderError, "Responses API dispatch is not supported for #{provider_name}" unless supports?(:responses)

  payload = build_responses_payload(
    body:     body,
    model:    model,
    messages: messages,
    stream:   stream,
    system:   opts[:system],
    tools:    opts[:tools]
  )

  if stream
    stream_responses_payload(payload, offering_metadata: opts[:offering_metadata], &)
  else
    response = provider.connection.post(responses_url, payload)
    responses_hash_response(response.body, offering_metadata: opts[:offering_metadata])
  end
end

#stream(model:, messages:, **opts, &block) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 48

def stream(model:, messages:, **opts, &block)
  accumulator = build_stream_accumulator
  response = provider.stream_chat(
    normalize_messages(messages, system: opts[:system]),
    model:      model,
    tools:      normalize_tools(opts[:tools]),
    params:     completion_params(opts),
    headers:    opts[:headers] || {},
    schema:     opts[:schema],
    thinking:   opts[:thinking],
    tool_prefs: opts[:tool_prefs]
  ) do |chunk|
    accumulate_stream_chunk(accumulator, chunk)
    block&.call(chunk)
  end

  if response
    (response, offering_metadata: opts[:offering_metadata])
  else
    stream_fallback_response(accumulator, model: model, offering_metadata: opts[:offering_metadata])
  end
end

#supports?(capability) ⇒ Boolean

Returns:

  • (Boolean)


99
100
101
102
103
# File 'lib/legion/llm/call/lex_llm_adapter.rb', line 99

def supports?(capability)
  return true unless capability.to_sym == :responses

  @capabilities.include?(:responses) || RESPONSES_PROVIDER_FAMILIES.include?(provider_name)
end