Skip to main content

codewhale_config/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
21use serde::{Deserialize, Serialize};
22
23use super::RequestProtocol;
24use super::capabilities::RouteCapabilities;
25use super::ids::{LogicalModelRef, ModelId, ProviderId, WireModelId};
26use super::offering::RouteLimits;
27use crate::ProviderKind;
28
29/// A concrete, resolved endpoint the route will talk to.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub 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")]
46pub 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")]
72pub 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")]
88pub enum OverrideSource {
89    /// Operator-configured context window.
90    UserContextWindow,
91    /// Catalog limits describe the public API offering, not the account-scoped
92    /// Codex route; the API-only limits are stripped.
93    CodexPublicApiLimitStrip,
94    /// Per-model context from the fresh Codex account roster.
95    CodexRosterCorrection,
96    /// Fresh, route-scoped provider-reported context metadata.
97    ProviderReportedContextWindow,
98    /// Conservative all-plan safe floor for a membership-plan route.
99    MembershipPlanSafeFloor,
100    /// Maximum output documented for one exact provider endpoint/model route.
101    DocumentedRouteOutputMaximum,
102}
103
104/// One sourced limit override, applied by the resolver BEFORE the candidate is
105/// constructed and recorded on the candidate as provenance.
106#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
107pub struct SourcedLimitOverride {
108    /// The limit field to override.
109    pub field: LimitField,
110    /// The value to set (`None` clears the limit to "unknown").
111    pub value: Option<u64>,
112    /// Why the override was requested.
113    pub source: OverrideSource,
114}
115
116/// Pricing/quota class for the resolved route.
117///
118/// Carries only coarse, non-sensitive shape; never secrets or account ids.
119///
120/// `PartialEq` (but not `Eq`: the `Token` rates are `f64`) lets offerings and
121/// candidates be compared in tests and lets
122/// [`super::offering::ProviderModelOffering`] carry a pricing meter.
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum PricingSku {
126    /// Per-token pricing.
127    Token {
128        /// Input price per million tokens, if known.
129        input_per_mtok: Option<f64>,
130        /// Output price per million tokens, if known.
131        output_per_mtok: Option<f64>,
132    },
133    /// Subscription quota usage.
134    SubscriptionQuota {
135        /// Percent of quota used, if known.
136        used_pct: Option<f32>,
137        /// When the quota resets, if known.
138        resets_at: Option<String>,
139    },
140    /// Prepaid account credits.
141    AccountCredits {
142        /// Remaining balance, if known.
143        balance: Option<f64>,
144    },
145    /// Local or otherwise not billed.
146    LocalOrNotApplicable,
147    /// Pricing unknown or stale.
148    UnknownOrStale,
149}
150
151/// Outcome of route validation.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ValidationReport {
154    /// Whether the route passed validation.
155    pub ok: bool,
156    /// Human-readable diagnostics (advisory; secret-free).
157    pub messages: Vec<String>,
158}
159
160/// A runtime-resolved, executable route.
161///
162/// The candidate is IMMUTABLE once minted: every field is private and exposed
163/// only through read-only getters, the type cannot be constructed outside this
164/// crate (private fields + no `Deserialize`), and there are no setters. The
165/// only constructor is `Self::new`, which is `pub(super)`; see module docs.
166/// Post-resolution limit adjustments must instead be requested up front via
167/// [`super::resolver::RouteRequest::limit_overrides`], which the resolver
168/// applies BEFORE construction and records in [`Self::applied_limit_overrides`].
169///
170/// Immutability is compile-time enforced — this does not build:
171///
172/// ```compile_fail
173/// use codewhale_config::route::{RouteRequest, RouteResolver};
174///
175/// let mut candidate = RouteResolver::new()
176///     .resolve(&RouteRequest::default())
177///     .unwrap();
178/// // ERROR: field `limits` of `ReadyRouteCandidate` is private
179/// candidate.limits.context_tokens = Some(1);
180/// ```
181#[derive(Debug, Clone, Serialize)]
182#[non_exhaustive]
183pub struct ReadyRouteCandidate {
184    /// Resolved provider id.
185    provider_id: ProviderId,
186    /// Resolved provider kind.
187    provider_kind: ProviderKind,
188    /// The selector the user/route requested.
189    logical_model: LogicalModelRef,
190    /// Canonical model identity, if one was resolved.
191    canonical_model: Option<ModelId>,
192    /// Provider-owned wire id put on the request.
193    wire_model_id: WireModelId,
194    /// Resolved endpoint transport facts.
195    endpoint: ResolvedEndpoint,
196    /// Resolved auth source CLASS (never a secret value).
197    auth: ResolvedAuthSource,
198    /// Selected wire protocol.
199    protocol: RequestProtocol,
200    /// Route/offering-scoped token limits, when known (overrides applied).
201    limits: RouteLimits,
202    /// Capability facts for the exact provider/model offering.
203    capabilities: RouteCapabilities,
204    /// Pricing/quota class, if known.
205    pricing: Option<PricingSku>,
206    /// Validation outcome.
207    validation: ValidationReport,
208    /// Provenance of every limit override applied at resolution time.
209    #[serde(skip_serializing_if = "Vec::is_empty")]
210    applied_limit_overrides: Vec<SourcedLimitOverride>,
211}
212
213impl ReadyRouteCandidate {
214    /// Mint a candidate. Restricted to [`super::resolver`] so the resolver is
215    /// the sole producer of executable routes (the #2608 mutation gate).
216    #[allow(clippy::too_many_arguments)]
217    pub(super) fn new(
218        provider_id: ProviderId,
219        provider_kind: ProviderKind,
220        logical_model: LogicalModelRef,
221        canonical_model: Option<ModelId>,
222        wire_model_id: WireModelId,
223        endpoint: ResolvedEndpoint,
224        auth: ResolvedAuthSource,
225        protocol: RequestProtocol,
226        limits: RouteLimits,
227        capabilities: RouteCapabilities,
228        pricing: Option<PricingSku>,
229        validation: ValidationReport,
230        applied_limit_overrides: Vec<SourcedLimitOverride>,
231    ) -> Self {
232        Self {
233            provider_id,
234            provider_kind,
235            logical_model,
236            canonical_model,
237            wire_model_id,
238            endpoint,
239            auth,
240            protocol,
241            limits,
242            capabilities,
243            pricing,
244            validation,
245            applied_limit_overrides,
246        }
247    }
248
249    /// Resolved provider id.
250    #[must_use]
251    pub fn provider_id(&self) -> &ProviderId {
252        &self.provider_id
253    }
254
255    /// Resolved provider kind.
256    #[must_use]
257    pub fn provider_kind(&self) -> ProviderKind {
258        self.provider_kind
259    }
260
261    /// The selector the user/route requested.
262    #[must_use]
263    pub fn logical_model(&self) -> &LogicalModelRef {
264        &self.logical_model
265    }
266
267    /// Canonical model identity, if one was resolved.
268    #[must_use]
269    pub fn canonical_model(&self) -> Option<&ModelId> {
270        self.canonical_model.as_ref()
271    }
272
273    /// Provider-owned wire id put on the request.
274    #[must_use]
275    pub fn wire_model_id(&self) -> &WireModelId {
276        &self.wire_model_id
277    }
278
279    /// Resolved endpoint transport facts.
280    #[must_use]
281    pub fn endpoint(&self) -> &ResolvedEndpoint {
282        &self.endpoint
283    }
284
285    /// Resolved auth source CLASS (never a secret value).
286    #[must_use]
287    pub fn auth(&self) -> &ResolvedAuthSource {
288        &self.auth
289    }
290
291    /// Selected wire protocol.
292    #[must_use]
293    pub fn protocol(&self) -> RequestProtocol {
294        self.protocol
295    }
296
297    /// Route/offering-scoped token limits, when known (overrides applied).
298    #[must_use]
299    pub fn limits(&self) -> RouteLimits {
300        self.limits
301    }
302
303    /// Capability facts for the exact provider/model offering.
304    #[must_use]
305    pub fn capabilities(&self) -> RouteCapabilities {
306        self.capabilities
307    }
308
309    /// Pricing/quota class, if known.
310    #[must_use]
311    pub fn pricing(&self) -> Option<&PricingSku> {
312        self.pricing.as_ref()
313    }
314
315    /// Validation outcome.
316    #[must_use]
317    pub fn validation(&self) -> &ValidationReport {
318        &self.validation
319    }
320
321    /// Provenance of every limit override applied at resolution time.
322    #[must_use]
323    pub fn applied_limit_overrides(&self) -> &[SourcedLimitOverride] {
324        &self.applied_limit_overrides
325    }
326}