JTokkit Documentation

repository·main·Indexed 20 days ago

https://github.com/knuddelsgmbh/jtokkit

A fast and efficient Java tokenizer library for OpenAI models, providing JVM-based functionality similar to the Python tiktoken library. JTokkit supports encodings such as cl100k_base, r50k_base, p50k_base, p50k_edit, and o200k_base. It features a thread-safe EncodingRegistry for managing encodings, support for custom BPE parameters via GptBytePairEncodingParams, and optimized methods for counting tokens and handling ChatML message overhead for models like gpt-4 and gpt-3.5-turbo. The library is lightweight with zero external dependencies and supports Java 8+.

Tokens
5K
Snippets
16
Records
17
Agent score
72%

What's inside JTokkit

  1. Overview of JTokkit features and capabilities

    main

    JTokkit is a high-performance Java tokenizer designed for natural language processing tasks involving OpenAI models (such as gpt-3.5-turbo). It aims to provide JVM-based capabilities similar to OpenAI's tiktoken library.

    Key Features

    • Supported Encodings: Implements r50k_base, p50k_base, p50k_edit, and cl100k_base for encoding and decoding.
    • Extensibility: Provides an easy way to extend the library with custom encoding algorithms.
    • Performance: Optimized for high throughput, reaching 2-3 times the throughput of comparable tokenizers.
    • Lightweight: Zero external dependencies and supports Java 8+.
  2. Install JTokkit via Maven or Gradle

    main

    To use JTokkit in your project, add the following dependency to your build configuration file. JTokkit has zero dependencies and supports Java 8 and above.

    <!-- Maven -->
    <dependency>
        <groupId>com.knuddels</groupId>
        <artifactId>jtokkit</artifactId>
        <version>1.1.0</version>
    </dependency>
    
    <!-- Gradle -->
    dependencies {
    	implementation 'com.knuddels:jtokkit:1.1.0'
    }
  3. Initialize an EncodingRegistry

    main

    To use JTokkit, you must first create an EncodingRegistry. You should maintain a single reference to this registry throughout your application's lifecycle because creating it is an expensive operation (it loads vocabularies from the classpath). The registry is thread-safe and handles caching of loaded encodings.

    There are two ways to initialize a registry:

    1. Default Registry: Loads all vocabularies for all encodings immediately upon creation.
    2. Lazy Loading Registry: Only loads vocabularies for encodings that are actually accessed. This is more efficient if you only use a subset of available encodings.
    // Default registry (loads all vocabularies immediately)
    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    
    // Lazy loading registry (loads vocabularies only when accessed)
    EncodingRegistry registry = Encodings.newLazyEncodingRegistry();
  4. Implement the `Encoding` interface for custom encodings

    main

    To support completely custom encodings, implement the Encoding interface and register your implementation with an EncodingRegistry.

    Requirements:

    • The name returned by Encoding#getName() must be unique.
    • Your implementation must be thread-safe, as the EncodingRegistry caches and reuses instances.

    Once registered, you can retrieve your encoding using registry.getEncoding("your-encoding-name").

    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    Encoding customEncoding = new CustomEncoding();
    registry.register(customEncoding);
    
    // Get the encoding from the registry
    Encoding encodingFromRegistry = registry.getEncoding("custom-name");
  5. Count tokens for ChatML messages

    main

    When using OpenAI chat models, you must account for extra tokens added by the ChatML format (e.g., <|start|>, <|end|>, and role/name metadata). Because these overhead tokens vary by model (e.g., gpt-4 vs gpt-3.5-turbo), you should implement a counting logic that applies specific offsets based on the model name and the structure of your ChatMessage list.

    To implement this, you need to:

    1. Retrieve the correct Encoding from the EncodingRegistry for the specific model.
    2. Determine the tokensPerMessage and tokensPerName overhead based on the model family.
    3. Iterate through the messages, summing the tokens for the content, the role, and (if present) the name, plus the model-specific overhead.
    4. Add the final priming tokens (e.g., 3 tokens for the assistant reply start).
    private int countMessageTokens(
    		EncodingRegistry registry,
    		String model,
    		List<ChatMessage> messages // consists of role, content and an optional name
    ) {
    	Encoding encoding = registry.getEncodingForModel(model).orElseThrow();
    	int tokensPerMessage;
    	int tokensPerName;
    	if (model.startsWith("gpt-4")) {
    		tokensPerMessage = 3;
    		tokensPerName = 1;
    	} else if (model.startsWith("gpt-3.5-turbo")) {
    		tokensPerMessage = 4; // every message follows <|start|>{role/name}\n{content}<|end|>
    		tokensPerName = -1; // if there's a name, the role is omitted
    	} else {
    		throw new IllegalArgumentException("Unsupported model: " + model);
    	}
    
    	int sum = 0;
    	for (final var message : messages) {
    		sum += tokensPerMessage;
    		sum += encoding.countTokens(message.getContent());
    		sum += encoding.countTokens(message.getRole());
    		if (message.hasName()) {
    			sum += encoding.countTokens(message.getName());
    			sum += tokensPerName;
    		}
    	}
    
    	sum += 3; // every reply is primed with <|start|>assistant<|message|>
    
    	return sum;
    }
  6. Add a new byte pair encoding using `GptBytePairEncodingParams`

    main

    If you want to add a new byte pair encoding (BPE) without implementing the full Encoding interface from scratch, you can use GptBytePairEncodingParams to define the necessary parameters and register it via registry.registerGptBytePairEncoding(params).

    This method is useful for creating variations of existing GPT-style encodings by providing custom patterns and maps.

    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    GptBytePairEncodingParams params = new GptBytePairEncodingParams(
            "custom-name",
            Pattern.compile("some custom pattern"),
            encodingMap,
            specialTokenEncodingMap
    );
    registry.registerGptBytePairEncoding(params);
    
    // Get the encoding from the registry
    Encoding encodingFromRegistry = registry.getEncoding("custom-name");
  7. Extend JTokkit with custom encodings

    main

    You can extend JTokkit's functionality to support custom encoding algorithms using two methods:

    1. Implement the Encoding interface: Create your own class that implements Encoding and register it with the EncodingRegistry using registerEncoding(Encoding).
    2. Register new BPE parameters: If you are using the Byte Pair Encoding (BPE) algorithm, you can create a GptBytePairEncodingParams object with your custom patterns and maps, then register it using registerGptBytePairEncoding(params).

    Custom encodings can be retrieved by name using registry.getEncoding("custom-name").

    // Option 1: Custom Encoding implementation
    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    Encoding customEncoding = new CustomEncoding();
    registry.registerEncoding(customEncoding);
    
    // Option 2: Custom BPE parameters
    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    GptBytePairEncodingParams params = new GptBytePairEncodingParams(
            "custom-name",
            Pattern.compile("some custom pattern"),
            encodingMap,
            specialTokenEncodingMap
    );
    registry.registerGptBytePairEncoding(params);
    
    // Accessing the custom encoding
    Encoding myEnc = registry.getEncoding("custom-name");
  8. Encode and decode text using JTokkit

    main

    To tokenize text, create an EncodingRegistry using Encodings.newDefaultEncodingRegistry(). You can retrieve a specific encoding using an EncodingType or by specifying a ModelType. Once you have an Encoding instance, use encode() to convert a string into an IntArrayList of tokens, and decode() to convert tokens back into a string.

    Note: EncodingRegistry and Encoding instances are thread-safe and can be shared across your application.

    EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
    
    // Option 1: Get encoding by type
    Encoding enc = registry.getEncoding(EncodingType.CL100K_BASE);
    
    // Option 2: Get encoding for a specific OpenAI model
    Encoding modelEnc = registry.getEncodingForModel(ModelType.TEXT_EMBEDDING_ADA_002);
    
    // Encoding text to tokens
    IntArrayList encoded = enc.encode("This is a sample sentence.");
    
    // Decoding tokens back to text
    String decoded = enc.decode(encoded);
  9. Encode text with truncation

    main

    You can limit the number of tokens produced by passing a maxTokens integer to the encode method. This will truncate the output to the specified length.

    JTokkit automatically handles Unicode characters that might be split by truncation; it ensures that if a character is split, the partial token is removed so that the resulting decoded string remains valid.

    // Truncate to 3 tokens
    IntArrayList encoded = encoding.encode("This is a sample sentence.", 3);
    // encoded = [2028, 374, 264]
    
    // Truncate while handling Unicode (e.g., emojis)
    IntArrayList encodedEmoji = encoding.encode("I love 🍕", 4);
    // The library ensures the emoji is not split incorrectly
  10. Count tokens in text

    main

    If you only need the number of tokens without performing the full encoding, use Encoding#countTokens or Encoding#countTokensOrdinary. These methods are optimized for speed and are faster than calling encode and checking the list size.

    Use countTokensOrdinary if the text might contain special tokens that you want to treat as normal text.

    // Fast token counting
    int tokenCount = encoding.countTokens("This is a sample sentence.");
    
    // Fast token counting including special tokens as ordinary text
    int tokenCountOrdinary = encoding.countTokensOrdinary("hello <|endoftext|> world");
  11. Retrieve an Encoding from the registry

    main

    Once you have an EncodingRegistry, you can retrieve specific Encoding instances using type-safe enums, model types, or string names.

    • Use getEncoding(EncodingType) for a specific encoding type.
    • Use getEncoding(String) to look up an encoding by its name.
    • Use getEncodingForModel(ModelType) to get the encoding associated with a specific model.
    • Use getEncodingForModel(String) to look up an encoding for a model by name.
    // Get encoding via type-safe enum
    Encoding encoding = registry.getEncoding(EncodingType.CL100K_BASE);
    
    // Get encoding via string name
    Optional<Encoding> encoding = registry.getEncoding("cl100k_base");
    
    // Get encoding for a specific model via type-safe enum
    Encoding encoding = registry.getEncodingForModel(ModelType.GPT_4);
    
    // Get encoding for a specific model via string name
    Optional<Encoding> encoding = registry.getEncodingForModel("gpt_4");
  12. Encode and decode text

    main

    An Encoding instance allows you to convert text into a list of token IDs (IntArrayList) and back into a string. The Encoding object is thread-safe.

    Handling Special Tokens: By default, Encoding#encode does not support special tokens (e.g., <|endoftext|>). If it encounters one, it throws an UnsupportedOperationException. To treat special tokens as ordinary text, use Encoding#encodeOrdinary instead.

    // Standard encoding/decoding
    IntArrayList encoded = encoding.encode("This is a sample sentence.");
    String decoded = encoding.decode(encoded);
    
    // Handling special tokens as ordinary text
    // encoding.encode("hello <|endoftext|> world") -> throws UnsupportedOperationException
    IntArrayList encodedOrdinary = encoding.encodeOrdinary("hello <|endoftext|> world");