Module: RubyLLM::Protocols::Mistral::MultiCompletion

Includes:
Content
Included in:
RubyLLM::Providers::Mistral::ChatCompletions
Defined in:
lib/ruby_llm/protocols/mistral/multi_completion.rb

Overview

:nodoc:

Constant Summary collapse

SERVER_TOOL_ALIASES =
{
  image_generation: { tool: { type: 'image_generation' } },
  mcp: { tool: { type: 'connector' } }
}.freeze

Class Method Summary collapse

Methods included from Content

parse_conversation_citation, parse_conversation_content, parse_conversation_file, parse_conversation_parts

Class Method Details

.append_multi_calls(entry, calls) ⇒ Object



123
124
125
126
127
128
129
130
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 123

def append_multi_calls(entry, calls)
  Array(calls).each do |call|
    target = entry['tool_calls'][call.fetch('index', 0)] ||= { 'function' => { 'arguments' => +'' } }
    target.merge!(call.slice('id', 'type', 'metadata'))
    target['function']['name'] = call.dig('function', 'name') if call.dig('function', 'name')
    target['function']['arguments'] << call.dig('function', 'arguments').to_s
  end
end

.append_multi_content(entry, content) ⇒ Object



113
114
115
116
117
118
119
120
121
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 113

def append_multi_content(entry, content)
  return if content.nil?

  if content.is_a?(String)
    entry['content'] << { 'type' => 'text', 'text' => content }
  else
    entry['content'].concat(content)
  end
end

.build_multi_chunk(data) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 97

def build_multi_chunk(data)
  @multi_model = data['model']
  @multi_usage[data['id']] = data['usage'] if data['usage']
  choice = data.dig('choices', 0) || {}
  delta = choice['delta'] || {}
  index = delta.fetch('index', 0)
  @multi_finish_reason = nil unless @multi_messages.key?(index)
  entry = @multi_messages[index] ||= { 'content' => [], 'tool_calls' => [] }
  entry.merge!(delta.slice('role', 'tool_call_id', 'metadata'))
  append_multi_content(entry, delta['content'])
  append_multi_calls(entry, delta['tool_calls'])
  @multi_finish_reason = choice['finish_reason'] if choice['finish_reason']
  content, thinking = extract_content_and_thinking(delta['content']) if entry['role'] == 'assistant'
  Chunk.new(role: :assistant, content: content, thinking: Thinking.build(text: thinking), model: data['model'])
end

.format_message_group(group, **options) ⇒ Object



20
21
22
23
24
25
26
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 20

def format_message_group(group, **options)
  content = group.first.raw_content
  return super unless group.one? && content.is_a?(Array) &&
                      content.all? { |entry| entry.is_a?(Hash) && entry['role'] }

  content.map { |entry| entry.except('index') }
end

.multi_content(messages) ⇒ Object



35
36
37
38
39
40
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 35

def multi_content(messages)
  output = messages.filter_map do |message|
    message.merge('type' => 'message.output') if message['role'] == 'assistant'
  end
  parse_conversation_content(output)
end

.multi_messagesObject



148
149
150
151
152
153
154
155
156
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 148

def multi_messages
  @multi_messages.sort.map do |_, message|
    message = message.reject { |key, value| key == 'tool_calls' && value.empty? }
    if message['role'] == 'tool' && message['content'].all? { |part| part['type'] == 'text' }
      message['content'] = message['content'].map { |part| part['text'] }.join
    end
    message
  end
end

.multi_pending_calls(calls, results, raw:) ⇒ Object



42
43
44
45
46
47
48
49
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 42

def multi_pending_calls(calls, results, raw:)
  pending = calls.reject { |call| results.key?(call['id']) }
  if pending.any? { |call| call.dig('metadata', 'tool_type') || call.dig('metadata', 'integration_id') }
    raise Error.new('Mistral returned an unfinished hosted tool call on Chat Completions', response: raw)
  end

  pending
end

.multi_responseObject



132
133
134
135
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 132

def multi_response
  { 'model' => @multi_model, 'usage' => multi_usage,
    'choices' => [{ 'messages' => multi_messages, 'finish_reason' => @multi_finish_reason }] }
end

.multi_usageObject



137
138
139
140
141
142
143
144
145
146
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 137

def multi_usage
  usages = @multi_usage.values
  usage = %w[prompt_tokens completion_tokens total_tokens].to_h do |key|
    [key, usages.sum { |value| value[key].to_i }]
  end
  usage['prompt_tokens_details'] = { 'cached_tokens' => usages.sum do |value|
    value.dig('prompt_tokens_details', 'cached_tokens').to_i
  end }
  usage
end

.parse_completion_body(data, raw:) ⇒ Object



28
29
30
31
32
33
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 28

def parse_completion_body(data, raw:)
  messages = data.dig('choices', 0, 'messages')
  return super unless messages.is_a?(Array)

  parse_multi_message(data, messages, raw:)
end

.parse_multi_message(data, messages, raw:) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 51

def parse_multi_message(data, messages, raw:)
  content = multi_content(messages)
  results = messages.filter_map do |message|
    [message['tool_call_id'], message] if message['role'] == 'tool'
  end.to_h
  calls = messages.flat_map { |message| Array(message['tool_calls']) }
  pending = multi_pending_calls(calls, results, raw:)
  usage = data['usage'] || {}
  Message.new(role: :assistant, content: content[:text], thinking: Thinking.build(text: content[:thinking]),
              attachments: content[:attachments], citations: content[:citations],
              tool_calls: parse_tool_calls(pending, response: raw, finish_reason: :tool_calls),
              server_tool_calls: parse_multi_steps(calls, results), raw_content: messages,
              input_tokens: input_tokens(usage), output_tokens: output_tokens(usage),
              cache_read_tokens: cache_read_tokens(usage), model: data['model'], raw: raw,
              finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason')))
end

.parse_multi_steps(calls, results) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 68

def parse_multi_steps(calls, results)
  calls.filter_map do |call|
    result = results[call['id']]
    next unless result

    ServerToolCall.new(type: result['role'], id: call['id'], name: call.dig('function', 'name'),
                       input: call.dig('function', 'arguments'), result: result['content'], raw: result)
  end
end

.server_tool_aliasesObject



16
17
18
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 16

def server_tool_aliases
  SERVER_TOOL_ALIASES
end

.stream_response(payload, additional_headers = {}, &block) ⇒ Object

Raises:



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ruby_llm/protocols/mistral/multi_completion.rb', line 78

def stream_response(payload, additional_headers = {}, &block)
  return super unless Array(payload[:tools]).any? { |tool| (tool[:type] || tool['type']) != 'function' }

  @multi_messages = {}
  @multi_usage = {}
  @multi_finish_reason = nil
  response = stream_events(completion_url, payload, additional_headers) do |data|
    block.call(build_multi_chunk(data))
  end
  raise Error.new('Mistral tool stream ended before completion', response:) unless @multi_finish_reason

  message = parse_completion_body(multi_response, raw: response)
  block.call(Chunk.new(role: :assistant, content: nil, tokens: message.tokens, model: message.model,
                       citations: message.citations, server_tool_calls: message.server_tool_calls,
                       tool_calls: message.tool_calls, raw_content: message.raw_content,
                       attachments: message.attachments, finish_reason: message.finish_reason))
  message
end