Commit 8580278

mo khan <mo@mokhan.ca>
2026-09-05 20:58:09
refactor: split features into separate gems
1 parent 0966ee2
ext/elelem/llama/extconf.rb
@@ -6,7 +6,7 @@ EXT_DIR   = __dir__
 GEM_ROOT  = File.expand_path("../../..", EXT_DIR)
 VENDOR    = File.join(GEM_ROOT, "vendor", "llama.cpp")
 BUILD_DIR = File.join(EXT_DIR, "build")
-NATIVE    = File.join(GEM_ROOT, "lib", "elelem", "gguf")
+NATIVE    = File.join(GEM_ROOT, "ext", "elelem", "llama", "native")
 STAMP     = File.join(BUILD_DIR, ".elelem_backend") # last backend built here
 VALID_BACKENDS = %w[cpu vulkan cuda].freeze
 
@@ -75,8 +75,7 @@ def configure_and_build(backend)
 end
 
 unless File.exist?(File.join(VENDOR, "CMakeLists.txt"))
-  abort "elelem: vendored llama.cpp missing at #{VENDOR}\n" \
-    "        run: git submodule update --init --recursive"
+  abort "elelem: vendored llama.cpp missing at #{VENDOR}\n        run: git submodule update --init --recursive"
 end
 
 backend = detect_backend
@@ -108,7 +107,7 @@ run(cxx, "-std=c++17", "-O2", "-Wall", "-Wextra", "-shared", "-fPIC",
     "-I", File.join(VENDOR, "ggml", "include"),
     "-I", File.join(VENDOR, "common"),   # chat.h (common_chat)
     "-I", File.join(VENDOR, "vendor"),   # nlohmann/json.hpp
-    File.join(EXT_DIR, "elelem/llama/elelem.cpp"),
+    File.join(EXT_DIR, "elelem.cpp"),
     "-o", File.join(NATIVE, "libelelem_llama.so"),
     "-L", libdir, "-lllama-common", "-lllama",
     "-Wl,-rpath,$ORIGIN")
lib/elelem/llama/client.rb
@@ -4,9 +4,9 @@ require "fiddle"
 require "json"
 
 module Elelem
-  module Net
-    class GGUF
-      NATIVE = File.expand_path("gguf", __dir__)
+  module Llama
+    class Client
+      NATIVE = File.expand_path("../../../ext/elelem/llama/native", __dir__)
       SHIM = File.join(NATIVE, "libelelem_llama.so")
       V = Fiddle::TYPE_VOIDP
       I = Fiddle::TYPE_INT
@@ -32,7 +32,7 @@ module Elelem
       def initialize(model:, n_ctx: 0, n_threads: 0, max_tokens: 0, n_gpu_layers: 0, temp: 0.7, seed: -1)
         @max_tokens = max_tokens
         @handle = self.class.functions[:open].call(model, n_gpu_layers, n_ctx, n_threads, temp, seed)
-        raise "gguf: failed to load model at #{model}" if @handle.null?
+        raise "llama: failed to load model at #{model}" if @handle.null?
 
         at_exit { self.class.functions[:close].call(@handle) }
       end
@@ -41,21 +41,21 @@ module Elelem
         ptr = self.class.functions[:generate].call(@handle, JSON.generate(messages), JSON.generate(tools), @max_tokens)
         result = JSON.parse(Fiddle::Pointer.new(ptr).to_s)
 
-        Elelem.logger.debug("gguf: tool-call fallback used") if result["fallback"]
-        Elelem.logger.debug("gguf: harmony tag fallback used") if result["harmony_tag_fallback"]
-        Elelem.logger.warn("gguf: generate error: #{result["error"]}") if result["error"]
+        Elelem.logger.debug("llama: tool-call fallback used") if result["fallback"]
+        Elelem.logger.debug("llama: harmony tag fallback used") if result["harmony_tag_fallback"]
+        Elelem.logger.warn("llama: generate error: #{result["error"]}") if result["error"]
         if result["ms_decode"]
           Elelem.logger.debug(format(
-            "gguf: reset=%.0fms prefill=%.0fms (n=%d, %d reused) decode=%.0fms (n=%d)",
+            "llama: reset=%.0fms prefill=%.0fms (n=%d, %d reused) decode=%.0fms (n=%d)",
             result["ms_reset"], result["ms_prefill"], result["n_prompt"], result["n_reused"].to_i,
             result["ms_decode"], result["n_decoded"]
           ))
         end
         if result["tool_calls"].to_a.empty? && !tools.empty? && result["content"].to_s.include?("\"name\"")
-          Elelem.logger.debug("gguf: no tool calls parsed, tools offered")
+          Elelem.logger.debug("llama: no tool calls parsed, tools offered")
         end
         reasoning = result["reasoning"].to_s
-        Elelem.logger.debug("gguf: reasoning: #{reasoning}") unless reasoning.empty?
+        Elelem.logger.debug("llama: reasoning: #{reasoning}") unless reasoning.empty?
         block&.call(type: "thinking", text: reasoning) unless reasoning.empty?
 
         content = result["content"].to_s
@@ -63,7 +63,7 @@ module Elelem
           content = "[error: #{result["error"]}]"
         elsif result["truncated"] && content.empty? && result.fetch("tool_calls", []).empty?
           limit = @max_tokens.positive? ? "max_tokens" : "available context"
-          Elelem.logger.warn("gguf: hit #{limit} (#{result["n_decoded"]}/#{result["budget"]}) before producing a reply")
+          Elelem.logger.warn("llama: hit #{limit} (#{result["n_decoded"]}/#{result["budget"]}) before producing a reply")
           content = "[no reply: ran out of output tokens before finishing]"
         end
         block&.call(type: "saying", text: content) unless content.empty?
lib/elelem/llama/plugin.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
-Elelem::Providers.register(:gguf) do
-  gpu = %w[vulkan cuda metal].include?(Elelem::Net::GGUF.backend)
+require_relative "../llama"
+
+Elelem::Providers.register(:llama) do
+  gpu = %w[vulkan cuda metal].include?(Elelem::Llama::Client.backend)
 
   default_model = -> {
     dir = File.expand_path("~/.agents/models")
@@ -16,7 +18,7 @@ Elelem::Providers.register(:gguf) do
     end
   }
 
-  Elelem::Net::GGUF.new(
+  Elelem::Llama::Client.new(
     model: ENV.fetch("ELELEM_GGUF_MODEL") { default_model.call },
     n_ctx: 0,
     n_threads: 0,
lib/elelem/llama/version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Llama
+    VERSION = "0.1.0"
+  end
+end
lib/elelem/llama.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require_relative "llama/version"
+require_relative "llama/client"
elelem-llama.gemspec
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "lib/elelem/llama/version"
+
+Gem::Specification.new do |spec|
+  spec.name = "elelem-llama"
+  spec.version = Elelem::Llama::VERSION
+  spec.authors = ["mo khan"]
+  spec.email = ["mo@mokhan.ca"]
+
+  spec.summary = "A llama.cpp plugin for elelem."
+  spec.description = "A llama.cpp plugin for elelem."
+  spec.homepage = "https://src.mokhan.ca/xlgmokha/elelem"
+  spec.license = "MIT"
+  spec.required_ruby_version = ">= 4.0.0"
+  spec.required_rubygems_version = ">= 4.0.0"
+  spec.metadata["allowed_push_host"] = "https://rubygems.org"
+  spec.metadata["homepage_uri"] = spec.homepage
+  spec.metadata["source_code_uri"] = "https://git.mokhan.ca/xlgmokha/elelem.git"
+
+  spec.files = [
+    "LICENSE.txt",
+    "lib/elelem/llama.rb",
+    "lib/elelem/llama/version.rb",
+    "lib/elelem/llama/client.rb",
+    "lib/elelem/llama/plugin.rb",
+    "ext/elelem/llama/elelem.cpp",
+    "ext/elelem/llama/extconf.rb",
+  ]
+  # Vendored llama.cpp source (git submodule) is compiled at install time by the
+  # extension below, so it must ship in the gem.
+  spec.files += Dir["vendor/llama.cpp/**/*"].select { |path| File.file?(path) }
+  spec.extensions = ["ext/elelem/llama/extconf.rb"]
+  spec.require_paths = ["lib"]
+
+  spec.add_dependency "elelem", "~> 0.10"
+  spec.add_dependency "fiddle", "~> 1.1"
+  spec.add_dependency "json", "~> 2.0"
+end