rllama 1.0.3-aarch64-linux-gnu → 1.2.0-aarch64-linux-gnu

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +18 -0
  3. data/README.md +107 -5
  4. data/bin/rllama +0 -2
  5. data/lib/rllama/aarch64-linux/libggml-base.so +1 -0
  6. data/lib/rllama/aarch64-linux/libggml-base.so.0 +1 -0
  7. data/lib/rllama/aarch64-linux/libggml-base.so.0.17.0 +0 -0
  8. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.0_1.so +0 -0
  9. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_1.so +0 -0
  10. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_2.so +0 -0
  11. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_3.so +0 -0
  12. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.6_1.so +0 -0
  13. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.6_2.so +0 -0
  14. data/lib/rllama/aarch64-linux/libggml-cpu-armv9.2_1.so +0 -0
  15. data/lib/rllama/aarch64-linux/libggml-cpu-armv9.2_2.so +0 -0
  16. data/lib/rllama/aarch64-linux/libggml-rpc.so +0 -0
  17. data/lib/rllama/aarch64-linux/libggml.so +1 -0
  18. data/lib/rllama/aarch64-linux/libggml.so.0 +1 -0
  19. data/lib/rllama/aarch64-linux/libggml.so.0.17.0 +0 -0
  20. data/lib/rllama/aarch64-linux/libllama-common.so +1 -0
  21. data/lib/rllama/aarch64-linux/libllama-common.so.0 +1 -0
  22. data/lib/rllama/aarch64-linux/libllama-common.so.0.0.10067 +0 -0
  23. data/lib/rllama/aarch64-linux/libllama.so +1 -0
  24. data/lib/rllama/aarch64-linux/libllama.so.0 +1 -0
  25. data/lib/rllama/aarch64-linux/libllama.so.0.0.10067 +0 -0
  26. data/lib/rllama/aarch64-linux/libllama_common.so +0 -0
  27. data/lib/rllama/cli.rb +11 -5
  28. data/lib/rllama/common.rb +134 -0
  29. data/lib/rllama/context.rb +220 -51
  30. data/lib/rllama/cpp.rb +18 -16
  31. data/lib/rllama/model.rb +67 -21
  32. data/lib/rllama/version.rb +1 -1
  33. data/lib/rllama.rb +2 -1
  34. data/licenses/LICENSE-boringssl +238 -0
  35. data/licenses/LICENSE-cpp-httplib +22 -0
  36. data/licenses/LICENSE-json +21 -0
  37. data/licenses/LICENSE-llama.cpp +21 -0
  38. metadata +28 -13
  39. data/lib/rllama/aarch64-linux/libggml-base.so +0 -0
  40. data/lib/rllama/aarch64-linux/libggml-cpu-alderlake.so +0 -0
  41. data/lib/rllama/aarch64-linux/libggml-cpu-haswell.so +0 -0
  42. data/lib/rllama/aarch64-linux/libggml-cpu-icelake.so +0 -0
  43. data/lib/rllama/aarch64-linux/libggml-cpu-sandybridge.so +0 -0
  44. data/lib/rllama/aarch64-linux/libggml-cpu-sapphirerapids.so +0 -0
  45. data/lib/rllama/aarch64-linux/libggml-cpu-skylakex.so +0 -0
  46. data/lib/rllama/aarch64-linux/libggml-cpu-sse42.so +0 -0
  47. data/lib/rllama/aarch64-linux/libggml-cpu-x64.so +0 -0
  48. data/lib/rllama/aarch64-linux/libggml-cpu.so +0 -0
  49. data/lib/rllama/aarch64-linux/libggml.so +0 -0
  50. data/lib/rllama/aarch64-linux/libllama.so +0 -0
@@ -1,13 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'etc'
4
+ require 'json'
4
5
 
5
6
  module Rllama
6
7
  class Context
8
+ TOOL_CALL_ID_PREFIX = 'call_'
9
+ GRAMMAR_TYPE_TOOL_CALLS = 3
10
+
7
11
  attr_reader :messages, :n_ctx, :n_batch, :n_past
8
12
 
9
- def initialize(model, embeddings: false, n_ctx: nil, n_batch: nil, n_threads: Etc.nprocessors)
13
+ def initialize(model, embeddings: false, n_ctx: nil, n_batch: nil, n_threads: Etc.nprocessors,
14
+ system: nil, tools: nil, reasoning: false)
10
15
  @model = model
16
+ @tools = tools
17
+ @reasoning = reasoning
11
18
  @n_ctx = n_ctx
12
19
  @n_batch = n_batch
13
20
  @embeddings = embeddings
@@ -38,13 +45,32 @@ module Rllama
38
45
 
39
46
  raise Error, 'Failed to create the llama_context' if @pointer.null?
40
47
 
48
+ @n_ctx = Cpp.llama_n_ctx(@pointer)
49
+ @n_batch = Cpp.llama_n_batch(@pointer)
50
+
41
51
  @n_past = 0
52
+ @cache_tokens = []
53
+ @tool_call_count = 0
42
54
  @messages = []
55
+ @messages << { role: 'system', content: system } if system
56
+
57
+ prefill if !@embeddings && (system || tools)
43
58
  end
44
59
 
45
- def generate(message, role: 'user', max_tokens: @n_ctx, temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05,
46
- seed: nil, system: nil)
47
- @messages << { role: 'system', content: system } if system && @messages.empty?
60
+ def generate(message, role: 'user', max_tokens: @n_ctx, temperature: nil, top_k: nil, top_p: nil, min_p: nil,
61
+ seed: nil, system: nil, tools: nil, reasoning: nil, &block)
62
+ temperature, top_k, top_p, min_p = resolve_sampling(temperature, top_k, top_p, min_p)
63
+
64
+ @tools = tools unless tools.nil?
65
+ @reasoning = reasoning unless reasoning.nil?
66
+
67
+ if system
68
+ if @messages.dig(0, :role).to_s == 'system'
69
+ @messages.first[:content] = system
70
+ else
71
+ @messages.unshift(role: 'system', content: system)
72
+ end
73
+ end
48
74
 
49
75
  if message.is_a?(Array)
50
76
  @messages.push(*message)
@@ -54,51 +80,19 @@ module Rllama
54
80
  @messages << { role: role, content: message }
55
81
  end
56
82
 
57
- prompt_string = @model.build_chat_template(@messages)
58
-
59
- n_prompt_tokens = -Cpp.llama_tokenize(@model.vocab, prompt_string, prompt_string.bytesize, nil, 0, true, true)
60
-
61
- raise Error, 'Prompt is too long.' if n_prompt_tokens.negative?
62
-
63
- prompt_tokens_ptr = FFI::MemoryPointer.new(:int32, n_prompt_tokens)
64
- tokens_written = Cpp.llama_tokenize(@model.vocab, prompt_string, prompt_string.bytesize, prompt_tokens_ptr,
65
- n_prompt_tokens, true, true)
66
-
67
- raise Error, 'Failed to tokenize prompt.' if tokens_written.negative?
68
-
69
- new_token_count = tokens_written - @n_past
70
-
71
- if new_token_count.positive?
72
- new_tokens_ptr = prompt_tokens_ptr + (@n_past * FFI.type_size(:int32))
73
-
74
- batch = Cpp.llama_batch_get_one(new_tokens_ptr, new_token_count)
75
-
76
- raise Error, 'llama_decode failed.' if Cpp.llama_decode(@pointer, batch) != 0
77
-
78
- @n_past = tokens_written
79
- end
80
-
81
- chain_params = Cpp.llama_sampler_chain_default_params
82
- sampler_chain = Cpp.llama_sampler_chain_init(chain_params)
83
+ applied = @model.apply_chat_template(@messages, tools: @tools, enable_thinking: @reasoning)
83
84
 
84
- Cpp.llama_sampler_chain_add(sampler_chain, Cpp.llama_sampler_init_min_p(min_p, 1)) if min_p
85
- Cpp.llama_sampler_chain_add(sampler_chain, Cpp.llama_sampler_init_top_k(top_k)) if top_k&.positive?
86
- Cpp.llama_sampler_chain_add(sampler_chain, Cpp.llama_sampler_init_top_p(top_p, 1)) if top_p && top_p < 1.0
87
- if temperature&.positive?
88
- Cpp.llama_sampler_chain_add(sampler_chain,
89
- Cpp.llama_sampler_init_temp(temperature))
90
- end
85
+ decode_prompt(applied['prompt'])
91
86
 
92
- is_probabilistic = temperature&.positive? || top_k&.positive? || (top_p && top_p < 1.0) || !min_p.nil?
93
87
  rng_seed = seed || (Random.new_seed & 0xFFFFFFFF)
94
88
 
95
- if is_probabilistic
96
- Cpp.llama_sampler_chain_add(sampler_chain, Cpp.llama_sampler_init_dist(rng_seed))
97
- else
98
- Cpp.llama_sampler_chain_add(sampler_chain, Cpp.llama_sampler_init_greedy)
99
- end
89
+ sampler = Common.sampler_init(@model.pointer, sampler_params(temperature, top_k, top_p, min_p, rng_seed, applied))
90
+
91
+ stops = applied['additional_stops'].to_a.map(&:b)
92
+ max_stop = stops.map(&:bytesize).max || 0
100
93
 
101
94
  n_decoded = 0
95
+ n_yielded = 0
102
96
 
103
97
  generated_text = ''.b
104
98
 
@@ -111,7 +105,9 @@ module Rllama
111
105
  loop do
112
106
  break if n_decoded >= max_tokens
113
107
 
114
- new_token_id = Cpp.llama_sampler_sample(sampler_chain, @pointer, -1)
108
+ new_token_id = Common.sampler_sample(sampler, @pointer)
109
+
110
+ Common.sampler_accept(sampler, new_token_id)
115
111
 
116
112
  break if Cpp.llama_vocab_is_eog(@model.vocab, new_token_id)
117
113
 
@@ -119,10 +115,10 @@ module Rllama
119
115
  n_chars = Cpp.llama_token_to_piece(@model.vocab, new_token_id, buffer, buffer.size, 0, true)
120
116
 
121
117
  if n_chars >= 0
122
- piece_bytes = buffer.read_string(n_chars)
123
- utf8_piece = piece_bytes.force_encoding(Encoding::UTF_8)
124
- generated_text << utf8_piece
125
- yield utf8_piece if block_given?
118
+ stop_at, n_yielded = emit_piece(generated_text, buffer.read_string(n_chars), stops, max_stop, n_yielded,
119
+ &block)
120
+
121
+ break if stop_at
126
122
  end
127
123
 
128
124
  token_ptr = FFI::MemoryPointer.new(:int32, 1).put_int32(0, new_token_id)
@@ -132,24 +128,54 @@ module Rllama
132
128
  raise Error, 'llama_decode failed.' if Cpp.llama_decode(@pointer, batch) != 0
133
129
 
134
130
  @n_past += 1
131
+ @cache_tokens << new_token_id
135
132
  n_decoded += 1
136
133
  end
137
134
 
135
+ if block_given? && n_yielded < generated_text.bytesize
136
+ yield generated_text.byteslice(n_yielded..).force_encoding(Encoding::UTF_8)
137
+ end
138
+
139
+ generated_text.force_encoding(Encoding::UTF_8)
140
+
138
141
  end_time = Time.now
139
142
 
140
143
  duration = end_time - start_time
141
144
 
142
145
  tps = n_decoded.positive? && duration.positive? ? n_decoded / duration : 0
143
146
 
144
- Cpp.llama_sampler_free(sampler_chain)
147
+ Common.sampler_free(sampler)
148
+
149
+ text = generated_text
150
+ reasoning = nil
151
+ tool_calls = []
152
+
153
+ if @tools || @reasoning
154
+ parsed = Common.chat_parse(generated_text, applied, reasoning: @reasoning)
155
+
156
+ reasoning = parsed['reasoning_content']
157
+ reasoning = nil if reasoning&.empty?
158
+ tool_calls = extract_tool_calls(parsed, assistant_message) if @tools
159
+
160
+ if @reasoning && tool_calls.empty?
161
+ text = parsed['content'].to_s
162
+ assistant_message[:content] = text
163
+ end
164
+ end
145
165
 
146
166
  Result.new(
147
- text: generated_text,
167
+ text:,
168
+ reasoning:,
169
+ tool_calls:,
148
170
  stats: {
149
171
  duration:,
150
172
  tokens_generated: n_decoded,
151
173
  tps:,
152
- seed: rng_seed
174
+ seed: rng_seed,
175
+ temperature:,
176
+ top_k:,
177
+ top_p:,
178
+ min_p:
153
179
  }
154
180
  )
155
181
  end
@@ -248,6 +274,149 @@ module Rllama
248
274
  Cpp.llama_free(@pointer)
249
275
  end
250
276
 
277
+ def prefill
278
+ decode_prompt(@model.build_chat_template(@messages, tools: @tools, add_generation_prompt: false,
279
+ enable_thinking: @reasoning))
280
+ rescue StandardError
281
+ nil
282
+ end
283
+
284
+ def extract_tool_calls(parsed, assistant_message)
285
+ calls = (parsed['tool_calls'] || []).map do |call|
286
+ id = call['id'].to_s
287
+ id = "#{TOOL_CALL_ID_PREFIX}#{@tool_call_count += 1}" if id.empty?
288
+
289
+ arguments = begin
290
+ JSON.parse(call.dig('function', 'arguments').to_s)
291
+ rescue JSON::ParserError
292
+ {}
293
+ end
294
+
295
+ { name: call.dig('function', 'name'), arguments:, id: }
296
+ end
297
+
298
+ return [] if calls.empty?
299
+
300
+ assistant_message.replace(
301
+ role: 'assistant',
302
+ content: nil,
303
+ tool_calls: calls.map do |call|
304
+ { type: 'function', id: call[:id],
305
+ function: { name: call[:name], arguments: call[:arguments].to_json } }
306
+ end
307
+ )
308
+
309
+ calls
310
+ end
311
+
312
+ def decode_prompt(prompt_string)
313
+ n_prompt_tokens = -Cpp.llama_tokenize(@model.vocab, prompt_string, prompt_string.bytesize, nil, 0, true, true)
314
+
315
+ raise Error, 'Prompt is too long.' if n_prompt_tokens.negative?
316
+
317
+ prompt_tokens_ptr = FFI::MemoryPointer.new(:int32, n_prompt_tokens)
318
+ tokens_written = Cpp.llama_tokenize(@model.vocab, prompt_string, prompt_string.bytesize, prompt_tokens_ptr,
319
+ n_prompt_tokens, true, true)
320
+
321
+ raise Error, 'Failed to tokenize prompt.' if tokens_written.negative?
322
+
323
+ prompt_tokens = prompt_tokens_ptr.read_array_of_int32(tokens_written)
324
+
325
+ common = common_prefix_length(prompt_tokens)
326
+
327
+ if common < @cache_tokens.length
328
+ memory = Cpp.llama_get_memory(@pointer)
329
+
330
+ if memory.null? || !Cpp.llama_memory_seq_rm(memory, 0, common, -1)
331
+ Cpp.llama_memory_clear(memory, true) unless memory.null?
332
+
333
+ common = 0
334
+ end
335
+
336
+ @n_past = common
337
+ end
338
+
339
+ while tokens_written > @n_past
340
+ n_eval = [tokens_written - @n_past, @n_batch].min
341
+
342
+ new_tokens_ptr = prompt_tokens_ptr + (@n_past * FFI.type_size(:int32))
343
+
344
+ batch = Cpp.llama_batch_get_one(new_tokens_ptr, n_eval)
345
+
346
+ raise Error, 'llama_decode failed.' if Cpp.llama_decode(@pointer, batch) != 0
347
+
348
+ @n_past += n_eval
349
+ end
350
+
351
+ @cache_tokens = prompt_tokens
352
+ end
353
+
354
+ def common_prefix_length(tokens)
355
+ limit = [@cache_tokens.length, tokens.length - 1].min
356
+
357
+ common = 0
358
+ common += 1 while common < limit && @cache_tokens[common] == tokens[common]
359
+
360
+ common
361
+ end
362
+
363
+ def sampler_params(temperature, top_k, top_p, min_p, seed, applied)
364
+ params = {
365
+ temp: temperature || 0.0,
366
+ top_k: top_k&.positive? ? top_k : 0,
367
+ top_p: top_p || 1.0,
368
+ min_p: min_p || 0.0,
369
+ seed:
370
+ }
371
+
372
+ grammar = applied['grammar'].to_s
373
+
374
+ unless grammar.empty?
375
+ params.merge!(grammar:, grammar_type: GRAMMAR_TYPE_TOOL_CALLS,
376
+ grammar_lazy: applied['grammar_lazy'],
377
+ grammar_triggers: applied['grammar_triggers'],
378
+ generation_prompt: applied['generation_prompt'])
379
+ end
380
+
381
+ params
382
+ end
383
+
384
+ def emit_piece(text, piece, stops, max_stop, n_yielded, &block)
385
+ tail_from = [text.bytesize - max_stop + 1, 0].max
386
+ text << piece
387
+
388
+ stop_at = max_stop.positive? ? stops.filter_map { |stop| text.index(stop, tail_from) }.min : nil
389
+ text.slice!(stop_at..) if stop_at
390
+
391
+ if block
392
+ safe = text.bytesize - (stop_at ? 0 : stop_holdback(text, stops))
393
+
394
+ if safe > n_yielded
395
+ yield(text.byteslice(n_yielded, safe - n_yielded).force_encoding(Encoding::UTF_8))
396
+ n_yielded = safe
397
+ end
398
+ end
399
+
400
+ [stop_at, n_yielded]
401
+ end
402
+
403
+ def stop_holdback(text, stops)
404
+ stops.map do |stop|
405
+ (stop.bytesize - 1).downto(1).find { |n| text.end_with?(stop.byteslice(0, n)) } || 0
406
+ end.max || 0
407
+ end
408
+
409
+ def resolve_sampling(temperature, top_k, top_p, min_p)
410
+ defaults = @model.sampling_defaults
411
+
412
+ [
413
+ temperature.nil? ? defaults[:temperature] : temperature,
414
+ top_k.nil? ? defaults[:top_k] : top_k,
415
+ top_p.nil? ? defaults[:top_p] : top_p,
416
+ min_p.nil? ? defaults[:min_p] : min_p
417
+ ]
418
+ end
419
+
251
420
  def norm(vec)
252
421
  Math.sqrt(vec.sum { |x| x**2 })
253
422
  end
data/lib/rllama/cpp.rb CHANGED
@@ -15,7 +15,11 @@ module Rllama
15
15
  when 'windows', 'mingw32'
16
16
  'x64-mingw32'
17
17
  else
18
- FFI::Platform::ARCH == 'aarch64' ? 'aarch64-linux' : 'x86_64-linux'
18
+ arch = FFI::Platform::ARCH == 'aarch64' ? 'aarch64' : 'x86_64'
19
+
20
+ is_musl = defined?(FFI::Platform::IS_GNU) ? !FFI::Platform::IS_GNU : RbConfig::CONFIG['host_os'].include?('musl')
21
+
22
+ is_musl ? "#{arch}-linux-musl" : "#{arch}-linux"
19
23
  end
20
24
 
21
25
  lib_file =
@@ -324,9 +328,12 @@ module Rllama
324
328
  :kv_overrides, :pointer, # const LlamaModelKvOverride*
325
329
  :vocab_only, :bool,
326
330
  :use_mmap, :bool,
331
+ :use_direct_io, :bool,
327
332
  :use_mlock, :bool,
328
333
  :check_tensors, :bool,
329
- :use_extra_bufts, :bool
334
+ :use_extra_bufts, :bool,
335
+ :no_host, :bool,
336
+ :no_alloc, :bool
330
337
  end
331
338
 
332
339
  class LlamaContextParams < FFI::Struct
@@ -334,8 +341,11 @@ module Rllama
334
341
  :n_batch, :uint32,
335
342
  :n_ubatch, :uint32,
336
343
  :n_seq_max, :uint32,
344
+ :n_rs_seq, :uint32,
345
+ :n_outputs_max, :uint32,
337
346
  :n_threads, :int32,
338
347
  :n_threads_batch, :int32,
348
+ :ctx_type, :int, # enum llama_context_type
339
349
  :rope_scaling_type, :int, # enum llama_rope_scaling_type
340
350
  :pooling_type, :int, # enum llama_pooling_type
341
351
  :attention_type, :int, # enum llama_attention_type
@@ -359,7 +369,9 @@ module Rllama
359
369
  :no_perf, :bool,
360
370
  :op_offload, :bool,
361
371
  :swa_full, :bool,
362
- :kv_unified, :bool
372
+ :kv_unified, :bool,
373
+ :samplers, :pointer,
374
+ :n_samplers, :size_t
363
375
  end
364
376
 
365
377
  class LlamaModelQuantizeParams < FFI::Struct
@@ -372,6 +384,7 @@ module Rllama
372
384
  :only_copy, :bool,
373
385
  :pure, :bool,
374
386
  :keep_split, :bool,
387
+ :dry_run, :bool,
375
388
  :imatrix, :pointer,
376
389
  :kv_overrides, :pointer,
377
390
  :tensor_types, :pointer,
@@ -387,11 +400,6 @@ module Rllama
387
400
  layout :no_perf, :bool
388
401
  end
389
402
 
390
- class LlamaChatMessage < FFI::Struct
391
- layout :role, :pointer,
392
- :content, :pointer
393
- end
394
-
395
403
  class LlamaSamplerI < FFI::Struct; end
396
404
 
397
405
  class LlamaSampler < FFI::Struct
@@ -533,10 +541,8 @@ module Rllama
533
541
  attach_function :llama_adapter_lora_free, [:llama_adapter_lora_p], :void
534
542
  attach_function :llama_adapter_get_alora_n_invocation_tokens, [:llama_adapter_lora_p], :uint64
535
543
  attach_function :llama_adapter_get_alora_invocation_tokens, [:llama_adapter_lora_p], :pointer # const llama_token*
536
- attach_function :llama_set_adapter_lora, %i[llama_context_p llama_adapter_lora_p float], :int32
537
- attach_function :llama_rm_adapter_lora, %i[llama_context_p llama_adapter_lora_p], :int32
538
- attach_function :llama_clear_adapter_lora, [:llama_context_p], :void
539
- attach_function :llama_apply_adapter_cvec, %i[llama_context_p pointer size_t int32 int32 int32], :int32
544
+ attach_function :llama_set_adapters_lora, %i[llama_context_p pointer size_t pointer], :int32
545
+ attach_function :llama_set_adapter_cvec, %i[llama_context_p pointer size_t int32 int32 int32], :int32
540
546
 
541
547
  # Memory management
542
548
  attach_function :llama_memory_clear, %i[llama_memory_t bool], :void
@@ -619,10 +625,6 @@ module Rllama
619
625
  attach_function :llama_token_to_piece, %i[llama_vocab_p llama_token pointer int32 int32 bool], :int32
620
626
  attach_function :llama_detokenize, %i[llama_vocab_p pointer int32 pointer int32 bool bool], :int32
621
627
 
622
- # Chat templates
623
- attach_function :llama_chat_apply_template, %i[string pointer size_t bool pointer int32], :int32
624
- attach_function :llama_chat_builtin_templates, %i[pointer size_t], :int32
625
-
626
628
  # Sampling API
627
629
  attach_function :llama_sampler_init, %i[pointer llama_sampler_context_t], :llama_sampler_p
628
630
  attach_function :llama_sampler_name, [:llama_sampler_p], :string
data/lib/rllama/model.rb CHANGED
@@ -4,6 +4,8 @@ module Rllama
4
4
  class Model
5
5
  DEFAULT_CONTEXT_LENGTH = 2**13
6
6
 
7
+ DEFAULT_SAMPLING = { temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05 }.freeze
8
+
7
9
  attr_reader :pointer
8
10
 
9
11
  def initialize(path_or_name, dir: nil)
@@ -36,11 +38,35 @@ module Rllama
36
38
  @n_ctx_train ||= Cpp.llama_model_n_ctx_train(@pointer)
37
39
  end
38
40
 
39
- def generate(prompt, max_tokens: DEFAULT_CONTEXT_LENGTH, temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05,
40
- seed: nil, system: nil, &block)
41
+ def meta(key)
42
+ buffer = FFI::MemoryPointer.new(:char, 256)
43
+ length = Cpp.llama_model_meta_val_str(@pointer, key.to_s, buffer, buffer.size)
44
+
45
+ length.negative? ? nil : buffer.read_string
46
+ end
47
+
48
+ def sampling_defaults
49
+ @sampling_defaults ||= {
50
+ temperature: fetch_meta_float('general.sampling.temp') || DEFAULT_SAMPLING[:temperature],
51
+ top_k: fetch_meta_int('general.sampling.top_k') || DEFAULT_SAMPLING[:top_k],
52
+ top_p: fetch_meta_float('general.sampling.top_p') || DEFAULT_SAMPLING[:top_p],
53
+ min_p: fetch_meta_float('general.sampling.min_p') || DEFAULT_SAMPLING[:min_p]
54
+ }
55
+ end
56
+
57
+ def bos_token
58
+ @bos_token ||= token_to_string(Cpp.llama_vocab_bos(vocab))
59
+ end
60
+
61
+ def eos_token
62
+ @eos_token ||= token_to_string(Cpp.llama_vocab_eos(vocab))
63
+ end
64
+
65
+ def generate(prompt, max_tokens: DEFAULT_CONTEXT_LENGTH, temperature: nil, top_k: nil, top_p: nil, min_p: nil,
66
+ seed: nil, system: nil, tools: nil, reasoning: nil, &block)
41
67
  init_context(n_ctx: max_tokens) do |ctx|
42
68
  ctx.generate(prompt, max_tokens: ctx.n_ctx,
43
- temperature:, top_k:, top_p:, seed:, system:, min_p:,
69
+ temperature:, top_k:, top_p:, seed:, system:, min_p:, tools:, reasoning:,
44
70
  &block)
45
71
  end
46
72
  end
@@ -74,11 +100,18 @@ module Rllama
74
100
  end
75
101
 
76
102
  def close
103
+ if @chat_templates
104
+ Common.chat_templates_free(@chat_templates)
105
+
106
+ @chat_templates = nil
107
+ end
108
+
77
109
  Cpp.llama_model_free(@pointer)
78
110
  end
79
111
 
80
- def init_context(embeddings: false, n_ctx: DEFAULT_CONTEXT_LENGTH, n_batch: 512)
81
- context = Context.new(self, embeddings:, n_ctx:, n_batch:)
112
+ def init_context(embeddings: false, n_ctx: DEFAULT_CONTEXT_LENGTH, n_batch: 512, system: nil, tools: nil,
113
+ reasoning: false)
114
+ context = Context.new(self, embeddings:, n_ctx:, n_batch:, system:, tools:, reasoning:)
82
115
 
83
116
  if block_given?
84
117
  result = yield context
@@ -95,30 +128,43 @@ module Rllama
95
128
  init_context(embeddings: true, n_ctx:, n_batch:, &)
96
129
  end
97
130
 
98
- def build_chat_template(messages)
131
+ def apply_chat_template(messages, tools: nil, add_generation_prompt: true, enable_thinking: false)
99
132
  raise Error, 'Model does not provide a chat template' if chat_template.nil? || chat_template.empty?
100
133
 
101
- count = messages.length
102
- struct_size = Cpp::LlamaChatMessage.size
103
- array_ptr = FFI::MemoryPointer.new(struct_size * count)
134
+ Common.chat_apply(chat_templates, messages:, tools:, add_generation_prompt:, enable_thinking:,
135
+ add_bos: Cpp.llama_vocab_get_add_bos(vocab),
136
+ add_eos: Cpp.llama_vocab_get_add_eos(vocab))
137
+ end
104
138
 
105
- messages.each_with_index do |m, i|
106
- struct_ptr = array_ptr + (i * struct_size)
107
- msg_struct = Cpp::LlamaChatMessage.new(struct_ptr)
108
- msg_struct[:role] = FFI::MemoryPointer.from_string(m[:role].to_s)
109
- msg_struct[:content] = FFI::MemoryPointer.from_string(m[:content].to_s)
110
- end
139
+ def build_chat_template(messages, tools: nil, add_generation_prompt: true, enable_thinking: false)
140
+ apply_chat_template(messages, tools:, add_generation_prompt:, enable_thinking:)['prompt']
141
+ end
142
+
143
+ private
111
144
 
112
- needed = Cpp.llama_chat_apply_template(chat_template, array_ptr, count, true, nil, 0)
145
+ def fetch_meta_float(key)
146
+ value = meta(key)
113
147
 
114
- raise Error, 'Failed to apply chat template' if needed.negative?
148
+ value && Float(value, exception: false)
149
+ end
115
150
 
116
- buf = FFI::MemoryPointer.new(:char, needed)
117
- written = Cpp.llama_chat_apply_template(chat_template, array_ptr, count, true, buf, needed)
151
+ def fetch_meta_int(key)
152
+ value = meta(key)
153
+
154
+ return nil unless value
155
+
156
+ Integer(value, exception: false) || Float(value, exception: false)&.to_i
157
+ end
158
+
159
+ def chat_templates
160
+ @chat_templates ||= Common.chat_templates_init(@pointer, chat_template, bos_token, eos_token)
161
+ end
118
162
 
119
- raise Error, 'Failed to apply chat template' if written.negative?
163
+ def token_to_string(token_id)
164
+ buf = FFI::MemoryPointer.new(:char, 256)
165
+ n = Cpp.llama_token_to_piece(vocab, token_id, buf, buf.size, 0, true)
120
166
 
121
- buf.read_string(written)
167
+ n.positive? ? buf.read_string(n) : ''
122
168
  end
123
169
  end
124
170
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rllama
4
- VERSION = '1.0.3'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/rllama.rb CHANGED
@@ -5,10 +5,11 @@ module Rllama
5
5
  autoload :Loader, 'rllama/loader'
6
6
  autoload :Context, 'rllama/context'
7
7
  autoload :Cpp, 'rllama/cpp'
8
+ autoload :Common, 'rllama/common'
8
9
  autoload :Cli, 'rllama/cli'
9
10
  autoload :VERSION, 'rllama/version'
10
11
 
11
- Result = Struct.new(:text, :stats, keyword_init: true)
12
+ Result = Struct.new(:text, :reasoning, :tool_calls, :stats, keyword_init: true)
12
13
  Error = Class.new(StandardError)
13
14
 
14
15
  module_function