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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! The pluggable generation backend a request is served from.
//!
//! Two implementations: [`ScriptedChatModel`], which drives the
//! raw-completion loop with a fixed, pre-scripted logit sequence (portable,
//! and what the integration tests use to prove the HTTP envelopes, chat
//! templating, and SSE framing), and [`crate::RealChatModel`] (macOS only),
//! which drives a real `RealForwardRunner` forward pass.
//!
//! [`ChatModel::with_producer`] is a pass-through rather than a
//! `new_producer() -> Box<dyn LogitProducer>` factory on purpose: a real
//! runner costs a multi-gigabyte mmap plus a full Metal pipeline compile to
//! open, so there is exactly one per process and it is borrowed mutably for
//! the duration of a request, not handed out by value.
use runtime::{
run_raw_completion_cancellable, CancelFlag, GenerationConfig, LogitProducer, RawDecodeProgress,
RawDecodeResult, RuntimeError,
};
use tokenizer::MfTokenizer;
pub trait ChatModel: Send + Sync {
fn tokenizer(&self) -> &MfTokenizer;
fn vocab_size(&self) -> usize;
fn max_context(&self) -> u32;
/// The canonical id `GET /v1/models` advertises. A registry routes an
/// exact request for this id (or an advertised alias) to this backend.
fn model_id(&self) -> &str;
/// Extra ids `GET /v1/models` advertises for this chat backend.
///
/// Claude Code gateway discovery keeps only ids containing `claude` or
/// `anthropic`. Giving every generative backend its own derived alias
/// keeps that discovery path useful on a multi-model local server without
/// pretending a local checkpoint is an Anthropic product model.
fn model_aliases(&self) -> Vec<String> {
vec![format!("claude-turbospark-{}", self.model_id())]
}
/// Lends a producer to `f` for one generation. Implementations may
/// serialize concurrent calls; `run_raw_completion` resets the producer
/// on entry, so a producer reused across calls carries no state over.
fn with_producer(
&self,
f: &mut dyn FnMut(&mut dyn LogitProducer) -> Result<RawDecodeResult, RuntimeError>,
) -> Result<RawDecodeResult, RuntimeError>;
/// Runs one generation, and owns the choice of WHICH decode loop.
///
/// **A method rather than a call in `exec.rs`, because the speculative
/// loop cannot be reached through [`Self::with_producer`] at all.**
/// `run_raw_completion_speculative` is generic over
/// `runtime::SpeculativeProducer`, which carries an associated
/// `Checkpoint` type and so is not object-safe; `with_producer` hands out
/// a `&mut dyn LogitProducer`. Only a backend holding the CONCRETE runner
/// can call it, so the decision belongs to the backend.
///
/// The default is the sequential loop through `with_producer`, which is
/// byte for byte what every caller did before this method existed --
/// `ScriptedChatModel` therefore needs no implementation and every
/// integration test keeps the exact path it had.
/// This install's vision configuration, or `None` where it has no tower.
///
/// Read off the INSTALL by the backend: the pixel budget and the special
/// ids are per-checkpoint, and a constant in the handler would be
/// AGENTS.md Gotcha 38's shape. `None` is what makes an image request a
/// reported DROP rather than a silent one.
fn vision(&self) -> Option<crate::vision::VisionInfo> {
None
}
/// Whether this model supports dense vector embedding generation.
fn supports_embeddings(&self) -> bool {
false
}
/// Encode input texts into dense vector embeddings.
fn encode(&self, _texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
Err(format!(
"model {} does not support embeddings",
self.model_id()
))
}
/// Runs one generation, and owns the choice of WHICH decode loop.
///
/// **`images` CANNOT BE A SEPARATE CALL, and that is a concurrency
/// property rather than a style choice.** A backend serializes on its one
/// runner per call (Gotcha 1), so `set_prompt_vision` followed by
/// `run_completion` would be two locks with a gap: a second request
/// arriving in that gap overwrites the map, and the first generation then
/// prefills the second's picture. Both are fluent. Passing the images
/// here is what puts the encode, the injection and the decode inside ONE
/// lock.
///
/// `cancel` is polled once per prefill and decoded token by the runtime
/// loop underneath; a caller with nothing to cancel on passes `&|| false`,
/// which is the same statement sequence this ran before cancellation
/// existed (`runtime::run_raw_completion`'s own `NEVER` constant, not
/// exported past that crate, is exactly this).
fn run_completion(
&self,
prompt_ids: &[foundation::TokenId],
config: &GenerationConfig,
images: Option<&crate::vision::RequestImages>,
cancel: CancelFlag<'_>,
on_progress: &mut dyn FnMut(RawDecodeProgress),
) -> Result<RawDecodeResult, RuntimeError> {
if images.is_some() {
// The DEFAULT backend is the scripted one, which has no tower and
// no runner to encode with. Refused BY NAME rather than dropped:
// a caller who sent a picture and got a text answer would read it
// as the model ignoring the image.
return Err(RuntimeError::Producer(
"this backend cannot encode images; run the server against a .gturbo install \
whose checkpoint carries a vision tower"
.to_string(),
));
}
self.with_producer(&mut |producer| {
run_raw_completion_cancellable(
producer,
self.tokenizer(),
prompt_ids,
config,
self.max_context(),
self.vocab_size(),
cancel,
&mut *on_progress,
)
})
}
/// The decode rate cap and thermal stepping this backend generates
/// under (ROADMAP Phase P2). Process-level, not per request: there is
/// one runner per process and a power setting is a property of the
/// machine, not of a caller's prompt. The default is uncapped, which is
/// what the scripted backend wants.
fn rate_control(&self) -> runtime::RateControl {
runtime::RateControl::default()
}
/// The FIFO admission gate this backend's generations pass through
/// (ROADMAP P1 item 5, `crate::queue`). `None` -- the default -- means
/// ungated: the scripted backend serializes on nothing today and keeps
/// that behavior, so the integration suite's timing is untouched.
///
/// A backend that owns a single contended runner (the real one) returns
/// ONE gate per runner, and the request paths acquire it in ARRIVAL
/// order before touching the runner -- the fairness the runner's own
/// mutex cannot provide. Callers acquire at a closed, non-nesting set
/// of async sites (`run_guarded`, the live-stream spawn sites, ollama's
/// `run`, `completions::run_full`, embeddings); acquiring twice on one
/// request would deadlock on the single permit, so the acquisition
/// points are load-bearing and documented in `queue.rs`.
fn generation_queue(&self) -> Option<std::sync::Arc<crate::queue::GenerationQueue>> {
None
}
/// Which tool-call guardrails this backend generates under.
///
/// Process-level for exactly the reason [`ChatModel::rate_control`] is,
/// and NOT for a symmetry: whether a deployment repairs a malformed tool
/// call is a property of the deployment, and a per-request field would let
/// any client opt its own traffic out of it.
///
/// The default is ON, which costs the scripted backend nothing: every
/// guardrail is keyed on the request carrying tools, and a request with
/// none takes the path it always took.
fn guardrails(&self) -> crate::guardrails::GuardrailConfig {
crate::guardrails::GuardrailConfig::default()
}
/// Default reasoning effort level when a request omits `reasoning_effort`.
fn default_reasoning(&self) -> tokenizer::ReasoningEffort {
tokenizer::ReasoningEffort::Off
}
/// Deployment-wide system prompt for requests that carry no system or
/// developer message of their own (`--system` / `--system-file`).
///
/// Process-level for `default_reasoning`'s reason, and applied at ONE
/// place -- `handler::plan` -- because every wire format this server
/// speaks converts to `ChatCompletionRequest` before it reaches there.
fn default_system(&self) -> Option<&str> {
None
}
}
/// Always replays the same scripted logit sequence, regardless of the
/// prompt. `steps` must include one entry per prefill token plus one per
/// decode step the caller expects to reach before a stop condition.
pub struct ScriptedChatModel {
tokenizer: MfTokenizer,
vocab_size: usize,
max_context: u32,
steps: Vec<Vec<foundation::LogitValue>>,
default_reasoning: tokenizer::ReasoningEffort,
default_system: Option<String>,
}
impl ScriptedChatModel {
pub fn new(
tokenizer: MfTokenizer,
max_context: u32,
steps: Vec<Vec<foundation::LogitValue>>,
) -> Self {
let vocab_size = tokenizer.vocab_size;
Self {
tokenizer,
vocab_size,
max_context,
steps,
default_reasoning: tokenizer::ReasoningEffort::Off,
default_system: None,
}
}
pub fn with_default_reasoning(mut self, reasoning: tokenizer::ReasoningEffort) -> Self {
self.default_reasoning = reasoning;
self
}
pub fn with_default_system(mut self, system: impl Into<String>) -> Self {
self.default_system = Some(system.into());
self
}
}
impl ChatModel for ScriptedChatModel {
fn tokenizer(&self) -> &MfTokenizer {
&self.tokenizer
}
fn vocab_size(&self) -> usize {
self.vocab_size
}
fn max_context(&self) -> u32 {
self.max_context
}
fn model_id(&self) -> &str {
"scripted"
}
fn default_reasoning(&self) -> tokenizer::ReasoningEffort {
self.default_reasoning
}
fn default_system(&self) -> Option<&str> {
self.default_system.as_deref()
}
fn with_producer(
&self,
f: &mut dyn FnMut(&mut dyn LogitProducer) -> Result<RawDecodeResult, RuntimeError>,
) -> Result<RawDecodeResult, RuntimeError> {
let mut producer = runtime::ScriptedLogitProducer::new(self.steps.clone());
f(&mut producer)
}
fn supports_embeddings(&self) -> bool {
true
}
fn encode(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
let mut out = Vec::with_capacity(texts.len());
for text in texts {
let len = text.len() as f32;
let mut vec = vec![1.0, len * 0.1, 0.5, -0.2];
let norm: f32 = vec.iter().map(|v| v * v).sum::<f32>().sqrt();
for x in &mut vec {
*x /= norm;
}
out.push(vec);
}
Ok(out)
}
}