Skip to main content

router_protocol/
route_identity.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Canonical provider/transport identity types for configuration schema v2.
4
5use serde::{Deserialize, Deserializer, Serialize};
6use std::collections::HashSet;
7use std::fmt;
8use thiserror::Error;
9
10pub const OLLAMA_CHAT_PATH: &str = "/api/chat";
11pub const OLLAMA_RELAY_ORIGIN: &str = "http://127.0.0.1:11434";
12pub const OLLAMA_CLOUD_ORIGIN: &str = "https://ollama.com";
13pub const OLLAMA_GLM_5_2_MODEL: &str = "glm-5.2";
14pub const OPENROUTER_ORIGIN: &str = "https://openrouter.ai";
15pub const OPENROUTER_CHAT_PATH: &str = "/api/v1/chat/completions";
16pub const DASHSCOPE_ORIGIN: &str = "https://dashscope-intl.aliyuncs.com";
17/// The operator's Alibaba Cloud Model Studio token-plan (budget) endpoint. A
18/// token-plan API key authenticates only against this host, never against the
19/// standard `dashscope-intl` origin, so a live Qwen route bound to a
20/// token-plan credential must resolve here.
21pub const DASHSCOPE_TOKEN_PLAN_ORIGIN: &str = "https://token-plan.ap-southeast-1.maas.aliyuncs.com";
22pub const DASHSCOPE_CHAT_PATH: &str = "/compatible-mode/v1/chat/completions";
23pub const XAI_ORIGIN: &str = "https://api.x.ai";
24pub const XAI_CHAT_PATH: &str = "/v1/chat/completions";
25pub const ZAI_ORIGIN: &str = "https://api.z.ai";
26pub const ZAI_CHAT_PATH: &str = "/api/paas/v4/chat/completions";
27pub const MOONSHOT_ORIGIN: &str = "https://api.moonshot.ai";
28pub const MOONSHOT_CHAT_PATH: &str = "/v1/chat/completions";
29
30// DeepSeek direct API (PRD-001y S1). Verified against the provider's published
31// API documentation at https://api-docs.deepseek.com/ (build-time verification
32// only; no live call). The OpenAI-compatible chat completions endpoint is
33// `POST {DEEPSEEK_ORIGIN}{DEEPSEEK_CHAT_PATH}` with a `Authorization: Bearer
34// <key>` header, SSE streaming with `stream_options.include_usage: true` and a
35// `data: [DONE]` terminal. The base URL carries no `/v1` prefix.
36pub const DEEPSEEK_ORIGIN: &str = "https://api.deepseek.com";
37pub const DEEPSEEK_CHAT_PATH: &str = "/chat/completions";
38
39// Anthropic Messages API (PRD-001y S2). Verified against the provider's
40// published API documentation at https://platform.claude.com/docs/en/api/messages
41// (build-time verification only; no live call). The endpoint is
42// `POST {ANTHROPIC_ORIGIN}{ANTHROPIC_CHAT_PATH}` with the `x-api-key` header
43// and the required `anthropic-version` header (2023-06-01). The model id is a
44// request-body field, so the route path is static. SSE streaming uses
45// `stream: true` and yields `message_start`/`content_block_*`/`message_delta`/
46// `message_stop` events.
47pub const ANTHROPIC_ORIGIN: &str = "https://api.anthropic.com";
48pub const ANTHROPIC_CHAT_PATH: &str = "/v1/messages";
49/// The header name carrying the Anthropic API key.
50pub const ANTHROPIC_API_KEY_HEADER: &str = "x-api-key";
51/// The header name carrying the required Anthropic API version.
52pub const ANTHROPIC_VERSION_HEADER: &str = "anthropic-version";
53/// The Anthropic API version pinned at build time from the published docs.
54pub const ANTHROPIC_VERSION: &str = "2023-06-01";
55
56// Gemini API (PRD-001y S2). Verified against the provider's published API
57// documentation at https://ai.google.dev/api/generate-content (build-time
58// verification only; no live call). The endpoint is
59// `POST {GEMINI_ORIGIN}{GEMINI_GENERATE_CONTENT_PATH}` with the `x-goog-api-key`
60// header. Unlike the chat-completions providers, the model id is a path
61// parameter (`models/{model}:generateContent`), so the route endpoint path is
62// a template that the Gemini adapter materializes with the route model id.
63// Streaming uses the `:streamGenerateContent` sibling path (documented SSE).
64pub const GEMINI_ORIGIN: &str = "https://generativelanguage.googleapis.com";
65/// The route endpoint path template. The Gemini adapter replaces the `{model}`
66/// token with the route model id to form the wire path.
67pub const GEMINI_GENERATE_CONTENT_PATH: &str = "/v1beta/models/{model}:generateContent";
68/// The header name carrying the Gemini API key.
69pub const GEMINI_API_KEY_HEADER: &str = "x-goog-api-key";
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum PhaseKind {
74    OpenaiSubscription,
75    OllamaCloud,
76    OpenrouterApi,
77    OllamaLocal,
78    QwenSubscription,
79    QwenApi,
80    GrokSubscription,
81    GrokApi,
82    ZaiSubscription,
83    ZaiApi,
84    KimiSubscription,
85    KimiApi,
86    DeepseekApi,
87    ClaudeSubscription,
88    ClaudeApi,
89    GeminiSubscription,
90    GeminiApi,
91    DirectMoonshotKimiExtension,
92    XaiGrokExtension,
93}
94
95/// Stable operator-defined phase identity. `PhaseKind` remains the executable
96/// adapter kind; ordering and fallback use this independent string identity.
97#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
98#[serde(transparent)]
99pub struct PhaseId(String);
100
101impl PhaseId {
102    pub const OPENAI_SUBSCRIPTION: &'static str = "openai_subscription";
103    pub const OLLAMA_CLOUD: &'static str = "ollama_cloud";
104    pub const OPENROUTER_API: &'static str = "openrouter_api";
105    pub const OLLAMA_LOCAL: &'static str = "ollama_local";
106    pub const QWEN_SUBSCRIPTION: &'static str = "qwen_subscription";
107    pub const QWEN_API: &'static str = "qwen_api";
108    pub const GROK_SUBSCRIPTION: &'static str = "grok_subscription";
109    pub const GROK_API: &'static str = "grok_api";
110    pub const ZAI_SUBSCRIPTION: &'static str = "zai_subscription";
111    pub const ZAI_API: &'static str = "zai_api";
112    pub const KIMI_SUBSCRIPTION: &'static str = "kimi_subscription";
113    pub const KIMI_API: &'static str = "kimi_api";
114    pub const DEEPSEEK_API: &'static str = "deepseek_api";
115    pub const CLAUDE_SUBSCRIPTION: &'static str = "claude_subscription";
116    pub const CLAUDE_API: &'static str = "claude_api";
117    pub const GEMINI_SUBSCRIPTION: &'static str = "gemini_subscription";
118    pub const GEMINI_API: &'static str = "gemini_api";
119
120    pub fn parse(value: impl Into<String>) -> Result<Self, RouteIdentityError> {
121        let value = value.into();
122        if value.is_empty()
123            || value.len() > 128
124            || !value
125                .bytes()
126                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
127        {
128            return Err(RouteIdentityError::InvalidPhaseId);
129        }
130        Ok(Self(value))
131    }
132
133    pub fn as_str(&self) -> &str {
134        &self.0
135    }
136}
137
138impl fmt::Display for PhaseId {
139    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140        formatter.write_str(&self.0)
141    }
142}
143
144impl<'de> Deserialize<'de> for PhaseId {
145    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146    where
147        D: Deserializer<'de>,
148    {
149        Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
150    }
151}
152
153impl PhaseKind {
154    /// Canonical S08 legacy phase order. This is the fixed order of the
155    /// original four fixture phases, not the full built-in set: the
156    /// follow-on phases (Qwen, Grok, Z.ai, Kimi) and any future phase are
157    /// never appended here, because this order is load-bearing for legacy
158    /// serialization and for `PhaseId` ordering in v2/v3 configs.
159    pub const BUILTIN_ORDER: [Self; 4] = [
160        Self::OpenaiSubscription,
161        Self::OllamaCloud,
162        Self::OpenrouterApi,
163        Self::OllamaLocal,
164    ];
165
166    pub const fn is_builtin(self) -> bool {
167        matches!(
168            self,
169            Self::OpenaiSubscription
170                | Self::OllamaCloud
171                | Self::OpenrouterApi
172                | Self::OllamaLocal
173                | Self::QwenSubscription
174                | Self::QwenApi
175                | Self::GrokSubscription
176                | Self::GrokApi
177                | Self::ZaiSubscription
178                | Self::ZaiApi
179                | Self::KimiSubscription
180                | Self::KimiApi
181                | Self::DeepseekApi
182                | Self::ClaudeSubscription
183                | Self::ClaudeApi
184                | Self::GeminiSubscription
185                | Self::GeminiApi
186        )
187    }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum ProviderService {
193    Openai,
194    OllamaCloud,
195    Openrouter,
196    OllamaLocal,
197    Qwen,
198    Grok,
199    Zai,
200    MoonshotKimi,
201    Xai,
202    Deepseek,
203    Claude,
204    Gemini,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum TransportKind {
210    HarnessManaged,
211    OllamaSignedInRelay,
212    DirectHttps,
213    OllamaLocalDaemon,
214    DisabledExtension,
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
218#[serde(rename_all = "snake_case")]
219pub enum CredentialSourceKind {
220    HarnessManaged,
221    OllamaDaemonSession,
222    SecretRef,
223    None,
224    Unconfigured,
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
228#[serde(rename_all = "snake_case")]
229pub enum CatalogSourceKind {
230    OpenaiHarnessCatalog,
231    OllamaCloudCatalog,
232    OpenrouterCatalog,
233    OllamaLocalInventory,
234    QwenCatalog,
235    GrokCatalog,
236    ZaiCatalog,
237    KimiCatalog,
238    DeepseekCatalog,
239    ClaudeCatalog,
240    GeminiCatalog,
241    ExtensionCatalog,
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum BillingAuthorityClass {
247    OpenaiSubscription,
248    OllamaCloud,
249    OpenrouterMeteredApi,
250    LocalCompute,
251    QwenSubscription,
252    QwenMeteredApi,
253    GrokSubscription,
254    GrokMeteredApi,
255    ZaiSubscription,
256    ZaiMeteredApi,
257    KimiSubscription,
258    KimiMeteredApi,
259    DeepseekMeteredApi,
260    ClaudeSubscription,
261    ClaudeMeteredApi,
262    GeminiSubscription,
263    GeminiMeteredApi,
264    ExtensionProvider,
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum ModelNamespace {
270    Openai,
271    OllamaCloud,
272    Openrouter,
273    OllamaLocal,
274    Qwen,
275    Grok,
276    Zai,
277    MoonshotKimi,
278    Xai,
279    Deepseek,
280    Claude,
281    Gemini,
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum ExecutionLocation {
287    RemoteCloud,
288    LocalMachine,
289}
290
291#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
292#[serde(rename_all = "snake_case")]
293pub enum PhasePlacement {
294    Builtin,
295    DisabledUnplacedExtension,
296}
297
298/// How a phase can serve Codex harness subagent (multi-agent) calls.
299///
300/// `Native` means the backend returns the `namespace` field the CLI requires
301/// (OpenAI subscription today). `NamespaceUnsupported` means the backend emits
302/// bare or flattened tool names that SteadyMux must normalize at the wire
303/// boundary (Ollama Cloud, OpenRouter, local Ollama today). `Disabled` means
304/// the phase cannot serve subagent-bearing requests at all.
305#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[serde(rename_all = "snake_case")]
307pub enum MultiAgentCapability {
308    Native,
309    NamespaceUnsupported,
310    Disabled,
311}
312
313impl MultiAgentCapability {
314    /// Whether a subagent-bearing request can be served natively on this phase.
315    pub const fn serves_subagents_natively(self) -> bool {
316        matches!(self, Self::Native)
317    }
318}
319
320impl PhaseKind {
321    /// Static multi-agent capability projection for the phase adapter kind.
322    /// The OpenAI subscription path returns the `namespace` field natively;
323    /// every other builtin phase emits bare or flattened tool names that
324    /// SteadyMux normalizes at the wire boundary.
325    pub const fn multi_agent_capability(self) -> MultiAgentCapability {
326        match self {
327            Self::OpenaiSubscription => MultiAgentCapability::Native,
328            Self::OllamaCloud
329            | Self::OpenrouterApi
330            | Self::OllamaLocal
331            | Self::QwenSubscription
332            | Self::QwenApi
333            | Self::GrokSubscription
334            | Self::GrokApi
335            | Self::ZaiSubscription
336            | Self::ZaiApi
337            | Self::KimiSubscription
338            | Self::KimiApi
339            | Self::DeepseekApi
340            | Self::ClaudeSubscription
341            | Self::ClaudeApi
342            | Self::GeminiSubscription
343            | Self::GeminiApi
344            | Self::DirectMoonshotKimiExtension
345            | Self::XaiGrokExtension => MultiAgentCapability::NamespaceUnsupported,
346        }
347    }
348}
349
350/// Multi-agent protocol version a route can serve. `V2` is the Codex MAv2
351/// namespace protocol (`collaboration`/`agents`); `V1` is the earlier
352/// `multi_agent_v1` namespace protocol; `Disabled` means the route cannot
353/// serve any namespace protocol.
354#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case")]
356pub enum MultiAgentVersion {
357    V1,
358    V2,
359    Disabled,
360}
361
362impl MultiAgentVersion {
363    pub const fn as_str(self) -> &'static str {
364        match self {
365            Self::V1 => "v1",
366            Self::V2 => "v2",
367            Self::Disabled => "disabled",
368        }
369    }
370}
371
372/// Wire API used for multi-agent calls. Only the Responses API is supported in
373/// this generation; the chat wire API was removed in the Codex CLI generation
374/// that introduced namespace tools.
375#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
376#[serde(rename_all = "snake_case")]
377pub enum MultiAgentWireApi {
378    Responses,
379}
380
381/// Observed namespace-call behavior of the backend, recorded per route so the
382/// wire normalizer and operators can see how each phase emits namespace tool
383/// calls (PRD-001u R1.1).
384#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386pub enum ObservedNamespaceCallBehavior {
387    /// Backend returns the `namespace` field on `function_call` items natively
388    /// (OpenAI subscription today).
389    ReturnsNamespaceField,
390    /// Backend emits a bare or flattened tool name without a namespace field;
391    /// SteadyMux injects the namespace at the wire boundary.
392    BareNameNoNamespace,
393    /// Backend accepts the flattened `namespace__name` request form.
394    FlattenedNamespaceName,
395    /// Route cannot serve namespace calls at all.
396    Disabled,
397}
398
399/// Per-route MAv2 capability projection (PRD-001u R1.1): whether the route
400/// represents namespace tools, the multi-agent protocol version it serves, the
401/// wire API, and the observed namespace-call behavior.
402#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
403#[serde(deny_unknown_fields)]
404pub struct MultiAgentCapabilityProjection {
405    /// Whether the route represents namespace tools (`functions.<ns>.<fn>`).
406    /// `false` means the backend cannot represent namespaces and SteadyMux
407    /// normalizes at the wire boundary.
408    pub namespace_tools: bool,
409    pub multi_agent_version: MultiAgentVersion,
410    pub wire_api: MultiAgentWireApi,
411    pub observed_namespace_calls: ObservedNamespaceCallBehavior,
412}
413
414/// Chosen MAv2 namespace for a route (PRD-001u R4.1). `Collaboration` is the
415/// model-reserved namespace for GPT-5.6 Sol/Terra/Luna and requires the exact
416/// canonical schema; `Agents` is the documented non-reserved namespace that
417/// bypasses the reserved-schema 400 for generated schemas.
418#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum MultiAgentNamespace {
421    Collaboration,
422    Agents,
423}
424
425impl MultiAgentNamespace {
426    pub const fn as_str(self) -> &'static str {
427        match self {
428            Self::Collaboration => "collaboration",
429            Self::Agents => "agents",
430        }
431    }
432}
433
434impl PhaseKind {
435    /// Static MAv2 capability projection for the phase adapter kind. The OpenAI
436    /// subscription path returns the `namespace` field natively under the
437    /// model-reserved `collaboration` namespace; every other builtin phase
438    /// emits bare or flattened tool names that SteadyMux normalizes at the
439    /// wire boundary into the non-reserved `agents` namespace.
440    pub const fn multi_agent_v2_projection(self) -> MultiAgentCapabilityProjection {
441        match self {
442            Self::OpenaiSubscription => MultiAgentCapabilityProjection {
443                namespace_tools: true,
444                multi_agent_version: MultiAgentVersion::V2,
445                wire_api: MultiAgentWireApi::Responses,
446                observed_namespace_calls: ObservedNamespaceCallBehavior::ReturnsNamespaceField,
447            },
448            Self::OllamaCloud
449            | Self::OpenrouterApi
450            | Self::OllamaLocal
451            | Self::QwenSubscription
452            | Self::QwenApi
453            | Self::GrokSubscription
454            | Self::GrokApi
455            | Self::ZaiSubscription
456            | Self::ZaiApi
457            | Self::KimiSubscription
458            | Self::KimiApi
459            | Self::DeepseekApi
460            | Self::ClaudeSubscription
461            | Self::ClaudeApi
462            | Self::GeminiSubscription
463            | Self::GeminiApi
464            | Self::DirectMoonshotKimiExtension
465            | Self::XaiGrokExtension => MultiAgentCapabilityProjection {
466                namespace_tools: false,
467                multi_agent_version: MultiAgentVersion::V2,
468                wire_api: MultiAgentWireApi::Responses,
469                observed_namespace_calls: ObservedNamespaceCallBehavior::BareNameNoNamespace,
470            },
471        }
472    }
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
476#[serde(deny_unknown_fields)]
477pub struct EndpointIdentity {
478    pub origin: String,
479    pub path: String,
480}
481
482#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
483#[serde(deny_unknown_fields)]
484pub struct RouteIdentity {
485    pub phase: PhaseKind,
486    pub placement: PhasePlacement,
487    pub provider: ProviderService,
488    pub transport: TransportKind,
489    pub endpoint: EndpointIdentity,
490    pub credential_source: CredentialSourceKind,
491    pub credential_ref: Option<String>,
492    /// The credential pool for this route: one or more opaque credential
493    /// references. When non-empty, the router selects and fails over within
494    /// this pool (key1 -> key2 -> ...). When empty, `credential_ref` is used
495    /// as the single credential. Additive; absent means a single credential.
496    #[serde(default)]
497    pub credential_refs: Vec<String>,
498    pub catalog_source: CatalogSourceKind,
499    pub catalog_ref: String,
500    pub billing_authority: BillingAuthorityClass,
501    pub model_namespace: ModelNamespace,
502    pub model_id: String,
503    pub execution_location: ExecutionLocation,
504    pub account_ref: String,
505    pub capacity_domain_ref: String,
506    #[serde(default)]
507    pub local_execution_proof: bool,
508    #[serde(default)]
509    pub approved_local_inventory: Vec<String>,
510    /// Provider-neutral reasoning modes the model advertises (for example
511    /// `reasoning`). The request normalizer uses this to decide whether an
512    /// Ollama transport may ask the model to think before responding.
513    #[serde(default)]
514    pub reasoning_modes: Vec<String>,
515}
516
517#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
518pub enum RouteIdentityError {
519    #[error("phase identity is invalid")]
520    InvalidPhaseId,
521    #[error("route identity is ambiguous or internally inconsistent")]
522    AmbiguousIdentity,
523    #[error("route endpoint does not match the frozen transport contract")]
524    InvalidEndpoint,
525    #[error("route credential source does not match its transport")]
526    InvalidCredentialSource,
527    #[error("local Ollama route lacks local execution and inventory proof")]
528    MissingLocalProof,
529    #[error("route reasoning mode claim is invalid")]
530    InvalidReasoningModes,
531    #[error("disabled extension cannot be placed or enabled")]
532    ExtensionMustRemainUnplaced,
533}
534
535impl RouteIdentity {
536    /// Multi-agent capability projection for this route, derived from its
537    /// phase adapter kind. A disabled extension route is never native.
538    pub fn multi_agent_capability(&self) -> MultiAgentCapability {
539        if self.placement == PhasePlacement::DisabledUnplacedExtension {
540            return MultiAgentCapability::Disabled;
541        }
542        self.phase.multi_agent_capability()
543    }
544
545    /// MAv2 capability projection for this route (PRD-001u R1.1). A disabled
546    /// extension route is never namespace-capable.
547    pub fn multi_agent_v2_projection(&self) -> MultiAgentCapabilityProjection {
548        if self.placement == PhasePlacement::DisabledUnplacedExtension {
549            return MultiAgentCapabilityProjection {
550                namespace_tools: false,
551                multi_agent_version: MultiAgentVersion::Disabled,
552                wire_api: MultiAgentWireApi::Responses,
553                observed_namespace_calls: ObservedNamespaceCallBehavior::Disabled,
554            };
555        }
556        self.phase.multi_agent_v2_projection()
557    }
558
559    /// The MAv2 namespace chosen for this route (PRD-001u R4.2). The native
560    /// OpenAI phase submits the reserved `collaboration` schema; every other
561    /// phase generates its MAv2 schema under the non-reserved `agents`
562    /// namespace to avoid the reserved-schema 400.
563    pub fn mav2_namespace_choice(&self) -> MultiAgentNamespace {
564        match self.phase {
565            PhaseKind::OpenaiSubscription => MultiAgentNamespace::Collaboration,
566            PhaseKind::OllamaCloud
567            | PhaseKind::OpenrouterApi
568            | PhaseKind::OllamaLocal
569            | PhaseKind::QwenSubscription
570            | PhaseKind::QwenApi
571            | PhaseKind::GrokSubscription
572            | PhaseKind::GrokApi
573            | PhaseKind::ZaiSubscription
574            | PhaseKind::ZaiApi
575            | PhaseKind::KimiSubscription
576            | PhaseKind::KimiApi
577            | PhaseKind::DeepseekApi
578            | PhaseKind::ClaudeSubscription
579            | PhaseKind::ClaudeApi
580            | PhaseKind::GeminiSubscription
581            | PhaseKind::GeminiApi
582            | PhaseKind::DirectMoonshotKimiExtension
583            | PhaseKind::XaiGrokExtension => MultiAgentNamespace::Agents,
584        }
585    }
586
587    /// The multi-agent protocol version this route serves (PRD-001u R1.1).
588    pub fn multi_agent_version_served(&self) -> MultiAgentVersion {
589        self.multi_agent_v2_projection().multi_agent_version
590    }
591
592    pub fn validate(&self) -> Result<(), RouteIdentityError> {
593        if !valid_ref(&self.account_ref)
594            || !valid_ref(&self.capacity_domain_ref)
595            || !valid_ref(&self.catalog_ref)
596            || !valid_model_id(&self.model_id)
597        {
598            return Err(RouteIdentityError::AmbiguousIdentity);
599        }
600        if self.reasoning_modes.len() > 8
601            || self.reasoning_modes.iter().any(|mode| !valid_ref(mode))
602            || self.reasoning_modes.iter().collect::<HashSet<_>>().len()
603                != self.reasoning_modes.len()
604        {
605            return Err(RouteIdentityError::InvalidReasoningModes);
606        }
607        self.validate_credential_pool()?;
608        if self.phase.is_builtin() != (self.placement == PhasePlacement::Builtin) {
609            return Err(RouteIdentityError::ExtensionMustRemainUnplaced);
610        }
611        if !self.phase.is_builtin() {
612            return self.validate_extension();
613        }
614
615        match self.phase {
616            PhaseKind::OpenaiSubscription => self.validate_openai_subscription(),
617            PhaseKind::OllamaCloud => self.validate_ollama_cloud(),
618            PhaseKind::OpenrouterApi => self.validate_openrouter(),
619            PhaseKind::OllamaLocal => self.validate_ollama_local(),
620            PhaseKind::QwenSubscription => self.validate_qwen_subscription(),
621            PhaseKind::QwenApi => self.validate_qwen_api(),
622            PhaseKind::GrokSubscription => self.validate_grok_subscription(),
623            PhaseKind::GrokApi => self.validate_grok_api(),
624            PhaseKind::ZaiSubscription => self.validate_zai_subscription(),
625            PhaseKind::ZaiApi => self.validate_zai_api(),
626            PhaseKind::KimiSubscription => self.validate_kimi_subscription(),
627            PhaseKind::KimiApi => self.validate_kimi_api(),
628            PhaseKind::DeepseekApi => self.validate_deepseek_api(),
629            PhaseKind::ClaudeSubscription => self.validate_claude_subscription(),
630            PhaseKind::ClaudeApi => self.validate_claude_api(),
631            PhaseKind::GeminiSubscription => self.validate_gemini_subscription(),
632            PhaseKind::GeminiApi => self.validate_gemini_api(),
633            PhaseKind::DirectMoonshotKimiExtension | PhaseKind::XaiGrokExtension => {
634                Err(RouteIdentityError::ExtensionMustRemainUnplaced)
635            }
636        }
637    }
638
639    /// Validate the credential-pool invariant. A populated pool must contain
640    /// unique, valid secret references, and the legacy `credential_ref`
641    /// fallback must be the first pool entry. An empty pool leaves
642    /// `credential_ref` as the single credential (validated by the phase
643    /// rules). Phases that require `credential_ref == None` therefore reject
644    /// any populated pool.
645    fn validate_credential_pool(&self) -> Result<(), RouteIdentityError> {
646        if self.credential_refs.is_empty() {
647            return Ok(());
648        }
649        if self.credential_refs.iter().collect::<HashSet<_>>().len() != self.credential_refs.len() {
650            return Err(RouteIdentityError::AmbiguousIdentity);
651        }
652        if self
653            .credential_refs
654            .iter()
655            .any(|reference| !valid_secret_ref(reference))
656        {
657            return Err(RouteIdentityError::InvalidCredentialSource);
658        }
659        if self.credential_ref.as_deref() != self.credential_refs.first().map(String::as_str) {
660            return Err(RouteIdentityError::AmbiguousIdentity);
661        }
662        Ok(())
663    }
664
665    fn validate_openai_subscription(&self) -> Result<(), RouteIdentityError> {
666        if self.provider != ProviderService::Openai
667            || self.transport != TransportKind::HarnessManaged
668            || self.credential_source != CredentialSourceKind::HarnessManaged
669            || self.credential_ref.is_some()
670            || self.catalog_source != CatalogSourceKind::OpenaiHarnessCatalog
671            || self.billing_authority != BillingAuthorityClass::OpenaiSubscription
672            || self.model_namespace != ModelNamespace::Openai
673            || self.execution_location != ExecutionLocation::RemoteCloud
674            || self.endpoint.origin != "harness://codex-app-server"
675            || self.endpoint.path != "/responses"
676        {
677            return Err(RouteIdentityError::AmbiguousIdentity);
678        }
679        Ok(())
680    }
681
682    fn validate_ollama_cloud(&self) -> Result<(), RouteIdentityError> {
683        if self.provider != ProviderService::OllamaCloud
684            || self.catalog_source != CatalogSourceKind::OllamaCloudCatalog
685            || self.billing_authority != BillingAuthorityClass::OllamaCloud
686            || self.model_namespace != ModelNamespace::OllamaCloud
687            || self.execution_location != ExecutionLocation::RemoteCloud
688        {
689            return Err(RouteIdentityError::AmbiguousIdentity);
690        }
691        match self.transport {
692            TransportKind::OllamaSignedInRelay => {
693                if self.endpoint.origin != OLLAMA_RELAY_ORIGIN
694                    || self.endpoint.path != OLLAMA_CHAT_PATH
695                {
696                    return Err(RouteIdentityError::InvalidEndpoint);
697                }
698                if self.credential_source != CredentialSourceKind::OllamaDaemonSession
699                    || self.credential_ref.is_some()
700                    || !valid_ollama_cloud_wire_model(
701                        &self.model_id,
702                        TransportKind::OllamaSignedInRelay,
703                    )
704                {
705                    return Err(RouteIdentityError::InvalidCredentialSource);
706                }
707            }
708            TransportKind::DirectHttps => {
709                if self.endpoint.origin != OLLAMA_CLOUD_ORIGIN
710                    || self.endpoint.path != OLLAMA_CHAT_PATH
711                {
712                    return Err(RouteIdentityError::InvalidEndpoint);
713                }
714                if self.credential_source != CredentialSourceKind::SecretRef
715                    || self
716                        .credential_ref
717                        .as_deref()
718                        .is_none_or(|value| !valid_secret_ref(value))
719                    || !valid_ollama_cloud_wire_model(&self.model_id, TransportKind::DirectHttps)
720                {
721                    return Err(RouteIdentityError::InvalidCredentialSource);
722                }
723            }
724            _ => return Err(RouteIdentityError::AmbiguousIdentity),
725        }
726        Ok(())
727    }
728
729    fn validate_openrouter(&self) -> Result<(), RouteIdentityError> {
730        if self.provider != ProviderService::Openrouter
731            || self.transport != TransportKind::DirectHttps
732            || self.endpoint.origin != OPENROUTER_ORIGIN
733            || self.endpoint.path != OPENROUTER_CHAT_PATH
734            || self.credential_source != CredentialSourceKind::SecretRef
735            || self
736                .credential_ref
737                .as_deref()
738                .is_none_or(|value| !valid_secret_ref(value))
739            || self.catalog_source != CatalogSourceKind::OpenrouterCatalog
740            || self.billing_authority != BillingAuthorityClass::OpenrouterMeteredApi
741            || self.model_namespace != ModelNamespace::Openrouter
742            || self.execution_location != ExecutionLocation::RemoteCloud
743        {
744            return Err(RouteIdentityError::AmbiguousIdentity);
745        }
746        Ok(())
747    }
748
749    fn validate_ollama_local(&self) -> Result<(), RouteIdentityError> {
750        if self.provider != ProviderService::OllamaLocal
751            || self.transport != TransportKind::OllamaLocalDaemon
752            || self.endpoint.origin != OLLAMA_RELAY_ORIGIN
753            || self.endpoint.path != OLLAMA_CHAT_PATH
754            || self.credential_source != CredentialSourceKind::None
755            || self.credential_ref.is_some()
756            || self.catalog_source != CatalogSourceKind::OllamaLocalInventory
757            || self.billing_authority != BillingAuthorityClass::LocalCompute
758            || self.model_namespace != ModelNamespace::OllamaLocal
759            || self.execution_location != ExecutionLocation::LocalMachine
760        {
761            return Err(RouteIdentityError::AmbiguousIdentity);
762        }
763        if !self.local_execution_proof
764            || has_ollama_cloud_wire_suffix(&self.model_id)
765            || !self
766                .approved_local_inventory
767                .iter()
768                .any(|model| model == &self.model_id && !has_ollama_cloud_wire_suffix(model))
769        {
770            return Err(RouteIdentityError::MissingLocalProof);
771        }
772        Ok(())
773    }
774
775    /// Shared contract for the follow-on provider phases (PRD-001s R1). Every
776    /// first-class follow-on phase is a direct HTTPS chat-completions route
777    /// with an opaque secret reference. The subscription and api phases differ
778    /// only in billing authority and catalog source.
779    fn validate_follow_on_route(
780        &self,
781        provider: ProviderService,
782        origin: &str,
783        path: &str,
784        catalog_source: CatalogSourceKind,
785        billing_authority: BillingAuthorityClass,
786        namespace: ModelNamespace,
787    ) -> Result<(), RouteIdentityError> {
788        self.validate_follow_on_route_any(
789            provider,
790            &[origin],
791            path,
792            catalog_source,
793            billing_authority,
794            namespace,
795        )
796    }
797
798    /// Like [`Self::validate_follow_on_route`] but accepts any one of several
799    /// equivalent provider origins. Qwen/DashScope is the only follow-on
800    /// provider with distinct account-type origins: the standard pay-as-you-go
801    /// `dashscope-intl` endpoint and the operator's token-plan (budget)
802    /// endpoint. A live route bound to either account type is a first-class
803    /// Qwen route.
804    fn validate_follow_on_route_any(
805        &self,
806        provider: ProviderService,
807        origins: &[&str],
808        path: &str,
809        catalog_source: CatalogSourceKind,
810        billing_authority: BillingAuthorityClass,
811        namespace: ModelNamespace,
812    ) -> Result<(), RouteIdentityError> {
813        if self.provider != provider
814            || self.transport != TransportKind::DirectHttps
815            || !origins.contains(&self.endpoint.origin.as_str())
816            || self.endpoint.path != path
817            || self.credential_source != CredentialSourceKind::SecretRef
818            || self
819                .credential_ref
820                .as_deref()
821                .is_none_or(|value| !valid_secret_ref(value))
822            || self.catalog_source != catalog_source
823            || self.billing_authority != billing_authority
824            || self.model_namespace != namespace
825            || self.execution_location != ExecutionLocation::RemoteCloud
826        {
827            return Err(RouteIdentityError::AmbiguousIdentity);
828        }
829        Ok(())
830    }
831
832    fn validate_qwen_subscription(&self) -> Result<(), RouteIdentityError> {
833        self.validate_follow_on_route_any(
834            ProviderService::Qwen,
835            &[DASHSCOPE_ORIGIN, DASHSCOPE_TOKEN_PLAN_ORIGIN],
836            DASHSCOPE_CHAT_PATH,
837            CatalogSourceKind::QwenCatalog,
838            BillingAuthorityClass::QwenSubscription,
839            ModelNamespace::Qwen,
840        )
841    }
842
843    fn validate_qwen_api(&self) -> Result<(), RouteIdentityError> {
844        self.validate_follow_on_route_any(
845            ProviderService::Qwen,
846            &[DASHSCOPE_ORIGIN, DASHSCOPE_TOKEN_PLAN_ORIGIN],
847            DASHSCOPE_CHAT_PATH,
848            CatalogSourceKind::QwenCatalog,
849            BillingAuthorityClass::QwenMeteredApi,
850            ModelNamespace::Qwen,
851        )
852    }
853
854    fn validate_grok_subscription(&self) -> Result<(), RouteIdentityError> {
855        self.validate_follow_on_route(
856            ProviderService::Grok,
857            XAI_ORIGIN,
858            XAI_CHAT_PATH,
859            CatalogSourceKind::GrokCatalog,
860            BillingAuthorityClass::GrokSubscription,
861            ModelNamespace::Grok,
862        )
863    }
864
865    fn validate_grok_api(&self) -> Result<(), RouteIdentityError> {
866        self.validate_follow_on_route(
867            ProviderService::Grok,
868            XAI_ORIGIN,
869            XAI_CHAT_PATH,
870            CatalogSourceKind::GrokCatalog,
871            BillingAuthorityClass::GrokMeteredApi,
872            ModelNamespace::Grok,
873        )
874    }
875
876    fn validate_zai_subscription(&self) -> Result<(), RouteIdentityError> {
877        self.validate_follow_on_route(
878            ProviderService::Zai,
879            ZAI_ORIGIN,
880            ZAI_CHAT_PATH,
881            CatalogSourceKind::ZaiCatalog,
882            BillingAuthorityClass::ZaiSubscription,
883            ModelNamespace::Zai,
884        )
885    }
886
887    fn validate_zai_api(&self) -> Result<(), RouteIdentityError> {
888        self.validate_follow_on_route(
889            ProviderService::Zai,
890            ZAI_ORIGIN,
891            ZAI_CHAT_PATH,
892            CatalogSourceKind::ZaiCatalog,
893            BillingAuthorityClass::ZaiMeteredApi,
894            ModelNamespace::Zai,
895        )
896    }
897
898    fn validate_kimi_subscription(&self) -> Result<(), RouteIdentityError> {
899        self.validate_follow_on_route(
900            ProviderService::MoonshotKimi,
901            MOONSHOT_ORIGIN,
902            MOONSHOT_CHAT_PATH,
903            CatalogSourceKind::KimiCatalog,
904            BillingAuthorityClass::KimiSubscription,
905            ModelNamespace::MoonshotKimi,
906        )
907    }
908
909    fn validate_kimi_api(&self) -> Result<(), RouteIdentityError> {
910        self.validate_follow_on_route(
911            ProviderService::MoonshotKimi,
912            MOONSHOT_ORIGIN,
913            MOONSHOT_CHAT_PATH,
914            CatalogSourceKind::KimiCatalog,
915            BillingAuthorityClass::KimiMeteredApi,
916            ModelNamespace::MoonshotKimi,
917        )
918    }
919
920    /// DeepSeek direct API route contract (PRD-001y S1). The `deepseek_api`
921    /// phase is a first-class OpenAI-compatible chat route against
922    /// `api.deepseek.com` with an opaque secret reference, matching the
923    /// follow-on provider contract. There is no DeepSeek subscription path
924    /// today, so this is the provider's single API phase.
925    fn validate_deepseek_api(&self) -> Result<(), RouteIdentityError> {
926        self.validate_follow_on_route(
927            ProviderService::Deepseek,
928            DEEPSEEK_ORIGIN,
929            DEEPSEEK_CHAT_PATH,
930            CatalogSourceKind::DeepseekCatalog,
931            BillingAuthorityClass::DeepseekMeteredApi,
932            ModelNamespace::Deepseek,
933        )
934    }
935
936    /// Claude/Anthropic route contract (PRD-001y S2). Both the `claude_subscription`
937    /// and `claude_api` phases are first-class direct HTTPS routes against the
938    /// published `api.anthropic.com/v1/messages` endpoint with an opaque secret
939    /// reference. The subscription and api phases differ only in billing
940    /// authority; the wire format, base URL, and `x-api-key`/`anthropic-version`
941    /// auth scheme were verified at build time against the Anthropic Messages
942    /// API documentation (no live call).
943    fn validate_claude_subscription(&self) -> Result<(), RouteIdentityError> {
944        self.validate_follow_on_route(
945            ProviderService::Claude,
946            ANTHROPIC_ORIGIN,
947            ANTHROPIC_CHAT_PATH,
948            CatalogSourceKind::ClaudeCatalog,
949            BillingAuthorityClass::ClaudeSubscription,
950            ModelNamespace::Claude,
951        )
952    }
953
954    fn validate_claude_api(&self) -> Result<(), RouteIdentityError> {
955        self.validate_follow_on_route(
956            ProviderService::Claude,
957            ANTHROPIC_ORIGIN,
958            ANTHROPIC_CHAT_PATH,
959            CatalogSourceKind::ClaudeCatalog,
960            BillingAuthorityClass::ClaudeMeteredApi,
961            ModelNamespace::Claude,
962        )
963    }
964
965    /// Gemini route contract (PRD-001y S2). Both Gemini phases are first-class
966    /// direct HTTPS routes against the published
967    /// `generativelanguage.googleapis.com/v1beta/models/{model}:generateContent`
968    /// endpoint with an opaque secret reference. The model id is a path
969    /// parameter, so the route endpoint path is the `{model}` template and the
970    /// adapter materializes it per request. Base URL, generateContent path, and
971    /// `x-goog-api-key` auth scheme were verified at build time against the
972    /// published documentation (no live call).
973    fn validate_gemini_subscription(&self) -> Result<(), RouteIdentityError> {
974        self.validate_gemini_route(BillingAuthorityClass::GeminiSubscription)
975    }
976
977    fn validate_gemini_api(&self) -> Result<(), RouteIdentityError> {
978        self.validate_gemini_route(BillingAuthorityClass::GeminiMeteredApi)
979    }
980
981    fn validate_gemini_route(
982        &self,
983        billing_authority: BillingAuthorityClass,
984    ) -> Result<(), RouteIdentityError> {
985        if self.provider != ProviderService::Gemini
986            || self.transport != TransportKind::DirectHttps
987            || self.endpoint.origin != GEMINI_ORIGIN
988            || self.endpoint.path != GEMINI_GENERATE_CONTENT_PATH
989            || self.credential_source != CredentialSourceKind::SecretRef
990            || self
991                .credential_ref
992                .as_deref()
993                .is_none_or(|value| !valid_secret_ref(value))
994            || self.catalog_source != CatalogSourceKind::GeminiCatalog
995            || self.billing_authority != billing_authority
996            || self.model_namespace != ModelNamespace::Gemini
997            || self.execution_location != ExecutionLocation::RemoteCloud
998        {
999            return Err(RouteIdentityError::AmbiguousIdentity);
1000        }
1001        Ok(())
1002    }
1003
1004    fn validate_extension(&self) -> Result<(), RouteIdentityError> {
1005        let expected = match self.phase {
1006            PhaseKind::DirectMoonshotKimiExtension => {
1007                (ProviderService::MoonshotKimi, ModelNamespace::MoonshotKimi)
1008            }
1009            PhaseKind::XaiGrokExtension => (ProviderService::Xai, ModelNamespace::Xai),
1010            _ => return Err(RouteIdentityError::ExtensionMustRemainUnplaced),
1011        };
1012        if self.placement != PhasePlacement::DisabledUnplacedExtension
1013            || self.provider != expected.0
1014            || self.model_namespace != expected.1
1015            || self.transport != TransportKind::DisabledExtension
1016            || self.credential_source != CredentialSourceKind::Unconfigured
1017            || self.credential_ref.is_some()
1018            || self.catalog_source != CatalogSourceKind::ExtensionCatalog
1019            || self.billing_authority != BillingAuthorityClass::ExtensionProvider
1020        {
1021            return Err(RouteIdentityError::ExtensionMustRemainUnplaced);
1022        }
1023        Ok(())
1024    }
1025}
1026
1027/// Derives the transport-specific Ollama Cloud wire model from a provider-neutral model.
1028///
1029/// Only the two approved Ollama Cloud transports are accepted. Provider-neutral names
1030/// may not already carry the relay-only suffix, preventing accidental double suffixes.
1031pub fn ollama_cloud_wire_model(
1032    provider_neutral_model: &str,
1033    transport: TransportKind,
1034) -> Result<String, RouteIdentityError> {
1035    if !valid_model_id(provider_neutral_model)
1036        || has_ollama_cloud_wire_suffix(provider_neutral_model)
1037    {
1038        return Err(RouteIdentityError::AmbiguousIdentity);
1039    }
1040    match transport {
1041        TransportKind::OllamaSignedInRelay => Ok(format!("{provider_neutral_model}:cloud")),
1042        TransportKind::DirectHttps => Ok(provider_neutral_model.to_owned()),
1043        _ => Err(RouteIdentityError::AmbiguousIdentity),
1044    }
1045}
1046
1047/// Returns the provider-neutral model represented by an exact transport wire ID.
1048///
1049/// Ollama's signed-in local relay exposes canonical Cloud aliases in both
1050/// `model:cloud` and `model-cloud` forms. Direct API routes remain bound to the
1051/// provider-neutral model and therefore reject either relay-only suffix.
1052pub fn ollama_cloud_provider_neutral_model(
1053    wire_model: &str,
1054    transport: TransportKind,
1055) -> Result<String, RouteIdentityError> {
1056    if !valid_model_id(wire_model) {
1057        return Err(RouteIdentityError::AmbiguousIdentity);
1058    }
1059    match transport {
1060        TransportKind::OllamaSignedInRelay => cloud_base_model_id(wire_model)
1061            .map(str::to_owned)
1062            .ok_or(RouteIdentityError::AmbiguousIdentity),
1063        TransportKind::DirectHttps if !has_ollama_cloud_wire_suffix(wire_model) => {
1064            Ok(wire_model.to_owned())
1065        }
1066        _ => Err(RouteIdentityError::AmbiguousIdentity),
1067    }
1068}
1069
1070fn valid_ollama_cloud_wire_model(model_id: &str, transport: TransportKind) -> bool {
1071    ollama_cloud_provider_neutral_model(model_id, transport).is_ok()
1072}
1073
1074fn cloud_base_model_id(value: &str) -> Option<&str> {
1075    let base = value
1076        .strip_suffix(":cloud")
1077        .or_else(|| value.strip_suffix("-cloud"))?;
1078    (!base.is_empty() && valid_model_id(base) && !has_ollama_cloud_wire_suffix(base))
1079        .then_some(base)
1080}
1081
1082fn has_ollama_cloud_wire_suffix(value: &str) -> bool {
1083    value.ends_with(":cloud") || value.ends_with("-cloud")
1084}
1085
1086fn valid_ref(value: &str) -> bool {
1087    !value.is_empty()
1088        && value.len() <= 256
1089        && value.bytes().all(|byte| {
1090            byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'/')
1091        })
1092}
1093
1094fn valid_model_id(value: &str) -> bool {
1095    valid_ref(value) && !value.contains("..")
1096}
1097
1098fn valid_secret_ref(value: &str) -> bool {
1099    if !valid_ref(value) {
1100        return false;
1101    }
1102    if let Some(name) = value.strip_prefix("env:") {
1103        // Match the resolver charset in provider-core so every env ref that
1104        // passes validation also resolves: ASCII alphanumeric plus underscore.
1105        return valid_environment_name(name);
1106    }
1107    [
1108        "local-file-v1:",
1109        "command:",
1110        "keychain:",
1111        "secret-service:",
1112        "credential-manager:",
1113    ]
1114    .iter()
1115    .any(|prefix| value.starts_with(prefix))
1116}
1117
1118/// Environment variable names are non-empty ASCII alphanumeric plus
1119/// underscore, at most 256 bytes. This mirrors `valid_environment_name` in
1120/// provider-core's secret resolver.
1121fn valid_environment_name(value: &str) -> bool {
1122    !value.is_empty()
1123        && value.len() <= 256
1124        && value
1125            .bytes()
1126            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132
1133    fn cloud_identity(transport: TransportKind, model_id: &str) -> RouteIdentity {
1134        let direct = transport == TransportKind::DirectHttps;
1135        RouteIdentity {
1136            phase: PhaseKind::OllamaCloud,
1137            placement: PhasePlacement::Builtin,
1138            provider: ProviderService::OllamaCloud,
1139            transport,
1140            endpoint: EndpointIdentity {
1141                origin: if direct {
1142                    OLLAMA_CLOUD_ORIGIN
1143                } else {
1144                    OLLAMA_RELAY_ORIGIN
1145                }
1146                .to_owned(),
1147                path: OLLAMA_CHAT_PATH.to_owned(),
1148            },
1149            credential_source: if direct {
1150                CredentialSourceKind::SecretRef
1151            } else {
1152                CredentialSourceKind::OllamaDaemonSession
1153            },
1154            credential_ref: direct.then(|| "keychain:ollama-cloud-device".to_owned()),
1155            credential_refs: Vec::new(),
1156            catalog_source: CatalogSourceKind::OllamaCloudCatalog,
1157            catalog_ref: "catalog:v2:ollama-cloud".to_owned(),
1158            billing_authority: BillingAuthorityClass::OllamaCloud,
1159            model_namespace: ModelNamespace::OllamaCloud,
1160            model_id: model_id.to_owned(),
1161            execution_location: ExecutionLocation::RemoteCloud,
1162            account_ref: "ollama-cloud-held".to_owned(),
1163            capacity_domain_ref: "capacity:ollama-cloud-held".to_owned(),
1164            local_execution_proof: false,
1165            approved_local_inventory: Vec::new(),
1166            reasoning_modes: vec!["reasoning".to_owned()],
1167        }
1168    }
1169
1170    #[test]
1171    fn ollama_cloud_wire_model_is_transport_specific_and_single_suffix() {
1172        assert_eq!(
1173            ollama_cloud_wire_model(OLLAMA_GLM_5_2_MODEL, TransportKind::OllamaSignedInRelay),
1174            Ok("glm-5.2:cloud".to_owned())
1175        );
1176        assert_eq!(
1177            ollama_cloud_wire_model(OLLAMA_GLM_5_2_MODEL, TransportKind::DirectHttps),
1178            Ok("glm-5.2".to_owned())
1179        );
1180        assert!(
1181            ollama_cloud_wire_model("glm-5.2:cloud", TransportKind::OllamaSignedInRelay).is_err()
1182        );
1183        assert!(
1184            ollama_cloud_wire_model("qwen3-coder:480b-cloud", TransportKind::OllamaSignedInRelay)
1185                .is_err()
1186        );
1187        assert!(
1188            ollama_cloud_wire_model(OLLAMA_GLM_5_2_MODEL, TransportKind::OllamaLocalDaemon)
1189                .is_err()
1190        );
1191
1192        assert!(
1193            cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2:cloud")
1194                .validate()
1195                .is_ok()
1196        );
1197        assert!(
1198            cloud_identity(TransportKind::OllamaSignedInRelay, "qwen3-coder:480b-cloud")
1199                .validate()
1200                .is_ok()
1201        );
1202        assert!(
1203            cloud_identity(TransportKind::OllamaSignedInRelay, "gpt-oss:120b-cloud")
1204                .validate()
1205                .is_ok()
1206        );
1207        assert!(
1208            cloud_identity(TransportKind::OllamaSignedInRelay, "kimi-k2.6:cloud")
1209                .validate()
1210                .is_ok()
1211        );
1212        assert!(
1213            cloud_identity(TransportKind::DirectHttps, "glm-5.2")
1214                .validate()
1215                .is_ok()
1216        );
1217        assert!(
1218            cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2:cloud:cloud")
1219                .validate()
1220                .is_err()
1221        );
1222        for invalid in [
1223            "model-cloud-cloud",
1224            "model:cloud-cloud",
1225            "model-cloud:cloud",
1226            "model:cloud:cloud",
1227        ] {
1228            assert!(
1229                cloud_identity(TransportKind::OllamaSignedInRelay, invalid)
1230                    .validate()
1231                    .is_err(),
1232                "accepted non-canonical double suffix {invalid}"
1233            );
1234        }
1235        assert!(
1236            cloud_identity(TransportKind::DirectHttps, "glm-5.2:cloud")
1237                .validate()
1238                .is_err()
1239        );
1240        assert!(
1241            cloud_identity(TransportKind::DirectHttps, "qwen3-coder:480b-cloud")
1242                .validate()
1243                .is_err()
1244        );
1245        assert_eq!(
1246            ollama_cloud_provider_neutral_model(
1247                "qwen3-coder:480b-cloud",
1248                TransportKind::OllamaSignedInRelay
1249            ),
1250            Ok("qwen3-coder:480b".to_owned())
1251        );
1252        assert_eq!(
1253            ollama_cloud_provider_neutral_model("glm-5.2", TransportKind::DirectHttps),
1254            Ok("glm-5.2".to_owned())
1255        );
1256    }
1257
1258    #[test]
1259    fn multi_agent_capability_projection_matches_phase_kind() {
1260        assert_eq!(
1261            PhaseKind::OpenaiSubscription.multi_agent_capability(),
1262            MultiAgentCapability::Native
1263        );
1264        assert!(
1265            PhaseKind::OpenaiSubscription
1266                .multi_agent_capability()
1267                .serves_subagents_natively()
1268        );
1269        for phase in [
1270            PhaseKind::OllamaCloud,
1271            PhaseKind::OpenrouterApi,
1272            PhaseKind::OllamaLocal,
1273            PhaseKind::DirectMoonshotKimiExtension,
1274            PhaseKind::XaiGrokExtension,
1275        ] {
1276            assert_eq!(
1277                phase.multi_agent_capability(),
1278                MultiAgentCapability::NamespaceUnsupported
1279            );
1280            assert!(!phase.multi_agent_capability().serves_subagents_natively());
1281        }
1282    }
1283
1284    #[test]
1285    fn disabled_extension_route_is_never_native() {
1286        let mut identity = cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2");
1287        identity.placement = PhasePlacement::DisabledUnplacedExtension;
1288        assert_eq!(
1289            identity.multi_agent_capability(),
1290            MultiAgentCapability::Disabled
1291        );
1292        assert!(
1293            !identity
1294                .multi_agent_capability()
1295                .serves_subagents_natively()
1296        );
1297    }
1298
1299    #[test]
1300    fn mav2_projection_is_per_route_and_matches_phase_kind() {
1301        let openai = RouteIdentity {
1302            phase: PhaseKind::OpenaiSubscription,
1303            placement: PhasePlacement::Builtin,
1304            provider: ProviderService::Openai,
1305            transport: TransportKind::HarnessManaged,
1306            endpoint: EndpointIdentity {
1307                origin: "harness://codex-app-server".to_owned(),
1308                path: "/responses".to_owned(),
1309            },
1310            credential_source: CredentialSourceKind::HarnessManaged,
1311            credential_ref: None,
1312            credential_refs: Vec::new(),
1313            catalog_source: CatalogSourceKind::OpenaiHarnessCatalog,
1314            catalog_ref: "catalog:v2:openai-harness".to_owned(),
1315            billing_authority: BillingAuthorityClass::OpenaiSubscription,
1316            model_namespace: ModelNamespace::Openai,
1317            model_id: "gpt-5.6-sol".to_owned(),
1318            execution_location: ExecutionLocation::RemoteCloud,
1319            account_ref: "openai-subscription-held".to_owned(),
1320            capacity_domain_ref: "capacity:openai-subscription-held".to_owned(),
1321            local_execution_proof: false,
1322            approved_local_inventory: Vec::new(),
1323            reasoning_modes: vec!["reasoning".to_owned()],
1324        };
1325        assert!(openai.validate().is_ok());
1326        let openai_projection = openai.multi_agent_v2_projection();
1327        assert_eq!(
1328            openai_projection,
1329            MultiAgentCapabilityProjection {
1330                namespace_tools: true,
1331                multi_agent_version: MultiAgentVersion::V2,
1332                wire_api: MultiAgentWireApi::Responses,
1333                observed_namespace_calls: ObservedNamespaceCallBehavior::ReturnsNamespaceField,
1334            }
1335        );
1336        assert_eq!(
1337            openai.mav2_namespace_choice(),
1338            MultiAgentNamespace::Collaboration
1339        );
1340        assert_eq!(openai.mav2_namespace_choice().as_str(), "collaboration");
1341
1342        let cloud = cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2:cloud");
1343        let cloud_projection = cloud.multi_agent_v2_projection();
1344        assert_eq!(
1345            cloud_projection,
1346            MultiAgentCapabilityProjection {
1347                namespace_tools: false,
1348                multi_agent_version: MultiAgentVersion::V2,
1349                wire_api: MultiAgentWireApi::Responses,
1350                observed_namespace_calls: ObservedNamespaceCallBehavior::BareNameNoNamespace,
1351            }
1352        );
1353        assert_eq!(cloud.mav2_namespace_choice(), MultiAgentNamespace::Agents);
1354        assert_eq!(cloud.mav2_namespace_choice().as_str(), "agents");
1355    }
1356
1357    #[test]
1358    fn mav2_projection_for_disabled_extension_is_never_namespace_capable() {
1359        let mut identity = cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2");
1360        identity.placement = PhasePlacement::DisabledUnplacedExtension;
1361        assert_eq!(
1362            identity.multi_agent_v2_projection(),
1363            MultiAgentCapabilityProjection {
1364                namespace_tools: false,
1365                multi_agent_version: MultiAgentVersion::Disabled,
1366                wire_api: MultiAgentWireApi::Responses,
1367                observed_namespace_calls: ObservedNamespaceCallBehavior::Disabled,
1368            }
1369        );
1370        assert_eq!(
1371            identity.multi_agent_version_served(),
1372            MultiAgentVersion::Disabled
1373        );
1374    }
1375
1376    #[test]
1377    fn credential_pool_invariant_rejects_invalid_entries() {
1378        // A valid pool with the legacy fallback as the first entry is accepted.
1379        let mut valid = cloud_identity(TransportKind::DirectHttps, "glm-5.2");
1380        valid.credential_refs = vec![
1381            "keychain:ollama-cloud-device".to_owned(),
1382            "keychain:ollama-cloud-device-2".to_owned(),
1383        ];
1384        assert!(valid.validate().is_ok());
1385
1386        // Duplicate pool entries are rejected.
1387        let mut duplicate = cloud_identity(TransportKind::DirectHttps, "glm-5.2");
1388        duplicate.credential_refs = vec![
1389            "keychain:ollama-cloud-device".to_owned(),
1390            "keychain:ollama-cloud-device".to_owned(),
1391        ];
1392        assert!(duplicate.validate().is_err());
1393
1394        // A non-secret-ref pool entry is rejected.
1395        let mut invalid = cloud_identity(TransportKind::DirectHttps, "glm-5.2");
1396        invalid.credential_refs = vec!["not-a-secret-ref".to_owned()];
1397        assert!(invalid.validate().is_err());
1398
1399        // A pool whose legacy fallback is not the first entry is rejected.
1400        let mut fallback = cloud_identity(TransportKind::DirectHttps, "glm-5.2");
1401        fallback.credential_refs = vec![
1402            "keychain:ollama-cloud-device".to_owned(),
1403            "keychain:ollama-cloud-device-2".to_owned(),
1404        ];
1405        fallback.credential_ref = Some("keychain:ollama-cloud-device-2".to_owned());
1406        assert!(fallback.validate().is_err());
1407
1408        // A populated pool on a phase that requires credential_ref == None is
1409        // rejected (Ollama Cloud signed-in relay).
1410        let mut relay = cloud_identity(TransportKind::OllamaSignedInRelay, "glm-5.2:cloud");
1411        relay.credential_refs = vec!["keychain:ollama-cloud-device".to_owned()];
1412        assert!(relay.validate().is_err());
1413    }
1414
1415    fn follow_on_identity(
1416        phase: PhaseKind,
1417        provider: ProviderService,
1418        origin: &str,
1419        path: &str,
1420        catalog_source: CatalogSourceKind,
1421        billing_authority: BillingAuthorityClass,
1422        namespace: ModelNamespace,
1423    ) -> RouteIdentity {
1424        RouteIdentity {
1425            phase,
1426            placement: PhasePlacement::Builtin,
1427            provider,
1428            transport: TransportKind::DirectHttps,
1429            endpoint: EndpointIdentity {
1430                origin: origin.to_owned(),
1431                path: path.to_owned(),
1432            },
1433            credential_source: CredentialSourceKind::SecretRef,
1434            credential_ref: Some("env:STEADYMUX_FOLLOW_ON_KEY".to_owned()),
1435            credential_refs: vec!["env:STEADYMUX_FOLLOW_ON_KEY".to_owned()],
1436            catalog_source,
1437            catalog_ref: "catalog:v2:follow-on".to_owned(),
1438            billing_authority,
1439            model_namespace: namespace,
1440            model_id: "follow-on/fixture".to_owned(),
1441            execution_location: ExecutionLocation::RemoteCloud,
1442            account_ref: "account:follow-on".to_owned(),
1443            capacity_domain_ref: "capacity:follow-on".to_owned(),
1444            local_execution_proof: false,
1445            approved_local_inventory: Vec::new(),
1446            reasoning_modes: Vec::new(),
1447        }
1448    }
1449
1450    #[test]
1451    fn follow_on_provider_phases_validate_as_first_class_routes() {
1452        let cases = [
1453            (
1454                PhaseKind::QwenSubscription,
1455                ProviderService::Qwen,
1456                DASHSCOPE_ORIGIN,
1457                DASHSCOPE_CHAT_PATH,
1458                CatalogSourceKind::QwenCatalog,
1459                BillingAuthorityClass::QwenSubscription,
1460                ModelNamespace::Qwen,
1461            ),
1462            (
1463                PhaseKind::QwenApi,
1464                ProviderService::Qwen,
1465                DASHSCOPE_ORIGIN,
1466                DASHSCOPE_CHAT_PATH,
1467                CatalogSourceKind::QwenCatalog,
1468                BillingAuthorityClass::QwenMeteredApi,
1469                ModelNamespace::Qwen,
1470            ),
1471            (
1472                PhaseKind::QwenApi,
1473                ProviderService::Qwen,
1474                DASHSCOPE_TOKEN_PLAN_ORIGIN,
1475                DASHSCOPE_CHAT_PATH,
1476                CatalogSourceKind::QwenCatalog,
1477                BillingAuthorityClass::QwenMeteredApi,
1478                ModelNamespace::Qwen,
1479            ),
1480            (
1481                PhaseKind::GrokSubscription,
1482                ProviderService::Grok,
1483                XAI_ORIGIN,
1484                XAI_CHAT_PATH,
1485                CatalogSourceKind::GrokCatalog,
1486                BillingAuthorityClass::GrokSubscription,
1487                ModelNamespace::Grok,
1488            ),
1489            (
1490                PhaseKind::GrokApi,
1491                ProviderService::Grok,
1492                XAI_ORIGIN,
1493                XAI_CHAT_PATH,
1494                CatalogSourceKind::GrokCatalog,
1495                BillingAuthorityClass::GrokMeteredApi,
1496                ModelNamespace::Grok,
1497            ),
1498            (
1499                PhaseKind::ZaiSubscription,
1500                ProviderService::Zai,
1501                ZAI_ORIGIN,
1502                ZAI_CHAT_PATH,
1503                CatalogSourceKind::ZaiCatalog,
1504                BillingAuthorityClass::ZaiSubscription,
1505                ModelNamespace::Zai,
1506            ),
1507            (
1508                PhaseKind::ZaiApi,
1509                ProviderService::Zai,
1510                ZAI_ORIGIN,
1511                ZAI_CHAT_PATH,
1512                CatalogSourceKind::ZaiCatalog,
1513                BillingAuthorityClass::ZaiMeteredApi,
1514                ModelNamespace::Zai,
1515            ),
1516            (
1517                PhaseKind::KimiSubscription,
1518                ProviderService::MoonshotKimi,
1519                MOONSHOT_ORIGIN,
1520                MOONSHOT_CHAT_PATH,
1521                CatalogSourceKind::KimiCatalog,
1522                BillingAuthorityClass::KimiSubscription,
1523                ModelNamespace::MoonshotKimi,
1524            ),
1525            (
1526                PhaseKind::KimiApi,
1527                ProviderService::MoonshotKimi,
1528                MOONSHOT_ORIGIN,
1529                MOONSHOT_CHAT_PATH,
1530                CatalogSourceKind::KimiCatalog,
1531                BillingAuthorityClass::KimiMeteredApi,
1532                ModelNamespace::MoonshotKimi,
1533            ),
1534            (
1535                PhaseKind::DeepseekApi,
1536                ProviderService::Deepseek,
1537                DEEPSEEK_ORIGIN,
1538                DEEPSEEK_CHAT_PATH,
1539                CatalogSourceKind::DeepseekCatalog,
1540                BillingAuthorityClass::DeepseekMeteredApi,
1541                ModelNamespace::Deepseek,
1542            ),
1543            (
1544                PhaseKind::ClaudeSubscription,
1545                ProviderService::Claude,
1546                ANTHROPIC_ORIGIN,
1547                ANTHROPIC_CHAT_PATH,
1548                CatalogSourceKind::ClaudeCatalog,
1549                BillingAuthorityClass::ClaudeSubscription,
1550                ModelNamespace::Claude,
1551            ),
1552            (
1553                PhaseKind::ClaudeApi,
1554                ProviderService::Claude,
1555                ANTHROPIC_ORIGIN,
1556                ANTHROPIC_CHAT_PATH,
1557                CatalogSourceKind::ClaudeCatalog,
1558                BillingAuthorityClass::ClaudeMeteredApi,
1559                ModelNamespace::Claude,
1560            ),
1561            (
1562                PhaseKind::GeminiSubscription,
1563                ProviderService::Gemini,
1564                GEMINI_ORIGIN,
1565                GEMINI_GENERATE_CONTENT_PATH,
1566                CatalogSourceKind::GeminiCatalog,
1567                BillingAuthorityClass::GeminiSubscription,
1568                ModelNamespace::Gemini,
1569            ),
1570            (
1571                PhaseKind::GeminiApi,
1572                ProviderService::Gemini,
1573                GEMINI_ORIGIN,
1574                GEMINI_GENERATE_CONTENT_PATH,
1575                CatalogSourceKind::GeminiCatalog,
1576                BillingAuthorityClass::GeminiMeteredApi,
1577                ModelNamespace::Gemini,
1578            ),
1579        ];
1580        for (phase, provider, origin, path, catalog, billing, namespace) in cases {
1581            let identity =
1582                follow_on_identity(phase, provider, origin, path, catalog, billing, namespace);
1583            assert!(identity.validate().is_ok(), "{phase:?} should validate");
1584            assert!(identity.phase.is_builtin(), "{phase:?} is first-class");
1585            assert_eq!(
1586                identity.phase.multi_agent_capability(),
1587                MultiAgentCapability::NamespaceUnsupported
1588            );
1589            assert_eq!(
1590                identity
1591                    .multi_agent_v2_projection()
1592                    .observed_namespace_calls,
1593                ObservedNamespaceCallBehavior::BareNameNoNamespace
1594            );
1595            assert_eq!(identity.multi_agent_version_served(), MultiAgentVersion::V2);
1596            assert_eq!(
1597                identity.mav2_namespace_choice(),
1598                MultiAgentNamespace::Agents
1599            );
1600        }
1601    }
1602
1603    #[test]
1604    fn claude_routes_validate_with_verified_anthropic_wire_contract() {
1605        // PRD-001y AC-05: the `claude_subscription` and `claude_api` phases are
1606        // first-class builtin routes against the published
1607        // `https://api.anthropic.com/v1/messages` endpoint with an opaque
1608        // secret reference, verified at build time against the Anthropic
1609        // Messages API documentation (no live call). The `x-api-key` and
1610        // `anthropic-version` headers and the pinned version are part of the
1611        // contract.
1612        for (phase, billing) in [
1613            (
1614                PhaseKind::ClaudeSubscription,
1615                BillingAuthorityClass::ClaudeSubscription,
1616            ),
1617            (
1618                PhaseKind::ClaudeApi,
1619                BillingAuthorityClass::ClaudeMeteredApi,
1620            ),
1621        ] {
1622            let identity = follow_on_identity(
1623                phase,
1624                ProviderService::Claude,
1625                ANTHROPIC_ORIGIN,
1626                ANTHROPIC_CHAT_PATH,
1627                CatalogSourceKind::ClaudeCatalog,
1628                billing,
1629                ModelNamespace::Claude,
1630            );
1631            assert!(identity.validate().is_ok());
1632            assert!(identity.phase.is_builtin(), "Claude is first-class");
1633            assert_eq!(
1634                identity.multi_agent_v2_projection(),
1635                MultiAgentCapabilityProjection {
1636                    namespace_tools: false,
1637                    multi_agent_version: MultiAgentVersion::V2,
1638                    wire_api: MultiAgentWireApi::Responses,
1639                    observed_namespace_calls: ObservedNamespaceCallBehavior::BareNameNoNamespace,
1640                }
1641            );
1642            assert_eq!(
1643                identity.mav2_namespace_choice(),
1644                MultiAgentNamespace::Agents
1645            );
1646        }
1647
1648        // The verified wire contract: base URL, path, and auth headers.
1649        assert_eq!(ANTHROPIC_ORIGIN, "https://api.anthropic.com");
1650        assert_eq!(ANTHROPIC_CHAT_PATH, "/v1/messages");
1651        assert_eq!(ANTHROPIC_API_KEY_HEADER, "x-api-key");
1652        assert_eq!(ANTHROPIC_VERSION_HEADER, "anthropic-version");
1653        assert_eq!(ANTHROPIC_VERSION, "2023-06-01");
1654        assert_eq!(PhaseId::CLAUDE_SUBSCRIPTION, "claude_subscription");
1655        assert_eq!(PhaseId::CLAUDE_API, "claude_api");
1656
1657        // A wrong origin or billing authority is rejected.
1658        let mut wrong_origin = follow_on_identity(
1659            PhaseKind::ClaudeApi,
1660            ProviderService::Claude,
1661            ANTHROPIC_ORIGIN,
1662            ANTHROPIC_CHAT_PATH,
1663            CatalogSourceKind::ClaudeCatalog,
1664            BillingAuthorityClass::ClaudeMeteredApi,
1665            ModelNamespace::Claude,
1666        );
1667        wrong_origin.endpoint = EndpointIdentity {
1668            origin: "https://api.anthropic.example.com".to_owned(),
1669            path: ANTHROPIC_CHAT_PATH.to_owned(),
1670        };
1671        assert!(wrong_origin.validate().is_err());
1672        let mut wrong_billing = follow_on_identity(
1673            PhaseKind::ClaudeApi,
1674            ProviderService::Claude,
1675            ANTHROPIC_ORIGIN,
1676            ANTHROPIC_CHAT_PATH,
1677            CatalogSourceKind::ClaudeCatalog,
1678            BillingAuthorityClass::ClaudeMeteredApi,
1679            ModelNamespace::Claude,
1680        );
1681        wrong_billing.billing_authority = BillingAuthorityClass::ClaudeSubscription;
1682        assert!(wrong_billing.validate().is_err());
1683    }
1684
1685    #[test]
1686    fn gemini_routes_and_verified_generate_content_wire_contract() {
1687        // PRD-001y AC-06: the `gemini_subscription` and `gemini_api` phases are
1688        // first-class builtin routes against the published
1689        // `generativelanguage.googleapis.com/v1beta/models/{model}:generateContent`
1690        // endpoint with an opaque secret reference and the `x-goog-api-key`
1691        // auth scheme, verified at build time (no live call). The model id is a
1692        // path parameter, so the route path is the `{model}` template.
1693        for (phase, billing) in [
1694            (
1695                PhaseKind::GeminiSubscription,
1696                BillingAuthorityClass::GeminiSubscription,
1697            ),
1698            (
1699                PhaseKind::GeminiApi,
1700                BillingAuthorityClass::GeminiMeteredApi,
1701            ),
1702        ] {
1703            let identity = follow_on_identity(
1704                phase,
1705                ProviderService::Gemini,
1706                GEMINI_ORIGIN,
1707                GEMINI_GENERATE_CONTENT_PATH,
1708                CatalogSourceKind::GeminiCatalog,
1709                billing,
1710                ModelNamespace::Gemini,
1711            );
1712            assert!(identity.validate().is_ok());
1713            assert!(identity.phase.is_builtin(), "Gemini is first-class");
1714            assert_eq!(
1715                identity.multi_agent_v2_projection(),
1716                MultiAgentCapabilityProjection {
1717                    namespace_tools: false,
1718                    multi_agent_version: MultiAgentVersion::V2,
1719                    wire_api: MultiAgentWireApi::Responses,
1720                    observed_namespace_calls: ObservedNamespaceCallBehavior::BareNameNoNamespace,
1721                }
1722            );
1723            assert_eq!(
1724                identity.mav2_namespace_choice(),
1725                MultiAgentNamespace::Agents
1726            );
1727        }
1728
1729        // The verified wire contract: base URL, generateContent path template,
1730        // and the API-key header.
1731        assert_eq!(GEMINI_ORIGIN, "https://generativelanguage.googleapis.com");
1732        assert_eq!(
1733            GEMINI_GENERATE_CONTENT_PATH,
1734            "/v1beta/models/{model}:generateContent"
1735        );
1736        assert_eq!(GEMINI_API_KEY_HEADER, "x-goog-api-key");
1737        assert_eq!(PhaseId::GEMINI_SUBSCRIPTION, "gemini_subscription");
1738        assert_eq!(PhaseId::GEMINI_API, "gemini_api");
1739
1740        // A wrong origin, a non-template path, or a wrong billing authority is
1741        // rejected.
1742        let mut wrong_origin = follow_on_identity(
1743            PhaseKind::GeminiApi,
1744            ProviderService::Gemini,
1745            GEMINI_ORIGIN,
1746            GEMINI_GENERATE_CONTENT_PATH,
1747            CatalogSourceKind::GeminiCatalog,
1748            BillingAuthorityClass::GeminiMeteredApi,
1749            ModelNamespace::Gemini,
1750        );
1751        wrong_origin.endpoint.origin = "https://generativelanguage.example.com".to_owned();
1752        assert!(wrong_origin.validate().is_err());
1753        let mut wrong_path = follow_on_identity(
1754            PhaseKind::GeminiApi,
1755            ProviderService::Gemini,
1756            GEMINI_ORIGIN,
1757            GEMINI_GENERATE_CONTENT_PATH,
1758            CatalogSourceKind::GeminiCatalog,
1759            BillingAuthorityClass::GeminiMeteredApi,
1760            ModelNamespace::Gemini,
1761        );
1762        wrong_path.endpoint.path = "/v1beta/models/gemini-2.5-flash:generateContent".to_owned();
1763        assert!(
1764            wrong_path.validate().is_err(),
1765            "the concrete model path is a template the adapter materializes"
1766        );
1767        let mut wrong_billing = follow_on_identity(
1768            PhaseKind::GeminiApi,
1769            ProviderService::Gemini,
1770            GEMINI_ORIGIN,
1771            GEMINI_GENERATE_CONTENT_PATH,
1772            CatalogSourceKind::GeminiCatalog,
1773            BillingAuthorityClass::GeminiMeteredApi,
1774            ModelNamespace::Gemini,
1775        );
1776        wrong_billing.billing_authority = BillingAuthorityClass::GeminiSubscription;
1777        assert!(wrong_billing.validate().is_err());
1778    }
1779
1780    #[test]
1781    fn follow_on_phase_rejects_wrong_provider_or_billing() {
1782        let mut identity = follow_on_identity(
1783            PhaseKind::QwenApi,
1784            ProviderService::Qwen,
1785            DASHSCOPE_ORIGIN,
1786            DASHSCOPE_CHAT_PATH,
1787            CatalogSourceKind::QwenCatalog,
1788            BillingAuthorityClass::QwenMeteredApi,
1789            ModelNamespace::Qwen,
1790        );
1791        assert!(identity.validate().is_ok());
1792        identity.provider = ProviderService::Zai;
1793        assert!(identity.validate().is_err());
1794        let mut billing = follow_on_identity(
1795            PhaseKind::QwenApi,
1796            ProviderService::Qwen,
1797            DASHSCOPE_ORIGIN,
1798            DASHSCOPE_CHAT_PATH,
1799            CatalogSourceKind::QwenCatalog,
1800            BillingAuthorityClass::QwenMeteredApi,
1801            ModelNamespace::Qwen,
1802        );
1803        billing.billing_authority = BillingAuthorityClass::QwenSubscription;
1804        assert!(billing.validate().is_err());
1805        let mut missing = follow_on_identity(
1806            PhaseKind::KimiApi,
1807            ProviderService::MoonshotKimi,
1808            MOONSHOT_ORIGIN,
1809            MOONSHOT_CHAT_PATH,
1810            CatalogSourceKind::KimiCatalog,
1811            BillingAuthorityClass::KimiMeteredApi,
1812            ModelNamespace::MoonshotKimi,
1813        );
1814        missing.credential_ref = None;
1815        missing.credential_refs = Vec::new();
1816        assert!(missing.validate().is_err());
1817    }
1818
1819    #[test]
1820    fn deepseek_api_route_is_first_class_openai_compat() {
1821        // The `deepseek_api` phase is a first-class builtin route against the
1822        // published `api.deepseek.com` origin and `/chat/completions` path
1823        // (build-time verification only, PRD-001y AC-02).
1824        let identity = follow_on_identity(
1825            PhaseKind::DeepseekApi,
1826            ProviderService::Deepseek,
1827            DEEPSEEK_ORIGIN,
1828            DEEPSEEK_CHAT_PATH,
1829            CatalogSourceKind::DeepseekCatalog,
1830            BillingAuthorityClass::DeepseekMeteredApi,
1831            ModelNamespace::Deepseek,
1832        );
1833        assert!(identity.validate().is_ok());
1834        assert!(identity.phase.is_builtin(), "DeepSeek API is first-class");
1835        assert_eq!(
1836            identity.phase.multi_agent_capability(),
1837            MultiAgentCapability::NamespaceUnsupported
1838        );
1839        assert_eq!(
1840            identity.multi_agent_v2_projection(),
1841            MultiAgentCapabilityProjection {
1842                namespace_tools: false,
1843                multi_agent_version: MultiAgentVersion::V2,
1844                wire_api: MultiAgentWireApi::Responses,
1845                observed_namespace_calls: ObservedNamespaceCallBehavior::BareNameNoNamespace,
1846            }
1847        );
1848        assert_eq!(
1849            identity.mav2_namespace_choice(),
1850            MultiAgentNamespace::Agents
1851        );
1852
1853        // The verified wire contract: base URL `https://api.deepseek.com`,
1854        // chat path `/chat/completions`, bearer auth.
1855        assert_eq!(DEEPSEEK_ORIGIN, "https://api.deepseek.com");
1856        assert_eq!(DEEPSEEK_CHAT_PATH, "/chat/completions");
1857        assert_eq!(PhaseId::DEEPSEEK_API, "deepseek_api");
1858
1859        // A wrong origin or billing authority is rejected.
1860        let mut wrong_origin = identity.clone();
1861        wrong_origin.endpoint = EndpointIdentity {
1862            origin: "https://api.deepseek.example.com".to_owned(),
1863            path: DEEPSEEK_CHAT_PATH.to_owned(),
1864        };
1865        assert!(wrong_origin.validate().is_err());
1866        let mut wrong_billing = identity;
1867        wrong_billing.billing_authority = BillingAuthorityClass::KimiMeteredApi;
1868        assert!(wrong_billing.validate().is_err());
1869    }
1870
1871    #[test]
1872    fn env_secret_refs_restrict_to_resolver_charset() {
1873        // `env:` names must match the provider-core resolver charset: ASCII
1874        // alphanumeric plus underscore. Dashes, dots, and slashes are rejected
1875        // here so a validated ref always resolves.
1876        for invalid in ["env:has-dash", "env:a.b", "env:a/b", "env:"] {
1877            assert!(
1878                !valid_secret_ref(invalid),
1879                "{invalid} should be rejected as an env secret ref"
1880            );
1881        }
1882        assert!(valid_secret_ref("env:MY_VAR"));
1883        assert!(valid_secret_ref("env:STEADYMUX_FOLLOW_ON_KEY"));
1884        assert!(valid_secret_ref("env:A1_B2"));
1885        // Non-env prefixes keep their existing charset.
1886        assert!(valid_secret_ref("keychain:service/account"));
1887        assert!(valid_secret_ref("command:fetch-token"));
1888    }
1889}