Class: Legion::Extensions::Llm::Canonical::Response

Inherits:
Data
  • Object
show all
Defined in:
lib/legion/extensions/llm/canonical/response.rb

Overview

rubocop:disable Lint/ConstantDefinitionInBlock – required for Data.define block scope Canonical response shape — the provider-boundary contract. Per R2: does NOT replace Inference::Response (the pipeline envelope). Per Amendment A: immutable Data.define with strict factory.

Constant Summary collapse

STOP_REASONS =
%i[end_turn tool_use max_tokens stop_sequence content_filter error].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#metadataObject (readonly)

Returns the value of attribute metadata

Returns:

  • (Object)

    the current value of metadata



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def 
  @metadata
end

#modelObject (readonly)

Returns the value of attribute model

Returns:

  • (Object)

    the current value of model



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def model
  @model
end

#routingObject (readonly)

Returns the value of attribute routing

Returns:

  • (Object)

    the current value of routing



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def routing
  @routing
end

#stop_reasonObject (readonly)

Returns the value of attribute stop_reason

Returns:

  • (Object)

    the current value of stop_reason



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def stop_reason
  @stop_reason
end

#textObject (readonly)

Returns the value of attribute text

Returns:

  • (Object)

    the current value of text



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def text
  @text
end

#thinkingObject (readonly)

Returns the value of attribute thinking

Returns:

  • (Object)

    the current value of thinking



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def thinking
  @thinking
end

#tool_callsObject (readonly)

Returns the value of attribute tool_calls

Returns:

  • (Object)

    the current value of tool_calls



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def tool_calls
  @tool_calls
end

#usageObject (readonly)

Returns the value of attribute usage

Returns:

  • (Object)

    the current value of usage



12
13
14
# File 'lib/legion/extensions/llm/canonical/response.rb', line 12

def usage
  @usage
end

Class Method Details

.build(text: '', thinking: nil, tool_calls: nil, usage: nil, stop_reason: nil, model: nil, routing: nil, metadata: nil) ⇒ Object

Build from keyword args (primary constructor).



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/legion/extensions/llm/canonical/response.rb', line 68

def self.build(
  text: '', thinking: nil, tool_calls: nil, usage: nil,
  stop_reason: nil, model: nil, routing: nil, metadata: nil
)
  stop_reason_sym = stop_reason&.to_sym
  unless stop_reason_sym.nil? || STOP_REASONS.include?(stop_reason_sym)
    raise ArgumentError,
          "Invalid stop_reason: #{stop_reason_sym.inspect}. Must be one of: #{STOP_REASONS.join(', ')}"
  end

  new(
    text: text.to_s,
    thinking: thinking,
    tool_calls: tool_calls || [],
    usage: usage,
    stop_reason: stop_reason_sym,
    model: model,
    routing: routing || {},
    metadata:  || {}
  )
end

.from_hash(source) ⇒ Object

Build from a Hash (raw provider response or deserialized wire payload). Unknown keys go to metadata, never silently dropped.



20
21
22
23
24
25
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
# File 'lib/legion/extensions/llm/canonical/response.rb', line 20

def self.from_hash(source)
  return nil if source.nil?

  h = source.transform_keys(&:to_sym)

  # Extract known fields
  text = h.delete(:text) || h.delete(:content) || ''
  text = text.to_s if text

  thinking_raw = h.delete(:thinking)
  thinking = thinking_raw.is_a?(Thinking) ? thinking_raw : Thinking.from_hash(thinking_raw)

  tool_calls_raw = h.delete(:tool_calls)
  tool_calls = Array(tool_calls_raw).filter_map do |tc|
    tc.is_a?(ToolCall) ? tc : ToolCall.from_hash(tc)
  end

  usage_raw = h.delete(:usage)
  usage = usage_raw.is_a?(Usage) ? usage_raw : Usage.from_hash(usage_raw)

  # Normalize stop_reason
  stop_reason_raw = h.delete(:stop_reason) || h.delete(:finish_reason)
  stop_reason = stop_reason_raw&.to_sym if stop_reason_raw
  unless stop_reason.nil? || STOP_REASONS.include?(stop_reason)
    raise ArgumentError,
          "Invalid stop_reason: #{stop_reason.inspect}. Must be one of: #{STOP_REASONS.join(', ')}"
  end

  model = h.delete(:model)
  routing = h.delete(:routing) || {}

  # Remaining keys become metadata
   = h.delete(:metadata) || {}
   = .merge(h).compact

  new(
    text: text,
    thinking: thinking,
    tool_calls: tool_calls,
    usage: usage,
    stop_reason: stop_reason,
    model: model,
    routing: routing,
    metadata: 
  )
end

Instance Method Details

#error?Boolean

Whether the response ended due to an error.

Returns:

  • (Boolean)


113
114
115
# File 'lib/legion/extensions/llm/canonical/response.rb', line 113

def error?
  stop_reason == :error
end

#to_hObject

Serialize to a Hash for AMQP/fleet/wire transport.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/legion/extensions/llm/canonical/response.rb', line 91

def to_h
  {
    text: text,
    thinking: thinking&.to_h,
    tool_calls: tool_calls&.map { |tc| tc.is_a?(ToolCall) ? tc.to_h : tc },
    usage: usage&.to_h,
    stop_reason: stop_reason,
    model: model,
    routing: routing,
    metadata: 
  }.compact.reject do |k, v|
    %i[tool_calls routing
       metadata].include?(k) && v.is_a?(Enumerable) && v.empty?
  end
end

#tool_call?Boolean

Whether the response includes tool calls.

Returns:

  • (Boolean)


108
109
110
# File 'lib/legion/extensions/llm/canonical/response.rb', line 108

def tool_call?
  !tool_calls.nil? && !tool_calls.empty?
end