llmtrim 0.1.10-arm64-darwin

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 371a1841e32aa99efe6b330d55e589357e2a9349413a3bdefa6f6239eae59592
4
+ data.tar.gz: 2a941fa33b123f62b2574cafa81f219f6cada9221a9111dbb1db3e7f4f24f8a1
5
+ SHA512:
6
+ metadata.gz: aaf480cc6694d87d992290a9f087f9f96c317983896a219f248a5c55cc6180f48c7c04f39a0bbbdffcac0ded343d7a492ad5ae02b7fa4d6c44eaf1cd189fb6c4
7
+ data.tar.gz: 3ba67355d060d10e136230c03c526ba58a4d03f87eec36433f80e5257994c121320a81a96d547f184d4fb2643142ad369455a71a6912b150a38fe760cc1b075a
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # llmtrim (Ruby)
2
+
3
+ Native, in-process bindings to the [llmtrim](https://github.com/fkiene/llmtrim)
4
+ compression engine, cutting LLM input tokens 30–90% with zero extra model calls, no network,
5
+ no server. The compiled engine is bundled in the gem, so no Rust toolchain is needed.
6
+
7
+ ```ruby
8
+ require "llmtrim"
9
+ require "json"
10
+
11
+ req = JSON.generate("model" => "gpt-4o",
12
+ "messages" => [{ "role" => "user", "content" => "…" }])
13
+ out = Llmtrim.compress(req, Llmtrim::Provider::OPEN_AI, "aggressive")
14
+ puts "#{out.input_tokens_before} -> #{out.input_tokens_after}"
15
+ # send out.request_json to the provider
16
+ ```
17
+
18
+ `compress(input, provider, preset)`: `provider` is `Llmtrim::Provider::OPEN_AI` /
19
+ `ANTHROPIC` / `GOOGLE` or `nil` to auto-detect; `preset` is a workload name
20
+ (`"aggressive"`, `"agent"`, `"code"`, `"rag"`, `"safe"`, …) or `nil` for the environment
21
+ config. Raises `Llmtrim::LlmtrimError::Compress` / `UnknownPreset` on error.
22
+
23
+ Built with `crates/llmtrim-uniffi/scripts/build-gem.sh` (platform-specific gem with the
24
+ bundled native library). License: AGPL-3.0-only.
Binary file
@@ -0,0 +1,865 @@
1
+ # This file was autogenerated by some hot garbage in the `uniffi` crate.
2
+ # Trust me, you don't want to mess with it!
3
+
4
+ # Common helper code.
5
+ #
6
+ # Ideally this would live in a separate .rb file where it can be unittested etc
7
+ # in isolation, and perhaps even published as a re-useable package.
8
+ #
9
+ # However, it's important that the details of how this helper code works (e.g. the
10
+ # way that different builtin types are passed across the FFI) exactly match what's
11
+ # expected by the rust code on the other side of the interface. In practice right
12
+ # now that means coming from the exact some version of `uniffi` that was used to
13
+ # compile the rust component. The easiest way to ensure this is to bundle the Ruby
14
+ # helpers directly inline like we're doing here.
15
+
16
+ require 'ffi'
17
+
18
+
19
+ module LlmtrimFfi
20
+ def self.uniffi_in_range(i, type_name, min, max)
21
+ raise TypeError, "no implicit conversion of #{i} into Integer" unless i.respond_to?(:to_int)
22
+ i = i.to_int
23
+ raise RangeError, "#{type_name} requires #{min} <= value < #{max}" unless (min <= i && i < max)
24
+ i
25
+ end
26
+
27
+ def self.uniffi_utf8(v)
28
+ raise TypeError, "no implicit conversion of #{v} into String" unless v.respond_to?(:to_str)
29
+ v = v.to_str.encode(Encoding::UTF_8)
30
+ raise Encoding::InvalidByteSequenceError, "not a valid UTF-8 encoded string" unless v.valid_encoding?
31
+ v
32
+ end
33
+
34
+ def self.uniffi_bytes(v)
35
+ raise TypeError, "no implicit conversion of #{v} into String" unless v.respond_to?(:to_str)
36
+ v.to_str
37
+ end
38
+
39
+ class RustBuffer < FFI::Struct
40
+ layout :capacity, :uint64,
41
+ :len, :uint64,
42
+ :data, :pointer
43
+
44
+ def self.alloc(size)
45
+ return LlmtrimFfi.rust_call(:ffi_llmtrim_ffi_rustbuffer_alloc, size)
46
+ end
47
+
48
+ def self.reserve(rbuf, additional)
49
+ return LlmtrimFfi.rust_call(:ffi_llmtrim_ffi_rustbuffer_reserve, rbuf, additional)
50
+ end
51
+
52
+ def free
53
+ LlmtrimFfi.rust_call(:ffi_llmtrim_ffi_rustbuffer_free, self)
54
+ end
55
+
56
+ def capacity
57
+ self[:capacity]
58
+ end
59
+
60
+ def len
61
+ self[:len]
62
+ end
63
+
64
+ def len=(value)
65
+ self[:len] = value
66
+ end
67
+
68
+ def data
69
+ self[:data]
70
+ end
71
+
72
+ def to_s
73
+ "RustBuffer(capacity=#{capacity}, len=#{len}, data=#{data.read_bytes len})"
74
+ end
75
+
76
+ # The allocated buffer will be automatically freed if an error occurs, ensuring that
77
+ # we don't accidentally leak it.
78
+ def self.allocWithBuilder
79
+ builder = RustBufferBuilder.new
80
+
81
+ begin
82
+ yield builder
83
+ rescue => e
84
+ builder.discard
85
+ raise e
86
+ end
87
+ end
88
+
89
+ # The RustBuffer will be freed once the context-manager exits, ensuring that we don't
90
+ # leak it even if an error occurs.
91
+ def consumeWithStream
92
+ stream = RustBufferStream.new self
93
+
94
+ yield stream
95
+
96
+ raise RuntimeError, 'junk data left in buffer after consuming' if stream.remaining != 0
97
+ ensure
98
+ free
99
+ end# The primitive String type.
100
+
101
+ def self.allocFromString(value)
102
+ RustBuffer.allocWithBuilder do |builder|
103
+ builder.write value.encode('utf-8')
104
+ return builder.finalize
105
+ end
106
+ end
107
+
108
+ def consumeIntoString
109
+ consumeWithStream do |stream|
110
+ return stream.read(stream.remaining).force_encoding(Encoding::UTF_8)
111
+ end
112
+ end
113
+
114
+ # The Record type CompressOutput.
115
+
116
+ def self.check_lower_TypeCompressOutput(v)
117
+
118
+
119
+ RustBuffer.check_lower_Optionalstring(v.model)
120
+
121
+
122
+
123
+
124
+
125
+
126
+ RustBuffer.check_lower_SequenceTypeStageReport(v.stages)
127
+ end
128
+
129
+ def self.alloc_from_TypeCompressOutput(v)
130
+ RustBuffer.allocWithBuilder do |builder|
131
+ builder.write_TypeCompressOutput(v)
132
+ return builder.finalize
133
+ end
134
+ end
135
+
136
+ def consumeIntoTypeCompressOutput
137
+ consumeWithStream do |stream|
138
+ return stream.readTypeCompressOutput
139
+ end
140
+ end
141
+
142
+ # The Record type StageReport.
143
+
144
+ def self.check_lower_TypeStageReport(v)
145
+
146
+
147
+
148
+
149
+ RustBuffer.check_lower_Optionalstring(v.note)
150
+ end
151
+
152
+ def self.alloc_from_TypeStageReport(v)
153
+ RustBuffer.allocWithBuilder do |builder|
154
+ builder.write_TypeStageReport(v)
155
+ return builder.finalize
156
+ end
157
+ end
158
+
159
+ def consumeIntoTypeStageReport
160
+ consumeWithStream do |stream|
161
+ return stream.readTypeStageReport
162
+ end
163
+ end
164
+
165
+
166
+
167
+ # The Enum type Provider.
168
+
169
+ def self.check_lower_TypeProvider(v)
170
+ end
171
+
172
+ def self.alloc_from_TypeProvider(v)
173
+ RustBuffer.allocWithBuilder do |builder|
174
+ builder.write_TypeProvider(v)
175
+ return builder.finalize
176
+ end
177
+ end
178
+
179
+ def consumeIntoTypeProvider
180
+ consumeWithStream do |stream|
181
+ return stream.readTypeProvider
182
+ end
183
+ end
184
+
185
+
186
+ # The Optional<T> type for string.
187
+
188
+ def self.check_lower_Optionalstring(v)
189
+ if not v.nil?
190
+
191
+ end
192
+ end
193
+
194
+ def self.alloc_from_Optionalstring(v)
195
+ RustBuffer.allocWithBuilder do |builder|
196
+ builder.write_Optionalstring(v)
197
+ return builder.finalize()
198
+ end
199
+ end
200
+
201
+ def consumeIntoOptionalstring
202
+ consumeWithStream do |stream|
203
+ return stream.readOptionalstring
204
+ end
205
+ end
206
+
207
+ # The Optional<T> type for TypeProvider.
208
+
209
+ def self.check_lower_OptionalTypeProvider(v)
210
+ if not v.nil?
211
+ RustBuffer.check_lower_TypeProvider(v)
212
+ end
213
+ end
214
+
215
+ def self.alloc_from_OptionalTypeProvider(v)
216
+ RustBuffer.allocWithBuilder do |builder|
217
+ builder.write_OptionalTypeProvider(v)
218
+ return builder.finalize()
219
+ end
220
+ end
221
+
222
+ def consumeIntoOptionalTypeProvider
223
+ consumeWithStream do |stream|
224
+ return stream.readOptionalTypeProvider
225
+ end
226
+ end
227
+
228
+ # The Sequence<T> type for TypeStageReport.
229
+
230
+ def self.check_lower_SequenceTypeStageReport(v)
231
+ v.each do |item|
232
+ RustBuffer.check_lower_TypeStageReport(item)
233
+ end
234
+ end
235
+
236
+ def self.alloc_from_SequenceTypeStageReport(v)
237
+ RustBuffer.allocWithBuilder do |builder|
238
+ builder.write_SequenceTypeStageReport(v)
239
+ return builder.finalize()
240
+ end
241
+ end
242
+
243
+ def consumeIntoSequenceTypeStageReport
244
+ consumeWithStream do |stream|
245
+ return stream.readSequenceTypeStageReport
246
+ end
247
+ end
248
+
249
+
250
+ end
251
+
252
+ module UniFFILib
253
+ class ForeignBytes < FFI::Struct
254
+ layout :len, :int32,
255
+ :data, :pointer
256
+
257
+ def len
258
+ self[:len]
259
+ end
260
+
261
+ def data
262
+ self[:data]
263
+ end
264
+
265
+ def to_s
266
+ "ForeignBytes(len=#{len}, data=#{data.read_bytes(len)})"
267
+ end
268
+ end
269
+ end
270
+
271
+ private_constant :UniFFILib
272
+
273
+ # Helper for structured reading of values from a RustBuffer.
274
+ class RustBufferStream
275
+
276
+ def initialize(rbuf)
277
+ @rbuf = rbuf
278
+ @offset = 0
279
+ end
280
+
281
+ def remaining
282
+ @rbuf.len - @offset
283
+ end
284
+
285
+ def read(size)
286
+ raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len
287
+
288
+ data = @rbuf.data.get_bytes @offset, size
289
+
290
+ @offset += size
291
+
292
+ data
293
+ end
294
+
295
+ def readU64
296
+ unpack_from 8, 'Q>'
297
+ end
298
+
299
+ def readBool
300
+ v = unpack_from 1, 'c'
301
+
302
+ return false if v == 0
303
+ return true if v == 1
304
+
305
+ raise InternalError, 'Unexpected byte for Boolean type'
306
+ end
307
+
308
+ def readString
309
+ size = unpack_from 4, 'l>'
310
+
311
+ raise InternalError, 'Unexpected negative string length' if size.negative?
312
+
313
+ read(size).force_encoding(Encoding::UTF_8)
314
+ end
315
+
316
+ # The Record type CompressOutput.
317
+
318
+ def readTypeCompressOutput
319
+ CompressOutput.new(
320
+ request_json: readString,
321
+ provider: readString,
322
+ model: readOptionalstring,
323
+ tokenizer_label: readString,
324
+ tokenizer_exact: readBool,
325
+ input_tokens_before: readU64,
326
+ input_tokens_after: readU64,
327
+ frozen_input_tokens: readU64,
328
+ output_shaped: readBool,
329
+ stages: readSequenceTypeStageReport
330
+ )
331
+ end
332
+
333
+ # The Record type StageReport.
334
+
335
+ def readTypeStageReport
336
+ StageReport.new(
337
+ name: readString,
338
+ applied: readBool,
339
+ tokens_before: readU64,
340
+ tokens_after: readU64,
341
+ note: readOptionalstring
342
+ )
343
+ end
344
+
345
+
346
+
347
+
348
+
349
+ # The Error type LlmtrimError
350
+
351
+ def readTypeLlmtrimError
352
+ variant = unpack_from 4, 'l>'
353
+
354
+ if variant == 1
355
+ return LlmtrimError::Compress.new(
356
+ readString()
357
+ )
358
+ end
359
+ if variant == 2
360
+ return LlmtrimError::UnknownPreset.new(
361
+ readString()
362
+ )
363
+ end
364
+
365
+ raise InternalError, 'Unexpected variant tag for TypeLlmtrimError'
366
+ end
367
+
368
+
369
+
370
+
371
+ # The Enum type Provider.
372
+
373
+ def readTypeProvider
374
+ variant = unpack_from 4, 'l>'
375
+
376
+ if variant == 1
377
+ return Provider::OPEN_AI
378
+ end
379
+ if variant == 2
380
+ return Provider::ANTHROPIC
381
+ end
382
+ if variant == 3
383
+ return Provider::GOOGLE
384
+ end
385
+
386
+ raise InternalError, 'Unexpected variant tag for TypeProvider'
387
+ end
388
+
389
+
390
+
391
+ # The Optional<T> type for string.
392
+
393
+ def readOptionalstring
394
+ flag = unpack_from 1, 'c'
395
+
396
+ if flag == 0
397
+ return nil
398
+ elsif flag == 1
399
+ return readString
400
+ else
401
+ raise InternalError, 'Unexpected flag byte for Optionalstring'
402
+ end
403
+ end
404
+
405
+ # The Optional<T> type for TypeProvider.
406
+
407
+ def readOptionalTypeProvider
408
+ flag = unpack_from 1, 'c'
409
+
410
+ if flag == 0
411
+ return nil
412
+ elsif flag == 1
413
+ return readTypeProvider
414
+ else
415
+ raise InternalError, 'Unexpected flag byte for OptionalTypeProvider'
416
+ end
417
+ end
418
+
419
+ # The Sequence<T> type for TypeStageReport.
420
+
421
+ def readSequenceTypeStageReport
422
+ count = unpack_from 4, 'l>'
423
+
424
+ raise InternalError, 'Unexpected negative sequence length' if count.negative?
425
+
426
+ items = []
427
+
428
+ count.times do
429
+ items.append readTypeStageReport
430
+ end
431
+
432
+ items
433
+ end
434
+
435
+
436
+
437
+ def unpack_from(size, format)
438
+ raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len
439
+
440
+ value = @rbuf.data.get_bytes(@offset, size).unpack format
441
+
442
+ @offset += size
443
+
444
+ # TODO: verify this
445
+ raise 'more than one element!!!' if value.size > 1
446
+
447
+ value[0]
448
+ end
449
+ end
450
+
451
+ private_constant :RustBufferStream
452
+
453
+ # Helper for structured writing of values into a RustBuffer.
454
+ class RustBufferBuilder
455
+ def initialize
456
+ @rust_buf = RustBuffer.alloc 16
457
+ @rust_buf.len = 0
458
+ end
459
+
460
+ def finalize
461
+ rbuf = @rust_buf
462
+
463
+ @rust_buf = nil
464
+
465
+ rbuf
466
+ end
467
+
468
+ def discard
469
+ return if @rust_buf.nil?
470
+
471
+ rbuf = finalize
472
+ rbuf.free
473
+ end
474
+
475
+ def write(value)
476
+ reserve(value.bytes.size) do
477
+ @rust_buf.data.put_array_of_char @rust_buf.len, value.bytes
478
+ end
479
+ end
480
+
481
+ def write_U64(v)
482
+ v = LlmtrimFfi::uniffi_in_range(v, "u64", 0, 2**64)
483
+ pack_into(8, 'Q>', v)
484
+ end
485
+
486
+ def write_Bool(v)
487
+ pack_into(1, 'c', v ? 1 : 0)
488
+ end
489
+
490
+ def write_String(v)
491
+ v = LlmtrimFfi::uniffi_utf8(v)
492
+ pack_into 4, 'l>', v.bytes.size
493
+ write v
494
+ end
495
+
496
+ # The Record type CompressOutput.
497
+
498
+ def write_TypeCompressOutput(v)
499
+ self.write_String(v.request_json)
500
+ self.write_String(v.provider)
501
+ self.write_Optionalstring(v.model)
502
+ self.write_String(v.tokenizer_label)
503
+ self.write_Bool(v.tokenizer_exact)
504
+ self.write_U64(v.input_tokens_before)
505
+ self.write_U64(v.input_tokens_after)
506
+ self.write_U64(v.frozen_input_tokens)
507
+ self.write_Bool(v.output_shaped)
508
+ self.write_SequenceTypeStageReport(v.stages)
509
+ end
510
+
511
+ # The Record type StageReport.
512
+
513
+ def write_TypeStageReport(v)
514
+ self.write_String(v.name)
515
+ self.write_Bool(v.applied)
516
+ self.write_U64(v.tokens_before)
517
+ self.write_U64(v.tokens_after)
518
+ self.write_Optionalstring(v.note)
519
+ end
520
+
521
+
522
+
523
+ # The Enum type Provider.
524
+
525
+ def write_TypeProvider(v)
526
+ pack_into(4, 'l>', v)
527
+ end
528
+
529
+
530
+ # The Optional<T> type for string.
531
+
532
+ def write_Optionalstring(v)
533
+ if v.nil?
534
+ pack_into(1, 'c', 0)
535
+ else
536
+ pack_into(1, 'c', 1)
537
+ self.write_String(v)
538
+ end
539
+ end
540
+
541
+ # The Optional<T> type for TypeProvider.
542
+
543
+ def write_OptionalTypeProvider(v)
544
+ if v.nil?
545
+ pack_into(1, 'c', 0)
546
+ else
547
+ pack_into(1, 'c', 1)
548
+ self.write_TypeProvider(v)
549
+ end
550
+ end
551
+
552
+ # The Sequence<T> type for TypeStageReport.
553
+
554
+ def write_SequenceTypeStageReport(items)
555
+ pack_into(4, 'l>', items.size)
556
+
557
+ items.each do |item|
558
+ self.write_TypeStageReport(item)
559
+ end
560
+ end
561
+
562
+
563
+
564
+ private
565
+
566
+ def reserve(num_bytes)
567
+ if @rust_buf.len + num_bytes > @rust_buf.capacity
568
+ @rust_buf = RustBuffer.reserve(@rust_buf, num_bytes)
569
+ end
570
+
571
+ yield
572
+
573
+ @rust_buf.len += num_bytes
574
+ end
575
+
576
+ def pack_into(size, format, value)
577
+ reserve(size) do
578
+ @rust_buf.data.put_array_of_char @rust_buf.len, [value].pack(format).bytes
579
+ end
580
+ end
581
+ end
582
+
583
+ private_constant :RustBufferBuilder
584
+
585
+ # Error definitions
586
+ class RustCallStatus < FFI::Struct
587
+ layout :code, :int8,
588
+ :error_buf, RustBuffer
589
+
590
+ def code
591
+ self[:code]
592
+ end
593
+
594
+ def error_buf
595
+ self[:error_buf]
596
+ end
597
+
598
+ def to_s
599
+ "RustCallStatus(code=#{self[:code]})"
600
+ end
601
+ end
602
+
603
+ # These match the values from the uniffi::rustcalls module
604
+ CALL_SUCCESS = 0
605
+ CALL_ERROR = 1
606
+ CALL_PANIC = 2
607
+
608
+
609
+ module LlmtrimError
610
+ class Compress < StandardError
611
+ def initialize(detail)
612
+ @detail = detail
613
+ super()
614
+ end
615
+
616
+ attr_reader :detail
617
+
618
+
619
+ def to_s
620
+ "#{self.class.name}(detail=#{@detail.inspect})"
621
+ end
622
+ end
623
+ class UnknownPreset < StandardError
624
+ def initialize(name)
625
+ @name = name
626
+ super()
627
+ end
628
+
629
+ attr_reader :name
630
+
631
+
632
+ def to_s
633
+ "#{self.class.name}(name=#{@name.inspect})"
634
+ end
635
+ end
636
+
637
+ end
638
+
639
+
640
+
641
+ # Map error modules to the RustBuffer method name that reads them
642
+ ERROR_MODULE_TO_READER_METHOD = {
643
+
644
+ LlmtrimError => :readTypeLlmtrimError,
645
+
646
+
647
+ }
648
+
649
+ private_constant :ERROR_MODULE_TO_READER_METHOD, :CALL_SUCCESS, :CALL_ERROR, :CALL_PANIC,
650
+ :RustCallStatus
651
+
652
+ def self.consume_buffer_into_error(error_module, rust_buffer)
653
+ rust_buffer.consumeWithStream do |stream|
654
+ reader_method = ERROR_MODULE_TO_READER_METHOD[error_module]
655
+ return stream.send(reader_method)
656
+ end
657
+ end
658
+
659
+ class InternalError < StandardError
660
+ end
661
+
662
+ def self.rust_call(fn_name, *args)
663
+ # Call a rust function
664
+ rust_call_with_error(nil, fn_name, *args)
665
+ end
666
+
667
+ def self.rust_call_with_error(error_module, fn_name, *args)
668
+ # Call a rust function and handle errors
669
+ #
670
+ # Use this when the rust function returns a Result<>. error_module must be the error_module that corresponds to that Result.
671
+
672
+
673
+ # Note: RustCallStatus.new zeroes out the struct, which is exactly what we
674
+ # want to pass to Rust (code=0, error_buf=RustBuffer(len=0, capacity=0,
675
+ # data=NULL))
676
+ status = RustCallStatus.new
677
+ args << status
678
+
679
+ result = UniFFILib.public_send(fn_name, *args)
680
+
681
+ case status.code
682
+ when CALL_SUCCESS
683
+ result
684
+ when CALL_ERROR
685
+ if error_module.nil?
686
+ status.error_buf.free
687
+ raise InternalError, "CALL_ERROR with no error_module set"
688
+ else
689
+ raise consume_buffer_into_error(error_module, status.error_buf)
690
+ end
691
+ when CALL_PANIC
692
+ # When the rust code sees a panic, it tries to construct a RustBuffer
693
+ # with the message. But if that code panics, then it just sends back
694
+ # an empty buffer.
695
+ if status.error_buf.len > 0
696
+ raise InternalError, status.error_buf.consumeIntoString()
697
+ else
698
+ raise InternalError, "Rust panic"
699
+ end
700
+ else
701
+ raise InternalError, "Unknown call status: #{status.code}"
702
+ end
703
+ end
704
+
705
+ private_class_method :consume_buffer_into_error
706
+
707
+ # This is how we find and load the dynamic library provided by the component.
708
+ # For now we just look it up by name.
709
+ module UniFFILib
710
+ extend FFI::Library
711
+
712
+
713
+ ffi_lib File.expand_path('libllmtrim_ffi.dylib', __dir__)
714
+
715
+
716
+ attach_function :uniffi_llmtrim_ffi_fn_func_compress,
717
+ [RustBuffer.by_value, RustBuffer.by_value, RustBuffer.by_value, RustCallStatus.by_ref],
718
+ RustBuffer.by_value
719
+ attach_function :ffi_llmtrim_ffi_rustbuffer_alloc,
720
+ [:uint64, RustCallStatus.by_ref],
721
+ RustBuffer.by_value
722
+ attach_function :ffi_llmtrim_ffi_rustbuffer_from_bytes,
723
+ [ForeignBytes, RustCallStatus.by_ref],
724
+ RustBuffer.by_value
725
+ attach_function :ffi_llmtrim_ffi_rustbuffer_free,
726
+ [RustBuffer.by_value, RustCallStatus.by_ref],
727
+ :void
728
+ attach_function :ffi_llmtrim_ffi_rustbuffer_reserve,
729
+ [RustBuffer.by_value, :uint64, RustCallStatus.by_ref],
730
+ RustBuffer.by_value
731
+ attach_function :uniffi_llmtrim_ffi_checksum_func_compress,
732
+ [RustCallStatus.by_ref],
733
+ :uint16
734
+ attach_function :ffi_llmtrim_ffi_uniffi_contract_version,
735
+ [RustCallStatus.by_ref],
736
+ :uint32
737
+
738
+ end
739
+
740
+ # Public interface members begin here.
741
+
742
+
743
+
744
+
745
+
746
+
747
+ class Provider
748
+ OPEN_AI = 1
749
+ ANTHROPIC = 2
750
+ GOOGLE = 3
751
+
752
+ end
753
+
754
+
755
+
756
+ # Record type CompressOutput
757
+ class CompressOutput
758
+ attr_reader :request_json, :provider, :model, :tokenizer_label, :tokenizer_exact, :input_tokens_before, :input_tokens_after, :frozen_input_tokens, :output_shaped, :stages
759
+
760
+ def initialize(request_json:, provider:, model:, tokenizer_label:, tokenizer_exact:, input_tokens_before:, input_tokens_after:, frozen_input_tokens:, output_shaped:, stages:)
761
+ @request_json = request_json
762
+ @provider = provider
763
+ @model = model
764
+ @tokenizer_label = tokenizer_label
765
+ @tokenizer_exact = tokenizer_exact
766
+ @input_tokens_before = input_tokens_before
767
+ @input_tokens_after = input_tokens_after
768
+ @frozen_input_tokens = frozen_input_tokens
769
+ @output_shaped = output_shaped
770
+ @stages = stages
771
+ end
772
+
773
+ def ==(other)
774
+ if @request_json != other.request_json
775
+ return false
776
+ end
777
+ if @provider != other.provider
778
+ return false
779
+ end
780
+ if @model != other.model
781
+ return false
782
+ end
783
+ if @tokenizer_label != other.tokenizer_label
784
+ return false
785
+ end
786
+ if @tokenizer_exact != other.tokenizer_exact
787
+ return false
788
+ end
789
+ if @input_tokens_before != other.input_tokens_before
790
+ return false
791
+ end
792
+ if @input_tokens_after != other.input_tokens_after
793
+ return false
794
+ end
795
+ if @frozen_input_tokens != other.frozen_input_tokens
796
+ return false
797
+ end
798
+ if @output_shaped != other.output_shaped
799
+ return false
800
+ end
801
+ if @stages != other.stages
802
+ return false
803
+ end
804
+
805
+ true
806
+ end
807
+ end
808
+
809
+ # Record type StageReport
810
+ class StageReport
811
+ attr_reader :name, :applied, :tokens_before, :tokens_after, :note
812
+
813
+ def initialize(name:, applied:, tokens_before:, tokens_after:, note:)
814
+ @name = name
815
+ @applied = applied
816
+ @tokens_before = tokens_before
817
+ @tokens_after = tokens_after
818
+ @note = note
819
+ end
820
+
821
+ def ==(other)
822
+ if @name != other.name
823
+ return false
824
+ end
825
+ if @applied != other.applied
826
+ return false
827
+ end
828
+ if @tokens_before != other.tokens_before
829
+ return false
830
+ end
831
+ if @tokens_after != other.tokens_after
832
+ return false
833
+ end
834
+ if @note != other.note
835
+ return false
836
+ end
837
+
838
+ true
839
+ end
840
+ end
841
+
842
+
843
+
844
+
845
+
846
+ def self.compress(input, provider, preset)
847
+ input = LlmtrimFfi::uniffi_utf8(input)
848
+
849
+
850
+ provider = (provider ? provider : nil)
851
+ RustBuffer.check_lower_OptionalTypeProvider(provider)
852
+
853
+ preset = (preset ? LlmtrimFfi::uniffi_utf8(preset) : nil)
854
+ RustBuffer.check_lower_Optionalstring(preset)
855
+
856
+ result = LlmtrimFfi.rust_call_with_error(LlmtrimError,:uniffi_llmtrim_ffi_fn_func_compress,RustBuffer.allocFromString(input),RustBuffer.alloc_from_OptionalTypeProvider(provider),RustBuffer.alloc_from_Optionalstring(preset))
857
+ return result.consumeIntoTypeCompressOutput
858
+ end
859
+
860
+
861
+
862
+
863
+
864
+ end
865
+
data/lib/llmtrim.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # llmtrim — static, deterministic LLM prompt/payload compression.
4
+ #
5
+ # Thin entry point: load the UniFFI-generated bindings (which the build step patched to
6
+ # load the native library bundled inside this gem) and re-expose them under `Llmtrim`.
7
+ #
8
+ # require "llmtrim"
9
+ # out = Llmtrim.compress(request_json, Llmtrim::Provider::OPEN_AI, "aggressive")
10
+ # out.input_tokens_before # => Integer
11
+ #
12
+ # The compression runs natively in-process via the bundled `llmtrim-core` engine.
13
+
14
+ require_relative "llmtrim/llmtrim_ffi"
15
+
16
+ # `LlmtrimFfi` is the module name UniFFI derives from the crate; alias it to the friendlier
17
+ # `Llmtrim` without breaking the generated internals.
18
+ Llmtrim = LlmtrimFfi
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: llmtrim
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.10
5
+ platform: arm64-darwin
6
+ authors:
7
+ - François Kiene
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ description: Native in-process bindings to the llmtrim-core compression engine (no
28
+ network, no extra model calls), generated via UniFFI.
29
+ email:
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/llmtrim.rb
36
+ - lib/llmtrim/libllmtrim_ffi.dylib
37
+ - lib/llmtrim/llmtrim_ffi.rb
38
+ homepage: https://github.com/fkiene/llmtrim
39
+ licenses:
40
+ - AGPL-3.0-only
41
+ metadata:
42
+ source_code_uri: https://github.com/fkiene/llmtrim
43
+ rubygems_mfa_required: 'true'
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.5.22
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Static, deterministic LLM prompt/payload compression that cuts input tokens
63
+ 30-90%.
64
+ test_files: []