tiktoken_ruby 0.0.16 → 0.0.17

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: 2d1b276821555850dac79a65fddb5152db1b0dfc7021cfa13e1b2435822eb6d1
4
- data.tar.gz: 5c72d2286f0bcb53d128dbb1b52ad91e7c593c91730d98300ee44d21a8c5e737
3
+ metadata.gz: b04168b5f23957815c4f9a7139d0629c29236befaaeab12deaa1e0fee2c3b814
4
+ data.tar.gz: aa8af07c52ba013e0e7d7ad0aa569db6419a3330eaace2d3401963c7f4f9d2cf
5
5
  SHA512:
6
- metadata.gz: f586ac763017ffde2c483d703ebc3fd65d1029238e0a8c2cfac0cb4e4bd8f2f03c7eb6156ccc0bcb8b4c72108d7f8c83e227f866205fa3771b14861116ade096
7
- data.tar.gz: 56ceeaf795a1b0817b5cde8641369e3963b5d613870069c1a62d17732922fc3a41edd66b02c725f3a373bf3697bcf1de7d9e2ed2bcebd70452942a6c102fd55c
6
+ metadata.gz: 0bf109e1ab9191f06bf7450cfa32589c5dc4fcff41084c3682694b9ddd03f5595151e3ba2e376916c85f5ea1ce8e0866743556ec2c0f778309ac3aad216caf8a
7
+ data.tar.gz: b105c5871dde75b05274e830f1b40c0a4a05364c2cb6947f1223c128af4c1564dfc573c8c6e3b521ccf11b4eb6eb0592882af734deeba39251f8a1eab619e4a5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # [v0.0.17] - 18-07-2026
2
+ ## What's Changed
3
+ * Bump the github-actions group with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/119
4
+ * Bump the bundler-dependencies group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/121
5
+ * Bump the cargo group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/120
6
+ * feat(decode): Add opt-in lossy mode and decode_bytes by @gjtorikian in https://github.com/IAPark/tiktoken_ruby/pull/123
7
+ * Bump version to 0.0.17 by @gjtorikian in https://github.com/IAPark/tiktoken_ruby/pull/124
8
+
9
+
10
+ **Full Changelog**: https://github.com/IAPark/tiktoken_ruby/compare/v0.0.16...v0.0.17
1
11
  # [v0.0.16] - 02-05-2026
2
12
  ## What's Changed
3
13
  * Bump rb-sys from 0.9.123 to 0.9.124 in the cargo group by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/107
data/Cargo.lock CHANGED
@@ -36,7 +36,7 @@ dependencies = [
36
36
  "proc-macro2",
37
37
  "quote",
38
38
  "regex",
39
- "rustc-hash 2.1.2",
39
+ "rustc-hash",
40
40
  "shlex",
41
41
  "syn",
42
42
  ]
@@ -218,18 +218,18 @@ dependencies = [
218
218
 
219
219
  [[package]]
220
220
  name = "rb-sys"
221
- version = "0.9.127"
221
+ version = "0.9.128"
222
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
223
- checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12"
223
+ checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
224
224
  dependencies = [
225
225
  "rb-sys-build",
226
226
  ]
227
227
 
228
228
  [[package]]
229
229
  name = "rb-sys-build"
230
- version = "0.9.127"
230
+ version = "0.9.128"
231
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
- checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9"
232
+ checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
233
233
  dependencies = [
234
234
  "bindgen",
235
235
  "lazy_static",
@@ -275,12 +275,6 @@ version = "0.8.8"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
276
  checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
277
277
 
278
- [[package]]
279
- name = "rustc-hash"
280
- version = "1.1.0"
281
- source = "registry+https://github.com/rust-lang/crates.io-index"
282
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
283
-
284
278
  [[package]]
285
279
  name = "rustc-hash"
286
280
  version = "2.1.2"
@@ -347,9 +341,9 @@ dependencies = [
347
341
 
348
342
  [[package]]
349
343
  name = "tiktoken-rs"
350
- version = "0.11.0"
344
+ version = "0.12.0"
351
345
  source = "registry+https://github.com/rust-lang/crates.io-index"
352
- checksum = "fac4a168cfc1d8ed65bf17a6ee0843ad9a68f863c63c0fb2fa7eab67838782ee"
346
+ checksum = "027853bbf8c7763b77c5c595f1c271c7d536ced7d6f83452911b944621e57fc2"
353
347
  dependencies = [
354
348
  "anyhow",
355
349
  "base64",
@@ -357,7 +351,7 @@ dependencies = [
357
351
  "fancy-regex",
358
352
  "lazy_static",
359
353
  "regex",
360
- "rustc-hash 1.1.0",
354
+ "rustc-hash",
361
355
  ]
362
356
 
363
357
  [[package]]
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tiktoken_ruby (0.0.16)
4
+ tiktoken_ruby (0.0.17)
5
5
  rb_sys (~> 0.9)
6
6
 
7
7
  GEM
@@ -9,13 +9,13 @@ GEM
9
9
  specs:
10
10
  ast (2.4.3)
11
11
  diff-lcs (1.6.2)
12
- json (2.18.1)
12
+ json (2.20.0)
13
13
  language_server-protocol (3.17.0.5)
14
14
  lint_roller (1.1.0)
15
15
  minitest (6.0.1)
16
16
  prism (~> 1.5)
17
- parallel (1.27.0)
18
- parser (3.3.10.2)
17
+ parallel (1.28.0)
18
+ parser (3.3.11.1)
19
19
  ast (~> 2.4.1)
20
20
  racc
21
21
  prism (1.9.0)
@@ -25,9 +25,9 @@ GEM
25
25
  rake-compiler (1.3.1)
26
26
  rake
27
27
  rake-compiler-dock (1.12.0)
28
- rb_sys (0.9.127)
28
+ rb_sys (0.9.128)
29
29
  rake-compiler-dock (= 1.12.0)
30
- regexp_parser (2.11.3)
30
+ regexp_parser (2.12.0)
31
31
  rspec (3.13.2)
32
32
  rspec-core (~> 3.13.0)
33
33
  rspec-expectations (~> 3.13.0)
@@ -41,18 +41,18 @@ GEM
41
41
  diff-lcs (>= 1.2.0, < 2.0)
42
42
  rspec-support (~> 3.13.0)
43
43
  rspec-support (3.13.6)
44
- rubocop (1.84.2)
44
+ rubocop (1.87.0)
45
45
  json (~> 2.3)
46
46
  language_server-protocol (~> 3.17.0.2)
47
47
  lint_roller (~> 1.1.0)
48
- parallel (~> 1.10)
48
+ parallel (>= 1.10)
49
49
  parser (>= 3.3.0.2)
50
50
  rainbow (>= 2.2.2, < 4.0)
51
51
  regexp_parser (>= 2.9.3, < 3.0)
52
52
  rubocop-ast (>= 1.49.0, < 2.0)
53
53
  ruby-progressbar (~> 1.7)
54
54
  unicode-display_width (>= 2.4.0, < 4.0)
55
- rubocop-ast (1.49.0)
55
+ rubocop-ast (1.49.1)
56
56
  parser (>= 3.3.7.2)
57
57
  prism (~> 1.7)
58
58
  rubocop-performance (1.26.1)
@@ -60,10 +60,10 @@ GEM
60
60
  rubocop (>= 1.75.0, < 2.0)
61
61
  rubocop-ast (>= 1.47.1, < 2.0)
62
62
  ruby-progressbar (1.13.0)
63
- standard (1.54.0)
63
+ standard (1.55.0)
64
64
  language_server-protocol (~> 3.17.0.2)
65
65
  lint_roller (~> 1.0)
66
- rubocop (~> 1.84.0)
66
+ rubocop (~> 1.87.0)
67
67
  standard-custom (~> 1.0.0)
68
68
  standard-performance (~> 1.8)
69
69
  standard-custom (1.0.2)
data/README.md CHANGED
@@ -17,9 +17,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
17
17
 
18
18
  ## Usage
19
19
 
20
- Usage should be very similar to the python library. Here's a simple example
21
-
22
- Encode and decode text
20
+ Encode and decode text:
23
21
 
24
22
  ```ruby
25
23
  require 'tiktoken_ruby'
@@ -27,7 +25,7 @@ enc = Tiktoken.get_encoding("cl100k_base")
27
25
  enc.decode(enc.encode("hello world")) #=> "hello world"
28
26
  ```
29
27
 
30
- Encoders can also be retrieved by model name
28
+ Encoders can also be retrieved by model name:
31
29
 
32
30
  ```ruby
33
31
  require 'tiktoken_ruby'
@@ -69,6 +67,27 @@ enc.encode_with_special_tokens(text)
69
67
 
70
68
  All methods round-trip correctly through `decode`.
71
69
 
70
+ ### Decoding methods
71
+
72
+ - `decode(tokens, errors: :strict)` - Decodes tokens back into a UTF-8 string
73
+ - `decode_bytes(tokens)` - Decodes tokens into their raw bytes (an `ASCII-8BIT` string), without UTF-8 validation
74
+
75
+ Because BPE tokens are byte-level, a single character (an emoji, or non-Latin scripts) can span multiple tokens. Truncating a token array (like, "trim text to N tokens") can leave a prefix that is not valid UTF-8. The `errors:` option controls how those invalid sequences are handled.
76
+
77
+ ```ruby
78
+ enc = Tiktoken.encoding_for_model("gpt-4o")
79
+ tokens = enc.encode("🦄") # the emoji spans multiple tokens
80
+
81
+ # :strict (default) - raise Tiktoken::UnicodeError on invalid UTF-8
82
+ enc.decode(tokens.first(2)) #=> raises Tiktoken::UnicodeError
83
+
84
+ # :replace - substitute invalid sequences with "�" (matches Python tiktoken's default)
85
+ enc.decode(tokens.first(2), errors: :replace) #=> "�"
86
+
87
+ # decode_bytes - get the raw bytes and handle them yourself
88
+ enc.decode_bytes(tokens.first(2)) #=> "\xF0\x9F\xA6" (ASCII-8BIT)
89
+ ```
90
+
72
91
  ## Development
73
92
 
74
93
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
12
12
  [dependencies]
13
13
  magnus = { version = "0.8.2" }
14
14
  rb-sys = { version = "0.9.127", features = ["stable-api-compiled-fallback"] }
15
- tiktoken-rs = { version = "0.11.0" }
15
+ tiktoken-rs = { version = "0.12.0" }
@@ -3,7 +3,7 @@ use std::ffi::c_void;
3
3
 
4
4
  use tiktoken_rs::Rank;
5
5
 
6
- use crate::uncicode_error;
6
+ use crate::unicode_error;
7
7
 
8
8
  #[magnus::wrap(class = "Tiktoken::Ext::CoreBPE")]
9
9
  pub struct CoreBPEWrapper {
@@ -20,7 +20,7 @@ struct EncodeData {
20
20
  core_bpe: *const tiktoken_rs::CoreBPE,
21
21
  text: String,
22
22
  allowed_special: HashSet<String>,
23
- result: Vec<Rank>,
23
+ result: Result<Vec<Rank>, String>,
24
24
  }
25
25
 
26
26
  struct EncodeSpecialData {
@@ -35,6 +35,12 @@ struct DecodeData {
35
35
  result: Result<String, String>,
36
36
  }
37
37
 
38
+ struct DecodeBytesData {
39
+ core_bpe: *const tiktoken_rs::CoreBPE,
40
+ ids: Vec<Rank>,
41
+ result: Result<Vec<u8>, String>,
42
+ }
43
+
38
44
  unsafe extern "C" fn encode_ordinary_without_gvl(data: *mut c_void) -> *mut c_void {
39
45
  let data = &mut *(data as *mut EncodeOrdinaryData);
40
46
  let core_bpe = &*data.core_bpe;
@@ -46,7 +52,11 @@ unsafe extern "C" fn encode_without_gvl(data: *mut c_void) -> *mut c_void {
46
52
  let data = &mut *(data as *mut EncodeData);
47
53
  let core_bpe = &*data.core_bpe;
48
54
  let allowed_special: HashSet<&str> = data.allowed_special.iter().map(|s| s.as_str()).collect();
49
- data.result = core_bpe.encode(&data.text, &allowed_special).0;
55
+ // Keep only the token vector and surface any tokenization failure to Ruby.
56
+ data.result = core_bpe
57
+ .encode(&data.text, &allowed_special)
58
+ .map(|(tokens, _last_piece_token_len)| tokens)
59
+ .map_err(|e| e.to_string());
50
60
  std::ptr::null_mut()
51
61
  }
52
62
 
@@ -64,6 +74,13 @@ unsafe extern "C" fn decode_without_gvl(data: *mut c_void) -> *mut c_void {
64
74
  std::ptr::null_mut()
65
75
  }
66
76
 
77
+ unsafe extern "C" fn decode_bytes_without_gvl(data: *mut c_void) -> *mut c_void {
78
+ let data = &mut *(data as *mut DecodeBytesData);
79
+ let core_bpe = &*data.core_bpe;
80
+ data.result = core_bpe.decode_bytes(&data.ids).map_err(|e| e.to_string());
81
+ std::ptr::null_mut()
82
+ }
83
+
67
84
  impl CoreBPEWrapper {
68
85
  pub fn new(core_bpe: tiktoken_rs::CoreBPE) -> Self {
69
86
  Self { core_bpe }
@@ -99,7 +116,7 @@ impl CoreBPEWrapper {
99
116
  core_bpe: &self.core_bpe as *const _,
100
117
  text,
101
118
  allowed_special: HashSet::from_iter(allowed_special),
102
- result: Vec::new(),
119
+ result: Ok(Vec::new()),
103
120
  };
104
121
 
105
122
  unsafe {
@@ -111,7 +128,9 @@ impl CoreBPEWrapper {
111
128
  );
112
129
  }
113
130
 
114
- Ok(data.result)
131
+ data.result.map_err(|e| {
132
+ magnus::Error::new(magnus::Ruby::get().unwrap().exception_runtime_error(), e)
133
+ })
115
134
  }
116
135
 
117
136
  pub fn encode_with_special_tokens(&self, text: String) -> Vec<Rank> {
@@ -150,7 +169,7 @@ impl CoreBPEWrapper {
150
169
  }
151
170
 
152
171
  data.result.map_err(|e| {
153
- let error = match uncicode_error() {
172
+ let error = match unicode_error() {
154
173
  Ok(error) => error,
155
174
  Err(e) => return e,
156
175
  };
@@ -158,4 +177,37 @@ impl CoreBPEWrapper {
158
177
  magnus::Error::new(error, e)
159
178
  })
160
179
  }
180
+
181
+ pub fn decode_bytes(&self, ids: Vec<Rank>) -> Result<magnus::RString, magnus::Error> {
182
+ let mut data = DecodeBytesData {
183
+ core_bpe: &self.core_bpe as *const _,
184
+ ids,
185
+ result: Err(String::new()),
186
+ };
187
+
188
+ unsafe {
189
+ rb_sys::rb_thread_call_without_gvl(
190
+ Some(decode_bytes_without_gvl),
191
+ &mut data as *mut _ as *mut c_void,
192
+ None,
193
+ std::ptr::null_mut(),
194
+ );
195
+ }
196
+
197
+ // Build the Ruby string only after the GVL has been re-acquired.
198
+ // `str_from_slice` yields an ASCII-8BIT (binary) string, so the bytes
199
+ // are handed back verbatim without any UTF-8 validation. We hold the
200
+ // GVL here (this is called from Ruby), so `Ruby::get` cannot fail.
201
+ let ruby = magnus::Ruby::get().unwrap();
202
+ data.result
203
+ .map(|bytes| ruby.str_from_slice(&bytes))
204
+ .map_err(|e| {
205
+ let error = match unicode_error() {
206
+ Ok(error) => error,
207
+ Err(e) => return e,
208
+ };
209
+
210
+ magnus::Error::new(error, e)
211
+ })
212
+ }
161
213
  }
@@ -34,7 +34,7 @@ fn module() -> Result<RModule, magnus::Error> {
34
34
  Ruby::get().unwrap().define_module("Tiktoken")
35
35
  }
36
36
 
37
- fn uncicode_error() -> Result<ExceptionClass, magnus::Error> {
37
+ fn unicode_error() -> Result<ExceptionClass, magnus::Error> {
38
38
  module()?.define_error(
39
39
  "UnicodeError",
40
40
  Ruby::get().unwrap().exception_standard_error(),
@@ -45,6 +45,10 @@ fn uncicode_error() -> Result<ExceptionClass, magnus::Error> {
45
45
  fn init() -> Result<(), Error> {
46
46
  let module = module()?;
47
47
 
48
+ // Define Tiktoken::UnicodeError eagerly so the constant exists as soon as
49
+ // the extension is loaded, rather than only after the first decode error.
50
+ unicode_error()?;
51
+
48
52
  let factory_module = module.define_module("BpeFactory")?;
49
53
  factory_module.define_singleton_method("r50k_base", function!(r50k_base, 0))?;
50
54
  factory_module.define_singleton_method("p50k_base", function!(p50k_base, 0))?;
@@ -67,5 +71,6 @@ fn init() -> Result<(), Error> {
67
71
  )?;
68
72
 
69
73
  bpe_class.define_method("decode", method!(CoreBPEWrapper::decode, 1))?;
74
+ bpe_class.define_method("decode_bytes", method!(CoreBPEWrapper::decode_bytes, 1))?;
70
75
  Ok(())
71
76
  }
@@ -47,11 +47,41 @@ class Tiktoken::Encoding
47
47
  @ext_base_bpe.encode_with_special_tokens(text)
48
48
  end
49
49
 
50
- # Decodes the tokens back into text
50
+ # Decodes the tokens back into text.
51
+ #
52
+ # BPE tokens are byte-level, so a single character (an emoji, non-Latin
53
+ # scripts) can span multiple tokens. Truncating a token array to a limit can
54
+ # therefore leave a prefix that is not valid UTF-8. The +errors+ option
55
+ # controls how those invalid byte sequences are handled:
56
+ #
57
+ # * +:strict+ (default) - raise Tiktoken::UnicodeError on invalid UTF-8.
58
+ # * +:replace+ - substitute each invalid sequence with the Unicode replacement
59
+ # character (U+FFFD, +�+), matching Python tiktoken's default behavior.
60
+ #
51
61
  # @param tokens [Array<Integer>] The tokens to decode
52
- # @return [String] The decoded text
53
- def decode(tokens)
54
- @ext_base_bpe.decode(tokens)
62
+ # @param errors [Symbol] How to handle invalid UTF-8 (:strict or :replace)
63
+ # @return [String] The decoded text (UTF-8)
64
+ def decode(tokens, errors: :strict)
65
+ case errors
66
+ when :strict
67
+ @ext_base_bpe.decode(tokens)
68
+ when :replace
69
+ decode_bytes(tokens).force_encoding(Encoding::UTF_8).scrub("\u{FFFD}")
70
+ else
71
+ raise ArgumentError, "errors must be :strict or :replace, got #{errors.inspect}"
72
+ end
73
+ end
74
+
75
+ # Decodes the tokens back into their raw bytes, without any UTF-8 validation.
76
+ #
77
+ # The returned string has ASCII-8BIT (binary) encoding and is not guaranteed
78
+ # to be valid UTF-8 — useful when you want to handle invalid sequences
79
+ # yourself. Matches Python tiktoken's +decode_bytes+.
80
+ #
81
+ # @param tokens [Array<Integer>] The tokens to decode
82
+ # @return [String] The decoded bytes (ASCII-8BIT)
83
+ def decode_bytes(tokens)
84
+ @ext_base_bpe.decode_bytes(tokens)
55
85
  end
56
86
 
57
87
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tiktoken
4
- VERSION = "0.0.16"
4
+ VERSION = "0.0.17"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tiktoken_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - IAPark
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
74
  - !ruby/object:Gem::Version
75
75
  version: 3.4.0
76
76
  requirements: []
77
- rubygems_version: 4.0.6
77
+ rubygems_version: 4.0.10
78
78
  specification_version: 4
79
79
  summary: Ruby wrapper for Tiktoken
80
80
  test_files: []