Module: Legion::LLM::API::StreamAssembler::ChunkAdapter

Defined in:
lib/legion/llm/api/stream_assembler.rb

Class Method Summary collapse

Class Method Details

.from_canonical(chunk) ⇒ Object



590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
# File 'lib/legion/llm/api/stream_assembler.rb', line 590

def from_canonical(chunk)
  case chunk.type
  when :text_delta
    AdaptedChunk.new(text: chunk.delta.to_s)
  when :thinking_delta
    AdaptedChunk.new(thinking_text: chunk.delta.to_s, thinking_signature: chunk.signature)
  when :tool_call_delta
    tc = chunk.tool_call
    return AdaptedChunk.new if tc.nil?

    source = tc.respond_to?(:source) ? tc.source : nil
    AdaptedChunk.new(
      tool_calls: [{
        id:          tc.respond_to?(:id) ? tc.id : nil,
        name:        tc.respond_to?(:name) ? tc.name : nil,
        arguments:   tc.respond_to?(:arguments) ? tc.arguments : {},
        result:      tc.respond_to?(:result) ? tc.result : nil,
        server_tool: server_tool_source?(source)
      }]
    )
  else
    AdaptedChunk.new
  end
end

.from_legacy(chunk) ⇒ Object

Legacy lex-llm Responses::StreamChunk shape (.content, .thinking). NB: legacy chunks in Anthropic/Chat lanes are text-only — the executor tool loop accumulates tool calls into the final response, not per-chunk. The Responses lane uses canonical chunks already. Only probe tool_calls when the chunk is the concrete StreamChunk.



620
621
622
623
624
625
626
627
628
629
# File 'lib/legion/llm/api/stream_assembler.rb', line 620

def from_legacy(chunk)
  text = chunk.respond_to?(:content) ? safe_call(chunk, :content).to_s : chunk.to_s
  thinking_text, thinking_signature = legacy_thinking(chunk)
  AdaptedChunk.new(
    text:               text,
    thinking_text:      thinking_text,
    thinking_signature: thinking_signature,
    tool_calls:         legacy_tool_calls_if_real(chunk)
  )
end

.legacy_thinking(chunk) ⇒ Object



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
# File 'lib/legion/llm/api/stream_assembler.rb', line 649

def legacy_thinking(chunk)
  return [nil, nil] unless chunk.respond_to?(:thinking)

  thinking = safe_call(chunk, :thinking)
  return [nil, nil] if thinking.nil?

  if thinking.is_a?(Hash)
    normalized = thinking.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
    [normalized[:content] || normalized[:text] || normalized[:thinking], normalized[:signature]]
  elsif thinking.respond_to?(:content)
    [thinking.content, thinking.respond_to?(:signature) ? thinking.signature : nil]
  else
    [thinking.to_s, nil]
  end
end

.legacy_tool_calls(chunk) ⇒ Object



665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# File 'lib/legion/llm/api/stream_assembler.rb', line 665

def legacy_tool_calls(chunk)
  return [] unless chunk.respond_to?(:tool_calls)

  calls = safe_call(chunk, :tool_calls)
  return [] if calls.nil? || (calls.respond_to?(:empty?) && calls.empty?)

  # Legacy yields tool_calls as { id => obj }. Canonical post-P3 yields arrays.
  iterable = calls.is_a?(Hash) ? calls.values : Array(calls)
  iterable.filter_map do |tc|
    name = tc.respond_to?(:name) ? tc.name.to_s : ''
    args = tc.respond_to?(:arguments) ? tc.arguments : {}
    next if name.empty? && (args.nil? || (args.respond_to?(:empty?) && args.empty?))

    {
      id:          tc.respond_to?(:id) ? tc.id : nil,
      name:        name,
      arguments:   args.is_a?(String) ? safe_parse_args(args) : args,
      server_tool: false
    }
  end
end

.legacy_tool_calls_if_real(chunk) ⇒ Object



631
632
633
634
635
636
# File 'lib/legion/llm/api/stream_assembler.rb', line 631

def legacy_tool_calls_if_real(chunk)
  return [] unless defined?(::Legion::Extensions::Llm::Responses::StreamChunk) &&
                   chunk.is_a?(::Legion::Extensions::Llm::Responses::StreamChunk)

  legacy_tool_calls(chunk)
end

.normalize(chunk) ⇒ Object



581
582
583
584
585
586
587
588
# File 'lib/legion/llm/api/stream_assembler.rb', line 581

def normalize(chunk)
  return nil if chunk.nil?

  return from_canonical(chunk) if defined?(::Legion::Extensions::Llm::Canonical::Chunk) &&
                                  chunk.is_a?(::Legion::Extensions::Llm::Canonical::Chunk)

  from_legacy(chunk)
end

.safe_call(obj, method) ⇒ Object

respond_to? alone isn’t sufficient for RSpec doubles that stub respond_to? to true but don’t actually implement every getter. RSpec::Mocks::MockExpectationError descends from Exception (not StandardError), so we widen the rescue here just for the adapter entry point.



643
644
645
646
647
# File 'lib/legion/llm/api/stream_assembler.rb', line 643

def safe_call(obj, method)
  obj.public_send(method)
rescue Exception # rubocop:disable Lint/RescueException -- isolating provider chunk shape probing
  nil
end

.safe_parse_args(str) ⇒ Object



687
688
689
690
691
692
693
# File 'lib/legion/llm/api/stream_assembler.rb', line 687

def safe_parse_args(str)
  return {} if str.to_s.empty?

  Legion::JSON.parse(str, symbolize_names: true)
rescue StandardError
  str
end

.server_tool_source?(source) ⇒ Boolean

Returns:

  • (Boolean)


695
696
697
698
699
700
# File 'lib/legion/llm/api/stream_assembler.rb', line 695

def server_tool_source?(source)
  return false if source.nil?

  type = source.is_a?(Hash) ? (source[:type] || source['type']) : source
  %i[special registry extension mcp].include?(type&.to_sym)
end