Skip to content

Commit 5026632

Browse files
committed
Show custom provider models in picker
1 parent 6d0f606 commit 5026632

1 file changed

Lines changed: 129 additions & 2 deletions

File tree

crates/jcode-base/src/provider/catalog_routes.rs

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,92 @@ fn append_openai_routes(
389389
}
390390
}
391391

392-
/// Configured OpenAI-compatible profiles (NVIDIA NIM, Groq, ...), excluding
393-
/// the active direct profile which contributes through the OpenRouter path.
392+
fn named_provider_profile_routes(
393+
profile_name: &str,
394+
profile: &crate::config::NamedProviderConfig,
395+
) -> Vec<ModelRoute> {
396+
let mut models = profile
397+
.models
398+
.iter()
399+
.filter(|model| {
400+
model.input.is_empty()
401+
|| model
402+
.input
403+
.iter()
404+
.any(|input| input.eq_ignore_ascii_case("text"))
405+
})
406+
.map(|model| model.id.trim().to_string())
407+
.filter(|model| !model.is_empty())
408+
.collect::<Vec<_>>();
409+
410+
let default_model = profile
411+
.default_model
412+
.as_deref()
413+
.map(str::trim)
414+
.filter(|model| !model.is_empty());
415+
if let Some(default_model) = default_model {
416+
if !models.iter().any(|model| model == default_model) {
417+
models.push(default_model.to_string());
418+
}
419+
}
420+
421+
let available = match profile.auth {
422+
crate::config::NamedProviderAuth::None => true,
423+
crate::config::NamedProviderAuth::Bearer | crate::config::NamedProviderAuth::Header => {
424+
profile
425+
.requires_api_key
426+
.map(|requires_key| !requires_key)
427+
.unwrap_or(false)
428+
|| profile
429+
.api_key
430+
.as_deref()
431+
.map(str::trim)
432+
.is_some_and(|key| !key.is_empty())
433+
|| profile
434+
.api_key_env
435+
.as_deref()
436+
.map(str::trim)
437+
.filter(|key_env| !key_env.is_empty())
438+
.is_some_and(|key_env| {
439+
std::env::var_os(key_env).is_some()
440+
|| profile
441+
.env_file
442+
.as_deref()
443+
.map(str::trim)
444+
.filter(|env_file| !env_file.is_empty())
445+
.is_some_and(|env_file| {
446+
crate::provider_catalog::load_env_value_from_env_or_config(
447+
key_env, env_file,
448+
)
449+
.is_some()
450+
})
451+
})
452+
}
453+
};
454+
let detail = if available {
455+
profile.base_url.clone()
456+
} else {
457+
"no credentials".to_string()
458+
};
459+
let api_method = format!("openai-compatible:{profile_name}");
460+
461+
models
462+
.into_iter()
463+
.filter(|model| is_listable_model_name(model))
464+
.map(|model| ModelRoute {
465+
model,
466+
provider: profile_name.to_string(),
467+
api_method: api_method.clone(),
468+
available,
469+
detail: detail.clone(),
470+
cheapness: None,
471+
})
472+
.collect()
473+
}
474+
475+
/// Configured OpenAI-compatible profiles (NVIDIA NIM, Groq, ...), plus custom
476+
/// `[providers.<name>]` profiles, excluding the active direct profile which
477+
/// contributes through the OpenRouter path.
394478
/// Returns whether any routes were added.
395479
fn append_openai_compatible_profile_routes(
396480
provider: &MultiProvider,
@@ -423,6 +507,17 @@ fn append_openai_compatible_profile_routes(
423507
added_any |= !profile_routes.is_empty();
424508
routes.extend(profile_routes);
425509
}
510+
511+
for (profile_name, profile) in &crate::config::config().providers {
512+
let api_method = format!("openai-compatible:{profile_name}");
513+
if active_direct_openai_compatible_api_method.as_deref() == Some(api_method.as_str()) {
514+
continue;
515+
}
516+
517+
let profile_routes = named_provider_profile_routes(profile_name, profile);
518+
added_any |= !profile_routes.is_empty();
519+
routes.extend(profile_routes);
520+
}
426521
added_any
427522
}
428523

@@ -1188,6 +1283,38 @@ mod tests {
11881283
}
11891284
}
11901285

1286+
#[test]
1287+
fn named_provider_profile_routes_include_configured_text_models() {
1288+
let profile = crate::config::NamedProviderConfig {
1289+
base_url: "https://llm.example.test/v1".to_string(),
1290+
auth: crate::config::NamedProviderAuth::None,
1291+
default_model: Some("gateway/default-model".to_string()),
1292+
models: vec![
1293+
crate::config::NamedProviderModelConfig {
1294+
id: "gateway/text-model".to_string(),
1295+
input: vec!["text".to_string()],
1296+
..Default::default()
1297+
},
1298+
crate::config::NamedProviderModelConfig {
1299+
id: "gateway/image-only".to_string(),
1300+
input: vec!["image".to_string()],
1301+
..Default::default()
1302+
},
1303+
],
1304+
..Default::default()
1305+
};
1306+
1307+
let routes = named_provider_profile_routes("my-gateway", &profile);
1308+
let models = routes.iter().map(|route| route.model.as_str()).collect::<Vec<_>>();
1309+
1310+
assert_eq!(models, vec!["gateway/text-model", "gateway/default-model"]);
1311+
assert!(routes.iter().all(|route| route.provider == "my-gateway"));
1312+
assert!(routes
1313+
.iter()
1314+
.all(|route| route.api_method == "openai-compatible:my-gateway"));
1315+
assert!(routes.iter().all(|route| route.available));
1316+
}
1317+
11911318
#[test]
11921319
fn remote_compatible_route_uses_live_cache_and_does_not_mark_fallback() {
11931320
let guard = EnvGuard::new();

0 commit comments

Comments
 (0)