| 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 |