1use 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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum ProviderPresence {
79 Current,
80 Removed,
81}
82
83#[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#[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#[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#[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 #[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 #[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 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 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#[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 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#[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#[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
1029pub 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(¤t, &store_snapshot, request)?;
1107 match transaction.propose_connect(&mutation, ¤t.catalog.revision)? {
1108 ConnectProposal::Replay(replayed) => {
1109 let mutation = *replayed;
1110 Ok(ModelMutationResult {
1111 effective_auth: effective_auth_for(¤t, 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(¤t.authored),
1121 Arc::clone(¤t.global_headers),
1122 Arc::clone(¤t.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(¤t, 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(¤t.authored),
1184 Arc::clone(¤t.global_headers),
1185 Arc::clone(¤t.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 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(¤t.authored),
1218 Arc::clone(¤t.global_headers),
1219 Arc::clone(¤t.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 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(¤t.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 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 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#[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
2531fn 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}