| 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 | /// 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)] |
| 107 | pub 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")] |
| 125 | pub 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)] |
| 153 | pub 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] |
| 183 | pub 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 | |
| 213 | impl 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 | } |
| 327 |