Class: Legion::Extensions::Llm::Ollama::Translator

Inherits:
Object
  • Object
show all
Includes:
Logging::Helper
Defined in:
lib/legion/extensions/llm/ollama/translator.rb

Overview

Canonical provider translator for Ollama (/api/chat NDJSON wire format).

Implements render_request, parse_response, parse_chunk, and capabilities. Ollama uses NDJSON streaming (not SSE), native tool calling, and the ‘think` flag for extended thinking support.

Ollama quirks (declared in capabilities):

  • tool_calls_as_text: false — Ollama returns structured tool_calls natively.

  • forced_tool_choice: false — Ollama does not support forced tool selection.

  • assistant_prefill: false — Ollama does not support assistant prefill.

Constant Summary collapse

OLLAMA_STOP_REASON_MAP =

Ollama-specific stop_reason mapping (done_reason field).

{
  'stop' => :end_turn,
  'tool_use' => :tool_use,
  'length' => :max_tokens
}.freeze
FALLBACK_STOP_REASON =
:end_turn
PARAM_OPTIONS_KEYS =

G18 parameter mapping: canonical params -> Ollama options keys.

{
  max_tokens: :num_predict,
  temperature: :temperature,
  top_p: :top_p,
  top_k: :top_k,
  stop_sequences: :stop,
  seed: :seed,
  frequency_penalty: :frequency_penalty,
  presence_penalty: :presence_penalty
}.freeze
SUPPORTED_PARAMS =
%i[
  max_tokens temperature top_p top_k stop_sequences
  seed frequency_penalty presence_penalty
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config: nil) ⇒ Translator

Returns a new instance of Translator.



50
51
52
# File 'lib/legion/extensions/llm/ollama/translator.rb', line 50

def initialize(config: nil)
  @config = config
end

Instance Method Details

#capabilitiesObject

Declared capabilities for the Ollama provider.



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/legion/extensions/llm/ollama/translator.rb', line 135

def capabilities
  {
    provider: 'ollama',
    streaming: true,
    tool_calls: true,
    thinking: true,
    vision: true,
    embeddings: true,
    tool_calls_as_text: false,
    forced_tool_choice: false,
    assistant_prefill: false
  }.freeze
end

#parse_chunk(raw) ⇒ Object

Parse a single NDJSON chunk into a Canonical::Chunk or nil.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/legion/extensions/llm/ollama/translator.rb', line 119

def parse_chunk(raw)
  return nil if raw.nil?

  data = normalize_chunk_input(raw)
  return nil if data.nil?

  # Handle canonical-form chunks (from conformance fixtures)
  return handle_canonical_chunk(data) if data['type'] || data[:type]

  parse_ollama_chunk(data)
rescue StandardError => e
  handle_exception(e, level: :error, handled: false, operation: 'ollama.translator.parse_chunk')
  raise
end

#parse_response(wire) ⇒ Object

Parse an Ollama /api/chat completion response into a Canonical::Response.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/legion/extensions/llm/ollama/translator.rb', line 78

def parse_response(wire)
  return canonical_error_response(wire) unless wire.is_a?(Hash)
  return Canonical::Response.from_hash(wire) if canonical_response?(wire)

  message = wire[:message] || wire['message'] || {}
  content = message[:content] || message['content'] || ''
  tool_calls_raw = message[:tool_calls] || message['tool_calls']
  model = wire[:model] || wire['model']
  done_reason = wire[:done_reason] || wire['done_reason']
  done = wire[:done] || wire['done']

  extraction = Responses::ThinkingExtractor.extract(
    content,
    metadata: (message)
  )

  text = extraction.content || ''
  thinking = build_canonical_thinking(extraction)
  tool_calls = parse_tool_calls(tool_calls_raw)
  stop_reason = map_stop_reason(done_reason, done)

  usage = Canonical::Usage.from_hash({
                                       input_tokens: wire[:prompt_eval_count] || wire['prompt_eval_count'],
                                       output_tokens: wire[:eval_count] || wire['eval_count']
                                     })

  Canonical::Response.build(
    text: text.to_s,
    thinking: thinking,
    tool_calls: tool_calls,
    usage: usage,
    stop_reason: stop_reason,
    model: model,
    metadata: {}
  )
rescue StandardError => e
  handle_exception(e, level: :error, handled: false, operation: 'ollama.translator.parse_response')
  raise
end

#render_request(request) ⇒ Object

Render a canonical request into Ollama /api/chat wire payload.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/legion/extensions/llm/ollama/translator.rb', line 55

def render_request(request)
  model = request.&.dig(:model) || 'default'
  messages = format_messages(request)
  payload = {
    model: model,
    messages: messages,
    stream: request.stream
  }

  payload[:tools] = format_tools(request.tools) unless request.tools.to_h.empty?
  apply_options(payload, request.params)
  apply_thinking_config(payload, request)
  apply_response_format(payload, request.params)

  log.debug do
    "[llm][ollama-translator] action=render_request model=#{model} stream=#{request.stream} " \
      "message_count=#{messages.size} tools=#{request.tools&.size || 0}"
  end

  payload.compact
end