@xuda.io/ai_module 1.1.5651 → 1.1.5652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +107 -4
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -231,6 +231,62 @@ const resolve_ai_model = function (m) {
|
|
|
231
231
|
return _conf?.ai_models?.[code]?.model || _conf?.ai_models?.[m]?.model || m;
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
// Every text model in the catalog is a gpt-5-family REASONING model, and the API
|
|
235
|
+
// default effort is 'medium'. Measured on dev 2026-08-11: gpt-5-nano asked to write
|
|
236
|
+
// a three line email burned 960-1600 hidden reasoning tokens and took 10-16s, while
|
|
237
|
+
// the same call at effort 'minimal' took 1.4-2.7s with 0 reasoning tokens and the
|
|
238
|
+
// same answer. The cheapest model was the SLOWEST thing we ran, purely because
|
|
239
|
+
// nobody set an effort. So every internal call now names one.
|
|
240
|
+
//
|
|
241
|
+
// 'minimal' is the right default for what submit_chat_gpt_prompt is actually used
|
|
242
|
+
// for (titles, categories, field assist, short classifications). A caller that
|
|
243
|
+
// genuinely needs the model to think passes effort: 'low' | 'medium' | 'high'.
|
|
244
|
+
//
|
|
245
|
+
// Non-reasoning ids (gpt-4o-mini and friends, still named by a few modules) reject
|
|
246
|
+
// the parameter outright, so only send it to a model that understands it.
|
|
247
|
+
const INTERNAL_AI_EFFORT = () => _conf?.internal_ai_effort || 'minimal';
|
|
248
|
+
|
|
249
|
+
const _is_reasoning_model = function (real_model) {
|
|
250
|
+
return /^(gpt-5|o[1-9])/.test(String(real_model || ''));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Responses-API shape: { reasoning: { effort } }. Returns {} when the model or the
|
|
254
|
+
// caller opted out, so it can always be spread into the request.
|
|
255
|
+
const reasoning_opt = function (real_model, effort) {
|
|
256
|
+
const e = effort === undefined ? INTERNAL_AI_EFFORT() : effort;
|
|
257
|
+
if (!e || e === 'default' || !_is_reasoning_model(real_model)) return {};
|
|
258
|
+
return { reasoning: { effort: e } };
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Request timeout, sized off the effort tier.
|
|
262
|
+
//
|
|
263
|
+
// Measured on dev 2026-08-11: roughly 13% of calls to api.openai.com go silent for
|
|
264
|
+
// 19.5-20s AFTER the edge has ACKed our request, on a 401 to /v1/models as readily
|
|
265
|
+
// as on a real completion, and from the laptop as readily as from dev. tcpdump shows
|
|
266
|
+
// no retransmission in either direction, so it is upstream at OpenAI and nothing here
|
|
267
|
+
// fixes it. See the cf-ray IDs in the change log.
|
|
268
|
+
//
|
|
269
|
+
// What we CAN stop doing is waiting it out. The SDK had no timeout set, so its 10
|
|
270
|
+
// minute default applied and every stall cost the full 20s. A stalled request never
|
|
271
|
+
// recovers early and a fresh one answers in ~200ms, so the cure is to give up and
|
|
272
|
+
// re-ask. The stall lands before generation starts, so the abandoned attempt bills
|
|
273
|
+
// nothing and there is no reason to hedge (leave the first running) instead.
|
|
274
|
+
//
|
|
275
|
+
// Timeouts are ~6x the observed normal for the tier, so only a genuinely stuck call
|
|
276
|
+
// trips them. The SDK's own maxRetries (2) does the re-asking and treats a timeout as
|
|
277
|
+
// retryable, which puts 'minimal' at ~9s instead of ~21s for a stall.
|
|
278
|
+
const AI_EFFORT_TIMEOUT_MS = { minimal: 8000, low: 15000, medium: 60000, high: 120000 };
|
|
279
|
+
const AI_TIMEOUT_DEFAULT_MS = 60000;
|
|
280
|
+
|
|
281
|
+
const ai_timeout_ms = function (effort, real_model, tools) {
|
|
282
|
+
// A hosted tool (web_search and friends) runs its own loop inside the one request
|
|
283
|
+
// and is legitimately slow, so those never get the short tier.
|
|
284
|
+
if (Array.isArray(tools) && tools.length) return _conf.ai_tools_timeout_ms || 180000;
|
|
285
|
+
if (!_is_reasoning_model(real_model)) return _conf.ai_timeout_ms?.default || AI_TIMEOUT_DEFAULT_MS;
|
|
286
|
+
const e = effort === undefined ? INTERNAL_AI_EFFORT() : effort;
|
|
287
|
+
return _conf.ai_timeout_ms?.[e] || AI_EFFORT_TIMEOUT_MS[e] || _conf.ai_timeout_ms?.default || AI_TIMEOUT_DEFAULT_MS;
|
|
288
|
+
};
|
|
289
|
+
|
|
234
290
|
// A catalog code a user may pick for Codex generation: an OpenAI text model
|
|
235
291
|
// flagged codex:true in ai_models. Guards generate_site_draft's model param so a
|
|
236
292
|
// non-codex pick (image/voice/unknown) falls back to the default rather than
|
|
@@ -4763,7 +4819,7 @@ export const delete_prompt_attachment = async function (req, job_id, headers, fi
|
|
|
4763
4819
|
};
|
|
4764
4820
|
|
|
4765
4821
|
export const submit_chat_gpt_prompt = async function (req) {
|
|
4766
|
-
const { model = _conf.default_ai_model, prompt = '', response_format, uid, metadata = {}, account_profile_info, tools = [], conversation_id, response_id } = req; //'gpt-5-mini
|
|
4822
|
+
const { model = _conf.default_ai_model, prompt = '', response_format, uid, metadata = {}, account_profile_info, tools = [], conversation_id, response_id, effort } = req; //'gpt-5-mini
|
|
4767
4823
|
|
|
4768
4824
|
let formattedParams;
|
|
4769
4825
|
if (response_format) {
|
|
@@ -4772,14 +4828,19 @@ export const submit_chat_gpt_prompt = async function (req) {
|
|
|
4772
4828
|
|
|
4773
4829
|
try {
|
|
4774
4830
|
let response;
|
|
4831
|
+
const real_model = resolve_ai_model(model);
|
|
4832
|
+
const eff = effort === undefined ? INTERNAL_AI_EFFORT() : effort;
|
|
4833
|
+
const timeout = ai_timeout_ms(effort, real_model, tools);
|
|
4834
|
+
const started = Date.now();
|
|
4775
4835
|
try {
|
|
4776
4836
|
let opt = {
|
|
4777
|
-
model:
|
|
4837
|
+
model: real_model,
|
|
4778
4838
|
input: prompt,
|
|
4779
4839
|
tools,
|
|
4780
4840
|
text: {
|
|
4781
4841
|
format: formattedParams,
|
|
4782
4842
|
},
|
|
4843
|
+
...reasoning_opt(real_model, effort),
|
|
4783
4844
|
};
|
|
4784
4845
|
if (conversation_id) {
|
|
4785
4846
|
opt.conversationId = conversation_id;
|
|
@@ -4787,10 +4848,25 @@ export const submit_chat_gpt_prompt = async function (req) {
|
|
|
4787
4848
|
if (response_id) {
|
|
4788
4849
|
opt.previous_response_id = response_id;
|
|
4789
4850
|
}
|
|
4790
|
-
|
|
4851
|
+
// Second argument is per-request options, not part of the body. The SDK's own
|
|
4852
|
+
// maxRetries (2) treats a timeout as retryable, which is the whole point: a
|
|
4853
|
+
// request the OpenAI edge has gone silent on is abandoned and re-asked instead
|
|
4854
|
+
// of waited out.
|
|
4855
|
+
response = await client.responses.create(opt, { timeout });
|
|
4791
4856
|
report_ai_status(model);
|
|
4857
|
+
// No timing existed on this path, so "the AI is slow" was never anything we
|
|
4858
|
+
// could point at a number. One line per call, only when it actually dragged.
|
|
4859
|
+
const ms = Date.now() - started;
|
|
4860
|
+
if (ms > (_conf.ai_slow_log_ms || 5000)) {
|
|
4861
|
+
console.warn(`[ai] slow prompt ${ms}ms model=${real_model} effort=${eff} timeout=${timeout}ms reasoning_tokens=${response?.usage?.output_tokens_details?.reasoning_tokens ?? '?'} func=${metadata?.func || '-'}`);
|
|
4862
|
+
}
|
|
4792
4863
|
} catch (err) {
|
|
4793
4864
|
report_ai_status(model, err);
|
|
4865
|
+
// A timeout here means every attempt was abandoned, so say so plainly rather
|
|
4866
|
+
// than handing the caller the SDK's generic connection-error wording.
|
|
4867
|
+
if (/timed? ?out/i.test(err?.message || '') || err?.name === 'APIConnectionTimeoutError') {
|
|
4868
|
+
console.warn(`[ai] prompt timed out after ${Date.now() - started}ms model=${real_model} effort=${eff} timeout=${timeout}ms func=${metadata?.func || '-'}`);
|
|
4869
|
+
}
|
|
4794
4870
|
throw err;
|
|
4795
4871
|
}
|
|
4796
4872
|
account_msa.record_ai_usage(uid, response.usage.input_tokens, response.usage.output_tokens, 'submit chat', prompt, model, metadata, account_profile_info, tools);
|
|
@@ -5017,6 +5093,9 @@ export const ai_field_assist = async function (req) {
|
|
|
5017
5093
|
uid,
|
|
5018
5094
|
prompt: parts.join('\n'),
|
|
5019
5095
|
model,
|
|
5096
|
+
// Sparkle-icon writing: the user is watching, and the per-field rules above are
|
|
5097
|
+
// explicit enough that hidden reasoning adds latency, not quality.
|
|
5098
|
+
effort: _conf.field_assist?.effort || 'low',
|
|
5020
5099
|
metadata: { func: 'ai_field_assist', field, mode },
|
|
5021
5100
|
account_profile_info,
|
|
5022
5101
|
});
|
|
@@ -5169,6 +5248,10 @@ export const triage_error_incident = async function (req) {
|
|
|
5169
5248
|
prompt,
|
|
5170
5249
|
response_format: verdict_schema,
|
|
5171
5250
|
uid: _conf.superuser_account_ids?.[0],
|
|
5251
|
+
// Root-causing an incident is the one thing here worth thinking about, and it
|
|
5252
|
+
// runs on a cron where nobody is watching a spinner, so it opts out of the
|
|
5253
|
+
// 'minimal' default.
|
|
5254
|
+
effort: _conf.error_resolver?.effort || 'medium',
|
|
5172
5255
|
metadata: { func: 'triage_error_incident', signature: incident?.signature, code: incident?.code },
|
|
5173
5256
|
});
|
|
5174
5257
|
|
|
@@ -5225,6 +5308,8 @@ export const diagnose_vps_snapshot = async function (req) {
|
|
|
5225
5308
|
prompt: parts.join('\n'),
|
|
5226
5309
|
response_format: verdict_schema,
|
|
5227
5310
|
uid,
|
|
5311
|
+
// Reading a server snapshot is analysis, not a one-liner, and it runs unattended.
|
|
5312
|
+
effort: _conf.auto_diagnose?.effort || 'medium',
|
|
5228
5313
|
metadata: { func: 'diagnose_vps_snapshot', app_name },
|
|
5229
5314
|
});
|
|
5230
5315
|
|
|
@@ -5374,6 +5459,9 @@ export const generate_release_notes = async function (req) {
|
|
|
5374
5459
|
uid,
|
|
5375
5460
|
prompt: parts.join('\n'),
|
|
5376
5461
|
model: _conf.release_notes?.model || _conf.default_ai_model,
|
|
5462
|
+
// Turning a changelog into readable notes is a writing job, so give it a little
|
|
5463
|
+
// more than 'minimal' without paying for full reasoning.
|
|
5464
|
+
effort: _conf.release_notes?.effort || 'low',
|
|
5377
5465
|
metadata: { func: 'generate_release_notes', app_id: app_ref, version },
|
|
5378
5466
|
account_profile_info,
|
|
5379
5467
|
});
|
|
@@ -6433,6 +6521,10 @@ Rules:
|
|
|
6433
6521
|
prompt,
|
|
6434
6522
|
model: ai_model || _conf.default_ai_model,
|
|
6435
6523
|
response_format: ComposedEmailSchema,
|
|
6524
|
+
// The "Writing your email..." spinner. It carries a long rule list, so it gets
|
|
6525
|
+
// 'low' rather than 'minimal' to keep instruction-following tight; that is still
|
|
6526
|
+
// a few seconds instead of the 10-16s the unset default was costing.
|
|
6527
|
+
effort: _conf.compose_email_effort || 'low',
|
|
6436
6528
|
metadata: { contact_id, func: 'compose_contact_email' },
|
|
6437
6529
|
account_profile_info,
|
|
6438
6530
|
});
|
|
@@ -6474,6 +6566,9 @@ const chat_email = async function (req, job_id, headers) {
|
|
|
6474
6566
|
// path, and every inbound/auto reply) leaves both empty and behaves exactly as before.
|
|
6475
6567
|
const composed_subject = String(req.subject || '').trim();
|
|
6476
6568
|
const composed_html = _sanitize_email_html(req.body_html);
|
|
6569
|
+
// UI-155: the template the user picked in the composer, for THIS message only. Empty on
|
|
6570
|
+
// every other path, which leaves the address's saved default in charge.
|
|
6571
|
+
const composed_style = String(req.template_style || '').trim();
|
|
6477
6572
|
try {
|
|
6478
6573
|
if (!account_profile_info.account_profile_obj?.email_account_id) {
|
|
6479
6574
|
throw await email_binding_error(account_profile_info);
|
|
@@ -6566,6 +6661,9 @@ const chat_email = async function (req, job_id, headers) {
|
|
|
6566
6661
|
uid,
|
|
6567
6662
|
email_account_id: profile_doc.email_account_id,
|
|
6568
6663
|
style: profile_doc.email_template?.style,
|
|
6664
|
+
// UI-155: a per-message choice made in the composer, which outranks the address's
|
|
6665
|
+
// saved default inside the renderer.
|
|
6666
|
+
...(composed_style ? { style_override: composed_style } : {}),
|
|
6569
6667
|
body_text: body,
|
|
6570
6668
|
// A composed body is already HTML the user laid out (bold, lists, links), so the
|
|
6571
6669
|
// template has to drop it in as-is. Escaping it into paragraphs the way a plain
|
|
@@ -6582,7 +6680,12 @@ const chat_email = async function (req, job_id, headers) {
|
|
|
6582
6680
|
// With no template style selected there is nothing wrapping the body, so the composed
|
|
6583
6681
|
// HTML is the whole message. Without this it would fall through to sendEmailFromAccount's
|
|
6584
6682
|
// "wrap the plain text in one <p>" default and the formatting would be lost.
|
|
6585
|
-
|
|
6683
|
+
// UI-157: cc / bcc come from the composer and are normalized inside sendEmailFromAccount,
|
|
6684
|
+
// so anything that is not an address is dropped rather than reaching the SMTP server.
|
|
6685
|
+
sent_email_result = await email_ms.sendEmailFromAccount(email_account_doc, contact_info.email, subject, body, template_html || composed_html || null, email_attachments, {
|
|
6686
|
+
cc: req.cc,
|
|
6687
|
+
bcc: req.bcc,
|
|
6688
|
+
});
|
|
6586
6689
|
if (!sent_email_result.success) {
|
|
6587
6690
|
throw new Error('error sending email');
|
|
6588
6691
|
}
|