返回 CodeWhale
candidate.rs
根目录 / crates / config / src / route / candidate.rs
1 //! The runtime-resolved executable route (#3384).
2 //!
3 //! A [`ReadyRouteCandidate`] is the concrete form of the #2608 contract:
4 //!
5 //! > Execution requires a `ReadyRouteCandidate`.
6 //! > A `ReadyRouteCandidate` can only be produced by `RouteResolver`.
7 //!
8 //! Fields are private and exposed only through read-only getters, so the type
9 //! can neither be *constructed* nor *mutated* outside this crate, and it
10 //! deliberately does not derive `Deserialize` (so it cannot be fabricated from
11 //! JSON either). The only constructor is `ReadyRouteCandidate::new`
12 //! (`pub(super)`), and [`super::resolver::RouteResolver::resolve`] is its sole
13 //! caller. A candidate's existence is therefore proof it passed the resolver,
14 //! and a candidate's limits are therefore exactly what the resolver produced
15 //! (including any [`SourcedLimitOverride`]s recorded on it).
16 //!
17 //! The route-owned, three-state capability profile lives in this crate and is
18 //! carried on the candidate. A full `config_snapshot: Config` remains deferred
19 //! because embedding it would couple the candidate to the full config model.
20
21 use serde::{Deserialize, Serialize};
22
23 use super::RequestProtocol;
24 use super::capabilities::RouteCapabilities;
25 use super::ids::{LogicalModelRef, ModelId, ProviderId, WireModelId};
26 use super::offering::RouteLimits;
27 use crate::ProviderKind;
28
29 /// A concrete, resolved endpoint the route will talk to.
30 #[derive(Debug, Clone, Serialize, Deserialize)]
31 pub struct ResolvedEndpoint {
32 /// Resolved base URL (after any override).
33 pub base_url: String,
34 /// Endpoint key (e.g. `"chat"`, `"responses"`).
35 pub endpoint_key: String,
36 /// Wire protocol spoken at this endpoint.
37 pub protocol: RequestProtocol,
38 }
39
40 /// The CLASS of auth source resolved for the route.
41 ///
42 /// This records only *where* a credential comes from, never the credential
43 /// value itself. There is intentionally no field that could hold a secret.
44 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45 #[serde(rename_all = "snake_case")]
46 pub enum ResolvedAuthSource {
47 /// Supplied via CLI flag/argument.
48 Cli,
49 /// Read from a config file.
50 ConfigFile,
51 /// Read from the OS keyring.
52 Keyring,
53 /// Read from an environment variable.
54 Env,
55 /// Produced by running a command.
56 Command,
57 /// Resolved from a named secret.
58 Secret,
59 /// No credential resolved.
60 Missing,
61 /// Auth resolution has not been performed for this candidate.
62 ///
63 /// The route resolver never inspects credentials, so a freshly resolved
64 /// candidate honestly reports `Unresolved` rather than claiming `Missing`
65 /// (which would assert a lookup that never happened).
66 Unresolved,
67 }
68
69 /// Which token limit a [`SourcedLimitOverride`] targets.
70 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
71 #[serde(rename_all = "snake_case")]
72 pub enum LimitField {
73 /// [`RouteLimits::context_tokens`](super::offering::RouteLimits).
74 ContextTokens,
75 /// [`RouteLimits::input_tokens`](super::offering::RouteLimits).
76 InputTokens,
77 /// [`RouteLimits::output_tokens`](super::offering::RouteLimits).
78 OutputTokens,
79 }
80
81 /// Why a limit override was requested.
82 ///
83 /// This is provenance, not policy: the resolver applies whatever the caller
84 /// requested and records the source on the candidate so every consumer can see
85 /// where an effective limit came from.
86 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
87 #[serde(rename_all = "snake_case")]
88 pub enum OverrideSource {
89 /// Operator-configured context window.
90 UserContextWindow,
91 /// Operator-configured context window for one exact wire model id
92 /// (`[providers.<id>.model_context_windows]`, #6108).
93 UserModelContextWindow,
94 /// Limits declared by the operator for an exact model and endpoint.
95 UserModelMetadata,
96 /// Catalog limits describe the public API offering, not the account-scoped
97 /// Codex route; the API-only limits are stripped.
98 CodexPublicApiLimitStrip,
99 /// Per-model context from the fresh Codex account roster.
100 CodexRosterCorrection,
101 /// Fresh, route-scoped provider-reported context metadata.
102 ProviderReportedContextWindow,
103 /// Conservative all-plan safe floor for a membership-plan route.
104 MembershipPlanSafeFloor,
105 /// Maximum output documented for one exact provider endpoint/model route.
106 DocumentedRouteOutputMaximum,
107 }
108
109 /// One sourced limit override, applied by the resolver BEFORE the candidate is
110 /// constructed and recorded on the candidate as provenance.
111 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112 pub struct SourcedLimitOverride {
113 /// The limit field to override.
114 pub field: LimitField,
115 /// The value to set (`None` clears the limit to "unknown").
116 pub value: Option<u64>,
117 /// Why the override was requested.
118 pub source: OverrideSource,
119 }
120
121 /// Pricing/quota class for the resolved route.
122 ///
123 /// Carries only coarse, non-sensitive shape; never secrets or account ids.
124 ///
125 /// `PartialEq` (but not `Eq`: the `Token` rates are `f64`) lets offerings and
126 /// candidates be compared in tests and lets
127 /// [`super::offering::ProviderModelOffering`] carry a pricing meter.
128 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129 #[serde(rename_all = "snake_case")]
130 pub enum PricingSku {
131 /// Per-token pricing.
132 Token {
133 /// Input price per million tokens, if known.
134 input_per_mtok: Option<f64>,
135 /// Output price per million tokens, if known.
136 output_per_mtok: Option<f64>,
137 },
138 /// Subscription quota usage.
139 SubscriptionQuota {
140 /// Percent of quota used, if known.
141 used_pct: Option<f32>,
142 /// When the quota resets, if known.
143 resets_at: Option<String>,
144 },
145 /// Prepaid account credits.
146 AccountCredits {
147 /// Remaining balance, if known.
148 balance: Option<f64>,
149 },
150 /// Local or otherwise not billed.
151 LocalOrNotApplicable,
152 /// Pricing unknown or stale.
153 UnknownOrStale,
154 }
155
156 /// Outcome of route validation.
157 #[derive(Debug, Clone, Serialize, Deserialize)]
158 pub struct ValidationReport {
159 /// Whether the route passed validation.
160 pub ok: bool,
161 /// Human-readable diagnostics (advisory; secret-free).
162 pub messages: Vec<String>,
163 }
164
165 /// A runtime-resolved, executable route.
166 ///
167 /// The candidate is IMMUTABLE once minted: every field is private and exposed
168 /// only through read-only getters, the type cannot be constructed outside this
169 /// crate (private fields + no `Deserialize`), and there are no setters. The
170 /// only constructor is `Self::new`, which is `pub(super)`; see module docs.
171 /// Post-resolution limit adjustments must instead be requested up front via
172 /// [`super::resolver::RouteRequest::limit_overrides`], which the resolver
173 /// applies BEFORE construction and records in [`Self::applied_limit_overrides`].
174 ///
175 /// Immutability is compile-time enforced — this does not build:
176 ///
177 /// ```compile_fail
178 /// use codewhale_config::route::{RouteRequest, RouteResolver};
179 ///
180 /// let mut candidate = RouteResolver::new()
181 /// .resolve(&RouteRequest::default())
182 /// .unwrap();
183 /// // ERROR: field `limits` of `ReadyRouteCandidate` is private
184 /// candidate.limits.context_tokens = Some(1);
185 /// ```
186 #[derive(Debug, Clone, Serialize)]
187 #[non_exhaustive]
188 pub struct ReadyRouteCandidate {
189 /// Resolved provider id.
190 provider_id: ProviderId,
191 /// Resolved provider kind.
192 provider_kind: ProviderKind,
193 /// The selector the user/route requested.
194 logical_model: LogicalModelRef,
195 /// Canonical model identity, if one was resolved.
196 canonical_model: Option<ModelId>,
197 /// Provider-owned wire id put on the request.
198 wire_model_id: WireModelId,
199 /// Resolved endpoint transport facts.
200 endpoint: ResolvedEndpoint,
201 /// Resolved auth source CLASS (never a secret value).
202 auth: ResolvedAuthSource,
203 /// Selected wire protocol.
204 protocol: RequestProtocol,
205 /// Route/offering-scoped token limits, when known (overrides applied).
206 limits: RouteLimits,
207 /// Capability facts for the exact provider/model offering.
208 capabilities: RouteCapabilities,
209 /// Pricing/quota class, if known.
210 pricing: Option<PricingSku>,
211 /// Validation outcome.
212 validation: ValidationReport,
213 /// Provenance of every limit override applied at resolution time.
214 #[serde(skip_serializing_if = "Vec::is_empty")]
215 applied_limit_overrides: Vec<SourcedLimitOverride>,
216 }
217
218 impl ReadyRouteCandidate {
219 /// Mint a candidate. Restricted to [`super::resolver`] so the resolver is
220 /// the sole producer of executable routes (the #2608 mutation gate).
221 #[allow(clippy::too_many_arguments)]
222 pub(super) fn new(
223 provider_id: ProviderId,
224 provider_kind: ProviderKind,
225 logical_model: LogicalModelRef,
226 canonical_model: Option<ModelId>,
227 wire_model_id: WireModelId,
228 endpoint: ResolvedEndpoint,
229 auth: ResolvedAuthSource,
230 protocol: RequestProtocol,
231 limits: RouteLimits,
232 capabilities: RouteCapabilities,
233 pricing: Option<PricingSku>,
234 validation: ValidationReport,
235 applied_limit_overrides: Vec<SourcedLimitOverride>,
236 ) -> Self {
237 Self {
238 provider_id,
239 provider_kind,
240 logical_model,
241 canonical_model,
242 wire_model_id,
243 endpoint,
244 auth,
245 protocol,
246 limits,
247 capabilities,
248 pricing,
249 validation,
250 applied_limit_overrides,
251 }
252 }
253
254 /// Resolved provider id.
255 #[must_use]
256 pub fn provider_id(&self) -> &ProviderId {
257 &self.provider_id
258 }
259
260 /// Resolved provider kind.
261 #[must_use]
262 pub fn provider_kind(&self) -> ProviderKind {
263 self.provider_kind
264 }
265
266 /// The selector the user/route requested.
267 #[must_use]
268 pub fn logical_model(&self) -> &LogicalModelRef {
269 &self.logical_model
270 }
271
272 /// Canonical model identity, if one was resolved.
273 #[must_use]
274 pub fn canonical_model(&self) -> Option<&ModelId> {
275 self.canonical_model.as_ref()
276 }
277
278 /// Provider-owned wire id put on the request.
279 #[must_use]
280 pub fn wire_model_id(&self) -> &WireModelId {
281 &self.wire_model_id
282 }
283
284 /// Resolved endpoint transport facts.
285 #[must_use]
286 pub fn endpoint(&self) -> &ResolvedEndpoint {
287 &self.endpoint
288 }
289
290 /// Resolved auth source CLASS (never a secret value).
291 #[must_use]
292 pub fn auth(&self) -> &ResolvedAuthSource {
293 &self.auth
294 }
295
296 /// Selected wire protocol.
297 #[must_use]
298 pub fn protocol(&self) -> RequestProtocol {
299 self.protocol
300 }
301
302 /// Route/offering-scoped token limits, when known (overrides applied).
303 #[must_use]
304 pub fn limits(&self) -> RouteLimits {
305 self.limits
306 }
307
308 /// Capability facts for the exact provider/model offering.
309 #[must_use]
310 pub fn capabilities(&self) -> RouteCapabilities {
311 self.capabilities
312 }
313
314 /// Pricing/quota class, if known.
315 #[must_use]
316 pub fn pricing(&self) -> Option<&PricingSku> {
317 self.pricing.as_ref()
318 }
319
320 /// Validation outcome.
321 #[must_use]
322 pub fn validation(&self) -> &ValidationReport {
323 &self.validation
324 }
325
326 /// Provenance of every limit override applied at resolution time.
327 #[must_use]
328 pub fn applied_limit_overrides(&self) -> &[SourcedLimitOverride] {
329 &self.applied_limit_overrides
330 }
331 }
332
332 lines RUST