AI spam classification: the model's opinion as one bounded spam signal, and the llm_prompt Sieve function (AI-1 to AI-28)

The classifier sends only the subject and text, between unforgeable markers
after the operator's prompt, to an OpenAI-compatible endpoint the operator
configured; nothing is preset. Its answer maps to an LLM_ tag whose score is
clamped (+5.0, -1.0 by default) and can never discard or reject on its own;
X-Spam-LLM is sanitized, encoded and folded, and a planted one is removed.
Failures, timeouts past the ceiling, a full slot or a paused model leave
mail flowing untagged. llm_prompt answers trusted scripts, and accounts
holding interactAi within an hourly limit. Redirects aren't followed and no
content or secret is logged. The limits live in inbuxa:AiLimits.
Acceptance tests 1 and 3 to 21; test 2 as the re-enabled shared llm case,
whose setup no longer waits on a rules file from a developer's own path;
test 22 written as the ignored ai_compat.
This commit is contained in:
2026-09-19 00:41:00 -07:00
parent cba48cf03b
commit 9490fc4677
39 changed files with 2945 additions and 28 deletions
+10 -14
View File
@@ -4,17 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#[cfg(feature = "pending-rebuild")] // inbuxa: pending-rebuild, see docs/spec/features/
use common::enterprise::llm::{
ChatCompletionChoice, ChatCompletionRequest, ChatCompletionResponse, Message,
};
#[cfg(feature = "pending-rebuild")]
use spam_filter::analysis::llm::SpamFilterAnalyzeLlm;
#[cfg(feature = "pending-rebuild")]
use crate::utils::http_server::{HttpMessage, spawn_mock_http_server};
#[cfg(feature = "pending-rebuild")]
use http_proto::{JsonResponse, ToHttpResponse};
#[cfg(feature = "pending-rebuild")]
use hyper::Method;
use crate::utils::{
dns::DnsCache,
@@ -158,7 +153,9 @@ async fn antispam() {
status: TaskStatus::now(),
}))
.await;
test.wait_for_tasks().await;
// inbuxa: a rules file that can't be read is retried later; don't wait
// for that retry (the path above is a developer's own checkout)
test.wait_for_tasks_skip_not_due().await;
admin.reload_settings().await;
admin.reload_lookup_stores().await;
test.reload_core();
@@ -226,7 +223,6 @@ async fn antispam() {
}
// Spawn mock OpenAI server
#[cfg(feature = "pending-rebuild")] // inbuxa: pending-rebuild, see docs/spec/features/
let _tx = spawn_mock_http_server(
&test,
Arc::new(|req: HttpMessage| {
@@ -235,8 +231,13 @@ async fn antispam() {
let req = serde_json::from_slice::<ChatCompletionRequest>(req.body.as_ref().unwrap())
.unwrap();
assert_eq!(req.model, "gpt-dummy");
let message = &req.messages[0].content;
assert!(message.contains("You are an AI assistant specialized in analyzing email"));
// inbuxa: AI-6: the prompt is the system message, the email the last
assert!(
req.messages[0]
.content
.contains("You are an AI assistant specialized in analyzing email")
);
let message = &req.messages.last().unwrap().content;
JsonResponse::new(&ChatCompletionResponse {
created: 0,
@@ -296,10 +297,6 @@ async fn antispam() {
{
continue;
}
// inbuxa: pending-rebuild. The LLM case needs the AI classifier rebuilt.
if cfg!(not(feature = "pending-rebuild")) && test_name == "llm" {
continue;
}
println!("===== {test_name} =====");
let contents = fs::read_to_string(base_path.join(format!("{test_name}.test"))).unwrap();
@@ -668,7 +665,6 @@ async fn antispam() {
"pyzor" => {
server.spam_filter_analyze_pyzor(&mut spam_ctx).await;
}
#[cfg(feature = "pending-rebuild")] // inbuxa: pending-rebuild, see docs/spec/features/
"llm" => {
server.spam_filter_analyze_llm(&mut spam_ctx).await;
}