feat: Implement tokenizer resolver and fallback tokenizer; update model profiles and README
This commit is contained in:
@@ -39,7 +39,7 @@ import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
import de.extio.lmlib.profile.ModelProfileService;
|
||||
import de.extio.lmlib.token.Tokenizer;
|
||||
import de.extio.lmlib.token.TokenizerResolver;
|
||||
|
||||
public abstract class AbstractCompletionClient implements Client, DisposableBean {
|
||||
|
||||
@@ -50,7 +50,7 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean
|
||||
protected RestClient.Builder restClientBuilder;
|
||||
|
||||
@Autowired
|
||||
protected Tokenizer tokenizer;
|
||||
protected TokenizerResolver tokenizerResolver;
|
||||
|
||||
@Autowired
|
||||
protected ModelProfileService modelProfileService;
|
||||
@@ -214,14 +214,14 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean
|
||||
}
|
||||
// Fallback to tokenizer estimation
|
||||
if (inTokens <= 0 && prompt != null && !prompt.isBlank()) {
|
||||
inTokens = this.tokenizer.count(prompt, modelProfile);
|
||||
inTokens = this.tokenizerResolver.count(prompt, modelProfile);
|
||||
}
|
||||
if (reasoningOutTokens <= 0 && reasoning != null && !reasoning.isBlank()) {
|
||||
reasoningOutTokens = this.tokenizer.count(reasoning, modelProfile);
|
||||
reasoningOutTokens = this.tokenizerResolver.count(reasoning, modelProfile);
|
||||
outTokens -= reasoningOutTokens;
|
||||
}
|
||||
if (outTokens <= 0 && response != null && !response.isBlank()) {
|
||||
outTokens = this.tokenizer.count(response, modelProfile);
|
||||
outTokens = this.tokenizerResolver.count(response, modelProfile);
|
||||
}
|
||||
|
||||
// Calculate cost - inTokens and outTokens are already exclusive of cached/reasoning tokens
|
||||
|
||||
@@ -308,13 +308,13 @@ public class ChatCompletionClient extends AbstractCompletionClient {
|
||||
var prompt = turn.text() != null ? turn.text() : "";
|
||||
var trimmed = false;
|
||||
|
||||
final var textTokens = this.tokenizer.count(prompt, modelProfile);
|
||||
final var textTokens = this.tokenizerResolver.count(prompt, modelProfile);
|
||||
final int maxLength = modelProfile.maxContextLength() - modelProfile.maxTokens() - 15; // 15 is a margin for EOT and other special tokens
|
||||
if (tokens.addAndGet(textTokens) > maxLength) {
|
||||
var tokenized = this.tokenizer.tokenize(prompt, modelProfile);
|
||||
var tokenized = this.tokenizerResolver.tokenize(prompt, modelProfile);
|
||||
LOGGER.warn("Prompt too long: {} tokens / {} max. Prompt will be cut!", tokenized.size(), maxLength);
|
||||
tokenized = tokenized.subList(0, tokenized.size() - (tokens.get() - maxLength));
|
||||
prompt = this.tokenizer.detokenize(tokenized, modelProfile);
|
||||
prompt = this.tokenizerResolver.detokenize(tokenized, modelProfile);
|
||||
trimmed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -200,12 +200,12 @@ public class TextCompletionClient extends AbstractCompletionClient {
|
||||
private String createPrompt(final Conversation conversation, final ModelProfile modelProfile, final PromptStrategy promptStrategy) {
|
||||
StringBuilder prompt = this.formatConversation(conversation, promptStrategy);
|
||||
|
||||
List<Long> tokenized = this.tokenizer.tokenize(prompt.toString(), modelProfile);
|
||||
List<Long> tokenized = this.tokenizerResolver.tokenize(prompt.toString(), modelProfile);
|
||||
final int maxLength = modelProfile.maxContextLength() - modelProfile.maxTokens() - 15; // 15 is a margin for EOT and other special tokens
|
||||
if (tokenized.size() > maxLength) {
|
||||
LOGGER.warn("Prompt too long: {} tokens / {} max. Prompt will be cut!", tokenized.size(), maxLength);
|
||||
tokenized = tokenized.subList(0, maxLength);
|
||||
prompt = new StringBuilder(this.tokenizer.detokenize(tokenized, modelProfile));
|
||||
prompt = new StringBuilder(this.tokenizerResolver.detokenize(tokenized, modelProfile));
|
||||
}
|
||||
|
||||
LOGGER.debug("Completion request for {}. Input tokens: {}", modelProfile.category(), tokenized.size());
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
|
||||
|
||||
public record ModelProfile(
|
||||
String prompt,
|
||||
String tokenizer,
|
||||
String tokenEncoding,
|
||||
int maxTokens,
|
||||
int maxContextLength,
|
||||
|
||||
@@ -40,6 +40,9 @@ public class ModelProfileService {
|
||||
final var promptTemplate = Optional.ofNullable(resource.getProperty("prompts"))
|
||||
.map(Object::toString)
|
||||
.orElse("");
|
||||
final var tokenizer = Optional.ofNullable(resource.getProperty("tokenizer"))
|
||||
.map(Object::toString)
|
||||
.orElse(null);
|
||||
final var tokenEncoding = Optional.ofNullable(resource.getProperty("tokenEncoding"))
|
||||
.map(Object::toString)
|
||||
.orElse("none");
|
||||
@@ -109,7 +112,7 @@ public class ModelProfileService {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
return new ModelProfile(promptTemplate, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costPerInToken, costPerCachedInToken, costPerOutToken, costPerReasoningOutToken, reasoningEffort, reasoningSummaryDetails, category);
|
||||
return new ModelProfile(promptTemplate, tokenizer, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costPerInToken, costPerCachedInToken, costPerOutToken, costPerReasoningOutToken, reasoningEffort, reasoningSummaryDetails, category);
|
||||
}
|
||||
catch (final IOException e) {
|
||||
LOGGER.error("Error while reading model profile", e);
|
||||
|
||||
169
src/main/java/de/extio/lmlib/token/FallbackTokenizer.java
Normal file
169
src/main/java/de/extio/lmlib/token/FallbackTokenizer.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
final class FallbackTokenizer implements Tokenizer {
|
||||
|
||||
static final String NAME = "fallback";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> tokenize(final String txt, final ModelProfile modelProfile) {
|
||||
if (txt == null || txt.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final var tokens = new ArrayList<Long>();
|
||||
var index = 0;
|
||||
while (index < txt.length()) {
|
||||
final var codePoint = txt.codePointAt(index);
|
||||
final var charCount = Character.charCount(codePoint);
|
||||
if (Character.isWhitespace(codePoint)) {
|
||||
final var end = this.consumeWhitespace(txt, index);
|
||||
tokens.addAll(this.encodeChunked(txt.substring(index, end), 6));
|
||||
index = end;
|
||||
continue;
|
||||
}
|
||||
if (Character.isLetterOrDigit(codePoint)) {
|
||||
final var end = this.consumeAlphaNumeric(txt, index);
|
||||
tokens.addAll(this.encodeWordLike(txt.substring(index, end)));
|
||||
index = end;
|
||||
continue;
|
||||
}
|
||||
tokens.add(this.encodeToken(txt.substring(index, index + charCount)));
|
||||
index += charCount;
|
||||
}
|
||||
return List.copyOf(tokens);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count(final String txt, final ModelProfile modelProfile) {
|
||||
return this.tokenize(txt, modelProfile).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String detokenize(final List<Long> tokens, final ModelProfile modelProfile) {
|
||||
final var builder = new StringBuilder();
|
||||
for (final var token : tokens) {
|
||||
builder.append(this.decodeToken(token));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private int consumeWhitespace(final String txt, final int start) {
|
||||
var index = start;
|
||||
while (index < txt.length()) {
|
||||
final var codePoint = txt.codePointAt(index);
|
||||
if (!Character.isWhitespace(codePoint)) {
|
||||
break;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private int consumeAlphaNumeric(final String txt, final int start) {
|
||||
var index = start;
|
||||
while (index < txt.length()) {
|
||||
final var codePoint = txt.codePointAt(index);
|
||||
if (!Character.isLetterOrDigit(codePoint)) {
|
||||
break;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private List<Long> encodeWordLike(final String text) {
|
||||
if (text.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (this.isAsciiDigits(text)) {
|
||||
return this.encodeChunked(text, 3);
|
||||
}
|
||||
if (this.isAsciiLetters(text)) {
|
||||
return this.encodeChunked(text, 4);
|
||||
}
|
||||
return this.encodeChunkedByUtf8Bytes(text, 6);
|
||||
}
|
||||
|
||||
private boolean isAsciiDigits(final String text) {
|
||||
return text.chars().allMatch(ch -> ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
private boolean isAsciiLetters(final String text) {
|
||||
return text.chars().allMatch(ch -> ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z');
|
||||
}
|
||||
|
||||
private List<Long> encodeChunked(final String text, final int maxCharsPerToken) {
|
||||
final var tokens = new ArrayList<Long>();
|
||||
for (var index = 0; index < text.length();) {
|
||||
var end = index;
|
||||
var charCount = 0;
|
||||
while (end < text.length() && charCount < maxCharsPerToken) {
|
||||
final var codePoint = text.codePointAt(end);
|
||||
end += Character.charCount(codePoint);
|
||||
charCount++;
|
||||
}
|
||||
tokens.add(this.encodeToken(text.substring(index, end)));
|
||||
index = end;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private List<Long> encodeChunkedByUtf8Bytes(final String text, final int maxBytesPerToken) {
|
||||
final var tokens = new ArrayList<Long>();
|
||||
for (var index = 0; index < text.length();) {
|
||||
var end = index;
|
||||
var bytes = 0;
|
||||
while (end < text.length()) {
|
||||
final var codePoint = text.codePointAt(end);
|
||||
final var candidateEnd = end + Character.charCount(codePoint);
|
||||
final var candidateBytes = text.substring(end, candidateEnd).getBytes(StandardCharsets.UTF_8).length;
|
||||
if (bytes > 0 && bytes + candidateBytes > maxBytesPerToken) {
|
||||
break;
|
||||
}
|
||||
end = candidateEnd;
|
||||
bytes += candidateBytes;
|
||||
if (bytes >= maxBytesPerToken) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokens.add(this.encodeToken(text.substring(index, end)));
|
||||
index = end;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private long encodeToken(final String tokenText) {
|
||||
final var bytes = tokenText.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length == 0 || bytes.length > 7) {
|
||||
throw new IllegalArgumentException("Fallback tokenizer token length must be between 1 and 7 bytes");
|
||||
}
|
||||
long value = bytes.length;
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
value |= (bytes[i] & 0xffL) << ((i + 1) * 8);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String decodeToken(final long token) {
|
||||
final var length = (int) (token & 0xffL);
|
||||
if (length <= 0 || length > 7) {
|
||||
throw new IllegalArgumentException("Invalid fallback tokenizer token length: " + length);
|
||||
}
|
||||
final var bytes = new byte[length];
|
||||
for (var i = 0; i < length; i++) {
|
||||
bytes[i] = (byte) ((token >> ((i + 1) * 8)) & 0xffL);
|
||||
}
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
final class JTokkitTokenizer implements Tokenizer {
|
||||
|
||||
static final String NAME = "jTokkit";
|
||||
|
||||
private static final EncodingRegistry registry;
|
||||
|
||||
static {
|
||||
@@ -26,10 +28,15 @@ final class JTokkitTokenizer implements Tokenizer {
|
||||
return registry.getEncoding(EncodingType.fromName(k).orElseThrow(() -> new IllegalArgumentException("Token encoding " + k)));
|
||||
});
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> tokenize(final String txt, final ModelProfile modelProfile) {
|
||||
return this.encodings
|
||||
.get(modelProfile.tokenEncoding())
|
||||
.get(this.getTokenEncoding(modelProfile))
|
||||
.encodeOrdinary(txt)
|
||||
.boxed()
|
||||
.stream()
|
||||
@@ -39,7 +46,7 @@ final class JTokkitTokenizer implements Tokenizer {
|
||||
|
||||
@Override
|
||||
public int count(final String txt, final ModelProfile modelProfile) {
|
||||
return this.encodings.get(modelProfile.tokenEncoding()).countTokensOrdinary(txt);
|
||||
return this.encodings.get(this.getTokenEncoding(modelProfile)).countTokensOrdinary(txt);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,7 +54,14 @@ final class JTokkitTokenizer implements Tokenizer {
|
||||
final var ial = new IntArrayList(tokens.size());
|
||||
tokens.forEach(t -> ial.add(t.intValue()));
|
||||
return this.encodings
|
||||
.get(modelProfile.tokenEncoding())
|
||||
.get(this.getTokenEncoding(modelProfile))
|
||||
.decode(ial);
|
||||
}
|
||||
|
||||
private String getTokenEncoding(final ModelProfile modelProfile) {
|
||||
if (modelProfile.tokenEncoding() == null || modelProfile.tokenEncoding().isBlank() || "none".equalsIgnoreCase(modelProfile.tokenEncoding())) {
|
||||
return "cl100k_base"; // default encoding, used by most models. Will be used if the profile doesn't specify an encoding or specifies an invalid one.
|
||||
}
|
||||
return modelProfile.tokenEncoding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -12,6 +11,8 @@ import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
final class LlamaServerTokenizer implements Tokenizer {
|
||||
|
||||
static final String NAME = "llamaServer";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LlamaServerTokenizer.class);
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
@@ -20,6 +21,11 @@ final class LlamaServerTokenizer implements Tokenizer {
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> tokenize(final String txt, final ModelProfile modelProfile) {
|
||||
LOGGER.debug("Requesting tokenizing");
|
||||
|
||||
@@ -6,6 +6,8 @@ import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
public interface Tokenizer {
|
||||
|
||||
String getName();
|
||||
|
||||
List<Long> tokenize(String txt, ModelProfile modelProfile);
|
||||
|
||||
int count(String txt, ModelProfile modelProfile);
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.extio.lmlib.token;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -13,18 +14,29 @@ import org.springframework.web.client.RestClient;
|
||||
public class TokenizerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "llamaServer")
|
||||
@ConditionalOnMissingBean(type = "de.extio.lmlib.token.Tokenizer")
|
||||
@ConditionalOnBean(name = "lmLibRestClientBuilder")
|
||||
@ConditionalOnMissingBean(LlamaServerTokenizer.class)
|
||||
Tokenizer llamaServerTokenizer(@Qualifier("lmLibRestClientBuilder") final RestClient.Builder restClientBuilder) {
|
||||
return new LlamaServerTokenizer(restClientBuilder);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "com.knuddels.jtokkit.Encodings")
|
||||
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "jTokkit", matchIfMissing = true)
|
||||
@ConditionalOnMissingBean(type = "de.extio.lmlib.token.Tokenizer")
|
||||
@ConditionalOnMissingBean(JTokkitTokenizer.class)
|
||||
Tokenizer jTokkitTokenizer() {
|
||||
return new JTokkitTokenizer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FallbackTokenizer.class)
|
||||
Tokenizer fallbackTokenizer() {
|
||||
return new FallbackTokenizer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
TokenizerResolver tokenizerResolver(final java.util.List<Tokenizer> tokenizers) {
|
||||
return new TokenizerResolver(tokenizers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
55
src/main/java/de/extio/lmlib/token/TokenizerResolver.java
Normal file
55
src/main/java/de/extio/lmlib/token/TokenizerResolver.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
public class TokenizerResolver {
|
||||
|
||||
private static final List<String> DEFAULT_TOKENIZER_ORDER = List.of(JTokkitTokenizer.NAME, FallbackTokenizer.NAME);
|
||||
|
||||
private final Map<String, Tokenizer> tokenizersByName;
|
||||
|
||||
public TokenizerResolver(final List<Tokenizer> tokenizers) {
|
||||
final var tokenizersByName = new LinkedHashMap<String, Tokenizer>();
|
||||
for (final var tokenizer : tokenizers) {
|
||||
final var previous = tokenizersByName.put(tokenizer.getName(), tokenizer);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate tokenizer name: " + tokenizer.getName());
|
||||
}
|
||||
}
|
||||
this.tokenizersByName = Map.copyOf(tokenizersByName);
|
||||
}
|
||||
|
||||
public Tokenizer resolve(final ModelProfile modelProfile) {
|
||||
if (modelProfile != null && modelProfile.tokenizer() != null && !modelProfile.tokenizer().isBlank()) {
|
||||
final var tokenizer = this.tokenizersByName.get(modelProfile.tokenizer());
|
||||
if (tokenizer == null) {
|
||||
throw new IllegalStateException("No tokenizer configured with name: " + modelProfile.tokenizer());
|
||||
}
|
||||
return tokenizer;
|
||||
}
|
||||
for (final var tokenizerName : DEFAULT_TOKENIZER_ORDER) {
|
||||
final var tokenizer = this.tokenizersByName.get(tokenizerName);
|
||||
if (tokenizer != null) {
|
||||
return tokenizer;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No tokenizer available");
|
||||
}
|
||||
|
||||
public List<Long> tokenize(final String txt, final ModelProfile modelProfile) {
|
||||
return this.resolve(modelProfile).tokenize(txt, modelProfile);
|
||||
}
|
||||
|
||||
public int count(final String txt, final ModelProfile modelProfile) {
|
||||
return this.resolve(modelProfile).count(txt, modelProfile);
|
||||
}
|
||||
|
||||
public String detokenize(final List<Long> tokens, final ModelProfile modelProfile) {
|
||||
return this.resolve(modelProfile).detokenize(tokens, modelProfile);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
|
||||
public class TokenizerResolverTest {
|
||||
|
||||
@Test
|
||||
void resolvesTokenizerFromProfileName() {
|
||||
final var fallbackTokenizer = new FallbackTokenizer();
|
||||
final var customTokenizer = new NamedTokenizer("custom");
|
||||
final var resolver = new TokenizerResolver(List.of(fallbackTokenizer, customTokenizer));
|
||||
|
||||
assertSame(customTokenizer, resolver.resolve(this.createProfile("custom")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void prefersJTokkitAsDefaultWhenAvailable() {
|
||||
final var fallbackTokenizer = new FallbackTokenizer();
|
||||
final var jTokkitTokenizer = new NamedTokenizer(JTokkitTokenizer.NAME);
|
||||
final var resolver = new TokenizerResolver(List.of(fallbackTokenizer, jTokkitTokenizer));
|
||||
|
||||
assertSame(jTokkitTokenizer, resolver.resolve(this.createProfile(null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackWhenNoPreferredTokenizerExists() {
|
||||
final var fallbackTokenizer = new FallbackTokenizer();
|
||||
final var resolver = new TokenizerResolver(List.of(fallbackTokenizer));
|
||||
|
||||
assertSame(fallbackTokenizer, resolver.resolve(this.createProfile(null)));
|
||||
}
|
||||
|
||||
private ModelProfile createProfile(final String tokenizer) {
|
||||
return new ModelProfile(
|
||||
"",
|
||||
tokenizer,
|
||||
"cl100k_base",
|
||||
1024,
|
||||
4096,
|
||||
0.4,
|
||||
1.0,
|
||||
ModelProvider.OAI_CHAT_COMPLETION,
|
||||
"test-model",
|
||||
"http://localhost:5001",
|
||||
"",
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
null,
|
||||
null,
|
||||
"TEST");
|
||||
}
|
||||
|
||||
private static final class NamedTokenizer implements Tokenizer {
|
||||
|
||||
private final String name;
|
||||
|
||||
private NamedTokenizer(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> tokenize(final String txt, final ModelProfile modelProfile) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count(final String txt, final ModelProfile modelProfile) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String detokenize(final List<Long> tokens, final ModelProfile modelProfile) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -41,6 +44,29 @@ public class TokenizerTest {
|
||||
assertEquals("[9906, 420, 374, 264, 1495, 0, 2650, 527, 499, 3815, 30, 578, 9282, 374, 2294, 3432, 0, 7445, 40798, 42299, 12340]", tokens.toString());
|
||||
assertEquals(TEXT, tokenizer.detokenize(tokens, profile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallback() {
|
||||
final var tokenizer = new FallbackTokenizer();
|
||||
final var profile = this.modelProfileService.getModelProfile(ModelCategory.MEDIUM.getModelProfile());
|
||||
|
||||
assertEquals(47, tokenizer.count(TEXT, profile));
|
||||
final var tokens = tokenizer.tokenize(TEXT, profile);
|
||||
assertEquals(47, tokens.size());
|
||||
assertEquals("[465675044868, 28417, 8193, 495689692164, 8193, 7563522, 8193, 24833, 8193, 500236121092, 8449, 8193, 2003781635, 8193, 1701994755, 8193, 1970239747, 8193, 474215310340, 26369, 16129, 8193, 1701336067, 8193, 499850245892, 1919248387, 8193, 7563522, 8193, 418313824004, 29697, 8193, 418296853508, 30977, 8449, 8193, 435678434052, 29185, 8193, 474299593476, 30977, 8193, 474214855428, 30977, 8449, 8449, 8449]", tokens.toString());
|
||||
assertEquals(TEXT, tokenizer.detokenize(tokens, profile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackTokenizerGroupsWhitespace() {
|
||||
final var tokenizer = new FallbackTokenizer();
|
||||
final var profile = this.modelProfileService.getModelProfile(ModelCategory.MEDIUM.getModelProfile());
|
||||
final var text = " ";
|
||||
final var tokens = tokenizer.tokenize(text, profile);
|
||||
|
||||
assertEquals(2, tokens.size());
|
||||
assertEquals(text, tokenizer.detokenize(tokens, profile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tested with a Llama 3.1 8B model. The test will likely fail if you load a different model.
|
||||
|
||||
@@ -11,5 +11,3 @@ profile.model.chatcompletion=local-chatcompletion
|
||||
agent.threads=2
|
||||
|
||||
client.collectStatistics=true
|
||||
|
||||
tokenizer.strategy=llamaServer
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
category=CHAT
|
||||
prompts=
|
||||
tokenizer=llamaServer
|
||||
tokenEncoding=cl100k_base
|
||||
maxTokens=2500
|
||||
maxContextLength=16000
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
category=TEXT
|
||||
prompts=gpt-oss
|
||||
tokenizer=llamaServer
|
||||
tokenEncoding=cl100k_base
|
||||
maxTokens=2500
|
||||
maxContextLength=16000
|
||||
|
||||
Reference in New Issue
Block a user