Skip to main content

eredu_core/
generation.rs

1//! Portable generation configuration, lifecycle, and speculative bookkeeping.
2
3use crate::backend::{
4    BoundedCompletionWait, BoundedCompletionWaitError, CompletionCancellationMode,
5};
6use serde::{Deserialize, Serialize};
7use std::sync::{
8    atomic::{AtomicBool, Ordering},
9    Arc,
10};
11
12/// Why generation reached a terminal state.
13#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum FinishReason {
16    /// A checkpoint end-of-sequence token was committed.
17    Eos,
18    /// A caller or protocol stop sequence matched.
19    StopSequence,
20    /// The committed generation grammar reached an accepting state.
21    GrammarComplete,
22    /// The configured output-token budget was exhausted.
23    MaxTokens,
24    /// The caller cooperatively cancelled generation.
25    Cancelled,
26}
27
28/// Cheap thread-safe cooperative cancellation observed between submissions.
29#[derive(Debug, Clone, Default)]
30pub struct GenerationCancellationToken {
31    cancelled: Arc<AtomicBool>,
32}
33
34impl GenerationCancellationToken {
35    /// Creates an active token.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Permanently requests cancellation for every clone.
41    pub fn cancel(&self) {
42        self.cancelled.store(true, Ordering::Release);
43    }
44
45    /// Returns whether cancellation has been requested.
46    pub fn is_cancelled(&self) -> bool {
47        self.cancelled.load(Ordering::Acquire)
48    }
49}
50
51/// Terminal signals observed while committing one token.
52#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
53pub struct TokenTerminalSignals {
54    /// Decoded output matched a stop sequence.
55    pub stop_sequence: bool,
56    /// The committed grammar is complete.
57    pub grammar_complete: bool,
58}
59
60/// Result of one canonical token commit.
61#[derive(Debug, Clone, Copy, Eq, PartialEq)]
62pub struct TokenCommit {
63    /// Token id committed to the logical output.
64    pub token_id: u32,
65    /// Zero-based generated-token position.
66    pub position: usize,
67    /// Terminal reason selected after this commit, if any.
68    pub finish_reason: Option<FinishReason>,
69}
70
71/// Canonical committed-token sequence and terminal-condition precedence.
72#[derive(Debug, Clone, Eq, PartialEq)]
73pub struct GenerationSequence {
74    max_tokens: usize,
75    eos_token_ids: Vec<u32>,
76    tokens: Vec<u32>,
77    finish_reason: Option<FinishReason>,
78}
79
80impl GenerationSequence {
81    /// Creates an empty output sequence with a fixed token budget.
82    pub fn new(max_tokens: usize, eos_token_ids: impl IntoIterator<Item = u32>) -> Self {
83        let mut eos_token_ids = eos_token_ids.into_iter().collect::<Vec<_>>();
84        eos_token_ids.sort_unstable();
85        eos_token_ids.dedup();
86        Self {
87            max_tokens,
88            eos_token_ids,
89            tokens: Vec::with_capacity(max_tokens),
90            finish_reason: (max_tokens == 0).then_some(FinishReason::MaxTokens),
91        }
92    }
93
94    /// Commits one token and applies stop, grammar, EOS, and budget precedence.
95    pub fn commit(
96        &mut self,
97        token_id: u32,
98        signals: TokenTerminalSignals,
99    ) -> Result<TokenCommit, GenerationError> {
100        if self.finish_reason.is_some() {
101            return Err(GenerationError::AlreadyFinished);
102        }
103        let position = self.tokens.len();
104        self.tokens.push(token_id);
105        let finish_reason = signals
106            .stop_sequence
107            .then_some(FinishReason::StopSequence)
108            .or_else(|| {
109                signals
110                    .grammar_complete
111                    .then_some(FinishReason::GrammarComplete)
112            })
113            .or_else(|| {
114                self.eos_token_ids
115                    .binary_search(&token_id)
116                    .is_ok()
117                    .then_some(FinishReason::Eos)
118            })
119            .or_else(|| (self.tokens.len() == self.max_tokens).then_some(FinishReason::MaxTokens));
120        self.finish_reason = finish_reason;
121        Ok(TokenCommit {
122            token_id,
123            position,
124            finish_reason,
125        })
126    }
127
128    /// Applies cancellation if the sequence has not already terminated.
129    pub fn cancel(&mut self) -> bool {
130        if self.tokens.is_empty() && self.finish_reason == Some(FinishReason::MaxTokens) {
131            self.finish_reason = Some(FinishReason::Cancelled);
132            true
133        } else if self.finish_reason.is_some() {
134            false
135        } else {
136            self.finish_reason = Some(FinishReason::Cancelled);
137            true
138        }
139    }
140
141    /// Observes a cooperative token at a submission boundary.
142    pub fn observe_cancellation(&mut self, cancellation: &GenerationCancellationToken) -> bool {
143        cancellation.is_cancelled() && self.cancel()
144    }
145
146    /// Committed tokenizer ids in canonical order.
147    pub fn tokens(&self) -> &[u32] {
148        &self.tokens
149    }
150
151    /// Consumes the state into committed tokenizer ids.
152    pub fn into_tokens(self) -> Vec<u32> {
153        self.tokens
154    }
155
156    /// Number of remaining token slots.
157    pub fn remaining(&self) -> usize {
158        self.max_tokens.saturating_sub(self.tokens.len())
159    }
160
161    /// Configured output-token budget.
162    pub const fn max_tokens(&self) -> usize {
163        self.max_tokens
164    }
165
166    /// Selected terminal reason.
167    pub const fn finish_reason(&self) -> Option<FinishReason> {
168        self.finish_reason
169    }
170
171    /// Whether no further token may be committed.
172    pub const fn is_finished(&self) -> bool {
173        self.finish_reason.is_some()
174    }
175}
176
177/// Kind of non-proposal token emitted by speculative verification.
178#[derive(Debug, Clone, Copy, Eq, PartialEq)]
179pub enum SpeculativeTail {
180    /// A rejected proposal was replaced from the target distribution.
181    Replacement,
182    /// Every proposal was accepted and the target emitted its bonus token.
183    Bonus,
184}
185
186/// Canonical bookkeeping for one proposal/verification transaction.
187#[derive(Debug, Clone, Eq, PartialEq)]
188pub struct SpeculativeRound {
189    proposal_count: usize,
190    accepted: usize,
191    committed_tokens: Vec<u32>,
192    tail: Option<SpeculativeTail>,
193    terminal: bool,
194}
195
196impl SpeculativeRound {
197    /// Starts a verification round for a non-empty proposal block.
198    pub fn new(proposal_count: usize) -> Result<Self, GenerationError> {
199        if proposal_count == 0 {
200            return Err(GenerationError::EmptyProposalBlock);
201        }
202        Ok(Self {
203            proposal_count,
204            accepted: 0,
205            committed_tokens: Vec::with_capacity(proposal_count + 1),
206            tail: None,
207            terminal: false,
208        })
209    }
210
211    /// Records the next accepted proposal token.
212    pub fn accept(&mut self, token: u32, terminal: bool) -> Result<(), GenerationError> {
213        if self.tail.is_some() || self.accepted == self.proposal_count || self.terminal {
214            return Err(GenerationError::InvalidSpeculativeTransition);
215        }
216        self.accepted += 1;
217        self.committed_tokens.push(token);
218        self.terminal = terminal;
219        Ok(())
220    }
221
222    /// Records the target replacement for the first rejected proposal.
223    pub fn reject_with(&mut self, token: u32, terminal: bool) -> Result<(), GenerationError> {
224        if self.tail.is_some() || self.accepted == self.proposal_count || self.terminal {
225            return Err(GenerationError::InvalidSpeculativeTransition);
226        }
227        self.tail = Some(SpeculativeTail::Replacement);
228        self.committed_tokens.push(token);
229        self.terminal = terminal;
230        Ok(())
231    }
232
233    /// Records the target bonus after complete proposal acceptance.
234    pub fn bonus(&mut self, token: u32, terminal: bool) -> Result<(), GenerationError> {
235        if self.tail.is_some() || self.accepted != self.proposal_count || self.terminal {
236            return Err(GenerationError::InvalidSpeculativeTransition);
237        }
238        self.tail = Some(SpeculativeTail::Bonus);
239        self.committed_tokens.push(token);
240        self.terminal = terminal;
241        Ok(())
242    }
243
244    /// Whether every proposal was accepted.
245    pub const fn is_full_acceptance(&self) -> bool {
246        self.accepted == self.proposal_count
247    }
248
249    /// Produces the exact cache-retention and output-publication plan.
250    pub fn commit_plan(&self) -> Result<SpeculativeCommitPlan<'_>, GenerationError> {
251        if !self.terminal && self.tail.is_none() {
252            return Err(GenerationError::IncompleteSpeculativeRound);
253        }
254        Ok(SpeculativeCommitPlan {
255            accepted_proposals: self.accepted,
256            committed_tokens: &self.committed_tokens,
257            verified_inputs: if self.tail.is_some() {
258                1 + self.accepted
259            } else {
260                self.accepted
261            },
262            full_acceptance: self.accepted == self.proposal_count,
263            tail: self.tail,
264            terminal: self.terminal,
265        })
266    }
267}
268
269/// Borrowed resolution plan for a speculative transaction.
270#[derive(Debug, Clone, Copy, Eq, PartialEq)]
271pub struct SpeculativeCommitPlan<'a> {
272    /// Number of accepted assistant proposals.
273    pub accepted_proposals: usize,
274    /// Tokens that become visible after the backend cache commit succeeds.
275    pub committed_tokens: &'a [u32],
276    /// Verification inputs retained in the target cache.
277    pub verified_inputs: usize,
278    /// Whether every assistant proposal was accepted.
279    pub full_acceptance: bool,
280    /// Optional replacement or target bonus.
281    pub tail: Option<SpeculativeTail>,
282    /// Whether the last committed token terminated generation.
283    pub terminal: bool,
284}
285
286/// Reuse decision for a proposal block drafted against an assumed prefix.
287#[derive(Debug, Clone, Copy, Eq, PartialEq)]
288pub enum OptimisticReuseDecision {
289    /// Terminal output makes the complete branch unusable.
290    DiscardTerminal,
291    /// The target bonus differed from the optimistic first token.
292    DiscardMismatch,
293    /// The matching first token consumed the complete optimistic block.
294    MatchedConsumed,
295    /// The first optimistic token matched and later proposals remain reusable.
296    MatchedRetained,
297}
298
299/// Resolves optimistic proposal reuse without inspecting backend state.
300pub fn resolve_optimistic_reuse(
301    assumed_prefix: &[u32],
302    canonical_prefix: &[u32],
303    optimistic_tokens: &[u32],
304    bonus: u32,
305    terminal: bool,
306) -> Result<OptimisticReuseDecision, GenerationError> {
307    if assumed_prefix != canonical_prefix {
308        return Err(GenerationError::OptimisticPrefixDiverged);
309    }
310    let first = optimistic_tokens
311        .first()
312        .ok_or(GenerationError::EmptyOptimisticBranch)?;
313    if terminal {
314        return Ok(OptimisticReuseDecision::DiscardTerminal);
315    }
316    if *first != bonus {
317        return Ok(OptimisticReuseDecision::DiscardMismatch);
318    }
319    Ok(if optimistic_tokens.len() == 1 {
320        OptimisticReuseDecision::MatchedConsumed
321    } else {
322        OptimisticReuseDecision::MatchedRetained
323    })
324}
325
326/// Options shared by speculative multi-token backends.
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct SpeculativeConfig {
329    /// Maximum number of output tokens, including terminal tokens.
330    pub max_tokens: usize,
331    /// Maximum assistant proposals per verification round.
332    pub max_draft_tokens: usize,
333    /// Sampling temperature. Zero selects greedy verification.
334    pub temperature: f32,
335    /// Token ids that terminate a sequence.
336    pub eos_token_ids: Vec<u32>,
337}
338
339impl Default for SpeculativeConfig {
340    fn default() -> Self {
341        Self {
342            max_tokens: 256,
343            max_draft_tokens: 4,
344            temperature: 0.0,
345            eos_token_ids: Vec::new(),
346        }
347    }
348}
349
350impl SpeculativeConfig {
351    /// Validates backend-independent speculative settings.
352    pub fn validate(&self) -> Result<(), GenerationError> {
353        if self.max_draft_tokens == 0 {
354            return Err(GenerationError::ZeroDraftTokens);
355        }
356        if !self.temperature.is_finite() || self.temperature < 0.0 {
357            return Err(GenerationError::InvalidTemperature(self.temperature));
358        }
359        Ok(())
360    }
361}
362
363/// Bounded fair-scheduler settings for speculative requests.
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
365#[serde(default)]
366pub struct SpeculativeSchedulerOptions {
367    /// Maximum retained target verification transactions.
368    pub max_in_flight_verifications: usize,
369    /// Maximum retained optimistic branches.
370    pub max_optimistic_branches: usize,
371    /// Proposal blocks drafted ahead per request; currently zero or one.
372    pub lookahead_blocks: usize,
373    /// Disable lookahead adaptively when discarded work dominates reuse.
374    pub adaptive_lookahead: bool,
375    /// Resolved branches required before adaptive disabling.
376    pub adaptive_lookahead_min_blocks: usize,
377    /// Maximum milliseconds spent awaiting one exact verification completion.
378    pub completion_timeout_milliseconds: u64,
379    /// Safe disposition required if verification remains live at its deadline.
380    pub completion_cancellation: CompletionCancellationMode,
381}
382
383impl Default for SpeculativeSchedulerOptions {
384    fn default() -> Self {
385        Self {
386            max_in_flight_verifications: 1,
387            max_optimistic_branches: 1,
388            lookahead_blocks: 1,
389            adaptive_lookahead: true,
390            adaptive_lookahead_min_blocks: 4,
391            completion_timeout_milliseconds: 30_000,
392            completion_cancellation: CompletionCancellationMode::QuarantineUntilComplete,
393        }
394    }
395}
396
397impl SpeculativeSchedulerOptions {
398    /// Enables or disables same-request optimistic lookahead.
399    pub fn with_lookahead(mut self, enabled: bool) -> Self {
400        self.lookahead_blocks = usize::from(enabled);
401        if enabled {
402            self.max_optimistic_branches = self.max_optimistic_branches.max(1);
403        }
404        self
405    }
406
407    /// Selects the exact-completion deadline and safe timeout disposition.
408    pub fn with_completion_wait(
409        mut self,
410        timeout: std::time::Duration,
411        cancellation: CompletionCancellationMode,
412    ) -> Result<Self, GenerationError> {
413        let timeout = u64::try_from(timeout.as_millis())
414            .map_err(|_| GenerationError::SpeculativeCompletionTimeoutTooLarge)?;
415        if timeout == 0 {
416            return Err(GenerationError::ZeroSpeculativeCompletionTimeout);
417        }
418        self.completion_timeout_milliseconds = timeout;
419        self.completion_cancellation = cancellation;
420        Ok(self)
421    }
422
423    /// Resolves the portable fields into the bounded backend completion policy.
424    pub fn completion_wait(self) -> Result<BoundedCompletionWait, GenerationError> {
425        BoundedCompletionWait::new(
426            std::time::Duration::from_millis(self.completion_timeout_milliseconds),
427            self.completion_cancellation,
428        )
429        .map_err(|error| match error {
430            BoundedCompletionWaitError::ZeroTimeout => {
431                GenerationError::ZeroSpeculativeCompletionTimeout
432            }
433        })
434    }
435
436    /// Validates scheduler capacity and lookahead invariants.
437    pub fn validate(self) -> Result<Self, GenerationError> {
438        if self.max_in_flight_verifications == 0 {
439            return Err(GenerationError::ZeroInFlightVerifications);
440        }
441        if self.lookahead_blocks > 1 {
442            return Err(GenerationError::TooManyLookaheadBlocks);
443        }
444        if self.lookahead_blocks > 0 && self.max_optimistic_branches == 0 {
445            return Err(GenerationError::LookaheadWithoutBranchCapacity);
446        }
447        if self.lookahead_blocks > 0
448            && self.adaptive_lookahead
449            && self.adaptive_lookahead_min_blocks == 0
450        {
451            return Err(GenerationError::ZeroAdaptiveLookaheadWindow);
452        }
453        self.completion_wait()?;
454        Ok(self)
455    }
456}
457
458/// Stable speculative scheduler request identifier.
459#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
460pub struct SpeculativeRequestId(usize);
461
462impl SpeculativeRequestId {
463    /// Creates an identifier from its stable scheduler insertion index.
464    pub const fn new(index: usize) -> Self {
465        Self(index)
466    }
467
468    /// Returns the scheduler insertion index.
469    pub const fn index(self) -> usize {
470        self.0
471    }
472}
473
474/// Explicit speculative request/round state.
475#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case")]
477#[non_exhaustive]
478pub enum SpeculativeRequestStatus {
479    /// Target prompt prefill and first-token sampling.
480    Prefill,
481    /// Committed target state is ready to seed proposals.
482    ReadyToDraft,
483    /// A proposal block is ready for target submission.
484    ReadyToSubmitVerification,
485    /// Target verification is submitted and unresolved.
486    TargetVerificationInFlight,
487    /// Same-request continuation is being drafted optimistically.
488    OptimisticDraftRunning,
489    /// Verification is in flight and its optimistic branch is ready.
490    OptimisticDraftReady,
491    /// Target results are being accepted/rejected and committed.
492    VerificationResolution,
493    /// The request reached a normal terminal condition.
494    Completed,
495    /// The request was cancelled.
496    Cancelled,
497}
498
499/// Result of requesting cancellation at a speculative submission boundary.
500#[derive(Debug, Clone, Copy, Eq, PartialEq)]
501pub enum SpeculativeCancellationDisposition {
502    /// The request was already terminal.
503    AlreadyTerminal,
504    /// No backend submission is retained, so cancellation completed now.
505    CancelNow,
506    /// A backend submission must reach its exact safe boundary first.
507    Deferred,
508}
509
510/// Validated backend-neutral lifecycle of one speculative request.
511#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
512pub struct SpeculativeRequestLifecycle {
513    status: SpeculativeRequestStatus,
514    cancellation_pending: bool,
515}
516
517impl Default for SpeculativeRequestLifecycle {
518    fn default() -> Self {
519        Self::new()
520    }
521}
522
523impl SpeculativeRequestLifecycle {
524    /// Starts a request at prompt prefill.
525    pub const fn new() -> Self {
526        Self {
527            status: SpeculativeRequestStatus::Prefill,
528            cancellation_pending: false,
529        }
530    }
531
532    /// Creates a request completed before backend submission.
533    pub const fn completed() -> Self {
534        Self {
535            status: SpeculativeRequestStatus::Completed,
536            cancellation_pending: false,
537        }
538    }
539
540    /// Creates a request cancelled before backend submission.
541    pub const fn cancelled() -> Self {
542        Self {
543            status: SpeculativeRequestStatus::Cancelled,
544            cancellation_pending: false,
545        }
546    }
547
548    /// Current lifecycle status.
549    pub const fn status(&self) -> SpeculativeRequestStatus {
550        self.status
551    }
552
553    /// Whether cancellation must be applied after an exact backend boundary.
554    pub const fn cancellation_pending(&self) -> bool {
555        self.cancellation_pending
556    }
557
558    /// Whether the request can own no further submissions.
559    pub const fn is_terminal(&self) -> bool {
560        matches!(
561            self.status,
562            SpeculativeRequestStatus::Completed | SpeculativeRequestStatus::Cancelled
563        )
564    }
565
566    /// Requests cancellation without discarding an in-flight backend transaction.
567    pub fn request_cancellation(
568        &mut self,
569        submission_retained: bool,
570    ) -> Result<SpeculativeCancellationDisposition, GenerationError> {
571        if self.is_terminal() {
572            return Ok(SpeculativeCancellationDisposition::AlreadyTerminal);
573        }
574        if submission_retained {
575            self.cancellation_pending = true;
576            Ok(SpeculativeCancellationDisposition::Deferred)
577        } else {
578            self.transition(SpeculativeRequestStatus::Cancelled)?;
579            Ok(SpeculativeCancellationDisposition::CancelNow)
580        }
581    }
582
583    /// Applies one legal lifecycle transition.
584    pub fn transition(&mut self, next: SpeculativeRequestStatus) -> Result<(), GenerationError> {
585        let allowed = matches!(
586            (self.status, next),
587            (
588                SpeculativeRequestStatus::Prefill,
589                SpeculativeRequestStatus::ReadyToDraft
590            ) | (
591                SpeculativeRequestStatus::Prefill,
592                SpeculativeRequestStatus::Completed
593            ) | (
594                SpeculativeRequestStatus::Prefill,
595                SpeculativeRequestStatus::Cancelled
596            ) | (
597                SpeculativeRequestStatus::ReadyToDraft,
598                SpeculativeRequestStatus::ReadyToSubmitVerification
599            ) | (
600                SpeculativeRequestStatus::ReadyToDraft,
601                SpeculativeRequestStatus::Completed
602            ) | (
603                SpeculativeRequestStatus::ReadyToDraft,
604                SpeculativeRequestStatus::Cancelled
605            ) | (
606                SpeculativeRequestStatus::ReadyToSubmitVerification,
607                SpeculativeRequestStatus::TargetVerificationInFlight
608            ) | (
609                SpeculativeRequestStatus::ReadyToSubmitVerification,
610                SpeculativeRequestStatus::Cancelled
611            ) | (
612                SpeculativeRequestStatus::TargetVerificationInFlight,
613                SpeculativeRequestStatus::OptimisticDraftRunning
614            ) | (
615                SpeculativeRequestStatus::TargetVerificationInFlight,
616                SpeculativeRequestStatus::VerificationResolution
617            ) | (
618                SpeculativeRequestStatus::OptimisticDraftRunning,
619                SpeculativeRequestStatus::OptimisticDraftReady
620            ) | (
621                SpeculativeRequestStatus::OptimisticDraftReady,
622                SpeculativeRequestStatus::VerificationResolution
623            ) | (
624                SpeculativeRequestStatus::VerificationResolution,
625                SpeculativeRequestStatus::ReadyToDraft
626            ) | (
627                SpeculativeRequestStatus::VerificationResolution,
628                SpeculativeRequestStatus::Completed
629            ) | (
630                SpeculativeRequestStatus::VerificationResolution,
631                SpeculativeRequestStatus::Cancelled
632            )
633        );
634        if !allowed {
635            return Err(GenerationError::InvalidSpeculativeStatusTransition {
636                from: self.status,
637                to: next,
638            });
639        }
640        self.status = next;
641        if self.is_terminal() {
642            self.cancellation_pending = false;
643        }
644        Ok(())
645    }
646}
647
648/// Sampling values declared by a checkpoint generation configuration.
649#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
650pub struct CheckpointGenerationConfig {
651    /// Whether stochastic sampling is recommended.
652    #[serde(default)]
653    pub do_sample: Option<bool>,
654    /// Recommended temperature.
655    #[serde(default)]
656    pub temperature: Option<f32>,
657    /// Recommended top-k cutoff.
658    #[serde(default)]
659    pub top_k: Option<i32>,
660    /// Recommended nucleus probability.
661    #[serde(default)]
662    pub top_p: Option<f32>,
663    /// Recommended minimum-probability filter.
664    #[serde(default)]
665    pub min_p: Option<f32>,
666    /// Recommended multiplicative repetition penalty.
667    #[serde(default)]
668    pub repetition_penalty: Option<f32>,
669    /// Recommended generated-history window for repetition penalties.
670    #[serde(default)]
671    pub repeat_last_n: Option<i32>,
672    /// Recommended additive frequency penalty.
673    #[serde(default)]
674    pub frequency_penalty: Option<f32>,
675    /// Recommended additive presence penalty.
676    #[serde(default)]
677    pub presence_penalty: Option<f32>,
678    /// Recommended maximum number of new tokens.
679    #[serde(default)]
680    pub max_new_tokens: Option<usize>,
681}
682
683/// Per-request overrides layered over checkpoint generation settings.
684#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
685pub struct GenerationConfigOverrides {
686    /// Overrides stochastic-versus-greedy selection.
687    pub do_sample: Option<bool>,
688    /// Overrides temperature.
689    pub temperature: Option<f32>,
690    /// Overrides top-k filtering.
691    pub top_k: Option<i32>,
692    /// Overrides top-p filtering.
693    pub top_p: Option<f32>,
694    /// Overrides min-p filtering.
695    pub min_p: Option<f32>,
696    /// Overrides the multiplicative repetition penalty.
697    pub repetition_penalty: Option<f32>,
698    /// Overrides the generated-history window used by repetition penalties.
699    pub repeat_last_n: Option<i32>,
700    /// Overrides the additive frequency penalty.
701    pub frequency_penalty: Option<f32>,
702    /// Overrides the additive presence penalty.
703    pub presence_penalty: Option<f32>,
704    /// Overrides the output-token budget.
705    pub max_new_tokens: Option<usize>,
706}
707
708/// Fully resolved and validated generation settings.
709#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
710pub struct ResolvedGenerationConfig {
711    /// Whether sampling is stochastic.
712    pub do_sample: bool,
713    /// Effective temperature.
714    pub temperature: f32,
715    /// Effective top-k cutoff.
716    pub top_k: i32,
717    /// Effective top-p probability.
718    pub top_p: f32,
719    /// Effective min-p probability.
720    pub min_p: f32,
721    /// Effective multiplicative repetition penalty.
722    pub repetition_penalty: f32,
723    /// Effective generated-history window for repetition penalties.
724    pub repeat_last_n: i32,
725    /// Effective additive frequency penalty.
726    pub frequency_penalty: f32,
727    /// Effective additive presence penalty.
728    pub presence_penalty: f32,
729    /// Effective token budget, when declared.
730    pub max_new_tokens: Option<usize>,
731}
732
733/// Resolves checkpoint and request sampling settings without a tensor runtime.
734pub fn resolve_generation_config(
735    checkpoint: Option<&CheckpointGenerationConfig>,
736    overrides: GenerationConfigOverrides,
737) -> Result<ResolvedGenerationConfig, GenerationError> {
738    let checkpoint_present = checkpoint.is_some();
739    let checkpoint = checkpoint.cloned().unwrap_or_default();
740    let (do_sample, temperature) = if let Some(do_sample) = overrides.do_sample {
741        if do_sample {
742            (
743                true,
744                overrides
745                    .temperature
746                    .or(checkpoint.temperature)
747                    .unwrap_or(1.0),
748            )
749        } else {
750            (false, 0.0)
751        }
752    } else if let Some(temperature) = overrides.temperature {
753        (temperature > 0.0, temperature)
754    } else if checkpoint.do_sample.unwrap_or(false) {
755        (true, checkpoint.temperature.unwrap_or(1.0))
756    } else {
757        (false, 0.0)
758    };
759    let resolved = ResolvedGenerationConfig {
760        do_sample,
761        temperature,
762        top_k: overrides
763            .top_k
764            .or(checkpoint.top_k)
765            .unwrap_or(if checkpoint_present { 50 } else { 40 }),
766        top_p: overrides
767            .top_p
768            .or(checkpoint.top_p)
769            .unwrap_or(if checkpoint_present { 1.0 } else { 0.95 }),
770        min_p: overrides
771            .min_p
772            .or(checkpoint.min_p)
773            .unwrap_or(if checkpoint_present { 0.0 } else { 0.05 }),
774        repetition_penalty: overrides
775            .repetition_penalty
776            .or(checkpoint.repetition_penalty)
777            .unwrap_or(1.0),
778        repeat_last_n: overrides
779            .repeat_last_n
780            .or(checkpoint.repeat_last_n)
781            .unwrap_or(64),
782        frequency_penalty: overrides
783            .frequency_penalty
784            .or(checkpoint.frequency_penalty)
785            .unwrap_or(0.0),
786        presence_penalty: overrides
787            .presence_penalty
788            .or(checkpoint.presence_penalty)
789            .unwrap_or(0.0),
790        max_new_tokens: overrides.max_new_tokens.or(checkpoint.max_new_tokens),
791    };
792    if !resolved.temperature.is_finite() || resolved.temperature < 0.0 {
793        return Err(GenerationError::InvalidTemperature(resolved.temperature));
794    }
795    if resolved.do_sample && resolved.temperature == 0.0 {
796        return Err(GenerationError::StochasticZeroTemperature);
797    }
798    if resolved.top_k < 0 {
799        return Err(GenerationError::InvalidTopK(resolved.top_k));
800    }
801    if !resolved.top_p.is_finite() || !(0.0..=1.0).contains(&resolved.top_p) {
802        return Err(GenerationError::InvalidTopP(resolved.top_p));
803    }
804    if !resolved.min_p.is_finite() || !(0.0..=1.0).contains(&resolved.min_p) {
805        return Err(GenerationError::InvalidMinP(resolved.min_p));
806    }
807    if !resolved.repetition_penalty.is_finite() || resolved.repetition_penalty <= 0.0 {
808        return Err(GenerationError::InvalidRepetitionPenalty(
809            resolved.repetition_penalty,
810        ));
811    }
812    if !resolved.frequency_penalty.is_finite() {
813        return Err(GenerationError::InvalidFrequencyPenalty(
814            resolved.frequency_penalty,
815        ));
816    }
817    if !resolved.presence_penalty.is_finite() {
818        return Err(GenerationError::InvalidPresencePenalty(
819            resolved.presence_penalty,
820        ));
821    }
822    if resolved.max_new_tokens == Some(0) {
823        return Err(GenerationError::ZeroTokenBudget);
824    }
825    Ok(resolved)
826}
827
828/// Semantic event emitted by generation orchestration.
829#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
830pub enum SemanticEvent {
831    /// Incremental reasoning content.
832    ReasoningDelta(String),
833    /// Incremental user-visible text.
834    TextDelta(String),
835    /// A structured tool call began.
836    ToolCallStart {
837        /// Zero-based tool-call position in the assistant turn.
838        index: usize,
839        /// Stable tool-call identifier.
840        id: String,
841        /// Tool name selected by the model.
842        name: String,
843    },
844    /// Incremental structured tool arguments.
845    ToolArgumentsDelta {
846        /// Zero-based tool-call position in the assistant turn.
847        index: usize,
848        /// A fragment of the tool call's JSON arguments.
849        json_fragment: String,
850    },
851    /// A structured tool call ended.
852    ToolCallEnd,
853    /// Generation terminated.
854    Finished {
855        /// The condition that ended the stream.
856        reason: FinishReason,
857    },
858}
859
860/// Invalid backend-neutral generation configuration or state transition.
861#[derive(Debug, Clone, PartialEq, thiserror::Error)]
862pub enum GenerationError {
863    /// A token was committed after termination.
864    #[error("generation has already finished")]
865    AlreadyFinished,
866    /// A proposal transaction cannot be empty.
867    #[error("speculative verification requires at least one proposal")]
868    EmptyProposalBlock,
869    /// Proposal acceptance/rejection order was invalid.
870    #[error("invalid speculative verification state transition")]
871    InvalidSpeculativeTransition,
872    /// No terminal outcome or replacement/bonus tail resolved the round.
873    #[error("speculative verification round is not resolved")]
874    IncompleteSpeculativeRound,
875    /// Optimistic state was forked from a different prefix.
876    #[error("optimistic proposal prefix diverged from the canonical committed prefix")]
877    OptimisticPrefixDiverged,
878    /// An optimistic branch contained no proposals.
879    #[error("optimistic proposal branch is empty")]
880    EmptyOptimisticBranch,
881    /// A second optimistic branch was installed for one target transaction.
882    #[error("speculative verification already retains an optimistic branch")]
883    OptimisticBranchAlreadyPresent,
884    /// A speculative request identifier does not belong to this table.
885    #[error("unknown speculative request id {index}")]
886    UnknownSpeculativeRequest {
887        /// Requested stable insertion index.
888        index: usize,
889    },
890    /// A promoted proposal block no longer fits the canonical request budget.
891    #[error(
892        "promoted speculative block has {proposed} proposals but canonical capacity is {capacity}"
893    )]
894    ProposalCapacityExceeded {
895        /// Proposals retained by the promoted block.
896        proposed: usize,
897        /// Current canonical proposal capacity.
898        capacity: usize,
899    },
900    /// A speculative request table was consumed before reaching terminal state.
901    #[error("cannot finish a speculative scheduler with active requests")]
902    ActiveSpeculativeRequests,
903    /// A terminal speculative request did not retain its canonical finish reason.
904    #[error("completed speculative request {index} has no finish reason")]
905    MissingSpeculativeFinishReason {
906        /// Stable request insertion index.
907        index: usize,
908    },
909    /// No assistant proposal may be generated per round.
910    #[error("speculative max_draft_tokens must be positive")]
911    ZeroDraftTokens,
912    /// The selected backend cannot submit an assistant proposal.
913    #[error("speculative backend does not permit any draft tokens")]
914    NoBackendDraftCapacity,
915    /// Temperature is NaN, infinite, or negative.
916    #[error("temperature must be finite and non-negative, got {0}")]
917    InvalidTemperature(f32),
918    /// Stochastic sampling cannot use zero temperature.
919    #[error("do_sample=true requires a temperature greater than zero")]
920    StochasticZeroTemperature,
921    /// Mirostat V2 target surprise is non-finite or non-positive.
922    #[error("Mirostat V2 tau must be finite and positive, got {0}")]
923    InvalidMirostatTau(f32),
924    /// Mirostat V2 adaptation rate is non-finite or non-positive.
925    #[error("Mirostat V2 eta must be finite and positive, got {0}")]
926    InvalidMirostatEta(f32),
927    /// Top-k is negative.
928    #[error("top_k must be non-negative, got {0}")]
929    InvalidTopK(i32),
930    /// Top-p lies outside zero through one.
931    #[error("top_p must be between zero and one, got {0}")]
932    InvalidTopP(f32),
933    /// Min-p lies outside zero through one.
934    #[error("min_p must be between zero and one, got {0}")]
935    InvalidMinP(f32),
936    /// Repetition penalty is non-finite or non-positive.
937    #[error("repetition_penalty must be finite and positive, got {0}")]
938    InvalidRepetitionPenalty(f32),
939    /// Frequency penalty is non-finite.
940    #[error("frequency_penalty must be finite, got {0}")]
941    InvalidFrequencyPenalty(f32),
942    /// Presence penalty is non-finite.
943    #[error("presence_penalty must be finite, got {0}")]
944    InvalidPresencePenalty(f32),
945    /// An explicit token budget was zero.
946    #[error("max_new_tokens must be positive when supplied")]
947    ZeroTokenBudget,
948    /// Scheduler cannot retain any target transaction.
949    #[error("speculative max_in_flight_verifications must be positive")]
950    ZeroInFlightVerifications,
951    /// Current scheduler supports no more than one lookahead block.
952    #[error("speculative scheduler currently supports at most one lookahead block")]
953    TooManyLookaheadBlocks,
954    /// Lookahead was enabled with no branch capacity.
955    #[error("speculative lookahead requires at least one optimistic branch slot")]
956    LookaheadWithoutBranchCapacity,
957    /// Adaptive lookahead needs a non-zero observation window.
958    #[error("speculative adaptive_lookahead_min_blocks must be positive")]
959    ZeroAdaptiveLookaheadWindow,
960    /// Exact verification must always have a positive completion deadline.
961    #[error("speculative completion timeout must be positive")]
962    ZeroSpeculativeCompletionTimeout,
963    /// The host duration cannot be represented by the portable millisecond field.
964    #[error("speculative completion timeout is too large")]
965    SpeculativeCompletionTimeoutTooLarge,
966    /// Active requests expose no legal scheduler action.
967    #[error("speculative scheduler reached a non-terminal state with no eligible operation")]
968    StalledSpeculativeSchedule,
969    /// The requested speculative lifecycle edge is invalid.
970    #[error("invalid speculative request status transition from {from:?} to {to:?}")]
971    InvalidSpeculativeStatusTransition {
972        /// Current status.
973        from: SpeculativeRequestStatus,
974        /// Requested status.
975        to: SpeculativeRequestStatus,
976    },
977}
978
979#[cfg(test)]
980mod tests {
981    use super::*;
982
983    #[test]
984    fn terminal_precedence_and_cancellation_are_canonical() {
985        let mut sequence = GenerationSequence::new(2, [7]);
986        let first = sequence.commit(1, TokenTerminalSignals::default()).unwrap();
987        assert_eq!(first.finish_reason, None);
988        let second = sequence
989            .commit(
990                7,
991                TokenTerminalSignals {
992                    stop_sequence: true,
993                    grammar_complete: true,
994                },
995            )
996            .unwrap();
997        assert_eq!(second.finish_reason, Some(FinishReason::StopSequence));
998        assert!(!sequence.cancel());
999
1000        let token = GenerationCancellationToken::new();
1001        let mut active = GenerationSequence::new(3, []);
1002        token.cancel();
1003        assert!(active.observe_cancellation(&token));
1004        assert_eq!(active.finish_reason(), Some(FinishReason::Cancelled));
1005    }
1006
1007    #[test]
1008    fn speculative_commit_plan_preserves_trailing_token_cache_semantics() {
1009        let mut rejected = SpeculativeRound::new(3).unwrap();
1010        rejected.accept(10, false).unwrap();
1011        rejected.reject_with(99, false).unwrap();
1012        let plan = rejected.commit_plan().unwrap();
1013        assert_eq!(plan.accepted_proposals, 1);
1014        assert_eq!(plan.committed_tokens, &[10, 99]);
1015        assert_eq!(plan.verified_inputs, 2);
1016        assert_eq!(plan.tail, Some(SpeculativeTail::Replacement));
1017
1018        let mut terminal_accept = SpeculativeRound::new(2).unwrap();
1019        terminal_accept.accept(10, false).unwrap();
1020        terminal_accept.accept(11, true).unwrap();
1021        let plan = terminal_accept.commit_plan().unwrap();
1022        assert!(plan.full_acceptance);
1023        assert_eq!(plan.verified_inputs, 2);
1024        assert_eq!(plan.tail, None);
1025
1026        let mut bonus = SpeculativeRound::new(2).unwrap();
1027        bonus.accept(10, false).unwrap();
1028        bonus.accept(11, false).unwrap();
1029        bonus.bonus(12, false).unwrap();
1030        assert_eq!(bonus.commit_plan().unwrap().verified_inputs, 3);
1031
1032        let mut incomplete = SpeculativeRound::new(2).unwrap();
1033        incomplete.accept(10, false).unwrap();
1034        assert!(matches!(
1035            incomplete.commit_plan(),
1036            Err(GenerationError::IncompleteSpeculativeRound)
1037        ));
1038    }
1039
1040    #[test]
1041    fn optimistic_reuse_is_pure_and_fail_closed() {
1042        assert_eq!(
1043            resolve_optimistic_reuse(&[1], &[1], &[2, 3], 2, false).unwrap(),
1044            OptimisticReuseDecision::MatchedRetained
1045        );
1046        assert_eq!(
1047            resolve_optimistic_reuse(&[1], &[1], &[2], 2, false).unwrap(),
1048            OptimisticReuseDecision::MatchedConsumed
1049        );
1050        assert_eq!(
1051            resolve_optimistic_reuse(&[1], &[1], &[2], 9, false).unwrap(),
1052            OptimisticReuseDecision::DiscardMismatch
1053        );
1054        assert!(matches!(
1055            resolve_optimistic_reuse(&[1], &[9], &[2], 2, false),
1056            Err(GenerationError::OptimisticPrefixDiverged)
1057        ));
1058    }
1059
1060    #[test]
1061    fn sampler_and_scheduler_configuration_validate_without_a_backend() {
1062        let checkpoint = CheckpointGenerationConfig {
1063            do_sample: Some(true),
1064            temperature: Some(0.8),
1065            top_k: Some(64),
1066            repetition_penalty: Some(1.1),
1067            ..CheckpointGenerationConfig::default()
1068        };
1069        let resolved =
1070            resolve_generation_config(Some(&checkpoint), GenerationConfigOverrides::default())
1071                .unwrap();
1072        assert!(resolved.do_sample);
1073        assert_eq!(resolved.top_k, 64);
1074        assert_eq!(resolved.repetition_penalty, 1.1);
1075        assert_eq!(resolved.repeat_last_n, 64);
1076        assert!(matches!(
1077            resolve_generation_config(
1078                None,
1079                GenerationConfigOverrides {
1080                    frequency_penalty: Some(f32::NAN),
1081                    ..GenerationConfigOverrides::default()
1082                }
1083            ),
1084            Err(GenerationError::InvalidFrequencyPenalty(value)) if value.is_nan()
1085        ));
1086        assert!(SpeculativeConfig::default().validate().is_ok());
1087        assert!(SpeculativeSchedulerOptions::default().validate().is_ok());
1088        assert!(matches!(
1089            SpeculativeSchedulerOptions {
1090                max_in_flight_verifications: 0,
1091                ..SpeculativeSchedulerOptions::default()
1092            }
1093            .validate(),
1094            Err(GenerationError::ZeroInFlightVerifications)
1095        ));
1096
1097        let config_json = serde_json::to_string(&resolved).unwrap();
1098        assert_eq!(
1099            serde_json::from_str::<ResolvedGenerationConfig>(&config_json).unwrap(),
1100            resolved
1101        );
1102        let options = SpeculativeSchedulerOptions::default();
1103        let options_json = serde_json::to_string(&options).unwrap();
1104        assert_eq!(
1105            serde_json::from_str::<SpeculativeSchedulerOptions>(&options_json).unwrap(),
1106            options
1107        );
1108    }
1109
1110    #[test]
1111    fn semantic_events_round_trip_without_a_backend() {
1112        let event = SemanticEvent::ToolCallStart {
1113            index: 2,
1114            id: "call_2".into(),
1115            name: "lookup".into(),
1116        };
1117        let json = serde_json::to_string(&event).unwrap();
1118        assert_eq!(serde_json::from_str::<SemanticEvent>(&json).unwrap(), event);
1119
1120        let mut zero_budget = GenerationSequence::new(0, []);
1121        assert_eq!(zero_budget.finish_reason(), Some(FinishReason::MaxTokens));
1122        assert!(zero_budget.cancel());
1123        assert_eq!(zero_budget.finish_reason(), Some(FinishReason::Cancelled));
1124    }
1125
1126    #[test]
1127    fn speculative_request_lifecycle_defers_cancellation_exactly() {
1128        let mut lifecycle = SpeculativeRequestLifecycle::new();
1129        lifecycle
1130            .transition(SpeculativeRequestStatus::ReadyToDraft)
1131            .unwrap();
1132        lifecycle
1133            .transition(SpeculativeRequestStatus::ReadyToSubmitVerification)
1134            .unwrap();
1135        lifecycle
1136            .transition(SpeculativeRequestStatus::TargetVerificationInFlight)
1137            .unwrap();
1138        assert_eq!(
1139            lifecycle.request_cancellation(true).unwrap(),
1140            SpeculativeCancellationDisposition::Deferred
1141        );
1142        assert!(lifecycle.cancellation_pending());
1143        lifecycle
1144            .transition(SpeculativeRequestStatus::VerificationResolution)
1145            .unwrap();
1146        lifecycle
1147            .transition(SpeculativeRequestStatus::Cancelled)
1148            .unwrap();
1149        assert!(lifecycle.is_terminal());
1150        assert!(!lifecycle.cancellation_pending());
1151        assert!(matches!(
1152            lifecycle.transition(SpeculativeRequestStatus::ReadyToDraft),
1153            Err(GenerationError::InvalidSpeculativeStatusTransition { .. })
1154        ));
1155    }
1156}