Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
490c419
model: K2 Horizon gguf conversion code
rayendito Aug 29, 2026
992bdde
model: loading hparams and tensors in k2-horizon.cpp
rayendito Aug 30, 2026
d63ad5e
model: K2 Horizon compute graph
rayendito Aug 31, 2026
d6b148a
model: K2 Horizon compute graph adjustment and registering tokenizers
rayendito Sep 1, 2026
35999d1
model: K2 Horizon chat template and accomodate safetensors naming
rayendito Sep 1, 2026
69d3a4e
unicode : add the K2-Horizon pre-tokenizer splitter
WestWaters Sep 11, 2026
a8104b5
tests: expand K2 Horizon unicode splitter coverage
TaskPuppyNatani Sep 14, 2026
0de0799
Merge pull request #1 from TaskPuppyNatani/test/k2-pr1-expanded-tests
WestWaters Sep 15, 2026
e78bd94
unicode: handle K2 Horizon case folding and empty input
aaryamonvikram Sep 17, 2026
42adf01
Merge pull request #1 from WestWaters/k2-horizon-msvc-pretokenizer
aaryamonvikram Sep 17, 2026
ecf9741
jinja : support sequence indices in selectattr and rejectattr
bitalov Sep 27, 2026
63dded7
model : add K2 Horizon dense and MoVA support
bitalov Sep 27, 2026
93819d9
chat : support K2 Horizon reasoning and tool calls
bitalov Sep 27, 2026
d70e235
Merge the original K2 Horizon branch into the validated integration
bitalov Sep 27, 2026
f13798d
conversion: remove obsolete K2 Aurora alias
bitalov Sep 27, 2026
540e938
k2-horizon: enforce response schemas and load YaRN betas
bitalov Sep 28, 2026
cbee4a2
renaming template fixture
bitalov Sep 30, 2026
757e62e
constants conflict fix
bitalov Sep 30, 2026
fe498bd
adressing cisc comments
bitalov Oct 1, 2026
39ddb2b
adressing cisc follows ups
bitalov Oct 1, 2026
336cc79
desloppify the parser / adress aldehir comments
bitalov Oct 2, 2026
59db596
clean test-chat
bitalov Oct 2, 2026
fbee594
remove fallback : model trained mostly on high anyway
bitalov Oct 2, 2026
50abacf
fix conflicts
bitalov Oct 3, 2026
c9b78f7
fix k2 attn_v_exp tn splitting and metal fusion baseline
bitalov Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,14 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
return common_chat_params_init_kimi_k3(tmpl, params);
}

// K2 Horizon - <|ifm|im_start|> turns, <ifm|think*> reasoning picked by reasoning_effort and
// <ifm|tool_calls> sections; the three think tag pairs defeat the autoparser's reasoning detection
if (src.find("<|ifm|im_start|>") != std::string::npos &&
src.find("<ifm|tool_calls>") != std::string::npos) {
LOG_DBG("Using specialized template: K2 Horizon\n");
return common_chat_params_init_k2_horizon(tmpl, params);
}

// Ling 3.0 / Bailing V3 - <role>X</role> sections with <arg_key>/<arg_value> tagged
// tool calls. <role> sections are unique to this family among the tagged-arg templates.
if (src.find("<role>ASSISTANT</role>") != std::string::npos &&
Expand Down
79 changes: 49 additions & 30 deletions common/jinja/caps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,38 +37,57 @@ static void caps_try_execute(jinja::program & prog,
const caps_ctx_fn & ctx_fn,
const caps_json_fn & tools_fn,
const caps_analyze_fn & analyze_fn) {
context ctx;
ctx.is_get_stats = true;
jinja::global_from_json(ctx, json{
{"messages", messages_fn()},
{"tools", tools_fn ? tools_fn() : json::array()},
{"bos_token", ""},
{"eos_token", ""},
{"add_generation_prompt", true}
}, true);

if (ctx_fn) {
ctx_fn(ctx);
}
json msgs = messages_fn();
for (int attempt = 0; attempt < 2; attempt++) {
context ctx;
ctx.is_get_stats = true;
jinja::global_from_json(ctx, json{
{"messages", msgs},
{"tools", tools_fn ? tools_fn() : json::array()},
{"bos_token", ""},
{"eos_token", ""},
{"add_generation_prompt", true}
}, true);

if (ctx_fn) {
ctx_fn(ctx);
}

auto messages = ctx.get_val("messages");
auto tools = ctx.get_val("tools");

bool success = false;
std::string result;
try {
jinja::runtime runtime(ctx);
auto results = runtime.execute(prog);
auto parts = jinja::runtime::gather_string_parts(results);
result = parts->as_string().str();
success = true;
} catch (const std::exception & e) {
JJ_DEBUG("Exception during execution: %s", e.what());
result = "";
// ignore exceptions during capability analysis
}
auto messages = ctx.get_val("messages");
auto tools = ctx.get_val("tools");

bool success = false;
std::string result;
try {
jinja::runtime runtime(ctx);
auto results = runtime.execute(prog);
auto parts = jinja::runtime::gather_string_parts(results);
result = parts->as_string().str();
success = true;
} catch (const std::exception & e) {
JJ_DEBUG("Exception during execution: %s", e.what());
result = "";
// ignore exceptions during capability analysis
}

// some templates require a thinking field on every assistant turn (e.g. K2 Horizon):
// retry once with an empty reasoning_content on the assistant turns that lack one
if (!success && attempt == 0) {
bool added = false;
for (auto & msg : msgs) {
if (msg.is_object() && msg.value("role", "") == "assistant" && !msg.contains("reasoning_content")) {
msg["reasoning_content"] = "";
added = true;
}
}
if (added) {
continue;
}
}

analyze_fn(ctx, success, messages, tools, result);
analyze_fn(ctx, success, messages, tools, result);
return;
}
}

// for debugging only
Expand Down
193 changes: 193 additions & 0 deletions common/parsers/k2-horizon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#include "parsers.h"

// K2 Horizon format:
// - Reasoning: <ifm|think>...</ifm|think>, or <ifm|think_fast>/<ifm|think_faster> for medium/low reasoning_effort
// - Tool calls: <ifm|tool_calls><ifm|tool_call>...</ifm|tool_call>...</ifm|tool_calls>, one call per <ifm|tool_call>:
// xml (default): name <ifm|arg_key>k</ifm|arg_key> [<ifm|arg_type>t</ifm|arg_type>] <ifm|arg_value>v</ifm|arg_value> ...
// json: {"name": "...", "arguments": {...}}
common_chat_params common_chat_params_init_k2_horizon(const common_chat_template & tmpl,
const autoparser::generation_params & inputs) {
common_chat_params data;

// The template requires a thinking field on every assistant message
auto messages = inputs.messages;
for (auto & msg : messages) {
if (msg.value("role", "") == "assistant" && !msg.contains("reasoning_content")) {
msg["reasoning_content"] = "";
}
}

data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, messages);
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, messages);
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
data.supports_thinking = true;

const std::string effort = inputs.extra_context.value("reasoning_effort", "high");
const std::string call_format = inputs.extra_context.value("tool_call_format", "xml");

// Templates that handle enable_thinking disable it with an empty <ifm|think></ifm|think> block for every effort
const bool thinking_off = !inputs.enable_thinking && tmpl.source().find("enable_thinking") != std::string::npos;
const std::string think = thinking_off ? "ifm|think" :
effort == "medium" ? "ifm|think_fast" :
effort == "low" ? "ifm|think_faster" : "ifm|think";

const std::string GEN_PREFIX = "<|ifm|im_start|>assistant\n";
const std::string THINK_START = "<" + think + ">";
const std::string THINK_END = "</" + think + ">";
const std::string SECTION_START = "<ifm|tool_calls>";
const std::string SECTION_END = "</ifm|tool_calls>";
const std::string CALL_START = "<ifm|tool_call>";
const std::string CALL_END = "</ifm|tool_call>";
const std::string ARG_KEY = "<ifm|arg_key>";
const std::string ARG_KEY_END = "</ifm|arg_key>";
const std::string ARG_TYPE = "<ifm|arg_type>";
const std::string ARG_TYPE_END = "</ifm|arg_type>";
const std::string ARG_VAL = "<ifm|arg_value>";
const std::string ARG_VAL_END = "</ifm|arg_value>";

data.thinking_start_tag = THINK_START;
data.thinking_end_tags = { THINK_END };

data.preserved_tokens = data.thinking_end_tags;
data.preserved_tokens.insert(data.preserved_tokens.end(), {
THINK_START, SECTION_START, SECTION_END, CALL_START, CALL_END,
ARG_KEY, ARG_KEY_END, ARG_TYPE, ARG_TYPE_END, ARG_VAL, ARG_VAL_END,
});

data.message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT, "<|ifm|im_start|>assistant" },
{ COMMON_CHAT_ROLE_USER, "<|ifm|im_start|>user" },
{ COMMON_CHAT_ROLE_TOOL, "<|ifm|im_start|>tool" },
{ COMMON_CHAT_ROLE_SYSTEM, "<|ifm|im_start|>system" },
};

auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
auto has_response_format = inputs.json_schema.is_object() && !inputs.json_schema.empty();
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);

if (inputs.has_continuation()) {
const auto & msg = inputs.continue_msg;

data.generation_prompt = GEN_PREFIX + THINK_START + "\n" + msg.reasoning_content;
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
data.generation_prompt += THINK_END + msg.render_content();
}

data.prompt += data.generation_prompt;
}

auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
auto generation_prompt = p.literal(GEN_PREFIX);

auto think_end = p.choice();
for (const auto & tag : data.thinking_end_tags) {
think_end |= p.literal(tag);
}
auto think_body = p.until_one_of(data.thinking_end_tags);
auto think_block = [&](const common_peg_parser & body) {
return p.optional(THINK_START + p.space() + p.ac(body + think_end, data.thinking_end_tags));
};
auto reasoning = extract_reasoning ? think_block(p.reasoning(think_body)) : p.eps();

if (has_response_format) {
// The answer must be bare JSON, so the think block is consumed even when it is not extracted
auto thoughts = extract_reasoning ? reasoning : think_block(think_body);
return generation_prompt + (thoughts << p.content(p.schema(p.json(), "response-format", inputs.json_schema)));
}

if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
return generation_prompt + (reasoning << p.content(p.rest()));
}

auto tool_choice = p.choice();
if (call_format == "json") {
tool_choice = p.standard_json_tools(CALL_START, CALL_END, inputs.tools, false, true);
} else {
auto arg_close = p.tool_arg_close(p.literal(ARG_VAL_END));
auto arg_string = p.rule("xml-arg-string", p.ac(p.tool_arg_string_value(p.until(ARG_VAL_END)) + arg_close, ARG_VAL_END));

// The models leave out <ifm|arg_type> even when asked for xml_typed
auto arg_type = call_format == "xml_typed" ? p.optional(ARG_TYPE + p.until(ARG_TYPE_END) + ARG_TYPE_END + p.space()) : p.eps();

foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");
std::string name = function.at("name");

std::vector<common_peg_parser> required_args;
std::vector<common_peg_parser> optional_args;
foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
auto rule_name = "tool-" + name + "-arg-" + param.name;
auto types = param.schema->value_types();
auto arg_value = arg_string;
if (!types.has(common_chat_schema::TYPE_STRING)) {
arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, *param.schema)) + arg_close;
}
if (types.has(common_chat_schema::TYPE_STRING) && !types.is_only(common_chat_schema::TYPE_STRING)) {
// The string alternative accepts any text, so only the parser needs the JSON alternatives.
auto json_value = p.choice();
if (types.has(common_chat_schema::TYPE_OBJECT)) {
json_value |= p.json_object();
}
if (types.has(common_chat_schema::TYPE_ARRAY)) {
json_value |= p.json_array();
}
if (types.has(common_chat_schema::TYPE_NUMBER) || types.has(common_chat_schema::TYPE_INTEGER)) {
json_value |= p.json_number();
}
if (types.has(common_chat_schema::TYPE_BOOLEAN)) {
json_value |= p.json_bool();
}
if (types.has(common_chat_schema::TYPE_NULL)) {
json_value |= p.json_null();
}
arg_value = p.gbnf(p.atomic(p.tool_arg_json_value(json_value) + arg_close) | arg_string, "xml-arg-string");
}

auto arg = p.space() + p.tool_arg(p.tool_arg_open(ARG_KEY + p.tool_arg_name(p.literal(param.name)) + ARG_KEY_END) <<
arg_type + ARG_VAL + arg_value);
(param.required ? required_args : optional_args).push_back(p.rule(rule_name, arg));
});

auto args = p.permute("tool-" + name + "-args", required_args);
if (!optional_args.empty()) {
args = args + p.zero_or_more(p.choice(optional_args));
}

tool_choice |= p.rule("tool-" + name, p.tool(
p.tool_open(CALL_START + p.tool_name(p.literal(name)) + "\n") + p.tool_args(args) << p.tool_close(p.literal(CALL_END))));
});
}

auto required = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
auto calls = inputs.parallel_tool_calls ? tool_choice + p.zero_or_more(p.space() + tool_choice) : tool_choice;
auto tool_calls = p.trigger_rule("tool-calls", p.repeat(SECTION_START << calls << SECTION_END, required ? 1 : 0, 1));

// Keep thinking inline when required calls bypass the content parser.
if (required && !extract_reasoning) {
reasoning = p.content(think_block(think_body));
}

// A required call follows the reasoning directly, the models otherwise keep writing content
auto content = required ? p.eps() : p.content(p.until(SECTION_START));

return generation_prompt + (reasoning << content << tool_calls);
});

data.parser = parser.save();

if (include_grammar) {
data.grammar_lazy = !(has_response_format || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED);
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
parser.build_grammar(builder, data.grammar_lazy);
});

if (data.grammar_lazy) {
data.grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, SECTION_START },
};
}
}

return data;
}
2 changes: 2 additions & 0 deletions common/parsers/parsers.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ common_chat_params common_chat_params_init_gigachat_v3(const common_chat_templat

common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & tmpl, const autoparser::generation_params & inputs);

common_chat_params common_chat_params_init_k2_horizon(const common_chat_template & tmpl, const autoparser::generation_params & inputs);

common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & tmpl, const autoparser::generation_params & inputs);

common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
Expand Down
1 change: 1 addition & 0 deletions common/parsers/sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set(LLAMA_CHAT_PARSERS_SOURCES
${CMAKE_CURRENT_LIST_DIR}/gemma4.cpp
${CMAKE_CURRENT_LIST_DIR}/gigachat-v3.cpp
${CMAKE_CURRENT_LIST_DIR}/gpt-oss.cpp
${CMAKE_CURRENT_LIST_DIR}/k2-horizon.cpp
${CMAKE_CURRENT_LIST_DIR}/kimi-k2.cpp
${CMAKE_CURRENT_LIST_DIR}/kimi-k3.cpp
${CMAKE_CURRENT_LIST_DIR}/ling3.cpp
Expand Down
1 change: 1 addition & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"JinaBertForMaskedLM": "bert",
"JinaBertModel": "bert",
"JinaEmbeddingsV5Model": "bert",
"K2HorizonForCausalLM": "k2_horizon",
"KORMoForCausalLM": "qwen",
"KimiK25ForConditionalGeneration": "deepseek",
"KimiK3ForConditionalGeneration": "kimi_k3",
Expand Down
8 changes: 7 additions & 1 deletion conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
if weight_map is None or not isinstance(weight_map, dict):
raise ValueError(f"Can't load 'weight_map' from {index_name!r}")
tensor_names_from_index.update(weight_map.keys())
part_dict: dict[str, None] = dict.fromkeys(weight_map.values(), None) # ty: ignore[invalid-assignment]

Check warning on line 257 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:257:91: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
part_names = sorted(part_dict.keys())
else:
weight_map = {}
Expand Down Expand Up @@ -1529,7 +1529,7 @@
self.gguf_writer.add_expert_group_used_count(n_group_used)
logger.info(f"gguf: expert groups used count = {n_group_used}")

if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn"], optional=True)) is not None:
if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn", "router_score_func"], optional=True)) is not None:
if score_func == "sigmoid":
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
elif score_func == "softmax":
Expand Down Expand Up @@ -1586,15 +1586,15 @@

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1589 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1589:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1590 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1590:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

tokpre = self.get_vocab_base_pre(tokenizer)

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 1594 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1594:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]

Check warning on line 1595 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1595:52: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]

Check warning on line 1597 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1597:64: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

for i in range(vocab_size):
if i not in reverse_vocab:
Expand All @@ -1607,7 +1607,7 @@
# To avoid unexpected issues - we make sure to normalize non-normalized tokens
if not added_tokens_decoder[i].normalized:
previous_token = token
token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False)) # ty: ignore[unresolved-attribute, invalid-assignment]

Check warning on line 1610 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1610:102: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
if previous_token != token:
logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")

Expand Down Expand Up @@ -1713,6 +1713,9 @@
if chkhsh == "0a766d034107bc736a3f2dc4968fd62e54a3570f1454443e0c5a4cc6bd7941ed":
# ref: https://huggingface.co/XHToken/Spark-X2.5-1.7B
res = "spark2_5"
if chkhsh == "1f9825a388f700a6b591722f17d470cbbcf10973ece35d2fd14239a14110ae1a":
# ref: https://huggingface.co/IFM/K2-Horizon-0.9B
res = "k2-horizon"
if chkhsh == "0ef9807a4087ebef797fc749390439009c3b9eda9ad1a097abbe738f486c01e5":
# ref: https://huggingface.co/meta-llama/Meta-Llama-3-8B
res = "llama-bpe"
Expand Down Expand Up @@ -1941,6 +1944,9 @@
if chkhsh == "4b05e02dad1c5ae07d266fd3342ddb644c6f6be058d728bc0a33af31a1d6ee66":
# ref: https://huggingface.co/jhu-clsp/mmBERT-base
res = "mmbert"
if chkhsh == "a9af07a84191f55098b248ae6f3dfe9e32d3190bebe8eafd91c1ddec9bc3449f":
# ref: https://huggingface.co/IFM/K2-Horizon-36B
res = "k2-horizon"

if res is None:
logger.warning("\n")
Expand Down Expand Up @@ -1989,10 +1995,10 @@
def _set_vocab_hybriddna(self):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1998 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1998:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1999 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1999:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 2001 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:2001:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
# k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get
# dropped by get_vocab(); a reserved marker suffix (U+E000) keeps each
# k-mer's own id (llama.cpp strips it on detokenization)
Expand Down
Loading
Loading