1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Pure, model-free generation-control logic: how a
//! [`crate::CompletionRequest`]'s `max_tokens` and a model's candidate stop
//! tokens become the `max_new_tokens`/`eos_token_id` fields of a
//! `kopitiam_runtime::GenerationConfig`.
//!
//! Factored out of `adapter.rs` and kept free of any `kopitiam-runtime`/
//! `kopitiam-tokenizer` type specifically so it is testable without
//! constructing a real (or even synthetic) model — see this module's tests.
/// Upper bound on generated tokens when a [`crate::CompletionRequest`] does
/// not set `max_tokens`. `kopitiam_runtime::GenerationConfig`'s own default
/// is `256`; this module picks the same number so "no limit requested"
/// behaves identically whether or not a caller thought to ask for the
/// default explicitly.
pub const DEFAULT_MAX_NEW_TOKENS: usize = 256;
/// The sampling defaults a local chat runs with.
///
/// # These numbers are ollama's, not ours
///
/// Every value below is transcribed from **ollama's `DefaultOptions()`**
/// (`crates/kopitiam-runtime/vendor/ollama/api/types.go:1124`, MIT), which is
/// the reference implementation for exactly this decision and has been proven
/// against far more models than this project will ever test:
///
/// ```text
/// Temperature: 0.8
/// TopK: 40
/// TopP: 0.9
/// RepeatLastN: 64
/// RepeatPenalty: 1.1
/// Seed: -1 // random; see the divergence note below
/// ```
///
/// # Why sampling at all — the bug that forced this
///
/// The local adapter used to call `kopitiam_runtime::generate`, which is
/// **greedy argmax with no repetition penalty**. On a small instruct model
/// that degenerates, and not subtly: asked "hello", SmolLM2-360M-Instruct
/// answered
///
/// ```text
/// "I'm a bot, I'm a bot, I'm a chatbot, I'm a chatbot, I'm a chatbot, ..."
/// ```
///
/// for the full 256-token budget, because the loop never made `<|im_end|>` the
/// argmax so end-of-turn was never reached. Nothing was wrong with the weights,
/// the tokenizer, the ChatML template or the EOS id — all four were verified
/// individually, and raw completion answered "The capital of France is" with
/// " Paris." correctly throughout. Greedy decoding alone caused it. The
/// repetition penalty is what breaks the loop; temperature/top-k/top-p keep the
/// distribution sane while it does.
///
/// # Where we deliberately differ from ollama, and why
///
/// **`seed`.** ollama defaults to `Seed: -1`, i.e. fresh entropy per request.
/// We pin a fixed seed instead, because `kopitiam_runtime::SamplingConfig`
/// makes seeding mandatory on purpose (see its docs) and this project values a
/// reproducible answer over a varied one: a bug report that cannot be
/// reproduced is barely a bug report. A caller wanting variation sets the seed
/// itself.
pub
/// Resolves [`crate::CompletionRequest::max_tokens`] into the `usize`
/// `kopitiam_runtime::GenerationConfig::max_new_tokens` expects, applying
/// [`DEFAULT_MAX_NEW_TOKENS`] when the caller did not set one.
pub
/// Picks the single token id `kopitiam_runtime::generate` should treat as
/// end-of-sequence.
///
/// `kopitiam_runtime::GenerationConfig::eos_token_id` accepts exactly one
/// id, so when a GGUF vocabulary offers more than one plausible stop token,
/// something has to choose between them. Priority, highest first:
///
/// 1. `<|im_end|>` — since [`super::chat_template`] always renders ChatML
/// and always primes the model with a trailing
/// `<|im_start|>assistant\n`, this is the token that actually closes
/// the turn this crate asked for.
/// 2. The GGUF's own `tokenizer.ggml.eos_token_id` metadata, when present —
/// the model file's own declared default end-of-sequence id, for
/// vocabularies where it differs from `<|im_end|>` (e.g. a base,
/// non-chat-tuned checkpoint with no ChatML special tokens at all).
/// 3. `<|endoftext|>` — the GPT-2/Qwen family's universal fallback
/// end-of-sequence marker, consulted last.
///
/// `None` if none of the three is available, in which case generation runs
/// for the full `max_new_tokens` budget every time — still correct, just
/// unable to stop early.
pub