feat(login): custom provider logins fetch and select models

After the base URL, name, and key steps, the login now opens a model
selection screen instead of blindly saving. The listing fetch runs
concurrently with a manual-id input — a WAF challenge page or a 404 no
longer passes validation silently: a base without a /vN tail is probed
once as {base}/v1 and the working base is adopted, an anthropic-wire
listing that 401s on x-api-key retries once with Bearer (relays gate
GET /models on Bearer while /messages takes the key header), and a
failed fetch shows its reason while manual add stays usable. The chosen
ids persist as [platforms.<name>] models = [...]; catalog assembly
synthesizes selected ids the listing never served (before [model.*]
overrides so they compose) and filters unselected provider entries
(after overrides, authoritative in the provider namespace). Provider-
owned entries keep their declared wire against donor backend
inheritance, and listing errors quote line/column only — a reflected
body could carry the key.
This commit is contained in:
雷电芽衣 committed 2026-09-29 22:00:20 -04:00
1 parent 1d631a07fa
commit 9a2a9eac99
20 files changed
+1655 -68

No files matched your search

+12
View File
@@ -165,6 +165,18 @@ pub fn normalize_base_url(raw: &str) -> Result<String, CustomProviderError> {
Ok(url.to_owned())
}
/// A manually added model id: no whitespace, no controls, bounded length.
pub fn validate_model_id(id: &str) -> Result<(), CustomProviderError> {
let err = |reason: &str| Err(CustomProviderError::Name(reason.to_owned()));
if id.is_empty() || id.len() > 128 {
return err("model id must be 1 to 128 characters");
}
if id.chars().any(|c| c.is_whitespace() || c.is_control()) {
return err("model id must not contain whitespace");
}
Ok(())
}
/// A valid provider name derived from a host.
pub fn default_name(base_url: &str) -> String {
let host = base_url