diff --git a/src/openhuman/inference/provider/compatible.rs b/src/openhuman/inference/provider/compatible.rs index d17bf8dd17..dccfea5fe1 100644 --- a/src/openhuman/inference/provider/compatible.rs +++ b/src/openhuman/inference/provider/compatible.rs @@ -490,6 +490,13 @@ impl OpenAiCompatibleProvider { Some(model), status, ); + } else if super::is_provider_config_rejection_http(status, self.name.as_str(), &error) { + super::log_provider_config_rejection( + "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(), @@ -856,6 +863,13 @@ impl OpenAiCompatibleProvider { Some(native_request.model.as_str()), status, ); + } else if super::is_provider_config_rejection_http(status, self.name.as_str(), &body) { + super::log_provider_config_rejection( + "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(), @@ -1348,6 +1362,13 @@ impl Provider for OpenAiCompatibleProvider { Some(model), status, ); + } else if super::is_provider_config_rejection_http(status, self.name.as_str(), &error) { + super::log_provider_config_rejection( + "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(), @@ -1797,6 +1818,13 @@ impl Provider for OpenAiCompatibleProvider { Some(model), status, ); + } else if super::is_provider_config_rejection_http(status, self.name.as_str(), &error) { + super::log_provider_config_rejection( + "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(), @@ -1952,6 +1980,17 @@ impl Provider for OpenAiCompatibleProvider { Some(model_owned.as_str()), status, ); + } else if super::is_provider_config_rejection_http( + status, + provider_name.as_str(), + &raw_error, + ) { + super::log_provider_config_rejection( + "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(), diff --git a/src/openhuman/inference/provider/compatible_tests.rs b/src/openhuman/inference/provider/compatible_tests.rs index 7285dac030..fd7d0266e7 100644 --- a/src/openhuman/inference/provider/compatible_tests.rs +++ b/src/openhuman/inference/provider/compatible_tests.rs @@ -1,4 +1,8 @@ use super::*; +use sentry::test::TestTransport; +use std::sync::Arc; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; fn make_provider(name: &str, url: &str, key: Option<&str>) -> OpenAiCompatibleProvider { OpenAiCompatibleProvider::new(name, url, key, AuthStyle::Bearer) @@ -374,6 +378,69 @@ async fn chat_via_responses_requires_non_system_message() { .contains("requires at least one non-system message")); } +#[tokio::test] +async fn streaming_chat_config_rejection_propagates_error_without_sentry_report() { + // Representative guardrail for the new provider-config-rejection + // suppression branches in compatible.rs: streaming_chat should still + // return an error, but it must not call report_error/Sentry for this + // deterministic user-config state. + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/chat/completions")) + .respond_with( + ResponseTemplate::new(400) + .set_body_string("invalid temperature: only 1 is allowed for this model"), + ) + .mount(&mock_server) + .await; + + let transport = TestTransport::new(); + let sentry_options = sentry::ClientOptions { + dsn: Some("https://public@sentry.invalid/1".parse().unwrap()), + transport: Some(Arc::new(transport.clone())), + ..Default::default() + }; + let sentry_hub = Arc::new(sentry::Hub::new( + Some(Arc::new(sentry_options.into())), + Arc::new(Default::default()), + )); + let _sentry_guard = sentry::HubSwitchGuard::new(sentry_hub); + + let provider = + OpenAiCompatibleProvider::new("custom_openai", &mock_server.uri(), None, AuthStyle::None); + let request = NativeChatRequest { + model: "kimi-k2".to_string(), + messages: vec![NativeMessage { + role: "user".to_string(), + content: Some("hello".to_string()), + tool_call_id: None, + tool_calls: None, + }], + temperature: Some(0.7), + stream: Some(true), + tools: None, + tool_choice: None, + thread_id: None, + stream_options: Some(super::compatible_types::OpenAiStreamOptions { + include_usage: true, + }), + }; + let (delta_tx, _delta_rx) = tokio::sync::mpsc::channel(8); + + let err = provider + .stream_native_chat(None, &request, &delta_tx, 0) + .await + .expect_err("400 provider config-rejection must still propagate as Err"); + assert!( + err.to_string().contains("streaming API error"), + "err: {err}" + ); + assert!( + transport.fetch_and_clear_events().is_empty(), + "provider config-rejection must not be reported to Sentry" + ); +} + // ---------------------------------------------------------- // Custom endpoint path tests (Issue #114) // ----------------------------------------------------------