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 pub-*read*, but the type cannot be *constructed* outside this
9//! crate: the struct is `#[non_exhaustive]` (no other crate can build it via a
10//! struct literal) and deliberately does not derive `Deserialize` (so it cannot
11//! be fabricated from JSON either). The only constructor is
12//! [`ReadyRouteCandidate::new`]
13//! (`pub(super)`), and [`super::resolver::RouteResolver::resolve`] is its sole
14//! caller. A candidate's existence is therefore proof it passed the resolver.
15//!
16//! DEFERRED: #3384's full sketch also carried `capabilities: CapabilityProfile`
17//! and `config_snapshot: Config`. Both are intentionally omitted here: pulling
18//! `CapabilityProfile` into `crates/config` would force a `tui -> config` type
19//! move, and embedding `Config` would couple the candidate to the full config
20//! model. They will be added when those types have a home in this crate.
21
22use serde::{Deserialize, Serialize};
23
24use super::RequestProtocol;
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}
62
63/// Pricing/quota class for the resolved route.
64///
65/// Carries only coarse, non-sensitive shape; never secrets or account ids.
66///
67/// `PartialEq` (but not `Eq`: the `Token` rates are `f64`) lets offerings and
68/// candidates be compared in tests and lets
69/// [`super::offering::ProviderModelOffering`] carry a pricing meter.
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum PricingSku {
73    /// Per-token pricing.
74    Token {
75        /// Input price per million tokens, if known.
76        input_per_mtok: Option<f64>,
77        /// Output price per million tokens, if known.
78        output_per_mtok: Option<f64>,
79    },
80    /// Subscription quota usage.
81    SubscriptionQuota {
82        /// Percent of quota used, if known.
83        used_pct: Option<f32>,
84        /// When the quota resets, if known.
85        resets_at: Option<String>,
86    },
87    /// Prepaid account credits.
88    AccountCredits {
89        /// Remaining balance, if known.
90        balance: Option<f64>,
91    },
92    /// Local or otherwise not billed.
93    LocalOrNotApplicable,
94    /// Pricing unknown or stale.
95    UnknownOrStale,
96}
97
98/// Outcome of route validation.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ValidationReport {
101    /// Whether the route passed validation.
102    pub ok: bool,
103    /// Human-readable diagnostics (advisory; secret-free).
104    pub messages: Vec<String>,
105}
106
107/// A runtime-resolved, executable route.
108///
109/// Fields are read-only to callers; the type cannot be constructed outside this
110/// crate (`#[non_exhaustive]` + no `Deserialize`). The only constructor is
111/// [`Self::new`], which is `pub(super)`; see module docs.
112#[derive(Debug, Clone, Serialize)]
113#[non_exhaustive]
114pub struct ReadyRouteCandidate {
115    /// Resolved provider id.
116    pub provider_id: ProviderId,
117    /// Resolved provider kind.
118    pub provider_kind: ProviderKind,
119    /// The selector the user/route requested.
120    pub logical_model: LogicalModelRef,
121    /// Canonical model identity, if one was resolved.
122    pub canonical_model: Option<ModelId>,
123    /// Provider-owned wire id put on the request.
124    pub wire_model_id: WireModelId,
125    /// Resolved endpoint transport facts.
126    pub endpoint: ResolvedEndpoint,
127    /// Resolved auth source CLASS (never a secret value).
128    pub auth: ResolvedAuthSource,
129    /// Selected wire protocol.
130    pub protocol: RequestProtocol,
131    /// Route/offering-scoped token limits, when known.
132    pub limits: RouteLimits,
133    /// Pricing/quota class, if known.
134    pub pricing: Option<PricingSku>,
135    /// Validation outcome.
136    pub validation: ValidationReport,
137}
138
139impl ReadyRouteCandidate {
140    /// Mint a candidate. Restricted to [`super::resolver`] so the resolver is
141    /// the sole producer of executable routes (the #2608 mutation gate).
142    #[allow(clippy::too_many_arguments)]
143    pub(super) fn new(
144        provider_id: ProviderId,
145        provider_kind: ProviderKind,
146        logical_model: LogicalModelRef,
147        canonical_model: Option<ModelId>,
148        wire_model_id: WireModelId,
149        endpoint: ResolvedEndpoint,
150        auth: ResolvedAuthSource,
151        protocol: RequestProtocol,
152        limits: RouteLimits,
153        pricing: Option<PricingSku>,
154        validation: ValidationReport,
155    ) -> Self {
156        Self {
157            provider_id,
158            provider_kind,
159            logical_model,
160            canonical_model,
161            wire_model_id,
162            endpoint,
163            auth,
164            protocol,
165            limits,
166            pricing,
167            validation,
168        }
169    }
170}