mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(observability): classify Kimi access_terminated 403 as expected provider user-state (#2090)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
544a68239d
commit
ff8d60c3bb
@@ -464,6 +464,16 @@ fn is_provider_user_state_message(lower: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// OPENHUMAN-TAURI-S7: provider policy rejection on Kimi's coding
|
||||
// endpoint when requests are not sent from an approved coding-agent
|
||||
// client. Canonical body contains `access_terminated_error` and:
|
||||
// "currently only available for Coding Agents ...".
|
||||
if lower.contains("access_terminated_error")
|
||||
|| lower.contains("currently only available for coding agents")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1615,6 +1625,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_access_terminated_provider_policy_as_provider_user_state() {
|
||||
assert_eq!(
|
||||
expected_error_kind(
|
||||
"custom_openai API error (403 Forbidden): {\"error\":{\"message\":\"Kimi For Coding is currently only available for Coding Agents such as Kimi CLI, Claude Code, Roo Code, Kilo Code, etc.\",\"type\":\"access_terminated_error\"}}"
|
||||
),
|
||||
Some(ExpectedErrorKind::ProviderUserState)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
expected_error_kind(
|
||||
"agent turn failed: custom_openai API error (403): currently only available for coding agents"
|
||||
),
|
||||
Some(ExpectedErrorKind::ProviderUserState)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_unrelated_500s_as_provider_user_state() {
|
||||
// Sanity check: a generic 500 with no provider-user-state body
|
||||
|
||||
@@ -472,6 +472,13 @@ impl OpenAiCompatibleProvider {
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::is_provider_access_policy_denied_http_403(status, &error) {
|
||||
super::log_provider_access_policy_denied_http_403(
|
||||
"responses_api",
|
||||
self.name.as_str(),
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::should_report_provider_http_failure(status) {
|
||||
crate::core::observability::report_error(
|
||||
message.as_str(),
|
||||
@@ -820,6 +827,13 @@ impl OpenAiCompatibleProvider {
|
||||
Some(native_request.model.as_str()),
|
||||
status,
|
||||
);
|
||||
} else if super::is_provider_access_policy_denied_http_403(status, &body) {
|
||||
super::log_provider_access_policy_denied_http_403(
|
||||
"streaming_chat",
|
||||
self.name.as_str(),
|
||||
Some(native_request.model.as_str()),
|
||||
status,
|
||||
);
|
||||
} else if super::should_report_provider_http_failure(status) {
|
||||
crate::core::observability::report_error(
|
||||
message.as_str(),
|
||||
@@ -1294,6 +1308,13 @@ impl Provider for OpenAiCompatibleProvider {
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::is_provider_access_policy_denied_http_403(status, &error) {
|
||||
super::log_provider_access_policy_denied_http_403(
|
||||
"chat_completions",
|
||||
self.name.as_str(),
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::should_report_provider_http_failure(status) {
|
||||
crate::core::observability::report_error(
|
||||
message.as_str(),
|
||||
@@ -1725,6 +1746,13 @@ impl Provider for OpenAiCompatibleProvider {
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::is_provider_access_policy_denied_http_403(status, &error) {
|
||||
super::log_provider_access_policy_denied_http_403(
|
||||
"native_chat",
|
||||
self.name.as_str(),
|
||||
Some(model),
|
||||
status,
|
||||
);
|
||||
} else if super::should_report_provider_http_failure(status) {
|
||||
crate::core::observability::report_error(
|
||||
message.as_str(),
|
||||
@@ -1862,6 +1890,13 @@ impl Provider for OpenAiCompatibleProvider {
|
||||
Some(model_owned.as_str()),
|
||||
status,
|
||||
);
|
||||
} else if super::is_provider_access_policy_denied_http_403(status, &raw_error) {
|
||||
super::log_provider_access_policy_denied_http_403(
|
||||
"stream_chat",
|
||||
provider_name.as_str(),
|
||||
Some(model_owned.as_str()),
|
||||
status,
|
||||
);
|
||||
} else if super::should_report_provider_http_failure(status) {
|
||||
crate::core::observability::report_error(
|
||||
message.as_str(),
|
||||
|
||||
@@ -301,6 +301,24 @@ pub(super) fn is_budget_exhausted_http_400(status: reqwest::StatusCode, body: &s
|
||||
status == reqwest::StatusCode::BAD_REQUEST && super::is_budget_exhausted_message(body)
|
||||
}
|
||||
|
||||
/// Whether a provider non-2xx response is a deterministic provider-policy
|
||||
/// denial (not a product bug) that should be demoted from Sentry.
|
||||
///
|
||||
/// Canonical example: Kimi's coding endpoint rejects non-agent clients with
|
||||
/// HTTP 403 + `access_terminated_error` and a message like:
|
||||
/// "currently only available for Coding Agents …".
|
||||
pub(super) fn is_provider_access_policy_denied_http_403(
|
||||
status: reqwest::StatusCode,
|
||||
body: &str,
|
||||
) -> bool {
|
||||
if status != reqwest::StatusCode::FORBIDDEN {
|
||||
return false;
|
||||
}
|
||||
let lower = body.to_ascii_lowercase();
|
||||
lower.contains("access_terminated_error")
|
||||
|| lower.contains("currently only available for coding agents")
|
||||
}
|
||||
|
||||
pub(super) fn log_budget_exhausted_http_400(
|
||||
operation: &str,
|
||||
provider: &str,
|
||||
@@ -319,6 +337,24 @@ pub(super) fn log_budget_exhausted_http_400(
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn log_provider_access_policy_denied_http_403(
|
||||
operation: &str,
|
||||
provider: &str,
|
||||
model: Option<&str>,
|
||||
status: reqwest::StatusCode,
|
||||
) {
|
||||
tracing::info!(
|
||||
domain = "llm_provider",
|
||||
operation = operation,
|
||||
provider = provider,
|
||||
model = model.unwrap_or(""),
|
||||
status = status.as_u16(),
|
||||
failure = "non_2xx",
|
||||
kind = "provider_access_policy",
|
||||
"[llm_provider] {operation} provider access-policy 403 — not reporting to Sentry"
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether a provider non-2xx response is a deterministic
|
||||
/// **configuration-rejection** user-state error (unknown model id,
|
||||
/// abstract tier leaked to a custom provider, model-specific temperature
|
||||
@@ -397,6 +433,7 @@ pub async fn api_error(provider: &str, response: reqwest::Response) -> anyhow::E
|
||||
let is_auth_failure = matches!(status.as_u16(), 401 | 403);
|
||||
let is_backend = provider == openhuman_backend::PROVIDER_LABEL;
|
||||
let is_budget_exhausted_user_state = is_budget_exhausted_http_400(status, &body);
|
||||
let is_provider_access_policy_denied = is_provider_access_policy_denied_http_403(status, &body);
|
||||
let is_provider_config_rejection = is_provider_config_rejection_http(status, provider, &body);
|
||||
|
||||
if is_auth_failure && is_backend {
|
||||
@@ -420,6 +457,8 @@ pub async fn api_error(provider: &str, response: reqwest::Response) -> anyhow::E
|
||||
);
|
||||
} else if is_budget_exhausted_user_state {
|
||||
log_budget_exhausted_http_400("api_error", provider, None, status);
|
||||
} else if is_provider_access_policy_denied {
|
||||
log_provider_access_policy_denied_http_403("api_error", provider, None, status);
|
||||
} else if is_provider_config_rejection {
|
||||
log_provider_config_rejection("api_error", provider, None, status);
|
||||
} else if should_report_provider_http_failure(status) {
|
||||
@@ -878,6 +917,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod provider_access_policy_suppression {
|
||||
use super::*;
|
||||
|
||||
const ACCESS_TERMINATED_BODY: &str =
|
||||
"{\"error\":{\"message\":\"Kimi For Coding is currently only available for Coding Agents.\",\"type\":\"access_terminated_error\"}}";
|
||||
|
||||
#[test]
|
||||
fn access_terminated_403_is_suppressed() {
|
||||
assert!(is_provider_access_policy_denied_http_403(
|
||||
reqwest::StatusCode::FORBIDDEN,
|
||||
ACCESS_TERMINATED_BODY,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_terminated_non_403_is_not_suppressed() {
|
||||
assert!(!is_provider_access_policy_denied_http_403(
|
||||
reqwest::StatusCode::BAD_REQUEST,
|
||||
ACCESS_TERMINATED_BODY,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_403_is_not_suppressed() {
|
||||
assert!(!is_provider_access_policy_denied_http_403(
|
||||
reqwest::StatusCode::FORBIDDEN,
|
||||
"{\"error\":{\"message\":\"forbidden\"}}",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Exercises the real `is_provider_config_rejection_http` decision used
|
||||
// by `api_error`, including the inverted provider-aware polarity.
|
||||
mod provider_config_rejection_suppression {
|
||||
|
||||
Reference in New Issue
Block a user