Module: Legion::LLM::Call::Dispatch

Extended by:
Dispatch, Legion::Logging::Helper
Included in:
Dispatch
Defined in:
lib/legion/llm/call/dispatch.rb

Constant Summary collapse

CAPABILITY_METHODS =

Mapping of supported capability names to extension method names.

{
  chat:         :chat,
  stream:       :stream,
  responses:    :responses,
  embed:        :embed,
  image:        :image,
  count_tokens: :count_tokens
}.freeze

Instance Method Summary collapse

Instance Method Details

#available?(provider) ⇒ Boolean

Returns true when the provider is registered in Registry.

Parameters:

  • provider (Symbol, String)

Returns:

  • (Boolean)


102
103
104
# File 'lib/legion/llm/call/dispatch.rb', line 102

def available?(provider)
  Registry.registered?(provider)
end

#call(provider:, capability:, instance: nil, model: nil) ⇒ Canonical::Response

Generic dispatch entry point. Routes to the appropriate extension method based on the capability name.

Parameters:

  • provider (Symbol, String)

    provider name

  • capability (Symbol, String)

    one of :chat, :stream, :embed, :count_tokens

  • instance (Symbol, String, nil) (defaults to: nil)

    provider instance (nil = default)

  • model (String, nil) (defaults to: nil)

    model identifier forwarded to the extension

  • block (Proc, nil)

    block forwarded to the extension (e.g. for streaming)

Returns:

  • (Canonical::Response)

    canonical provider response

Raises:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/legion/llm/call/dispatch.rb', line 36

def call(provider:, capability:, instance: nil, model: nil, **, &)
  cap_sym = capability.to_sym
  method_name = CAPABILITY_METHODS[cap_sym]
  raise Legion::LLM::ProviderError, "unsupported capability: #{capability}" unless method_name

  ext = fetch_extension!(provider, instance: instance)
  raise Legion::LLM::ProviderError, "unsupported capability #{capability} for provider #{provider}" if ext.respond_to?(:supports?) && !ext.supports?(cap_sym)

  enforce_model_policy!(ext, provider: provider, model: model)

  log.info("[llm][dispatch] capability=#{cap_sym} provider=#{provider} " \
           "instance=#{instance || 'default'} model=#{model}")

  raw = ext.public_send(method_name, model: model, **, &)
  normalize_response(raw)
rescue Legion::LLM::LLMError
  raise
rescue StandardError => e
  raise unless defined?(Legion::Extensions::Llm)

  map_lex_llm_error(e, provider: provider, model: model)
end

#dispatch_chat(provider:, model:, messages:) ⇒ Object

Deprecated.

Use #call with capability: :chat instead.



63
64
65
66
67
68
69
# File 'lib/legion/llm/call/dispatch.rb', line 63

def dispatch_chat(provider:, model:, messages:, **)
  unless @chat_deprecation_warned
    log.warn('[llm][dispatch] DEPRECATED: dispatch_chat — use Dispatch.call(capability: :chat)')
    @chat_deprecation_warned = true
  end
  call(provider: provider, capability: :chat, model: model, messages: messages, **)
end

#dispatch_count_tokens(provider:, model:, messages:) ⇒ Object

Deprecated.

Use #call with capability: :count_tokens instead.



90
91
92
93
94
95
96
# File 'lib/legion/llm/call/dispatch.rb', line 90

def dispatch_count_tokens(provider:, model:, messages:, **)
  unless @count_tokens_deprecation_warned
    log.warn('[llm][dispatch] DEPRECATED: dispatch_count_tokens — use Dispatch.call(capability: :count_tokens)')
    @count_tokens_deprecation_warned = true
  end
  call(provider: provider, capability: :count_tokens, model: model, messages: messages, **)
end

#dispatch_embed(provider:, model:, text:) ⇒ Object

Deprecated.

Use #call with capability: :embed instead.



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

def dispatch_embed(provider:, model:, text:, **)
  unless @embed_deprecation_warned
    log.warn('[llm][dispatch] DEPRECATED: dispatch_embed — use Dispatch.call(capability: :embed)')
    @embed_deprecation_warned = true
  end
  call(provider: provider, capability: :embed, model: model, text: text, **)
end

#dispatch_stream(provider:, model:, messages:) ⇒ Object

Deprecated.

Use #call with capability: :stream instead.



81
82
83
84
85
86
87
# File 'lib/legion/llm/call/dispatch.rb', line 81

def dispatch_stream(provider:, model:, messages:, **, &)
  unless @stream_deprecation_warned
    log.warn('[llm][dispatch] DEPRECATED: dispatch_stream — use Dispatch.call(capability: :stream)')
    @stream_deprecation_warned = true
  end
  call(provider: provider, capability: :stream, model: model, messages: messages, **, &)
end

#normalize_response(raw) ⇒ Canonical::Response

Normalize a raw extension response into a Canonical::Response.

Dispatch-boundary response contract: Call::Dispatch.call has always returned this shape, and the SSOT v3 executor dispatch (Inference::RouteAttempts#ssot_v3_direct_dispatch) normalizes the raw SelectionDispatch value through this before the executor consumes it — the native tool loop and response translation are written against Canonical::Response, never the raw provider Message.

Expected extension return shapes (any subset is acceptable):

{ content:, usage: { input_tokens:, output_tokens: }, model: }
{ result:, usage: ... }
lex-llm Message (the sync/stream provider return shape)

Returns:

  • (Canonical::Response)


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
# File 'lib/legion/llm/call/dispatch.rb', line 121

def normalize_response(raw)
  # Already a canonical response — no conversion needed
  return raw if defined?(Canonical::Response) && raw.is_a?(Canonical::Response)

  # Non-hash objects (object with .content) or plain values → hash first
  raw = coerce_to_hash(raw) unless raw.is_a?(Hash)

  text = extract_response_text(raw)
  raw_usage = raw[:usage] || {}

  # Preserve non-string payload (e.g., image URLs, embedding vectors as metadata)
  # so the compat adapter can return [:result] / [:content] for non-chat results.
   = raw[:metadata] || raw[:offering_metadata] || {}
  raw_result = [raw[:result], raw[:content], raw[:response]].find { |v| !v.nil? && !v.is_a?(String) }
  [:raw_result] = raw_result if raw_result

  usage = coerce_usage(raw_usage)

  log.debug("[llm][native] normalized_response usage_class=#{usage.class}")
  text, thinking = extract_thinking_payload(text, raw, )

  tool_calls = to_canonical_tool_calls(
    raw[:tool_calls] || raw['tool_calls'] || raw[:tools] || raw['tools'] || text
  )
  stop_reason = to_canonical_stop_reason(raw[:stop_reason] || raw['stop_reason'], tool_calls)

  Canonical::Response.build(
    text:        text.to_s,
    thinking:    thinking,
    tool_calls:  tool_calls,
    usage:       usage,
    stop_reason: stop_reason,
    model:       raw[:model] || raw['model'],
    routing:     {},
    metadata:    
  )
end