Commit 5ac95e4

mo khan <mo@mokhan.ca>
2026-09-23 06:46:57
refactor: extract model resolution into testable ModelResolver
1 parent 30795df
lib/elelem/llama/model_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Llama
+    module ModelResolver
+      def self.resolve(model:, env: ENV, models_dir: "~/.agents/models")
+        model || env.fetch("ELELEM_GGUF_MODEL") { default_model(models_dir) }
+      end
+
+      def self.default_model(models_dir)
+        dir = File.expand_path(models_dir)
+        link = File.join(dir, "default.gguf")
+        return link if File.symlink?(link) || File.exist?(link)
+
+        models = Dir[File.join(dir, "*.gguf")]
+        case models.length
+        when 1 then models.first
+        when 0 then raise "no .gguf model found in #{dir}. Set ELELEM_GGUF_MODEL=/path/to/model.gguf"
+        else raise "multiple .gguf models found in #{dir}. Set ELELEM_GGUF_MODEL=/path/to/model.gguf or symlink " \
+                   "#{link} to the one to use by default"
+        end
+      end
+    end
+  end
+end
lib/elelem/llama.rb
@@ -5,25 +5,12 @@ require "json"
 require "elelem"
 
 require_relative "llama/version"
+require_relative "llama/model_resolver"
 require_relative "llama/provider"
 
 Elelem.configure do |config|
   config.provider(:llama) do |model: nil, **|
-    default_model = lambda {
-      dir = File.expand_path("~/.agents/models")
-      link = File.join(dir, "default.gguf")
-      return link if File.symlink?(link) || File.exist?(link)
-
-      models = Dir[File.join(dir, "*.gguf")]
-      case models.length
-      when 1 then models.first
-      when 0 then raise "no .gguf model found in #{dir}. Set ELELEM_GGUF_MODEL=/path/to/model.gguf"
-      else raise "multiple .gguf models found in #{dir}. Set ELELEM_GGUF_MODEL=/path/to/model.gguf or symlink " \
-                 "#{link} to the one to use by default"
-      end
-    }
-
-    model_path = model || ENV.fetch("ELELEM_GGUF_MODEL") { default_model.call }
+    model_path = Elelem::Llama::ModelResolver.resolve(model:)
 
     default_gpu_layers = lambda {
       backend = Elelem::Llama::Provider.backend
spec/elelem/llama/model_resolver_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Llama::ModelResolver do
+  around do |example|
+    Dir.mktmpdir { |dir| @models_dir = dir and example.run }
+  end
+
+  def resolve(model: nil, env: {})
+    described_class.resolve(model:, env:, models_dir: @models_dir)
+  end
+
+  it "prefers an explicit model" do
+    expect(resolve(model: "/explicit.gguf", env: { "ELELEM_GGUF_MODEL" => "/env.gguf" })).to eq("/explicit.gguf")
+  end
+
+  it "falls back to ELELEM_GGUF_MODEL when no explicit model is given" do
+    expect(resolve(env: { "ELELEM_GGUF_MODEL" => "/env.gguf" })).to eq("/env.gguf")
+  end
+
+  context "with no explicit model or ENV" do
+    it "picks the only .gguf file in the models dir" do
+      FileUtils.touch(File.join(@models_dir, "only.gguf"))
+
+      expect(resolve).to eq(File.join(@models_dir, "only.gguf"))
+    end
+
+    it "prefers a default.gguf symlink when multiple models exist" do
+      FileUtils.touch(File.join(@models_dir, "a.gguf"))
+      FileUtils.touch(File.join(@models_dir, "b.gguf"))
+      File.symlink("a.gguf", File.join(@models_dir, "default.gguf"))
+
+      expect(resolve).to eq(File.join(@models_dir, "default.gguf"))
+    end
+
+    it "raises when no .gguf model is found" do
+      expect { resolve }.to raise_error(/no \.gguf model found/)
+    end
+
+    it "raises when multiple .gguf models exist with no default" do
+      FileUtils.touch(File.join(@models_dir, "a.gguf"))
+      FileUtils.touch(File.join(@models_dir, "b.gguf"))
+
+      expect { resolve }.to raise_error(/multiple \.gguf models found/)
+    end
+  end
+end
spec/spec_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "tmpdir"
+require_relative "../lib/elelem/llama/model_resolver"
+
+RSpec.configure do |config|
+  config.disable_monkey_patching!
+
+  config.expect_with :rspec do |c|
+    c.syntax = :expect
+  end
+end
.rspec
@@ -0,0 +1,1 @@
+--require spec_helper
elelem-llama.gemspec
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
     "LICENSE.txt",
     "lib/elelem/llama.rb",
     "lib/elelem/llama/version.rb",
+    "lib/elelem/llama/model_resolver.rb",
     "lib/elelem/llama/provider.rb",
     "exe/elelem-llama",
     "ext/elelem/llama/elelem.cpp",
Gemfile.lock
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    elelem-llama (0.3.0)
+    elelem-llama (0.3.1)
       elelem (~> 0.13)
       fiddle (~> 1.1)
       json (~> 3.0)
@@ -123,7 +123,7 @@ CHECKSUMS
   bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae
   diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
   elelem (0.13.0)
-  elelem-llama (0.3.0)
+  elelem-llama (0.3.1)
   erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
   fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
   forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838