stephenlb commited on
Commit
347abca
·
verified ·
1 Parent(s): ed20cdd

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ 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
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ license: gemma
4
+ base_model: google/gemma-4-12B
5
+ pipeline_tag: text-classification
6
+ tags:
7
+ - custom_code
8
+ - gemma4
9
+ - typed-decisions
10
+ ---
11
+
12
+ # Truetype System One (Gemma 4 12B, 26-letter output layer)
13
+
14
+ `google/gemma-4-12B` with its 262k-token language-model head replaced by a
15
+ 26-row layer that outputs one logit per letter `A`-`Z`. The decoder is
16
+ unchanged. One forward pass gives the 26 logits; questions (`noul`, `choice`,
17
+ `score`) are answered by softmax over the letters each question declares legal.
18
+
19
+ Requires `transformers>=5.17`. The repo ships custom code, so pass
20
+ `trust_remote_code=True`.
21
+
22
+ ## Model API
23
+
24
+ ```python
25
+ from transformers import AutoModelForMultimodalLM, AutoTokenizer
26
+
27
+ repo = "stephenlb/system-one-model"
28
+ tokenizer = AutoTokenizer.from_pretrained(repo)
29
+ model = AutoModelForMultimodalLM.from_pretrained(
30
+ repo, trust_remote_code=True, dtype="bfloat16", device_map="auto"
31
+ )
32
+
33
+ result = model.system_one(
34
+ tokenizer,
35
+ state="I was charged twice for order A-104.",
36
+ questions={
37
+ "refund": {"type": "noul", "instructions": "Does the text request a refund?"},
38
+ "team": {
39
+ "type": "choice",
40
+ "instructions": "Which team should handle this?",
41
+ "criteria": {"billing": "Charges", "returns": "Refunds"},
42
+ },
43
+ },
44
+ )
45
+ print(result["answers"])
46
+ ```
47
+
48
+ ## Pipeline API
49
+
50
+ ```python
51
+ from transformers import pipeline
52
+
53
+ pipe = pipeline("system-one", model=repo, trust_remote_code=True, dtype="bfloat16", device_map="auto")
54
+ print(pipe({"state": "I was charged twice.", "questions": {...}}))
55
+ ```
56
+
57
+ ## Output
58
+
59
+ ```json
60
+ {"answers": {
61
+ "refund": {"type": "noul", "noul": 0.93},
62
+ "team": {"type": "choice", "choice": "billing",
63
+ "probabilities": {"billing": 0.97, "returns": 0.03}, "confidence": 0.8},
64
+ "urgency": {"type": "score", "score": 3.1, "legend": {"0": "..."},
65
+ "probabilities": {"0": 0.0, "1": 0.1}, "confidence": 0.5}}}
66
+ ```
67
+
68
+ `noul` is the probability of yes. `choice` returns the top label with
69
+ probabilities per option. `score` returns the expected level over an ordered
70
+ `criteria` list. Pass `temperature=` (default 0.7) to sharpen or soften.
71
+
72
+ ## Raw letter logits
73
+
74
+ ```python
75
+ enc = tokenizer(prompts, return_tensors="pt", padding=True) # padding_side="left"
76
+ logits = model.letter_logits(enc.input_ids, enc.attention_mask) # [batch, 26]
77
+ ```
78
+
79
+ Source and prompt format: https://github.com/stephenlb/truetype.ai-open
__init__.py ADDED
File without changes
__pycache__/__init__.cpython-314.pyc ADDED
Binary file (177 Bytes). View file
 
__pycache__/configuration_truetype.cpython-314.pyc ADDED
Binary file (1.57 kB). View file
 
__pycache__/letters.cpython-314.pyc ADDED
Binary file (8.8 kB). View file
 
__pycache__/modeling_truetype.cpython-314.pyc ADDED
Binary file (5.99 kB). View file
 
__pycache__/questions.cpython-314.pyc ADDED
Binary file (17.2 kB). View file
 
__pycache__/render.cpython-314.pyc ADDED
Binary file (9.43 kB). View file
 
config.json ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "TrueTypeSystemOneModel"
4
+ ],
5
+ "audio_config": {
6
+ "_name_or_path": "",
7
+ "architectures": null,
8
+ "audio_embed_dim": 640,
9
+ "chunk_size_feed_forward": 0,
10
+ "dtype": "bfloat16",
11
+ "id2label": {
12
+ "0": "LABEL_0",
13
+ "1": "LABEL_1"
14
+ },
15
+ "initializer_range": 0.02,
16
+ "is_encoder_decoder": false,
17
+ "label2id": {
18
+ "LABEL_0": 0,
19
+ "LABEL_1": 1
20
+ },
21
+ "model_type": "gemma4_unified_audio",
22
+ "output_attentions": false,
23
+ "output_hidden_states": false,
24
+ "problem_type": null,
25
+ "return_dict": true,
26
+ "rms_norm_eps": 1e-06
27
+ },
28
+ "audio_token_id": 258881,
29
+ "boa_token_id": 256000,
30
+ "boi_token_id": 255999,
31
+ "dtype": "bfloat16",
32
+ "eoa_token_index": 258883,
33
+ "eoi_token_id": 258882,
34
+ "image_token_id": 258880,
35
+ "initializer_range": 0.02,
36
+ "letter_token_ids": [
37
+ 562,
38
+ 603,
39
+ 565,
40
+ 622,
41
+ 645,
42
+ 633,
43
+ 667,
44
+ 640,
45
+ 564,
46
+ 730,
47
+ 751,
48
+ 639,
49
+ 587,
50
+ 646,
51
+ 708,
52
+ 593,
53
+ 1366,
54
+ 630,
55
+ 555,
56
+ 558,
57
+ 749,
58
+ 782,
59
+ 649,
60
+ 1684,
61
+ 895,
62
+ 1491
63
+ ],
64
+ "model_type": "truetype_system_one",
65
+ "num_letters": 26,
66
+ "text_config": {
67
+ "attention_bias": false,
68
+ "attention_dropout": 0.0,
69
+ "attention_k_eq_v": true,
70
+ "bos_token_id": 2,
71
+ "dtype": "bfloat16",
72
+ "enable_moe_block": false,
73
+ "eos_token_id": 1,
74
+ "final_logit_softcapping": 30.0,
75
+ "head_dim": 256,
76
+ "hidden_activation": "gelu_pytorch_tanh",
77
+ "hidden_size": 3840,
78
+ "hidden_size_per_layer_input": 0,
79
+ "initializer_range": 0.02,
80
+ "intermediate_size": 15360,
81
+ "layer_types": [
82
+ "sliding_attention",
83
+ "sliding_attention",
84
+ "sliding_attention",
85
+ "sliding_attention",
86
+ "sliding_attention",
87
+ "full_attention",
88
+ "sliding_attention",
89
+ "sliding_attention",
90
+ "sliding_attention",
91
+ "sliding_attention",
92
+ "sliding_attention",
93
+ "full_attention",
94
+ "sliding_attention",
95
+ "sliding_attention",
96
+ "sliding_attention",
97
+ "sliding_attention",
98
+ "sliding_attention",
99
+ "full_attention",
100
+ "sliding_attention",
101
+ "sliding_attention",
102
+ "sliding_attention",
103
+ "sliding_attention",
104
+ "sliding_attention",
105
+ "full_attention",
106
+ "sliding_attention",
107
+ "sliding_attention",
108
+ "sliding_attention",
109
+ "sliding_attention",
110
+ "sliding_attention",
111
+ "full_attention",
112
+ "sliding_attention",
113
+ "sliding_attention",
114
+ "sliding_attention",
115
+ "sliding_attention",
116
+ "sliding_attention",
117
+ "full_attention",
118
+ "sliding_attention",
119
+ "sliding_attention",
120
+ "sliding_attention",
121
+ "sliding_attention",
122
+ "sliding_attention",
123
+ "full_attention",
124
+ "sliding_attention",
125
+ "sliding_attention",
126
+ "sliding_attention",
127
+ "sliding_attention",
128
+ "sliding_attention",
129
+ "full_attention"
130
+ ],
131
+ "max_position_embeddings": 262144,
132
+ "model_type": "gemma4_unified_text",
133
+ "moe_intermediate_size": null,
134
+ "num_attention_heads": 16,
135
+ "num_experts": null,
136
+ "num_hidden_layers": 48,
137
+ "num_key_value_heads": 8,
138
+ "num_kv_shared_layers": 0,
139
+ "pad_token_id": 0,
140
+ "per_layer_config": {
141
+ "05": {
142
+ "head_dim": 512,
143
+ "num_key_value_heads": 1
144
+ },
145
+ "11": {
146
+ "head_dim": 512,
147
+ "num_key_value_heads": 1
148
+ },
149
+ "17": {
150
+ "head_dim": 512,
151
+ "num_key_value_heads": 1
152
+ },
153
+ "23": {
154
+ "head_dim": 512,
155
+ "num_key_value_heads": 1
156
+ },
157
+ "29": {
158
+ "head_dim": 512,
159
+ "num_key_value_heads": 1
160
+ },
161
+ "35": {
162
+ "head_dim": 512,
163
+ "num_key_value_heads": 1
164
+ },
165
+ "41": {
166
+ "head_dim": 512,
167
+ "num_key_value_heads": 1
168
+ },
169
+ "47": {
170
+ "head_dim": 512,
171
+ "num_key_value_heads": 1
172
+ }
173
+ },
174
+ "rms_norm_eps": 1e-06,
175
+ "rope_parameters": {
176
+ "full_attention": {
177
+ "partial_rotary_factor": 0.25,
178
+ "rope_theta": 1000000.0,
179
+ "rope_type": "proportional"
180
+ },
181
+ "sliding_attention": {
182
+ "rope_theta": 10000.0,
183
+ "rope_type": "default"
184
+ }
185
+ },
186
+ "sliding_window": 1024,
187
+ "tie_word_embeddings": true,
188
+ "top_k_experts": null,
189
+ "use_bidirectional_attention": "vision",
190
+ "use_cache": true,
191
+ "use_double_wide_mlp": false,
192
+ "vocab_size": 262144,
193
+ "vocab_size_per_layer_input": 262144
194
+ },
195
+ "tie_word_embeddings": false,
196
+ "transformers_version": "5.17.0",
197
+ "video_token_id": 258884,
198
+ "vision_config": {
199
+ "_name_or_path": "",
200
+ "architectures": null,
201
+ "chunk_size_feed_forward": 0,
202
+ "dtype": "bfloat16",
203
+ "id2label": {
204
+ "0": "LABEL_0",
205
+ "1": "LABEL_1"
206
+ },
207
+ "initializer_range": 0.02,
208
+ "is_encoder_decoder": false,
209
+ "label2id": {
210
+ "LABEL_0": 0,
211
+ "LABEL_1": 1
212
+ },
213
+ "mm_embed_dim": 3840,
214
+ "mm_posemb_size": 1120,
215
+ "model_type": "gemma4_unified_vision",
216
+ "num_soft_tokens": 280,
217
+ "output_attentions": false,
218
+ "output_hidden_states": false,
219
+ "output_proj_dims": 3840,
220
+ "patch_size": 16,
221
+ "pooling_kernel_size": 3,
222
+ "problem_type": null,
223
+ "return_dict": true,
224
+ "rms_norm_eps": 1e-06
225
+ },
226
+ "auto_map": {
227
+ "AutoConfig": "configuration_truetype.TrueTypeSystemOneConfig",
228
+ "AutoModel": "modeling_truetype.TrueTypeSystemOneModel",
229
+ "AutoModelForMultimodalLM": "modeling_truetype.TrueTypeSystemOneModel",
230
+ "AutoModelForImageTextToText": "modeling_truetype.TrueTypeSystemOneModel"
231
+ },
232
+ "custom_pipelines": {
233
+ "system-one": {
234
+ "impl": "pipeline_truetype.SystemOnePipeline",
235
+ "pt": [
236
+ "AutoModelForMultimodalLM"
237
+ ],
238
+ "tf": []
239
+ }
240
+ }
241
+ }
configuration_truetype.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Config for the Gemma 4 System One model with a 26-logit A-Z output layer."""
2
+
3
+ from transformers.models.gemma4_unified.configuration_gemma4_unified import Gemma4UnifiedConfig
4
+
5
+ LETTERS = tuple("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
6
+
7
+
8
+ class TrueTypeSystemOneConfig(Gemma4UnifiedConfig):
9
+ """Gemma 4 config plus the tokenizer ids the 26 output rows were copied from.
10
+
11
+ ``letter_token_ids[i]`` is the vocabulary id whose embedding row became output
12
+ row ``i`` (``A`` to ``Z``). It is kept for provenance and for verification
13
+ against the original vocabulary head.
14
+ """
15
+
16
+ model_type = "truetype_system_one"
17
+
18
+ def __init__(self, letter_token_ids=None, num_letters=len(LETTERS), **kwargs):
19
+ # The 26-row head is its own parameter, not the input embedding.
20
+ kwargs["tie_word_embeddings"] = False
21
+ super().__init__(**kwargs)
22
+ self.letter_token_ids = list(letter_token_ids or [])
23
+ self.num_letters = num_letters
generation_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 2,
3
+ "do_sample": true,
4
+ "eos_token_id": 1,
5
+ "pad_token_id": 0,
6
+ "suppress_tokens": [
7
+ 258883,
8
+ 258882
9
+ ],
10
+ "temperature": 1.0,
11
+ "top_k": 64,
12
+ "top_p": 0.95,
13
+ "transformers_version": "5.17.0"
14
+ }
letters.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extract single-letter logits and compute top-k softmax scores.
2
+
3
+ The engine reads one next-token position, keeps logits for the 26 uppercase
4
+ ASCII letters, and converts the top-k letter logits into a probability
5
+ distribution. It does not sample or decode text.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ import string
12
+ from dataclasses import dataclass
13
+
14
+ LETTERS: tuple[str, ...] = tuple(string.ascii_uppercase)
15
+ LETTER_INDEX: dict[str, int] = {c: i for i, c in enumerate(LETTERS)}
16
+
17
+ # Rank at which we stop tracking for the reported top-k. k <= 26 by construction.
18
+ MAX_TOP_K = len(LETTERS)
19
+
20
+
21
+ class LetterLogitError(ValueError):
22
+ """Raised when the model's next-token distribution cannot be read as letters."""
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LetterReadout:
27
+ """The full 26-letter logit readout for one prompt, plus a top-k view.
28
+
29
+ ``logits`` keeps every letter when a question has more options than ``top_k``.
30
+ ``top`` is the reported top-k softmax distribution.
31
+ ``letter_mass`` is retained as a nullable compatibility field. The optimized
32
+ 26-row output head intentionally cannot report it because it never computes
33
+ a full-vocabulary distribution.
34
+ """
35
+
36
+ logits: dict[str, float]
37
+ top: "SoftmaxDistribution"
38
+ letter_mass: float | None = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class SoftmaxDistribution:
43
+ """A probability distribution over a chosen slice of letters."""
44
+
45
+ ranked: tuple[tuple[str, float], ...]
46
+ probabilities: dict[str, float]
47
+ top_k: int
48
+ temperature: float
49
+
50
+ @property
51
+ def top_letter(self) -> str:
52
+ return self.ranked[0][0]
53
+
54
+ @property
55
+ def top_probability(self) -> float:
56
+ return self.probabilities[self.top_letter]
57
+
58
+
59
+ def _softmax(logits: list[float], temperature: float) -> list[float]:
60
+ if temperature <= 0:
61
+ raise ValueError("temperature must be > 0")
62
+ scaled = [x / temperature for x in logits]
63
+ peak = max(scaled)
64
+ exps = [math.exp(x - peak) for x in scaled]
65
+ total = sum(exps)
66
+ if total <= 0 or not math.isfinite(total):
67
+ raise LetterLogitError("softmax denominator is not finite")
68
+ return [e / total for e in exps]
69
+
70
+
71
+ def distribution_from_letter_logits(
72
+ letter_logits: dict[str, float],
73
+ *,
74
+ top_k: int = 5,
75
+ temperature: float = 1.0,
76
+ ) -> SoftmaxDistribution:
77
+ """Softmax the top-k letter logits into a probability distribution.
78
+
79
+ `top_k` is clamped to 2..26. The softmax is taken *only* over the k letters we
80
+ keep, which is what TypeSafe-compatible consumers expect: the reported
81
+ probabilities sum to 1 across the surfaced options.
82
+ """
83
+ unknown = set(letter_logits) - set(LETTER_INDEX)
84
+ if unknown:
85
+ raise LetterLogitError(f"non-letter logit keys: {sorted(unknown)}")
86
+ if not letter_logits:
87
+ raise LetterLogitError("no letter logits supplied")
88
+
89
+ k = max(1, min(int(top_k), len(letter_logits), MAX_TOP_K))
90
+ ranked = sorted(letter_logits.items(), key=lambda kv: kv[1], reverse=True)[:k]
91
+ probs = _softmax([v for _, v in ranked], temperature)
92
+
93
+ distribution = {letter: p for (letter, _), p in zip(ranked, probs)}
94
+ # Keep the distribution in descending-probability order for stable output.
95
+ ordered = dict(sorted(distribution.items(), key=lambda kv: kv[1], reverse=True))
96
+ return SoftmaxDistribution(
97
+ ranked=tuple((letter, logit) for letter, logit in ranked),
98
+ probabilities=ordered,
99
+ top_k=k,
100
+ temperature=temperature,
101
+ )
102
+
103
+
104
+ def gather_letter_logits(next_token_logits, letter_token_ids):
105
+ """Return the ``[batch, 26]`` A-Z slice of a vocabulary-logit tensor.
106
+
107
+ ``letter_token_ids`` may be the device-resident tensor owned by the engine.
108
+ Keeping it on-device avoids allocating a Python list and an indexing tensor on
109
+ every request.
110
+ """
111
+ if next_token_logits.dim() != 2:
112
+ raise LetterLogitError("expected next-token logits of shape [batch, vocab]")
113
+
114
+ import torch
115
+
116
+ ids = (
117
+ letter_token_ids
118
+ if isinstance(letter_token_ids, torch.Tensor)
119
+ else torch.tensor(list(letter_token_ids.values()), device=next_token_logits.device)
120
+ )
121
+ return next_token_logits.index_select(1, ids)
122
+
123
+
124
+ def batch_letter_logits(
125
+ gathered_logits, letters: tuple[str, ...] | dict[str, int] = LETTERS
126
+ ) -> list[dict[str, float]]:
127
+ """Convert a previously gathered ``[batch, 26]`` A-Z tensor to dictionaries."""
128
+ # Preserve the original public helper signature for downstream callers while
129
+ # allowing the engine to pass the already-gathered slice.
130
+ if isinstance(letters, dict):
131
+ gathered_logits = gather_letter_logits(gathered_logits, letters)
132
+ letters = tuple(letters)
133
+ if gathered_logits.dim() != 2 or gathered_logits.shape[1] != len(letters):
134
+ raise LetterLogitError("expected gathered letter logits of shape [batch, 26]")
135
+ return [
136
+ {letter: float(value) for letter, value in zip(letters, row)}
137
+ for row in gathered_logits
138
+ ]
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cc96bd25caeb0cccae009757983a7a4cba3b0866b55915c54b903149d0066d8
3
+ size 23919749176
modeling_truetype.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gemma 4 with a 26-logit A-Z output layer instead of the 262k-token LM head.
2
+
3
+ The decoder is unchanged. Only the final projection differs: ``lm_head`` has 26
4
+ rows (the A-Z rows of the original tied embedding), so one forward pass returns
5
+ one logit per letter and the vocabulary-sized matmul disappears. Gemma's final
6
+ logit soft-capping still runs on those 26 values, exactly as it did on the
7
+ original rows.
8
+
9
+ model = AutoModelForMultimodalLM.from_pretrained(repo, trust_remote_code=True)
10
+ tokenizer = AutoTokenizer.from_pretrained(repo)
11
+ result = model.system_one(tokenizer, state="...", questions={...})
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+
18
+ import torch
19
+ from torch import nn
20
+ from transformers.conversion_mapping import (
21
+ get_checkpoint_conversion_mapping,
22
+ register_checkpoint_conversion_mapping,
23
+ )
24
+ from transformers.models.gemma4_unified.modeling_gemma4_unified import (
25
+ Gemma4UnifiedForConditionalGeneration,
26
+ )
27
+
28
+ from .configuration_truetype import LETTERS, TrueTypeSystemOneConfig
29
+ from .questions import build_question, score_question_tensor
30
+ from .render import render_batch
31
+
32
+ DEFAULT_TEMPERATURE = 0.7
33
+
34
+ # Gemma's checkpoints keep the vision embedder under legacy names that
35
+ # transformers renames per model_type. This model shares that layout.
36
+ register_checkpoint_conversion_mapping(
37
+ TrueTypeSystemOneConfig.model_type, get_checkpoint_conversion_mapping("gemma4_unified"), overwrite=True
38
+ )
39
+
40
+
41
+ class TrueTypeSystemOneModel(Gemma4UnifiedForConditionalGeneration):
42
+ config_class = TrueTypeSystemOneConfig
43
+ _tied_weights_keys = {}
44
+
45
+ def __init__(self, config: TrueTypeSystemOneConfig):
46
+ super().__init__(config)
47
+ self.lm_head = nn.Linear(config.text_config.hidden_size, len(LETTERS), bias=False)
48
+ self.post_init()
49
+
50
+ def letter_logits(self, input_ids, attention_mask=None, **kwargs) -> torch.Tensor:
51
+ """``[batch, 26]`` float32 logits for A-Z at the next-token position.
52
+
53
+ Inputs must be left-padded so the last position is the answer slot.
54
+ """
55
+ out = self(
56
+ input_ids=input_ids,
57
+ attention_mask=attention_mask,
58
+ logits_to_keep=1,
59
+ use_cache=False,
60
+ **kwargs,
61
+ )
62
+ return out.logits[:, -1, :].float()
63
+
64
+ @torch.inference_mode()
65
+ def system_one(
66
+ self,
67
+ tokenizer,
68
+ state,
69
+ questions: dict[str, dict],
70
+ temperature: float = DEFAULT_TEMPERATURE,
71
+ batch_size: int = 16,
72
+ include_letter_logits: bool = False,
73
+ ) -> dict:
74
+ """Answer typed questions (``noul``, ``choice``, ``score``) about ``state``.
75
+
76
+ ``questions`` maps an id to ``{"type", "instructions", "criteria"}``.
77
+ Returns ``{"answers": {id: {...probabilities and answer...}}}``.
78
+ """
79
+ if not questions:
80
+ raise ValueError("at least one question is required")
81
+ state_text = state if isinstance(state, str) else json.dumps(state, indent=2)
82
+ parsed = [build_question(qid, spec) for qid, spec in questions.items()]
83
+ prompts = [item.text for item in render_batch(parsed, state_text)]
84
+
85
+ rows = []
86
+ padding_side, tokenizer.padding_side = tokenizer.padding_side, "left"
87
+ try:
88
+ for start in range(0, len(prompts), max(1, batch_size)):
89
+ encoded = tokenizer(
90
+ prompts[start : start + batch_size],
91
+ return_tensors="pt",
92
+ padding=True,
93
+ add_special_tokens=True,
94
+ ).to(self.device)
95
+ rows.extend(self.letter_logits(encoded["input_ids"], encoded["attention_mask"]))
96
+ finally:
97
+ tokenizer.padding_side = padding_side
98
+
99
+ result = {"answers": {}}
100
+ for question, row in zip(parsed, rows):
101
+ result["answers"][question.id] = score_question_tensor(
102
+ question, row, temperature=temperature
103
+ ).to_dict()
104
+ if include_letter_logits:
105
+ result["letter_logits"] = {
106
+ question.id: dict(zip(LETTERS, row.tolist())) for question, row in zip(parsed, rows)
107
+ }
108
+ return result
pipeline_truetype.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """``pipeline("system-one", ...)``: typed questions in, probabilities and answers out.
2
+
3
+ pipe = pipeline("system-one", model=repo, trust_remote_code=True)
4
+ pipe({"state": "I was charged twice.",
5
+ "questions": {"refund": {"type": "noul", "instructions": "Does the text request a refund?"}}})
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import torch
13
+ from transformers import Pipeline
14
+
15
+ from .configuration_truetype import LETTERS
16
+ from .questions import build_question, score_question_tensor
17
+ from .render import render_batch
18
+
19
+
20
+ class SystemOnePipeline(Pipeline):
21
+ def _sanitize_parameters(self, temperature=None, include_letter_logits=None, **kwargs):
22
+ postprocess = {}
23
+ if temperature is not None:
24
+ postprocess["temperature"] = temperature
25
+ if include_letter_logits is not None:
26
+ postprocess["include_letter_logits"] = include_letter_logits
27
+ return {}, {}, postprocess
28
+
29
+ def preprocess(self, inputs):
30
+ state, questions = inputs["state"], inputs["questions"]
31
+ if not questions:
32
+ raise ValueError("at least one question is required")
33
+ state_text = state if isinstance(state, str) else json.dumps(state, indent=2)
34
+ parsed = [build_question(qid, spec) for qid, spec in questions.items()]
35
+ prompts = [item.text for item in render_batch(parsed, state_text)]
36
+ self.tokenizer.padding_side = "left"
37
+ encoded = self.tokenizer(prompts, return_tensors="pt", padding=True, add_special_tokens=True)
38
+ return {"questions": parsed, "input_ids": encoded["input_ids"], "attention_mask": encoded["attention_mask"]}
39
+
40
+ def _forward(self, model_inputs):
41
+ with torch.inference_mode():
42
+ logits = self.model.letter_logits(
43
+ model_inputs["input_ids"].to(self.model.device),
44
+ model_inputs["attention_mask"].to(self.model.device),
45
+ )
46
+ return {"questions": model_inputs["questions"], "logits": logits}
47
+
48
+ def postprocess(self, model_outputs, temperature=0.7, include_letter_logits=False):
49
+ questions, logits = model_outputs["questions"], model_outputs["logits"]
50
+ result = {
51
+ "answers": {
52
+ q.id: score_question_tensor(q, row, temperature=temperature).to_dict()
53
+ for q, row in zip(questions, logits)
54
+ }
55
+ }
56
+ if include_letter_logits:
57
+ result["letter_logits"] = {q.id: dict(zip(LETTERS, row.tolist())) for q, row in zip(questions, logits)}
58
+ return result
questions.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Map Gemma letter logits onto TypeSafe's three question primitives.
2
+
3
+ The letter distribution is the model's raw output. Each primitive renormalizes
4
+ the distribution over its legal answer letters, so the probabilities sum to 1
5
+ across the caller's options.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass, field
12
+
13
+ from .letters import LETTER_INDEX, LETTERS, _softmax
14
+
15
+ # Fixed bank of question-agnostic demonstrations. Their predicates differ from the
16
+ # target question on purpose: they teach the letter-slot format, not the task.
17
+ GENERIC_EXAMPLES: tuple[tuple[str, str, str], ...] = (
18
+ ("I need this fixed immediately, it is blocking everything.", "Does the text express urgency?", "Y"),
19
+ ("Here is the documentation link for reference.", "Does the text express urgency?", "N"),
20
+ ("The app crashes when I open the settings page.", "Does the text contain a bug report?", "Y"),
21
+ ("Just wanted to share the meeting notes from today.", "Does the text contain a bug report?", "N"),
22
+ ("Please send this to a human agent right now.", "Does the text ask for a human agent?", "Y"),
23
+ ("No rush on this, whenever you get a chance.", "Does the text ask for a human agent?", "N"),
24
+ ("URGENT: the payment system is failing right now.", "Does the text express urgency?", "Y"),
25
+ ("The sky is blue and the grass is green.", "Does the text express urgency?", "N"),
26
+ )
27
+
28
+ DEFAULT_EXAMPLE_COUNT = 6
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class QuestionOption:
33
+ letter: str
34
+ label: str
35
+ description: str | None = None
36
+
37
+
38
+ @dataclass
39
+ class Question:
40
+ id: str
41
+ type: str # noul | choice | score
42
+ instructions: str
43
+ options: list[QuestionOption] = field(default_factory=list)
44
+ criteria: object = None
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class QuestionAnswer:
49
+ type: str
50
+ choice: str | None = None
51
+ probabilities: dict[str, float] = field(default_factory=dict)
52
+ confidence: float | None = None
53
+ noul: float | None = None
54
+ score: float | None = None
55
+ legend: dict[str, str] | None = None
56
+
57
+ def to_dict(self) -> dict:
58
+ payload: dict = {"type": self.type}
59
+ if self.type == "noul":
60
+ payload["noul"] = round(self.noul or 0.0, 6)
61
+ elif self.type == "choice":
62
+ payload["choice"] = self.choice
63
+ payload["probabilities"] = {k: round(v, 6) for k, v in self.probabilities.items()}
64
+ payload["confidence"] = round(self.confidence or 0.0, 6)
65
+ elif self.type == "score":
66
+ payload["score"] = round(self.score or 0.0, 6)
67
+ payload["legend"] = self.legend or {}
68
+ payload["probabilities"] = {k: round(v, 6) for k, v in self.probabilities.items()}
69
+ payload["confidence"] = round(self.confidence or 0.0, 6)
70
+ return payload
71
+
72
+
73
+ def confidence_from_probabilities(probabilities: list[float]) -> float:
74
+ """1.0 when all mass is on one option, 0.0 when uniform."""
75
+ n = len(probabilities)
76
+ if n <= 1:
77
+ return 1.0
78
+ entropy = -sum(p * math.log(p) for p in probabilities if p > 0)
79
+ return max(0.0, 1.0 - entropy / math.log(n))
80
+
81
+
82
+ def _predicate_forms(instructions: str) -> tuple[str, str]:
83
+ """Turn an instruction into affirmative/negative option text.
84
+
85
+ "Does the text request a refund?" -> ("Yes, the text requests a refund.",
86
+ "No, the text does not request a refund.")
87
+ """
88
+ text = instructions.strip().rstrip(".").strip()
89
+ lowered = text.lower()
90
+
91
+ if lowered.startswith("does the text "):
92
+ body = text[len("Does the text ") :]
93
+ affirmative = f"the text {body}"
94
+ elif lowered.startswith("is the text "):
95
+ body = text[len("Is the text ") :]
96
+ affirmative = f"the text {body}"
97
+ elif lowered.startswith("is this "):
98
+ body = text[len("Is this ") :]
99
+ affirmative = f"this {body}"
100
+ elif lowered.startswith("does this "):
101
+ body = text[len("Does this ") :]
102
+ affirmative = f"this {body}"
103
+ elif lowered.startswith("does the "):
104
+ body = text[len("Does the ") :]
105
+ affirmative = f"the {body}"
106
+ elif lowered.startswith("is the "):
107
+ body = text[len("Is the ") :]
108
+ affirmative = f"the {body}"
109
+ elif lowered.startswith("did the "):
110
+ body = text[len("Did the ") :]
111
+ affirmative = f"the {body}"
112
+ elif lowered.startswith("can the "):
113
+ body = text[len("Can the ") :]
114
+ affirmative = f"the {body}"
115
+ else:
116
+ affirmative = text
117
+
118
+ affirmative = affirmative.rstrip("?").strip()
119
+ if affirmative.lower().startswith("the "):
120
+ negative = "the " + affirmative[4:].split(" ", 1)[0] + " does not" + (
121
+ " " + affirmative[4:].split(" ", 1)[1] if " " in affirmative[4:] else ""
122
+ )
123
+ else:
124
+ negative = f"it is not the case that {affirmative}"
125
+ return f"Yes, {affirmative}.", f"No, {negative}."
126
+
127
+
128
+ def build_question(qid: str, spec: dict) -> Question:
129
+ """Parse one TypeSafe question spec into a letter-addressable Question."""
130
+ qtype = spec.get("type")
131
+ if qtype not in {"noul", "choice", "score"}:
132
+ raise ValueError(f"question {qid!r} has unsupported type {qtype!r}")
133
+
134
+ instructions = spec.get("instructions")
135
+ if not isinstance(instructions, str) or not instructions.strip():
136
+ raise ValueError(f"question {qid!r} requires non-empty string instructions")
137
+
138
+ criteria = spec.get("criteria")
139
+
140
+ if qtype == "noul":
141
+ if isinstance(criteria, dict):
142
+ yes_desc = criteria.get("true")
143
+ no_desc = criteria.get("false")
144
+ else:
145
+ yes_desc = no_desc = None
146
+ yes_text, no_text = _predicate_forms(instructions)
147
+ return Question(
148
+ id=qid,
149
+ type=qtype,
150
+ instructions=instructions,
151
+ options=[
152
+ QuestionOption("Y", "yes", yes_desc or yes_text),
153
+ QuestionOption("N", "no", no_desc or no_text),
154
+ ],
155
+ criteria=criteria,
156
+ )
157
+
158
+ if qtype == "choice":
159
+ if not isinstance(criteria, dict) or not criteria:
160
+ raise ValueError(f"choice question {qid!r} requires a non-empty criteria map")
161
+ if len(criteria) > len(LETTERS):
162
+ raise ValueError(f"choice question {qid!r} supports at most 26 options")
163
+ entries = list(criteria.items())
164
+ else:
165
+ if not isinstance(criteria, list) or len(criteria) < 2:
166
+ raise ValueError(f"score question {qid!r} requires an ordered list of at least two levels")
167
+ if len(criteria) > len(LETTERS):
168
+ raise ValueError(f"score question {qid!r} supports at most 26 levels")
169
+ entries = [(f"level {i}", level) for i, level in enumerate(criteria)]
170
+
171
+ options = [
172
+ QuestionOption(
173
+ LETTERS[i],
174
+ label,
175
+ description if isinstance(description, str) else str(description),
176
+ )
177
+ for i, (label, description) in enumerate(entries)
178
+ ]
179
+ return Question(id=qid, type=qtype, instructions=instructions, options=options, criteria=criteria)
180
+
181
+
182
+ def score_question(question: Question, letter_logits: dict[str, float], temperature: float = 1.0) -> QuestionAnswer:
183
+ """Turn raw letter logits into a typed answer, renormalized over legal options."""
184
+ option_letters = [option.letter for option in question.options]
185
+ missing = [letter for letter in option_letters if letter not in letter_logits]
186
+ if missing:
187
+ raise ValueError(f"question {question.id!r} is missing logits for {missing}")
188
+
189
+ logits = [letter_logits[letter] for letter in option_letters]
190
+ probs = _softmax(logits, temperature)
191
+
192
+ if question.type == "noul":
193
+ noul = probs[0]
194
+ return QuestionAnswer(
195
+ type="noul",
196
+ noul=noul,
197
+ )
198
+
199
+ if question.type == "choice":
200
+ pairs = sorted(zip(question.options, probs), key=lambda op: op[1], reverse=True)
201
+ top = pairs[0]
202
+ return QuestionAnswer(
203
+ type="choice",
204
+ choice=top[0].label,
205
+ probabilities={option.label: p for option, p in pairs},
206
+ confidence=confidence_from_probabilities(probs),
207
+ )
208
+
209
+ score = sum(i * p for i, p in enumerate(probs))
210
+ return QuestionAnswer(
211
+ type="score",
212
+ score=score,
213
+ probabilities={str(i): p for i, p in enumerate(probs)},
214
+ legend={str(i): option.description or "" for i, option in enumerate(question.options)},
215
+ confidence=confidence_from_probabilities(probs),
216
+ )
217
+
218
+
219
+ def score_question_tensor(question: Question, letter_logits, temperature: float = 1.0) -> QuestionAnswer:
220
+ """Score a ``[26]`` device tensor, transferring only final answer values.
221
+
222
+ The logits, legal-option gather, softmax, entropy, ranking, and score
223
+ reduction all remain on the tensor's device. Constructing ``QuestionAnswer``
224
+ necessarily creates Python/JSON values at the API boundary.
225
+ """
226
+ if temperature <= 0:
227
+ raise ValueError("temperature must be > 0")
228
+ if letter_logits.dim() != 1 or letter_logits.shape[0] != len(LETTERS):
229
+ raise ValueError("expected one [26] letter-logit tensor")
230
+
231
+ import torch
232
+
233
+ indices = torch.tensor(
234
+ [LETTER_INDEX[option.letter] for option in question.options],
235
+ device=letter_logits.device,
236
+ dtype=torch.long,
237
+ )
238
+ probs = torch.softmax(letter_logits.index_select(0, indices).float() / temperature, dim=0)
239
+
240
+ if question.type == "noul":
241
+ return QuestionAnswer(type="noul", noul=float(probs[0].item()))
242
+
243
+ if len(question.options) <= 1:
244
+ confidence_value = 1.0
245
+ else:
246
+ entropy = -(probs * torch.where(probs > 0, probs.log(), torch.zeros_like(probs))).sum()
247
+ confidence = 1.0 - entropy / torch.log(torch.tensor(float(len(question.options)), device=probs.device))
248
+ confidence_value = float(torch.clamp(confidence, min=0.0).item())
249
+
250
+ if question.type == "choice":
251
+ # Stable GPU argsort preserves the request's option order for exact ties.
252
+ order = torch.argsort(probs, descending=True, stable=True).tolist()
253
+ probability_values = probs.tolist()
254
+ return QuestionAnswer(
255
+ type="choice",
256
+ choice=question.options[order[0]].label,
257
+ probabilities={question.options[index].label: probability_values[index] for index in order},
258
+ confidence=confidence_value,
259
+ )
260
+
261
+ probability_values = probs.tolist()
262
+ levels = torch.arange(len(question.options), device=probs.device, dtype=probs.dtype)
263
+ score = float((levels * probs).sum().item())
264
+ return QuestionAnswer(
265
+ type="score",
266
+ score=score,
267
+ probabilities={str(index): value for index, value in enumerate(probability_values)},
268
+ legend={str(index): option.description or "" for index, option in enumerate(question.options)},
269
+ confidence=confidence_value,
270
+ )
render.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render every question in a request to a base-completion prompt.
2
+
3
+ Prompt layout
4
+ -------------
5
+ Every block puts the answer-relevant scaffolding *first* and the text being
6
+ judged *last*, so a prompt is::
7
+
8
+ Choices:
9
+ Y. Yes, ...
10
+ N. No, ...
11
+ Question: <question>
12
+ Text: <example text>
13
+ Answer: Y
14
+
15
+ ... more examples ...
16
+
17
+ Choices:
18
+ ...
19
+ Question: <target question>
20
+ Text: <the caller's state>
21
+ Answer:
22
+
23
+ Putting the state last makes the engine's prefix KV cache effective:
24
+ everything above ``Text:`` depends only on the question, so it is byte-identical
25
+ across calls and can be cached once. That took a warm Doom decision from 357ms to
26
+ 178ms (mean prompt tail 68 -> 20 tokens) with no accuracy change: 63/63 on the
27
+ 50-case suite plus the Doom probe and 8 held-out noul cases.
28
+
29
+ Two example strategies, chosen by question type:
30
+
31
+ - Noul (Y/N) uses a fixed bank of demonstrations with their own questions.
32
+ The predicate in the options ("Yes, the text requests a refund.") carries the
33
+ task; the examples teach the letter slot. Their questions
34
+ differ from the target question, so their Y/N labels stay truthful rather than
35
+ being relabelled under a question they were not written for. This lifted
36
+ accuracy from 3/6 to 6/6 in tests/example_transfer.py.
37
+
38
+ - Choice and Score use examples generated from the caller's criteria. Each
39
+ option gets one synthetic demonstration mapping it to its letter, visited in a
40
+ non-alphabetical order so the model cannot exploit A,B,C positional cues. This
41
+ reached 10/10 on Choice and 11/12 on Score in tests/choice_score_bakeoff2.py and
42
+ needs no hand-authoring.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from dataclasses import dataclass
48
+
49
+ from .questions import DEFAULT_EXAMPLE_COUNT, GENERIC_EXAMPLES, Question
50
+
51
+
52
+ @dataclass
53
+ class RenderedPrompt:
54
+ question_id: str
55
+ text: str
56
+
57
+
58
+ def _render_options(question: Question) -> str:
59
+ lines = []
60
+ for option in question.options:
61
+ if question.type == "noul":
62
+ lines.append(f"{option.letter}. {option.description or option.label}")
63
+ elif option.description:
64
+ lines.append(f"{option.letter}. {option.label} - {option.description}")
65
+ else:
66
+ lines.append(f"{option.letter}. {option.label}")
67
+ return "\n".join(lines)
68
+
69
+
70
+ def _header(question: Question, instructions: str | None = None) -> str:
71
+ """Choices + question. Identical for every call on the same question."""
72
+ return (
73
+ f"Choices:\n{_render_options(question)}\n"
74
+ f"Question: {instructions or question.instructions}\n"
75
+ )
76
+
77
+
78
+ def _noul_examples(question: Question, count: int) -> list[str]:
79
+ blocks = []
80
+ for ex_text, ex_question, ex_letter in GENERIC_EXAMPLES[:count]:
81
+ blocks.append(f"{_header(question, ex_question)}Text: {ex_text}\nAnswer: {ex_letter}")
82
+ return blocks
83
+
84
+
85
+ def _criteria_roundtrip_examples(question: Question) -> list[str]:
86
+ """One demonstration per option, built from the option's own description.
87
+
88
+ Options use a non-alphabetical order that
89
+ interleaves the scale (low, high, middle, ...) so no positional shortcut exists.
90
+ """
91
+ header = _header(question)
92
+ blocks = []
93
+ for index in _interleaved_order(len(question.options)):
94
+ option = question.options[index]
95
+ if question.type == "choice":
96
+ detail = f" This example is about the topic of {option.label}."
97
+ else:
98
+ detail = " " + (option.description or option.label)
99
+ blocks.append(
100
+ f"{header}Text: An example of {option.label}:{detail}\nAnswer: {option.letter}"
101
+ )
102
+ return blocks
103
+
104
+
105
+ def _interleaved_order(n: int) -> list[int]:
106
+ """Low, high, then remaining indices ascending. Breaks A,B,C positional cues."""
107
+ if n <= 2:
108
+ return list(range(n))
109
+ order = [0, n - 1]
110
+ order.extend(i for i in range(1, n - 1))
111
+ return order
112
+
113
+
114
+ def render_example_prefix(
115
+ question: Question,
116
+ *,
117
+ example_count: int = DEFAULT_EXAMPLE_COUNT,
118
+ ) -> str:
119
+ """The part of the prompt that depends only on the question, never on the state.
120
+
121
+ This is what the engine keeps in a KV cache across calls: the few-shot examples
122
+ plus the target question's own Choices/Question header. For a fixed question it
123
+ is byte-identical every time, and it is ~90% of the prompt.
124
+ """
125
+ if question.type == "noul":
126
+ blocks = _noul_examples(question, example_count)
127
+ else:
128
+ blocks = _criteria_roundtrip_examples(question)
129
+ body = "\n\n".join(blocks) + "\n\n" if blocks else ""
130
+ return body + _header(question)
131
+
132
+
133
+ def render_target_block(question: Question, state: str) -> str:
134
+ """The state-dependent tail, ending at the ``Answer:`` slot.
135
+
136
+ There is no trailing space after ``Answer:``. The letter tokens
137
+ the engine reads are space-prefixed (``"▁Y"``), which is how the examples above
138
+ tokenize (``['Answer', ':', '▁Y']``). Adding a trailing space here would emit a
139
+ standalone ``'��'`` token and strand the space, so the model would want a bare
140
+ ``"Y"`` while the engine read ``"▁Y"``. This measured 0.0000 probability mass on
141
+ the letters being scored, versus 0.9871 without the space.
142
+ """
143
+ return f"Text: {state}\nAnswer:"
144
+
145
+
146
+ def render_question(
147
+ question: Question,
148
+ state: str,
149
+ *,
150
+ example_count: int = DEFAULT_EXAMPLE_COUNT,
151
+ ) -> RenderedPrompt:
152
+ # Composed from the two halves so the cached-prefix path and the plain path
153
+ # always produce the exact same prompt string.
154
+ text = render_example_prefix(question, example_count=example_count) + render_target_block(
155
+ question, state
156
+ )
157
+ return RenderedPrompt(question_id=question.id, text=text)
158
+
159
+
160
+ def render_batch(
161
+ questions: list[Question],
162
+ state: str,
163
+ *,
164
+ example_count: int = DEFAULT_EXAMPLE_COUNT,
165
+ ) -> list[RenderedPrompt]:
166
+ """One prompt per question. Every question sees the same state, as in TypeSafe."""
167
+ return [
168
+ render_question(question, state, example_count=example_count) for question in questions
169
+ ]
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12bac982b793c44b03d52a250a9f0d0b666813da566b910c24a6da0695fd11e6
3
+ size 32170070
tokenizer_config.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|audio|>",
3
+ "backend": "tokenizers",
4
+ "boa_token": "<|audio>",
5
+ "boi_token": "<|image>",
6
+ "bos_token": "<bos>",
7
+ "eoa_token": "<audio|>",
8
+ "eoc_token": "<channel|>",
9
+ "eoi_token": "<image|>",
10
+ "eos_token": "<eos>",
11
+ "eot_token": "<turn|>",
12
+ "escape_token": "<|\"|>",
13
+ "etc_token": "<tool_call|>",
14
+ "etd_token": "<tool|>",
15
+ "etr_token": "<tool_response|>",
16
+ "extra_special_tokens": [
17
+ "<|video|>"
18
+ ],
19
+ "image_token": "<|image|>",
20
+ "is_local": false,
21
+ "local_files_only": false,
22
+ "mask_token": "<mask>",
23
+ "model_max_length": 1000000000000000019884624838656,
24
+ "model_specific_special_tokens": {
25
+ "audio_token": "<|audio|>",
26
+ "boa_token": "<|audio>",
27
+ "boi_token": "<|image>",
28
+ "eoa_token": "<audio|>",
29
+ "eoc_token": "<channel|>",
30
+ "eoi_token": "<image|>",
31
+ "eot_token": "<turn|>",
32
+ "escape_token": "<|\"|>",
33
+ "etc_token": "<tool_call|>",
34
+ "etd_token": "<tool|>",
35
+ "etr_token": "<tool_response|>",
36
+ "image_token": "<|image|>",
37
+ "soc_token": "<|channel>",
38
+ "sot_token": "<|turn>",
39
+ "stc_token": "<|tool_call>",
40
+ "std_token": "<|tool>",
41
+ "str_token": "<|tool_response>",
42
+ "think_token": "<|think|>"
43
+ },
44
+ "pad_token": "<pad>",
45
+ "padding_side": "left",
46
+ "processor_class": "Gemma4UnifiedProcessor",
47
+ "soc_token": "<|channel>",
48
+ "sot_token": "<|turn>",
49
+ "stc_token": "<|tool_call>",
50
+ "std_token": "<|tool>",
51
+ "str_token": "<|tool_response>",
52
+ "think_token": "<|think|>",
53
+ "tokenizer_class": "GemmaTokenizer",
54
+ "unk_token": "<unk>"
55
+ }