diff --git a/src/core/observability.rs b/src/core/observability.rs index caabdd624..081a7e800 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -6196,6 +6196,25 @@ mod tests { assert!(is_quota_exhausted_message(body)); } + #[test] + fn quota_exhausted_filter_matches_responses_usage_limit_reached_event() { + // TAURI-RUST-AFE: verbatim message as formatted by the `chat_via_responses` + // emit site — the Codex/ChatGPT OAuth `/responses` plan cap. Mirrors the + // real production shape `" Responses API error (): "` + // (`compatible_helpers.rs:135`), including the `(429)` status segment and + // the full AFE payload (`plan_type` + `resets_at`) from `ops/http_error.rs`, + // so this stays coupled to the actual wire format rather than a loose + // substring. No "monthly"/"quota" co-marker, so it exercises the AFE + // phrase extension reaching the before_send net on both message and + // exception paths (the subconscious loop retries until `resets_at`). + let body = "openai Responses API error (429): {\"error\":{\"type\":\ + \"usage_limit_reached\",\"message\":\"The usage limit has been reached\",\ + \"plan_type\":\"plus\",\"resets_at\":1750000000}}"; + assert!(is_quota_exhausted_event(&event_with_message(body))); + assert!(is_quota_exhausted_event(&event_with_exception_value(body))); + assert!(is_quota_exhausted_message(body)); + } + #[test] fn quota_exhausted_filter_ignores_generic_500_and_rate_limit() { // A generic 500 outage and a 429 rate-limit are not plan-quota diff --git a/src/openhuman/inference/provider/compatible_helpers.rs b/src/openhuman/inference/provider/compatible_helpers.rs index 801c67c52..4b8460d75 100644 --- a/src/openhuman/inference/provider/compatible_helpers.rs +++ b/src/openhuman/inference/provider/compatible_helpers.rs @@ -222,6 +222,20 @@ impl OpenAiCompatibleProvider { Some(model), status, ); + } else if super::super::is_provider_quota_exhausted(&error) { + // Codex/ChatGPT OAuth `/responses` plan cap hit + // (`usage_limit_reached` / "The usage limit has been reached"): a + // third-party plan limit with no local lever. The subconscious loop + // retries until `resets_at`, so a single capped Plus user emits + // hundreds of identical events — demote to info instead of paging on + // every retry (TAURI-RUST-AFE, extends the #4076/C9A quota machinery + // to the Responses path). + super::super::log_provider_quota_exhausted( + "responses_api", + self.name.as_str(), + Some(model), + status, + ); } else if super::super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), diff --git a/src/openhuman/inference/provider/ops/http_error.rs b/src/openhuman/inference/provider/ops/http_error.rs index 0aaf903e3..bdeea9077 100644 --- a/src/openhuman/inference/provider/ops/http_error.rs +++ b/src/openhuman/inference/provider/ops/http_error.rs @@ -255,6 +255,13 @@ pub fn body_indicates_quota_exhausted(body: &str) -> bool { || lower.contains("monthly quota") || lower.contains("quota exceeded") || lower.contains("usage limit exceeded") + // Codex/ChatGPT OAuth `/responses` plan-cap body (TAURI-RUST-AFE): + // `usage_limit_reached` / "The usage limit has been reached" — a plan + // quota with no "monthly"/"quota" co-marker, so the phrases above miss + // it. Both are quota-specific enough to match on their own (the loop + // retries until `resets_at`, flooding from a single capped Plus user). + || lower.contains("usage_limit_reached") + || lower.contains("usage limit has been reached") // "reached the limit" alone is ambiguous (rate-limit, token-limit), so // require a quota/plan/request/monthly co-marker to keep the blast // radius on plan-quota exhaustion only. @@ -1051,6 +1058,15 @@ mod tests { reached the limit.\\\",\\\"reason\\\":\\\"MONTHLY_REQUEST_COUNT\\\"}\",\ \"type\":\"server_error\"}}"; + /// Verbatim TAURI-RUST-AFE Responses-API body — the Codex/ChatGPT OAuth + /// `/responses` endpoint refuses with `usage_limit_reached` once the Plus + /// plan cap is hit. It carries no "monthly"/"quota" co-marker, so the C9A + /// phrase set missed it; couple the test to the exact string so a wording + /// drift fails CI rather than silently leaking events back to Sentry. + const AFE_BODY: &str = "openai Responses API error: {\"error\":{\"type\":\ + \"usage_limit_reached\",\"message\":\"The usage limit has been reached\",\ + \"plan_type\":\"plus\",\"resets_at\":1750000000}}"; + #[test] fn quota_exhausted_matches_verbatim_c9a_body() { // Status-agnostic: the verbatim 500-wrapped body must match even though @@ -1059,6 +1075,20 @@ mod tests { assert!(body_indicates_quota_exhausted(C9A_BODY)); } + #[test] + fn quota_exhausted_matches_verbatim_afe_body() { + // Coverage gap closed (TAURI-RUST-AFE): the Responses `usage_limit_reached` + // body must demote through the same #4076 quota machinery even though it + // lacks a "monthly"/"quota" co-marker. + assert!(is_provider_quota_exhausted(AFE_BODY)); + assert!(body_indicates_quota_exhausted(AFE_BODY)); + // Bare phrasings (no surrounding envelope) must also match. + assert!(body_indicates_quota_exhausted("usage_limit_reached")); + assert!(body_indicates_quota_exhausted( + "The usage limit has been reached" + )); + } + #[test] fn quota_exhausted_matches_common_phrasings() { for body in [