@@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
2323use serde_json:: { json, Value } ;
2424use tokio:: sync:: { broadcast, Mutex } ;
2525use tokio:: time:: interval;
26- use tracing:: warn;
26+ use tracing:: { info , warn} ;
2727use utoipa:: { IntoParams , OpenApi , ToSchema } ;
2828
2929use crate :: router:: {
@@ -656,21 +656,38 @@ fn default_agent_mode() -> &'static str {
656656}
657657
658658async fn opencode_model_cache ( state : & OpenCodeAppState ) -> OpenCodeModelCache {
659- {
660- let cache = state. opencode . model_cache . lock ( ) . await ;
661- if let Some ( cache) = cache. as_ref ( ) {
662- return cache. clone ( ) ;
663- }
659+ // Keep this lock for the full build to enforce singleflight behavior.
660+ // Concurrent requests wait for the same in-flight build instead of
661+ // spawning duplicate provider/model fetches.
662+ let mut slot = state. opencode . model_cache . lock ( ) . await ;
663+ if let Some ( cache) = slot. as_ref ( ) {
664+ info ! (
665+ entries = cache. entries. len( ) ,
666+ groups = cache. group_names. len( ) ,
667+ connected = cache. connected. len( ) ,
668+ "opencode model cache hit"
669+ ) ;
670+ return cache. clone ( ) ;
664671 }
665672
673+ let started = std:: time:: Instant :: now ( ) ;
674+ info ! ( "opencode model cache miss; building cache" ) ;
666675 let cache = build_opencode_model_cache ( state) . await ;
667- let mut slot = state. opencode . model_cache . lock ( ) . await ;
676+ info ! (
677+ elapsed_ms = started. elapsed( ) . as_millis( ) as u64 ,
678+ entries = cache. entries. len( ) ,
679+ groups = cache. group_names. len( ) ,
680+ connected = cache. connected. len( ) ,
681+ "opencode model cache built"
682+ ) ;
668683 * slot = Some ( cache. clone ( ) ) ;
669684 cache
670685}
671686
672687async fn build_opencode_model_cache ( state : & OpenCodeAppState ) -> OpenCodeModelCache {
688+ let started = std:: time:: Instant :: now ( ) ;
673689 // Check credentials upfront
690+ let creds_started = std:: time:: Instant :: now ( ) ;
674691 let credentials = match tokio:: task:: spawn_blocking ( || {
675692 extract_all_credentials ( & CredentialExtractionOptions :: new ( ) )
676693 } )
@@ -684,6 +701,10 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
684701 } ;
685702 let has_anthropic = credentials. anthropic . is_some ( ) ;
686703 let has_openai = credentials. openai . is_some ( ) ;
704+ info ! (
705+ elapsed_ms = creds_started. elapsed( ) . as_millis( ) as u64 ,
706+ has_anthropic, has_openai, "opencode model cache credential scan complete"
707+ ) ;
687708
688709 let mut entries = Vec :: new ( ) ;
689710 let mut model_lookup = HashMap :: new ( ) ;
@@ -693,11 +714,38 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
693714 let mut group_names: HashMap < String , String > = HashMap :: new ( ) ;
694715 let mut default_model: Option < String > = None ;
695716
696- for agent in available_agent_ids ( ) {
697- let response = match state. inner . session_manager ( ) . agent_models ( agent) . await {
717+ let agents = available_agent_ids ( ) ;
718+ let manager = state. inner . session_manager ( ) ;
719+ let fetches = agents. iter ( ) . copied ( ) . map ( |agent| {
720+ let manager = manager. clone ( ) ;
721+ async move {
722+ let agent_started = std:: time:: Instant :: now ( ) ;
723+ let response = manager. agent_models ( agent) . await ;
724+ ( agent, agent_started. elapsed ( ) , response)
725+ }
726+ } ) ;
727+ let fetch_results = futures:: future:: join_all ( fetches) . await ;
728+
729+ for ( agent, elapsed, response) in fetch_results {
730+ let response = match response {
698731 Ok ( response) => response,
699- Err ( _) => continue ,
732+ Err ( err) => {
733+ warn ! (
734+ agent = agent. as_str( ) ,
735+ elapsed_ms = elapsed. as_millis( ) as u64 ,
736+ ?err,
737+ "opencode model cache failed fetching agent models"
738+ ) ;
739+ continue ;
740+ }
700741 } ;
742+ info ! (
743+ agent = agent. as_str( ) ,
744+ elapsed_ms = elapsed. as_millis( ) as u64 ,
745+ model_count = response. models. len( ) ,
746+ has_default = response. default_model. is_some( ) ,
747+ "opencode model cache fetched agent models"
748+ ) ;
701749
702750 let first_model_id = response. models . first ( ) . map ( |model| model. id . clone ( ) ) ;
703751 for model in response. models {
@@ -805,7 +853,7 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
805853 }
806854 }
807855
808- OpenCodeModelCache {
856+ let cache = OpenCodeModelCache {
809857 entries,
810858 model_lookup,
811859 group_defaults,
@@ -814,7 +862,17 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
814862 default_group,
815863 default_model,
816864 connected,
817- }
865+ } ;
866+ info ! (
867+ elapsed_ms = started. elapsed( ) . as_millis( ) as u64 ,
868+ entries = cache. entries. len( ) ,
869+ groups = cache. group_names. len( ) ,
870+ connected = cache. connected. len( ) ,
871+ default_group = cache. default_group. as_str( ) ,
872+ default_model = cache. default_model. as_str( ) ,
873+ "opencode model cache build complete"
874+ ) ;
875+ cache
818876}
819877
820878fn resolve_agent_from_model (
@@ -1123,8 +1181,16 @@ async fn proxy_native_opencode(
11231181 headers : & HeaderMap ,
11241182 body : Option < Value > ,
11251183) -> Option < Response > {
1126- let Some ( base_url) = state. opencode . proxy_base_url ( ) else {
1127- return None ;
1184+ let base_url = if let Some ( base_url) = state. opencode . proxy_base_url ( ) {
1185+ base_url. to_string ( )
1186+ } else {
1187+ match state. inner . ensure_opencode_server ( ) . await {
1188+ Ok ( base_url) => base_url,
1189+ Err ( err) => {
1190+ warn ! ( path, ?err, "failed to lazily start native opencode server" ) ;
1191+ return None ;
1192+ }
1193+ }
11281194 } ;
11291195
11301196 let mut request = state
0 commit comments