best-email-builder 0.5.0 → 0.6.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 +57 -0
- package/README.md +50 -10
- package/dist/ai-server.js +6 -4
- package/dist/ai.js +167 -39
- package/dist/ai.js.map +1 -1
- package/dist/builder.js +9 -9
- package/dist/{chunk-ECHGJZ2W.js → chunk-4U7L2NYQ.js} +32 -3
- package/dist/chunk-4U7L2NYQ.js.map +1 -0
- package/dist/{chunk-6D3SLT5W.js → chunk-5W4TUUPD.js} +4 -4
- package/dist/{chunk-LYMJ2TBX.js → chunk-CNJZLZRH.js} +2 -2
- package/dist/{chunk-DFNHICTF.js → chunk-JI36FNWE.js} +3 -1
- package/dist/chunk-JI36FNWE.js.map +1 -0
- package/dist/{chunk-LXR4QTGP.js → chunk-JU3OUO6X.js} +3 -3
- package/dist/{chunk-PB7YZ3WP.js → chunk-NC2YDZSP.js} +19 -65
- package/dist/chunk-NC2YDZSP.js.map +1 -0
- package/dist/{chunk-MPX5CWTZ.js → chunk-NZIQF7IW.js} +172 -29
- package/dist/chunk-NZIQF7IW.js.map +1 -0
- package/dist/{chunk-2RZHGAH5.js → chunk-OWVXKMLI.js} +59 -2
- package/dist/chunk-OWVXKMLI.js.map +1 -0
- package/dist/{chunk-7U6BAKM7.js → chunk-QPAZLCPO.js} +3 -3
- package/dist/{chunk-7U6BAKM7.js.map → chunk-QPAZLCPO.js.map} +1 -1
- package/dist/{chunk-KXJJSVN5.js → chunk-SAIW46IC.js} +2 -2
- package/dist/{chunk-5SASKCUC.js → chunk-W424KPCR.js} +2 -2
- package/dist/{chunk-3LKF4QBL.js → chunk-XEFBPP5Y.js} +4 -4
- package/dist/index.js +11 -11
- package/dist/library.js +5 -5
- package/dist/server.js +12 -29
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/ai/ConnectAi.d.ts +4 -0
- package/dist/types/ai/claude.d.ts +30 -5
- package/dist/types/ai/createAi.d.ts +5 -3
- package/dist/types/ai/draft.d.ts +12 -0
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/keys.d.ts +15 -26
- package/dist/types/ai/providers.d.ts +24 -2
- package/dist/types/ai/server.d.ts +1 -1
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/engine/NativeAdapter.d.ts +1 -1
- package/dist/types/renderer/prepareMjml.d.ts +8 -2
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/server/render.d.ts +5 -2
- package/package.json +1 -1
- package/dist/chunk-2RZHGAH5.js.map +0 -1
- package/dist/chunk-DFNHICTF.js.map +0 -1
- package/dist/chunk-ECHGJZ2W.js.map +0 -1
- package/dist/chunk-MPX5CWTZ.js.map +0 -1
- package/dist/chunk-PB7YZ3WP.js.map +0 -1
- /package/dist/{chunk-6D3SLT5W.js.map → chunk-5W4TUUPD.js.map} +0 -0
- /package/dist/{chunk-LYMJ2TBX.js.map → chunk-CNJZLZRH.js.map} +0 -0
- /package/dist/{chunk-LXR4QTGP.js.map → chunk-JU3OUO6X.js.map} +0 -0
- /package/dist/{chunk-KXJJSVN5.js.map → chunk-SAIW46IC.js.map} +0 -0
- /package/dist/{chunk-5SASKCUC.js.map → chunk-W424KPCR.js.map} +0 -0
- /package/dist/{chunk-3LKF4QBL.js.map → chunk-XEFBPP5Y.js.map} +0 -0
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LONGHAND_SPACING,
|
|
3
|
+
MJML_DEFAULTS,
|
|
3
4
|
SIDES,
|
|
4
5
|
parseSpacing
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OWVXKMLI.js";
|
|
6
7
|
import {
|
|
7
8
|
isSafeUrl,
|
|
8
9
|
validateMjml
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-W424KPCR.js";
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_VARIABLES,
|
|
12
13
|
FONT_OPTIONS,
|
|
13
14
|
PALETTE,
|
|
14
15
|
applyBrand,
|
|
15
16
|
cleanBrand,
|
|
17
|
+
contrastRatio,
|
|
18
|
+
createId,
|
|
16
19
|
createNode,
|
|
20
|
+
parseColor,
|
|
17
21
|
parseMjml,
|
|
22
|
+
parseMjmlFragment,
|
|
23
|
+
readableOn,
|
|
18
24
|
serializeMjml
|
|
19
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-JI36FNWE.js";
|
|
20
26
|
|
|
21
27
|
// src/ai/draft.js
|
|
22
28
|
var GENERATION_ERRORS = {
|
|
@@ -113,6 +119,11 @@ function cleanAttributes(node, keepImages) {
|
|
|
113
119
|
if (unitless && (PIXEL_ATTRIBUTES.has(name) || /^padding-/.test(name))) attributes[name] = `${value.trim()}px`;
|
|
114
120
|
}
|
|
115
121
|
if (node.tag === "mj-image" && attributes.src !== void 0 && !keepImages.has(attributes.src.trim())) attributes.src = "";
|
|
122
|
+
if (node.tag === "mj-button") {
|
|
123
|
+
const fill = attributes["background-color"] ?? MJML_DEFAULTS["mj-button"]["background-color"];
|
|
124
|
+
const words = attributes.color ?? MJML_DEFAULTS["mj-button"].color;
|
|
125
|
+
if (parseColor(fill) && parseColor(words) && contrastRatio(fill, words) < 3) attributes.color = readableOn(fill);
|
|
126
|
+
}
|
|
116
127
|
delete attributes["background-url"];
|
|
117
128
|
return attributes;
|
|
118
129
|
}
|
|
@@ -133,6 +144,30 @@ function wrapLoose(children, isLoose, wrap) {
|
|
|
133
144
|
flush();
|
|
134
145
|
return out;
|
|
135
146
|
}
|
|
147
|
+
var MJML_IN_HTML = /<mj-image\b(?:[^>"']|"[^"]*"|'[^']*')*?(?:\/>|>[\s\S]*?<\/mj-image\s*>)|<\/?mj-[a-z-]*\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
|
148
|
+
function liftImages(node, attributes, keepImages) {
|
|
149
|
+
const content = String(node.content);
|
|
150
|
+
const parts = [];
|
|
151
|
+
let text2 = "";
|
|
152
|
+
const flush = () => {
|
|
153
|
+
if (text2.replace(/<[^>]*>| |\s/g, "")) {
|
|
154
|
+
parts.push({ ...node, id: parts.length ? createId() : node.id, attributes, content: text2.trim() });
|
|
155
|
+
}
|
|
156
|
+
text2 = "";
|
|
157
|
+
};
|
|
158
|
+
let last = 0;
|
|
159
|
+
for (const match of content.matchAll(MJML_IN_HTML)) {
|
|
160
|
+
text2 += content.slice(last, match.index);
|
|
161
|
+
last = (match.index ?? 0) + match[0].length;
|
|
162
|
+
const image = /^<mj-image\b/i.test(match[0]) && parseMjmlFragment(match[0]).find((child) => child.tag === "mj-image");
|
|
163
|
+
if (!image) continue;
|
|
164
|
+
flush();
|
|
165
|
+
parts.push(...cleanNode(image, keepImages));
|
|
166
|
+
}
|
|
167
|
+
text2 += content.slice(last);
|
|
168
|
+
flush();
|
|
169
|
+
return parts;
|
|
170
|
+
}
|
|
136
171
|
function cleanNode(node, keepImages) {
|
|
137
172
|
if (REMOVED_TAGS.has(node.tag)) return [];
|
|
138
173
|
const children = node.children.flatMap((child) => cleanNode(child, keepImages));
|
|
@@ -156,6 +191,10 @@ function cleanNode(node, keepImages) {
|
|
|
156
191
|
const wrapped = wrapLoose(sectioned, (child) => CONTENT_TAGS.has(child.tag), (nodes) => whiteSection([column(nodes)]));
|
|
157
192
|
return [{ ...node, attributes: { "background-color": PALETTE.canvas, ...attributes, width: "600px" }, children: wrapped }];
|
|
158
193
|
}
|
|
194
|
+
if (typeof node.content === "string" && /<\/?mj-/i.test(node.content)) {
|
|
195
|
+
if (node.tag === "mj-text") return liftImages(node, attributes, keepImages);
|
|
196
|
+
return [{ ...node, attributes, children, content: node.content.replace(MJML_IN_HTML, "") }];
|
|
197
|
+
}
|
|
159
198
|
return [{ ...node, attributes, children }];
|
|
160
199
|
}
|
|
161
200
|
var placeholderOf = (key) => `[${key.replace(/[._]+/g, " ").trim()}]`;
|
|
@@ -173,6 +212,21 @@ function normalizeGeneratedMjml(mjml, { variables = [], keepImages = [] } = {})
|
|
|
173
212
|
const known = /* @__PURE__ */ new Set(["unsubscribe", ...variables.map((variable) => variable.key)]);
|
|
174
213
|
return replaceUnknownVariables(serializeMjml(root), known);
|
|
175
214
|
}
|
|
215
|
+
function withoutIllegalAttributes(mjml, warnings) {
|
|
216
|
+
const illegal = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const warning of warnings) {
|
|
218
|
+
const name = /Attribute ([\w-]+) is illegal/.exec(warning.message)?.[1];
|
|
219
|
+
const tag = warning.tagName || /\((mj-[\w-]+)\)/.exec(warning.message)?.[1];
|
|
220
|
+
if (name && tag) illegal.set(tag, /* @__PURE__ */ new Set([...illegal.get(tag) || [], name]));
|
|
221
|
+
}
|
|
222
|
+
if (!illegal.size) return null;
|
|
223
|
+
const strip = (node) => {
|
|
224
|
+
const names = illegal.get(node.tag);
|
|
225
|
+
const attributes = names ? Object.fromEntries(Object.entries(node.attributes).filter(([name]) => !names.has(name))) : node.attributes;
|
|
226
|
+
return { ...node, attributes, children: node.children.map(strip) };
|
|
227
|
+
};
|
|
228
|
+
return serializeMjml(strip(parseMjml(mjml)));
|
|
229
|
+
}
|
|
176
230
|
function reviewDraft(draft, { kind = "marketing", compileWarnings = [] } = {}) {
|
|
177
231
|
const blocking = validateMjml(draft.mjml).issues.map((issue) => issue.message);
|
|
178
232
|
const advice = [];
|
|
@@ -241,7 +295,7 @@ Return one JSON object, nothing else: {"name": string, "subject": string, "prehe
|
|
|
241
295
|
|
|
242
296
|
# Copy
|
|
243
297
|
- Write the real email for the purpose and the details given: specific, useful, warm and concise. Never lorem ipsum, never meta comments.
|
|
244
|
-
- Personalize with the variables listed in the request, written exactly as {{key}}. A greeting gives the first name a natural default after a pipe, in the email's language,
|
|
298
|
+
- Personalize with the variables listed in the request, written exactly as {{key}}. A greeting gives the first name a natural default after a pipe, used when the name is unknown; the greeting and its default are in the email's language: Hello {{contact.firstname|there}}, in English, Bonjour {{contact.firstname|\xE0 vous}}, in French, \u0412\u0456\u0442\u0430\u0454\u043C\u043E, {{contact.firstname|\u0434\u0440\u0443\u0436\u0435}}! in Ukrainian. Use no other {{...}} variable.
|
|
245
299
|
- Information that is not given and not in the variables (an order number, a date, a price, a code, an address) is written as a short placeholder in square brackets that the user will replace: [order number], [date], [20%]. Never invent facts that would be wrong.
|
|
246
300
|
- Links read as what they do ("Reset my password", "See the collection"), never "click here".
|
|
247
301
|
- The footer (last section): the sender's name and postal address (or [Company address]) in 12px #6d7484 text, centered, and the footer line the request asks for. The unsubscribe link is exactly <a href="{{ unsubscribe }}" style="color:#6d7484;text-decoration:underline;">...</a>.
|
|
@@ -1330,7 +1384,10 @@ var fr = {
|
|
|
1330
1384
|
"Free models, with a free account": "Mod\xE8les gratuits, avec un compte gratuit",
|
|
1331
1385
|
"Free tier, with rate limits": "Offre gratuite, avec des limites de d\xE9bit",
|
|
1332
1386
|
'Free "Experiment" plan': "Offre gratuite \xAB Experiment \xBB",
|
|
1333
|
-
"Runs on your own machine": "Tourne sur votre propre machine"
|
|
1387
|
+
"Runs on your own machine": "Tourne sur votre propre machine",
|
|
1388
|
+
"AI provider": "Fournisseur d'IA",
|
|
1389
|
+
"Key saved": "Cl\xE9 enregistr\xE9e",
|
|
1390
|
+
"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."
|
|
1334
1391
|
};
|
|
1335
1392
|
|
|
1336
1393
|
// src/ai/i18n/index.js
|
|
@@ -1385,7 +1442,12 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1385
1442
|
const draft = parseDraft(answer);
|
|
1386
1443
|
draft.mjml = normalizeGeneratedMjml(draft.mjml, { variables: request.variables, keepImages });
|
|
1387
1444
|
draft.mjml = applyBrand(draft.mjml, request.brand);
|
|
1388
|
-
|
|
1445
|
+
let compileWarnings = compile ? (await compile(draft.mjml)).warnings || [] : [];
|
|
1446
|
+
const legal = withoutIllegalAttributes(draft.mjml, compileWarnings);
|
|
1447
|
+
if (legal && compile) {
|
|
1448
|
+
draft.mjml = legal;
|
|
1449
|
+
compileWarnings = (await compile(draft.mjml)).warnings || [];
|
|
1450
|
+
}
|
|
1389
1451
|
const { blocking, advice } = reviewDraft(draft, { kind, compileWarnings });
|
|
1390
1452
|
if (!blocking.length) {
|
|
1391
1453
|
usable = { ...draft, warnings: advice, attempts: attempt };
|
|
@@ -1412,18 +1474,24 @@ function createEmailGenerator({ model, maxAttempts = 2, compile } = (
|
|
|
1412
1474
|
}
|
|
1413
1475
|
|
|
1414
1476
|
// src/ai/providers.js
|
|
1415
|
-
var define = (provider) => Object.freeze({ format: "json_schema", tokens: "max_tokens", ...provider });
|
|
1477
|
+
var define = (provider) => Object.freeze({ format: "json_schema", tokens: "max_tokens", maxTokens: 16e3, ...provider });
|
|
1416
1478
|
var AI_PROVIDERS = Object.freeze({
|
|
1417
1479
|
openrouter: define({
|
|
1418
1480
|
id: "openrouter",
|
|
1419
1481
|
name: "OpenRouter",
|
|
1420
1482
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
1421
|
-
// A router across the free models of the day
|
|
1483
|
+
// A router across the free models of the day — a name that lasts, where
|
|
1484
|
+
// the free models themselves come and go (and an unknown one is a 400).
|
|
1422
1485
|
model: "openrouter/free",
|
|
1423
1486
|
keyUrl: "https://openrouter.ai/keys",
|
|
1424
1487
|
keyHint: "sk-or-",
|
|
1425
1488
|
free: "Free models, with a free account",
|
|
1426
|
-
headers: { "X-Title": "Best Email Builder" }
|
|
1489
|
+
headers: { "X-Title": "Best Email Builder" },
|
|
1490
|
+
// Most free models reason before they answer: 5 000 to 12 000 tokens
|
|
1491
|
+
// before the email itself, which a limit of 16 000 cut short. Less
|
|
1492
|
+
// reasoning where the model lets it be chosen.
|
|
1493
|
+
maxTokens: 32e3,
|
|
1494
|
+
body: { reasoning: { effort: "low" } }
|
|
1427
1495
|
}),
|
|
1428
1496
|
openai: define({
|
|
1429
1497
|
id: "openai",
|
|
@@ -1531,6 +1599,31 @@ function responseFormat(format, schema, task) {
|
|
|
1531
1599
|
return void 0;
|
|
1532
1600
|
}
|
|
1533
1601
|
var formatRefused = (status, text2) => (status === 400 || status === 422) && /response_format|json_schema|json_object|structured|schema|grammar/i.test(text2);
|
|
1602
|
+
function errorMessage(text2) {
|
|
1603
|
+
try {
|
|
1604
|
+
const data = JSON.parse(text2);
|
|
1605
|
+
const error = Array.isArray(data) ? data[0]?.error : data?.error;
|
|
1606
|
+
return String(error?.metadata?.raw || error?.message || text2);
|
|
1607
|
+
} catch {
|
|
1608
|
+
return text2;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function refusal(status, message) {
|
|
1612
|
+
if (status === 401) return "KEY_REFUSED";
|
|
1613
|
+
if (status === 400) return /api[ _-]?key not valid|API_KEY_INVALID|invalid[ _-]api[ _-]key/i.test(message) ? "KEY_REFUSED" : null;
|
|
1614
|
+
if (status !== 403) return null;
|
|
1615
|
+
if (/flagged|moderation/i.test(message)) return "REFUSED";
|
|
1616
|
+
if (/credit|limit exceeded|quota/i.test(message)) return "MODEL_CREDITS";
|
|
1617
|
+
if (/\bmodels?\b|harness|region|country|territory|not available|not allowed to (sample|access|use)/i.test(message)) return "MODEL_ERROR";
|
|
1618
|
+
return "KEY_REFUSED";
|
|
1619
|
+
}
|
|
1620
|
+
function failureCode(status, message) {
|
|
1621
|
+
const refused = refusal(status, message);
|
|
1622
|
+
if (refused) return refused;
|
|
1623
|
+
if (status === 402 || /credit balance|insufficient[ _](quota|credits|balance)|exceeded your current quota/i.test(message)) return "MODEL_CREDITS";
|
|
1624
|
+
if (status === 429 || status === 503 || status === 529) return "MODEL_BUSY";
|
|
1625
|
+
return "MODEL_ERROR";
|
|
1626
|
+
}
|
|
1534
1627
|
function textOf(message) {
|
|
1535
1628
|
const content = message?.content;
|
|
1536
1629
|
if (typeof content === "string") return content;
|
|
@@ -1538,8 +1631,9 @@ function textOf(message) {
|
|
|
1538
1631
|
return "";
|
|
1539
1632
|
}
|
|
1540
1633
|
function providerEmailModel(options = {}) {
|
|
1541
|
-
const { apiKey,
|
|
1634
|
+
const { apiKey, temperature, fetch: fetchImpl } = options;
|
|
1542
1635
|
const provider = resolveProvider(options.provider ?? "openai-compatible");
|
|
1636
|
+
const maxTokens = options.maxTokens ?? provider.maxTokens;
|
|
1543
1637
|
const model = options.model || provider.model;
|
|
1544
1638
|
const baseUrl = String(options.baseUrl || provider.baseUrl).replace(/\/+$/, "");
|
|
1545
1639
|
if (!baseUrl) throw new TypeError(`providerEmailModel: ${provider.name} needs a \`baseUrl\``);
|
|
@@ -1555,6 +1649,8 @@ function providerEmailModel(options = {}) {
|
|
|
1555
1649
|
if (!key && !provider.keyless) throw new EmailGenerationError("KEY_MISSING", `${provider.name}: no key`);
|
|
1556
1650
|
for (; ; ) {
|
|
1557
1651
|
const body = {
|
|
1652
|
+
...provider.body,
|
|
1653
|
+
...options.body,
|
|
1558
1654
|
model,
|
|
1559
1655
|
messages: [{ role: "system", content: system + answerInstruction(schema) }, ...messages],
|
|
1560
1656
|
[provider.tokens]: maxTokens,
|
|
@@ -1587,10 +1683,7 @@ function providerEmailModel(options = {}) {
|
|
|
1587
1683
|
continue;
|
|
1588
1684
|
}
|
|
1589
1685
|
const detail = `${provider.name} ${response.status}: ${text2.slice(0, 300)}`;
|
|
1590
|
-
|
|
1591
|
-
if (response.status === 402) throw new EmailGenerationError("MODEL_CREDITS", detail);
|
|
1592
|
-
if (response.status === 429 || response.status === 503 || response.status === 529) throw new EmailGenerationError("MODEL_BUSY", detail);
|
|
1593
|
-
throw new EmailGenerationError("MODEL_ERROR", detail);
|
|
1686
|
+
throw new EmailGenerationError(failureCode(response.status, errorMessage(text2)), detail);
|
|
1594
1687
|
}
|
|
1595
1688
|
let data;
|
|
1596
1689
|
try {
|
|
@@ -1601,9 +1694,7 @@ function providerEmailModel(options = {}) {
|
|
|
1601
1694
|
if (data?.error) {
|
|
1602
1695
|
const code = Number(data.error.code) || 0;
|
|
1603
1696
|
const detail = `${provider.name}: ${data.error.message || JSON.stringify(data.error).slice(0, 300)}`;
|
|
1604
|
-
|
|
1605
|
-
if (code === 429 || code === 503) throw new EmailGenerationError("MODEL_BUSY", detail);
|
|
1606
|
-
throw new EmailGenerationError("MODEL_ERROR", detail);
|
|
1697
|
+
throw new EmailGenerationError(failureCode(code, String(data.error.metadata?.raw || data.error.message || "")), detail);
|
|
1607
1698
|
}
|
|
1608
1699
|
const choice = data?.choices?.[0];
|
|
1609
1700
|
if (choice?.message?.refusal) throw new EmailGenerationError("REFUSED", String(choice.message.refusal));
|
|
@@ -1631,16 +1722,32 @@ function claudeEmailModel({
|
|
|
1631
1722
|
throw new Error("claudeEmailModel: pass an Anthropic client \u2014 `new Anthropic()` from @anthropic-ai/sdk");
|
|
1632
1723
|
}
|
|
1633
1724
|
return async ({ system, messages, schema, signal, task }) => {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1725
|
+
let message;
|
|
1726
|
+
try {
|
|
1727
|
+
message = await client.messages.stream(
|
|
1728
|
+
{
|
|
1729
|
+
model,
|
|
1730
|
+
max_tokens: maxTokens,
|
|
1731
|
+
system,
|
|
1732
|
+
messages,
|
|
1733
|
+
output_config: { effort: task === "rewrite" || task === "subjects" ? rewriteEffort : effort, format: { type: "json_schema", schema } }
|
|
1734
|
+
},
|
|
1735
|
+
{ signal }
|
|
1736
|
+
).finalMessage();
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
const { status, name } = (
|
|
1739
|
+
/** @type {any} */
|
|
1740
|
+
error || {}
|
|
1741
|
+
);
|
|
1742
|
+
if (signal?.aborted || name === "AbortError" || name === "APIUserAbortError" || error instanceof EmailGenerationError) throw error;
|
|
1743
|
+
const text2 = String(
|
|
1744
|
+
/** @type {any} */
|
|
1745
|
+
error?.error?.error?.message || /** @type {any} */
|
|
1746
|
+
error?.message || error
|
|
1747
|
+
);
|
|
1748
|
+
if (typeof status !== "number") throw new EmailGenerationError("MODEL_ERROR", `Claude: ${text2.slice(0, 300)}`);
|
|
1749
|
+
throw new EmailGenerationError(failureCode(status, text2), `Claude ${status}: ${text2.slice(0, 300)}`);
|
|
1750
|
+
}
|
|
1644
1751
|
if (message.stop_reason === "refusal") {
|
|
1645
1752
|
throw new EmailGenerationError("REFUSED");
|
|
1646
1753
|
}
|
|
@@ -1650,6 +1757,40 @@ function claudeEmailModel({
|
|
|
1650
1757
|
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1651
1758
|
};
|
|
1652
1759
|
}
|
|
1760
|
+
function claudeProvider(options) {
|
|
1761
|
+
const { client, ...rest } = options || /** @type {any} */
|
|
1762
|
+
{};
|
|
1763
|
+
if (typeof client !== "function") {
|
|
1764
|
+
throw new TypeError("claudeProvider: `client` is a function that makes the Anthropic client from a key");
|
|
1765
|
+
}
|
|
1766
|
+
return Object.freeze({
|
|
1767
|
+
id: "anthropic",
|
|
1768
|
+
name: "Claude",
|
|
1769
|
+
baseUrl: "https://api.anthropic.com",
|
|
1770
|
+
model: rest.model || DEFAULT_CLAUDE_MODEL,
|
|
1771
|
+
keyUrl: "https://console.anthropic.com/settings/keys",
|
|
1772
|
+
keyHint: "sk-ant-",
|
|
1773
|
+
free: null,
|
|
1774
|
+
format: (
|
|
1775
|
+
/** @type {const} */
|
|
1776
|
+
"json_schema"
|
|
1777
|
+
),
|
|
1778
|
+
tokens: (
|
|
1779
|
+
/** @type {const} */
|
|
1780
|
+
"max_tokens"
|
|
1781
|
+
),
|
|
1782
|
+
maxTokens: rest.maxTokens ?? 32e3,
|
|
1783
|
+
createModel: ({ apiKey }) => {
|
|
1784
|
+
let current = null;
|
|
1785
|
+
return async (request) => {
|
|
1786
|
+
const key = String(await apiKey() || "").trim();
|
|
1787
|
+
if (!key) throw new EmailGenerationError("KEY_MISSING", "Claude: no key");
|
|
1788
|
+
if (!current || current.key !== key) current = { key, model: claudeEmailModel({ ...rest, client: await client(key) }) };
|
|
1789
|
+
return current.model(request);
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1653
1794
|
|
|
1654
1795
|
export {
|
|
1655
1796
|
GENERATION_ERRORS,
|
|
@@ -1680,8 +1821,10 @@ export {
|
|
|
1680
1821
|
createEmailGenerator,
|
|
1681
1822
|
AI_PROVIDERS,
|
|
1682
1823
|
resolveProvider,
|
|
1824
|
+
failureCode,
|
|
1683
1825
|
providerEmailModel,
|
|
1684
1826
|
DEFAULT_CLAUDE_MODEL,
|
|
1685
|
-
claudeEmailModel
|
|
1827
|
+
claudeEmailModel,
|
|
1828
|
+
claudeProvider
|
|
1686
1829
|
};
|
|
1687
|
-
//# sourceMappingURL=chunk-
|
|
1830
|
+
//# sourceMappingURL=chunk-NZIQF7IW.js.map
|