rllama 1.1.0-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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +18 -0
  3. data/README.md +100 -8
  4. data/lib/rllama/aarch64-linux/libggml-base.so +1 -0
  5. data/lib/rllama/aarch64-linux/libggml-base.so.0 +1 -0
  6. data/lib/rllama/aarch64-linux/libggml-base.so.0.17.0 +0 -0
  7. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.0_1.so +0 -0
  8. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_1.so +0 -0
  9. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_2.so +0 -0
  10. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.2_3.so +0 -0
  11. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.6_1.so +0 -0
  12. data/lib/rllama/aarch64-linux/libggml-cpu-armv8.6_2.so +0 -0
  13. data/lib/rllama/aarch64-linux/libggml-cpu-armv9.2_1.so +0 -0
  14. data/lib/rllama/aarch64-linux/libggml-cpu-armv9.2_2.so +0 -0
  15. data/lib/rllama/aarch64-linux/libggml-rpc.so +0 -0
  16. data/lib/rllama/aarch64-linux/libggml.so +1 -0
  17. data/lib/rllama/aarch64-linux/libggml.so.0 +1 -0
  18. data/lib/rllama/aarch64-linux/libggml.so.0.17.0 +0 -0
  19. data/lib/rllama/aarch64-linux/libllama-common.so +1 -0
  20. data/lib/rllama/aarch64-linux/libllama-common.so.0 +1 -0
  21. data/lib/rllama/aarch64-linux/libllama-common.so.0.0.10067 +0 -0
  22. data/lib/rllama/aarch64-linux/libllama.so +1 -0
  23. data/lib/rllama/aarch64-linux/libllama.so.0 +1 -0
  24. data/lib/rllama/aarch64-linux/libllama.so.0.0.10067 +0 -0
  25. data/lib/rllama/aarch64-linux/libllama_common.so +0 -0
  26. data/lib/rllama/common.rb +134 -0
  27. data/lib/rllama/context.rb +220 -51
  28. data/lib/rllama/cpp.rb +8 -10
  29. data/lib/rllama/model.rb +56 -58
  30. data/lib/rllama/version.rb +1 -1
  31. data/lib/rllama.rb +2 -1
  32. data/licenses/LICENSE-boringssl +238 -0
  33. data/licenses/LICENSE-cpp-httplib +22 -0
  34. data/licenses/LICENSE-json +21 -0
  35. data/licenses/LICENSE-llama.cpp +21 -0
  36. metadata +18 -11
  37. data/lib/rllama/aarch64-linux/libggml-base.so +0 -0
  38. data/lib/rllama/aarch64-linux/libggml-cpu-alderlake.so +0 -0
  39. data/lib/rllama/aarch64-linux/libggml-cpu-haswell.so +0 -0
  40. data/lib/rllama/aarch64-linux/libggml-cpu-icelake.so +0 -0
  41. data/lib/rllama/aarch64-linux/libggml-cpu-sandybridge.so +0 -0
  42. data/lib/rllama/aarch64-linux/libggml-cpu-sapphirerapids.so +0 -0
  43. data/lib/rllama/aarch64-linux/libggml-cpu-skylakex.so +0 -0
  44. data/lib/rllama/aarch64-linux/libggml-cpu-sse42.so +0 -0
  45. data/lib/rllama/aarch64-linux/libggml-cpu-x64.so +0 -0
  46. data/lib/rllama/aarch64-linux/libggml-cpu.so +0 -0
  47. data/lib/rllama/aarch64-linux/libggml.so +0 -0
  48. 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
@@ -328,9 +328,12 @@ module Rllama
328
328
  :kv_overrides, :pointer, # const LlamaModelKvOverride*
329
329
  :vocab_only, :bool,
330
330
  :use_mmap, :bool,
331
+ :use_direct_io, :bool,
331
332
  :use_mlock, :bool,
332
333
  :check_tensors, :bool,
333
- :use_extra_bufts, :bool
334
+ :use_extra_bufts, :bool,
335
+ :no_host, :bool,
336
+ :no_alloc, :bool
334
337
  end
335
338
 
336
339
  class LlamaContextParams < FFI::Struct
@@ -338,8 +341,11 @@ module Rllama
338
341
  :n_batch, :uint32,
339
342
  :n_ubatch, :uint32,
340
343
  :n_seq_max, :uint32,
344
+ :n_rs_seq, :uint32,
345
+ :n_outputs_max, :uint32,
341
346
  :n_threads, :int32,
342
347
  :n_threads_batch, :int32,
348
+ :ctx_type, :int, # enum llama_context_type
343
349
  :rope_scaling_type, :int, # enum llama_rope_scaling_type
344
350
  :pooling_type, :int, # enum llama_pooling_type
345
351
  :attention_type, :int, # enum llama_attention_type
@@ -378,6 +384,7 @@ module Rllama
378
384
  :only_copy, :bool,
379
385
  :pure, :bool,
380
386
  :keep_split, :bool,
387
+ :dry_run, :bool,
381
388
  :imatrix, :pointer,
382
389
  :kv_overrides, :pointer,
383
390
  :tensor_types, :pointer,
@@ -393,11 +400,6 @@ module Rllama
393
400
  layout :no_perf, :bool
394
401
  end
395
402
 
396
- class LlamaChatMessage < FFI::Struct
397
- layout :role, :pointer,
398
- :content, :pointer
399
- end
400
-
401
403
  class LlamaSamplerI < FFI::Struct; end
402
404
 
403
405
  class LlamaSampler < FFI::Struct
@@ -623,10 +625,6 @@ module Rllama
623
625
  attach_function :llama_token_to_piece, %i[llama_vocab_p llama_token pointer int32 int32 bool], :int32
624
626
  attach_function :llama_detokenize, %i[llama_vocab_p pointer int32 pointer int32 bool bool], :int32
625
627
 
626
- # Chat templates
627
- attach_function :llama_chat_apply_template, %i[string pointer size_t bool pointer int32], :int32
628
- attach_function :llama_chat_builtin_templates, %i[pointer size_t], :int32
629
-
630
628
  # Sampling API
631
629
  attach_function :llama_sampler_init, %i[pointer llama_sampler_context_t], :llama_sampler_p
632
630
  attach_function :llama_sampler_name, [:llama_sampler_p], :string
data/lib/rllama/model.rb CHANGED
@@ -4,15 +4,7 @@ module Rllama
4
4
  class Model
5
5
  DEFAULT_CONTEXT_LENGTH = 2**13
6
6
 
7
- FALLBACK_TEMPLATES = {
8
- 'gemma4' => {
9
- bos: '<bos>',
10
- role_map: { 'assistant' => 'model' },
11
- turn_start: ->(role) { "<|turn>#{role}\n" },
12
- turn_end: "<turn|>\n",
13
- generation_prompt: "<|turn>model\n"
14
- }
15
- }.freeze
7
+ DEFAULT_SAMPLING = { temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05 }.freeze
16
8
 
17
9
  attr_reader :pointer
18
10
 
@@ -46,21 +38,35 @@ module Rllama
46
38
  @n_ctx_train ||= Cpp.llama_model_n_ctx_train(@pointer)
47
39
  end
48
40
 
49
- def architecture
50
- @architecture ||= begin
51
- buf = FFI::MemoryPointer.new(:char, 256)
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)
52
44
 
53
- n = Cpp.llama_model_meta_val_str(@pointer, 'general.architecture', buf, 256)
45
+ length.negative? ? nil : buffer.read_string
46
+ end
54
47
 
55
- n.positive? ? buf.read_string(n) : nil
56
- end
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))
57
59
  end
58
60
 
59
- def generate(prompt, max_tokens: DEFAULT_CONTEXT_LENGTH, temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05,
60
- seed: nil, system: nil, &block)
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)
61
67
  init_context(n_ctx: max_tokens) do |ctx|
62
68
  ctx.generate(prompt, max_tokens: ctx.n_ctx,
63
- temperature:, top_k:, top_p:, seed:, system:, min_p:,
69
+ temperature:, top_k:, top_p:, seed:, system:, min_p:, tools:, reasoning:,
64
70
  &block)
65
71
  end
66
72
  end
@@ -94,11 +100,18 @@ module Rllama
94
100
  end
95
101
 
96
102
  def close
103
+ if @chat_templates
104
+ Common.chat_templates_free(@chat_templates)
105
+
106
+ @chat_templates = nil
107
+ end
108
+
97
109
  Cpp.llama_model_free(@pointer)
98
110
  end
99
111
 
100
- def init_context(embeddings: false, n_ctx: DEFAULT_CONTEXT_LENGTH, n_batch: 512)
101
- 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:)
102
115
 
103
116
  if block_given?
104
117
  result = yield context
@@ -115,58 +128,43 @@ module Rllama
115
128
  init_context(embeddings: true, n_ctx:, n_batch:, &)
116
129
  end
117
130
 
118
- def build_chat_template(messages)
131
+ def apply_chat_template(messages, tools: nil, add_generation_prompt: true, enable_thinking: false)
119
132
  raise Error, 'Model does not provide a chat template' if chat_template.nil? || chat_template.empty?
120
133
 
121
- result = apply_chat_template(messages)
122
-
123
- return result if result
124
-
125
- apply_chat_template_fallback(messages)
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))
126
137
  end
127
138
 
128
- def apply_chat_template(messages)
129
- count = messages.length
130
- struct_size = Cpp::LlamaChatMessage.size
131
- array_ptr = FFI::MemoryPointer.new(struct_size * count)
132
-
133
- messages.each_with_index do |m, i|
134
- struct_ptr = array_ptr + (i * struct_size)
135
- msg_struct = Cpp::LlamaChatMessage.new(struct_ptr)
136
- msg_struct[:role] = FFI::MemoryPointer.from_string(m[:role].to_s)
137
- msg_struct[:content] = FFI::MemoryPointer.from_string(m[:content].to_s)
138
- end
139
-
140
- needed = Cpp.llama_chat_apply_template(chat_template, array_ptr, count, true, nil, 0)
141
-
142
- return nil if needed.negative?
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
143
142
 
144
- buf = FFI::MemoryPointer.new(:char, needed)
145
- written = Cpp.llama_chat_apply_template(chat_template, array_ptr, count, true, buf, needed)
143
+ private
146
144
 
147
- return nil if written.negative?
145
+ def fetch_meta_float(key)
146
+ value = meta(key)
148
147
 
149
- buf.read_string(written)
148
+ value && Float(value, exception: false)
150
149
  end
151
150
 
152
- def apply_chat_template_fallback(messages)
153
- tmpl = FALLBACK_TEMPLATES[architecture]
151
+ def fetch_meta_int(key)
152
+ value = meta(key)
154
153
 
155
- raise Error, "Unsupported chat template for architecture: #{architecture || 'unknown'}" unless tmpl
154
+ return nil unless value
156
155
 
157
- result = String.new(tmpl[:bos] || '')
158
- role_map = tmpl[:role_map] || {}
156
+ Integer(value, exception: false) || Float(value, exception: false)&.to_i
157
+ end
159
158
 
160
- messages.each do |m|
161
- role = role_map[m[:role].to_s] || m[:role].to_s
162
- result << tmpl[:turn_start].call(role)
163
- result << m[:content].to_s
164
- result << tmpl[:turn_end]
165
- end
159
+ def chat_templates
160
+ @chat_templates ||= Common.chat_templates_init(@pointer, chat_template, bos_token, eos_token)
161
+ end
166
162
 
167
- result << tmpl[:generation_prompt]
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)
168
166
 
169
- result
167
+ n.positive? ? buf.read_string(n) : ''
170
168
  end
171
169
  end
172
170
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rllama
4
- VERSION = '1.1.0'
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