Module: Legion::LLM::API::StreamAssembler::ChunkAdapter
- Defined in:
- lib/legion/llm/api/stream_assembler.rb
Class Method Summary collapse
- .from_canonical(chunk) ⇒ Object
-
.from_legacy(chunk) ⇒ Object
Legacy lex-llm Responses::StreamChunk shape (.content, .thinking).
- .legacy_thinking(chunk) ⇒ Object
- .legacy_tool_calls(chunk) ⇒ Object
- .legacy_tool_calls_if_real(chunk) ⇒ Object
- .normalize(chunk) ⇒ Object
-
.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.
- .safe_parse_args(str) ⇒ Object
- .server_tool_source?(source) ⇒ Boolean
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
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 |