blobbybob commited on
Commit
f7d1bd2
·
verified ·
1 Parent(s): 60109c5

Initial release: v3.1 merged weights, q8_0 GGUF, usage harness, card

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ gguf/compressor-v31-q8_0.gguf filter=lfs diff=lfs merge=lfs -text
37
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen2.5-Coder-1.5B-Instruct
4
+ language:
5
+ - en
6
+ - zh
7
+ pipeline_tag: text-generation
8
+ tags:
9
+ - reasoning-compression
10
+ - cjk
11
+ - chain-of-thought
12
+ - distillation
13
+ - qwen2.5
14
+ ---
15
+
16
+ ![tessera-compressor](banner.png)
17
+
18
+ # tessera-compressor
19
+
20
+ A 1.5B model that compresses English reasoning text into a telegraphic CJK/symbol register under deterministic fidelity gates. It minted the training data for [Tessera-Preview-9B](https://huggingface.co/ZelligeAI/tessera-preview-9b) and replaces the frontier-model teacher that originally produced the register: any English reasoning dataset becomes compressed-register training data at local-inference cost, with no API key and no external dependency.
21
+
22
+ Example (real training pair, 85 to 49 tokens):
23
+
24
+ ```text
25
+ EN : So the classes are: - Integer (line 32) - Boolean (line 262) - BitString (line 341)
26
+ - OctetString (line 693) ... Let me look at the base class to see if it defines __mul__:
27
+
28
+ CJK: Integer(line32),Boolean(line262),BitString(line341),OctetString(line693). 查基类是否定义__mul__:
29
+ ```
30
+
31
+ ## How it works
32
+
33
+ The compressor operates on passages, not whole blocks. A reasoning block is segmented (code fences stay atomic), sentences are grouped into step-sized passages, each passage is classified as fact-dense or narrative, and the model compresses it against the tail of the chain built so far. Every model output then passes a deterministic gate: the sets of numbers, identifiers, and operators must survive, the output must not blow up in length, and it must beat a rules-only compression of the same passage in token count. A passage that fails any check falls back to the rules-only version, so a bad generation costs savings, never content.
34
+
35
+ ## Acceptance record
36
+
37
+ Measured on 103 held-out reasoning blocks the model never trained on, under criteria fixed before evaluation:
38
+
39
+ | Criterion | Result |
40
+ | --- | --- |
41
+ | Per-passage fidelity gate (numbers, identifiers, operators survive) | 99.0% |
42
+ | Median per-passage compression ratio (output/input tokens) | 0.716 |
43
+ | CJK adoption | 98.9% of compressed passages |
44
+ | Judged semantic equivalence | 103/103 blocks (teacher references on the same blocks: 97.1%) |
45
+ | Degenerate outputs | 0 |
46
+ | Net corpus savings (after 24% rules-only fallback) | 30.4% |
47
+
48
+ On whole thinks in downstream production use (45,202 pairs), the compressed rendering costs a median 0.58x the tokens of its English source.
49
+
50
+ ## Files
51
+
52
+ - Root: merged model, standard Hugging Face format (bf16). Base: Qwen2.5-Coder-1.5B-Instruct, LoRA r=16 merged in.
53
+ - `gguf/compressor-v31-q8_0.gguf`: llama.cpp quantization, validated behaviorally (scores 4/4 on the same acceptance suite). q4_k_m showed visible drift and is not published.
54
+ - `scripts/`: the complete usage harness. No tokens or keys required anywhere.
55
+
56
+ ## Usage
57
+
58
+ Serve the model behind any OpenAI-compatible endpoint:
59
+
60
+ ```bash
61
+ vllm serve ZelligeAI/tessera-compressor --port 8001
62
+ # or, CPU-friendly:
63
+ llama-server -m gguf/compressor-v31-q8_0.gguf --port 8001
64
+ ```
65
+
66
+ Then run the harness:
67
+
68
+ ```bash
69
+ cd scripts && pip install -r requirements.txt
70
+
71
+ # compress one reasoning block from a text file
72
+ python compress.py --in think.txt --endpoint http://localhost:8001/v1
73
+
74
+ # compress a corpus: {"id": ..., "text": ...} per JSONL line
75
+ python compress.py --in blocks.jsonl --out compressed.jsonl \
76
+ --endpoint http://localhost:8001/v1
77
+ ```
78
+
79
+ Output records carry the compressed text, source and output token counts, and per-block harness stats (model-accepted vs rules-fallback passage counts).
80
+
81
+ `scripts/` contents:
82
+
83
+ - `compress.py`: the driver. Segment, classify, compress per passage with chain context, gate, fall back on failure.
84
+ - `segmenting.py`: segmentation, passage grouping, fact extraction, classification, and the fidelity gate. Pure text processing.
85
+ - `tokenmax.py`: deterministic token-saving substitutions, used as the rules-only fallback and as a post-processor.
86
+
87
+ One note on token counting: the gate compares token counts under a tokenizer you choose (`--tokenizer`, default this repo). To reproduce the acceptance harness exactly, point it at the tokenizer of the model you are minting data for (the acceptance run used the Qwen3.5 target tokenizer).
88
+
89
+ Throughput on the acceptance hardware was 19.6K blocks/hour on one GPU, which is why the marginal cost of minting compressed data rounds to zero.
90
+
91
+ ## License
92
+
93
+ Apache-2.0, same as the base model.
banner.png ADDED
chat_template.jinja ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0]['role'] == 'system' %}
4
+ {{- messages[0]['content'] }}
5
+ {%- else %}
6
+ {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
7
+ {%- endif %}
8
+ {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
9
+ {%- for tool in tools %}
10
+ {{- "\n" }}
11
+ {{- tool | tojson }}
12
+ {%- endfor %}
13
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
14
+ {%- else %}
15
+ {%- if messages[0]['role'] == 'system' %}
16
+ {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
17
+ {%- else %}
18
+ {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
19
+ {%- endif %}
20
+ {%- endif %}
21
+ {%- for message in messages %}
22
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
23
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
24
+ {%- elif message.role == "assistant" %}
25
+ {{- '<|im_start|>' + message.role }}
26
+ {%- if message.content %}
27
+ {{- '\n' + message.content }}
28
+ {%- endif %}
29
+ {%- for tool_call in message.tool_calls %}
30
+ {%- if tool_call.function is defined %}
31
+ {%- set tool_call = tool_call.function %}
32
+ {%- endif %}
33
+ {{- '\n<tool_call>\n{"name": "' }}
34
+ {{- tool_call.name }}
35
+ {{- '", "arguments": ' }}
36
+ {{- tool_call.arguments | tojson }}
37
+ {{- '}\n</tool_call>' }}
38
+ {%- endfor %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- elif message.role == "tool" %}
41
+ {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %} {{- '<|im_start|>user' }}
42
+ {%- endif %}
43
+ {{- '\n<tool_response>\n' }}
44
+ {{- message.content }}
45
+ {{- '\n</tool_response>' }}
46
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
47
+ {{- '<|im_end|>\n' }}
48
+ {%- endif %}
49
+ {%- endif %}
50
+ {%- endfor %}
51
+ {%- if add_generation_prompt %}
52
+ {{- '<|im_start|>assistant\n' }}
53
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen2ForCausalLM"
4
+ ],
5
+ "attention_dropout": 0.0,
6
+ "bos_token_id": null,
7
+ "torch_dtype": "bfloat16",
8
+ "eos_token_id": 151645,
9
+ "hidden_act": "silu",
10
+ "hidden_size": 1536,
11
+ "initializer_range": 0.02,
12
+ "intermediate_size": 8960,
13
+ "layer_types": [
14
+ "full_attention",
15
+ "full_attention",
16
+ "full_attention",
17
+ "full_attention",
18
+ "full_attention",
19
+ "full_attention",
20
+ "full_attention",
21
+ "full_attention",
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention",
28
+ "full_attention",
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention"
42
+ ],
43
+ "max_position_embeddings": 32768,
44
+ "max_window_layers": 21,
45
+ "model_type": "qwen2",
46
+ "num_attention_heads": 12,
47
+ "num_hidden_layers": 28,
48
+ "num_key_value_heads": 2,
49
+ "pad_token_id": 151665,
50
+ "rms_norm_eps": 1e-06,
51
+ "rope_parameters": {
52
+ "rope_theta": 1000000.0,
53
+ "rope_type": "default"
54
+ },
55
+ "sliding_window": null,
56
+ "tie_word_embeddings": true,
57
+ "unsloth_fixed": true,
58
+ "unsloth_version": "2026.7.1",
59
+ "use_cache": true,
60
+ "use_sliding_window": false,
61
+ "vocab_size": 151936
62
+ }
generation_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_sample": true,
3
+ "eos_token_id": [
4
+ 151645,
5
+ 151643
6
+ ],
7
+ "max_length": 32768,
8
+ "pad_token_id": 151665,
9
+ "repetition_penalty": 1.1,
10
+ "temperature": 0.7,
11
+ "top_k": 20,
12
+ "top_p": 0.8,
13
+ "transformers_version": "5.5.0"
14
+ }
gguf/compressor-v31-q8_0.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce8b7b729409c6ae35d8a81fb11fcc9c286c924ec4e7d0b4b052d742af285f4d
3
+ size 1646572480
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5660fdaa65f175f6aafb2ebd35e7e6d24e535f0deecc949cd92cc7f7858a3524
3
+ size 3087467144
scripts/compress.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ compress.py — Compress English reasoning text into the telegraphic CJK register
4
+ using tessera-compressor behind any OpenAI-compatible endpoint (vLLM, llama.cpp
5
+ server, etc.). No API keys or HF token required; the endpoint is yours.
6
+
7
+ This is the same harness the compressor was accepted under: segment the block,
8
+ group sentences into step-sized passages, classify each passage, compress it
9
+ against the chain built so far, then run the deterministic fidelity gate. A
10
+ passage that fails the gate falls back to a rules-only compression, so a bad
11
+ model output costs savings, never content.
12
+
13
+ Serve the model first, e.g.:
14
+ vllm serve ZelligeAI/tessera-compressor --port 8001
15
+ or with the GGUF:
16
+ llama-server -m compressor-v31-q8_0.gguf --port 8001
17
+
18
+ Then:
19
+ # one block from a text file
20
+ python compress.py --in think.txt --endpoint http://localhost:8001/v1
21
+
22
+ # a JSONL corpus: {"id": ..., "text": ...} per line
23
+ python compress.py --in blocks.jsonl --out compressed.jsonl \
24
+ --endpoint http://localhost:8001/v1
25
+
26
+ Token counting: the fidelity gate compares token counts under a target
27
+ tokenizer. For results matching the accepted harness, point --tokenizer at the
28
+ model you are producing training data FOR (default: the compressor's own
29
+ tokenizer, which is close but not identical to the Qwen3.5 target used in the
30
+ acceptance run).
31
+ """
32
+ import argparse
33
+ import json
34
+ import sys
35
+
36
+ from openai import OpenAI
37
+ from tokenizers import Tokenizer
38
+
39
+ from segmenting import segment, group_steps, classify_passage, facts, gate
40
+ from tokenmax import _apply_subs
41
+
42
+ PASSAGE_SYSTEM = (
43
+ "你是推理压缩器。Re-notate the NEXT PASSAGE of a reasoning chain into telegraphic "
44
+ "CJK/symbol notation. Every NEW logical step, fact, number and identifier must "
45
+ "survive — unless already stated in the chain. Never restate chain content. "
46
+ "[passage=load]: step-lossless telegraphic. [passage=narr]: minimal stubs "
47
+ "(试X→否). Output only the re-notated continuation."
48
+ )
49
+
50
+ MAX_NEW_TOKENS = 512
51
+
52
+
53
+ def compress_block(text, client, model, ntok):
54
+ """Compress one reasoning block. Returns (compressed_text, stats)."""
55
+ segs = group_steps(segment(text))
56
+ chain, seen = [], set()
57
+ stats = {"segments": len(segs), "model_ok": 0, "fallback": 0,
58
+ "narr_skipped": 0, "code": 0, "calls": 0}
59
+
60
+ for kind, s in segs:
61
+ if kind == "code":
62
+ chain.append(s)
63
+ seen |= facts(s)
64
+ stats["code"] += 1
65
+ continue
66
+ cls = classify_passage(s, seen, ntok)
67
+ novel = facts(s) - seen
68
+ rules_s, _ = _apply_subs(s)
69
+ if not rules_s.strip():
70
+ continue
71
+ tail = "\n".join(chain)[-500:] or "(start)"
72
+ stats["calls"] += 1
73
+ r = client.chat.completions.create(
74
+ model=model, temperature=0.0, max_tokens=MAX_NEW_TOKENS,
75
+ messages=[
76
+ {"role": "system", "content": PASSAGE_SYSTEM},
77
+ {"role": "user", "content": f"[passage={cls}]\n链:\n{tail}\n\n段:\n{s[:2000]}"},
78
+ ],
79
+ extra_body={"repetition_penalty": 1.15},
80
+ )
81
+ out = (r.choices[0].message.content or "").strip()
82
+
83
+ if out == "∅" and cls == "narr" and not novel:
84
+ stats["narr_skipped"] += 1
85
+ seen |= facts(s)
86
+ continue
87
+ if gate(s, rules_s, out, ntok) is None:
88
+ chain.append(out)
89
+ stats["model_ok"] += 1
90
+ else:
91
+ chain.append(rules_s)
92
+ stats["fallback"] += 1
93
+ seen |= facts(s)
94
+
95
+ return "\n".join(chain), stats
96
+
97
+
98
+ def main():
99
+ ap = argparse.ArgumentParser(description=__doc__,
100
+ formatter_class=argparse.RawDescriptionHelpFormatter)
101
+ ap.add_argument("--in", dest="inp", required=True,
102
+ help=".txt (one block) or .jsonl ({'id','text'} per line)")
103
+ ap.add_argument("--out", default=None, help="output JSONL (default: stdout)")
104
+ ap.add_argument("--endpoint", default="http://localhost:8001/v1")
105
+ ap.add_argument("--model", default="ZelligeAI/tessera-compressor",
106
+ help="served model name at the endpoint")
107
+ ap.add_argument("--tokenizer", default="ZelligeAI/tessera-compressor",
108
+ help="HF repo id or local tokenizer.json for gate token counts")
109
+ args = ap.parse_args()
110
+
111
+ if args.tokenizer.endswith(".json"):
112
+ tok = Tokenizer.from_file(args.tokenizer)
113
+ else:
114
+ tok = Tokenizer.from_pretrained(args.tokenizer)
115
+
116
+ def ntok(s):
117
+ return len(tok.encode(s).ids) if s else 0
118
+
119
+ client = OpenAI(base_url=args.endpoint, api_key="none")
120
+
121
+ if args.inp.endswith(".jsonl"):
122
+ rows = [json.loads(l) for l in open(args.inp) if l.strip()]
123
+ else:
124
+ rows = [{"id": args.inp, "text": open(args.inp).read()}]
125
+
126
+ sink = open(args.out, "w") if args.out else sys.stdout
127
+ for row in rows:
128
+ compressed, stats = compress_block(row["text"], client, args.model, ntok)
129
+ rec = {"id": row.get("id"), "compressed": compressed,
130
+ "src_tokens": ntok(row["text"]), "out_tokens": ntok(compressed),
131
+ "harness": stats}
132
+ sink.write(json.dumps(rec, ensure_ascii=False) + "\n")
133
+ sink.flush()
134
+ print(f"[{row.get('id')}] {rec['src_tokens']} -> {rec['out_tokens']} tokens "
135
+ f"(model_ok={stats['model_ok']} fallback={stats['fallback']})",
136
+ file=sys.stderr)
137
+ if args.out:
138
+ sink.close()
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()
scripts/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ openai>=1.0
2
+ tokenizers>=0.15
scripts/segmenting.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ segmenting.py — Passage segmentation, classification, and fidelity gates for the
3
+ tessera-compressor harness.
4
+
5
+ Extracted from the harness the compressor was accepted under (same functions the
6
+ teacher mint used). Pure text processing: no network, no credentials.
7
+
8
+ Flow: segment -> group_steps -> classify_passage per passage -> model call ->
9
+ gate -> rules fallback on failure. A failed passage costs a few dozen tokens of
10
+ savings, never content.
11
+ """
12
+ import re
13
+
14
+ CJK = re.compile(r'[一-鿿㐀-䶿]')
15
+ NUM = re.compile(r'\d+(?:\.\d+)?')
16
+ IDENT = re.compile(r'`[^`\n]+`|\b[A-Za-z]+(?:_[A-Za-z0-9]+)+\b|\b[a-z]+[A-Z][A-Za-z0-9]*\b')
17
+ FENCE = re.compile(r'```.*?```', re.DOTALL)
18
+ SENT_SPLIT = re.compile(r'(?<=[.!?;])\s+')
19
+ _LIST_MARKER = re.compile(r'(?:^|[\n\s(])(\d{1,2})[.)]\s')
20
+ _OPS = set('+-*/=<>≤≥≠∈∀∃¬→⇒%^{}[]')
21
+
22
+
23
+ def segment(text):
24
+ """Split a reasoning block into ordered segments; code fences are atomic and marked."""
25
+ segs = [] # (kind, text) kind ∈ {'code','prose'}
26
+ pos = 0
27
+ for m in FENCE.finditer(text):
28
+ before = text[pos:m.start()]
29
+ segs.extend(('prose', s) for s in _split_prose(before))
30
+ segs.append(('code', m.group(0)))
31
+ pos = m.end()
32
+ segs.extend(('prose', s) for s in _split_prose(text[pos:]))
33
+ return [(k, s) for k, s in segs if s.strip()]
34
+
35
+
36
+ def _split_prose(text):
37
+ out = []
38
+ for line in text.split('\n'):
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+ out.extend(s.strip() for s in SENT_SPLIT.split(line) if s.strip())
43
+ return out
44
+
45
+
46
+ def group_steps(segs, max_words=160, max_sents=10):
47
+ """Merge consecutive prose sentences into step-sized passages; code stays atomic."""
48
+ out, buf, words = [], [], 0
49
+
50
+ def flush():
51
+ nonlocal buf, words
52
+ if buf:
53
+ out.append(('prose', ' '.join(buf)))
54
+ buf, words = [], 0
55
+
56
+ for kind, s in segs:
57
+ if kind == 'code':
58
+ flush()
59
+ out.append((kind, s))
60
+ continue
61
+ buf.append(s)
62
+ words += len(s.split())
63
+ if words >= max_words or len(buf) >= max_sents:
64
+ flush()
65
+ flush()
66
+ return out
67
+
68
+
69
+ def facts(s):
70
+ """Numbers + identifiers that must survive compression.
71
+ List-enumeration markers ("1. Load...") are structure, not facts."""
72
+ nums = set(NUM.findall(s)) - set(_LIST_MARKER.findall(s))
73
+ idents = set(i.strip('`') for i in IDENT.findall(s))
74
+ return nums | idents
75
+
76
+
77
+ def facts_preserved(src, out):
78
+ """Substring presence — regex \\b breaks against adjacent CJK chars.
79
+ Returns the list of MISSING facts (empty list = all preserved)."""
80
+ out_n = out.replace(',', '')
81
+ return [f for f in facts(src) if f.replace(',', '') not in out_n]
82
+
83
+
84
+ def classify_passage(seg, seen_facts, ntok):
85
+ """'load' = fact-dense or novel-fact-bearing (step-faithful treatment);
86
+ 'narr' = search/narrative (stub treatment).
87
+ ntok is a callable: text -> token count under your target tokenizer."""
88
+ f = facts(seg)
89
+ novel = f - seen_facts
90
+ toks = max(ntok(seg), 1)
91
+ dens = (len(NUM.findall(seg)) + len(IDENT.findall(seg))
92
+ + sum(seg.count(o) for o in _OPS)) / toks
93
+ if novel and (dens >= 0.08 or len(novel) >= 3):
94
+ return 'load'
95
+ if dens >= 0.15:
96
+ return 'load'
97
+ return 'narr'
98
+
99
+
100
+ def gate(src_seg, rules_seg, out, ntok):
101
+ """Deterministic per-passage fidelity gate.
102
+ Returns None if the model output is admissible, else a short fail-reason
103
+ string; on failure the caller uses rules_seg instead."""
104
+ if not out or not out.strip():
105
+ return "empty"
106
+ if '```' in out:
107
+ return "fence"
108
+ if len(out) > 2 * len(src_seg) + 40: # explanation/blow-up guard
109
+ return "blowup"
110
+ if facts_preserved(src_seg, out): # every fact survives (substring check)
111
+ return "facts"
112
+ if ntok(out) > ntok(rules_seg): # must beat the deterministic version
113
+ return "tokens"
114
+ return None
scripts/tokenmax.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tokenmax.py — Deterministic token-maxing post-processor for compressed think blocks.
3
+
4
+ Applies ONLY substitutions that are verified to save tokens on the Qwen 248K tokenizer
5
+ (OmniCoder-9B / Qwen3.5). Every substitution was tested in-context (not isolation) to
6
+ confirm real token savings without boundary interference.
7
+
8
+ Design:
9
+ - LLM does semantic compression (what to keep vs drop)
10
+ - This code enforces consistent notation deterministically
11
+ - GUARD: only returns the processed version if ntok(result) < ntok(original)
12
+ - Idempotent: safe to run multiple times
13
+
14
+ Usage:
15
+ from caveman.compress.tokenmax import tokenmax, tokenmax_with_stats
16
+ compressed = tokenmax(think_text, tokenizer)
17
+ compressed, stats = tokenmax_with_stats(think_text, tokenizer)
18
+
19
+ Verified: 2026-05-31 on Qwen 248K vocab. 27/28 substitutions save in-context.
20
+ Zero false positives. One zero-effect (贪心 for "greedy" — boundary-dependent).
21
+ """
22
+
23
+ import re
24
+ from typing import Optional
25
+
26
+ # ── Phase 1: Filler drops ──────────────────────────────────────────────
27
+ # Phrases that carry zero information in compressed reasoning.
28
+ # Only patterns that are NEVER load-bearing in a think block.
29
+ _FILLER_PATTERNS = [
30
+ # Metacognition (the model narrating its own process)
31
+ r"\bI need to\b",
32
+ r"\bwe need to\b",
33
+ r"\bI will\b",
34
+ r"\bI'll\b",
35
+ r"\blet me\b",
36
+ r"\blet's\b",
37
+ r"\bI want to\b",
38
+ r"\bI should\b",
39
+ r"\bwe should\b",
40
+ # Hedging
41
+ r"\bprobably\b",
42
+ r"\bbasically\b",
43
+ r"\bessentially\b",
44
+ r"\bit seems like\b",
45
+ # Filler transitions
46
+ r"\bin order to\b",
47
+ r"\bfirst of all\b",
48
+ r"\bin other words\b",
49
+ r"\bon the other hand\b",
50
+ r"\bmore specifically\b",
51
+ r"\bto be more precise\b",
52
+ r"\band so on\b",
53
+ # Conversational padding (require word boundary at end to avoid "Greatest", "Perfectly")
54
+ r"\bGreat\b[,!.]?\s*",
55
+ r"\bPerfect\b[,!.]?\s*",
56
+ # Obvious statements
57
+ r"\bAs (?:we|you) can see\b",
58
+ r"\bAs mentioned (?:above|earlier|before)\b",
59
+ ]
60
+
61
+ # ── Phase 2: Phrase → cheapest token ───────────────────────────────────
62
+ # Ordered LONGEST FIRST to prevent partial matches.
63
+ # Each entry: (regex_pattern, replacement, category)
64
+ # Categories: 'cjk', 'symbol', 'abbrev' — for stats tracking.
65
+ _SUBSTITUTIONS = [
66
+ # ── COMPOUND PATTERNS FIRST (must fire before their components) ──
67
+
68
+ # Verbose comparison phrases (+5t savings)
69
+ (r'\bis\s+greater\s+than\s+or\s+equal\s+to\b', '≥', 'symbol'), # +5t
70
+ (r'\bis\s+less\s+than\s+or\s+equal\s+to\b', '≤', 'symbol'), # +5t
71
+
72
+ # Verbose discourse phrases (+3t savings)
73
+ (r'\bwe\s+can\s+see\s+that\b', '可知', 'cjk'), # +3t
74
+ (r'\bat\s+the\s+same\s+time\b', '同时', 'cjk'), # +3t
75
+ (r'\bthat\s+is\s+to\s+say\b', '即', 'cjk'), # +3t
76
+
77
+ # Multi-word phrases (+2t savings)
78
+ (r'\bin\s+this\s+case\b', '此时', 'cjk'), # +2t
79
+ (r'\bthe\s+number\s+of\b', '个数', 'cjk'), # +2t
80
+ (r'\bis\s+equal\s+to\b', '等于', 'cjk'), # +2t
81
+
82
+ # Complexity boilerplate (biggest per-occurrence savings)
83
+ (r'[Oo]\(n\)\s*time[,;]?\s*[Oo]\(n\)\s*space\.?', 'O(n|n).', 'abbrev'),
84
+ (r'[Oo]\(n\)\s*time[,;]?\s*[Oo]\(1\)\s*space\.?', 'O(n|1).', 'abbrev'),
85
+ (r'[Oo]\(n\s*log\s*n\)\s*time[,;]?\s*[Oo]\(n\)\s*space', 'O(n㏒n|n)', 'abbrev'),
86
+ (r'[Oo]\(n\s*log\s*n\)\s*time[,;]?\s*[Oo]\(1\)\s*space', 'O(n㏒n|1)', 'abbrev'),
87
+ (r'[Tt]ime\s*complexity[:\s]+', 'T=', 'abbrev'),
88
+ (r'[Ss]pace\s*complexity[:\s]+', 'S=', 'abbrev'),
89
+
90
+ # Multi-word compounds (BEFORE their single-word components)
91
+ (r'\bassume without loss of generality\b', '设 不妨', 'cjk'), # before "assume"
92
+ (r'\bproof by contradiction\b', '反证', 'cjk'), # before "proof", "contradiction"
93
+ (r'\bnecessary and sufficient\b', '充要', 'cjk'), # before "sufficient"
94
+ (r'\bnot equal(?:\s+to)?\b', '≠', 'symbol'), # before "is not", "does not"
95
+ (r'\bif and only if\b', 'iff', 'abbrev'), # before "for all"
96
+ (r'\bmuch greater than\b', '≫', 'symbol'), # before "greater than"
97
+ (r'\bkeep track(?:\s+of)?\b', '记录', 'cjk'), # before article strip
98
+ (r'\bin ascending order\b', 'asc', 'abbrev'),
99
+ (r'\bin descending order\b', 'desc', 'abbrev'),
100
+ (r'\bmaximum value\b', '最大值', 'cjk'),
101
+ (r'\bminimum value\b', '最小值', 'cjk'),
102
+ (r'\breturn value\b', '返回値', 'cjk'),
103
+ (r'\brather than\b', '而非', 'cjk'),
104
+ (r'\baccording to\b', '按照', 'cjk'),
105
+
106
+ # DS compounds (before components)
107
+ (r'\bdoubly linked list\b', 'DLL', 'abbrev'), # BEFORE "linked list"
108
+ (r'\bbinary indexed tree\b', 'BIT', 'abbrev'), # BEFORE "binary"
109
+ (r'\bminimum spanning tree\b', 'MST', 'abbrev'),
110
+ (r'\bdepth[- ]first search\b', 'DFS', 'abbrev'),
111
+ (r'\bbreadth[- ]first search\b', 'BFS', 'abbrev'),
112
+ (r'\bdynamic programming\b', 'DP', 'abbrev'),
113
+ (r'\bdivide and conquer\b', '分治', 'cjk'),
114
+ (r'\bmonot(?:onic|one)\s*stack\b', '单调栈', 'cjk'),
115
+ (r'\btime limit exceeded\b', '超时', 'cjk'),
116
+ (r'\bout of bounds\b', '越界', 'cjk'),
117
+ (r'\bremove duplicates?\b', '去重', 'cjk'),
118
+ (r'\benumerate all\b', '穷举', 'cjk'),
119
+ (r'\bbinary search\b', '二分', 'cjk'),
120
+ (r'\bsliding window\b', 'sw', 'abbrev'),
121
+ (r'\bunion[- ]find\b', 'UF', 'abbrev'),
122
+ (r'\btopological sort\b', '拓扑序', 'cjk'),
123
+ (r'\bshortest path\b', 'sp', 'abbrev'),
124
+ (r'\blinked list\b', 'LL', 'abbrev'),
125
+ (r'\bpriority queue\b', 'heap', 'abbrev'),
126
+ (r'\bprefix sum\b', 'ps', 'abbrev'),
127
+ (r'\bbrute force\b', '暴力', 'cjk'),
128
+ (r'\bno solution\b', '无解', 'cjk'),
129
+ (r'\bedge cases?\b', '边界', 'cjk'),
130
+ (r'\bbase case\b', 'bc', 'abbrev'),
131
+ (r'\bworst case\b', 'wc', 'abbrev'),
132
+
133
+ # ── SINGLE-WORD SUBSTITUTIONS (safe after compounds consumed) ──
134
+
135
+ # +4t savings
136
+ (r'\bobviously\b', '显然', 'cjk'),
137
+ # +3t savings
138
+ (r'\bredundant\b', '冗余', 'cjk'),
139
+ (r'\bsatisf(?:y|ies|ied)\b', '满足', 'cjk'), # +2t, 333x in data
140
+ (r'\bunsorted\b', '无序', 'cjk'),
141
+ (r'\bdue to\b', '由于', 'cjk'),
142
+ (r'\bhence\b', '故', 'cjk'),
143
+ (r'\bnamely\b', '即', 'cjk'),
144
+ (r'\bassume\b', '设', 'cjk'),
145
+ (r'\bsuppose\b', '设', 'cjk'),
146
+ (r'\bderive\b', '推导', 'cjk'),
147
+ # +2t savings
148
+ (r'\bmonotone\b', '单调', 'cjk'),
149
+ (r'\bconvergent\b', '收敛', 'cjk'),
150
+ (r'\bdivergent\b', '发散', 'cjk'),
151
+ (r'\bcommutative\b', '交换', 'cjk'),
152
+ (r'\bdeterministic\b', '确定', 'cjk'),
153
+ (r'\bprobabilistic\b', '概率', 'cjk'),
154
+ (r'\bprove\b', '证明', 'cjk'),
155
+ (r'\bproof\b', '证明', 'cjk'),
156
+ (r'\bflip\b', '翻转', 'cjk'),
157
+ (r'\bsorted\b(?!\s*[=(\[])', '有序', 'cjk'), # not before = ( [ (assignment/call)
158
+ # +1t savings
159
+ (r'\bbacktrack(?:ing)?\b', '回溯', 'cjk'),
160
+ (r'\btravers(?:e|al|ing)\b', '遍历', 'cjk'),
161
+ (r'\brecursi(?:on|ve|vely)\b', '递归', 'cjk'),
162
+ (r'\bcontradiction\b', '矛盾', 'cjk'),
163
+ (r'\bsufficient\b', '充分', 'cjk'),
164
+ (r'\bequivalent\b', '等价', 'cjk'),
165
+ (r'\bsymmetric\b', '对称', 'cjk'),
166
+ (r'\binvariant\b', '不变', 'cjk'),
167
+ (r'\bexponential\b', '指数', 'cjk'),
168
+ (r'\bpermutation\b', '排列', 'cjk'),
169
+ (r'\badjacent\b', '相邻', 'cjk'),
170
+ (r'\boptimal\b', '最优', 'cjk'),
171
+ (r'\bfeasible\b', '可行', 'cjk'),
172
+ (r'\binduction\b', '归纳', 'cjk'),
173
+ (r'\bmaintain\b', '维护', 'cjk'),
174
+ (r'\bswap\b(?!\s*[=(\[])', '交换', 'cjk'), # not before = ( [ (assignment/call)
175
+ (r'\bcumulative\b', '累积', 'cjk'),
176
+ (r'\bquotient\b', '商', 'cjk'),
177
+ (r'\bmemoiz(?:ation|e)\b', 'memo', 'abbrev'),
178
+ # +2t savings (mined from v19 data)
179
+ (r'\bmathematical\b', '数学', 'cjk'),
180
+ (r'\bcorresponding(?:ly)?\b', '对应', 'cjk'),
181
+ (r'\brequirement\b', '需求', 'cjk'),
182
+ # +1t savings (mined from v19 data)
183
+ (r'\bcomplexity\b', '复杂度', 'cjk'),
184
+ (r'\bsimilarly\b', '同理', 'cjk'),
185
+ (r'\bsubstitut(?:e|ion)\b', '代入', 'cjk'),
186
+ (r'\bincreasing(?:ly)?\b', '递增', 'cjk'),
187
+ (r'\bdecreasing(?:ly)?\b', '递减', 'cjk'),
188
+ (r'\brespectively\b', '分别', 'cjk'),
189
+ (r'\bnecessarily\b', '必然', 'cjk'),
190
+ (r'\btransformation\b', '变换', 'cjk'),
191
+ (r'\bprerequisite\b', '前提', 'cjk'),
192
+ (r'\bconsequently\b', '从而', 'cjk'),
193
+ (r'\boverlapping\b', '重叠', 'cjk'),
194
+ (r'\bcontribut(?:e|ion)\b', '贡献', 'cjk'),
195
+ (r'\bindependent(?:ly)?\b', '独立', 'cjk'),
196
+ (r'\bimpossible\b', '不可能', 'cjk'),
197
+ (r'\biterat(?:e|ion|ing)\b', '迭代', 'cjk'),
198
+ (r'\benumerat(?:e|ion|ing)\b', '枚举', 'cjk'),
199
+
200
+ # ── LOGIC SYMBOLS ──
201
+ (r'\btherefore\b', '⇒', 'symbol'),
202
+ (r'\bthus\b', '⇒', 'symbol'),
203
+ (r'\bsuch that\b', 'st', 'abbrev'),
204
+ (r'\bthere exists?\b', '∃', 'symbol'),
205
+ (r'\bfor each\b', '∀', 'symbol'),
206
+ (r'\bfor every\b', '∀', 'symbol'),
207
+ (r'\bfor all\b', '∀', 'symbol'),
208
+ (r'\bdoes not\b', '¬', 'symbol'),
209
+ (r"\bdoesn't\b", '¬', 'symbol'),
210
+ (r'\bis not\b(?!\s+(?:None|null|undefined|empty|zero|0))', '非', 'cjk'), # protect "is not None" etc
211
+ (r'\bat least\b', '≥', 'symbol'),
212
+ (r'\bat most\b', '≤', 'symbol'),
213
+ (r'\bgreater than\b', '>', 'symbol'),
214
+ (r'\bless than\b', '<', 'symbol'),
215
+
216
+ # ── ARTICLE STRIPPING (last — lowest priority) ──
217
+ (r'\bthe\b\s+(?!(?:same|only|first|last|next|other)\b)', '', 'filler'), # protect "the same", "the only" etc
218
+ (r'\ba\b\s+(?=[bcdfghjklmnpqrstvwxyz])', '', 'filler'),
219
+ (r'\ban\b\s+', '', 'filler'),
220
+ ]
221
+
222
+ # ── Compile once ───────────────────────────���───────────────────────────
223
+ _FILLER_COMPILED = [(re.compile(p, re.IGNORECASE), '') for p in _FILLER_PATTERNS]
224
+ _SUBS_COMPILED = [(re.compile(p, re.IGNORECASE), r, cat) for p, r, cat in _SUBSTITUTIONS]
225
+
226
+
227
+ def _ntok(text: str, tokenizer) -> int:
228
+ """Token count using the provided tokenizer."""
229
+ return len(tokenizer.encode(text, add_special_tokens=False))
230
+
231
+
232
+ def _protect_code_fences(text: str) -> tuple[str, list]:
233
+ """Extract code-fenced blocks, replace with placeholders.
234
+ Returns (text_with_placeholders, list_of_extracted_blocks)."""
235
+ blocks = []
236
+ def _replace(m):
237
+ blocks.append(m.group(0))
238
+ return f'\x00CODEFENCE{len(blocks)-1}\x00'
239
+ # Match ```...``` and inline `...` (non-greedy)
240
+ protected = re.sub(r'```.*?```|`[^`\n]+`', _replace, text, flags=re.DOTALL)
241
+ return protected, blocks
242
+
243
+
244
+ def _restore_code_fences(text: str, blocks: list) -> str:
245
+ """Restore code-fenced blocks from placeholders."""
246
+ for i, block in enumerate(blocks):
247
+ text = text.replace(f'\x00CODEFENCE{i}\x00', block)
248
+ return text
249
+
250
+
251
+ def _apply_subs(text: str) -> tuple[str, dict]:
252
+ """Apply all substitutions, return (result, stats).
253
+ Code fences (``` and inline `) are protected from substitution."""
254
+ stats = {'filler_drops': 0, 'cjk': 0, 'symbol': 0, 'abbrev': 0, 'total_subs': 0}
255
+
256
+ # Phase 0: protect code fences from substitution
257
+ text, code_blocks = _protect_code_fences(text)
258
+
259
+ # Phase 1: filler drops
260
+ for pat, repl in _FILLER_COMPILED:
261
+ text, n = pat.subn(repl, text)
262
+ if n:
263
+ stats['filler_drops'] += n
264
+ stats['total_subs'] += n
265
+
266
+ # Phase 2: substitutions
267
+ for pat, repl, cat in _SUBS_COMPILED:
268
+ text, n = pat.subn(repl, text)
269
+ if n:
270
+ stats[cat] = stats.get(cat, 0) + n
271
+ stats['total_subs'] += n
272
+
273
+ # Phase 3: restore code fences
274
+ text = _restore_code_fences(text, code_blocks)
275
+
276
+ # Phase 4: whitespace normalization
277
+ text = re.sub(r'[ \t]+', ' ', text)
278
+ text = re.sub(r'\n{3,}', '\n\n', text)
279
+ text = re.sub(r' *\n *', '\n', text)
280
+ text = text.strip()
281
+
282
+ return text, stats
283
+
284
+
285
+ def tokenmax(text: str, tokenizer, force_cjk: bool = False) -> str:
286
+ """Apply token-maxing. Returns original if no savings achieved.
287
+
288
+ Args:
289
+ text: The think block content (without <think> tags).
290
+ tokenizer: A HuggingFace tokenizer with .encode() method.
291
+ force_cjk: If True, always return the processed version when CJK
292
+ substitutions were applied, even if total token count increased.
293
+ Use this to maximize CJK adoption in training data.
294
+
295
+ Returns:
296
+ The token-maxed text, or the original if processing didn't save tokens
297
+ (unless force_cjk=True and CJK subs were applied).
298
+ """
299
+ if not text or not text.strip():
300
+ return text
301
+
302
+ original_tokens = _ntok(text, tokenizer)
303
+ result, stats = _apply_subs(text)
304
+ result_tokens = _ntok(result, tokenizer)
305
+
306
+ # GUARD: only return processed version if it actually saves tokens
307
+ # OVERRIDE: force_cjk bypasses the guard when CJK substitutions were made
308
+ if result_tokens < original_tokens:
309
+ return result
310
+ if force_cjk and stats.get('cjk', 0) > 0:
311
+ return result
312
+ return text
313
+
314
+
315
+ def tokenmax_with_stats(text: str, tokenizer, force_cjk: bool = False) -> tuple[str, dict]:
316
+ """Like tokenmax() but also returns substitution statistics.
317
+
318
+ Args:
319
+ force_cjk: If True, always apply when CJK substitutions were made,
320
+ even if total token count increased. Prioritizes CJK adoption
321
+ over token savings.
322
+
323
+ Returns:
324
+ (processed_text, stats_dict) where stats_dict contains:
325
+ - original_tokens: token count before processing
326
+ - result_tokens: token count after processing
327
+ - saved: tokens saved (negative = token increase; check forced_cjk)
328
+ - applied: whether the processed version was used
329
+ - forced_cjk: True when force_cjk override caused acceptance despite no savings
330
+ - filler_drops, cjk, symbol, abbrev: substitution counts by category
331
+ - total_subs: total substitutions applied
332
+ """
333
+ if not text or not text.strip():
334
+ return text, {'original_tokens': 0, 'result_tokens': 0, 'saved': 0,
335
+ 'applied': False, 'forced_cjk': False, 'total_subs': 0}
336
+
337
+ original_tokens = _ntok(text, tokenizer)
338
+ result, stats = _apply_subs(text)
339
+ result_tokens = _ntok(result, tokenizer)
340
+ saved = original_tokens - result_tokens
341
+
342
+ stats['original_tokens'] = original_tokens
343
+ stats['result_tokens'] = result_tokens
344
+ stats['saved'] = saved
345
+ stats['forced_cjk'] = False
346
+
347
+ if saved > 0:
348
+ stats['applied'] = True
349
+ return result, stats
350
+ if force_cjk and stats.get('cjk', 0) > 0:
351
+ stats['applied'] = True
352
+ stats['forced_cjk'] = True
353
+ return result, stats
354
+ stats['applied'] = False
355
+ return text, stats
356
+
357
+
358
+ # ── CLI: batch process a JSONL file ────────────────────────────────────
359
+ if __name__ == '__main__':
360
+ import json, sys, argparse
361
+ from transformers import AutoTokenizer
362
+
363
+ parser = argparse.ArgumentParser(description='Token-max post-processor for think blocks')
364
+ parser.add_argument('--input', required=True, help='Input JSONL (messages format)')
365
+ parser.add_argument('--output', help='Output JSONL (default: dry run, stats only)')
366
+ parser.add_argument('--tokenizer', default='assets/omnicoder-tokenizer',
367
+ help='Path to tokenizer')
368
+ parser.add_argument('--force-cjk', action='store_true',
369
+ help='Force CJK substitutions even if total tokens increase. '
370
+ 'Prioritizes CJK adoption over token savings.')
371
+ args = parser.parse_args()
372
+
373
+ tok = AutoTokenizer.from_pretrained(args.tokenizer, trust_remote_code=True)
374
+
375
+ total_before = total_after = applied = skipped = forced = 0
376
+
377
+ out_lines = []
378
+ with open(args.input) as f:
379
+ for line in f:
380
+ line = line.strip()
381
+ if not line:
382
+ continue
383
+ rec = json.loads(line)
384
+ for m in rec.get('messages', rec.get('conversations', [])):
385
+ role = m.get('role', m.get('from', ''))
386
+ if role not in ('assistant', 'gpt'):
387
+ continue
388
+ content_key = 'content' if 'content' in m else 'value'
389
+ c = m.get(content_key, '') or ''
390
+ if '<think>' not in c or '</think>' not in c:
391
+ continue
392
+
393
+ # Extract think content, preserving prefix before <think> and suffix after </think>
394
+ think_start = c.index('<think>') + len('<think>')
395
+ think_end = c.index('</think>')
396
+ prefix = c[:think_start - len('<think>')]
397
+ think = c[think_start:think_end]
398
+ suffix = c[think_end + len('</think>'):]
399
+
400
+ maxed, stats = tokenmax_with_stats(think, tok, force_cjk=args.force_cjk)
401
+
402
+ total_before += stats['original_tokens']
403
+ if stats['applied']:
404
+ applied += 1
405
+ total_after += stats['result_tokens']
406
+ m[content_key] = f'{prefix}<think>{maxed}</think>{suffix}'
407
+ if stats.get('forced_cjk'):
408
+ forced += 1
409
+ else:
410
+ skipped += 1
411
+ total_after += stats['original_tokens']
412
+
413
+ out_lines.append(json.dumps(rec, ensure_ascii=False))
414
+
415
+ if args.output:
416
+ with open(args.output, 'w') as f:
417
+ for line in out_lines:
418
+ f.write(line + '\n')
419
+
420
+ total = applied + skipped
421
+ saved = total_before - total_after
422
+ if total > 0:
423
+ print(f'Processed {total} think blocks')
424
+ print(f' Applied: {applied} ({100*applied/total:.0f}%)')
425
+ if forced:
426
+ print(f' Forced CJK: {forced} (applied despite no token savings)')
427
+ print(f' Skipped (no savings): {skipped}')
428
+ pct = f'{100*saved/total_before:.1f}' if total_before > 0 else '0.0'
429
+ print(f' Tokens: {total_before} → {total_after} = {saved:+d} ({pct}%)')
430
+ else:
431
+ print(f'No think blocks found in {args.input}')
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea43b288542655d72d632195ab9b58ca2cd9532c292bf6667827ce899ad196bc
3
+ size 11422082
tokenizer_config.json ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<|im_start|>",
10
+ "<|im_end|>",
11
+ "<|object_ref_start|>",
12
+ "<|object_ref_end|>",
13
+ "<|box_start|>",
14
+ "<|box_end|>",
15
+ "<|quad_start|>",
16
+ "<|quad_end|>",
17
+ "<|vision_start|>",
18
+ "<|vision_end|>",
19
+ "<|vision_pad|>",
20
+ "<|image_pad|>",
21
+ "<|video_pad|>"
22
+ ],
23
+ "is_local": false,
24
+ "model_max_length": 32768,
25
+ "pad_token": "<|PAD_TOKEN|>",
26
+ "padding_side": "left",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null,
30
+ "added_tokens_decoder": {
31
+ "151643": {
32
+ "content": "<|endoftext|>",
33
+ "single_word": false,
34
+ "lstrip": false,
35
+ "rstrip": false,
36
+ "normalized": false,
37
+ "special": true
38
+ },
39
+ "151644": {
40
+ "content": "<|im_start|>",
41
+ "single_word": false,
42
+ "lstrip": false,
43
+ "rstrip": false,
44
+ "normalized": false,
45
+ "special": true
46
+ },
47
+ "151645": {
48
+ "content": "<|im_end|>",
49
+ "single_word": false,
50
+ "lstrip": false,
51
+ "rstrip": false,
52
+ "normalized": false,
53
+ "special": true
54
+ },
55
+ "151646": {
56
+ "content": "<|object_ref_start|>",
57
+ "single_word": false,
58
+ "lstrip": false,
59
+ "rstrip": false,
60
+ "normalized": false,
61
+ "special": true
62
+ },
63
+ "151647": {
64
+ "content": "<|object_ref_end|>",
65
+ "single_word": false,
66
+ "lstrip": false,
67
+ "rstrip": false,
68
+ "normalized": false,
69
+ "special": true
70
+ },
71
+ "151648": {
72
+ "content": "<|box_start|>",
73
+ "single_word": false,
74
+ "lstrip": false,
75
+ "rstrip": false,
76
+ "normalized": false,
77
+ "special": true
78
+ },
79
+ "151649": {
80
+ "content": "<|box_end|>",
81
+ "single_word": false,
82
+ "lstrip": false,
83
+ "rstrip": false,
84
+ "normalized": false,
85
+ "special": true
86
+ },
87
+ "151650": {
88
+ "content": "<|quad_start|>",
89
+ "single_word": false,
90
+ "lstrip": false,
91
+ "rstrip": false,
92
+ "normalized": false,
93
+ "special": true
94
+ },
95
+ "151651": {
96
+ "content": "<|quad_end|>",
97
+ "single_word": false,
98
+ "lstrip": false,
99
+ "rstrip": false,
100
+ "normalized": false,
101
+ "special": true
102
+ },
103
+ "151652": {
104
+ "content": "<|vision_start|>",
105
+ "single_word": false,
106
+ "lstrip": false,
107
+ "rstrip": false,
108
+ "normalized": false,
109
+ "special": true
110
+ },
111
+ "151653": {
112
+ "content": "<|vision_end|>",
113
+ "single_word": false,
114
+ "lstrip": false,
115
+ "rstrip": false,
116
+ "normalized": false,
117
+ "special": true
118
+ },
119
+ "151654": {
120
+ "content": "<|vision_pad|>",
121
+ "single_word": false,
122
+ "lstrip": false,
123
+ "rstrip": false,
124
+ "normalized": false,
125
+ "special": true
126
+ },
127
+ "151655": {
128
+ "content": "<|image_pad|>",
129
+ "single_word": false,
130
+ "lstrip": false,
131
+ "rstrip": false,
132
+ "normalized": false,
133
+ "special": true
134
+ },
135
+ "151656": {
136
+ "content": "<|video_pad|>",
137
+ "single_word": false,
138
+ "lstrip": false,
139
+ "rstrip": false,
140
+ "normalized": false,
141
+ "special": true
142
+ },
143
+ "151657": {
144
+ "content": "<tool_call>",
145
+ "single_word": false,
146
+ "lstrip": false,
147
+ "rstrip": false,
148
+ "normalized": false,
149
+ "special": false
150
+ },
151
+ "151658": {
152
+ "content": "</tool_call>",
153
+ "single_word": false,
154
+ "lstrip": false,
155
+ "rstrip": false,
156
+ "normalized": false,
157
+ "special": false
158
+ },
159
+ "151659": {
160
+ "content": "<|fim_prefix|>",
161
+ "single_word": false,
162
+ "lstrip": false,
163
+ "rstrip": false,
164
+ "normalized": false,
165
+ "special": false
166
+ },
167
+ "151660": {
168
+ "content": "<|fim_middle|>",
169
+ "single_word": false,
170
+ "lstrip": false,
171
+ "rstrip": false,
172
+ "normalized": false,
173
+ "special": false
174
+ },
175
+ "151661": {
176
+ "content": "<|fim_suffix|>",
177
+ "single_word": false,
178
+ "lstrip": false,
179
+ "rstrip": false,
180
+ "normalized": false,
181
+ "special": false
182
+ },
183
+ "151662": {
184
+ "content": "<|fim_pad|>",
185
+ "single_word": false,
186
+ "lstrip": false,
187
+ "rstrip": false,
188
+ "normalized": false,
189
+ "special": false
190
+ },
191
+ "151663": {
192
+ "content": "<|repo_name|>",
193
+ "single_word": false,
194
+ "lstrip": false,
195
+ "rstrip": false,
196
+ "normalized": false,
197
+ "special": false
198
+ },
199
+ "151664": {
200
+ "content": "<|file_sep|>",
201
+ "single_word": false,
202
+ "lstrip": false,
203
+ "rstrip": false,
204
+ "normalized": false,
205
+ "special": false
206
+ },
207
+ "151665": {
208
+ "content": "<|PAD_TOKEN|>",
209
+ "single_word": false,
210
+ "lstrip": false,
211
+ "rstrip": false,
212
+ "normalized": false,
213
+ "special": true
214
+ }
215
+ },
216
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %} {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n"
217
+ }