返回 CodeWhale
descriptor.rs
根目录 / crates / config / src / route / descriptor.rs
1 //! Provider descriptors over the existing built-in provider registry (#3084).
2 //!
3 //! A [`ProviderDescriptor`] is a thin, route-facing view over the static
4 //! [`provider::Provider`] trait objects already in [`crate::provider`]. It
5 //! surfaces only the transport facts route resolution needs (id, base URL,
6 //! default wire model, env vars, protocol) without duplicating the registry.
7 //!
8 //! Because a descriptor holds a `&'static dyn Provider`, it is intentionally
9 //! NOT `Serialize`/`PartialEq`-derivable. Never embed a [`ProviderDescriptor`]
10 //! inside a `Serialize` struct; serialize the resolved facts instead.
11
12 use crate::ProviderKind;
13 use crate::provider::{self, CredentialAcquisition, Provider, WirePolicy};
14 use serde::{Deserialize, Serialize};
15
16 use super::RequestProtocol;
17 use super::auth::AuthMethod;
18 use super::ids::{ProviderId, RouteId, WireModelId};
19
20 /// Route-facing view of a built-in provider's transport facts.
21 ///
22 /// Holds a trait object, so it is deliberately not serializable/comparable.
23 #[derive(Clone, Copy)]
24 pub struct ProviderDescriptor {
25 /// The provider kind this descriptor describes.
26 pub kind: ProviderKind,
27 /// Backing static provider metadata entry.
28 pub inner: &'static dyn Provider,
29 }
30
31 impl ProviderDescriptor {
32 /// Build a descriptor for a known provider kind.
33 #[must_use]
34 pub fn for_kind(kind: ProviderKind) -> Self {
35 Self {
36 kind,
37 inner: provider::provider_for_kind(kind),
38 }
39 }
40
41 /// Canonical provider id.
42 #[must_use]
43 pub fn id(&self) -> ProviderId {
44 ProviderId::from(self.inner.id())
45 }
46
47 /// Flat kebab route id for this descriptor.
48 #[must_use]
49 pub fn route_id(&self) -> RouteId {
50 RouteId::from_kind(self.kind)
51 }
52
53 /// Display-grouping family. Not a second identity: stored identity is [`Self::route_id`].
54 #[must_use]
55 pub fn family(&self) -> &'static str {
56 family_for(self.kind)
57 }
58
59 /// Bespoke-transport classification. OpenAI-compatible catalog rows share one kind.
60 #[must_use]
61 pub fn transport(&self) -> TransportKind {
62 TransportKind::for_kind(self.kind)
63 }
64
65 /// Declared auth methods. OAuth is a type only; no adapter is implemented here.
66 #[must_use]
67 pub fn auth_methods(&self) -> &'static [AuthMethod] {
68 auth_methods_for(self.kind)
69 }
70
71 /// Default base URL when no override is present.
72 #[must_use]
73 pub fn default_base_url(&self) -> &'static str {
74 self.inner.default_base_url()
75 }
76
77 /// Default wire model id when no model is selected.
78 #[must_use]
79 pub fn default_wire_model(&self) -> WireModelId {
80 WireModelId::from(self.inner.default_model())
81 }
82
83 /// Environment variable candidates for this provider's API key.
84 #[must_use]
85 pub fn env_vars(&self) -> &'static [&'static str] {
86 self.inner.env_vars()
87 }
88
89 /// Policy used to select this provider's wire protocol.
90 #[must_use]
91 pub fn wire_policy(&self) -> WirePolicy {
92 self.inner.wire_policy()
93 }
94
95 /// Resolve the concrete protocol for an offering endpoint key.
96 #[must_use]
97 pub fn protocol_for_endpoint(&self, endpoint_key: &str) -> Option<RequestProtocol> {
98 self.wire_policy().resolve(endpoint_key)
99 }
100 }
101
102 impl std::fmt::Debug for ProviderDescriptor {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("ProviderDescriptor")
105 .field("kind", &self.kind)
106 .field("id", &self.inner.id())
107 .field("wire_policy", &self.inner.wire_policy())
108 .finish()
109 }
110 }
111
112 /// A concrete endpoint's transport facts.
113 ///
114 /// Unlike [`ProviderDescriptor`], this owns plain data and is safe to embed in
115 /// serializable route output (see [`super::candidate::ResolvedEndpoint`]).
116 #[derive(Debug, Clone)]
117 pub struct EndpointDescriptor {
118 /// Stable endpoint key (e.g. `"chat"`, `"responses"`).
119 pub endpoint_key: String,
120 /// Wire protocol spoken at this endpoint.
121 pub protocol: RequestProtocol,
122 /// Default base URL for this endpoint.
123 pub default_base_url: String,
124 /// Whether streaming is supported.
125 pub streaming: bool,
126 }
127
128 /// Bespoke-transport classification. Catalog rows that speak OpenAI Chat
129 /// Completions share [`TransportKind::ChatCompletions`]; only genuinely
130 /// different wires get their own kind. This is the direction `ProviderKind`
131 /// shrinks toward.
132 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133 #[serde(rename_all = "kebab-case")]
134 pub enum TransportKind {
135 /// OpenAI-compatible `/v1/chat/completions`.
136 ChatCompletions,
137 /// Native Anthropic Messages (`/v1/messages`).
138 AnthropicMessages,
139 /// OpenAI Responses (`/responses`).
140 OpenAiResponses,
141 /// Closed, model-aware protocol roster (OpenCode Zen, DeepSeek dual-wire).
142 ModelAware,
143 /// ChatGPT Codex OAuth route.
144 Codex,
145 /// Retired Antigravity identity retained for legacy config inspection.
146 Antigravity,
147 /// Local runtime (Ollama / vLLM / SGLang).
148 LocalRuntime,
149 /// User-defined OpenAI-compatible endpoint.
150 Custom,
151 }
152
153 impl TransportKind {
154 /// Map a known kind onto the ~8 bespoke transports. Everything else is a
155 /// catalog row on [`Self::ChatCompletions`].
156 #[must_use]
157 pub fn for_kind(kind: ProviderKind) -> Self {
158 match kind {
159 ProviderKind::Anthropic
160 | ProviderKind::DeepseekAnthropic
161 | ProviderKind::MinimaxAnthropic
162 | ProviderKind::ModelstudioTokenPlanAnthropic
163 | ProviderKind::ModelstudioCodingPlanAnthropic => Self::AnthropicMessages,
164 ProviderKind::OpenaiCodex => Self::Codex,
165 ProviderKind::Antigravity => Self::Antigravity,
166 ProviderKind::Ollama | ProviderKind::Vllm | ProviderKind::Sglang => Self::LocalRuntime,
167 ProviderKind::Custom => Self::Custom,
168 ProviderKind::Deepseek | ProviderKind::OpencodeZen | ProviderKind::OpencodeGo => {
169 Self::ModelAware
170 }
171 _ => match kind.provider().wire_policy() {
172 WirePolicy::ModelAware => Self::ModelAware,
173 WirePolicy::Fixed(crate::provider::WireFormat::Responses) => Self::OpenAiResponses,
174 WirePolicy::Fixed(crate::provider::WireFormat::AnthropicMessages) => {
175 Self::AnthropicMessages
176 }
177 WirePolicy::Fixed(crate::provider::WireFormat::ChatCompletions) => {
178 Self::ChatCompletions
179 }
180 },
181 }
182 }
183 }
184
185 /// Display-grouping family. Selecting a family with multiple routes asks a
186 /// `select` whose option value **is a route id**.
187 #[must_use]
188 pub fn family_for(kind: ProviderKind) -> &'static str {
189 match kind {
190 ProviderKind::Deepseek | ProviderKind::DeepseekAnthropic => "deepseek",
191 ProviderKind::Minimax | ProviderKind::MinimaxAnthropic => "minimax",
192 ProviderKind::ModelstudioTokenPlan
193 | ProviderKind::ModelstudioTokenPlanAnthropic
194 | ProviderKind::ModelstudioCodingPlan
195 | ProviderKind::ModelstudioCodingPlanAnthropic => "alibaba-modelstudio",
196 ProviderKind::Siliconflow | ProviderKind::SiliconflowCN => "siliconflow",
197 ProviderKind::Ollama | ProviderKind::OllamaCloud => "ollama",
198 other => other.as_str(),
199 }
200 }
201
202 /// Auth methods declared for a provider kind. OAuth is a type, not an adapter.
203 #[must_use]
204 pub fn auth_methods_for(kind: ProviderKind) -> &'static [AuthMethod] {
205 if kind == ProviderKind::Antigravity {
206 return &[];
207 }
208 match kind.provider().credential_help().acquisition {
209 CredentialAcquisition::ApiKey => &[AuthMethod::API_KEY],
210 CredentialAcquisition::ApiKeyOrOAuth => &[AuthMethod::API_KEY, AuthMethod::OAUTH],
211 CredentialAcquisition::LocalOptional => &[AuthMethod::KEYLESS],
212 CredentialAcquisition::OAuth => &[AuthMethod::OAUTH],
213 CredentialAcquisition::Configuration => &[AuthMethod::EXTERNAL_CONSENT],
214 }
215 }
216
216 lines RUST