@@ -1036,6 +1036,86 @@ pub fn parse_pi_models_output(text: &str) -> Vec<String> {
10361036 models. into_iter ( ) . collect ( )
10371037}
10381038
1039+ /// Parse `omp models --json`, preserving selectors with scoped or nested model IDs.
1040+ pub fn parse_omp_models_output ( text : & str ) -> Vec < String > {
1041+ let Ok ( value) = serde_json:: from_str :: < serde_json:: Value > ( text) else {
1042+ return Vec :: new ( ) ;
1043+ } ;
1044+ let Some ( entries) = value. get ( "models" ) . and_then ( serde_json:: Value :: as_array) else {
1045+ return Vec :: new ( ) ;
1046+ } ;
1047+
1048+ let mut models = std:: collections:: BTreeSet :: new ( ) ;
1049+ for entry in entries {
1050+ if let Some ( selector) = entry. get ( "selector" ) . and_then ( serde_json:: Value :: as_str) {
1051+ if !selector. is_empty ( ) {
1052+ models. insert ( selector. to_string ( ) ) ;
1053+ continue ;
1054+ }
1055+ }
1056+ if let ( Some ( provider) , Some ( id) ) = (
1057+ entry. get ( "provider" ) . and_then ( serde_json:: Value :: as_str) ,
1058+ entry. get ( "id" ) . and_then ( serde_json:: Value :: as_str) ,
1059+ ) {
1060+ if !provider. is_empty ( ) && !id. is_empty ( ) {
1061+ models. insert ( format ! ( "{provider}/{id}" ) ) ;
1062+ }
1063+ }
1064+ }
1065+ models. into_iter ( ) . collect ( )
1066+ }
1067+
1068+ async fn get_available_omp_models ( ) -> Vec < String > {
1069+ let candidates = if cfg ! ( target_os = "windows" ) {
1070+ let appdata = std:: env:: var ( "APPDATA" ) . unwrap_or_default ( ) ;
1071+ let mut list = Vec :: new ( ) ;
1072+ if !appdata. is_empty ( ) {
1073+ list. push ( format ! ( "{}\\ npm\\ omp.cmd" , appdata) ) ;
1074+ list. push ( format ! ( "{}\\ npm\\ omp.exe" , appdata) ) ;
1075+ }
1076+ list. push ( "omp" . to_string ( ) ) ;
1077+ list. push ( "omp.exe" . to_string ( ) ) ;
1078+ list
1079+ } else {
1080+ let home = std:: env:: var ( "HOME" ) . unwrap_or_default ( ) ;
1081+ vec ! [
1082+ format!( "{}/.bun/bin/omp" , home) ,
1083+ "omp" . to_string( ) ,
1084+ format!( "{}/.local/bin/omp" , home) ,
1085+ "/usr/local/bin/omp" . to_string( ) ,
1086+ "/opt/homebrew/bin/omp" . to_string( ) ,
1087+ format!( "{}/.local/share/mise/shims/omp" , home) ,
1088+ format!( "{}/.asdf/shims/omp" , home) ,
1089+ format!( "{}/.volta/bin/omp" , home) ,
1090+ ]
1091+ } ;
1092+
1093+ for bin in & candidates {
1094+ if let Some ( text) = run_bounded_binary ( bin, & [ "models" , "--json" ] ) . await {
1095+ let models = parse_omp_models_output ( & text) ;
1096+ if !models. is_empty ( ) {
1097+ return models;
1098+ }
1099+ }
1100+ }
1101+
1102+ if cfg ! ( target_os = "windows" ) {
1103+ if let Some ( bin) = resolve_via_where ( "omp" ) . await {
1104+ if let Some ( text) = run_bounded_binary ( & bin, & [ "models" , "--json" ] ) . await {
1105+ let models = parse_omp_models_output ( & text) ;
1106+ if !models. is_empty ( ) {
1107+ return models;
1108+ }
1109+ }
1110+ }
1111+ }
1112+
1113+ if let Some ( text) = run_via_login_shell ( "omp models --json" . to_string ( ) ) . await {
1114+ return parse_omp_models_output ( & text) ;
1115+ }
1116+ Vec :: new ( )
1117+ }
1118+
10391119#[ tauri:: command]
10401120pub async fn get_available_pi_models ( ) -> Vec < String > {
10411121 // GUI apps on macOS don't inherit shell PATH; try common locations.
@@ -1108,6 +1188,10 @@ pub async fn get_available_pi_models() -> Vec<String> {
11081188 }
11091189 }
11101190
1191+ let omp_models = get_available_omp_models ( ) . await ;
1192+ if !omp_models. is_empty ( ) {
1193+ return omp_models;
1194+ }
11111195 // Last resort: resolve `pi` through the user's login shell. This is the
11121196 // universal fallback for version managers (mise/nvm/fnm) that install into
11131197 // per-version dirs the candidates above can't enumerate — the login shell
@@ -1318,9 +1402,9 @@ pub fn get_db_health(state: State<'_, AppState>) -> db::DbHealth {
13181402#[ cfg( test) ]
13191403mod tests {
13201404 use super :: {
1321- opencode_desktop_detected_for_env, parse_pi_models_output , pick_first_line ,
1322- prepare_embedding_probe_options , run_bounded_binary , windows_opencode_candidates ,
1323- DesktopPlatform , OpencodeDesktopEnv , OPENCODE_DESKTOP_APP_IDS ,
1405+ opencode_desktop_detected_for_env, parse_omp_models_output , parse_pi_models_output ,
1406+ pick_first_line , prepare_embedding_probe_options , run_bounded_binary ,
1407+ windows_opencode_candidates , DesktopPlatform , OpencodeDesktopEnv , OPENCODE_DESKTOP_APP_IDS ,
13241408 } ;
13251409 use crate :: embedding_probe:: EmbeddingProbeOutcome ;
13261410 use std:: path:: { Path , PathBuf } ;
@@ -1576,6 +1660,19 @@ mod tests {
15761660 assert_eq ! ( result, vec![ "anthropic/claude-sonnet-4-5" ] ) ;
15771661 }
15781662
1663+ #[ test]
1664+ fn test_parse_omp_models_output_preserves_selectors ( ) {
1665+ let input = r#"{"models":[{"provider":"anthropic","id":"claude-opus-4-5","selector":"anthropic/claude-opus-4-5"},{"provider":"modal","id":"@modal/qwen/model-v1","selector":"modal/@modal/qwen/model-v1"},{"provider":"openai","id":"fallback/model"}]}"# ;
1666+ assert_eq ! (
1667+ parse_omp_models_output( input) ,
1668+ vec![
1669+ "anthropic/claude-opus-4-5" ,
1670+ "modal/@modal/qwen/model-v1" ,
1671+ "openai/fallback/model" ,
1672+ ]
1673+ ) ;
1674+ }
1675+
15791676 #[ test]
15801677 fn test_pick_first_line ( ) {
15811678 assert_eq ! ( pick_first_line( "" ) , None ) ;
0 commit comments