timeout = 10s;
# Prompt for the model (use default if not set)
#prompt = "xxx";
+ # Reply trimming mode for LLM input: always|none|replies
+ #reply_trim_mode = "replies";
# Custom condition (lua function)
#condition = "xxx";
# Autolearn if gpt classified
-- Rspamd uses bytes for limit.
-- Let's stick with what we had but using extract_text_limited
+ local reply_trim_mode = opts.reply_trim_mode or 'replies'
+ local trim_replies = false
+ if reply_trim_mode == 'always' then
+ trim_replies = true
+ elseif reply_trim_mode == 'none' then
+ trim_replies = false
+ else
+ trim_replies = task:has_header('In-Reply-To') or task:has_header('References')
+ end
+
local extraction_opts = {
max_bytes = max_tokens * 6, -- Rough estimate
max_words = max_tokens, -- Better estimate if available
- strip_quotes = true, -- Default cleanup for LLM
- smart_trim = true, -- Enable heuristics
+ strip_quotes = trim_replies,
+ strip_reply_headers = trim_replies,
+ smart_trim = trim_replies,
}
local res = lua_mime.extract_text_limited(task, extraction_opts)
local model_cfg = { max_tokens = 256 }
local content_tbl
if sel_part then
- local itbl = llm_common.build_llm_input(task, { max_tokens = model_cfg.max_tokens })
+ local itbl = llm_common.build_llm_input(task, {
+ max_tokens = model_cfg.max_tokens,
+ reply_trim_mode = opts.reply_trim_mode,
+ })
content_tbl = itbl
else
- content_tbl = llm_common.build_llm_input(task, { max_tokens = model_cfg.max_tokens })
+ content_tbl = llm_common.build_llm_input(task, {
+ max_tokens = model_cfg.max_tokens,
+ reply_trim_mode = opts.reply_trim_mode,
+ })
end
if type(content_tbl) ~= 'table' then
return nil
local N = "neural.llm"
-local function select_text(task)
- local input_tbl = llm_common.build_llm_input(task)
- return input_tbl
+local function select_text(task, opts)
+ return llm_common.build_llm_input(task, opts)
end
local function compose_llm_settings(pcfg)
ssl_timeout = pcfg.ssl_timeout or gpt_settings.ssl_timeout,
write_timeout = pcfg.write_timeout or gpt_settings.write_timeout,
read_timeout = pcfg.read_timeout or gpt_settings.read_timeout,
+ reply_trim_mode = pcfg.reply_trim_mode or gpt_settings.reply_trim_mode,
}
end
end
end
- local input_tbl = select_text(task)
+ local input_tbl = select_text(task, { reply_trim_mode = llm.reply_trim_mode })
if not input_tbl then
rspamd_logger.debugm(N, task, 'llm provider has no content to embed; skip')
cont(nil)
type = 'openai',
api_key = nil,
model = 'gpt-5-mini', -- or parallel model requests: [ 'gpt-5-mini', 'gpt-4o-mini' ],
+ reply_trim_mode = 'replies',
model_parameters = {
["gpt-5-mini"] = {
max_completion_tokens = 1000,
-- Unified LLM input building (subject/from/urls/body one-line)
local model_cfg = settings.model_parameters[settings.model] or {}
local max_tokens = model_cfg.max_completion_tokens or model_cfg.max_tokens or 1000
- local input_tbl, sel_part = llm_common.build_llm_input(task, { max_tokens = max_tokens })
+ local input_tbl, sel_part = llm_common.build_llm_input(task, {
+ max_tokens = max_tokens,
+ reply_trim_mode = settings.reply_trim_mode,
+ })
if not sel_part then
return false, 'no text part found'
end
symbol_ham = "NEURAL_HAM";
ann_expire = 86400;
watch_interval = 0.5;
- providers = [{ type = "llm"; model = "dummy-embed"; url = "http://127.0.0.1:18080"; weight = 1.0; }];
+ providers = [{ type = "llm"; model = "dummy-embed"; url = "http://127.0.0.1:18080"; weight = 1.0;
+ #reply_trim_mode = "replies"; # always|none|replies
+ }];
fusion { normalization = "none"; }
roc_enabled = false;
}