Commit 573c2b0

mo khan <mo@mokhan.ca>
2026-08-30 05:32:39
feat: gate GGUF/llama.cpp logging behind LOG_LEVEL
llama.cpp's native logger printed unconditional load-time chatter straight to stderr, bypassing Ruby entirely. Register a llama_log_set callback filtered by LOG_LEVEL (default warn, debug for full verbosity) and drive Elelem.logger's level from the same variable, so native and Ruby logging share one knob.
Changed files (3)
ext
elelem_llama
lib
ext/elelem_llama/elelem_llama.cpp
@@ -8,6 +8,8 @@
 #include "chat.h"
 #include <nlohmann/json.hpp>
 #include <algorithm>
+#include <cstdlib>
+#include <cstring>
 #include <string>
 #include <vector>
 
@@ -26,10 +28,33 @@ struct el_handle {
 
 static bool g_backend = false;
 
+// llama.cpp/ggml log to stderr by default; gate that behind LOG_LEVEL so
+// GGUF model loads aren't noisy unless a caller opts in, matching the
+// verbosity Elelem.logger is configured with in lib/elelem.rb.
+static enum ggml_log_level el_log_threshold() {
+    const char *level = std::getenv("LOG_LEVEL");
+    if (!level) return GGML_LOG_LEVEL_WARN;
+    std::string v(level);
+    std::transform(v.begin(), v.end(), v.begin(), ::tolower);
+    if (v == "debug") return GGML_LOG_LEVEL_DEBUG;
+    if (v == "info") return GGML_LOG_LEVEL_INFO;
+    if (v == "error") return GGML_LOG_LEVEL_ERROR;
+    return GGML_LOG_LEVEL_WARN;
+}
+
+static void el_log_callback(enum ggml_log_level level, const char *text, void *user_data) {
+    if (level < el_log_threshold()) return;
+    fputs(text, stderr);
+}
+
 // temp <= 0 selects greedy/deterministic sampling; seed is the RNG seed for the
 // sampled path (both surfaced so callers -- notably the eval harness -- can pin them).
 void *el_open(const char *path, int n_gpu_layers, int n_ctx, int n_threads, float temp, int seed) {
-    if (!g_backend) { llama_backend_init(); g_backend = true; }
+    if (!g_backend) {
+        llama_log_set(el_log_callback, nullptr);
+        llama_backend_init();
+        g_backend = true;
+    }
 
     llama_model_params mp = llama_model_default_params();
     mp.n_gpu_layers = n_gpu_layers;
lib/elelem/net/gguf.rb
@@ -38,6 +38,11 @@ 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"]
+        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")
+        end
+
         content = result["content"].to_s
         block&.call(type: "saying", text: content) unless content.empty?
 
lib/elelem.rb
@@ -38,9 +38,13 @@ require_relative "elelem/web_terminal"
 
 module Elelem
     def logger
-      @logger ||= Logger.new("./elelem/current.log")
+      @logger ||= Logger.new("./elelem/current.log").tap do |log|
+        log.level = Logger.const_get(ENV.fetch("LOG_LEVEL", "warn").upcase)
+      end
     end
 
+  OUTPUT_LIMIT = 10_000
+
   def self.sh(cmd, args: [], cwd: Dir.pwd, env: {}, timeout: nil)
     output = StringIO.new
     options = { chdir: cwd }
@@ -63,10 +67,18 @@ module Elelem
 
       status = wait_thr.value
       note = timed_out ? "\n[command timed out after #{timeout}s]" : ""
-      { exit_status: status.exitstatus || (timed_out ? 124 : 1), content: output.string + note }
+      { exit_status: status.exitstatus || (timed_out ? 124 : 1), content: truncate(output.string) + note }
     end
   end
 
+  def self.truncate(content, limit: OUTPUT_LIMIT)
+    return content if content.length <= limit
+
+    half = limit / 2
+    elided = content.length - limit
+    "#{content[0, half]}\n[#{elided} characters elided]\n#{content[-half, half]}"
+  end
+
   def self.terminate(pid)
     Process.kill("TERM", -pid)
     sleep 0.5