Skip to main content

cookie_agent_models/manager/
mod.rs

1//! Atomic dynamic model runtime construction and provider-store orchestration.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    fmt,
6    sync::{Arc, Mutex},
7};
8
9use arc_swap::ArcSwap;
10use cookie_agent_identity::{
11    AuthFieldName, AuthMethodId, AuthParameterId, CatalogRevision, ModelKey, ModelRevision,
12    ModelSelection, ProtocolRecipeId, ProviderId, ProviderRecipeId, ProviderSetupRecipeId,
13    ProviderStateRevision, RecipeCompilerVersion, RuntimeRevision, SetupFieldId,
14};
15use cookie_agent_protocol as protocol;
16use oven_sdk::{
17    AdapterId, CancellationCapability as OvenCancellation, Capability,
18    CompactionCapability as OvenCompaction, LanguageModel, LanguageModelDescriptor,
19    MediaCapabilities, MediaInputSupport, MediaSourceSupport, Modalities, Modality as OvenModality,
20    ModelCapabilities as OvenCapabilities, ModelId, ModelIdentity, ModelLimits,
21    ProviderId as OvenProviderId, ReplayCapability as OvenReplay, ReplayDeclaration, ReplayPolicy,
22    Request,
23};
24use serde::Serialize;
25use serde_json::{Value, json};
26use sha2::{Digest as _, Sha256};
27use thiserror::Error;
28
29use crate::{
30    BoundedSetupString, ProviderDefinition, SafeSetupValue, SecretString, Sha256Digest,
31    adapters::{
32        CacheStrategyConfig, GoogleCacheMode, OpenAiCacheMode, OpenAiPromptCacheRetention,
33        OpenAiPromptCacheTtl, OvenAdapterFamily,
34        oven::{AnthropicCacheStrategyConfig, AnthropicCacheTtlConfig, ModelBuildError},
35    },
36    authoring::{AuthOverride, ModelsDevProvider},
37    catalog::CatalogSnapshot,
38    compiler::{
39        AuthSourceCategory, CompiledAuthShape, CompiledDynamicModel, CompiledModelStatus,
40        DynamicCompileError, DynamicCompiler, ExecutableBehaviorInput,
41        ExecutableCredentialMaterial, compile_executable, managed_provider_adapter,
42        validate_managed_cache,
43    },
44    manifests::{
45        CompiledSafeModelBlueprint, FrozenAuthParameterValue, FrozenCredentialBinding,
46        FrozenCredentialSource, FrozenProviderSource, FrozenResolvedRequestDefaults,
47        FrozenSetupBinding, FrozenVariantBlueprint, HeaderName, ModelSnapshotPayloadV1,
48        NormalizedDecimal, SafeEndpointIdentity, SafeStaticHeaderValue, behavior_fingerprint,
49        blueprint_fingerprint, canonical_state_fingerprint, selected_behavior,
50        selection_fingerprint,
51    },
52    provider_store::{
53        ClientConnectId, ClientRequestId, ConnectMutation, ConnectProposal, DisconnectMutation,
54        DisconnectProposal, DurableConnectionDescriptor, ProviderAuthValues,
55        ProviderConnectionGeneration, ProviderStore, ProviderStoreError, ProviderStoreMutation,
56        ProviderStoreSnapshot, SafePolicyString, StoredManagedConnection,
57        StoredProviderPolicyProjection,
58    },
59    recipes::{
60        COMPILER_VERSION, FamilyRecipe, FamilyRecipeRegistry, SetupRecipe, auth_method,
61        family_registry, validate_setup,
62    },
63};
64
65/// Exact credential category selected after authored/store/no-auth precedence.
66#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
67#[serde(rename_all = "snake_case")]
68pub enum EffectiveCredentialSource {
69    AuthoredApiKey,
70    AuthoredOverride,
71    ProviderStore,
72    NoAuth,
73    Unavailable,
74}
75
76/// Current catalog presence for one managed provider row.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum ProviderPresence {
79    Current,
80    Removed,
81}
82
83/// Current Registry-1 verdict for a removed provider's retained store policy.
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum RetainedFamilyMatch {
86    SupportedRemoved,
87    RemovedWithoutRetainedFamilyMatch,
88}
89
90impl RetainedFamilyMatch {
91    #[must_use]
92    pub const fn reason(self) -> Option<&'static str> {
93        match self {
94            Self::SupportedRemoved => None,
95            Self::RemovedWithoutRetainedFamilyMatch => {
96                Some("removed_without_retained_recipe_match")
97            }
98        }
99    }
100}
101
102/// Safe current provider row, including configured providers removed from the catalog.
103#[derive(Clone, Debug)]
104pub struct CompiledProviderState {
105    pub id: ProviderId,
106    pub display_name: String,
107    pub presence: ProviderPresence,
108    pub support_reason: Option<String>,
109    pub retained_family_match: Option<RetainedFamilyMatch>,
110    pub authored: bool,
111    pub stored: bool,
112    pub effective_auth: EffectiveCredentialSource,
113    pub durable_connection: Option<DurableConnectionDescriptor>,
114}
115
116/// One complete compiled model plus its exact safe source and credential bindings.
117#[derive(Clone)]
118pub struct CompiledRuntimeModel {
119    pub key: ModelKey,
120    pub model: CompiledDynamicModel,
121    pub source: RuntimeProviderSource,
122    pub config_override_fingerprint: Sha256Digest,
123    pub setup_values: BTreeMap<SetupFieldId, SafeSetupValue>,
124    pub setup_fingerprint: Sha256Digest,
125    pub setup_recipe: ProviderSetupRecipeId,
126    pub credential_source: EffectiveCredentialSource,
127    pub static_headers: BTreeMap<crate::HeaderName, crate::SafeStaticHeaderValue>,
128    executable: Option<ExecutableBehavior>,
129    variant_executables: BTreeMap<cookie_agent_identity::VariantId, ExecutableBehavior>,
130}
131
132#[derive(Clone)]
133struct ExecutableBehavior {
134    adapter: OvenAdapterFamily,
135    model: Arc<dyn LanguageModel>,
136    defaults: crate::ResolvedRequestDefaults,
137    provider_options: oven_sdk::ProviderOptions,
138    behavior_fingerprint: Sha256Digest,
139}
140
141/// Exact executable model behavior retained by one runtime publication.
142#[derive(Clone)]
143pub struct ResolvedExecutableModel {
144    selection: ModelSelection,
145    model: Arc<dyn LanguageModel>,
146    defaults: crate::ResolvedRequestDefaults,
147    provider_options: oven_sdk::ProviderOptions,
148    behavior_fingerprint: Sha256Digest,
149    adapter: OvenAdapterFamily,
150}
151
152impl ResolvedExecutableModel {
153    #[must_use]
154    pub fn selection(&self) -> &ModelSelection {
155        &self.selection
156    }
157
158    #[must_use]
159    pub fn model(&self) -> &Arc<dyn LanguageModel> {
160        &self.model
161    }
162
163    #[must_use]
164    pub fn behavior_fingerprint(&self) -> &Sha256Digest {
165        &self.behavior_fingerprint
166    }
167
168    #[must_use]
169    pub const fn adapter_family(&self) -> OvenAdapterFamily {
170        self.adapter
171    }
172
173    #[must_use]
174    pub fn prepare_request(&self, request: Request) -> Request {
175        self.prepare_request_inner(request, None)
176    }
177
178    #[must_use]
179    pub fn prepare_request_with_cache_strategy(
180        &self,
181        request: Request,
182        strategy: Option<&CacheStrategyConfig>,
183    ) -> Request {
184        self.prepare_request_inner(request, Some(strategy))
185    }
186
187    /// Applies model defaults while deferring prompt-cache placement until interception completes.
188    #[must_use]
189    pub fn prepare_request_before_cache_strategy(
190        &self,
191        request: Request,
192        strategy: Option<&CacheStrategyConfig>,
193    ) -> Request {
194        self.prepare_request_defaults(request, Some(strategy)).0
195    }
196
197    /// Applies prompt-cache placement once to an authoritative intercepted request.
198    #[must_use]
199    pub fn apply_prompt_cache_strategy(
200        &self,
201        mut request: Request,
202        strategy: Option<&CacheStrategyConfig>,
203    ) -> Request {
204        if let Some(strategy) = strategy {
205            apply_cache_strategy(&mut request, self.adapter, strategy);
206        }
207        request
208    }
209
210    /// Serializes the prepared provider request at the engine interception boundary.
211    pub fn provider_request_payload(
212        &self,
213        request: &Request,
214    ) -> Result<Value, Box<oven_sdk::ModelError>> {
215        serde_json::to_value(request)
216            .map_err(|error| Box::new(oven_sdk::ModelError::invalid_request(error.to_string())))
217    }
218
219    /// Rebuilds and validates a plugin-replaced request payload before provider execution.
220    pub fn request_from_provider_payload(
221        &self,
222        payload: Value,
223    ) -> Result<Request, Box<oven_sdk::ModelError>> {
224        if !payload.is_object() {
225            return Err(Box::new(oven_sdk::ModelError::invalid_request(
226                "replacement provider payload must be a JSON object",
227            )));
228        }
229        let request: Request = serde_json::from_value(payload)
230            .map_err(|error| Box::new(oven_sdk::ModelError::invalid_request(error.to_string())))?;
231        self.model.validate_request(&request).map_err(Box::new)?;
232        Ok(request)
233    }
234
235    fn prepare_request_inner(
236        &self,
237        request: Request,
238        strategy_override: Option<Option<&CacheStrategyConfig>>,
239    ) -> Request {
240        let (mut request, strategy) = self.prepare_request_defaults(request, strategy_override);
241        if (self
242            .model
243            .capabilities()
244            .features
245            .contains(Capability::PROMPT_CACHING)
246            || matches!(
247                self.adapter,
248                OvenAdapterFamily::OpenaiChat
249                    | OvenAdapterFamily::OpenaiResponses
250                    | OvenAdapterFamily::OpenaiCompatible
251                    | OvenAdapterFamily::AzureOpenaiChat
252                    | OvenAdapterFamily::AzureOpenaiResponses
253            ))
254            && let Some(strategy) = strategy.as_ref()
255        {
256            apply_cache_strategy(&mut request, self.adapter, strategy);
257        }
258        request
259    }
260
261    fn prepare_request_defaults(
262        &self,
263        request: Request,
264        strategy_override: Option<Option<&CacheStrategyConfig>>,
265    ) -> (Request, Option<CacheStrategyConfig>) {
266        let mut request = self
267            .defaults
268            .apply(&crate::ProviderOptions::default(), request);
269        request
270            .provider_options
271            .extend(self.provider_options.clone());
272        let strategy = strategy_override.flatten().cloned();
273        (request, strategy)
274    }
275}
276
277fn apply_cache_strategy(
278    request: &mut Request,
279    adapter: OvenAdapterFamily,
280    strategy: &CacheStrategyConfig,
281) {
282    match (adapter, strategy) {
283        (
284            OvenAdapterFamily::Anthropic | OvenAdapterFamily::AnthropicCompatible,
285            CacheStrategyConfig::Anthropic(strategy),
286        ) => apply_anthropic_cache_strategy(request, strategy),
287        (OvenAdapterFamily::AwsBedrockConverse, CacheStrategyConfig::Bedrock(strategy)) => {
288            let mut strategy = strategy.clone();
289            if request.tools.is_empty() {
290                strategy.tools = None;
291            }
292            if !request.history.iter().any(|turn| {
293                matches!(
294                    turn,
295                    oven_sdk::HistoryTurn::System(message)
296                        if message.content.iter().any(|part| {
297                            matches!(part, oven_sdk::SystemPart::Text(text) if !text.text.is_empty())
298                        })
299                )
300            }) {
301                strategy.system = None;
302            }
303            let last_message = last_non_system_history_index(&request.history);
304            strategy.messages.retain_mut(|point| {
305                if point.history_index != usize::MAX {
306                    return true;
307                }
308                if let Some(index) = last_message {
309                    point.history_index = index;
310                    true
311                } else {
312                    false
313                }
314            });
315            set_option(
316                &mut request.provider_options,
317                "bedrock",
318                None,
319                "cache",
320                Some(serde_json::to_value(strategy).expect("Bedrock cache strategy serializes")),
321            );
322        }
323        (
324            OvenAdapterFamily::GoogleGemini | OvenAdapterFamily::GoogleVertexGemini,
325            CacheStrategyConfig::Google(strategy),
326        ) => {
327            let namespace = if adapter == OvenAdapterFamily::GoogleGemini {
328                "google"
329            } else {
330                "google_vertex"
331            };
332            match strategy.mode {
333                GoogleCacheMode::Implicit => {}
334                GoogleCacheMode::Off => set_option(
335                    &mut request.provider_options,
336                    namespace,
337                    None,
338                    "cached_content",
339                    None,
340                ),
341                GoogleCacheMode::Explicit => set_option(
342                    &mut request.provider_options,
343                    namespace,
344                    None,
345                    "cached_content",
346                    strategy.cached_content.clone().map(Value::String),
347                ),
348            }
349        }
350        (
351            OvenAdapterFamily::OpenaiChat
352            | OvenAdapterFamily::OpenaiResponses
353            | OvenAdapterFamily::OpenaiCompatible
354            | OvenAdapterFamily::AzureOpenaiChat
355            | OvenAdapterFamily::AzureOpenaiResponses,
356            CacheStrategyConfig::OpenAi(strategy),
357        ) => {
358            let (namespace, section) = match adapter {
359                OvenAdapterFamily::OpenaiChat => ("openai", "chat"),
360                OvenAdapterFamily::OpenaiResponses => ("openai", "responses"),
361                OvenAdapterFamily::OpenaiCompatible => ("openai", "chat"),
362                OvenAdapterFamily::AzureOpenaiChat => ("azure_openai", "chat"),
363                OvenAdapterFamily::AzureOpenaiResponses => ("azure_openai", "responses"),
364                _ => unreachable!("OpenAI family match checked"),
365            };
366            set_option(
367                &mut request.provider_options,
368                namespace,
369                Some(section),
370                "prompt_cache_key",
371                strategy.prompt_cache_key.clone().map(Value::String),
372            );
373            set_option(
374                &mut request.provider_options,
375                namespace,
376                Some(section),
377                "prompt_cache_retention",
378                strategy.prompt_cache_retention.map(|retention| {
379                    Value::String(
380                        match retention {
381                            OpenAiPromptCacheRetention::InMemory => "in_memory",
382                            OpenAiPromptCacheRetention::TwentyFourHours => "24h",
383                        }
384                        .into(),
385                    )
386                }),
387            );
388            set_option(
389                &mut request.provider_options,
390                namespace,
391                Some(section),
392                "prompt_cache_options",
393                openai_prompt_cache_options(adapter, strategy),
394            );
395            apply_openai_cache_breakpoints(request, adapter, strategy);
396        }
397        _ => {}
398    }
399}
400
401fn openai_prompt_cache_options(
402    adapter: OvenAdapterFamily,
403    strategy: &crate::adapters::OpenAiCacheStrategyConfig,
404) -> Option<Value> {
405    let (Some(mode), Some(ttl)) = (strategy.mode, strategy.ttl) else {
406        return None;
407    };
408    Some(match adapter {
409        OvenAdapterFamily::OpenaiChat | OvenAdapterFamily::OpenaiResponses => {
410            serde_json::to_value(oven_sdk_openai::OpenAiPromptCacheOptions {
411                mode: match mode {
412                    OpenAiCacheMode::Implicit => oven_sdk_openai::OpenAiPromptCacheMode::Implicit,
413                    OpenAiCacheMode::Explicit => oven_sdk_openai::OpenAiPromptCacheMode::Explicit,
414                },
415                ttl: match ttl {
416                    OpenAiPromptCacheTtl::ThirtyMinutes => {
417                        oven_sdk_openai::OpenAiPromptCacheTtl::ThirtyMinutes
418                    }
419                },
420            })
421            .expect("OpenAI cache options serialize")
422        }
423        OvenAdapterFamily::AzureOpenaiChat | OvenAdapterFamily::AzureOpenaiResponses => {
424            serde_json::to_value(oven_sdk_azure::AzureOpenAiPromptCacheOptions {
425                mode: match mode {
426                    OpenAiCacheMode::Implicit => {
427                        oven_sdk_azure::AzureOpenAiPromptCacheMode::Implicit
428                    }
429                    OpenAiCacheMode::Explicit => {
430                        oven_sdk_azure::AzureOpenAiPromptCacheMode::Explicit
431                    }
432                },
433                ttl: match ttl {
434                    OpenAiPromptCacheTtl::ThirtyMinutes => {
435                        oven_sdk_azure::AzureOpenAiPromptCacheTtl::ThirtyMinutes
436                    }
437                },
438            })
439            .expect("Azure OpenAI cache options serialize")
440        }
441        _ => return None,
442    })
443}
444
445fn apply_openai_cache_breakpoints(
446    request: &mut Request,
447    adapter: OvenAdapterFamily,
448    strategy: &crate::adapters::OpenAiCacheStrategyConfig,
449) {
450    if strategy.system
451        && let Some(oven_sdk::HistoryTurn::System(message)) = request.history.first_mut()
452        && let Some(text) = message
453            .content
454            .iter_mut()
455            .rev()
456            .find_map(|part| match part {
457                oven_sdk::SystemPart::Text(text) if !text.text.is_empty() => Some(text),
458                oven_sdk::SystemPart::Text(_) | oven_sdk::SystemPart::Custom(_) => None,
459            })
460    {
461        mark_openai_cache_breakpoint(text, adapter);
462    }
463    if strategy.rolling
464        && let Some(index) = request.history.iter().rposition(|turn| {
465            matches!(turn, oven_sdk::HistoryTurn::User(message) if message.content.iter().any(
466                |part| matches!(part, oven_sdk::InputPart::Text(text) if !text.text.is_empty())
467            ))
468        })
469        && let oven_sdk::HistoryTurn::User(message) = &mut request.history[index]
470        && let Some(text) = message
471            .content
472            .iter_mut()
473            .rev()
474            .find_map(|part| match part {
475                oven_sdk::InputPart::Text(text) if !text.text.is_empty() => Some(text),
476                oven_sdk::InputPart::Text(_)
477                | oven_sdk::InputPart::File(_)
478                | oven_sdk::InputPart::Custom(_) => None,
479            })
480    {
481        mark_openai_cache_breakpoint(text, adapter);
482    }
483}
484
485fn mark_openai_cache_breakpoint(text: &mut oven_sdk::TextPart, adapter: OvenAdapterFamily) {
486    *text = match adapter {
487        OvenAdapterFamily::OpenaiChat | OvenAdapterFamily::OpenaiResponses => {
488            oven_sdk_openai::OpenAiPromptCacheBreakpointExt::with_openai_prompt_cache_breakpoint(
489                text.clone(),
490            )
491        }
492        OvenAdapterFamily::AzureOpenaiChat | OvenAdapterFamily::AzureOpenaiResponses => {
493            oven_sdk_azure::AzureOpenAiPromptCacheBreakpointExt::with_azure_openai_prompt_cache_breakpoint(
494                text.clone(),
495            )
496        }
497        _ => return,
498    };
499}
500
501fn set_option(
502    options: &mut oven_sdk::ProviderOptions,
503    namespace: &str,
504    section: Option<&str>,
505    key: &str,
506    value: Option<Value>,
507) {
508    let namespace = options
509        .entry(namespace.into())
510        .or_insert_with(|| Value::Object(Default::default()));
511    let Value::Object(namespace) = namespace else {
512        return;
513    };
514    let target = if let Some(section) = section {
515        let section = namespace
516            .entry(section)
517            .or_insert_with(|| Value::Object(Default::default()));
518        let Value::Object(section) = section else {
519            return;
520        };
521        section
522    } else {
523        namespace
524    };
525    if let Some(value) = value {
526        target.insert(key.into(), value);
527    } else {
528        target.remove(key);
529    }
530}
531
532fn clear_message_cache(options: &mut oven_sdk::ProviderOptions) {
533    if let Some(Value::Object(anthropic)) = options.get_mut("anthropic") {
534        anthropic.remove("cache_control");
535    }
536}
537
538fn set_message_cache(options: &mut oven_sdk::ProviderOptions, ttl: AnthropicCacheTtlConfig) {
539    let anthropic = options
540        .entry("anthropic".into())
541        .or_insert_with(|| Value::Object(Default::default()));
542    if let Value::Object(anthropic) = anthropic {
543        anthropic.insert("cache_control".into(), json!({ "ttl": ttl }));
544    }
545}
546
547fn apply_anthropic_cache_strategy(request: &mut Request, strategy: &AnthropicCacheStrategyConfig) {
548    for tool in &mut request.tools {
549        clear_message_cache(&mut tool.provider_options);
550    }
551    for turn in &mut request.history {
552        clear_message_cache(turn_provider_options_mut(turn));
553    }
554
555    let system_index = request.history.first().and_then(|turn| match turn {
556        oven_sdk::HistoryTurn::System(message) if system_message_is_eligible(message) => Some(0),
557        _ => None,
558    });
559    if let (Some(index), Some(ttl)) = (system_index, strategy.system) {
560        set_message_cache(turn_provider_options_mut(&mut request.history[index]), ttl);
561    }
562
563    if !request.tools.is_empty()
564        && !matches!(request.tool_choice, oven_sdk::ToolChoice::None)
565        && let Some(ttl) = strategy.tools
566        && let Some(tool) = request.tools.last_mut()
567    {
568        set_message_cache(&mut tool.provider_options, ttl);
569    }
570
571    if let Some(ttl) = strategy.rolling
572        && let Some(index) = last_non_system_history_index(&request.history)
573        && Some(index) != system_index
574    {
575        set_message_cache(turn_provider_options_mut(&mut request.history[index]), ttl);
576    }
577}
578
579fn turn_provider_options_mut(turn: &mut oven_sdk::HistoryTurn) -> &mut oven_sdk::ProviderOptions {
580    match turn {
581        oven_sdk::HistoryTurn::System(message) => &mut message.provider_options,
582        oven_sdk::HistoryTurn::User(message) => &mut message.provider_options,
583        oven_sdk::HistoryTurn::Assistant(turn) => &mut turn.message.provider_options,
584        oven_sdk::HistoryTurn::Tool(message) => &mut message.provider_options,
585    }
586}
587
588fn system_message_is_eligible(message: &oven_sdk::SystemMessage) -> bool {
589    message
590        .content
591        .iter()
592        .any(|part| matches!(part, oven_sdk::SystemPart::Text(text) if !text.text.is_empty()))
593}
594
595fn last_non_system_history_index(history: &[oven_sdk::HistoryTurn]) -> Option<usize> {
596    history
597        .iter()
598        .rposition(|turn| !matches!(turn, oven_sdk::HistoryTurn::System(_)))
599}
600
601impl fmt::Debug for ResolvedExecutableModel {
602    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
603        formatter
604            .debug_struct("ResolvedExecutableModel")
605            .field("selection", &self.selection)
606            .field("behavior_fingerprint", &self.behavior_fingerprint)
607            .finish_non_exhaustive()
608    }
609}
610
611impl fmt::Debug for CompiledRuntimeModel {
612    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
613        formatter
614            .debug_struct("CompiledRuntimeModel")
615            .field("key", &self.key)
616            .field("model", &self.model)
617            .field("source", &self.source)
618            .field(
619                "config_override_fingerprint",
620                &self.config_override_fingerprint,
621            )
622            .field("setup_values", &self.setup_values)
623            .field("setup_fingerprint", &self.setup_fingerprint)
624            .field("setup_recipe", &self.setup_recipe)
625            .field("credential_source", &self.credential_source)
626            .field("static_headers", &self.static_headers)
627            .field("executable", &self.executable.is_some())
628            .field(
629                "variant_executables",
630                &self.variant_executables.keys().collect::<Vec<_>>(),
631            )
632            .finish()
633    }
634}
635
636#[derive(Clone, Debug)]
637pub enum RuntimeProviderSource {
638    Managed {
639        family_id: ProviderRecipeId,
640        source_record_digest: Sha256Digest,
641        recipe_fingerprint: Sha256Digest,
642        package_claim: String,
643    },
644    Custom {
645        safe_definition_fingerprint: Sha256Digest,
646    },
647}
648
649/// Immutable complete model runtime used by all manager mutations.
650#[derive(Clone)]
651pub struct CompiledModelRuntime {
652    authored: Arc<BTreeMap<ProviderId, ProviderDefinition>>,
653    global_headers: Arc<BTreeMap<crate::HeaderName, crate::SafeStaticHeaderValue>>,
654    catalog: Arc<CatalogSnapshot>,
655    store: ProviderStoreSnapshot,
656    recipe_registry_revision: cookie_agent_identity::RecipeRegistryRevision,
657    model_revision: ModelRevision,
658    runtime_revision: RuntimeRevision,
659    providers: Arc<Vec<CompiledProviderState>>,
660    models: Arc<BTreeMap<ModelKey, CompiledRuntimeModel>>,
661}
662
663impl CompiledModelRuntime {
664    #[must_use]
665    pub fn authored(&self) -> &BTreeMap<ProviderId, ProviderDefinition> {
666        &self.authored
667    }
668
669    #[must_use]
670    pub fn catalog(&self) -> &Arc<CatalogSnapshot> {
671        &self.catalog
672    }
673
674    #[must_use]
675    pub fn store(&self) -> &ProviderStoreSnapshot {
676        &self.store
677    }
678
679    #[must_use]
680    pub fn provider_state_revision(&self) -> ProviderStateRevision {
681        self.store.provider_state_revision()
682    }
683
684    #[must_use]
685    pub fn model_revision(&self) -> &ModelRevision {
686        &self.model_revision
687    }
688
689    #[must_use]
690    pub fn runtime_revision(&self) -> &RuntimeRevision {
691        &self.runtime_revision
692    }
693
694    #[must_use]
695    pub fn providers(&self) -> &[CompiledProviderState] {
696        &self.providers
697    }
698
699    #[must_use]
700    pub fn models(&self) -> &BTreeMap<ModelKey, CompiledRuntimeModel> {
701        &self.models
702    }
703
704    #[must_use]
705    pub fn model(&self, key: &ModelKey) -> Option<&CompiledRuntimeModel> {
706        self.models.get(key)
707    }
708
709    pub fn resolve(
710        &self,
711        selection: &ModelSelection,
712    ) -> Result<ResolvedExecutableModel, ModelManagerError> {
713        self.models
714            .get(&selection.model)
715            .ok_or_else(|| ModelManagerError::UnknownModel(selection.model.clone()))?
716            .resolve(selection)
717    }
718
719    /// Allocates the secret-free schema-1 payload that run bindings are frozen against.
720    pub fn manifest_payload(&self) -> Result<ModelSnapshotPayloadV1, ModelManagerError> {
721        let mut blueprints = self
722            .models
723            .values()
724            .filter(|model| model.model.status == CompiledModelStatus::Available)
725            .map(CompiledRuntimeModel::blueprint)
726            .collect::<Result<Vec<_>, _>>()?;
727        blueprints.sort_by(|left, right| left.selection.model.cmp(&right.selection.model));
728        Ok(ModelSnapshotPayloadV1 {
729            catalog_revision: self.catalog.revision.clone(),
730            recipe_registry_revision: self.recipe_registry_revision.clone(),
731            provider_state_revision: self.store.provider_state_revision(),
732            model_revision: self.model_revision.clone(),
733            blueprints,
734        })
735    }
736}
737
738impl fmt::Debug for CompiledModelRuntime {
739    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
740        formatter
741            .debug_struct("CompiledModelRuntime")
742            .field("catalog_revision", &self.catalog.revision)
743            .field("provider_state_revision", &self.provider_state_revision())
744            .field("model_revision", &self.model_revision)
745            .field("runtime_revision", &self.runtime_revision)
746            .field("providers", &self.providers.len())
747            .field("models", &self.models.keys().collect::<Vec<_>>())
748            .finish()
749    }
750}
751
752impl CompiledRuntimeModel {
753    pub fn resolve(
754        &self,
755        selection: &ModelSelection,
756    ) -> Result<ResolvedExecutableModel, ModelManagerError> {
757        if selection.model != self.key {
758            return Err(ModelManagerError::UnknownModel(selection.model.clone()));
759        }
760        let behavior = match selection.variant.as_ref() {
761            Some(variant) => self
762                .variant_executables
763                .get(variant)
764                .ok_or_else(|| ModelManagerError::UnknownVariant(selection.clone()))?,
765            None => self
766                .executable
767                .as_ref()
768                .ok_or_else(|| ModelManagerError::ModelUnavailable(self.key.clone()))?,
769        };
770        Ok(ResolvedExecutableModel {
771            selection: selection.clone(),
772            adapter: behavior.adapter,
773            model: Arc::clone(&behavior.model),
774            defaults: behavior.defaults.clone(),
775            provider_options: behavior.provider_options.clone(),
776            behavior_fingerprint: behavior.behavior_fingerprint.clone(),
777        })
778    }
779
780    pub fn resolved_defaults(
781        &self,
782        variant: Option<&cookie_agent_identity::VariantId>,
783    ) -> Option<&crate::ResolvedRequestDefaults> {
784        match variant {
785            Some(variant) => self
786                .variant_executables
787                .get(variant)
788                .map(|value| &value.defaults),
789            None => self.executable.as_ref().map(|value| &value.defaults),
790        }
791    }
792
793    pub fn frozen_behavior(
794        &self,
795        variant: Option<&cookie_agent_identity::VariantId>,
796    ) -> Result<
797        (
798            protocol::FrozenResolvedRequestDefaults,
799            protocol::FrozenProviderOptions,
800            protocol::Sha256Digest,
801            protocol::Sha256Digest,
802        ),
803        ModelManagerError,
804    > {
805        let blueprint = self.blueprint()?;
806        let selection = ModelSelection {
807            model: self.key.clone(),
808            variant: variant.cloned(),
809        };
810        let behavior = selected_behavior(&blueprint, &selection)
811            .ok_or(ModelManagerError::UnknownVariant(selection))?;
812        Ok((
813            behavior.defaults.clone(),
814            behavior.options.clone(),
815            behavior.behavior_fingerprint.clone(),
816            behavior.selection_fingerprint.clone(),
817        ))
818    }
819
820    fn blueprint(&self) -> Result<CompiledSafeModelBlueprint, ModelManagerError> {
821        let provider_id = self.key.provider_id();
822        let source = match &self.source {
823            RuntimeProviderSource::Managed {
824                family_id,
825                source_record_digest,
826                recipe_fingerprint,
827                package_claim,
828            } => FrozenProviderSource::Managed {
829                provider_recipe: family_id.clone(),
830                source_record_digest: protocol_digest(source_record_digest),
831                recipe_fingerprint: protocol_digest(recipe_fingerprint),
832                package_claim: package_claim.clone(),
833            },
834            RuntimeProviderSource::Custom {
835                safe_definition_fingerprint,
836            } => FrozenProviderSource::Custom {
837                safe_definition_fingerprint: protocol_digest(safe_definition_fingerprint),
838            },
839        };
840        let credential_source = match self.credential_source {
841            EffectiveCredentialSource::AuthoredApiKey => FrozenCredentialSource::AuthoredApiKey,
842            EffectiveCredentialSource::AuthoredOverride => FrozenCredentialSource::AuthoredOverride,
843            EffectiveCredentialSource::ProviderStore => FrozenCredentialSource::ProviderStore,
844            EffectiveCredentialSource::NoAuth => FrozenCredentialSource::NoAuth,
845            EffectiveCredentialSource::Unavailable => {
846                return Err(ModelManagerError::RuntimeCompileFailed);
847            }
848        };
849        let setup_values = protocol_setup_values(&self.setup_values)?;
850        let defaults = frozen_defaults(&crate::ResolvedRequestDefaults {
851            request: self.model.defaults.clone(),
852            reasoning: None,
853        })?;
854        let options = protocol_options(&self.model, &self.setup_values)?;
855        let descriptor = safe_descriptor(&provider_id, &self.model)?;
856        let mut variants = self
857            .model
858            .variants
859            .iter()
860            .map(|(id, variant)| {
861                let selected = self
862                    .model
863                    .selected(&variant.options, variant.model_id.as_ref())?;
864                Ok(FrozenVariantBlueprint {
865                    id: id.clone(),
866                    descriptor: safe_descriptor(&provider_id, &selected)?,
867                    defaults: frozen_defaults(&crate::ResolvedRequestDefaults {
868                        request: variant.defaults.clone(),
869                        reasoning: variant.reasoning.clone(),
870                    })?,
871                    options: { protocol_options(&selected, &self.setup_values)? },
872                    static_headers: variant
873                        .headers
874                        .iter()
875                        .map(|(name, value)| {
876                            Ok((
877                                HeaderName::new(name.as_str())
878                                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
879                                SafeStaticHeaderValue::new(value.as_str())
880                                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
881                            ))
882                        })
883                        .collect::<Result<_, ModelManagerError>>()?,
884                    behavior_fingerprint: protocol::Sha256Digest::of_bytes(b"pending"),
885                    selection_fingerprint: protocol::Sha256Digest::of_bytes(b"pending"),
886                })
887            })
888            .collect::<Result<Vec<_>, ModelManagerError>>()?;
889        variants.sort_by(|left, right| left.id.cmp(&right.id));
890        let static_headers = self
891            .static_headers
892            .iter()
893            .map(|(name, value)| {
894                Ok((
895                    HeaderName::new(name.as_str())
896                        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
897                    SafeStaticHeaderValue::new(value.as_str())
898                        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
899                ))
900            })
901            .collect::<Result<BTreeMap<_, _>, ModelManagerError>>()?;
902        let setup_recipe = self.setup_recipe.clone();
903        let auth_method = AuthMethodId::new(self.model.auth.method.clone())
904            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
905        let family_id = ProviderRecipeId::new(self.model.family_id.clone())
906            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
907        let protocol_recipe = ProtocolRecipeId::new(self.model.adapter_id.clone())
908            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
909        let compiler_version = RecipeCompilerVersion::new(COMPILER_VERSION)
910            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
911        let endpoint = self
912            .model
913            .endpoint
914            .as_ref()
915            .ok_or(ModelManagerError::RuntimeCompileFailed)?;
916        let mut blueprint = CompiledSafeModelBlueprint {
917            blueprint_fingerprint: protocol::Sha256Digest::of_bytes(b"pending"),
918            selection: ModelSelection {
919                model: self.key.clone(),
920                variant: None,
921            },
922            source,
923            config_override_fingerprint: protocol_digest(&self.config_override_fingerprint),
924            setup_binding: FrozenSetupBinding {
925                setup_recipe: setup_recipe.clone(),
926                values: setup_values,
927                setup_fingerprint: protocol_digest(&self.setup_fingerprint),
928            },
929            credential_binding: FrozenCredentialBinding {
930                source: credential_source,
931                auth_method: auth_method.clone(),
932                fields: self
933                    .model
934                    .auth
935                    .credential_fields
936                    .iter()
937                    .map(|field| AuthFieldName::new(field.clone()))
938                    .collect::<Result<Vec<_>, _>>()
939                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
940                parameters: self
941                    .model
942                    .auth
943                    .safe_parameters
944                    .iter()
945                    .map(|(name, value)| {
946                        Ok((
947                            AuthParameterId::new(name.clone())
948                                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
949                            FrozenAuthParameterValue::new(value.clone())
950                                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
951                        ))
952                    })
953                    .collect::<Result<_, ModelManagerError>>()?,
954                owned_headers: self
955                    .model
956                    .auth
957                    .owned_headers
958                    .iter()
959                    .map(|header| {
960                        HeaderName::new(header.clone())
961                            .map_err(|_| ModelManagerError::RuntimeCompileFailed)
962                    })
963                    .collect::<Result<Vec<_>, _>>()?,
964            },
965            endpoint_identity: SafeEndpointIdentity::new(endpoint.clone())
966                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
967            provider_recipe: family_id,
968            protocol_recipe,
969            setup_recipe,
970            auth_method,
971            compiler_version,
972            descriptor,
973            defaults,
974            options,
975            static_headers,
976            variants,
977            behavior_fingerprint: protocol::Sha256Digest::of_bytes(b"pending"),
978            selection_fingerprint: protocol::Sha256Digest::of_bytes(b"pending"),
979        };
980        blueprint.credential_binding.fields.sort();
981        blueprint.credential_binding.owned_headers.sort();
982        blueprint.behavior_fingerprint = behavior_fingerprint(&blueprint, &blueprint.selection)?;
983        blueprint.selection_fingerprint = selection_fingerprint(&blueprint, &blueprint.selection)?;
984        for index in 0..blueprint.variants.len() {
985            let selection = ModelSelection {
986                model: blueprint.selection.model.clone(),
987                variant: Some(blueprint.variants[index].id.clone()),
988            };
989            blueprint.variants[index].behavior_fingerprint =
990                behavior_fingerprint(&blueprint, &selection)?;
991            blueprint.variants[index].selection_fingerprint =
992                selection_fingerprint(&blueprint, &selection)?;
993        }
994        blueprint.blueprint_fingerprint = blueprint_fingerprint(&blueprint)?;
995        Ok(blueprint)
996    }
997}
998
999/// High-level connect request. Policy and store expectations are manager-owned.
1000#[derive(Clone, Debug)]
1001pub struct ProviderConnectRequest {
1002    pub provider_id: ProviderId,
1003    pub expected_catalog_revision: CatalogRevision,
1004    pub setup_values: BTreeMap<SetupFieldId, SafeSetupValue>,
1005    pub auth_method: AuthMethodId,
1006    pub auth_values: ProviderAuthValues,
1007    pub client_connect_id: ClientConnectId,
1008}
1009
1010#[derive(Clone, Debug)]
1011pub struct ProviderDisconnectRequest {
1012    pub provider_id: ProviderId,
1013    pub expected_runtime_revision: RuntimeRevision,
1014    pub expected_provider_state_revision: ProviderStateRevision,
1015    pub expected_connection_generation: Option<ProviderConnectionGeneration>,
1016    pub client_request_id: ClientRequestId,
1017}
1018
1019/// A mutation result whose optional publication was fully prepared before commit.
1020#[derive(Debug)]
1021pub struct ModelMutationResult<P> {
1022    pub mutation: ProviderStoreMutation,
1023    pub runtime: Arc<CompiledModelRuntime>,
1024    pub effective_auth: EffectiveCredentialSource,
1025    pub replayed: bool,
1026    pub publication: Option<P>,
1027}
1028
1029/// Atomic manager for current and retained exact compiled model runtimes.
1030pub struct ModelManager {
1031    store: ProviderStore,
1032    current: ArcSwap<CompiledModelRuntime>,
1033    retained: ArcSwap<BTreeMap<ModelRevision, Vec<Arc<CompiledModelRuntime>>>>,
1034    mutation: Mutex<()>,
1035}
1036
1037impl ModelManager {
1038    pub fn new(
1039        authored: BTreeMap<ProviderId, ProviderDefinition>,
1040        catalog: Arc<CatalogSnapshot>,
1041        store: ProviderStore,
1042    ) -> Result<Self, ModelManagerError> {
1043        Self::new_with_headers(authored, BTreeMap::new(), catalog, store)
1044    }
1045
1046    pub fn new_with_headers(
1047        authored: BTreeMap<ProviderId, ProviderDefinition>,
1048        global_headers: BTreeMap<crate::HeaderName, crate::SafeStaticHeaderValue>,
1049        catalog: Arc<CatalogSnapshot>,
1050        store: ProviderStore,
1051    ) -> Result<Self, ModelManagerError> {
1052        let store_snapshot = store.load()?;
1053        let initial = Arc::new(compile_runtime(
1054            Arc::new(authored),
1055            Arc::new(global_headers),
1056            catalog,
1057            store_snapshot,
1058        )?);
1059        let retained = Arc::new(BTreeMap::from([(
1060            initial.model_revision.clone(),
1061            vec![Arc::clone(&initial)],
1062        )]));
1063        Ok(Self {
1064            store,
1065            current: ArcSwap::from(initial),
1066            retained: ArcSwap::from(retained),
1067            mutation: Mutex::new(()),
1068        })
1069    }
1070
1071    #[must_use]
1072    pub fn current(&self) -> Arc<CompiledModelRuntime> {
1073        self.current.load_full()
1074    }
1075
1076    #[must_use]
1077    pub fn retained(&self, revision: &ModelRevision) -> Option<Arc<CompiledModelRuntime>> {
1078        self.retained
1079            .load()
1080            .get(revision)
1081            .and_then(|snapshots| snapshots.last())
1082            .cloned()
1083    }
1084
1085    #[must_use]
1086    pub fn retained_all(&self, revision: &ModelRevision) -> Vec<Arc<CompiledModelRuntime>> {
1087        self.retained
1088            .load()
1089            .get(revision)
1090            .cloned()
1091            .unwrap_or_default()
1092    }
1093
1094    pub fn connect<P>(
1095        &self,
1096        request: ProviderConnectRequest,
1097        prepare_publication: impl FnOnce(
1098            &Arc<CompiledModelRuntime>,
1099            &ProviderStoreMutation,
1100        ) -> Result<P, ModelManagerError>,
1101    ) -> Result<ModelMutationResult<P>, ModelManagerError> {
1102        let _guard = lock(&self.mutation);
1103        let current = self.current();
1104        let transaction = self.store.begin_transaction()?;
1105        let store_snapshot = transaction.snapshot();
1106        let mutation = normalize_connect(&current, &store_snapshot, request)?;
1107        match transaction.propose_connect(&mutation, &current.catalog.revision)? {
1108            ConnectProposal::Replay(replayed) => {
1109                let mutation = *replayed;
1110                Ok(ModelMutationResult {
1111                    effective_auth: effective_auth_for(&current, mutation_provider(&mutation)),
1112                    mutation,
1113                    runtime: current,
1114                    replayed: true,
1115                    publication: None,
1116                })
1117            }
1118            ConnectProposal::Proposed(proposal) => {
1119                let candidate = Arc::new(compile_runtime(
1120                    Arc::clone(&current.authored),
1121                    Arc::clone(&current.global_headers),
1122                    Arc::clone(&current.catalog),
1123                    proposal.snapshot(),
1124                )?);
1125                let mutation = proposal.mutation().clone();
1126                let publication = prepare_publication(&candidate, &mutation)?;
1127                let retained = prepared_retained(&self.retained.load_full(), &candidate);
1128                let effective_auth = effective_auth_for(&candidate, mutation_provider(&mutation));
1129                transaction.commit(*proposal).map_err(map_commit_error)?;
1130                self.retained.store(retained);
1131                self.current.store(Arc::clone(&candidate));
1132                Ok(ModelMutationResult {
1133                    mutation,
1134                    runtime: candidate,
1135                    effective_auth,
1136                    replayed: false,
1137                    publication: Some(publication),
1138                })
1139            }
1140        }
1141    }
1142
1143    pub fn disconnect<P>(
1144        &self,
1145        request: ProviderDisconnectRequest,
1146        prepare_publication: impl FnOnce(
1147            &Arc<CompiledModelRuntime>,
1148            &ProviderStoreMutation,
1149        ) -> Result<P, ModelManagerError>,
1150    ) -> Result<ModelMutationResult<P>, ModelManagerError> {
1151        let _guard = lock(&self.mutation);
1152        let current = self.current();
1153        if matches!(
1154            current.authored.get(&request.provider_id),
1155            Some(ProviderDefinition::Custom(_))
1156        ) {
1157            return Err(ModelManagerError::CustomProviderNotStoreBacked);
1158        }
1159        let transaction = self.store.begin_transaction()?;
1160        let snapshot = transaction.snapshot();
1161        let mutation = DisconnectMutation {
1162            client_request_id: request.client_request_id,
1163            provider_id: request.provider_id,
1164            expected_runtime_revision: request.expected_runtime_revision,
1165            expected_provider_state_revision: request.expected_provider_state_revision,
1166            expected_store_generation: snapshot.generation(),
1167            expected_store_revision: snapshot.store_revision().clone(),
1168            expected_connection_generation: request.expected_connection_generation,
1169        };
1170        match transaction.propose_disconnect(&mutation, current.runtime_revision())? {
1171            DisconnectProposal::Replay(replayed) => {
1172                let mutation = *replayed;
1173                Ok(ModelMutationResult {
1174                    effective_auth: effective_auth_for(&current, mutation_provider(&mutation)),
1175                    mutation,
1176                    runtime: current,
1177                    replayed: true,
1178                    publication: None,
1179                })
1180            }
1181            DisconnectProposal::Proposed(proposal) => {
1182                let candidate = Arc::new(compile_runtime(
1183                    Arc::clone(&current.authored),
1184                    Arc::clone(&current.global_headers),
1185                    Arc::clone(&current.catalog),
1186                    proposal.snapshot(),
1187                )?);
1188                let mutation = proposal.mutation().clone();
1189                let publication = prepare_publication(&candidate, &mutation)?;
1190                let retained = prepared_retained(&self.retained.load_full(), &candidate);
1191                let effective_auth = effective_auth_for(&candidate, mutation_provider(&mutation));
1192                transaction.commit(*proposal).map_err(map_commit_error)?;
1193                self.retained.store(retained);
1194                self.current.store(Arc::clone(&candidate));
1195                Ok(ModelMutationResult {
1196                    mutation,
1197                    runtime: candidate,
1198                    effective_auth,
1199                    replayed: false,
1200                    publication: Some(publication),
1201                })
1202            }
1203        }
1204    }
1205
1206    /// Recompiles and publishes another process's newer provider-store generation.
1207    pub fn reload_store_if_changed<P>(
1208        &self,
1209        prepare_publication: impl FnOnce(&Arc<CompiledModelRuntime>) -> Result<P, ModelManagerError>,
1210    ) -> Result<Option<(Arc<CompiledModelRuntime>, P)>, ModelManagerError> {
1211        let _guard = lock(&self.mutation);
1212        let current = self.current();
1213        let Some(store) = self.store.reload_if_changed(current.store.generation())? else {
1214            return Ok(None);
1215        };
1216        let candidate = Arc::new(compile_runtime(
1217            Arc::clone(&current.authored),
1218            Arc::clone(&current.global_headers),
1219            Arc::clone(&current.catalog),
1220            store,
1221        )?);
1222        let publication = prepare_publication(&candidate)?;
1223        let retained = prepared_retained(&self.retained.load_full(), &candidate);
1224        self.retained.store(retained);
1225        self.current.store(Arc::clone(&candidate));
1226        Ok(Some((candidate, publication)))
1227    }
1228
1229    /// Atomically recompiles a config/catalog replacement before publication.
1230    pub fn reload_inputs<P>(
1231        &self,
1232        authored: BTreeMap<ProviderId, ProviderDefinition>,
1233        catalog: Arc<CatalogSnapshot>,
1234        prepare_publication: impl FnOnce(&Arc<CompiledModelRuntime>) -> Result<P, ModelManagerError>,
1235    ) -> Result<(Arc<CompiledModelRuntime>, P), ModelManagerError> {
1236        let _guard = lock(&self.mutation);
1237        let current = self.current();
1238        let store = self.store.load()?;
1239        let candidate = Arc::new(compile_runtime(
1240            Arc::new(authored),
1241            Arc::clone(&current.global_headers),
1242            catalog,
1243            store,
1244        )?);
1245        let publication = prepare_publication(&candidate)?;
1246        let retained = prepared_retained(&self.retained.load_full(), &candidate);
1247        self.retained.store(retained);
1248        self.current.store(Arc::clone(&candidate));
1249        Ok((candidate, publication))
1250    }
1251}
1252
1253impl fmt::Debug for ModelManager {
1254    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1255        formatter
1256            .debug_struct("ModelManager")
1257            .field("current", &self.current())
1258            .field("retained", &self.retained.load().len())
1259            .finish_non_exhaustive()
1260    }
1261}
1262
1263fn validate_unresolved_managed_cache(
1264    provider_id: &ProviderId,
1265    provider: &ModelsDevProvider,
1266    stored: Option<&StoredManagedConnection>,
1267) -> Result<(), ModelManagerError> {
1268    let Some(cache) = provider.cache.as_ref() else {
1269        return Ok(());
1270    };
1271    cache
1272        .validate_supported_shape()
1273        .map_err(|reason| ModelManagerError::ProviderCache {
1274            provider: provider_id.clone(),
1275            reason,
1276        })?;
1277    let Some(recipe) = stored
1278        .and_then(|connection| family_registry().by_npm(connection.policy.package_claim.as_str()))
1279    else {
1280        return Ok(());
1281    };
1282    let adapter = managed_provider_adapter(recipe.family, None, None);
1283    crate::authoring::validate_provider_cache(Some(cache), adapter.id()).map_err(|reason| {
1284        ModelManagerError::ProviderCache {
1285            provider: provider_id.clone(),
1286            reason,
1287        }
1288    })
1289}
1290
1291fn compile_runtime(
1292    authored: Arc<BTreeMap<ProviderId, ProviderDefinition>>,
1293    global_headers: Arc<BTreeMap<crate::HeaderName, crate::SafeStaticHeaderValue>>,
1294    catalog: Arc<CatalogSnapshot>,
1295    store: ProviderStoreSnapshot,
1296) -> Result<CompiledModelRuntime, ModelManagerError> {
1297    let registry = family_registry();
1298    let compiler = DynamicCompiler::family_registry();
1299    let mut provider_ids = catalog.providers.keys().cloned().collect::<BTreeSet<_>>();
1300    provider_ids.extend(
1301        authored
1302            .iter()
1303            .filter(|(_, definition)| matches!(definition, ProviderDefinition::ModelsDev(_)))
1304            .map(|(id, _)| id.clone()),
1305    );
1306    provider_ids.extend(store.providers().keys().cloned());
1307
1308    let mut providers = Vec::new();
1309    let mut models = BTreeMap::new();
1310    for provider_id in provider_ids {
1311        // Authored custom providers take precedence over catalog and
1312        // provider-store entries for the same ID.
1313        if matches!(
1314            authored.get(&provider_id),
1315            Some(ProviderDefinition::Custom(_))
1316        ) {
1317            continue;
1318        }
1319        let authored_managed = match authored.get(&provider_id) {
1320            Some(ProviderDefinition::ModelsDev(provider)) => Some(provider),
1321            _ => None,
1322        };
1323        let stored = store.provider(&provider_id);
1324        let entry = catalog.provider(&provider_id);
1325        if let Some(provider) = authored_managed
1326            && provider.cache.is_some()
1327        {
1328            if let Some(record) = entry.and_then(|entry| entry.record.as_ref()) {
1329                let family =
1330                    registry
1331                        .classify(record)
1332                        .ok_or_else(|| ModelManagerError::ProviderCache {
1333                            provider: provider_id.clone(),
1334                            reason: "cache configuration requires a supported provider family"
1335                                .into(),
1336                        })?;
1337                validate_managed_cache(record, Some(provider), family)?;
1338            } else {
1339                validate_unresolved_managed_cache(&provider_id, provider, stored)?;
1340            }
1341        }
1342        let presence = if entry.is_some() {
1343            ProviderPresence::Current
1344        } else {
1345            ProviderPresence::Removed
1346        };
1347        let mut state = CompiledProviderState {
1348            id: provider_id.clone(),
1349            display_name: entry
1350                .and_then(|entry| entry.record.as_ref())
1351                .map_or_else(|| provider_id.to_string(), |record| record.name.clone()),
1352            presence,
1353            support_reason: None,
1354            retained_family_match: None,
1355            authored: authored_managed.is_some(),
1356            stored: stored.is_some(),
1357            effective_auth: EffectiveCredentialSource::Unavailable,
1358            durable_connection: stored.map(StoredManagedConnection::descriptor),
1359        };
1360        let Some(record) = entry.and_then(|entry| entry.record.as_ref()) else {
1361            if let Some(reason) = entry.and_then(|entry| entry.quarantine.as_ref()) {
1362                state.support_reason = Some(reason.code().to_owned());
1363            } else {
1364                let retained_match = stored.map_or(
1365                    RetainedFamilyMatch::RemovedWithoutRetainedFamilyMatch,
1366                    |connection| retained_family_match(&provider_id, connection),
1367                );
1368                state.retained_family_match = Some(retained_match);
1369                state.support_reason = retained_match.reason().map(str::to_owned);
1370                if retained_match == RetainedFamilyMatch::SupportedRemoved {
1371                    state.effective_auth = EffectiveCredentialSource::ProviderStore;
1372                }
1373            }
1374            providers.push(state);
1375            continue;
1376        };
1377        let Some(family) = registry.classify(record) else {
1378            state.support_reason = Some("no_known_protocol_family".to_owned());
1379            providers.push(state);
1380            continue;
1381        };
1382        let effective = effective_managed(&provider_id, record, family, authored_managed, stored)?;
1383        state.effective_auth = effective.credential_source;
1384        let compiled = compiler.compile_managed_with_headers(
1385            catalog.revision.as_str(),
1386            record,
1387            Some(&effective.provider),
1388            &global_headers,
1389        );
1390        let compiled = match compiled {
1391            Ok(compiled) => compiled,
1392            Err(DynamicCompileError::UnsupportedProvider) => {
1393                state.support_reason = Some("no_known_protocol_family".to_owned());
1394                providers.push(state);
1395                continue;
1396            }
1397            Err(error) if authored_managed.is_none() && stored.is_none() => {
1398                state.support_reason = Some(error.to_string());
1399                providers.push(state);
1400                continue;
1401            }
1402            Err(error) => return Err(ModelManagerError::DynamicCompile(error)),
1403        };
1404        for (model_id, model) in compiled.models {
1405            let key = ModelKey::new(provider_id.clone(), model_id)
1406                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
1407            let model = with_effective_status(model, effective.credential_source);
1408            let model_credentials = if model.status == CompiledModelStatus::Available {
1409                mapped_credentials(&effective.credentials, &model.auth)?
1410            } else {
1411                ExecutableCredentialMaterial {
1412                    method: model.auth.method.clone(),
1413                    values: BTreeMap::new(),
1414                }
1415            };
1416            let (executable, variant_executables) =
1417                compile_behaviors(&provider_id, &model, &model.headers, &model_credentials)?;
1418            let model_recipe = family_registry()
1419                .by_npm(&model.effective_npm)
1420                .ok_or(ModelManagerError::RuntimeCompileFailed)?;
1421            let runtime_model = CompiledRuntimeModel {
1422                key: key.clone(),
1423                source: RuntimeProviderSource::Managed {
1424                    family_id: ProviderRecipeId::new(model.family_id.clone())
1425                        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1426                    source_record_digest: effective.source_record_digest.clone(),
1427                    recipe_fingerprint: retained_recipe_fingerprint(
1428                        model_recipe,
1429                        &model.auth.method,
1430                    )?,
1431                    package_claim: model.effective_npm.clone(),
1432                },
1433                config_override_fingerprint: effective.config_fingerprint.clone(),
1434                setup_values: effective.setup_values.clone(),
1435                setup_fingerprint: effective.setup_fingerprint.clone(),
1436                setup_recipe: ProviderSetupRecipeId::new("family-derived-setup-v1")
1437                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1438                credential_source: effective.credential_source,
1439                static_headers: model.headers.clone(),
1440                model,
1441                executable,
1442                variant_executables,
1443            };
1444            if models.insert(key.clone(), runtime_model).is_some() {
1445                return Err(ModelManagerError::DuplicateModel(key));
1446            }
1447        }
1448        providers.push(state);
1449    }
1450
1451    for (provider_id, definition) in authored.iter() {
1452        let ProviderDefinition::Custom(custom) = definition else {
1453            continue;
1454        };
1455        let compiled =
1456            compiler.compile_custom_with_headers(provider_id, custom, &global_headers)?;
1457        let fingerprint = safe_definition_fingerprint(provider_id, definition);
1458        let setup_recipe = crate::adapters::custom_setup_recipe(
1459            OvenAdapterFamily::parse(custom.adaptor.as_str())
1460                .ok_or(ModelManagerError::RuntimeCompileFailed)?,
1461        );
1462        let setup_values = normalized_setup_values(setup_recipe, &custom.setup)?;
1463        let setup_fingerprint = setup_fingerprint(&setup_values);
1464        let credential_source = if custom.auth.method.as_str() == "no-auth-v1" {
1465            EffectiveCredentialSource::NoAuth
1466        } else {
1467            EffectiveCredentialSource::AuthoredOverride
1468        };
1469        let credentials = ExecutableCredentialMaterial {
1470            method: custom.auth.method.as_str().to_owned(),
1471            values: custom
1472                .auth
1473                .values
1474                .iter()
1475                .map(|(field, value)| (field.clone(), value.expose().to_owned()))
1476                .collect(),
1477        };
1478        for (model_id, model) in compiled.models {
1479            let key = ModelKey::new(provider_id.clone(), model_id)
1480                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
1481            let (executable, variant_executables) =
1482                compile_behaviors(provider_id, &model, &model.headers, &credentials)?;
1483            let static_headers = model.headers.clone();
1484            let runtime_model = CompiledRuntimeModel {
1485                key: key.clone(),
1486                model,
1487                source: RuntimeProviderSource::Custom {
1488                    safe_definition_fingerprint: fingerprint.clone(),
1489                },
1490                config_override_fingerprint: fingerprint.clone(),
1491                setup_values: setup_values.clone(),
1492                setup_fingerprint: setup_fingerprint.clone(),
1493                setup_recipe: ProviderSetupRecipeId::new(setup_recipe.id)
1494                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1495                credential_source,
1496                static_headers,
1497                executable,
1498                variant_executables,
1499            };
1500            if models.insert(key.clone(), runtime_model).is_some() {
1501                return Err(ModelManagerError::DuplicateModel(key));
1502            }
1503        }
1504    }
1505
1506    providers.sort_by(|left, right| left.id.cmp(&right.id));
1507    let model_revision = revision::<ModelRevision, _>(
1508        "cookie-agent/model-runtime/v1",
1509        &models
1510            .iter()
1511            .map(|(key, model)| {
1512                (
1513                    key,
1514                    &model.model.behavior_fingerprint,
1515                    model.credential_source,
1516                    &model.setup_fingerprint,
1517                    &model.config_override_fingerprint,
1518                )
1519            })
1520            .collect::<Vec<_>>(),
1521        ModelRevision::new,
1522    )?;
1523    let runtime_revision = revision::<RuntimeRevision, _>(
1524        "cookie-agent/model-manager-runtime/v1",
1525        &(
1526            registry.revision(),
1527            &catalog.revision,
1528            store.provider_state_revision(),
1529            &model_revision,
1530        ),
1531        RuntimeRevision::new,
1532    )?;
1533    Ok(CompiledModelRuntime {
1534        authored,
1535        global_headers,
1536        catalog,
1537        store,
1538        recipe_registry_revision: registry.revision(),
1539        model_revision,
1540        runtime_revision,
1541        providers: Arc::new(providers),
1542        models: Arc::new(models),
1543    })
1544}
1545
1546struct EffectiveManaged {
1547    provider: ModelsDevProvider,
1548    credential_source: EffectiveCredentialSource,
1549    setup_values: BTreeMap<SetupFieldId, SafeSetupValue>,
1550    setup_fingerprint: Sha256Digest,
1551    source_record_digest: Sha256Digest,
1552    config_fingerprint: Sha256Digest,
1553    credentials: ExecutableCredentialMaterial,
1554}
1555
1556fn effective_managed(
1557    provider_id: &ProviderId,
1558    record: &crate::catalog::CatalogProviderRecord,
1559    recipe: &'static FamilyRecipe,
1560    authored: Option<&ModelsDevProvider>,
1561    stored: Option<&StoredManagedConnection>,
1562) -> Result<EffectiveManaged, ModelManagerError> {
1563    let source_record_digest = validated_provider_source_digest(record)?;
1564    let config_fingerprint = authored.map_or_else(no_authored_override_fingerprint, |authored| {
1565        safe_definition_fingerprint(
1566            provider_id,
1567            &ProviderDefinition::ModelsDev(authored.clone()),
1568        )
1569    });
1570    let authored_setup = authored.filter(|value| !value.setup.is_empty());
1571    let stored_endpoint = stored
1572        .map(|connection| resolved_provider_endpoint(record, recipe, &connection.setup_values))
1573        .transpose()?;
1574    let store_eligible = authored.is_none_or(|value| value.base_url.is_none())
1575        && stored.is_some_and(|connection| {
1576            retained_recipe_for_endpoint(connection, provider_id, stored_endpoint.as_deref())
1577                .is_some_and(|retained| retained.family == recipe.family)
1578        });
1579    let setup_input = authored_setup.map(|value| &value.setup).or_else(|| {
1580        store_eligible
1581            .then_some(stored)
1582            .flatten()
1583            .map(|value| &value.setup_values)
1584    });
1585    let setup_values = setup_input.cloned().unwrap_or_default();
1586    let setup_fingerprint = setup_fingerprint(&setup_values);
1587
1588    let credential_source = if authored.is_some_and(|value| value.api_key.is_some()) {
1589        EffectiveCredentialSource::AuthoredApiKey
1590    } else if authored.is_some_and(|value| value.auth_override.is_some()) {
1591        EffectiveCredentialSource::AuthoredOverride
1592    } else if store_eligible
1593        && stored.is_some_and(|connection| {
1594            connection.setup_fingerprint == setup_fingerprint
1595                && recipe
1596                    .allowed_auth_methods
1597                    .contains(&connection.auth_method.as_str())
1598        })
1599    {
1600        EffectiveCredentialSource::ProviderStore
1601    } else if auth_method(recipe.default_auth_method)
1602        .is_some_and(|method| method.credentials.is_empty())
1603    {
1604        EffectiveCredentialSource::NoAuth
1605    } else {
1606        EffectiveCredentialSource::Unavailable
1607    };
1608
1609    let mut provider = authored.cloned().unwrap_or(ModelsDevProvider {
1610        base_url: None,
1611        setup: BTreeMap::new(),
1612        api_key: None,
1613        auth_override: None,
1614        cache: None,
1615        headers: BTreeMap::new(),
1616        model_overrides: BTreeMap::new(),
1617    });
1618    provider.setup = setup_values.clone();
1619    let credentials = match credential_source {
1620        EffectiveCredentialSource::AuthoredApiKey => ExecutableCredentialMaterial {
1621            method: recipe.default_auth_method.to_owned(),
1622            values: BTreeMap::from([(
1623                AuthFieldName::new("api_key")
1624                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1625                authored
1626                    .and_then(|value| value.api_key.as_ref())
1627                    .ok_or(ModelManagerError::RuntimeCompileFailed)?
1628                    .expose()
1629                    .to_owned(),
1630            )]),
1631        },
1632        EffectiveCredentialSource::AuthoredOverride => {
1633            let override_ = authored
1634                .and_then(|value| value.auth_override.as_ref())
1635                .ok_or(ModelManagerError::RuntimeCompileFailed)?;
1636            ExecutableCredentialMaterial {
1637                method: override_.method.as_str().to_owned(),
1638                values: override_
1639                    .values
1640                    .iter()
1641                    .map(|(field, value)| (field.clone(), value.expose().to_owned()))
1642                    .collect(),
1643            }
1644        }
1645        EffectiveCredentialSource::ProviderStore => {
1646            let connection = stored.ok_or(ModelManagerError::RuntimeCompileFailed)?;
1647            ExecutableCredentialMaterial {
1648                method: connection.auth_method.as_str().to_owned(),
1649                values: connection
1650                    .credential_fields()
1651                    .map(|field| {
1652                        Ok((
1653                            field.clone(),
1654                            connection
1655                                .credential(field)
1656                                .ok_or(ModelManagerError::RuntimeCompileFailed)?
1657                                .to_owned(),
1658                        ))
1659                    })
1660                    .collect::<Result<_, ModelManagerError>>()?,
1661            }
1662        }
1663        EffectiveCredentialSource::NoAuth => ExecutableCredentialMaterial {
1664            method: recipe.default_auth_method.to_owned(),
1665            values: BTreeMap::new(),
1666        },
1667        EffectiveCredentialSource::Unavailable => ExecutableCredentialMaterial {
1668            method: recipe.default_auth_method.to_owned(),
1669            values: BTreeMap::new(),
1670        },
1671    };
1672    if credential_source == EffectiveCredentialSource::ProviderStore {
1673        let connection = stored.expect("store credential source has a connection");
1674        provider.api_key = None;
1675        provider.auth_override = Some(AuthOverride {
1676            method: connection.auth_method.clone(),
1677            values: connection
1678                .credential_fields()
1679                .map(|field| {
1680                    Ok((
1681                        field.clone(),
1682                        SecretString::new("present")
1683                            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1684                    ))
1685                })
1686                .collect::<Result<_, ModelManagerError>>()?,
1687        });
1688    } else if credential_source == EffectiveCredentialSource::NoAuth {
1689        provider.api_key = None;
1690        provider.auth_override = Some(AuthOverride {
1691            method: AuthMethodId::new(recipe.default_auth_method)
1692                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
1693            values: BTreeMap::new(),
1694        });
1695    }
1696    Ok(EffectiveManaged {
1697        provider,
1698        credential_source,
1699        setup_values,
1700        setup_fingerprint,
1701        source_record_digest,
1702        config_fingerprint,
1703        credentials,
1704    })
1705}
1706
1707fn compile_behaviors(
1708    provider_id: &ProviderId,
1709    model: &CompiledDynamicModel,
1710    static_headers: &BTreeMap<crate::HeaderName, crate::SafeStaticHeaderValue>,
1711    credentials: &ExecutableCredentialMaterial,
1712) -> Result<
1713    (
1714        Option<ExecutableBehavior>,
1715        BTreeMap<cookie_agent_identity::VariantId, ExecutableBehavior>,
1716    ),
1717    ModelManagerError,
1718> {
1719    if model.status != CompiledModelStatus::Available {
1720        return Ok((None, BTreeMap::new()));
1721    }
1722    let headers = static_headers
1723        .iter()
1724        .map(|(name, value)| (name.as_str().to_owned(), value.as_str().to_owned()))
1725        .collect::<BTreeMap<_, _>>();
1726    let capabilities = oven_capabilities(&model.capabilities, model.adapter)?;
1727    let base = compile_executable(
1728        provider_id.as_str(),
1729        model,
1730        capabilities.clone(),
1731        headers.clone(),
1732        credentials,
1733        ExecutableBehaviorInput {
1734            defaults: &model.defaults,
1735            options: &model.options,
1736            reasoning: None,
1737        },
1738    )?;
1739    let executable = ExecutableBehavior {
1740        adapter: model.adapter,
1741        model: base.model,
1742        defaults: crate::ResolvedRequestDefaults {
1743            request: model.defaults.clone(),
1744            reasoning: None,
1745        },
1746        provider_options: base.provider_options,
1747        behavior_fingerprint: model.behavior_fingerprint.clone(),
1748    };
1749    let variants = model
1750        .variants
1751        .iter()
1752        .map(|(id, variant)| {
1753            let selected = model.selected(&variant.options, variant.model_id.as_ref())?;
1754            let compiled = compile_executable(
1755                provider_id.as_str(),
1756                &selected,
1757                oven_capabilities(&selected.capabilities, selected.adapter)?,
1758                variant
1759                    .headers
1760                    .iter()
1761                    .map(|(name, value)| (name.as_str().to_owned(), value.as_str().to_owned()))
1762                    .collect(),
1763                credentials,
1764                ExecutableBehaviorInput {
1765                    defaults: &variant.defaults,
1766                    options: &variant.options,
1767                    reasoning: variant.reasoning.as_ref(),
1768                },
1769            )?;
1770            let behavior_fingerprint = safe_hash(
1771                "cookie-agent/model-variant/v1",
1772                &(
1773                    id,
1774                    &variant.defaults,
1775                    &variant.options,
1776                    &variant.reasoning,
1777                    &selected.wire_model_id,
1778                ),
1779            );
1780            Ok((
1781                id.clone(),
1782                ExecutableBehavior {
1783                    adapter: selected.adapter,
1784                    model: compiled.model,
1785                    defaults: crate::ResolvedRequestDefaults {
1786                        request: variant.defaults.clone(),
1787                        reasoning: variant.reasoning.clone(),
1788                    },
1789                    provider_options: compiled.provider_options,
1790                    behavior_fingerprint,
1791                },
1792            ))
1793        })
1794        .collect::<Result<BTreeMap<_, _>, ModelManagerError>>()?;
1795    Ok((Some(executable), variants))
1796}
1797
1798fn mapped_credentials(
1799    source: &ExecutableCredentialMaterial,
1800    target: &CompiledAuthShape,
1801) -> Result<ExecutableCredentialMaterial, ModelManagerError> {
1802    if target.source == AuthSourceCategory::Unavailable {
1803        return Ok(ExecutableCredentialMaterial {
1804            method: target.method.clone(),
1805            values: BTreeMap::new(),
1806        });
1807    }
1808    let values = target
1809        .credential_fields
1810        .iter()
1811        .map(|target_field| {
1812            let source_field =
1813                crate::recipes::compatible_credential_field(&source.method, target_field)
1814                    .ok_or(ModelManagerError::RuntimeCompileFailed)?;
1815            let value = source
1816                .values
1817                .iter()
1818                .find(|(field, _)| field.as_str() == source_field)
1819                .map(|(_, value)| value.clone());
1820            if value.is_none()
1821                && auth_method(&target.method).is_some_and(|method| {
1822                    method
1823                        .credentials
1824                        .iter()
1825                        .any(|field| field.name == target_field && field.required)
1826                })
1827            {
1828                return Err(ModelManagerError::RuntimeCompileFailed);
1829            }
1830            value
1831                .map(|value| {
1832                    AuthFieldName::new(target_field.clone())
1833                        .map(|field| (field, value))
1834                        .map_err(|_| ModelManagerError::RuntimeCompileFailed)
1835                })
1836                .transpose()
1837        })
1838        .filter_map(Result::transpose)
1839        .collect::<Result<BTreeMap<_, _>, _>>()?;
1840    Ok(ExecutableCredentialMaterial {
1841        method: target.method.clone(),
1842        values,
1843    })
1844}
1845
1846#[must_use]
1847pub fn retained_family_match(
1848    provider_id: &ProviderId,
1849    connection: &StoredManagedConnection,
1850) -> RetainedFamilyMatch {
1851    if retained_recipe(connection, provider_id).is_some() {
1852        RetainedFamilyMatch::SupportedRemoved
1853    } else {
1854        RetainedFamilyMatch::RemovedWithoutRetainedFamilyMatch
1855    }
1856}
1857
1858pub(crate) fn validated_provider_source_digest(
1859    record: &crate::catalog::CatalogProviderRecord,
1860) -> Result<Sha256Digest, ModelManagerError> {
1861    canonical_state_fingerprint(b"cookie-agent/catalog-provider-source/v1\0", record)
1862        .map_err(ModelManagerError::from)
1863}
1864
1865pub(crate) fn retained_recipe_fingerprint(
1866    recipe: &FamilyRecipe,
1867    selected_auth_method: &str,
1868) -> Result<Sha256Digest, ModelManagerError> {
1869    let registry = family_registry();
1870    let auth = auth_method(selected_auth_method).ok_or(ModelManagerError::UnsupportedAuthMethod)?;
1871    canonical_state_fingerprint(
1872        b"cookie-agent/retained-provider-recipe/v1\0",
1873        &(
1874            registry.revision(),
1875            COMPILER_VERSION,
1876            recipe,
1877            auth,
1878            "catalog-derived-model-semantics-v1",
1879        ),
1880    )
1881    .map_err(ModelManagerError::from)
1882}
1883
1884pub(crate) fn retained_recipe_package(recipe: &FamilyRecipe) -> Option<&'static str> {
1885    Some(recipe.npm)
1886}
1887
1888fn retained_recipe(
1889    connection: &StoredManagedConnection,
1890    provider_id: &ProviderId,
1891) -> Option<&'static FamilyRecipe> {
1892    retained_recipe_for_endpoint(connection, provider_id, None)
1893}
1894
1895fn retained_recipe_for_endpoint(
1896    connection: &StoredManagedConnection,
1897    provider_id: &ProviderId,
1898    current_endpoint: Option<&str>,
1899) -> Option<&'static FamilyRecipe> {
1900    if &connection.provider_id != provider_id {
1901        return None;
1902    }
1903    let package = connection.policy.package_claim.as_str();
1904    let recipe = family_registry().by_npm(package)?;
1905    let auth = auth_method(connection.auth_method.as_str())?;
1906    let credential_fields = connection
1907        .credential_fields()
1908        .map(AuthFieldName::as_str)
1909        .collect::<Vec<_>>();
1910    let expected_fields = auth
1911        .credentials
1912        .iter()
1913        .map(|credential| credential.name)
1914        .collect::<Vec<_>>();
1915    let recipe_fingerprint =
1916        retained_recipe_fingerprint(recipe, connection.auth_method.as_str()).ok()?;
1917    // Endpoint identity is only enforced when the caller can resolve the
1918    // current catalog endpoint. Callers without catalog access (a
1919    // removed-provider reconnect) cannot perform this check:
1920    // comparing against the family default endpoint would falsely reject
1921    // nested providers whose catalog endpoint differs from the default.
1922    if current_endpoint
1923        .is_some_and(|endpoint| connection.policy.default_endpoint_identity.as_str() != endpoint)
1924    {
1925        return None;
1926    }
1927    (connection.policy.family_id.as_str() == recipe.family.id()
1928        && connection.policy.adapter_id.as_str() == recipe.family.id()
1929        && connection.policy.setup_recipe.as_str() == "family-derived-setup-v1"
1930        && connection.policy.compiler_version.as_str() == COMPILER_VERSION
1931        && recipe
1932            .allowed_auth_methods
1933            .contains(&connection.auth_method.as_str())
1934        && connection.policy.package_claim.as_str() == package
1935        && connection.policy.recipe_fingerprint == recipe_fingerprint
1936        && connection.setup_fingerprint == setup_fingerprint(&connection.setup_values)
1937        && credential_fields == expected_fields)
1938        .then_some(recipe)
1939}
1940
1941fn with_effective_status(
1942    mut model: CompiledDynamicModel,
1943    source: EffectiveCredentialSource,
1944) -> CompiledDynamicModel {
1945    if model.setup.is_none() {
1946        model.status = CompiledModelStatus::SetupUnavailable;
1947    } else if source == EffectiveCredentialSource::Unavailable
1948        || model.auth.source == AuthSourceCategory::Unavailable
1949    {
1950        model.status = CompiledModelStatus::CredentialsUnavailable;
1951    } else {
1952        model.status = CompiledModelStatus::Available;
1953    }
1954    model
1955}
1956
1957fn matched_family(
1958    registry: FamilyRecipeRegistry,
1959    record: &crate::catalog::CatalogProviderRecord,
1960) -> Result<&'static FamilyRecipe, ModelManagerError> {
1961    registry
1962        .classify(record)
1963        .ok_or(ModelManagerError::UnsupportedProvider)
1964}
1965
1966fn normalize_connect(
1967    current: &CompiledModelRuntime,
1968    store: &ProviderStoreSnapshot,
1969    request: ProviderConnectRequest,
1970) -> Result<ConnectMutation, ModelManagerError> {
1971    if matches!(
1972        current.authored.get(&request.provider_id),
1973        Some(ProviderDefinition::Custom(_))
1974    ) {
1975        return Err(ModelManagerError::CustomProviderNotStoreBacked);
1976    }
1977    let (recipe, package_claim, source_record_digest, catalog_revision) = if let Some(record) =
1978        current
1979            .catalog
1980            .provider(&request.provider_id)
1981            .and_then(|entry| entry.record.as_ref())
1982    {
1983        let recipe = matched_family(family_registry(), record)?;
1984        (
1985            recipe,
1986            retained_recipe_package(recipe)
1987                .ok_or(ModelManagerError::RuntimeCompileFailed)?
1988                .to_owned(),
1989            validated_provider_source_digest(record)?,
1990            current.catalog.revision.clone(),
1991        )
1992    } else if let Some(connection) = store.provider(&request.provider_id) {
1993        let recipe = retained_recipe(connection, &request.provider_id)
1994            .ok_or(ModelManagerError::RemovedWithoutRetainedRecipeMatch)?;
1995        (
1996            recipe,
1997            retained_recipe_package(recipe)
1998                .ok_or(ModelManagerError::RemovedWithoutRetainedRecipeMatch)?
1999                .to_owned(),
2000            connection.policy.source_record_digest.clone(),
2001            current.catalog.revision.clone(),
2002        )
2003    } else {
2004        return Err(ModelManagerError::UnknownProvider);
2005    };
2006    let setup_values = request.setup_values.clone();
2007    let method = auth_method(request.auth_method.as_str())
2008        .filter(|method| recipe.allowed_auth_methods.contains(&method.id))
2009        .ok_or(ModelManagerError::UnsupportedAuthMethod)?;
2010    let actual = request
2011        .auth_values
2012        .field_names()
2013        .map(AuthFieldName::as_str)
2014        .collect::<BTreeSet<_>>();
2015    let required = method
2016        .credentials
2017        .iter()
2018        .filter(|field| field.required)
2019        .map(|field| field.name)
2020        .collect::<BTreeSet<_>>();
2021    let allowed = method
2022        .credentials
2023        .iter()
2024        .map(|field| field.name)
2025        .collect::<BTreeSet<_>>();
2026    if !required.is_subset(&actual) || !actual.is_subset(&allowed) {
2027        return Err(ModelManagerError::InvalidCredentials);
2028    }
2029    let endpoint = if let Some(record) = current
2030        .catalog
2031        .provider(&request.provider_id)
2032        .and_then(|entry| entry.record.as_ref())
2033    {
2034        resolved_provider_endpoint(record, recipe, &setup_values)?
2035    } else {
2036        store
2037            .provider(&request.provider_id)
2038            .map(|connection| {
2039                connection
2040                    .policy
2041                    .default_endpoint_identity
2042                    .as_str()
2043                    .to_owned()
2044            })
2045            .ok_or(ModelManagerError::InvalidSetup)?
2046    };
2047    let recipe_fingerprint = retained_recipe_fingerprint(recipe, request.auth_method.as_str())?;
2048    Ok(ConnectMutation {
2049        client_connect_id: request.client_connect_id,
2050        provider_id: request.provider_id,
2051        expected_catalog_revision: request.expected_catalog_revision,
2052        expectation: store.expectation(),
2053        setup_values,
2054        auth_method: request.auth_method,
2055        auth_values: request.auth_values,
2056        policy: StoredProviderPolicyProjection {
2057            catalog_revision,
2058            family_id: SafePolicyString::new(recipe.family.id())
2059                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2060            setup_recipe: ProviderSetupRecipeId::new("family-derived-setup-v1")
2061                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2062            adapter_id: SafePolicyString::new(recipe.family.id())
2063                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2064            compiler_version: RecipeCompilerVersion::new(COMPILER_VERSION)
2065                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2066            default_endpoint_identity: SafePolicyString::new(endpoint)
2067                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2068            package_claim: SafePolicyString::new(package_claim)
2069                .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2070            source_record_digest,
2071            recipe_fingerprint,
2072            model_overrides: BTreeMap::new(),
2073        },
2074    })
2075}
2076
2077fn normalized_setup_values(
2078    recipe: &SetupRecipe,
2079    values: &BTreeMap<SetupFieldId, SafeSetupValue>,
2080) -> Result<BTreeMap<SetupFieldId, SafeSetupValue>, ModelManagerError> {
2081    let validated = validate_setup(recipe, values).map_err(|_| ModelManagerError::InvalidSetup)?;
2082    validated
2083        .values
2084        .into_iter()
2085        .map(|(id, value)| {
2086            Ok((
2087                SetupFieldId::new(id).map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2088                SafeSetupValue::String(
2089                    BoundedSetupString::new(value)
2090                        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2091                ),
2092            ))
2093        })
2094        .collect()
2095}
2096
2097fn resolved_provider_endpoint(
2098    record: &crate::catalog::CatalogProviderRecord,
2099    recipe: &FamilyRecipe,
2100    setup: &BTreeMap<SetupFieldId, SafeSetupValue>,
2101) -> Result<String, ModelManagerError> {
2102    let values = setup
2103        .iter()
2104        .map(|(id, value)| {
2105            let value = match value {
2106                SafeSetupValue::String(value) => value.as_str(),
2107                SafeSetupValue::Code(value) => value.as_str(),
2108                SafeSetupValue::Integer(_) | SafeSetupValue::Bool(_) => {
2109                    return Err(ModelManagerError::InvalidSetup);
2110                }
2111            };
2112            Ok((id.as_str().to_owned(), value.to_owned()))
2113        })
2114        .collect::<Result<BTreeMap<_, _>, ModelManagerError>>()?;
2115    let template = record.api.as_deref().or(recipe.default_endpoint);
2116    if let Some(template) = template {
2117        return crate::recipes::substitute_placeholders(template, &values)
2118            .map(|value| value.trim_end_matches('/').to_owned())
2119            .ok_or(ModelManagerError::InvalidSetup);
2120    }
2121    match recipe.family {
2122        crate::recipes::FamilyKind::Vertex | crate::recipes::FamilyKind::VertexAnthropic => {
2123            let project = values
2124                .get("project")
2125                .ok_or(ModelManagerError::InvalidSetup)?;
2126            let location = values
2127                .get("location")
2128                .ok_or(ModelManagerError::InvalidSetup)?;
2129            Ok(format!(
2130                "https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}"
2131            ))
2132        }
2133        _ => Err(ModelManagerError::InvalidSetup),
2134    }
2135}
2136
2137pub(crate) fn setup_fingerprint(values: &BTreeMap<SetupFieldId, SafeSetupValue>) -> Sha256Digest {
2138    crate::provider_store::setup_fingerprint(values)
2139        .expect("normalized setup values have a canonical provider-store fingerprint")
2140}
2141
2142/// Secret-free exact authored provider fingerprint recorded in manifests.
2143#[must_use]
2144pub fn safe_definition_fingerprint(
2145    provider_id: &ProviderId,
2146    definition: &ProviderDefinition,
2147) -> Sha256Digest {
2148    let value = match definition {
2149        ProviderDefinition::ModelsDev(provider) => json!({
2150            "provider_id": provider_id,
2151            "source": "models_dev",
2152            "base_url": provider.base_url.as_ref().map(crate::EndpointUrl::as_str),
2153            "setup": provider.setup,
2154            "auth": if provider.api_key.is_some() {
2155                json!({
2156                    "source":"api_key",
2157                    "method": "catalog_family_default",
2158                    "fields":["api_key"]
2159                })
2160            } else if let Some(auth) = &provider.auth_override {
2161                json!({"source":"auth_override","method":auth.method,"fields":auth.values.keys().collect::<Vec<_>>()})
2162            } else {
2163                Value::Null
2164            },
2165            "model_overrides": provider.model_overrides.iter().map(|(id, value)| (id.as_str(), json!({
2166                "enabled": value.enabled,
2167                "display_name": value.display_name,
2168                "defaults": value.defaults,
2169                "model_id": value.model_id,
2170                "variants": value.variants,
2171                "default_variant": value.default_variant,
2172                "adaptor_options": value.options,
2173                "pricing": value.pricing,
2174                "headers": value.headers,
2175            }))).collect::<BTreeMap<_, _>>(),
2176            "headers": provider.headers,
2177        }),
2178        ProviderDefinition::Custom(provider) => json!({
2179            "provider_id": provider_id,
2180            "source": "custom",
2181            "endpoint": provider.endpoint.as_str(),
2182            "adaptor": provider.adaptor,
2183            "setup": provider.setup,
2184            "auth": {
2185                "method": provider.auth.method,
2186                "parameters": provider.auth.parameters,
2187                "fields": provider.auth.values.keys().collect::<Vec<_>>(),
2188            },
2189            "headers": provider.headers,
2190            "models": provider.models.iter().map(|(id, value)| (id.as_str(), json!({
2191                "enabled": value.enabled,
2192                "display_name": value.display_name,
2193                "capabilities": value.capabilities,
2194                "model_id": value.model_id,
2195                "defaults": value.defaults,
2196                "options": value.options,
2197                "headers": value.headers,
2198                "variants": value.variants,
2199                "default_variant": value.default_variant,
2200            }))).collect::<BTreeMap<_, _>>(),
2201        }),
2202    };
2203    let mut value = value;
2204    let cache = match definition {
2205        ProviderDefinition::ModelsDev(provider) => provider.cache.as_ref(),
2206        ProviderDefinition::Custom(provider) => provider.cache.as_ref(),
2207    };
2208    if let Some(cache) = cache {
2209        value
2210            .as_object_mut()
2211            .expect("provider fingerprint input is an object")
2212            .insert(
2213                "cache".into(),
2214                serde_json::to_value(cache).expect("provider cache serializes"),
2215            );
2216    }
2217    safe_hash("cookie-agent/authored-provider-definition/v1", &value)
2218}
2219
2220fn no_authored_override_fingerprint() -> Sha256Digest {
2221    safe_hash(
2222        "cookie-agent/authored-provider-definition/v1",
2223        &"no-authored-override",
2224    )
2225}
2226
2227fn safe_hash(domain: &str, value: &impl Serialize) -> Sha256Digest {
2228    Sha256Digest::hash(domain, value).expect("safe model state serializes")
2229}
2230
2231fn revision<T, E>(
2232    domain: &str,
2233    value: &impl Serialize,
2234    constructor: impl FnOnce(String) -> Result<T, E>,
2235) -> Result<T, ModelManagerError> {
2236    let mut hasher = Sha256::new();
2237    hasher.update(domain.as_bytes());
2238    hasher.update([0]);
2239    hasher.update(serde_json::to_vec(value).map_err(|_| ModelManagerError::RuntimeCompileFailed)?);
2240    constructor(format!("sha256:{:x}", hasher.finalize()))
2241        .map_err(|_| ModelManagerError::RuntimeCompileFailed)
2242}
2243
2244fn protocol_digest(value: &Sha256Digest) -> protocol::Sha256Digest {
2245    protocol::Sha256Digest::new(value.as_str()).expect("model digest is protocol-safe")
2246}
2247
2248fn protocol_setup_values(
2249    values: &BTreeMap<SetupFieldId, SafeSetupValue>,
2250) -> Result<BTreeMap<SetupFieldId, protocol::SafeSetupValue>, ModelManagerError> {
2251    values
2252        .iter()
2253        .map(|(id, value)| {
2254            let value = serde_json::from_value(
2255                serde_json::to_value(value).map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2256            )
2257            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
2258            Ok((id.clone(), value))
2259        })
2260        .collect()
2261}
2262
2263fn protocol_options(
2264    model: &CompiledDynamicModel,
2265    setup: &BTreeMap<SetupFieldId, SafeSetupValue>,
2266) -> Result<protocol::ProviderOptions, ModelManagerError> {
2267    let string = |name: &str| {
2268        setup
2269            .iter()
2270            .find(|(id, _)| id.as_str() == name)
2271            .and_then(|(_, value)| match value {
2272                SafeSetupValue::String(value) => Some(value.as_str().to_owned()),
2273                SafeSetupValue::Code(value) => Some(value.as_str().to_owned()),
2274                SafeSetupValue::Integer(_) | SafeSetupValue::Bool(_) => None,
2275            })
2276    };
2277    Ok(match model.adapter {
2278        OvenAdapterFamily::Anthropic | OvenAdapterFamily::AnthropicCompatible => {
2279            protocol::ProviderOptions::Anthropic {
2280                api_version: model.options.api_version.clone(),
2281                beta: model.options.beta.clone(),
2282            }
2283        }
2284        OvenAdapterFamily::OpenaiChat => protocol::ProviderOptions::OpenAiChat {
2285            organization: model.options.organization.clone(),
2286            project: model.options.project.clone(),
2287        },
2288        OvenAdapterFamily::OpenaiResponses => protocol::ProviderOptions::OpenAiResponses {
2289            organization: model.options.organization.clone(),
2290            project: model.options.project.clone(),
2291            store: model.options.store,
2292        },
2293        OvenAdapterFamily::OpenaiCompatible => protocol::ProviderOptions::OpenAiCompatible {
2294            api_path: model.options.api_path.clone(),
2295        },
2296        OvenAdapterFamily::GoogleGemini => protocol::ProviderOptions::GoogleGemini {
2297            api_version: model.options.api_version.clone(),
2298        },
2299        OvenAdapterFamily::GoogleVertexGemini => protocol::ProviderOptions::GoogleVertexGemini {
2300            project: string("project").ok_or(ModelManagerError::RuntimeCompileFailed)?,
2301            location: string("location").ok_or(ModelManagerError::RuntimeCompileFailed)?,
2302        },
2303        OvenAdapterFamily::AwsBedrockConverse => protocol::ProviderOptions::AwsBedrockConverse {
2304            region: string("region").ok_or(ModelManagerError::RuntimeCompileFailed)?,
2305        },
2306        OvenAdapterFamily::AzureOpenaiChat => protocol::ProviderOptions::AzureOpenAiChat {
2307            deployment: model.wire_model_id.as_str().to_owned(),
2308            api_version: string("api_version").unwrap_or_else(|| "v1".to_owned()),
2309        },
2310        OvenAdapterFamily::AzureOpenaiResponses => {
2311            protocol::ProviderOptions::AzureOpenAiResponses {
2312                deployment: model.wire_model_id.as_str().to_owned(),
2313                api_version: string("api_version").unwrap_or_else(|| "v1".to_owned()),
2314            }
2315        }
2316        OvenAdapterFamily::CohereV2Chat => protocol::ProviderOptions::CohereV2Chat {
2317            api_version: model.options.api_version.clone(),
2318        },
2319    })
2320}
2321
2322fn frozen_defaults(
2323    value: &crate::ResolvedRequestDefaults,
2324) -> Result<FrozenResolvedRequestDefaults, ModelManagerError> {
2325    let temperature = value
2326        .request
2327        .temperature
2328        .map(|value| NormalizedDecimal::from_f32(value.get()))
2329        .transpose()
2330        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
2331    let top_p = value
2332        .request
2333        .top_p
2334        .map(|value| NormalizedDecimal::from_f32(value.get()))
2335        .transpose()
2336        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
2337    let tool_choice = value
2338        .request
2339        .tool_choice
2340        .as_ref()
2341        .map(|choice| {
2342            serde_json::from_value(
2343                serde_json::to_value(choice)
2344                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2345            )
2346            .map_err(|_| ModelManagerError::RuntimeCompileFailed)
2347        })
2348        .transpose()?;
2349    let reasoning = value
2350        .reasoning
2351        .as_ref()
2352        .map(|reasoning| {
2353            serde_json::from_value(
2354                serde_json::to_value(reasoning)
2355                    .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2356            )
2357            .map_err(|_| ModelManagerError::RuntimeCompileFailed)
2358        })
2359        .transpose()?;
2360    let frozen = FrozenResolvedRequestDefaults {
2361        request: protocol::FrozenRequestDefaults {
2362            temperature,
2363            top_p,
2364            max_output_tokens: value.request.max_output_tokens,
2365            stop: value.request.stop.clone(),
2366            seed: value.request.seed,
2367            tool_choice,
2368        },
2369        reasoning,
2370    };
2371    frozen
2372        .validate()
2373        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
2374    Ok(frozen)
2375}
2376
2377fn safe_descriptor(
2378    provider_id: &ProviderId,
2379    model: &CompiledDynamicModel,
2380) -> Result<LanguageModelDescriptor, ModelManagerError> {
2381    LanguageModelDescriptor::new(
2382        ModelIdentity::new(
2383            OvenProviderId::new(provider_id.as_str()),
2384            ModelId::new(model.id.as_str()),
2385        )
2386        .map_err(|_| ModelManagerError::RuntimeCompileFailed)?,
2387        AdapterId::new(model.adapter_id.clone()),
2388        oven_capabilities(&model.capabilities, model.adapter)?,
2389    )
2390    .map(|descriptor| {
2391        descriptor.with_provider_metadata(BTreeMap::from([
2392            (
2393                "cookie_agent.wire_model_id".into(),
2394                Value::String(model.wire_model_id.as_str().into()),
2395            ),
2396            (
2397                "cookie_agent.wire_provider_id".into(),
2398                Value::String(
2399                    crate::compiler::executable_provider_id(
2400                        provider_id.as_str(),
2401                        model.adapter,
2402                        model.custom,
2403                    )
2404                    .into(),
2405                ),
2406            ),
2407        ]))
2408    })
2409    .map_err(|_| ModelManagerError::RuntimeCompileFailed)
2410}
2411
2412fn oven_capabilities(
2413    value: &crate::ModelCapabilities,
2414    adapter: OvenAdapterFamily,
2415) -> Result<OvenCapabilities, ModelManagerError> {
2416    let mut features = Capability::MAX_OUTPUT_TOKENS;
2417    if value.tool_calling {
2418        features |= Capability::TOOL_CALLING;
2419    }
2420    if value.parallel_tool_calls {
2421        features |= Capability::PARALLEL_TOOLS;
2422    }
2423    if value.structured_output {
2424        features |= Capability::STRUCTURED_OUTPUT;
2425    }
2426    if value.reasoning {
2427        features |= Capability::REASONING;
2428    }
2429    if value.temperature {
2430        features |= Capability::TEMPERATURE;
2431    }
2432    if value.top_p {
2433        features |= Capability::TOP_P;
2434    }
2435    if matches!(
2436        adapter,
2437        OvenAdapterFamily::Anthropic
2438            | OvenAdapterFamily::AnthropicCompatible
2439            | OvenAdapterFamily::GoogleGemini
2440            | OvenAdapterFamily::GoogleVertexGemini
2441            | OvenAdapterFamily::AwsBedrockConverse
2442    ) {
2443        features |= Capability::PROMPT_CACHING;
2444    }
2445    let modality = |value: &crate::Modality| match value {
2446        crate::Modality::Text => OvenModality::text(),
2447        crate::Modality::Image => OvenModality::image(),
2448        crate::Modality::Audio => OvenModality::audio(),
2449        crate::Modality::Pdf => OvenModality::pdf(),
2450        crate::Modality::Video => OvenModality::video(),
2451    };
2452    let media = value
2453        .media
2454        .iter()
2455        .map(|(kind, support)| {
2456            let modality = modality(&match kind {
2457                crate::MediaKind::Image => crate::Modality::Image,
2458                crate::MediaKind::Audio => crate::Modality::Audio,
2459                crate::MediaKind::Pdf => crate::Modality::Pdf,
2460                crate::MediaKind::Video => crate::Modality::Video,
2461            });
2462            let support = MediaInputSupport::new(
2463                support
2464                    .mime_types
2465                    .iter()
2466                    .map(|mime| mime.as_str().to_owned()),
2467                MediaSourceSupport::INLINE_BYTES,
2468            )
2469            .map_err(|_| ModelManagerError::RuntimeCompileFailed)?;
2470            Ok((modality, support))
2471        })
2472        .collect::<Result<BTreeMap<_, _>, ModelManagerError>>()?;
2473    Ok(OvenCapabilities {
2474        features,
2475        limits: ModelLimits::new(Some(value.context_tokens), None, Some(value.output_tokens)),
2476        modalities: Modalities::new(
2477            value.input.iter().map(modality),
2478            value.output.iter().map(modality),
2479        ),
2480        media: MediaCapabilities { input: media },
2481        cancellation: match value.cancellation {
2482            crate::CancellationCapability::LocalOnly => OvenCancellation::LocalOnly,
2483            crate::CancellationCapability::Provider => OvenCancellation::RemoteBestEffort,
2484        },
2485        compaction: match value.compaction {
2486            crate::CompactionCapability::Unsupported => OvenCompaction::Unsupported,
2487            crate::CompactionCapability::Native => OvenCompaction::Native,
2488        },
2489        replay: ReplayDeclaration {
2490            policy: if value.native_replay == crate::ReplayCapability::Unsupported {
2491                ReplayPolicy::Never
2492            } else {
2493                ReplayPolicy::IfValid
2494            },
2495            capability: match value.native_replay {
2496                crate::ReplayCapability::Unsupported => OvenReplay::Unsupported,
2497                crate::ReplayCapability::Optional => OvenReplay::Optional,
2498                crate::ReplayCapability::Required => OvenReplay::Required,
2499            },
2500            reasoning: value.reasoning
2501                && adapter != OvenAdapterFamily::AzureOpenaiChat
2502                && value.native_replay != crate::ReplayCapability::Unsupported,
2503        },
2504    })
2505}
2506
2507fn prepared_retained(
2508    current: &Arc<BTreeMap<ModelRevision, Vec<Arc<CompiledModelRuntime>>>>,
2509    candidate: &Arc<CompiledModelRuntime>,
2510) -> Arc<BTreeMap<ModelRevision, Vec<Arc<CompiledModelRuntime>>>> {
2511    let mut retained = current.as_ref().clone();
2512    retained
2513        .entry(candidate.model_revision.clone())
2514        .or_default()
2515        .push(Arc::clone(candidate));
2516    Arc::new(retained)
2517}
2518
2519#[cfg(test)]
2520mod cache_strategy_tests;
2521
2522fn mutation_provider(mutation: &ProviderStoreMutation) -> &ProviderId {
2523    match mutation {
2524        ProviderStoreMutation::Connect {
2525            durable_connection, ..
2526        } => &durable_connection.provider_id,
2527        ProviderStoreMutation::Disconnect { provider_id, .. } => provider_id,
2528    }
2529}
2530
2531/// Maps a failed commit to a manager error. A lost commit-time compare-and-swap
2532/// is a distinct retryable outcome; the client layer resubmits the identical
2533/// request (same id) exactly once. The manager never replays it internally.
2534fn map_commit_error(error: ProviderStoreError) -> ModelManagerError {
2535    match error {
2536        ProviderStoreError::Stale => ModelManagerError::Contention,
2537        error => ModelManagerError::ProviderStore(error),
2538    }
2539}
2540
2541fn effective_auth_for(
2542    runtime: &CompiledModelRuntime,
2543    provider_id: &ProviderId,
2544) -> EffectiveCredentialSource {
2545    runtime
2546        .providers
2547        .iter()
2548        .find(|provider| &provider.id == provider_id)
2549        .map_or(EffectiveCredentialSource::Unavailable, |provider| {
2550            provider.effective_auth
2551        })
2552}
2553
2554fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
2555    mutex.lock().unwrap_or_else(|error| error.into_inner())
2556}
2557
2558#[derive(Debug, Error)]
2559pub enum ModelManagerError {
2560    #[error("unknown_provider")]
2561    UnknownProvider,
2562    #[error("unsupported_provider")]
2563    UnsupportedProvider,
2564    #[error("removed_without_retained_recipe_match")]
2565    RemovedWithoutRetainedRecipeMatch,
2566    #[error("custom_provider_not_store_backed")]
2567    CustomProviderNotStoreBacked,
2568    #[error("invalid_setup")]
2569    InvalidSetup,
2570    #[error("unsupported_auth_method")]
2571    UnsupportedAuthMethod,
2572    #[error("invalid_credentials")]
2573    InvalidCredentials,
2574    #[error("runtime_compile_failed")]
2575    RuntimeCompileFailed,
2576    #[error("unknown_model")]
2577    UnknownModel(ModelKey),
2578    #[error("unknown_variant")]
2579    UnknownVariant(ModelSelection),
2580    #[error("model_unavailable")]
2581    ModelUnavailable(ModelKey),
2582    #[error("duplicate model `{0}`")]
2583    DuplicateModel(ModelKey),
2584    #[error("dynamic model compilation failed: {0}")]
2585    DynamicCompile(#[from] DynamicCompileError),
2586    #[error("invalid cache config for provider `{provider}`: {reason}")]
2587    ProviderCache {
2588        provider: ProviderId,
2589        reason: String,
2590    },
2591    #[error("runtime_compile_failed")]
2592    ExecutableBuild(#[from] ModelBuildError),
2593    #[error("provider_store_reload_failed")]
2594    ProviderStore(#[from] ProviderStoreError),
2595    #[error("provider store contention")]
2596    Contention,
2597    #[error("model manifest construction failed")]
2598    Manifest(#[from] crate::manifests::ManifestError),
2599}