1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! Cached-input-classification seam (the onwards ↔ dwctl boundary).
//!
//! This module defines the *dormant plumbing* for Anthropic-style cached input
//! classification. onwards owns the abstraction (the [`CacheClassifier`] trait, the
//! neutral [`CacheStats`] result, and the [`NoopCacheClassifier`] default); the
//! real classification logic — index lookup, prefix hashing, tokenizer calls —
//! is implemented downstream in dwctl and injected at startup via
//! [`crate::AppState::with_cache_classifier`]. This is dependency inversion: the
//! compile-time dependency points one way (dwctl → onwards), the runtime call
//! goes the other way through the trait object, so there is no crate cycle.
//!
//! ## Dormant by default
//!
//! When no classifier is wired (the default), onwards behaviour is byte-identical
//! to today: no request forking, no `cache_control` stripping, and no usage
//! injection. The entire cache path in [`crate::handlers`] is gated on
//! `Some(classifier)`. A standalone onwards therefore sees zero observable change.
//!
//! ## The active path (when a classifier is wired)
//!
//! On each request onwards *forks*: it forwards to the upstream model and, in
//! parallel, calls [`CacheClassifier::classify`] under a deadline. The neutral
//! [`CacheStats`] split is then injected into the response `usage` object by
//! the OpenAI shaping helpers in [`crate::cache_usage`]. The no-op classifier
//! returns all-zero stats, so even with the classifier wired the injected fields
//! are zero and downstream billing is unaffected.
//!
//! The seam lives in `target_message_handler`, which both the regular and the strict
//! routers route through — so strict-mode requests get the same treatment
//! automatically, with no extra wiring.
//!
//! See `input-token-cache-pricing.md` §5.3, §6.1, §6.2, §6.3 for the design.
use async_trait;
use Duration;
/// Default deadline for the parallel classify branch.
///
/// On a real classifier this bounds how long the response join will wait for the
/// classification before falling back to all-zero stats (best-effort, billed
/// as un-cached). The no-op classifier returns instantly so this is never hit in
/// practice, but the deadline structure is built so a real classifier slots in.
pub const DEFAULT_CLASSIFY_DEADLINE: Duration = from_secs;
/// The input handed to a [`CacheClassifier`] for classification.
///
/// Carries what a real classifier needs to compute the read/write split: the
/// **virtual** model string (the user-facing alias / `OriginalModel`, *not*
/// the rewritten underlying `model_name` — see `handlers.rs`), and the request
/// body it must parse for `cache_control` markers. Kept minimal but real.
/// The paradigm-neutral result of classification: the read/write token split.
///
/// This is deliberately *not* OpenAI- or Anthropic-shaped — it is the neutral
/// representation that a per-endpoint module (the `openai` shaping today, an
/// `anthropic` shaping later) projects into the wire `usage` object. All
/// counts default to zero, which is exactly what [`NoopCacheClassifier`] returns.
///
/// `read` is the number of cached input tokens read back (charged at the
/// discounted rate); `creation_*` are the new tokens written to the cache,
/// split per TTL tier because their write premiums differ.
/// The abstraction onwards owns: given a request, return the read/write token
/// split. Implemented by dwctl (with the real index/tokenizer logic) and
/// injected at startup; onwards holds it as a trait object and never names or
/// imports the implementor.
///
/// `classify` must be cheap relative to the model call — it runs concurrently
/// with generation and is joined under a deadline, so a slow or failing classifier
/// degrades to all-zero stats rather than delaying the response.
/// The default no-op classifier: always returns all-zero [`CacheStats`].
///
/// This is what makes onwards runnable standalone with the cache path "on" but
/// effectively invisible — the injected `usage` cache fields are all zero, so
/// downstream billing sees zeros and is unaffected. It is also the classifier
/// dwctl wires for local end-to-end validation of the spine.
;