Module: OllamaChat::ModelHandling

Included in:
Chat
Defined in:
lib/ollama_chat/model_handling.rb

Overview

A module that provides functionality for managing Ollama models, including checking model availability, pulling models from remote servers, and handling model presence verification.

This module encapsulates the logic for interacting with Ollama models, ensuring that required models are available locally before attempting to use them in chat sessions. It handles both local model verification and remote model retrieval when necessary.

Examples:

Checking if a model is present

chat.model_present?('llama3.1')

Pulling a model from a remote server

chat.pull_model_from_remote('mistral')

Ensuring a model is available locally

chat.pull_model_unless_present('phi3', {})

Defined Under Namespace

Classes: ModelMetadata

Instance Method Summary collapse

Instance Method Details

#all_modelsArray<SearchUI::Wrapper> (private)

Retrieves a sorted list of all available Ollama models, enriched with size information and marked as favorites where applicable.

This method fetches the list of models from the Ollama server, sorts them alphabetically by name, and wraps each in a SearchUI::Wrapper for consistent display in the user interface.

Returns:

  • (Array<SearchUI::Wrapper>)

    a sorted list of available models with metadata



339
340
341
342
343
# File 'lib/ollama_chat/model_handling.rb', line 339

def all_models
  favs = all_favourited('model')
  ollama.tags.models.sort_by(&:name).
    map { |m| model_with_size(m, favourited: favs[m.name]) }
end

#choose_model(cli_model, current_model) ⇒ String (private)

The choose_model method selects a model from the available list based on CLI input or user interaction. It processes the provided CLI model parameter to determine if a regex selector is used, filters the models accordingly, and prompts the user to choose from the filtered list if needed. The method ensures that a model is selected and displays a connection message with the chosen model and base URL.

Parameters:

  • cli_model (String)

    the model name or pattern provided via CLI

  • current_model (String)

    the fallback model if selection fails

Returns:

  • (String)

    the selected model name



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/ollama_chat/model_handling.rb', line 356

def choose_model(cli_model, current_model)
  selector = if cli_model =~ /\A\?+(.*)\z/
               cli_model = ''
               Regexp.new($1)
             end
  models = all_models
  selector and models = models.select { _1.value =~ selector }
  model =
    if models.size == 1
      models.first.value
    elsif cli_model == ''
      choose_entry(models)&.value || current_model
    else
      cli_model || current_model
    end
ensure
  connect_message(model, ollama.base_url)
end

#choose_profile_for_model(model_name) ⇒ String? (private)

Presents a list of stored profiles for the given model and prompts the user to select one.

Parameters:

  • model_name (String)

    the name of the model whose profiles are to be listed

Returns:

  • (String, nil)

    the selected profile name, or nil if none was chosen



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/ollama_chat/model_handling.rb', line 163

def choose_profile_for_model(model_name)
  profiles = models::ModelOptions.where(model_name:).order(:profile).map(&:profile)
  profiles = [ '[EXIT]' ] + profiles
  case chosen = choose_entry(profiles, prompt: "Choose profile for #{bold{model_name}}: ")
  when '[EXIT]', nil
    STDOUT.puts "Cancelled."
    return
  else
    chosen
  end
end

#copy_model_options_from_session(profile: nil) ⇒ Object (private)

This method retrieves the options stored for the current session and updates the active model options to match, ensuring the model behavior aligns with the session's specific configuration.



140
141
142
143
144
145
146
# File 'lib/ollama_chat/model_handling.rb', line 140

def copy_model_options_from_session(profile: nil)
  profile       ||= 'default'
  model_name      = @model
  model_options   = get_session_model_options
  store_model_options(model_name, model_options, profile:)
  STDOUT.puts "Model options #{italic{profile}} for #{bold{model_name}} were copied from session model options."
end

#copy_model_options_to_session(profile: nil) ⇒ Object (private)

Resets the session's model options to match the stored defaults for the current model.



150
151
152
153
154
155
156
# File 'lib/ollama_chat/model_handling.rb', line 150

def copy_model_options_to_session(profile: nil)
  profile              ||= 'default'
  model_name             = @model
  stored_model_options   = get_stored_model_options(model_name, profile:)
  session.update(model_options: stored_model_options)
  STDOUT.puts "Model options #{italic{profile}} of #{bold{model_name}} were copied to session model options."
end

#edit_model_options(model_name, profile: nil) ⇒ Object (private)

The edit_model_options method retrieves the current options for the specified model, presents them to the user for editing, and returns a new Ollama::Options instance based on the edited configuration.

Parameters:

  • model_name (String)

    the name of the model whose options are to be edited.



112
113
114
115
116
117
118
119
120
121
# File 'lib/ollama_chat/model_handling.rb', line 112

def edit_model_options(model_name, profile: nil)
  profile            ||= 'default'
  model_options        = get_stored_model_options(model_name, profile:)
  model_options        = fill_up_model_options(model_options)
  model_options_json   = edit_text(JSON.pretty_generate(model_options))
  model_options        = JSON.load(model_options_json)
  store_model_options(model_name, model_options, profile:)
rescue JSON::ParserError => e
  log(:error, "Caught in #{__method__} #{e.class}: #{e}", warn: true)
end

#edit_session_model_optionsself (private)

Presents the current session's model options to the user for editing.

Returns:

  • (self)

    the instance of the module



126
127
128
129
130
131
132
133
134
135
# File 'lib/ollama_chat/model_handling.rb', line 126

def edit_session_model_options
  model_options        = get_session_model_options
  model_options        = fill_up_model_options(model_options)
  model_options_json   = edit_text(JSON.pretty_generate(model_options))
  model_options        = JSON.load(model_options_json).compact
  session.update(model_options:)
  self
rescue JSON::ParserError => e
  log(:error, "Caught in #{__method__} #{e.class}: #{e}", warn: true)
end

#fill_up_model_options(model_options) ⇒ Ollama::Options (private)

Fills in missing keys in a model options hash using the attributes of Ollama::Options.

Parameters:

  • model_options (Hash)

    the hash containing the available model options

Returns:

  • (Ollama::Options)

    an Ollama::Options object containing all required keys



82
83
84
85
86
87
# File 'lib/ollama_chat/model_handling.rb', line 82

def fill_up_model_options(model_options)
  Ollama::Options.attributes.each_with_object(model_options) do |name, mo|
    mo[name] = model_options[name]
  end
  model_options
end

#get_default_model_optionsHash (private)

Retrieves the default model options from the application configuration.

Returns:

  • (Hash)

    the default model options as a hash



67
68
69
# File 'lib/ollama_chat/model_handling.rb', line 67

def get_default_model_options
  config.model.options.to_h
end

#get_session_model_optionsHash (private)

Retrieves the model options currently associated with the active session.

Returns:

  • (Hash)

    the session model options as a hash with symbolized keys



60
61
62
# File 'lib/ollama_chat/model_handling.rb', line 60

def get_session_model_options
  session.model_options.to_h.symbolize_keys_recursive
end

#get_stored_model_options(model_name, profile: nil) ⇒ Hash

Retrieves the stored model options from the database for a given model name.

Parameters:

  • model_name (String)

    the name of the model to look up

Returns:

  • (Hash)

    the model options as a hash with symbolized keys



40
41
42
43
44
# File 'lib/ollama_chat/model_handling.rb', line 40

def get_stored_model_options(model_name, profile: nil)
  profile ||= 'default'
  models::ModelOptions.where(model_name:, profile:).first&.options.
    to_h.symbolize_keys_recursive
end

#model_optionsOllama::Options (private)

Computes the current model options as an Ollama::Options object.

Returns:

  • (Ollama::Options)

    the current model options object



74
75
76
# File 'lib/ollama_chat/model_handling.rb', line 74

def model_options
  Ollama::Options[session.model_options.to_h.symbolize_keys_recursive]
end

#model_present?(model) ⇒ ModelMetadata, NilClass (private)

The model_present? method checks if the specified Ollama model is available.

Parameters:

  • model (String)

    the name of the Ollama model

Returns:

  • (ModelMetadata, NilClass)

    if the model is present, nil otherwise



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ollama_chat/model_handling.rb', line 182

def model_present?(model)
  ollama.show(model:) do |md|
    return ModelMetadata.new(
      name:         model,
      system:       md.system,
      capabilities: md.capabilities,
      families:     md.details.families,
    )
  end
rescue Ollama::Errors::NotFoundError
  nil
end

#model_with_size(model, favourited: false) ⇒ Object (private)

The model_with_size method formats a model's size for display by creating a formatted string that includes the model name and its size in a human-readable format with appropriate units.

Parameters:

  • model (Object)

    the model object that has name and size attributes

Returns:

  • (Object)

    a result object with an overridden to_s method that combines the model name and formatted size



235
236
237
238
239
240
241
# File 'lib/ollama_chat/model_handling.rb', line 235

def model_with_size(model, favourited: false)
  formatted_size = Term::ANSIColor.bold {
    format_bytes(model.size)
  }
  display = prefix_favourite("#{model.name} #{formatted_size}", favourited)
  SearchUI::Wrapper.new(model.name, display:)
end

#prepare_model(model) ⇒ OllamaChat::ModelHandling::ModelMetadata (private)

Ensures the specified model is available locally and synchronizes the session's capability settings with the model's actual supported features.

This method performs a lazy-load check: it pulls the model if it's missing and then immediately validates and updates 'thinking' and 'tools' support to prevent invalid API requests.

Parameters:

  • model (String)

    the name of the model to prepare for use

Returns:



253
254
255
256
257
258
259
260
261
262
# File 'lib/ollama_chat/model_handling.rb', line 253

def prepare_model(model)
  @model_metadata = pull_model_unless_present(model)
  if think? && !@model_metadata.can?('thinking')
    think_mode.selected = 'disabled'
  end

  if tools_support.on? && !@model_metadata.can?('tools')
    tools_support.set false
  end
end

#pull_model_from_remote(model) ⇒ Object (private)

The pull_model_from_remote method attempts to retrieve a model from the remote server if it is not found locally.

Parameters:

  • model (String)

    the name of the model to be pulled



199
200
201
202
# File 'lib/ollama_chat/model_handling.rb', line 199

def pull_model_from_remote(model)
  STDOUT.puts "Model #{bold{model}} not found locally, attempting to pull it from remote now…"
  ollama.pull(model:)
end

#pull_model_unless_present(model) ⇒ ModelMetadata (private)

The pull_model_unless_present method ensures that a specified model is available on the Ollama server. It first checks if the model metadata exists locally; if not, it pulls the model from a remote source and verifies its presence again. If the model still cannot be found, it raises an UnknownModelError indicating the missing model name.

Parameters:

  • model (String)

    the name of the model to ensure is present

Returns:

Raises:



215
216
217
218
219
220
221
222
223
224
225
# File 'lib/ollama_chat/model_handling.rb', line 215

def pull_model_unless_present(model)
  if  = model_present?(model)
    return 
  else
    pull_model_from_remote(model)
    if  = model_present?(model)
      return 
    end
    raise OllamaChat::UnknownModelError, "unknown model named #{@model.inspect}"
  end
end

#store_model_options(model_name, model_options, profile: nil) ⇒ Hash (private)

Stores or updates model options in the database for a specific model.

Parameters:

  • model_name (String)

    the name of the model to target

  • model_options (Hash, Ollama::Options)

    the options to persist

Returns:

  • (Hash)

    the updated model options hash



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/ollama_chat/model_handling.rb', line 94

def store_model_options(model_name, model_options, profile: nil)
  profile ||= 'default'
  options   = model_options.to_h.symbolize_keys_recursive.compact
  mo        = nil
  if mo = stored_model_options_exist?(model_name, profile:)
    mo.update(options:)
  else
    mo = models::ModelOptions.create(model_name:, options:, profile:)
  end
  mo.options
end

#stored_model_options_exist?(model_name, profile: nil) ⇒ OllamaChat::Database::Models::ModelOptions? (private)

Checks if model options exist in the database for the given model name.

Parameters:

  • model_name (String)

    the name of the model to check

Returns:



52
53
54
55
# File 'lib/ollama_chat/model_handling.rb', line 52

def stored_model_options_exist?(model_name, profile: nil)
  profile ||= 'default'
  models::ModelOptions.where(model_name:, profile:).first
end

#use_model(model = nil, keep_options: false, profile: nil) ⇒ ModelMetadata (private)

The use_model method selects and sets the model to be used for the chat session.

It allows specifying a particular model or defaults to the current model. After selecting, it pulls the model metadata if necessary. If think? is true and the chosen model does not support thinking, the think mode selector is set to 'disabled'. If tools_support.on? is true and the chosen model does not support tools, tool support is disabled. Returns the metadata for the selected model.

Parameters:

  • model (String, nil) (defaults to: nil)

    the model name to use; if omitted, the current model is retained

  • keep_options (Boolean) (defaults to: false)

    if true, session-specific model options are retained instead of reverting to model defaults.

Returns:



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/ollama_chat/model_handling.rb', line 280

def use_model(model = nil, keep_options: false, profile: nil)
  profile   ||= 'default'
  old_model   = @model

  if model.nil?
    @model = choose_model('', @model)
  else
    @model = choose_model(model, config.model.name)
  end

  if @model_metadata = model_present?(@model)
    session.update(current_model: @model)
  else
    session.update(current_model: nil)
  end

  if old_model != @model
    default_model_options = get_default_model_options
    session_model_options = get_session_model_options
    unless stored_model_options_exist?(@model, profile:)
      store_model_options(@model, default_model_options, profile:)
    end
    stored_model_options = get_stored_model_options(@model, profile:)
    if session_model_options.blank?
      if stored_model_options.present?
        session.update(model_options: stored_model_options)
      else
        store_model_options(@model, default_model_options)
        session.update(model_options: default_model_options)
      end
    elsif !keep_options && session_model_options != stored_model_options
      STDOUT.puts <<~EOT
        ⚠️ Session model options differ from defaults for model #@model!
        Session model options:
        #{JSON.pretty_generate(session_model_options)}
        Default model options:
        #{JSON.pretty_generate(stored_model_options)}
      EOT
      if confirm?(
          prompt: "❓ Overwrite session model options with defaults? (y/n) ", yes: /\Ay/i
        )
      then
        session.update(model_options: stored_model_options)
      end
    end
  end

  @model_metadata
end