返回 CodeWhale
route_budget.rs
根目录 / crates / tui / src / route_budget.rs
1 use codewhale_config::route::RouteLimits;
2
3 use crate::config::{ApiProvider, provider_capability};
4 use crate::context_budget::ContextBudget;
5 use codewhale_models::{DEFAULT_COMPACTION_TOKEN_THRESHOLD, context_window_for_model};
6
7 /// Safe ordinary API request cap across provider routes.
8 const API_MAX_OUTPUT_TOKENS: u32 = 65_536;
9
10 /// Preserve only route limits that came from a concrete offering.
11 #[must_use]
12 pub(crate) fn known_route_limits(limits: RouteLimits) -> Option<RouteLimits> {
13 limits.has_known_limit().then_some(limits)
14 }
15
16 /// Whether this exact transport can represent an explicit output allowance.
17 /// Codex OAuth Responses rejects the field; other supported dialects carry it.
18 pub(crate) fn route_supports_output_token_limit(
19 provider: ApiProvider,
20 protocol: codewhale_config::route::RequestProtocol,
21 ) -> bool {
22 !(provider == ApiProvider::OpenaiCodex
23 && protocol == codewhale_config::route::RequestProtocol::Responses)
24 }
25
26 pub(crate) fn effective_max_output_tokens_for_turn(
27 provider: ApiProvider,
28 model: &str,
29 route_limits: Option<RouteLimits>,
30 allowance: Option<std::num::NonZeroU32>,
31 ) -> u32 {
32 let ceiling = effective_max_output_tokens_for_route(provider, model, route_limits);
33 allowance.map_or(ceiling, |allowance| ceiling.min(allowance.get()))
34 }
35
36 /// Context window for a resolved runtime route.
37 ///
38 /// Route/offering facts win when known; otherwise this falls back to the
39 /// existing provider+model capability matrix so startup and custom/local
40 /// routes keep their previous conservative behavior.
41 #[must_use]
42 pub(crate) fn route_context_window_tokens(
43 provider: ApiProvider,
44 model: &str,
45 route_limits: Option<RouteLimits>,
46 ) -> u32 {
47 route_limits
48 .and_then(|limits| limits.context_tokens)
49 .and_then(|tokens| u32::try_from(tokens).ok())
50 .filter(|tokens| *tokens > 0)
51 .unwrap_or_else(|| provider_capability(provider, model).context_window)
52 }
53
54 /// Provider/offering output cap, when the resolved route reports one.
55 #[must_use]
56 pub(crate) fn route_output_limit_tokens(route_limits: Option<RouteLimits>) -> Option<u32> {
57 route_limits
58 .and_then(|limits| limits.output_tokens)
59 .and_then(|tokens| u32::try_from(tokens).ok())
60 .filter(|tokens| *tokens > 0)
61 }
62
63 /// Provider/offering input cap, when the resolved route reports one.
64 #[must_use]
65 pub(crate) fn route_input_limit_tokens(route_limits: Option<RouteLimits>) -> Option<u32> {
66 route_limits
67 .and_then(|limits| limits.input_tokens)
68 .and_then(|tokens| u32::try_from(tokens).ok())
69 .filter(|tokens| *tokens > 0)
70 }
71
72 /// Explicit operator request cap, when configured.
73 ///
74 /// Keep this separate from catalogue/default resolution: a published maximum
75 /// is a ceiling, while these environment variables are an actual request from
76 /// the operator. Route/window validation still clamps the value before it is
77 /// sent.
78 #[must_use]
79 fn explicit_max_output_tokens_override() -> Option<u32> {
80 match std::env::var("CODEWHALE_MAX_OUTPUT_TOKENS") {
81 Ok(raw) if !raw.trim().is_empty() => {
82 // A non-blank canonical value is authoritative. Invalid/zero
83 // values deliberately fall back to the safe automatic default;
84 // they must not silently activate a stale legacy setting.
85 return raw.trim().parse::<u32>().ok().filter(|tokens| *tokens > 0);
86 }
87 Ok(_) | Err(_) => {}
88 }
89 std::env::var("DEEPSEEK_MAX_OUTPUT_TOKENS")
90 .ok()
91 .and_then(|raw| raw.trim().parse::<u32>().ok())
92 .filter(|tokens| *tokens > 0)
93 }
94
95 /// Effective `max_tokens` for a model before provider/route caps are applied.
96 #[must_use]
97 pub(crate) fn effective_max_output_tokens(model: &str) -> u32 {
98 if let Some(tokens) = explicit_max_output_tokens_override() {
99 return tokens;
100 }
101
102 // A documented catalogue value is a capability ceiling, not necessarily a
103 // sensible default request size. In particular, DeepSeek V4 advertises a
104 // 384K maximum. Treating that maximum as the default made Codewhale ask a
105 // 262K/327K self-hosted route for almost its whole context as output before
106 // it had counted a single input token (#5516/#5518). Keep documented
107 // ceilings through the normal compatibility intersection, but automatic
108 // requests start at the ordinary 64K cap unless the operator explicitly
109 // overrides it. A maximum describes what a provider may allow, not what
110 // every response should reserve by default.
111 //
112 // Provenance for the ceiling (deepseek-v4-flash/pro: 384_000 output):
113 // - models_dev.bundled.json documents limit.output = 384000.
114 // - The DS4 provider contract corroborates 384K
115 // (crates/config/src/model_reference.rs pins max_output 384_000 / "384K").
116 // - Official DeepSeek API docs confirm the model ids (deepseek-v4-flash ->
117 // V4-Flash-0731, deepseek-v4-pro -> V4-Pro-0813) but do not publish the
118 // output ceiling in a machine-readable form; that number remains a
119 // catalogue-sourced value to re-verify against official docs when they
120 // publish one (#5373).
121 if let Some(documented) = codewhale_models::max_output_tokens_for_model(model) {
122 return documented.min(API_MAX_OUTPUT_TOKENS);
123 }
124
125 let window = context_window_for_model(model).unwrap_or(128_000);
126 (window / 2).min(API_MAX_OUTPUT_TOKENS)
127 }
128
129 /// Automatic allowance when an exact remote model has no output metadata.
130 /// This is policy, not a discovered provider limit. Reasoning and tool arguments
131 /// share this allowance; an 8K fallback truncated ordinary file writes after
132 /// reasoning consumed most of the response. Known route limits still constrain
133 /// requests, and an explicit operator setting may replace this fallback.
134 const UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS: u32 = API_MAX_OUTPUT_TOKENS;
135
136 /// Assumed output ceiling for an Anthropic-family model the catalogue does
137 /// not describe (#5440). The 64K Messages floor is real, but applying it to
138 /// an unknown model is an assumption about that model, not a documented
139 /// fact, so it clamps under an `unverified` label.
140 const ANTHROPIC_UNKNOWN_MAX_OUTPUT_TOKENS: u32 = 64_000;
141
142 /// Assumed output ceiling for the ChatGPT/Codex OAuth route, which publishes
143 /// no output ceiling of its own (#5440). Same clamp as the long-standing 4K
144 /// policy — relabeled, not revalued.
145 const CODEX_OAUTH_MAX_OUTPUT_TOKENS: u32 = 4_096;
146
147 /// Why a route's compatibility output ceiling has the value it does.
148 ///
149 /// Carried so a clamp is always attributable: "unknown" is only allowed to
150 /// mean "no clamp" when a route *truthfully publishes no ceiling*, never when
151 /// the catalogue simply has no row for the model.
152 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
153 pub(crate) enum OutputCeilingSource {
154 /// The static catalogue publishes an exact/conservative ceiling.
155 Documented(u32),
156 /// The route is known to publish no output maximum we can stand behind
157 /// (Kimi Code membership ids, operator-owned self-hosted engines). Unknown
158 /// stays unknown and nothing is clamped.
159 RouteDeclaredUnknown,
160 /// The catalogue has no row for this model. Fail closed to a conservative
161 /// ceiling rather than treating absence as permission.
162 Uncatalogued(u32),
163 /// The route publishes no ceiling we can stand behind, but a defensible
164 /// floor is still applied: an Anthropic-family model the catalogue does
165 /// not describe (64K Messages floor) and the Codex OAuth route (4K
166 /// policy). Clamping trades late provider failure for early truncation;
167 /// lying about why is not part of that trade, so receipts and pickers
168 /// must render this as an assumption, never as "documented" (#5440).
169 Unverified(u32),
170 }
171
172 impl OutputCeilingSource {
173 /// The ceiling to intersect a requested cap with, if any.
174 #[must_use]
175 pub(crate) const fn clamp_tokens(self) -> Option<u32> {
176 match self {
177 Self::Documented(tokens) | Self::Uncatalogued(tokens) | Self::Unverified(tokens) => {
178 Some(tokens)
179 }
180 Self::RouteDeclaredUnknown => None,
181 }
182 }
183
184 /// Stable provenance label, surfaced in exec stream metadata so a wrong
185 /// ceiling is visible in a receipt rather than requiring packet capture.
186 #[must_use]
187 pub(crate) const fn as_str(self) -> &'static str {
188 match self {
189 Self::Documented(_) => "documented",
190 Self::Uncatalogued(_) => "uncatalogued",
191 Self::RouteDeclaredUnknown => "route-declared",
192 Self::Unverified(_) => "unverified",
193 }
194 }
195 }
196
197 /// Whether an absent compatibility ceiling is a *declared* unknown for this
198 /// route, rather than a gap in the catalogue.
199 ///
200 /// Deliberately an allowlist. Everything not named here is uncatalogued and
201 /// gets the conservative ceiling.
202 #[must_use]
203 fn route_declares_unknown_output_ceiling(provider: ApiProvider, model: &str) -> bool {
204 match provider {
205 // Operator-owned engines: the local server, not this process, owns the
206 // output ceiling, and it is routinely far above any catalogue row.
207 ApiProvider::Ollama | ApiProvider::Sglang | ApiProvider::Vllm => true,
208 // Kimi Code membership ids publish their limits in the membership
209 // catalog rather than the static model catalogue.
210 ApiProvider::Moonshot => crate::config::is_kimi_code_membership_model(model),
211 _ => false,
212 }
213 }
214
215 /// Resolve the compatibility output ceiling for a route, with its provenance.
216 #[must_use]
217 pub(crate) fn output_ceiling_source(provider: ApiProvider, model: &str) -> OutputCeilingSource {
218 // #5440: two routes clamp to a number the route itself never documented.
219 // The clamps stay (see `OutputCeilingSource::Unverified`); the labels must
220 // not borrow the documented rung's authority.
221 if provider == ApiProvider::OpenaiCodex {
222 return OutputCeilingSource::Unverified(CODEX_OAUTH_MAX_OUTPUT_TOKENS);
223 }
224 if matches!(
225 provider,
226 ApiProvider::Anthropic | ApiProvider::MinimaxAnthropic | ApiProvider::Openmodel
227 ) && codewhale_models::max_output_tokens_for_model(model).is_none()
228 {
229 return OutputCeilingSource::Unverified(ANTHROPIC_UNKNOWN_MAX_OUTPUT_TOKENS);
230 }
231 provider_capability(provider, model).max_output.map_or_else(
232 || {
233 if route_declares_unknown_output_ceiling(provider, model) {
234 OutputCeilingSource::RouteDeclaredUnknown
235 } else {
236 OutputCeilingSource::Uncatalogued(UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS)
237 }
238 },
239 OutputCeilingSource::Documented,
240 )
241 }
242
243 /// Effective request output cap for a fully resolved provider/model route.
244 #[must_use]
245 pub(crate) fn effective_max_output_tokens_for_route(
246 provider: ApiProvider,
247 model: &str,
248 route_limits: Option<RouteLimits>,
249 ) -> u32 {
250 let window = route_context_window_tokens(provider, model, route_limits);
251 let compatibility_source = output_ceiling_source(provider, model);
252 let compatibility_cap = compatibility_source.clamp_tokens();
253 let route_cap = route_output_limit_tokens(route_limits);
254 // With a known route window and no published output limit, reserve a
255 // conservative part of that window. The model-only fallback reserved 64K even
256 // for a configured 32K Ollama route, leaving just 1K for input (#5820).
257 // Explicit requests and documented ceilings retain their existing rules.
258 let requested_cap = if explicit_max_output_tokens_override().is_none()
259 && codewhale_models::max_output_tokens_for_model(model).is_none()
260 && route_cap.is_none()
261 && matches!(
262 compatibility_source,
263 OutputCeilingSource::RouteDeclaredUnknown | OutputCeilingSource::Uncatalogued(_)
264 )
265 && route_limits
266 .and_then(|limits| limits.context_tokens)
267 .is_some_and(|tokens| (1..=u64::from(u32::MAX)).contains(&tokens))
268 {
269 (window / 4).clamp(1, UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS)
270 } else {
271 effective_max_output_tokens(model)
272 };
273 // Unknown means unknown only where a route *declares* it: membership ids
274 // such as the `kimi-for-coding` family, and operator-owned self-hosted
275 // engines. For those there is nothing to clamp against and the requested
276 // cap stands. A model the catalogue simply has no row for is not the same
277 // fact — absence keeps a labeled automatic allowance
278 // (see `output_ceiling_source`). A concrete route/offering maximum is the
279 // missing evidence for that exact route and may replace only the generic
280 // uncatalogued guess; known compatibility caps stay authoritative and are
281 // still intersected with any route maximum.
282 let cap = match (compatibility_source, route_cap) {
283 // A concrete route/offering maximum is evidence about this exact
284 // route. It therefore outranks the generic fallback that exists only
285 // because the static catalogue has no row for the wire id. With no
286 // route fact the conservative guess still applies, and the route fact
287 // can never raise the caller's requested cap.
288 (OutputCeilingSource::Uncatalogued(_), Some(route_cap)) => requested_cap.min(route_cap),
289 (OutputCeilingSource::Uncatalogued(_), None)
290 if explicit_max_output_tokens_override().is_some() =>
291 {
292 requested_cap
293 }
294 _ => {
295 let cap = compatibility_cap.map_or(requested_cap, |compat| requested_cap.min(compat));
296 route_cap.map_or(cap, |route_cap| cap.min(route_cap))
297 }
298 };
299 // Clamp against the effective route window even when it came from the
300 // capability fallback rather than an explicit offering. This keeps a
301 // suffix/config/catalog-derived small window from ever receiving a request
302 // cap larger than the window itself.
303 u32::try_from(ContextBudget::new(u64::from(window), 0, u64::from(cap)).output_cap_tokens)
304 .unwrap_or(cap)
305 .max(1)
306 }
307
308 /// Share of one output allowance a single review pass must keep for visible
309 /// text, as a percentage.
310 ///
311 /// A reasoning route shares one `max_tokens` allowance between hidden
312 /// reasoning and visible text, and Codewhale has no wire-level separation
313 /// (`thinking.budget_tokens` is not plumbed), so "reserving" means two things
314 /// together: state the reserve, and cap the reasoning level that may consume
315 /// it (`review::bounded_review_reasoning_effort`).
316 ///
317 /// The share is sized from the model's reasoning behaviour rather than a flat
318 /// constant:
319 ///
320 /// * `Some(false)` — nothing to reserve; the whole allowance is visible text.
321 /// * `Some(true)` in the summarized-reasoning families whose reasoning is
322 /// counted as ordinary output tokens
323 /// ([`codewhale_models::model_is_openai_reasoning_family`]) — half. These are
324 /// the models observed consuming an entire 64K allowance on reasoning and
325 /// returning zero visible text with stop reason `length` (#6285).
326 /// * `Some(true)` otherwise, and `None` (no catalogue row) — a quarter. A
327 /// review pass needs only enough text for its structured findings, and an
328 /// unknown model is not evidence that it does not reason (#6032).
329 #[must_use]
330 pub(crate) fn review_visible_text_reserve_percent(model: &str) -> u32 {
331 review_reserve_percent_for(
332 codewhale_models::model_reasoning_capability(model),
333 codewhale_models::model_is_openai_reasoning_family(model),
334 )
335 }
336
337 /// Pure core of [`review_visible_text_reserve_percent`]: the mapping from a
338 /// model's reasoning classification to the reserved share. Split out so the
339 /// mapping is testable without the process-global model catalog.
340 fn review_reserve_percent_for(capability: Option<bool>, openai_reasoning_family: bool) -> u32 {
341 match capability {
342 // Nothing to reserve; the whole allowance is visible text.
343 Some(false) => 0,
344 Some(true) if openai_reasoning_family => 50,
345 Some(true) | None => 25,
346 }
347 }
348
349 /// Visible-text reserve in tokens for one review pass on this exact model and
350 /// resolved output allowance.
351 #[must_use]
352 pub(crate) fn review_visible_text_reserve_tokens(model: &str, allowance: u32) -> u32 {
353 allowance.saturating_mul(review_visible_text_reserve_percent(model)) / 100
354 }
355
356 /// Output reservation used by the internal input budget for a route.
357 #[must_use]
358 pub(crate) fn route_output_reservation(
359 provider: ApiProvider,
360 model: &str,
361 route_limits: Option<RouteLimits>,
362 ) -> u32 {
363 // Use exactly the value that can reach the wire on every window size.
364 // The previous split reserved 65K for a possible 325K wire request below
365 // 500K, then jumped to an unrequested 262K reservation at 500K. Both
366 // directions made preflight disagree with the actual request. Reasoning
367 // effort is a request control, not separately metered non-wire output, so
368 // it does not justify a second hidden context reservation.
369 effective_max_output_tokens_for_route(provider, model, route_limits)
370 }
371
372 #[must_use]
373 pub(crate) fn route_context_budget(
374 provider: ApiProvider,
375 model: &str,
376 route_limits: Option<RouteLimits>,
377 input_tokens: usize,
378 ) -> Option<ContextBudget> {
379 let window = route_context_window_tokens(provider, model, route_limits);
380 let output_cap = route_output_reservation(provider, model, route_limits);
381 Some(ContextBudget::new_with_input_limit(
382 u64::from(window),
383 u64::try_from(input_tokens).ok()?,
384 u64::from(output_cap),
385 route_input_limit_tokens(route_limits).map(u64::from),
386 ))
387 }
388
389 #[must_use]
390 pub(crate) fn compaction_threshold_for_route_at_percent(
391 provider: ApiProvider,
392 model: &str,
393 route_limits: Option<RouteLimits>,
394 percent: f64,
395 ) -> usize {
396 route_context_budget(provider, model, route_limits, 0)
397 .and_then(|budget| {
398 usize::try_from(budget.compaction_trigger_for_percent(percent.clamp(10.0, 100.0))).ok()
399 })
400 .unwrap_or(DEFAULT_COMPACTION_TOKEN_THRESHOLD)
401 }
402
403 #[must_use]
404 pub(crate) fn auto_compact_default_for_route(
405 provider: ApiProvider,
406 model: &str,
407 route_limits: Option<RouteLimits>,
408 ) -> bool {
409 // Every resolved route has either concrete offering limits or a
410 // conservative provider/model fallback. Large windows need continuity too;
411 // their size is not a reason to disable compaction entirely.
412 route_context_window_tokens(provider, model, route_limits) > 0
413 }
414
415 #[cfg(test)]
416 mod tests {
417 use super::*;
418
419 #[test]
420 fn provider_regression_5820_small_unknown_windows_keep_room_for_input() {
421 let _lock = crate::test_support::lock_test_env();
422 let _canonical = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
423 let _legacy = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
424 let model = "qwen2.5:7b";
425 assert!(codewhale_models::max_output_tokens_for_model(model).is_none());
426 for provider in [
427 ApiProvider::Ollama,
428 ApiProvider::Sglang,
429 ApiProvider::Vllm,
430 ApiProvider::Custom,
431 ] {
432 for (window, expected_output) in [
433 (16_384, 4_096),
434 (32_768, 8_192),
435 (65_536, 16_384),
436 (262_144, 65_536),
437 ] {
438 let limits = Some(RouteLimits {
439 context_tokens: Some(window),
440 ..RouteLimits::default()
441 });
442 let wire_cap = effective_max_output_tokens_for_route(provider, model, limits);
443 let budget = route_context_budget(provider, model, limits, 6_225).unwrap();
444 assert_eq!(wire_cap, expected_output, "{provider:?}, window={window}");
445 assert_eq!(budget.output_cap_tokens, u64::from(wire_cap));
446 assert_eq!(
447 budget.input_budget_ceiling,
448 window - u64::from(wire_cap) - 1_024
449 );
450 assert!(
451 budget.input_tokens < budget.input_budget_ceiling,
452 "{budget:?}"
453 );
454 assert!(!budget.should_compact(), "{budget:?}");
455 }
456 }
457 let _explicit =
458 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "16384");
459 let limits = RouteLimits {
460 context_tokens: Some(32_768),
461 ..RouteLimits::default()
462 };
463 assert_eq!(
464 effective_max_output_tokens_for_route(ApiProvider::Ollama, model, Some(limits)),
465 16_384
466 );
467 assert_eq!(
468 effective_max_output_tokens_for_route(
469 ApiProvider::Ollama,
470 model,
471 Some(RouteLimits {
472 output_tokens: Some(4_096),
473 ..limits
474 })
475 ),
476 4_096
477 );
478 }
479
480 /// Absence of a catalogue row is not evidence of a large ceiling. An
481 /// unrecognized wire alias on a remote OpenAI-compatible route keeps the
482 /// conservative compatibility ceiling, with an attributable source.
483 #[test]
484 fn uncatalogued_remote_model_keeps_a_conservative_ceiling() {
485 let source = output_ceiling_source(ApiProvider::Openai, "totally-unknown-alias-v9");
486 assert_eq!(
487 source,
488 OutputCeilingSource::Uncatalogued(UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS)
489 );
490 assert_eq!(
491 source.clamp_tokens(),
492 Some(UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS)
493 );
494 assert!(
495 effective_max_output_tokens_for_route(
496 ApiProvider::Openai,
497 "totally-unknown-alias-v9",
498 None
499 ) <= UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS
500 );
501 }
502
503 /// #5460: absence is not permission, but a positive output maximum on the
504 /// resolved route is permission for that exact route. The concrete fact
505 /// replaces only the catalogue-absence guess; it never raises the caller's
506 /// requested cap or a documented model ceiling.
507 #[test]
508 fn concrete_route_output_limit_outranks_uncatalogued_guess() {
509 let _lock = crate::test_support::lock_test_env();
510 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
511 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
512 let model = "totally-unknown-alias-v9";
513
514 assert_eq!(effective_max_output_tokens(model), 64_000);
515 for provider in [ApiProvider::Openai, ApiProvider::Custom] {
516 assert_eq!(
517 output_ceiling_source(provider, model),
518 OutputCeilingSource::Uncatalogued(UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS)
519 );
520 assert_eq!(
521 effective_max_output_tokens_for_route(provider, model, None),
522 64_000,
523 "{provider:?}: no route fact must preserve the labeled automatic allowance"
524 );
525 for route_cap in [24_576, 64_000] {
526 assert_eq!(
527 effective_max_output_tokens_for_route(
528 provider,
529 model,
530 Some(RouteLimits {
531 output_tokens: Some(route_cap),
532 ..RouteLimits::default()
533 }),
534 ),
535 u32::try_from(route_cap).unwrap(),
536 "{provider:?}: the exact route fact must replace the catalogue-absence guess"
537 );
538 }
539 assert_eq!(
540 effective_max_output_tokens_for_route(
541 provider,
542 model,
543 Some(RouteLimits {
544 output_tokens: Some(65_536),
545 ..RouteLimits::default()
546 }),
547 ),
548 64_000,
549 "{provider:?}: a route fact must not raise the requested cap"
550 );
551 }
552
553 assert_eq!(
554 effective_max_output_tokens_for_route(
555 ApiProvider::Moonshot,
556 "kimi-k2.7-code",
557 Some(RouteLimits {
558 output_tokens: Some(64_000),
559 ..RouteLimits::default()
560 }),
561 ),
562 32_768,
563 "a route fact must not raise a documented model ceiling"
564 );
565 }
566
567 /// Routes that *declare* an unknown ceiling still avoid the clamp.
568 #[test]
569 fn route_declared_unknown_ceilings_are_not_clamped() {
570 for (provider, model) in [
571 (ApiProvider::Moonshot, "kimi-for-coding"),
572 (ApiProvider::Moonshot, "kimi-for-coding-highspeed"),
573 (ApiProvider::Ollama, "some-local-build"),
574 ] {
575 assert_eq!(
576 output_ceiling_source(provider, model),
577 OutputCeilingSource::RouteDeclaredUnknown,
578 "{provider:?}/{model} must declare its unknown ceiling"
579 );
580 assert_eq!(output_ceiling_source(provider, model).clamp_tokens(), None);
581 }
582 // Bare `k3` is a membership id, but unlike the `kimi-for-coding`
583 // family the K3 quickstart documents its output maximum, and the model
584 // catalogue carries it. A documented ceiling is authoritative — the
585 // membership allowlist only covers ids the catalogue has nothing to
586 // say about, and must not turn a real fact back into an unknown.
587 assert_eq!(
588 output_ceiling_source(ApiProvider::Moonshot, "k3"),
589 OutputCeilingSource::Documented(131_072)
590 );
591 assert_eq!(
592 output_ceiling_source(ApiProvider::OllamaCloud, "some-cloud-build"),
593 OutputCeilingSource::Uncatalogued(UNCATALOGUED_COMPAT_MAX_OUTPUT_TOKENS),
594 "hosted Ollama Cloud must not inherit the local runtime's unbounded output semantics"
595 );
596 }
597
598 /// #5440: an Anthropic-family model the catalogue does not describe keeps
599 /// the 64K Messages floor as its clamp, but the floor is an assumption
600 /// about that model — never a "documented" ceiling.
601 #[test]
602 fn anthropic_unknown_model_ceiling_is_an_unverified_assumed_floor() {
603 let source = output_ceiling_source(ApiProvider::Anthropic, "claude-future-99");
604 assert_eq!(source, OutputCeilingSource::Unverified(64_000));
605 assert_eq!(source.as_str(), "unverified");
606 assert_eq!(source.clamp_tokens(), Some(64_000));
607 // Same honesty on the compatibility dialects of the same family.
608 assert_eq!(
609 output_ceiling_source(ApiProvider::MinimaxAnthropic, "claude-future-99"),
610 OutputCeilingSource::Unverified(64_000)
611 );
612 assert_eq!(
613 output_ceiling_source(ApiProvider::Openmodel, "claude-future-99"),
614 OutputCeilingSource::Unverified(64_000)
615 );
616 }
617
618 /// #5440: a model the catalogue does describe keeps its documented
619 /// ceiling and its documented label — the unverified rung must not
620 /// swallow real facts.
621 #[test]
622 fn anthropic_documented_model_ceiling_stays_documented() {
623 let source = output_ceiling_source(ApiProvider::Anthropic, "claude-sonnet-4-6");
624 assert_eq!(source, OutputCeilingSource::Documented(128_000));
625 assert_eq!(source.as_str(), "documented");
626 assert_eq!(source.clamp_tokens(), Some(128_000));
627 }
628
629 /// #5440: the Codex OAuth route clamps every response to 4K by policy,
630 /// because the OAuth cache publishes no ceiling. The clamp stands; the
631 /// receipt must call the number what it is.
632 #[test]
633 fn codex_oauth_ceiling_clamps_but_never_claims_documented() {
634 let source = output_ceiling_source(ApiProvider::OpenaiCodex, "gpt-5.5");
635 assert_eq!(source, OutputCeilingSource::Unverified(4_096));
636 assert_eq!(source.as_str(), "unverified");
637 assert_eq!(source.clamp_tokens(), Some(4_096));
638 assert_eq!(
639 effective_max_output_tokens_for_route(ApiProvider::OpenaiCodex, "gpt-5.5", None),
640 4_096,
641 "the honesty relabel must not revalue the long-standing clamp"
642 );
643 }
644
645 #[test]
646 fn codex_missing_route_metadata_uses_provider_context_floor() {
647 assert_eq!(
648 route_context_window_tokens(ApiProvider::OpenaiCodex, "gpt-5.5", None),
649 128_000
650 );
651 // 80% of the 128K window (102_400) fits under the input ceiling.
652 assert_eq!(
653 compaction_threshold_for_route_at_percent(
654 ApiProvider::OpenaiCodex,
655 "gpt-5.5",
656 None,
657 80.0,
658 ),
659 102_400
660 );
661 assert!(auto_compact_default_for_route(
662 ApiProvider::OpenaiCodex,
663 "gpt-5.5",
664 None,
665 ));
666 }
667
668 /// The assertion values here depend on `explicit_max_output_tokens_override`
669 /// seeing no ambient env override, and sibling tests in this binary
670 /// (this module, `client`, `vision/tools`, `core/engine`) set
671 /// `CODEWHALE_MAX_OUTPUT_TOKENS`/`DEEPSEEK_MAX_OUTPUT_TOKENS` while holding
672 /// `lock_test_env`. Without the lock and guards this test could read a
673 /// concurrent writer's value mid-assertion (process-global env, parallel
674 /// threads), which is the order-dependent flake this guards against.
675 #[test]
676 fn v4_trigger_uses_window_percent_when_it_fits_spendable_input() {
677 let _lock = crate::test_support::lock_test_env();
678 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
679 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
680
681 let budget = route_context_budget(ApiProvider::Deepseek, "deepseek-v4-pro", None, 0)
682 .expect("V4 route budget");
683
684 assert_eq!(budget.window_tokens, 1_000_000);
685 assert_eq!(budget.output_cap_tokens, u64::from(API_MAX_OUTPUT_TOKENS));
686 assert_eq!(budget.input_budget_ceiling, 933_440);
687 // 80% of the 1M window fits below the spendable input ceiling.
688 assert_eq!(
689 compaction_threshold_for_route_at_percent(
690 ApiProvider::Deepseek,
691 "deepseek-v4-pro",
692 None,
693 80.0,
694 ),
695 800_000
696 );
697 }
698
699 #[test]
700 fn kimi_k3_defaults_auto_compaction_on() {
701 assert!(auto_compact_default_for_route(
702 ApiProvider::Moonshot,
703 "kimi-k3",
704 None,
705 ));
706 }
707
708 #[test]
709 fn kimi_catalog_output_ceiling_preserves_input_budget() {
710 let _lock = crate::test_support::lock_test_env();
711 let _max_output = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
712 // #4368/#4378: Models.dev may report Kimi's full 262K context as both
713 // context and output ceilings. Reserve the route-effective 32K request
714 // cap rather than treating that catalog maximum as the amount every
715 // turn will emit.
716 let limits = RouteLimits {
717 context_tokens: Some(262_144),
718 output_tokens: Some(262_144),
719 ..RouteLimits::default()
720 };
721 let budget = route_context_budget(ApiProvider::Moonshot, "kimi-k2.7-code", Some(limits), 0)
722 .expect("Kimi route budget");
723 let trigger = compaction_threshold_for_route_at_percent(
724 ApiProvider::Moonshot,
725 "kimi-k2.7-code",
726 Some(limits),
727 80.0,
728 );
729
730 assert_eq!(budget.output_cap_tokens, 32_768);
731 assert_eq!(budget.input_budget_ceiling, 228_352);
732 // 80% of the 262_144 window; fits under the 228_352 ceiling because
733 // the output reservation is the route-effective 32K request cap.
734 assert_eq!(trigger, 209_715);
735 assert!(trigger as u64 <= budget.input_budget_ceiling);
736 }
737
738 #[test]
739 fn explicit_route_output_limit_beats_unknown_model_name_fallback() {
740 let _lock = crate::test_support::lock_test_env();
741 let _max_output =
742 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "65536");
743 let limits = RouteLimits {
744 context_tokens: Some(262_144),
745 output_tokens: Some(24_576),
746 ..RouteLimits::default()
747 };
748
749 assert_eq!(
750 effective_max_output_tokens_for_route(
751 ApiProvider::Vllm,
752 "arbitrary-local-wire-alias",
753 Some(limits),
754 ),
755 24_576
756 );
757 assert_eq!(
758 effective_max_output_tokens_for_route(
759 ApiProvider::Vllm,
760 "arbitrary-local-wire-alias",
761 None,
762 ),
763 65_536,
764 "an unknown compatibility cap must not clamp; only the requested cap applies"
765 );
766 assert_eq!(
767 effective_max_output_tokens_for_route(
768 ApiProvider::Vllm,
769 "kimi-k2.7-code",
770 Some(RouteLimits {
771 output_tokens: Some(262_144),
772 ..RouteLimits::default()
773 }),
774 ),
775 32_768,
776 "known model caps must remain authoritative on self-hosted routes"
777 );
778 }
779
780 /// #4368 follow-up: the Kimi Code membership ids deliberately have no
781 /// static output cap (the membership catalog owns their limits). The old
782 /// generic `unwrap_or(4096)` in `provider_capability` turned that unknown
783 /// into a hard 4K clamp here, silently truncating every offline membership
784 /// turn. Unknown must mean "no compatibility clamp".
785 #[test]
786 fn kimi_membership_unknown_output_cap_does_not_clamp_to_4k() {
787 let _lock = crate::test_support::lock_test_env();
788 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
789 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
790
791 for model in ["kimi-for-coding", "kimi-for-coding-highspeed"] {
792 assert_eq!(
793 provider_capability(ApiProvider::Moonshot, model).max_output,
794 None,
795 "{model}: membership output ceiling must stay unknown, not a placeholder"
796 );
797
798 let cap = effective_max_output_tokens_for_route(ApiProvider::Moonshot, model, None);
799 assert_eq!(
800 cap,
801 effective_max_output_tokens(model),
802 "{model}: unknown compatibility cap must leave the requested cap intact"
803 );
804 assert_ne!(cap, 4_096, "{model}: must not inherit the old 4K fallback");
805 // No invented sentinel ceiling either.
806 assert_ne!(cap, u32::MAX);
807 assert_ne!(cap, 32_768);
808 }
809 }
810
811 /// A concrete membership offering limit is still authoritative — "unknown
812 /// means no clamp" must not become "never clamp".
813 #[test]
814 fn kimi_membership_route_limit_still_caps_output() {
815 let _lock = crate::test_support::lock_test_env();
816 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
817 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
818
819 let limits = RouteLimits {
820 context_tokens: Some(262_144),
821 output_tokens: Some(16_384),
822 ..RouteLimits::default()
823 };
824 assert_eq!(
825 effective_max_output_tokens_for_route(
826 ApiProvider::Moonshot,
827 "kimi-for-coding",
828 Some(limits),
829 ),
830 16_384
831 );
832 }
833
834 /// GLM and MiniMax publish real output ceilings; those stay authoritative
835 /// so relaxing the unknown case cannot leak into known routes.
836 #[test]
837 fn known_glm_and_minimax_output_caps_remain_authoritative() {
838 let _lock = crate::test_support::lock_test_env();
839 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
840 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
841
842 // GLM 5.2: 1M window, documented 131K output. The capability remains
843 // known even though the safe automatic request starts at 64K.
844 let glm = provider_capability(ApiProvider::Zai, "glm-5.2");
845 assert_eq!(glm.max_output, Some(131_072));
846
847 let minimax = provider_capability(ApiProvider::Minimax, "minimax-m3");
848 assert_eq!(minimax.max_output, Some(524_288));
849
850 // A known cap below the requested cap must still clamp.
851 assert_eq!(
852 effective_max_output_tokens_for_route(ApiProvider::Moonshot, "kimi-k2.7-code", None),
853 32_768,
854 );
855 }
856
857 /// A documented capability maximum is not itself a sane default request
858 /// size. The capability remains documented and available as an explicit
859 /// override; only the automatic request is bounded.
860 #[test]
861 fn documented_ceiling_is_a_bound_not_an_unbounded_default_request() {
862 let _lock = crate::test_support::lock_test_env();
863 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
864 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
865
866 for model in [
867 "deepseek-v4-flash",
868 "deepseek-v4-pro",
869 "deepseek-v4flash",
870 "deepseek-ai/deepseek-v4-pro",
871 "deepseek-chat",
872 "deepseek-reasoner",
873 ] {
874 assert_eq!(
875 output_ceiling_source(ApiProvider::Deepseek, model),
876 OutputCeilingSource::Documented(384_000),
877 "{model}"
878 );
879 }
880 assert_eq!(
881 effective_max_output_tokens("deepseek-v4-flash"),
882 API_MAX_OUTPUT_TOKENS,
883 "a 384K capability maximum must not become the no-config request size"
884 );
885 assert_eq!(
886 effective_max_output_tokens("glm-5.2"),
887 API_MAX_OUTPUT_TOKENS,
888 "a 131K capability maximum must also remain a ceiling, not a default"
889 );
890 }
891
892 #[test]
893 fn uncatalogued_deepseek_variants_require_exact_output_metadata() {
894 let _env_lock = crate::test_support::lock_test_env();
895 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
896 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
897 let _catalog_lock = codewhale_models::model_catalog::test_catalog_lock();
898 let catalog = codewhale_models::model_catalog::MergedCatalog::from_sources(
899 std::collections::BTreeMap::new(),
900 None,
901 codewhale_models::model_catalog::bundled_catalog(),
902 chrono::Utc::now(),
903 );
904 let _catalog = codewhale_models::model_catalog::replace_active_catalog_for_test(catalog);
905
906 for provider in [
907 ApiProvider::Deepseek,
908 ApiProvider::DeepseekCN,
909 ApiProvider::DeepseekAnthropic,
910 ApiProvider::Custom,
911 ] {
912 for model in [
913 "deepseek-v4.1-flash-expires-on-0910",
914 "deepseek-v4.1-flash",
915 "deepseek-v4-flash-vendor",
916 ] {
917 assert_eq!(provider_capability(provider, model).max_output, None);
918 let source = output_ceiling_source(provider, model);
919 assert_eq!(source, OutputCeilingSource::Uncatalogued(65_536));
920 assert_eq!(source.as_str(), "uncatalogued");
921 assert_eq!(
922 effective_max_output_tokens_for_route(provider, model, None),
923 64_000,
924 "{provider:?}: {model}"
925 );
926 }
927 }
928
929 // Exact operator metadata can supply a missing ceiling or replace an
930 // existing catalog value; neither case may inherit a family guess.
931 let overrides = [
932 ("deepseek-v4.1-flash-expires-on-0910", 24_576),
933 ("deepseek-v4-flash", 32_768),
934 ]
935 .map(|(id, max_output)| {
936 (
937 id.to_string(),
938 codewhale_models::model_catalog::CatalogEntry {
939 id: id.to_string(),
940 context_window: Some(128_000),
941 max_output: Some(max_output),
942 supports_reasoning: None,
943 input_usd_per_million: None,
944 output_usd_per_million: None,
945 modalities: Vec::new(),
946 supported_parameters: Vec::new(),
947 provider_model_id: None,
948 provenance: codewhale_models::model_catalog::MetadataProvenance::UserOverride,
949 },
950 )
951 })
952 .into_iter()
953 .collect();
954 let catalog = codewhale_models::model_catalog::MergedCatalog::from_sources(
955 overrides,
956 None,
957 codewhale_models::model_catalog::bundled_catalog(),
958 chrono::Utc::now(),
959 );
960 let _override = codewhale_models::model_catalog::replace_active_catalog_for_test(catalog);
961 for (model, expected) in [
962 ("deepseek-v4.1-flash-expires-on-0910", 24_576),
963 ("deepseek-v4-flash", 32_768),
964 ] {
965 assert_eq!(
966 provider_capability(ApiProvider::Deepseek, model).max_output,
967 Some(expected),
968 "{model}"
969 );
970 assert_eq!(
971 effective_max_output_tokens_for_route(ApiProvider::Deepseek, model, None),
972 expected,
973 "{model}"
974 );
975 }
976 }
977
978 #[test]
979 fn deepseek_v4_explicit_mid_windows_share_one_safe_no_config_budget() {
980 let _lock = crate::test_support::lock_test_env();
981 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
982 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
983
984 for window in [262_144, 327_680, 393_216] {
985 let limits = RouteLimits {
986 context_tokens: Some(window),
987 ..RouteLimits::default()
988 };
989 let cap = effective_max_output_tokens_for_route(
990 ApiProvider::Vllm,
991 "DeepSeek-V4-Flash",
992 Some(limits),
993 );
994 let reservation =
995 route_output_reservation(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits));
996 let budget = route_context_budget(
997 ApiProvider::Vllm,
998 "DeepSeek-V4-Flash",
999 Some(limits),
1000 105_000,
1001 )
1002 .expect("explicit vLLM route budget");
1003
1004 assert_eq!(cap, API_MAX_OUTPUT_TOKENS, "window={window}");
1005 assert_eq!(reservation, cap, "window={window}");
1006 assert_eq!(
1007 budget.input_budget_ceiling,
1008 window - u64::from(API_MAX_OUTPUT_TOKENS) - 1_024,
1009 "window={window}"
1010 );
1011 assert!(
1012 105_000 < budget.input_budget_ceiling,
1013 "ordinary 85K-105K inputs must not trigger emergency compaction: {budget:?}"
1014 );
1015 assert!(budget.available_input_tokens > 0, "window={window}");
1016 }
1017 }
1018
1019 #[test]
1020 fn explicit_output_override_is_preserved_and_reserved_on_mid_windows() {
1021 let _lock = crate::test_support::lock_test_env();
1022 let _codewhale =
1023 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "100000");
1024 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1025 let limits = RouteLimits {
1026 context_tokens: Some(327_680),
1027 ..RouteLimits::default()
1028 };
1029
1030 let cap = effective_max_output_tokens_for_route(
1031 ApiProvider::Vllm,
1032 "DeepSeek-V4-Flash",
1033 Some(limits),
1034 );
1035 assert_eq!(cap, 100_000);
1036 assert_eq!(
1037 route_output_reservation(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits),),
1038 cap
1039 );
1040 let budget = route_context_budget(
1041 ApiProvider::Vllm,
1042 "DeepSeek-V4-Flash",
1043 Some(limits),
1044 105_000,
1045 )
1046 .expect("override route budget");
1047 assert_eq!(budget.input_budget_ceiling, 226_656);
1048 assert!(budget.available_input_tokens > 0);
1049 }
1050
1051 #[test]
1052 fn explicit_uncatalogued_allowance_respects_route_and_context_limits() {
1053 let _lock = crate::test_support::lock_test_env();
1054 let _canonical =
1055 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "100000");
1056 let _legacy = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1057 let model = "uncatalogued-preview-for-output-test";
1058 let limits = RouteLimits {
1059 context_tokens: Some(327_680),
1060 ..RouteLimits::default()
1061 };
1062 assert_eq!(
1063 effective_max_output_tokens_for_route(ApiProvider::Custom, model, Some(limits)),
1064 100_000
1065 );
1066 assert_eq!(
1067 effective_max_output_tokens_for_route(
1068 ApiProvider::Custom,
1069 model,
1070 Some(RouteLimits {
1071 output_tokens: Some(32_768),
1072 ..limits
1073 })
1074 ),
1075 32_768
1076 );
1077 let small = RouteLimits {
1078 context_tokens: Some(32_768),
1079 ..RouteLimits::default()
1080 };
1081 let cap = effective_max_output_tokens_for_route(ApiProvider::Custom, model, Some(small));
1082 assert_eq!(cap, 30_720);
1083 assert_eq!(
1084 route_output_reservation(ApiProvider::Custom, model, Some(small)),
1085 cap
1086 );
1087 }
1088
1089 #[test]
1090 fn oversized_explicit_override_is_clamped_and_reserved_to_the_route_window() {
1091 let _lock = crate::test_support::lock_test_env();
1092 let _codewhale =
1093 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "384000");
1094 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1095 let limits = RouteLimits {
1096 context_tokens: Some(327_680),
1097 ..RouteLimits::default()
1098 };
1099
1100 let cap = effective_max_output_tokens_for_route(
1101 ApiProvider::Vllm,
1102 "DeepSeek-V4-Flash",
1103 Some(limits),
1104 );
1105 assert_eq!(cap, 325_632);
1106 assert_eq!(
1107 route_output_reservation(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits),),
1108 cap,
1109 "preflight must reserve every token the explicit override can put on the wire"
1110 );
1111 let budget = route_context_budget(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits), 0)
1112 .expect("oversized override route budget");
1113 assert_eq!(budget.input_budget_ceiling, 1_024);
1114 }
1115
1116 #[test]
1117 fn explicit_override_on_large_window_stays_unified() {
1118 let _lock = crate::test_support::lock_test_env();
1119 let _codewhale =
1120 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", "384000");
1121 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1122 let limits = RouteLimits {
1123 context_tokens: Some(1_000_000),
1124 ..RouteLimits::default()
1125 };
1126
1127 let cap = effective_max_output_tokens_for_route(
1128 ApiProvider::Vllm,
1129 "DeepSeek-V4-Flash",
1130 Some(limits),
1131 );
1132 let reservation =
1133 route_output_reservation(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits));
1134 let budget = route_context_budget(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits), 0)
1135 .expect("large explicit route budget");
1136
1137 assert_eq!(cap, 384_000);
1138 assert_eq!(reservation, cap);
1139 assert_eq!(budget.output_cap_tokens, u64::from(cap));
1140 assert_eq!(budget.input_budget_ceiling, 614_976);
1141 }
1142
1143 #[test]
1144 fn automatic_wire_cap_and_reservation_have_no_large_window_cliff() {
1145 let _lock = crate::test_support::lock_test_env();
1146 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
1147 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1148
1149 for window in [499_999, 500_000, 1_000_000] {
1150 let limits = RouteLimits {
1151 context_tokens: Some(window),
1152 ..RouteLimits::default()
1153 };
1154 let wire = effective_max_output_tokens_for_route(
1155 ApiProvider::Vllm,
1156 "DeepSeek-V4-Flash",
1157 Some(limits),
1158 );
1159 let reservation =
1160 route_output_reservation(ApiProvider::Vllm, "DeepSeek-V4-Flash", Some(limits));
1161 assert_eq!(wire, API_MAX_OUTPUT_TOKENS, "window={window}");
1162 assert_eq!(reservation, wire, "window={window}");
1163 }
1164 }
1165
1166 #[test]
1167 fn concrete_route_input_limit_clamps_preflight_and_compaction() {
1168 let _lock = crate::test_support::lock_test_env();
1169 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
1170 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1171 let limits = RouteLimits {
1172 context_tokens: Some(1_000_000),
1173 input_tokens: Some(128_000),
1174 output_tokens: Some(64_000),
1175 };
1176
1177 let budget = route_context_budget(
1178 ApiProvider::Vllm,
1179 "DeepSeek-V4-Flash",
1180 Some(limits),
1181 200_000,
1182 )
1183 .expect("route budget");
1184 assert_eq!(route_input_limit_tokens(Some(limits)), Some(128_000));
1185 assert_eq!(budget.input_budget_ceiling, 128_000);
1186 assert_eq!(budget.available_input_tokens, 0);
1187 assert_eq!(budget.compaction_trigger_for_percent(80.0), 128_000);
1188 }
1189
1190 #[test]
1191 fn canonical_output_override_blank_falls_through_but_invalid_is_authoritative() {
1192 let _lock = crate::test_support::lock_test_env();
1193 let _legacy = crate::test_support::EnvVarGuard::set("DEEPSEEK_MAX_OUTPUT_TOKENS", "100000");
1194
1195 {
1196 let _canonical =
1197 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", " ");
1198 assert_eq!(explicit_max_output_tokens_override(), Some(100_000));
1199 }
1200 for invalid in ["not-a-number", "0"] {
1201 let _canonical =
1202 crate::test_support::EnvVarGuard::set("CODEWHALE_MAX_OUTPUT_TOKENS", invalid);
1203 assert_eq!(explicit_max_output_tokens_override(), None, "{invalid}");
1204 assert_eq!(
1205 effective_max_output_tokens("deepseek-v4-pro"),
1206 API_MAX_OUTPUT_TOKENS
1207 );
1208 }
1209 }
1210
1211 #[test]
1212 fn mid_window_internal_reservation_stays_on_the_ordinary_request_floor() {
1213 let _lock = crate::test_support::lock_test_env();
1214 let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_MAX_OUTPUT_TOKENS");
1215 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MAX_OUTPUT_TOKENS");
1216 let reservation =
1217 route_output_reservation(ApiProvider::Arcee, "trinity-large-thinking", None);
1218 assert_eq!(reservation, API_MAX_OUTPUT_TOKENS);
1219 let budget = route_context_budget(ApiProvider::Arcee, "trinity-large-thinking", None, 0)
1220 .expect("trinity route budget");
1221 assert_eq!(budget.compaction_trigger_for_percent(80.0), 195_584);
1222 }
1223
1224 #[test]
1225 fn review_reserve_is_sized_from_reasoning_classification() {
1226 // Mapping core (#6285): every classification arm.
1227 assert_eq!(review_reserve_percent_for(Some(false), false), 0);
1228 assert_eq!(review_reserve_percent_for(Some(false), true), 0);
1229 assert_eq!(review_reserve_percent_for(Some(true), false), 25);
1230 assert_eq!(review_reserve_percent_for(Some(true), true), 50);
1231 // Unknown (no catalogue row) is not evidence of no reasoning (#6032).
1232 assert_eq!(review_reserve_percent_for(None, false), 25);
1233 assert_eq!(review_reserve_percent_for(None, true), 25);
1234 }
1235
1236 #[test]
1237 fn review_reserve_tokens_follow_the_model_classification() {
1238 // A model no catalogue row resolves for: a quarter of the allowance is
1239 // reserved as visible text, and the token math scales off the exact
1240 // resolved allowance.
1241 let unknown = "not-a-catalogue-model-6285";
1242 assert_eq!(review_visible_text_reserve_percent(unknown), 25);
1243 assert_eq!(review_visible_text_reserve_tokens(unknown, 65_536), 16_384);
1244 assert_eq!(review_visible_text_reserve_tokens(unknown, 0), 0);
1245 }
1246 }
1247
1247 lines RUST