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}