best-email-builder 0.5.1 → 0.7.0
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/CHANGELOG.md +63 -0
- package/README.md +74 -8
- package/dist/ai-server.js +116 -1
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.js +639 -176
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +4 -4
- package/dist/{chunk-2Y7CDXZM.js → chunk-3RPWH5PC.js} +3 -3
- package/dist/{chunk-H4W65GBX.js → chunk-C6WHIBGH.js} +2 -2
- package/dist/{chunk-H4W65GBX.js.map → chunk-C6WHIBGH.js.map} +1 -1
- package/dist/{chunk-BAA5XMWY.js → chunk-CTZ56YJG.js} +252 -28
- package/dist/chunk-CTZ56YJG.js.map +1 -0
- package/dist/{chunk-7QQH6MTV.js → chunk-QH5OI43L.js} +4 -4
- package/dist/{chunk-N7DEVAIP.js → chunk-QRUMXCGW.js} +2 -2
- package/dist/{chunk-2RKVJQSD.js → chunk-UMVBT7NW.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/library.js +4 -4
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +5 -0
- package/dist/types/ai/ModelPicker.d.ts +34 -0
- package/dist/types/ai/agent.d.ts +31 -0
- package/dist/types/ai/agentEndpoint.d.ts +69 -0
- package/dist/types/ai/claude.d.ts +31 -5
- package/dist/types/ai/createAi.d.ts +5 -3
- package/dist/types/ai/draft.d.ts +10 -4
- package/dist/types/ai/generator.d.ts +10 -4
- package/dist/types/ai/index.d.ts +3 -2
- package/dist/types/ai/keys.d.ts +28 -26
- package/dist/types/ai/providers.d.ts +58 -1
- package/dist/types/ai/rewrite.d.ts +2 -1
- package/dist/types/ai/server.d.ts +3 -1
- package/dist/types/ai/subjects.d.ts +2 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/dist/types/server/handler.d.ts +3 -0
- package/package.json +1 -1
- package/dist/chunk-BAA5XMWY.js.map +0 -1
- /package/dist/{chunk-2Y7CDXZM.js.map → chunk-3RPWH5PC.js.map} +0 -0
- /package/dist/{chunk-7QQH6MTV.js.map → chunk-QH5OI43L.js.map} +0 -0
- /package/dist/{chunk-N7DEVAIP.js.map → chunk-QRUMXCGW.js.map} +0 -0
- /package/dist/{chunk-2RKVJQSD.js.map → chunk-UMVBT7NW.js.map} +0 -0
|
@@ -40,6 +40,7 @@ var GENERATION_ERRORS = {
|
|
|
40
40
|
// The provider's side (providers.js). Never 401 to the browser: the key is the AI's, not the user's session.
|
|
41
41
|
KEY_MISSING: { status: 502, message: "Connect an AI provider first: it needs a key." },
|
|
42
42
|
KEY_REFUSED: { status: 502, message: "The AI provider refused the key. Check it, or use another one." },
|
|
43
|
+
MODEL_UNAVAILABLE: { status: 502, message: "The AI provider does not have this model. Choose another one." },
|
|
43
44
|
MODEL_CREDITS: { status: 502, message: "The AI provider account has no credits left." },
|
|
44
45
|
MODEL_BUSY: { status: 503, message: "The AI is busy right now. Try again in a minute." },
|
|
45
46
|
MODEL_ERROR: { status: 502, message: "The AI provider could not answer. Try again." }
|
|
@@ -963,7 +964,7 @@ function reviewRewrite(original, rewritten, format) {
|
|
|
963
964
|
return problems;
|
|
964
965
|
}
|
|
965
966
|
function createRewriter({ model, maxAttempts = 2 }) {
|
|
966
|
-
return async function rewrite(input, { signal } = {}) {
|
|
967
|
+
return async function rewrite(input, { signal, user } = {}) {
|
|
967
968
|
const request = cleanRewriteInput(input);
|
|
968
969
|
const messages = [{ role: "user", content: buildRewritePrompt(request) }];
|
|
969
970
|
let lastProblem = "";
|
|
@@ -971,7 +972,7 @@ function createRewriter({ model, maxAttempts = 2 }) {
|
|
|
971
972
|
signal?.throwIfAborted();
|
|
972
973
|
let answer;
|
|
973
974
|
try {
|
|
974
|
-
answer = await model({ system: REWRITE_SYSTEM_PROMPT, messages, schema: REWRITE_SCHEMA, signal, task: "rewrite" });
|
|
975
|
+
answer = await model({ system: REWRITE_SYSTEM_PROMPT, messages, schema: REWRITE_SCHEMA, signal, task: "rewrite", user });
|
|
975
976
|
} catch (error) {
|
|
976
977
|
if (error instanceof EmailGenerationError && error.code === "REFUSED") throw new EmailGenerationError("REWRITE_REFUSED");
|
|
977
978
|
throw error;
|
|
@@ -1213,7 +1214,7 @@ ${request.text}`;
|
|
|
1213
1214
|
return { usable: usable.slice(0, SUBJECT_COUNT), problems: usable.length >= SUBJECT_COUNT ? [] : problems };
|
|
1214
1215
|
}
|
|
1215
1216
|
function createSubjectSuggester({ model, maxAttempts = 2 }) {
|
|
1216
|
-
return async function suggestSubjects(input, { signal } = {}) {
|
|
1217
|
+
return async function suggestSubjects(input, { signal, user } = {}) {
|
|
1217
1218
|
const request = cleanSubjectsInput(input);
|
|
1218
1219
|
const messages = [{ role: "user", content: buildSubjectsPrompt(request) }];
|
|
1219
1220
|
let best = [];
|
|
@@ -1221,7 +1222,7 @@ function createSubjectSuggester({ model, maxAttempts = 2 }) {
|
|
|
1221
1222
|
signal?.throwIfAborted();
|
|
1222
1223
|
let answer;
|
|
1223
1224
|
try {
|
|
1224
|
-
answer = await model({ system: SUBJECTS_SYSTEM_PROMPT, messages, schema: SUBJECTS_SCHEMA, signal, task: "subjects" });
|
|
1225
|
+
answer = await model({ system: SUBJECTS_SYSTEM_PROMPT, messages, schema: SUBJECTS_SCHEMA, signal, task: "subjects", user });
|
|
1225
1226
|
} catch (error) {
|
|
1226
1227
|
if (error instanceof EmailGenerationError && error.code === "REFUSED") throw new EmailGenerationError("SUBJECTS_REFUSED");
|
|
1227
1228
|
throw error;
|
|
@@ -1360,6 +1361,7 @@ var fr = {
|
|
|
1360
1361
|
"No usable subject came back. Try again.": "Aucun objet utilisable n'a \xE9t\xE9 propos\xE9. R\xE9essayez.",
|
|
1361
1362
|
"Connect an AI provider first: it needs a key.": "Connectez d'abord un fournisseur d'IA : il faut une cl\xE9.",
|
|
1362
1363
|
"The AI provider refused the key. Check it, or use another one.": "Le fournisseur d'IA a refus\xE9 la cl\xE9. V\xE9rifiez-la ou utilisez-en une autre.",
|
|
1364
|
+
"The AI provider does not have this model. Choose another one.": "Le fournisseur d'IA n'a pas ce mod\xE8le. Choisissez-en un autre.",
|
|
1363
1365
|
"The AI provider account has no credits left.": "Le compte du fournisseur d'IA n'a plus de cr\xE9dits.",
|
|
1364
1366
|
"The AI is busy right now. Try again in a minute.": "L'IA est occup\xE9e pour le moment. R\xE9essayez dans une minute.",
|
|
1365
1367
|
"The AI provider could not answer. Try again.": "Le fournisseur d'IA n'a pas pu r\xE9pondre. R\xE9essayez.",
|
|
@@ -1384,7 +1386,25 @@ var fr = {
|
|
|
1384
1386
|
"Free models, with a free account": "Mod\xE8les gratuits, avec un compte gratuit",
|
|
1385
1387
|
"Free tier, with rate limits": "Offre gratuite, avec des limites de d\xE9bit",
|
|
1386
1388
|
'Free "Experiment" plan': "Offre gratuite \xAB Experiment \xBB",
|
|
1387
|
-
"Runs on your own machine": "Tourne sur votre propre machine"
|
|
1389
|
+
"Runs on your own machine": "Tourne sur votre propre machine",
|
|
1390
|
+
"AI provider": "Fournisseur d'IA",
|
|
1391
|
+
"Key saved": "Cl\xE9 enregistr\xE9e",
|
|
1392
|
+
"A key for {provider} is saved in this browser: Connect uses it, or paste another one.": "Une cl\xE9 {provider} est enregistr\xE9e dans ce navigateur : Connecter l'utilise, ou collez-en une autre.",
|
|
1393
|
+
"Model": "Mod\xE8le",
|
|
1394
|
+
"Choose a model": "Choisir un mod\xE8le",
|
|
1395
|
+
"Free": "Gratuit",
|
|
1396
|
+
"Free only": "Gratuits",
|
|
1397
|
+
"Search the models": "Rechercher un mod\xE8le",
|
|
1398
|
+
"Search the models\u2026": "Rechercher un mod\xE8le\u2026",
|
|
1399
|
+
"Use \u201C{model}\u201D": "Utiliser \xAB {model} \xBB",
|
|
1400
|
+
"No model matches.": "Aucun mod\xE8le ne correspond.",
|
|
1401
|
+
"Loading the models of {provider}\u2026": "Chargement des mod\xE8les de {provider}\u2026",
|
|
1402
|
+
"Paste the key to see all the models of {provider}.": "Collez la cl\xE9 pour voir tous les mod\xE8les de {provider}.",
|
|
1403
|
+
"{provider} refused this key: its models could not be listed.": "{provider} a refus\xE9 cette cl\xE9 : ses mod\xE8les n'ont pas pu \xEAtre list\xE9s.",
|
|
1404
|
+
"The list of models could not be loaded. The default model works, or type the id of another.": "La liste des mod\xE8les n'a pas pu \xEAtre charg\xE9e. Le mod\xE8le par d\xE9faut fonctionne, ou tapez l'identifiant d'un autre.",
|
|
1405
|
+
"{count} models, the free ones first.": "{count} mod\xE8les, les gratuits en premier.",
|
|
1406
|
+
"{count} models.": "{count} mod\xE8les.",
|
|
1407
|
+
"{provider} lists no models here: type the id of the one you want.": "{provider} ne liste aucun mod\xE8le ici : tapez l'identifiant de celui que vous voulez."
|
|
1388
1408
|
};
|
|
1389
1409
|
|
|
1390
1410
|
// src/ai/i18n/index.js
|
|
@@ -1421,7 +1441,7 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1421
1441
|
{}
|
|
1422
1442
|
)) {
|
|
1423
1443
|
if (typeof model !== "function") throw new Error("createEmailGenerator: `model` must be a function");
|
|
1424
|
-
async function generate(input, { signal } = {}) {
|
|
1444
|
+
async function generate(input, { signal, user } = {}) {
|
|
1425
1445
|
const request = cleanGenerateInput(input);
|
|
1426
1446
|
if (!request.purpose && !request.details) {
|
|
1427
1447
|
throw new EmailGenerationError("NOTHING_TO_GENERATE");
|
|
@@ -1433,7 +1453,7 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1433
1453
|
let lastProblem = "";
|
|
1434
1454
|
for (let attempt = 1; attempt <= Math.max(1, maxAttempts); attempt += 1) {
|
|
1435
1455
|
signal?.throwIfAborted();
|
|
1436
|
-
const answer = await model({ system: SYSTEM_PROMPT, messages, schema: EMAIL_DRAFT_SCHEMA, signal, task: "generate" });
|
|
1456
|
+
const answer = await model({ system: SYSTEM_PROMPT, messages, schema: EMAIL_DRAFT_SCHEMA, signal, task: "generate", user });
|
|
1437
1457
|
let problems;
|
|
1438
1458
|
try {
|
|
1439
1459
|
const draft = parseDraft(answer);
|
|
@@ -1483,6 +1503,7 @@ var AI_PROVIDERS = Object.freeze({
|
|
|
1483
1503
|
keyUrl: "https://openrouter.ai/keys",
|
|
1484
1504
|
keyHint: "sk-or-",
|
|
1485
1505
|
free: "Free models, with a free account",
|
|
1506
|
+
publicModels: true,
|
|
1486
1507
|
headers: { "X-Title": "Best Email Builder" },
|
|
1487
1508
|
// Most free models reason before they answer: 5 000 to 12 000 tokens
|
|
1488
1509
|
// before the email itself, which a limit of 16 000 cut short. Less
|
|
@@ -1585,6 +1606,66 @@ function resolveProvider(provider) {
|
|
|
1585
1606
|
] : AI_PROVIDERS["openai-compatible"];
|
|
1586
1607
|
return define({ ...base, ...provider });
|
|
1587
1608
|
}
|
|
1609
|
+
var NOT_A_WRITER = /embed|tts|audio|transcri|whisper|speech|voxtral|image|imagen|dall-e|sora|veo|lyria|music|moderation|realtime|(^|[-_/.])live($|[-_.])|search|computer-use|robotics|\baqa\b|nano-banana|deep-research|antigravity|guard|safety|ocr|rerank|translate|customtools|codex|^(davinci|babbage)|turbo-instruct/i;
|
|
1610
|
+
function writes(entry) {
|
|
1611
|
+
const id = typeof entry?.id === "string" ? entry.id : "";
|
|
1612
|
+
if (!id || NOT_A_WRITER.test(id)) return false;
|
|
1613
|
+
const outputs = entry.architecture?.output_modalities;
|
|
1614
|
+
if (Array.isArray(outputs) && !outputs.every((output) => output === "text")) return false;
|
|
1615
|
+
return entry.capabilities?.completion_chat !== false;
|
|
1616
|
+
}
|
|
1617
|
+
var entryOf = (entry) => {
|
|
1618
|
+
const id = String(entry.id).replace(/^models\//, "");
|
|
1619
|
+
const price = entry.pricing;
|
|
1620
|
+
return {
|
|
1621
|
+
id,
|
|
1622
|
+
name: String(entry.display_name || entry.name || id),
|
|
1623
|
+
free: /:free$/.test(id) || price != null && Number(price.prompt) === 0 && Number(price.completion) === 0,
|
|
1624
|
+
created: Number(entry.created) || (entry.created_at ? Date.parse(entry.created_at) / 1e3 : 0) || 0
|
|
1625
|
+
};
|
|
1626
|
+
};
|
|
1627
|
+
var versionOf = (id) => Number(/(\d+(?:\.\d+)?)/.exec(id)?.[1] || 0);
|
|
1628
|
+
function orderModels(entries, defaultModel) {
|
|
1629
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1630
|
+
const models = entries.filter((entry) => entry?.id && !seen.has(entry.id) && seen.add(entry.id));
|
|
1631
|
+
const rank = (entry) => entry.id === defaultModel ? 0 : entry.free ? 1 : 2;
|
|
1632
|
+
const dated = models.some((entry) => entry.created);
|
|
1633
|
+
return models.map((entry, at) => ({ entry, at })).sort(
|
|
1634
|
+
(a, b) => rank(a.entry) - rank(b.entry) || (dated ? (b.entry.created || 0) - (a.entry.created || 0) : versionOf(b.entry.id) - versionOf(a.entry.id)) || a.at - b.at
|
|
1635
|
+
).map(({ entry }) => Object.freeze({ id: entry.id, name: entry.name || entry.id, free: Boolean(entry.free) }));
|
|
1636
|
+
}
|
|
1637
|
+
async function listProviderModels(options = {}) {
|
|
1638
|
+
const { apiKey = "", fetch: fetchImpl, signal } = options;
|
|
1639
|
+
const provider = resolveProvider(options.provider ?? "openai-compatible");
|
|
1640
|
+
if (provider.listModels) return orderModels(await provider.listModels({ apiKey, fetch: fetchImpl, signal }), provider.model);
|
|
1641
|
+
const baseUrl = String(provider.baseUrl).replace(/\/+$/, "");
|
|
1642
|
+
if (!baseUrl) return [];
|
|
1643
|
+
if (!apiKey && !provider.keyless && !provider.publicModels) throw new EmailGenerationError("KEY_MISSING", `${provider.name}: no key`);
|
|
1644
|
+
let response;
|
|
1645
|
+
try {
|
|
1646
|
+
response = await (fetchImpl || globalThis.fetch)(`${baseUrl}/models`, {
|
|
1647
|
+
headers: { ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, ...provider.headers },
|
|
1648
|
+
signal
|
|
1649
|
+
});
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (signal?.aborted || /** @type {any} */
|
|
1652
|
+
error?.name === "AbortError") throw error;
|
|
1653
|
+
throw new EmailGenerationError("MODEL_ERROR", `${provider.name}: ${/** @type {any} */
|
|
1654
|
+
error?.message || error}`);
|
|
1655
|
+
}
|
|
1656
|
+
const text2 = await response.text();
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
throw new EmailGenerationError(failureCode(response.status, errorMessage(text2)), `${provider.name} ${response.status}: ${text2.slice(0, 300)}`);
|
|
1659
|
+
}
|
|
1660
|
+
let data;
|
|
1661
|
+
try {
|
|
1662
|
+
data = JSON.parse(text2);
|
|
1663
|
+
} catch {
|
|
1664
|
+
throw new EmailGenerationError("MODEL_ERROR", `${provider.name}: not JSON \u2014 ${text2.slice(0, 200)}`);
|
|
1665
|
+
}
|
|
1666
|
+
const entries = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [];
|
|
1667
|
+
return orderModels(entries.filter(writes).map(entryOf), provider.model);
|
|
1668
|
+
}
|
|
1588
1669
|
var answerInstruction = (schema) => `
|
|
1589
1670
|
|
|
1590
1671
|
# Answer format
|
|
@@ -1614,6 +1695,15 @@ function refusal(status, message) {
|
|
|
1614
1695
|
if (/\bmodels?\b|harness|region|country|territory|not available|not allowed to (sample|access|use)/i.test(message)) return "MODEL_ERROR";
|
|
1615
1696
|
return "KEY_REFUSED";
|
|
1616
1697
|
}
|
|
1698
|
+
var missingModel = (status, message) => status === 404 && /\bmodel/i.test(message) || (status === 400 || status === 404) && /not a valid model|(unknown|invalid) model|model\b.*\b(not found|does not exist|no longer available)/i.test(message);
|
|
1699
|
+
function failureCode(status, message) {
|
|
1700
|
+
if (missingModel(status, message)) return "MODEL_UNAVAILABLE";
|
|
1701
|
+
const refused = refusal(status, message);
|
|
1702
|
+
if (refused) return refused;
|
|
1703
|
+
if (status === 402 || /credit balance|insufficient[ _](quota|credits|balance)|exceeded your current quota/i.test(message)) return "MODEL_CREDITS";
|
|
1704
|
+
if (status === 429 || status === 503 || status === 529) return "MODEL_BUSY";
|
|
1705
|
+
return "MODEL_ERROR";
|
|
1706
|
+
}
|
|
1617
1707
|
function textOf(message) {
|
|
1618
1708
|
const content = message?.content;
|
|
1619
1709
|
if (typeof content === "string") return content;
|
|
@@ -1673,11 +1763,7 @@ function providerEmailModel(options = {}) {
|
|
|
1673
1763
|
continue;
|
|
1674
1764
|
}
|
|
1675
1765
|
const detail = `${provider.name} ${response.status}: ${text2.slice(0, 300)}`;
|
|
1676
|
-
|
|
1677
|
-
if (refused) throw new EmailGenerationError(refused, detail);
|
|
1678
|
-
if (response.status === 402) throw new EmailGenerationError("MODEL_CREDITS", detail);
|
|
1679
|
-
if (response.status === 429 || response.status === 503 || response.status === 529) throw new EmailGenerationError("MODEL_BUSY", detail);
|
|
1680
|
-
throw new EmailGenerationError("MODEL_ERROR", detail);
|
|
1766
|
+
throw new EmailGenerationError(failureCode(response.status, errorMessage(text2)), detail);
|
|
1681
1767
|
}
|
|
1682
1768
|
let data;
|
|
1683
1769
|
try {
|
|
@@ -1688,10 +1774,7 @@ function providerEmailModel(options = {}) {
|
|
|
1688
1774
|
if (data?.error) {
|
|
1689
1775
|
const code = Number(data.error.code) || 0;
|
|
1690
1776
|
const detail = `${provider.name}: ${data.error.message || JSON.stringify(data.error).slice(0, 300)}`;
|
|
1691
|
-
|
|
1692
|
-
if (refused) throw new EmailGenerationError(refused, detail);
|
|
1693
|
-
if (code === 429 || code === 503) throw new EmailGenerationError("MODEL_BUSY", detail);
|
|
1694
|
-
throw new EmailGenerationError("MODEL_ERROR", detail);
|
|
1777
|
+
throw new EmailGenerationError(failureCode(code, String(data.error.metadata?.raw || data.error.message || "")), detail);
|
|
1695
1778
|
}
|
|
1696
1779
|
const choice = data?.choices?.[0];
|
|
1697
1780
|
if (choice?.message?.refusal) throw new EmailGenerationError("REFUSED", String(choice.message.refusal));
|
|
@@ -1719,16 +1802,21 @@ function claudeEmailModel({
|
|
|
1719
1802
|
throw new Error("claudeEmailModel: pass an Anthropic client \u2014 `new Anthropic()` from @anthropic-ai/sdk");
|
|
1720
1803
|
}
|
|
1721
1804
|
return async ({ system, messages, schema, signal, task }) => {
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1805
|
+
let message;
|
|
1806
|
+
try {
|
|
1807
|
+
message = await client.messages.stream(
|
|
1808
|
+
{
|
|
1809
|
+
model,
|
|
1810
|
+
max_tokens: maxTokens,
|
|
1811
|
+
system,
|
|
1812
|
+
messages,
|
|
1813
|
+
output_config: { effort: task === "rewrite" || task === "subjects" ? rewriteEffort : effort, format: { type: "json_schema", schema } }
|
|
1814
|
+
},
|
|
1815
|
+
{ signal }
|
|
1816
|
+
).finalMessage();
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
throw sdkFailure(error, signal);
|
|
1819
|
+
}
|
|
1732
1820
|
if (message.stop_reason === "refusal") {
|
|
1733
1821
|
throw new EmailGenerationError("REFUSED");
|
|
1734
1822
|
}
|
|
@@ -1738,6 +1826,136 @@ function claudeEmailModel({
|
|
|
1738
1826
|
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1739
1827
|
};
|
|
1740
1828
|
}
|
|
1829
|
+
function sdkFailure(error, signal) {
|
|
1830
|
+
const { status, name } = (
|
|
1831
|
+
/** @type {any} */
|
|
1832
|
+
error || {}
|
|
1833
|
+
);
|
|
1834
|
+
if (signal?.aborted || name === "AbortError" || name === "APIUserAbortError" || error instanceof EmailGenerationError) return error;
|
|
1835
|
+
const text2 = String(
|
|
1836
|
+
/** @type {any} */
|
|
1837
|
+
error?.error?.error?.message || /** @type {any} */
|
|
1838
|
+
error?.message || error
|
|
1839
|
+
);
|
|
1840
|
+
if (typeof status !== "number") return new EmailGenerationError("MODEL_ERROR", `Claude: ${text2.slice(0, 300)}`);
|
|
1841
|
+
return new EmailGenerationError(failureCode(status, text2), `Claude ${status}: ${text2.slice(0, 300)}`);
|
|
1842
|
+
}
|
|
1843
|
+
function claudeProvider(options) {
|
|
1844
|
+
const { client, ...rest } = options || /** @type {any} */
|
|
1845
|
+
{};
|
|
1846
|
+
if (typeof client !== "function") {
|
|
1847
|
+
throw new TypeError("claudeProvider: `client` is a function that makes the Anthropic client from a key");
|
|
1848
|
+
}
|
|
1849
|
+
return Object.freeze({
|
|
1850
|
+
id: "anthropic",
|
|
1851
|
+
name: "Claude",
|
|
1852
|
+
baseUrl: "https://api.anthropic.com",
|
|
1853
|
+
model: rest.model || DEFAULT_CLAUDE_MODEL,
|
|
1854
|
+
keyUrl: "https://console.anthropic.com/settings/keys",
|
|
1855
|
+
keyHint: "sk-ant-",
|
|
1856
|
+
free: null,
|
|
1857
|
+
format: (
|
|
1858
|
+
/** @type {const} */
|
|
1859
|
+
"json_schema"
|
|
1860
|
+
),
|
|
1861
|
+
tokens: (
|
|
1862
|
+
/** @type {const} */
|
|
1863
|
+
"max_tokens"
|
|
1864
|
+
),
|
|
1865
|
+
maxTokens: rest.maxTokens ?? 32e3,
|
|
1866
|
+
createModel: ({ apiKey, model }) => {
|
|
1867
|
+
let current = null;
|
|
1868
|
+
return async (request) => {
|
|
1869
|
+
const key = String(await apiKey() || "").trim();
|
|
1870
|
+
if (!key) throw new EmailGenerationError("KEY_MISSING", "Claude: no key");
|
|
1871
|
+
if (!current || current.key !== key) {
|
|
1872
|
+
current = { key, model: claudeEmailModel({ ...rest, ...model ? { model } : {}, client: await client(key) }) };
|
|
1873
|
+
}
|
|
1874
|
+
return current.model(request);
|
|
1875
|
+
};
|
|
1876
|
+
},
|
|
1877
|
+
listModels: async ({ apiKey, signal }) => {
|
|
1878
|
+
const key = String(apiKey || "").trim();
|
|
1879
|
+
if (!key) throw new EmailGenerationError("KEY_MISSING", "Claude: no key");
|
|
1880
|
+
try {
|
|
1881
|
+
const page = await (await client(key)).models.list({ limit: 100 }, { signal });
|
|
1882
|
+
return (page?.data || []).map((entry) => ({
|
|
1883
|
+
id: String(entry.id),
|
|
1884
|
+
name: String(entry.display_name || entry.id),
|
|
1885
|
+
created: entry.created_at ? Date.parse(entry.created_at) / 1e3 : 0
|
|
1886
|
+
}));
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
throw sdkFailure(error, signal);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/ai/agent.js
|
|
1895
|
+
var AGENT_PROTOCOL = "best-email-builder/agent@1";
|
|
1896
|
+
var AGENT_ERROR_CODES = Object.freeze(["REFUSED", "TRUNCATED", "MODEL_UNAVAILABLE", "MODEL_CREDITS", "MODEL_BUSY", "MODEL_ERROR"]);
|
|
1897
|
+
var DEFAULT_TIMEOUT = 18e4;
|
|
1898
|
+
var statusCode = (status) => status === 429 || status === 503 || status === 504 || status === 529 ? "MODEL_BUSY" : "MODEL_ERROR";
|
|
1899
|
+
var isAgentCode = (code) => typeof code === "string" && AGENT_ERROR_CODES.includes(
|
|
1900
|
+
/** @type {any} */
|
|
1901
|
+
code
|
|
1902
|
+
);
|
|
1903
|
+
var errorText = (data) => {
|
|
1904
|
+
const error = data?.error ?? data?.detail;
|
|
1905
|
+
if (typeof error === "string") return error;
|
|
1906
|
+
return String(error?.message || (error ? JSON.stringify(error) : ""));
|
|
1907
|
+
};
|
|
1908
|
+
function agentEmailModel(options) {
|
|
1909
|
+
const { url, headers, fetch: fetchImpl, timeout = DEFAULT_TIMEOUT, name = "Agent" } = options || /** @type {any} */
|
|
1910
|
+
{};
|
|
1911
|
+
if (typeof url !== "string" || !url) throw new TypeError("agentEmailModel: `url` is required \u2014 the endpoint of your agent");
|
|
1912
|
+
if (headers !== void 0 && typeof headers !== "function" && (typeof headers !== "object" || headers === null)) {
|
|
1913
|
+
throw new TypeError("agentEmailModel: `headers` is an object, or a function of { task, user } that returns one");
|
|
1914
|
+
}
|
|
1915
|
+
const emailModel = async ({ system, messages, schema, signal, task = "generate", user }) => {
|
|
1916
|
+
const call = fetchImpl || globalThis.fetch;
|
|
1917
|
+
const own = typeof headers === "function" ? await headers({ task, user }) : headers;
|
|
1918
|
+
signal?.throwIfAborted();
|
|
1919
|
+
const controller = new AbortController();
|
|
1920
|
+
const stop = () => controller.abort(signal?.reason);
|
|
1921
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
1922
|
+
let late = false;
|
|
1923
|
+
const timer = timeout > 0 ? setTimeout(() => (late = true, controller.abort()), timeout) : null;
|
|
1924
|
+
let response;
|
|
1925
|
+
let text2;
|
|
1926
|
+
try {
|
|
1927
|
+
response = await call(url, {
|
|
1928
|
+
method: "POST",
|
|
1929
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...own },
|
|
1930
|
+
body: JSON.stringify({ protocol: AGENT_PROTOCOL, task, system: system + answerInstruction(schema), messages, schema }),
|
|
1931
|
+
signal: controller.signal
|
|
1932
|
+
});
|
|
1933
|
+
text2 = await response.text();
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
1936
|
+
if (late) throw new EmailGenerationError("MODEL_BUSY", `${name}: no answer after ${Math.round(timeout / 1e3)} s`);
|
|
1937
|
+
throw new EmailGenerationError("MODEL_ERROR", `${name}: ${/** @type {any} */
|
|
1938
|
+
error?.message || error}`);
|
|
1939
|
+
} finally {
|
|
1940
|
+
if (timer) clearTimeout(timer);
|
|
1941
|
+
signal?.removeEventListener("abort", stop);
|
|
1942
|
+
}
|
|
1943
|
+
let data = null;
|
|
1944
|
+
try {
|
|
1945
|
+
data = JSON.parse(text2);
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
if (!response.ok || data && typeof data === "object" && "error" in data && !("text" in data) && !("output" in data)) {
|
|
1949
|
+
const said = data ? errorText(data) : text2;
|
|
1950
|
+
const detail = `${name} ${response.status}: ${said.slice(0, 300)}`;
|
|
1951
|
+
throw new EmailGenerationError(isAgentCode(data?.code) ? data.code : statusCode(response.ok ? 502 : response.status), detail);
|
|
1952
|
+
}
|
|
1953
|
+
if (typeof data?.text === "string") return data.text;
|
|
1954
|
+
if (data?.output && typeof data.output === "object") return JSON.stringify(data.output);
|
|
1955
|
+
return text2;
|
|
1956
|
+
};
|
|
1957
|
+
return Object.assign(emailModel, { url });
|
|
1958
|
+
}
|
|
1741
1959
|
|
|
1742
1960
|
export {
|
|
1743
1961
|
GENERATION_ERRORS,
|
|
@@ -1768,8 +1986,14 @@ export {
|
|
|
1768
1986
|
createEmailGenerator,
|
|
1769
1987
|
AI_PROVIDERS,
|
|
1770
1988
|
resolveProvider,
|
|
1989
|
+
listProviderModels,
|
|
1990
|
+
failureCode,
|
|
1771
1991
|
providerEmailModel,
|
|
1772
1992
|
DEFAULT_CLAUDE_MODEL,
|
|
1773
|
-
claudeEmailModel
|
|
1993
|
+
claudeEmailModel,
|
|
1994
|
+
claudeProvider,
|
|
1995
|
+
AGENT_PROTOCOL,
|
|
1996
|
+
AGENT_ERROR_CODES,
|
|
1997
|
+
agentEmailModel
|
|
1774
1998
|
};
|
|
1775
|
-
//# sourceMappingURL=chunk-
|
|
1999
|
+
//# sourceMappingURL=chunk-CTZ56YJG.js.map
|