elelem-llama 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e027f21b17d21828927f0528f474f1c53de820390b7c37369380a12b8d77817
4
- data.tar.gz: 1ead5c9fec9523b024ac7dd0ea2d9dba0b2056f00394a4566b0f59e0431224d9
3
+ metadata.gz: '0099260d12a1606376955d0d2df33c00795e344b1634ddb7b5b36ded6cdeb2f9'
4
+ data.tar.gz: 4b0179b842631b931caade9e87846759543b9513fdf314ec958a0663f4cfcf79
5
5
  SHA512:
6
- metadata.gz: 53cc83cc2f42991baf132d13396f60827d573589fb0339da494db409883701c5abb7d8bc3ccf041155f7df108c3618282784d77d8ce96174fcd0a122454c9a7d
7
- data.tar.gz: 28bd9cc76747826feca48f504184979e1e0a8caf29bdc41d7dac8cf705d8ad569cc4dbc723c75b04799fe0d131f507f6bcd356aa5be9edd353314c2631445728
6
+ metadata.gz: 58f68b1012d8870dc6ffdeb8171ccd033c83e8ca1a4ac97ce3d9720b2c55223be3a02a2b395a0d13d14f60518377efe9cb33a95ba31b50ae0200868219df91b9
7
+ data.tar.gz: 89331d664c308f1c9b6ebce438953c9a613d4d54d1e13e2489ad20d09af0ffbef332d89d418e5cd82891574f0c7d848ff50cd7e0c9253fa6b829af31b7007099
@@ -23,9 +23,29 @@ extern "C" {
23
23
  float temp;
24
24
  uint32_t seed;
25
25
  bool dead = false;
26
+ bool in_generate = false;
26
27
  std::vector<llama_token> cached;
27
28
  };
28
29
 
30
+ typedef void (*el_token_cb)(void *userdata, const char *piece);
31
+
32
+ // Returns the length of the prefix of `s` that ends on a complete UTF-8
33
+ // sequence, so callers don't flush a truncated multi-byte codepoint.
34
+ static size_t el_utf8_safe_len(const std::string &s) {
35
+ size_t n = s.size();
36
+ size_t back = 0;
37
+ while (back < n && back < 4 && ((unsigned char) s[n - 1 - back] & 0xC0) == 0x80) back++;
38
+ if (back == n) return 0;
39
+ unsigned char lead = (unsigned char) s[n - 1 - back];
40
+ size_t seq_len = 1;
41
+ if ((lead & 0xE0) == 0xC0) seq_len = 2;
42
+ else if ((lead & 0xF0) == 0xE0) seq_len = 3;
43
+ else if ((lead & 0xF8) == 0xF0) seq_len = 4;
44
+ else if (lead >= 0x80) seq_len = back + 1; // stray continuation/invalid lead; hold it
45
+ if (back + 1 < seq_len) return n - back - 1;
46
+ return n;
47
+ }
48
+
29
49
  static bool g_backend = false;
30
50
 
31
51
  static enum ggml_log_level el_log_threshold() {
@@ -154,7 +174,7 @@ extern "C" {
154
174
  return buf.c_str();
155
175
  }
156
176
 
157
- static const char *el_generate_impl(void *handle, const char *messages_json, const char *tools_json, int max_tokens) {
177
+ static const char *el_generate_impl(void *handle, const char *messages_json, const char *tools_json, int max_tokens, el_token_cb on_token, void *userdata) {
158
178
  auto *h = (el_handle *) handle;
159
179
  const llama_vocab *vocab = llama_model_get_vocab(h->model);
160
180
  static thread_local std::string buf;
@@ -239,22 +259,40 @@ extern "C" {
239
259
  int budget = max_tokens > 0 ? std::min(max_tokens, headroom) : std::max(1, headroom / 2);
240
260
 
241
261
  std::string output;
262
+ std::string pending;
242
263
  char piece[512];
243
264
  int n_decoded = 0;
244
265
  bool eog = false;
245
266
  bool decode_failed = false;
267
+ auto last_flush = std::chrono::steady_clock::now();
246
268
  for (int t = 0; t < budget; t++) {
247
269
  llama_token id = llama_sampler_sample(smpl.get(), ctx, -1);
248
270
  if (llama_vocab_is_eog(vocab, id)) { eog = true; break; }
249
271
  int np = llama_token_to_piece(vocab, id, piece, (int32_t) sizeof(piece), 0, true);
250
- if (np > 0) output.append(piece, np);
272
+ if (np > 0) {
273
+ output.append(piece, np);
274
+ if (on_token) pending.append(piece, np);
275
+ }
251
276
  n_decoded++;
277
+ if (on_token && !pending.empty()) {
278
+ auto now = std::chrono::steady_clock::now();
279
+ if (std::chrono::duration<double, std::milli>(now - last_flush).count() >= 50.0) {
280
+ size_t safe_len = el_utf8_safe_len(pending);
281
+ if (safe_len > 0) {
282
+ std::string chunk = pending.substr(0, safe_len);
283
+ on_token(userdata, chunk.c_str());
284
+ pending.erase(0, safe_len);
285
+ }
286
+ last_flush = now;
287
+ }
288
+ }
252
289
  if (t + 1 < budget && llama_decode(ctx, llama_batch_get_one(&id, 1)) != 0) {
253
290
  decode_failed = true;
254
291
  break;
255
292
  }
256
293
  if (t + 1 < budget) h->cached.push_back(id);
257
294
  }
295
+ if (on_token && !pending.empty()) on_token(userdata, pending.c_str());
258
296
  auto t3 = std::chrono::steady_clock::now();
259
297
 
260
298
  if (!decode_failed) guard.committed = true;
@@ -363,14 +401,22 @@ extern "C" {
363
401
  return buf.c_str();
364
402
  }
365
403
 
366
- const char *el_generate(void *handle, const char *messages_json, const char *tools_json, int max_tokens) {
404
+ const char *el_generate(void *handle, const char *messages_json, const char *tools_json, int max_tokens, el_token_cb on_token, void *userdata) {
367
405
  static thread_local std::string errbuf;
368
406
  if (!handle) return el_error(errbuf, "null handle");
369
407
  if (!messages_json) return el_error(errbuf, "null messages");
370
- if (((el_handle *) handle)->dead) return el_error(errbuf, "handle is unusable after a previous fatal error");
408
+ auto *h = (el_handle *) handle;
409
+ if (h->dead) return el_error(errbuf, "handle is unusable after a previous fatal error");
410
+ if (h->in_generate) return el_error(errbuf, "generate is not reentrant");
411
+
412
+ h->in_generate = true;
413
+ struct reentrancy_guard {
414
+ el_handle *h;
415
+ ~reentrancy_guard() { h->in_generate = false; }
416
+ } guard{h};
371
417
 
372
418
  try {
373
- return el_generate_impl(handle, messages_json, tools_json, max_tokens);
419
+ return el_generate_impl(handle, messages_json, tools_json, max_tokens, on_token, userdata);
374
420
  } catch (const std::exception &e) {
375
421
  ((el_handle *) handle)->dead = true;
376
422
  return el_error(errbuf, e.what());
@@ -20,7 +20,7 @@ module Elelem
20
20
  lib = Fiddle.dlopen(SHIM)
21
21
  {
22
22
  open: Fiddle::Function.new(lib["el_open"], [V, I, I, I, F, I], V),
23
- generate: Fiddle::Function.new(lib["el_generate"], [V, V, V, I], V),
23
+ generate: Fiddle::Function.new(lib["el_generate"], [V, V, V, I, V, V], V, need_gvl: true),
24
24
  close: Fiddle::Function.new(lib["el_close"], [V], Fiddle::TYPE_VOID)
25
25
  }
26
26
  end
@@ -35,7 +35,18 @@ module Elelem
35
35
  end
36
36
 
37
37
  def fetch(messages, tools = [], &block)
38
- ptr = self.class.functions[:generate].call(@handle, JSON.generate(messages), JSON.generate(tools), @max_tokens)
38
+ streamed = false
39
+ on_token = Fiddle::Closure::BlockCaller.new(Fiddle::TYPE_VOID, [V, V]) do |_userdata, piece|
40
+ streamed = true
41
+ text = Fiddle::Pointer.new(piece).to_s.force_encoding(Encoding::UTF_8).scrub
42
+ block&.call(type: "thinking", text: text)
43
+ rescue Exception => e # rubocop:disable Lint/RescueException
44
+ Elelem.logger.warn("llama: streaming callback failed: #{e.message}")
45
+ end
46
+
47
+ ptr = self.class.functions[:generate].call(
48
+ @handle, JSON.generate(messages), JSON.generate(tools), @max_tokens, on_token, nil
49
+ )
39
50
  result = JSON.parse(Fiddle::Pointer.new(ptr).to_s)
40
51
 
41
52
  Elelem.logger.debug("llama: tool-call fallback used") if result["fallback"]
@@ -53,7 +64,7 @@ module Elelem
53
64
  end
54
65
  reasoning = result["reasoning"].to_s
55
66
  Elelem.logger.debug("llama: reasoning: #{reasoning}") unless reasoning.empty?
56
- block&.call(type: "thinking", text: reasoning) unless reasoning.empty?
67
+ block&.call(type: "thinking", text: reasoning) unless reasoning.empty? || streamed
57
68
 
58
69
  content = result["content"].to_s
59
70
  if result["error"] && content.empty?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Elelem
4
4
  module Llama
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elelem-llama
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan