From 0aed6974566d3101365f7b33e9c2c1c0923c4ba6 Mon Sep 17 00:00:00 2001 From: Ben <2bmillerb@gmail.com> Date: Fri, 25 Sep 2026 08:39:40 -0700 Subject: [PATCH 1/2] feat(openai): request a Daybreak cyber access program on supporting models ChatGPT/Codex accounts approved for OpenAI's Daybreak program select it per request with `access_programs: {"cyber": "daybreak_blue"}`, as the Codex CLI does. The model catalog lists the programs each model accepts in `available_access_programs.cyber` (gpt-6-sol, gpt-6-luna and the gpt-5.6 family list daybreak_blue, gpt-6-astra does not, and gpt-daybreak-blue-latest lists only daybreak_blue). Parse and cache those per-model programs, and add `provider.openai_cyber_access_program` (env JCODE_OPENAI_CYBER_ACCESS_PROGRAM). When set, ChatGPT-mode requests and persistent WebSocket continuations carry the program only for models whose catalog entry lists it. Astra and models without catalog data are unchanged, avoiding the backend's 403 for Astra. Default is unset. --- crates/jcode-base/src/config.rs | 1 + crates/jcode-base/src/config/default_file.rs | 3 + crates/jcode-base/src/config/env_overrides.rs | 7 +++ crates/jcode-base/src/provider/mod.rs | 6 +- crates/jcode-base/src/provider/models.rs | 22 ++++++++ .../jcode-base/src/provider/models_catalog.rs | 52 +++++++++++++++++ .../provider/tests/catalog_subscription.rs | 1 + crates/jcode-config-types/src/lib.rs | 6 ++ .../jcode-provider-openai-runtime/src/lib.rs | 56 ++++++++++++++++++- .../src/openai_provider_impl.rs | 13 +++++ .../src/openai_stream_runtime.rs | 3 + .../src/openai_tests/payloads.rs | 41 ++++++++++++++ 12 files changed, 206 insertions(+), 5 deletions(-) diff --git a/crates/jcode-base/src/config.rs b/crates/jcode-base/src/config.rs index af5f0bec5a..82cabcbcfc 100644 --- a/crates/jcode-base/src/config.rs +++ b/crates/jcode-base/src/config.rs @@ -190,6 +190,7 @@ const CONFIG_ENV_KEYS: &[&str] = &[ "JCODE_NTFY_TOPIC", "JCODE_OPENAI_NATIVE_COMPACTION_MODE", "JCODE_OPENAI_NATIVE_COMPACTION_THRESHOLD_TOKENS", + "JCODE_OPENAI_CYBER_ACCESS_PROGRAM", "JCODE_OPENAI_REASONING_EFFORT", "JCODE_OPENAI_SERVICE_TIER", "JCODE_OPENAI_TRANSPORT", diff --git a/crates/jcode-base/src/config/default_file.rs b/crates/jcode-base/src/config/default_file.rs index affbe3183d..a0c69a0f15 100644 --- a/crates/jcode-base/src/config/default_file.rs +++ b/crates/jcode-base/src/config/default_file.rs @@ -441,6 +441,9 @@ openai_reasoning_effort = "low" # Defaults to `priority` to match Codex /fast behavior for OpenAI OAuth # (higher speed, higher usage). Set to "off" (or "standard") to disable. openai_service_tier = "priority" +# Cyber access program for OpenAI OAuth, sent only to models whose catalog +# lists it (for example Daybreak Blue on GPT-5.6/6 Sol). Needs an approved account. +# openai_cyber_access_program = "daybreak_blue" # Preserve provider-native reasoning/thinking for future-turn context when supported. # Applies to OpenRouter, Anthropic, and OpenAI native reasoning replay. Display is separate. preserve_reasoning_context = true diff --git a/crates/jcode-base/src/config/env_overrides.rs b/crates/jcode-base/src/config/env_overrides.rs index eb1b0ecb9f..5bd30ac45a 100644 --- a/crates/jcode-base/src/config/env_overrides.rs +++ b/crates/jcode-base/src/config/env_overrides.rs @@ -872,6 +872,13 @@ impl Config { } } } + if let Ok(v) = std::env::var("JCODE_OPENAI_CYBER_ACCESS_PROGRAM") { + let trimmed = v.trim().to_ascii_lowercase(); + self.provider.openai_cyber_access_program = match trimmed.as_str() { + "" | "off" | "none" => None, + _ => Some(trimmed), + }; + } if let Ok(v) = std::env::var("JCODE_PRESERVE_REASONING_CONTEXT") { if let Some(parsed) = parse_env_bool(&v) { self.provider.preserve_reasoning_context = parsed; diff --git a/crates/jcode-base/src/provider/mod.rs b/crates/jcode-base/src/provider/mod.rs index ded7a6c315..a3c4fae957 100644 --- a/crates/jcode-base/src/provider/mod.rs +++ b/crates/jcode-base/src/provider/mod.rs @@ -329,9 +329,9 @@ pub use self::models::{ ModelCatalogHttpStatus, OpenAIModelCatalog, anthropic_catalog_scope_for_route, begin_anthropic_model_catalog_refresh, begin_anthropic_model_catalog_refresh_for_scope, begin_openai_model_catalog_refresh, cached_anthropic_model_ids, - cached_anthropic_model_ids_for_scope, cached_context_limit_for_model, cached_openai_model_ids, - cached_openai_reasoning_efforts, clear_all_model_unavailability_for_account, - clear_all_provider_unavailability_for_account, + cached_anthropic_model_ids_for_scope, cached_context_limit_for_model, + cached_openai_cyber_access_programs, cached_openai_cyber_access_programs_for_scope, cached_openai_model_ids, cached_openai_reasoning_efforts, + clear_all_model_unavailability_for_account, clear_all_provider_unavailability_for_account, clear_claude_provider_unavailability_for_account_label, clear_model_unavailable_for_account, clear_openai_provider_unavailability_for_account_label, clear_provider_unavailable_for_account, context_limit_for_model, context_limit_for_model_with_provider, fetch_anthropic_model_catalog, diff --git a/crates/jcode-base/src/provider/models.rs b/crates/jcode-base/src/provider/models.rs index d3f0631c1a..7ea8aaec6c 100644 --- a/crates/jcode-base/src/provider/models.rs +++ b/crates/jcode-base/src/provider/models.rs @@ -42,6 +42,8 @@ struct PersistedModelCatalogScope { context_limits: HashMap, #[serde(default)] reasoning_efforts: HashMap>, + #[serde(default)] + cyber_access_programs: HashMap>, observed_at_unix_secs: u64, } @@ -428,6 +430,7 @@ fn persist_scoped_model_catalog( models: &[String], context_limits: &HashMap, reasoning_efforts: &HashMap>, + cyber_access_programs: &HashMap>, observed_at: SystemTime, ) { if models.is_empty() { @@ -447,6 +450,7 @@ fn persist_scoped_model_catalog( models: models.to_vec(), context_limits: context_limits.clone(), reasoning_efforts: reasoning_efforts.clone(), + cyber_access_programs: cyber_access_programs.clone(), observed_at_unix_secs: observed_at_unix_secs(observed_at), }, ); @@ -524,6 +528,22 @@ pub fn cached_openai_reasoning_efforts_for_scope( (!efforts.is_empty()).then_some(efforts) } +/// Return the cyber access programs each OpenAI model accepts for the active +/// account, from its scoped disk snapshot. +pub fn cached_openai_cyber_access_programs() -> Option>> { + cached_openai_cyber_access_programs_for_scope(¤t_openai_account_scope()) +} + +/// Return the cyber access programs each OpenAI model accepts for `scope`, +/// from that scope's disk snapshot. +pub fn cached_openai_cyber_access_programs_for_scope( + scope: &str, +) -> Option>> { + let store = load_persisted_model_catalog_store(OPENAI_MODEL_CATALOG_CACHE_FILE)?; + let programs = store.scopes.get(scope)?.cyber_access_programs.clone(); + (!programs.is_empty()).then_some(programs) +} + /// Test-only: clear the process-global in-memory model catalogs. The catalog /// services are statics shared by every test in the process; a test that /// hydrates a scope (directly or via `persist_*` + `cached_*`) otherwise leaks @@ -545,6 +565,7 @@ pub fn persist_openai_model_catalog_for_scope(scope: &str, catalog: &OpenAIModel &catalog.available_models, &catalog.context_limits, &catalog.reasoning_efforts, + &catalog.cyber_access_programs, SystemTime::now(), ); } @@ -560,6 +581,7 @@ pub fn persist_anthropic_model_catalog_for_scope(scope: &str, catalog: &Anthropi &catalog.available_models, &catalog.context_limits, &HashMap::new(), + &HashMap::new(), SystemTime::now(), ); } diff --git a/crates/jcode-base/src/provider/models_catalog.rs b/crates/jcode-base/src/provider/models_catalog.rs index df0bddf419..600abeb267 100644 --- a/crates/jcode-base/src/provider/models_catalog.rs +++ b/crates/jcode-base/src/provider/models_catalog.rs @@ -29,6 +29,9 @@ pub struct OpenAIModelCatalog { pub context_limits: HashMap, /// Ordered reasoning-effort values advertised by each Codex model. pub reasoning_efforts: HashMap>, + /// Cyber access programs each Codex model accepts in + /// `access_programs.cyber` (for example `standard`, `daybreak_blue`). + pub cyber_access_programs: HashMap>, } #[derive(Debug, Clone, Default)] @@ -90,6 +93,7 @@ pub(crate) fn parse_openai_model_catalog(data: &serde_json::Value) -> OpenAIMode let mut available: HashSet = HashSet::new(); let mut limits: HashMap = HashMap::new(); let mut reasoning_efforts: HashMap> = HashMap::new(); + let mut cyber_access_programs: HashMap> = HashMap::new(); for model in models.into_iter().flatten() { let Some(slug) = model @@ -116,6 +120,22 @@ pub(crate) fn parse_openai_model_catalog(data: &serde_json::Value) -> OpenAIMode limits.insert(slug.clone(), ctx as usize); } + if let Some(programs) = model + .get("available_access_programs") + .and_then(|value| value.get("cyber")) + .and_then(|value| value.as_array()) + { + let programs: Vec = programs + .iter() + .filter_map(|value| value.as_str()) + .map(|value| value.trim().to_ascii_lowercase()) + .filter(|value| !value.is_empty()) + .collect(); + if !programs.is_empty() { + cyber_access_programs.insert(slug.clone(), programs); + } + } + if let Some(values) = model .get("supported_reasoning_levels") .or_else(|| model.get("supported_reasoning_efforts")) @@ -151,6 +171,7 @@ pub(crate) fn parse_openai_model_catalog(data: &serde_json::Value) -> OpenAIMode available_models, context_limits: limits, reasoning_efforts, + cyber_access_programs, } } @@ -330,6 +351,7 @@ pub async fn fetch_openai_api_key_model_catalog(api_key: &str) -> Result Result, /// Preserve provider-native reasoning/thinking items for future-turn context when supported. pub preserve_reasoning_context: bool, /// How to handle cross-provider failover when the same input would be resent elsewhere. @@ -1316,6 +1321,7 @@ impl Default for ProviderConfig { openai_service_tier: Some("priority".to_string()), openai_native_compaction_mode: "auto".to_string(), openai_native_compaction_threshold_tokens: 200_000, + openai_cyber_access_program: None, preserve_reasoning_context: true, cross_provider_failover: CrossProviderFailoverMode::Countdown, same_provider_account_failover: true, diff --git a/crates/jcode-provider-openai-runtime/src/lib.rs b/crates/jcode-provider-openai-runtime/src/lib.rs index 5a7902d0d3..4f687d271c 100644 --- a/crates/jcode-provider-openai-runtime/src/lib.rs +++ b/crates/jcode-provider-openai-runtime/src/lib.rs @@ -87,6 +87,20 @@ fn model_supports_image_generation(model_id: &str) -> bool { !model_id.to_ascii_lowercase().contains("codex") } +/// Pick the cyber access program for a request. Returns the requested program +/// only when the model's catalog entry lists it, so unsupported models (and +/// models without catalog data) keep sending no `access_programs` field. +fn select_cyber_access_program(requested: &str, supported: Option<&[String]>) -> Option { + let requested = requested.trim().to_ascii_lowercase(); + if requested.is_empty() { + return None; + } + supported? + .iter() + .any(|program| program.eq_ignore_ascii_case(&requested)) + .then_some(requested) +} + /// Maximum number of retries for transient errors const MAX_RETRIES: u32 = 3; @@ -710,6 +724,10 @@ pub struct OpenAIProvider { max_output_tokens: Option, reasoning_effort: Arc>>, model_reasoning_efforts: Arc>>>, + /// Cyber access programs each model accepts, from the Codex catalog. + model_cyber_access_programs: Arc>>>, + /// Requested cyber access program (`provider.openai_cyber_access_program`). + cyber_access_program: Option, service_tier: Arc>>, native_compaction_mode: OpenAINativeCompactionMode, native_compaction_threshold_tokens: usize, @@ -838,6 +856,15 @@ impl OpenAIProvider { let model_reasoning_efforts = jcode_base::provider::cached_openai_reasoning_efforts_for_scope(&catalog_scope) .unwrap_or_default(); + let model_cyber_access_programs = + jcode_base::provider::cached_openai_cyber_access_programs_for_scope(&catalog_scope) + .unwrap_or_default(); + let cyber_access_program = jcode_base::config::config() + .provider + .openai_cyber_access_program + .as_deref() + .map(|program| program.trim().to_ascii_lowercase()) + .filter(|program| !program.is_empty() && program != "off" && program != "none"); let provider = Self { client: jcode_provider_core::shared_http_client(), @@ -850,6 +877,8 @@ impl OpenAIProvider { max_output_tokens, reasoning_effort: Arc::new(StdRwLock::new(reasoning_effort)), model_reasoning_efforts: Arc::new(StdRwLock::new(model_reasoning_efforts)), + model_cyber_access_programs: Arc::new(StdRwLock::new(model_cyber_access_programs)), + cyber_access_program, service_tier: Arc::new(StdRwLock::new(service_tier)), native_compaction_mode, native_compaction_threshold_tokens, @@ -954,9 +983,28 @@ impl OpenAIProvider { Ok(mut efforts) => *efforts = cached, Err(poisoned) => *poisoned.into_inner() = cached, } + let programs = + jcode_base::provider::cached_openai_cyber_access_programs().unwrap_or_default(); + match self.model_cyber_access_programs.write() { + Ok(mut guard) => *guard = programs, + Err(poisoned) => *poisoned.into_inner() = programs, + } self.revalidate_reasoning_effort(); } + /// The cyber access program to send for `model_id`, if the user requested one + /// and the model's catalog entry lists it. Models without catalog data, or + /// that do not list the program (for example Astra for Daybreak Blue), get + /// no `access_programs` field so their requests are unchanged. + fn cyber_access_program_for_model(&self, model_id: &str) -> Option { + let requested = self.cyber_access_program.as_deref()?; + let programs = self + .model_cyber_access_programs + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + select_cyber_access_program(requested, programs.get(model_id).map(Vec::as_slice)) + } + pub(crate) fn credential_mode_snapshot(&self) -> OpenAICredentialMode { self.credential_mode .try_read() @@ -1303,7 +1351,7 @@ impl OpenAIProvider { .unwrap_or_else(|poisoned| poisoned.into_inner().clone()); let native_compaction_threshold = self.native_compaction_threshold_for_context_window(self.context_window()); - Self::build_response_request( + let mut request = Self::build_response_request( model_id, system.to_string(), input, @@ -1315,7 +1363,11 @@ impl OpenAIProvider { self.prompt_cache_key.as_deref(), self.prompt_cache_retention.as_deref(), native_compaction_threshold, - ) + ); + if is_chatgpt_mode && let Some(program) = self.cyber_access_program_for_model(model_id) { + request["access_programs"] = serde_json::json!({ "cyber": program }); + } + request } #[expect( diff --git a/crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs b/crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs index 7c282d001b..a6ddbf5fd9 100644 --- a/crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs +++ b/crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs @@ -954,6 +954,10 @@ impl Provider for OpenAIProvider { Ok(mut efforts) => *efforts = catalog.reasoning_efforts.clone(), Err(poisoned) => *poisoned.into_inner() = catalog.reasoning_efforts.clone(), } + match self.model_cyber_access_programs.write() { + Ok(mut programs) => *programs = catalog.cyber_access_programs.clone(), + Err(poisoned) => *poisoned.into_inner() = catalog.cyber_access_programs.clone(), + } self.revalidate_reasoning_effort(); jcode_base::provider::persist_openai_model_catalog_for_scope(&scope, &catalog); if !catalog.context_limits.is_empty() { @@ -1231,6 +1235,15 @@ impl Provider for OpenAIProvider { .unwrap_or_else(|p| p.into_inner()) .clone(), )), + // Per-account catalog metadata, refreshed beside the efforts map: + // a fork pinned to another account must not write into the parent's. + model_cyber_access_programs: Arc::new(StdRwLock::new( + self.model_cyber_access_programs + .read() + .unwrap_or_else(|p| p.into_inner()) + .clone(), + )), + cyber_access_program: self.cyber_access_program.clone(), service_tier: Arc::new(StdRwLock::new(self.service_tier())), native_compaction_mode: self.native_compaction_mode, native_compaction_threshold_tokens: self.native_compaction_threshold_tokens, diff --git a/crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs b/crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs index a6838496e8..f774286cb3 100644 --- a/crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs +++ b/crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs @@ -901,6 +901,9 @@ async fn continue_persistent_ws_locked( if let Some(prompt_cache_retention) = request.get("prompt_cache_retention") { continuation_request["prompt_cache_retention"] = prompt_cache_retention.clone(); } + if let Some(access_programs) = request.get("access_programs") { + continuation_request["access_programs"] = access_programs.clone(); + } continuation_request["store"] = serde_json::json!(false); continuation_request["parallel_tool_calls"] = serde_json::json!(false); diff --git a/crates/jcode-provider-openai-runtime/src/openai_tests/payloads.rs b/crates/jcode-provider-openai-runtime/src/openai_tests/payloads.rs index 40c86398b4..aad25f77e5 100644 --- a/crates/jcode-provider-openai-runtime/src/openai_tests/payloads.rs +++ b/crates/jcode-provider-openai-runtime/src/openai_tests/payloads.rs @@ -1,3 +1,44 @@ +#[test] +fn test_base_chatgpt_request_has_no_access_programs() { + let request = OpenAIProvider::build_response_request( + "gpt-5.4", + "system".to_string(), + &[], + &[], + true, + None, + None, + None, + None, + None, + None, + ); + assert!( + request.get("access_programs").is_none(), + "the base request never carries access_programs" + ); +} + +#[test] +fn test_cyber_access_program_sent_only_to_models_that_list_it() { + let sol = vec!["standard".to_string(), "daybreak_blue".to_string()]; + let astra = vec!["standard".to_string()]; + assert_eq!( + select_cyber_access_program("daybreak_blue", Some(&sol)), + Some("daybreak_blue".to_string()) + ); + assert_eq!( + select_cyber_access_program(" Daybreak_Blue ", Some(&sol)), + Some("daybreak_blue".to_string()) + ); + assert_eq!( + select_cyber_access_program("daybreak_blue", Some(&astra)), + None + ); + assert_eq!(select_cyber_access_program("daybreak_blue", None), None); + assert_eq!(select_cyber_access_program("", Some(&sol)), None); +} + #[test] fn test_build_response_request_includes_stream_for_http() { let request = OpenAIProvider::build_response_request( From d2b07fb78f76c768eeea739ebce65790bfaf2869 Mon Sep 17 00:00:00 2001 From: Ben <2bmillerb@gmail.com> Date: Fri, 25 Sep 2026 08:53:33 -0700 Subject: [PATCH 2/2] fix(catalog): refresh snapshots written before cyber access programs were persisted A catalog cache written by an older build has no cyber_access_programs, and a recent one counts as fresh, so the configured program would not be sent until the TTL expired. Version the persisted scope and hydrate older snapshots as stale (keeping their real observed_at) so a live refresh runs at once. --- .../src/provider/model_catalog_service.rs | 33 +++++++++++++++++++ crates/jcode-base/src/provider/models.rs | 16 ++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/jcode-base/src/provider/model_catalog_service.rs b/crates/jcode-base/src/provider/model_catalog_service.rs index 543bbe43aa..ea66c4819d 100644 --- a/crates/jcode-base/src/provider/model_catalog_service.rs +++ b/crates/jcode-base/src/provider/model_catalog_service.rs @@ -104,6 +104,21 @@ impl ModelCatalogService { self.replace_scope_models_inner(scope, models, observed_at, fetched_at) } + /// Hydrate from a snapshot that must not count as fresh (for example one + /// written before the persisted schema gained fields), keeping its real + /// `observed_at` for display while letting the next refresh run at once. + pub(crate) fn hydrate_scope_models_from_stale_snapshot( + &self, + scope: &str, + models: HashSet, + observed_at: SystemTime, + ) -> bool { + let fetched_at = Instant::now() + .checked_sub(self.cache_ttl + Duration::from_secs(1)) + .unwrap_or_else(Instant::now); + self.replace_scope_models_inner(scope, models, observed_at, fetched_at) + } + fn replace_scope_models_inner( &self, scope: &str, @@ -393,6 +408,24 @@ mod tests { assert!(!service.should_refresh("default")); } + #[test] + fn hydrating_old_schema_snapshot_forces_refresh_but_keeps_observed_at() { + let service = service(); + let models = HashSet::from(["gpt-5.5".to_string()]); + + // Even a seconds-old snapshot must refresh when its schema is outdated. + let observed_at = SystemTime::now() - Duration::from_secs(5); + assert!(service.hydrate_scope_models_from_stale_snapshot("default", models, observed_at)); + + assert_eq!( + service.model_ids("default"), + Some(vec!["gpt-5.5".to_string()]) + ); + assert!(!service.is_fresh("default")); + assert!(service.should_refresh("default")); + assert_eq!(service.observed_at("default"), Some(observed_at)); + } + #[test] fn empty_model_sets_do_not_replace_existing_catalog() { let service = service(); diff --git a/crates/jcode-base/src/provider/models.rs b/crates/jcode-base/src/provider/models.rs index 7ea8aaec6c..f06484e733 100644 --- a/crates/jcode-base/src/provider/models.rs +++ b/crates/jcode-base/src/provider/models.rs @@ -30,6 +30,11 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; const OPENAI_MODEL_CATALOG_CACHE_FILE: &str = "openai_model_catalog_cache.json"; const ANTHROPIC_MODEL_CATALOG_CACHE_FILE: &str = "anthropic_model_catalog_cache.json"; +/// Bump when a persisted catalog scope gains a field parsed from the live +/// catalog. Older snapshots still hydrate for display, but are treated as stale +/// so the next refresh fetches the new fields instead of waiting out the TTL. +const MODEL_CATALOG_SCHEMA_VERSION: u32 = 2; + #[derive(Debug, Clone, Default, Serialize, Deserialize)] struct PersistedModelCatalogStore { scopes: HashMap, @@ -44,6 +49,8 @@ struct PersistedModelCatalogScope { reasoning_efforts: HashMap>, #[serde(default)] cyber_access_programs: HashMap>, + #[serde(default)] + schema_version: u32, observed_at_unix_secs: u64, } @@ -451,6 +458,7 @@ fn persist_scoped_model_catalog( context_limits: context_limits.clone(), reasoning_efforts: reasoning_efforts.clone(), cyber_access_programs: cyber_access_programs.clone(), + schema_version: MODEL_CATALOG_SCHEMA_VERSION, observed_at_unix_secs: observed_at_unix_secs(observed_at), }, ); @@ -480,7 +488,13 @@ fn hydrate_catalog_cache_from_disk( } let observed_at = system_time_from_unix_secs(persisted.observed_at_unix_secs); - service.hydrate_scope_models_from_snapshot(scope, normalized, observed_at); + // A snapshot from an older schema lacks newer catalog fields. Hydrate it as + // stale so a live refresh fetches them promptly instead of after the TTL. + if persisted.schema_version < MODEL_CATALOG_SCHEMA_VERSION { + service.hydrate_scope_models_from_stale_snapshot(scope, normalized, observed_at); + } else { + service.hydrate_scope_models_from_snapshot(scope, normalized, observed_at); + } if !persisted.context_limits.is_empty() { populate_context_limits(persisted.context_limits.clone()); }