Merge pull request #3271 from Aneesh-382005/fix/local-model-validation-bypass

fix: generalize local-model validation bypass beyond Ollama tags
This commit is contained in:
Bellman authored and GitHub committed 2026-10-13 07:13:39 +09:00
commit 8aeef8d2e2
6 files changed
+89 -7

No files matched your search

+17
View File
@@ -262,4 +262,21 @@ mod tests {
other => panic!("Expected ProviderClient::OpenAi for local model, got: {other:?}"),
}
}
#[test]
fn is_local_openai_compatible_base_url_is_reachable_via_crate_root() {
// Regression: this helper must stay `pub` and re-exported from the
// crate root so `rusty-claude-cli`'s model-syntax validation can
// reuse the same local/private-address detection used here to
// waive the API-key requirement.
assert!(crate::is_local_openai_compatible_base_url(
"http://127.0.0.1:1234/v1"
));
assert!(crate::is_local_openai_compatible_base_url(
"http://192.168.1.50:8000/v1"
));
assert!(!crate::is_local_openai_compatible_base_url(
"https://openrouter.ai/api/v1"
));
}
}
+3 -3
View File
@@ -22,9 +22,9 @@ pub use prompt_cache::{
pub use providers::anthropic::{AnthropicClient, AnthropicClient as ApiClient, AuthSource};
pub use providers::openai_compat::{
build_chat_completion_request, check_request_body_size, estimate_request_body_size,
flatten_tool_result_content, is_reasoning_model, model_rejects_is_error_field,
model_requires_reasoning_content_in_history, translate_message, OpenAiCompatClient,
OpenAiCompatConfig,
flatten_tool_result_content, is_local_openai_compatible_base_url, is_reasoning_model,
model_rejects_is_error_field, model_requires_reasoning_content_in_history, translate_message,
OpenAiCompatClient, OpenAiCompatConfig,
};
pub use providers::{
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
@@ -1027,7 +1027,12 @@ fn url_host(url: &str) -> &str {
host_port.split(':').next().unwrap_or("")
}
fn is_local_openai_compatible_base_url(url: &str) -> bool {
/// Returns true when `url`'s host is loopback or a private-network address
/// (`localhost`, `127.0.0.1`, `10.x`, `192.168.x`, `172.16-31.x`). Used to
/// waive auth requirements for local OpenAI-compatible servers (Ollama, LM
/// Studio, llama.cpp, vLLM) that don't expect a real API key.
#[must_use]
pub fn is_local_openai_compatible_base_url(url: &str) -> bool {
let host = url_host(url.trim());
if host.eq_ignore_ascii_case("localhost") || host == "::1" {
return true;
+45 -1
View File
@@ -2987,7 +2987,18 @@ fn is_local_openai_model_syntax(model: &str) -> bool {
if let Some(rest) = model.strip_prefix("local/") {
return !rest.is_empty() && rest.split('/').all(|segment| !segment.is_empty());
}
std::env::var_os("OPENAI_BASE_URL").is_some() && (model.contains(':') || model.contains('.'))
let Ok(base_url) = std::env::var("OPENAI_BASE_URL") else {
return false;
};
// A local/private OPENAI_BASE_URL (Ollama, LM Studio, llama.cpp, vLLM, ...)
// is trusted to accept any non-empty model name the server actually
// serves, matching the OLLAMA_HOST bypass above. Remote gateways keep the
// stricter dot/colon-tag heuristic so a bare typo doesn't silently route
// to a paid hosted API.
if api::is_local_openai_compatible_base_url(&base_url) {
return !model.is_empty();
}
model.contains(':') || model.contains('.')
}
fn config_alias_for_current_dir(alias: &str) -> Option<String> {
@@ -19947,4 +19958,37 @@ mod alias_resolution_tests {
// Empty model still rejected
assert!(validate_model_syntax("").is_err());
}
#[test]
fn local_openai_base_url_bypasses_bare_model_names() {
let _guard = ollama_env_lock();
let _ollama_env = EnvVarGuard::unset("OLLAMA_HOST");
// LM Studio's default port; a bare alias with no dot/colon/slash,
// as LM Studio and llama.cpp commonly register models under.
let _openai_env = EnvVarGuard::set("OPENAI_BASE_URL", "http://127.0.0.1:1234/v1");
assert!(validate_model_syntax("mistral").is_ok());
assert!(validate_model_syntax("local-7b").is_ok());
// Empty model is still rejected even when the base URL is local.
assert!(validate_model_syntax("").is_err());
}
#[test]
fn private_network_openai_base_url_bypasses_bare_model_names() {
let _guard = ollama_env_lock();
let _ollama_env = EnvVarGuard::unset("OLLAMA_HOST");
let _openai_env = EnvVarGuard::set("OPENAI_BASE_URL", "http://192.168.1.50:8000/v1");
assert!(validate_model_syntax("mistral").is_ok());
}
#[test]
fn remote_openai_base_url_still_requires_provider_model_or_tag_syntax() {
let _guard = ollama_env_lock();
let _ollama_env = EnvVarGuard::unset("OLLAMA_HOST");
let _openai_env = EnvVarGuard::set("OPENAI_BASE_URL", "https://openrouter.ai/api/v1");
// A bare name with no dot/colon/slash against a remote gateway must
// keep failing so a typo doesn't silently route to a paid hosted API.
assert!(validate_model_syntax("mistral").is_err());
// The existing dot/colon-tag heuristic still applies for remote hosts.
assert!(validate_model_syntax("qwen2.5-coder:7b").is_ok());
}
}