fix(provider): gate Responses API by endpoint host + demote first 404 (#4209)

This commit is contained in:
Mega Mind
2026-06-28 21:25:23 -07:00
committed by GitHub
parent 5239ef3ceb
commit a2daa8c657
3 changed files with 181 additions and 8 deletions
+150 -1
View File
@@ -229,6 +229,65 @@ pub fn builtin_cloud_supports_responses_api(slug: &str) -> bool {
matches!(slug, "openai")
}
/// Extract the lowercased authority host from an endpoint URL, dropping the
/// scheme, any userinfo, the port, and the path. Returns `None` when no host
/// can be parsed. Tolerant of a missing scheme and of IPv6 literals.
pub(crate) fn endpoint_host(endpoint: &str) -> Option<String> {
let s = endpoint.trim();
// Drop the scheme (`https://…`); tolerate a bare `host/path` form.
let after_scheme = s.split_once("://").map(|(_, rest)| rest).unwrap_or(s);
// The authority ends at the first path / query / fragment delimiter.
let authority = after_scheme
.split(['/', '?', '#'])
.next()
.unwrap_or(after_scheme);
// Strip any `user:pass@` userinfo prefix.
let host_port = authority
.rsplit_once('@')
.map(|(_, host)| host)
.unwrap_or(authority);
// Strip the port, handling bracketed IPv6 literals (`[::1]:8080`).
let host = if let Some(rest) = host_port.strip_prefix('[') {
rest.split_once(']').map(|(h, _)| h).unwrap_or(rest)
} else {
host_port
.rsplit_once(':')
.map(|(h, _)| h)
.unwrap_or(host_port)
};
let host = host.trim().to_ascii_lowercase();
(!host.is_empty()).then_some(host)
}
/// Whether an endpoint **host** is a known cloud host that does NOT serve the
/// OpenAI Responses API (`/v1/responses`) — i.e. it is chat-completions-only,
/// regardless of which user slug points at it.
///
/// Derived entirely from [`BUILTIN_CLOUD_PROVIDERS`]: a host is chat-only when
/// some built-in preset uses it AND no preset at that host advertises the
/// Responses API (only OpenAI's `api.openai.com` does). This closes the
/// custom-slug gap behind the builtin-slug gate
/// ([`builtin_cloud_supports_responses_api`]): a user slug pointed at, e.g.,
/// `integrate.api.nvidia.com` must never attempt `/responses` (TAURI-RUST-5A1),
/// while a genuinely unknown proxy host keeps the permissive fallback so a real
/// OpenAI proxy still gets `/responses`.
pub fn endpoint_host_is_chat_completions_only(endpoint: &str) -> bool {
let Some(host) = endpoint_host(endpoint) else {
return false;
};
let mut matched_chat_only = false;
for provider in BUILTIN_CLOUD_PROVIDERS {
if endpoint_host(provider.endpoint).as_deref() == Some(host.as_str()) {
if builtin_cloud_supports_responses_api(provider.slug) {
// A Responses-capable built-in lives at this host → not chat-only.
return false;
}
matched_chat_only = true;
}
}
matched_chat_only
}
/// Authentication header style for a cloud provider.
///
/// Wire format is lowercase (e.g. `"bearer"`). Determines which HTTP headers
@@ -504,7 +563,8 @@ impl CloudProviderType {
#[cfg(test)]
mod tests {
use super::{
builtin_cloud_supports_responses_api, is_builtin_cloud_slug, is_slug_reserved,
builtin_cloud_supports_responses_api, endpoint_host,
endpoint_host_is_chat_completions_only, is_builtin_cloud_slug, is_slug_reserved,
migrate_legacy_fields, AuthStyle, CloudProviderCreds, BUILTIN_CLOUD_PROVIDERS,
};
@@ -634,4 +694,93 @@ mod tests {
);
}
}
#[test]
fn endpoint_host_parses_scheme_path_and_port() {
assert_eq!(
endpoint_host("https://integrate.api.nvidia.com/v1").as_deref(),
Some("integrate.api.nvidia.com")
);
// Missing scheme, mixed case, trailing path.
assert_eq!(
endpoint_host("API.OpenAI.com/v1/chat").as_deref(),
Some("api.openai.com")
);
// Userinfo + explicit port are stripped.
assert_eq!(
endpoint_host("https://user:pass@api.groq.com:443/openai/v1").as_deref(),
Some("api.groq.com")
);
// Bracketed IPv6 literal with port.
assert_eq!(
endpoint_host("http://[::1]:8080/v1").as_deref(),
Some("::1")
);
assert_eq!(endpoint_host(" ").as_deref(), None);
}
/// TAURI-RUST-5A1: a *custom* slug pointed at a known chat-only host (NVIDIA)
/// must be classified chat-only so the factory disables the guaranteed-404
/// `/responses` fallback — the builtin-slug gate alone misses this because
/// the slug is not builtin.
#[test]
fn nvidia_host_is_chat_completions_only_regardless_of_slug() {
assert!(endpoint_host_is_chat_completions_only(
"https://integrate.api.nvidia.com/v1"
));
// Other chat-only built-in hosts too.
for endpoint in [
"https://api.deepseek.com/v1",
"https://api.groq.com/openai/v1",
"https://api.mistral.ai/v1",
] {
assert!(
endpoint_host_is_chat_completions_only(endpoint),
"{endpoint} is a chat-completions-only built-in host"
);
}
}
#[test]
fn openai_host_and_unknown_proxies_keep_the_responses_fallback() {
// OpenAI's first-party host serves /responses — must NOT be gated off,
// even via a custom proxy slug pointed at it.
assert!(!endpoint_host_is_chat_completions_only(
"https://api.openai.com/v1"
));
// Genuinely unknown proxy hosts keep the permissive default (they may be
// real OpenAI proxies that implement /responses).
for endpoint in [
"https://my-llm-proxy.internal.example/v1",
"https://litellm.mycorp.dev/v1",
"",
] {
assert!(
!endpoint_host_is_chat_completions_only(endpoint),
"{endpoint:?} is an unknown host and must keep the fallback"
);
}
}
/// Drift guard: the host-based gate must agree with the slug-based
/// capability for every built-in preset's own endpoint, so adding a preset
/// can't silently desync the two gates.
#[test]
fn host_gate_agrees_with_slug_capability_for_every_builtin() {
for provider in BUILTIN_CLOUD_PROVIDERS {
// OpenhumanJwt / Anthropic presets never route through the
// OpenAI-compatible Responses fallback; the gate only matters for
// the Bearer OpenAI-compatible hosts.
if provider.auth_style != AuthStyle::Bearer {
continue;
}
let host_chat_only = endpoint_host_is_chat_completions_only(provider.endpoint);
let slug_supports = builtin_cloud_supports_responses_api(provider.slug);
assert_eq!(
host_chat_only, !slug_supports,
"host gate for built-in {} disagrees with its slug capability",
provider.slug
);
}
}
}
@@ -144,13 +144,28 @@ impl OpenAiCompatibleProvider {
// model on a Responses-capable endpoint. Skip when Responses is the
// primary path (Codex OAuth): the fallback flag is never consulted
// and a 404 there is not evidence the route is missing.
if status == reqwest::StatusCode::NOT_FOUND
let responses_route_missing = status == reqwest::StatusCode::NOT_FOUND
&& !self.responses_api_primary
&& Self::responses_404_indicates_missing_route(&error)
{
&& Self::responses_404_indicates_missing_route(&error);
if responses_route_missing {
super::mark_responses_api_unsupported(&self.base_url);
}
if super::super::is_budget_exhausted_http_400(status, &error) {
if responses_route_missing {
// The `/responses` route 404'd: this endpoint is chat-completions
// only. We've just cached it unsupported so the fallback won't fire
// again this process, but the very first probe per process still
// lands here — and reporting it floods Sentry with one
// `"<provider> Responses API error (404): …"` event per fresh
// session (TAURI-RUST-5A1, ~900 status=404 events). It is an
// expected capability-probe miss (the chat-completions path serves
// the request), not a Sentry-actionable defect, so demote to info.
log::info!(
"[provider] {} /responses route 404 — endpoint is chat-completions only; \
demoting and caching unsupported (fallback disabled henceforth): {}",
self.name,
super::super::factory::redact_endpoint(&self.base_url),
);
} else if super::super::is_budget_exhausted_http_400(status, &error) {
super::super::log_budget_exhausted_http_400(
"responses_api",
self.name.as_str(),
+12 -3
View File
@@ -25,7 +25,8 @@
//! Unknown slugs and missing-creds configurations produce actionable errors.
use crate::openhuman::config::schema::cloud_providers::{
builtin_cloud_supports_responses_api, is_builtin_cloud_slug, AuthStyle,
builtin_cloud_supports_responses_api, endpoint_host_is_chat_completions_only,
is_builtin_cloud_slug, AuthStyle,
};
use crate::openhuman::config::Config;
use crate::openhuman::credentials::AuthService;
@@ -1639,8 +1640,16 @@ fn make_cloud_provider_by_slug(
// local-provider TAURI-RUST-59Y fix). OpenAI keeps the fallback
// (genuine `/responses`), and so do custom / unknown slugs, whose
// endpoint may be a real OpenAI proxy.
let responses_fallback =
!is_builtin_cloud_slug(slug) || builtin_cloud_supports_responses_api(slug);
//
// The builtin-slug gate alone leaks for a *custom* slug pointed at a
// known chat-only host (e.g. a user slug at
// `integrate.api.nvidia.com`): `is_builtin_cloud_slug` is false so
// the fallback stayed on and `/responses` 404'd (TAURI-RUST-5A1).
// Also consult the endpoint host so a chat-only host disables the
// fallback regardless of slug; an unknown proxy host still keeps it.
let responses_fallback = (!is_builtin_cloud_slug(slug)
|| builtin_cloud_supports_responses_api(slug))
&& !endpoint_host_is_chat_completions_only(&openai_codex_routing.endpoint);
let credential = (!key.trim().is_empty()).then_some(key.as_str());
let base_provider = if responses_fallback {
OpenAiCompatibleProvider::new(