tiktoken_ruby 0.0.15.1 → 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: ca9715653ab21761d567bd9f7d95d23752d62efab79e6fcef888769a1b9486a5
4
- data.tar.gz: 706aa0a3920fc6505d76ccc0fae74a92e874161ed249c566b78fceb57b3cdb15
3
+ metadata.gz: b04168b5f23957815c4f9a7139d0629c29236befaaeab12deaa1e0fee2c3b814
4
+ data.tar.gz: aa8af07c52ba013e0e7d7ad0aa569db6419a3330eaace2d3401963c7f4f9d2cf
5
5
  SHA512:
6
- metadata.gz: 1c989aebcac3182851ecf17d956e13f10cb2d4d48cfb0a7e101bd2ecef76686584d0146e8858b85c57ecbe7f1757d6ebd3aa84c6dc8aecace051581ae0d56276
7
- data.tar.gz: 9799d0ce8bf67b5b82d78c1883226907ab8f5da984615b2d56b89bb832941328fd58f4f2b928590040696357a4bd1549b02d2eb51c1418381cac1a848ad9cc51
6
+ metadata.gz: 0bf109e1ab9191f06bf7450cfa32589c5dc4fcff41084c3682694b9ddd03f5595151e3ba2e376916c85f5ea1ce8e0866743556ec2c0f778309ac3aad216caf8a
7
+ data.tar.gz: b105c5871dde75b05274e830f1b40c0a4a05364c2cb6947f1223c128af4c1564dfc573c8c6e3b521ccf11b4eb6eb0592882af734deeba39251f8a1eab619e4a5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
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
11
+ # [v0.0.16] - 02-05-2026
12
+ ## What's Changed
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
14
+ * Bump the bundler-dependencies group with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/108
15
+ * Bump standard from 1.53.0 to 1.54.0 in the bundler-dependencies group by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/109
16
+ * Bump the cargo group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/113
17
+ * Bump the bundler-dependencies group across 1 directory with 2 updates by @dependabot[bot] in https://github.com/IAPark/tiktoken_ruby/pull/114
18
+ * bump version to 0.0.16 by @gjtorikian in https://github.com/IAPark/tiktoken_ruby/pull/115
19
+
20
+
21
+ **Full Changelog**: https://github.com/IAPark/tiktoken_ruby/compare/v0.0.15.1...v0.0.16
1
22
  # [v0.0.15.1] - 07-01-2026
2
23
  ## What's Changed
3
24
  * Re-release 0.0.15 for mistakenly yanked gems by @gjtorikian in https://github.com/IAPark/tiktoken_ruby/pull/105
data/Cargo.lock CHANGED
@@ -13,9 +13,9 @@ dependencies = [
13
13
 
14
14
  [[package]]
15
15
  name = "anyhow"
16
- version = "1.0.100"
16
+ version = "1.0.102"
17
17
  source = "registry+https://github.com/rust-lang/crates.io-index"
18
- checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
18
+ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
19
19
 
20
20
  [[package]]
21
21
  name = "base64"
@@ -25,16 +25,14 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
25
25
 
26
26
  [[package]]
27
27
  name = "bindgen"
28
- version = "0.69.5"
28
+ version = "0.72.1"
29
29
  source = "registry+https://github.com/rust-lang/crates.io-index"
30
- checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
30
+ checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
31
31
  dependencies = [
32
32
  "bitflags",
33
33
  "cexpr",
34
34
  "clang-sys",
35
35
  "itertools",
36
- "lazy_static",
37
- "lazycell",
38
36
  "proc-macro2",
39
37
  "quote",
40
38
  "regex",
@@ -45,18 +43,18 @@ dependencies = [
45
43
 
46
44
  [[package]]
47
45
  name = "bit-set"
48
- version = "0.5.3"
46
+ version = "0.8.0"
49
47
  source = "registry+https://github.com/rust-lang/crates.io-index"
50
- checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
48
+ checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
51
49
  dependencies = [
52
50
  "bit-vec",
53
51
  ]
54
52
 
55
53
  [[package]]
56
54
  name = "bit-vec"
57
- version = "0.6.3"
55
+ version = "0.8.0"
58
56
  source = "registry+https://github.com/rust-lang/crates.io-index"
59
- checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
57
+ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
60
58
 
61
59
  [[package]]
62
60
  name = "bitflags"
@@ -109,9 +107,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
109
107
 
110
108
  [[package]]
111
109
  name = "fancy-regex"
112
- version = "0.13.0"
110
+ version = "0.17.0"
113
111
  source = "registry+https://github.com/rust-lang/crates.io-index"
114
- checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
112
+ checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
115
113
  dependencies = [
116
114
  "bit-set",
117
115
  "regex-automata",
@@ -139,12 +137,6 @@ version = "1.5.0"
139
137
  source = "registry+https://github.com/rust-lang/crates.io-index"
140
138
  checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
141
139
 
142
- [[package]]
143
- name = "lazycell"
144
- version = "1.3.0"
145
- source = "registry+https://github.com/rust-lang/crates.io-index"
146
- checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
147
-
148
140
  [[package]]
149
141
  name = "libc"
150
142
  version = "0.2.177"
@@ -226,18 +218,18 @@ dependencies = [
226
218
 
227
219
  [[package]]
228
220
  name = "rb-sys"
229
- version = "0.9.123"
221
+ version = "0.9.128"
230
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
231
- checksum = "45fb1a185af97ee456f1c9e56dbe6e2e662bec4fdeaf83c4c28e0e6adfb18816"
223
+ checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
232
224
  dependencies = [
233
225
  "rb-sys-build",
234
226
  ]
235
227
 
236
228
  [[package]]
237
229
  name = "rb-sys-build"
238
- version = "0.9.123"
230
+ version = "0.9.128"
239
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
240
- checksum = "a58ebd02d7a6033e6a5f6f8d150c1e9f16506039092b84a73e6bedce6d3adf41"
232
+ checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
241
233
  dependencies = [
242
234
  "bindgen",
243
235
  "lazy_static",
@@ -256,9 +248,9 @@ checksum = "08f8d2924cf136a1315e2b4c7460a39f62ef11ee5d522df9b2750fab55b868b6"
256
248
 
257
249
  [[package]]
258
250
  name = "regex"
259
- version = "1.12.2"
251
+ version = "1.12.3"
260
252
  source = "registry+https://github.com/rust-lang/crates.io-index"
261
- checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
253
+ checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
262
254
  dependencies = [
263
255
  "aho-corasick",
264
256
  "memchr",
@@ -285,9 +277,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
285
277
 
286
278
  [[package]]
287
279
  name = "rustc-hash"
288
- version = "1.1.0"
280
+ version = "2.1.2"
289
281
  source = "registry+https://github.com/rust-lang/crates.io-index"
290
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
282
+ checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
291
283
 
292
284
  [[package]]
293
285
  name = "seq-macro"
@@ -349,9 +341,9 @@ dependencies = [
349
341
 
350
342
  [[package]]
351
343
  name = "tiktoken-rs"
352
- version = "0.9.1"
344
+ version = "0.12.0"
353
345
  source = "registry+https://github.com/rust-lang/crates.io-index"
354
- checksum = "3a19830747d9034cd9da43a60eaa8e552dfda7712424aebf187b7a60126bae0d"
346
+ checksum = "027853bbf8c7763b77c5c595f1c271c7d536ced7d6f83452911b944621e57fc2"
355
347
  dependencies = [
356
348
  "anyhow",
357
349
  "base64",
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tiktoken_ruby (0.0.15.1)
4
+ tiktoken_ruby (0.0.17)
5
5
  rb_sys (~> 0.9)
6
6
 
7
7
  GEM
@@ -9,25 +9,25 @@ GEM
9
9
  specs:
10
10
  ast (2.4.3)
11
11
  diff-lcs (1.6.2)
12
- json (2.16.0)
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.0)
17
+ parallel (1.28.0)
18
+ parser (3.3.11.1)
19
19
  ast (~> 2.4.1)
20
20
  racc
21
- prism (1.6.0)
21
+ prism (1.9.0)
22
22
  racc (1.8.1)
23
23
  rainbow (3.1.1)
24
- rake (13.3.1)
24
+ rake (13.4.2)
25
25
  rake-compiler (1.3.1)
26
26
  rake
27
- rake-compiler-dock (1.10.0)
28
- rb_sys (0.9.123)
29
- rake-compiler-dock (= 1.10.0)
30
- regexp_parser (2.11.3)
27
+ rake-compiler-dock (1.12.0)
28
+ rb_sys (0.9.128)
29
+ rake-compiler-dock (= 1.12.0)
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,37 +41,37 @@ 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.81.7)
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
- rubocop-ast (>= 1.47.1, < 2.0)
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.48.0)
55
+ rubocop-ast (1.49.1)
56
56
  parser (>= 3.3.7.2)
57
- prism (~> 1.4)
58
- rubocop-performance (1.25.0)
57
+ prism (~> 1.7)
58
+ rubocop-performance (1.26.1)
59
59
  lint_roller (~> 1.1)
60
60
  rubocop (>= 1.75.0, < 2.0)
61
- rubocop-ast (>= 1.38.0, < 2.0)
61
+ rubocop-ast (>= 1.47.1, < 2.0)
62
62
  ruby-progressbar (1.13.0)
63
- standard (1.52.0)
63
+ standard (1.55.0)
64
64
  language_server-protocol (~> 3.17.0.2)
65
65
  lint_roller (~> 1.0)
66
- rubocop (~> 1.81.7)
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)
70
70
  lint_roller (~> 1.0)
71
71
  rubocop (~> 1.50)
72
- standard-performance (1.8.0)
72
+ standard-performance (1.9.0)
73
73
  lint_roller (~> 1.1)
74
- rubocop-performance (~> 1.25.0)
74
+ rubocop-performance (~> 1.26.0)
75
75
  unicode-display_width (3.2.0)
76
76
  unicode-emoji (~> 4.1)
77
77
  unicode-emoji (4.2.0)
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.
@@ -11,5 +11,5 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  magnus = { version = "0.8.2" }
14
- rb-sys = { version = "0.9.123", features = ["stable-api-compiled-fallback"] }
15
- tiktoken-rs = { version = "0.9.0" }
14
+ rb-sys = { version = "0.9.127", features = ["stable-api-compiled-fallback"] }
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
 
@@ -60,7 +70,14 @@ unsafe extern "C" fn encode_special_without_gvl(data: *mut c_void) -> *mut c_voi
60
70
  unsafe extern "C" fn decode_without_gvl(data: *mut c_void) -> *mut c_void {
61
71
  let data = &mut *(data as *mut DecodeData);
62
72
  let core_bpe = &*data.core_bpe;
63
- data.result = core_bpe.decode(std::mem::take(&mut data.ids)).map_err(|e| e.to_string());
73
+ data.result = core_bpe.decode(&data.ids).map_err(|e| e.to_string());
74
+ std::ptr::null_mut()
75
+ }
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());
64
81
  std::ptr::null_mut()
65
82
  }
66
83
 
@@ -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.15.1"
4
+ VERSION = "0.0.17"
5
5
  end
data/lib/tiktoken_ruby.rb CHANGED
@@ -79,6 +79,7 @@ module Tiktoken
79
79
  o3: "o200k_base",
80
80
  "o4-mini": "o200k_base",
81
81
  # chat
82
+ "gpt-5": "o200k_base",
82
83
  "gpt-4.1": "o200k_base",
83
84
  "chatgpt-4o": "o200k_base",
84
85
  "gpt-4o": "o200k_base",
@@ -136,7 +137,9 @@ module Tiktoken
136
137
  "o3-": "o200k_base",
137
138
  "o4-": "o200k_base",
138
139
  # chat
140
+ "gpt-5.": "o200k_base", # e.g., gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2-pro
139
141
  "gpt-5-": "o200k_base",
142
+ "codex-mini": "o200k_base", # e.g., codex-mini, codex-mini-latest
140
143
  "gpt-4.5-": "o200k_base",
141
144
  "gpt-4.1-": "o200k_base",
142
145
  "chatgpt-4o-": "o200k_base",
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.15.1
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.3
77
+ rubygems_version: 4.0.10
78
78
  specification_version: 4
79
79
  summary: Ruby wrapper for Tiktoken
80
80
  test_files: []