@nextblock-cms/cortex 0.15.10 → 0.16.2
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.cjs.js +1 -1
- package/index.d.ts +2 -0
- package/index.es.js +153 -139
- package/lib/ai-client.cjs.js +1 -1
- package/lib/ai-client.es.js +20 -18
- package/lib/ai-seo-metadata.cjs.js +4 -0
- package/lib/ai-seo-metadata.d.ts +127 -0
- package/lib/ai-seo-metadata.es.js +193 -0
- package/lib/ai-vision.cjs.js +3 -0
- package/lib/ai-vision.d.ts +133 -0
- package/lib/ai-vision.es.js +227 -0
- package/package.json +4 -4
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { generateText as _ } from "ai";
|
|
2
|
+
import { runWithCortexAiModelFallback as w, omitUnsupportedCortexAiModelOptions as I, getHttpStatusCode as y, isOpenRouterRecoverableRoutingError as R } from "./ai-model-registry.es.js";
|
|
3
|
+
import { truncateOnWordBoundary as C } from "./ai-seo-metadata.es.js";
|
|
4
|
+
const S = "Cortex AI vision generation can only be imported from server-side code.";
|
|
5
|
+
function L() {
|
|
6
|
+
if (!(typeof window > "u"))
|
|
7
|
+
throw new Error(S);
|
|
8
|
+
}
|
|
9
|
+
const p = [
|
|
10
|
+
"google/gemma-4-31b-it:free",
|
|
11
|
+
"google/gemma-4-26b-a4b-it:free",
|
|
12
|
+
"google/gemini-2.5-flash-lite",
|
|
13
|
+
"qwen/qwen3-vl-32b-instruct",
|
|
14
|
+
"openai/gpt-4o-mini",
|
|
15
|
+
"google/gemini-2.5-flash"
|
|
16
|
+
], O = [
|
|
17
|
+
// Dedicated vision-language builds: "qwen3-vl-32b-instruct", "internvl3-78b".
|
|
18
|
+
/(?:^|\/)[^/]*-vl(?:[-:]|$)/i,
|
|
19
|
+
// Explicitly named vision variants: "llama-3.2-11b-vision-instruct".
|
|
20
|
+
/vision/i,
|
|
21
|
+
// Whole families that are multimodal end to end.
|
|
22
|
+
/(?:^|\/)gemini-/i,
|
|
23
|
+
/(?:^|\/)gemma-[3-9]/i,
|
|
24
|
+
/(?:^|\/)gpt-(?:4o|4\.1|5)/i,
|
|
25
|
+
/(?:^|\/)claude-/i,
|
|
26
|
+
/(?:^|\/)llama-4/i,
|
|
27
|
+
/(?:^|\/)pixtral/i,
|
|
28
|
+
/(?:^|\/)mistral-(?:small|medium|large)-3/i,
|
|
29
|
+
/(?:^|\/)nova-(?:lite|pro|premier)/i
|
|
30
|
+
], f = 125, m = { max: 1e3, min: 20 }, M = 6e4, g = /* @__PURE__ */ new Set([
|
|
31
|
+
'"',
|
|
32
|
+
"'",
|
|
33
|
+
"«",
|
|
34
|
+
// «
|
|
35
|
+
"»",
|
|
36
|
+
// »
|
|
37
|
+
"‘",
|
|
38
|
+
// ‘
|
|
39
|
+
"’",
|
|
40
|
+
// ’
|
|
41
|
+
"‚",
|
|
42
|
+
// ‚
|
|
43
|
+
"“",
|
|
44
|
+
// “
|
|
45
|
+
"”",
|
|
46
|
+
// ”
|
|
47
|
+
"„",
|
|
48
|
+
// „
|
|
49
|
+
"`"
|
|
50
|
+
]), v = /^(?:here(?:'s|’s| is)\s+)?(?:the\s+)?alt(?:[-\s]?text)?\s*[:–—-]\s*/i, N = /^(?:this\s+is\s+)?(?:an?|the)?\s*(?:image|photo|photograph|picture|pic|graphic|illustration|rendering|render)\s+(?:of|showing|depicting|that\s+shows)\s+/i;
|
|
51
|
+
function U(e) {
|
|
52
|
+
const o = /* @__PURE__ */ new Set(), t = [];
|
|
53
|
+
for (const n of e) {
|
|
54
|
+
const r = n?.trim();
|
|
55
|
+
!r || o.has(r) || (o.add(r), t.push(r));
|
|
56
|
+
}
|
|
57
|
+
return t;
|
|
58
|
+
}
|
|
59
|
+
function X(e) {
|
|
60
|
+
const o = e?.trim();
|
|
61
|
+
return o ? p.includes(o) ? !0 : O.some((t) => t.test(o)) : !1;
|
|
62
|
+
}
|
|
63
|
+
function P(e) {
|
|
64
|
+
const o = e.requestedModelId?.trim() || null, t = e.selectedModel?.modelId?.trim() || null, n = e.credentialSource !== "env" && t && X(t) ? t : null;
|
|
65
|
+
return {
|
|
66
|
+
modelIds: U([
|
|
67
|
+
o,
|
|
68
|
+
n,
|
|
69
|
+
...p
|
|
70
|
+
])
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function h(e) {
|
|
74
|
+
let o = e.trim();
|
|
75
|
+
for (; o.length >= 2 && g.has(o[0]) && g.has(o[o.length - 1]); )
|
|
76
|
+
o = o.slice(1, -1).trim();
|
|
77
|
+
return o;
|
|
78
|
+
}
|
|
79
|
+
function q(e) {
|
|
80
|
+
return !e.endsWith(".") || e.endsWith("...") || e.endsWith("…") ? e : (e.match(/[.!?](?=\s|$)/g) || []).length === 1 ? e.slice(0, -1).trimEnd() : e;
|
|
81
|
+
}
|
|
82
|
+
function D(e, o = f) {
|
|
83
|
+
let t = e.replace(/\s+/g, " ").trim(), n = !1;
|
|
84
|
+
for (let r = 0; r < 2; r++) {
|
|
85
|
+
t = h(t);
|
|
86
|
+
const a = t.replace(v, "");
|
|
87
|
+
a !== t && (t = a.trim(), n = !0), t = h(t);
|
|
88
|
+
const i = t.replace(N, "");
|
|
89
|
+
i !== t && (t = i.trim(), n = !0);
|
|
90
|
+
}
|
|
91
|
+
return n && t && (t = t.charAt(0).toUpperCase() + t.slice(1)), C(q(t), o);
|
|
92
|
+
}
|
|
93
|
+
function k(e) {
|
|
94
|
+
const o = e.trim();
|
|
95
|
+
if (!o)
|
|
96
|
+
throw new Error("Cortex AI alt text generation requires an image URL.");
|
|
97
|
+
let t;
|
|
98
|
+
try {
|
|
99
|
+
t = new URL(o);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Cortex AI alt text generation requires an absolute http(s) image URL, received "${o}". A storage key or site-relative path must be resolved to a publicly fetchable URL first, because the model provider downloads the image server-side.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (t.protocol !== "http:" && t.protocol !== "https:")
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Cortex AI alt text generation requires an http(s) image URL, received the "${t.protocol}" scheme.`
|
|
108
|
+
);
|
|
109
|
+
return t;
|
|
110
|
+
}
|
|
111
|
+
function B(e) {
|
|
112
|
+
return [
|
|
113
|
+
"You are NextBlock Cortex AI, writing the alt attribute for an image on a website.",
|
|
114
|
+
"You are writing for a person using a screen reader who cannot see the image at all.",
|
|
115
|
+
"Describe only what is visibly present in the image: subjects, their appearance, what they are doing, the setting, and any text that appears in the image.",
|
|
116
|
+
"Never state or guess anything you cannot see — no names, no locations, no dates, no brands, no emotions, and no backstory unless they are legible in the image itself.",
|
|
117
|
+
`Write one short factual description of at most ${e} characters.`,
|
|
118
|
+
'Do not begin with "image of", "photo of", "picture of", "graphic of", or any similar phrase; the screen reader already announces that this is an image.',
|
|
119
|
+
'Do not add a label such as "Alt text:" and do not wrap the description in quotation marks.',
|
|
120
|
+
"Do not keyword-stuff, do not list search terms, and do not repeat a word for emphasis.",
|
|
121
|
+
"Return plain text only: one line, no markdown, no HTML, no JSON, no commentary, no alternatives to choose between.",
|
|
122
|
+
"If surrounding page context is supplied, use it ONLY to disambiguate what you can already see — for example to choose the right word for an object or to know which product is pictured. Never use it to add detail that is not visible in the image."
|
|
123
|
+
].join(" ");
|
|
124
|
+
}
|
|
125
|
+
function F(e) {
|
|
126
|
+
return [
|
|
127
|
+
`Write the alt attribute for this image, at most ${e.maxLength} characters.`,
|
|
128
|
+
e.context ? `Surrounding page context, for disambiguation only: ${e.context}` : null
|
|
129
|
+
].filter(Boolean).join(`
|
|
130
|
+
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
function V(e) {
|
|
134
|
+
const o = y(e);
|
|
135
|
+
if (o === 401 || o === 402 || o === 403)
|
|
136
|
+
return !1;
|
|
137
|
+
if (R(e) || o && o >= 500)
|
|
138
|
+
return !0;
|
|
139
|
+
const t = e instanceof Error ? e.message : String(e);
|
|
140
|
+
return /NoContentGenerated|No content generated|Provider returned error|empty alt text|image|modality|multimodal|not support|unsupported|aborted|abort|timeout|timed out/i.test(
|
|
141
|
+
t
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
async function H(e) {
|
|
145
|
+
L();
|
|
146
|
+
const o = k(e.imageUrl), t = Number.isFinite(e.maxLength) ? Math.round(e.maxLength) : f, n = Math.min(
|
|
147
|
+
m.max,
|
|
148
|
+
Math.max(m.min, t)
|
|
149
|
+
), r = e.context?.replace(/\s+/g, " ").trim() || null, { createCortexAiOpenRouterClient: a } = await import("./ai-client.es.js"), i = await a({
|
|
150
|
+
apiKey: e.apiKey || void 0
|
|
151
|
+
}), T = P({
|
|
152
|
+
credentialSource: i.credentialSource,
|
|
153
|
+
requestedModelId: e.modelId,
|
|
154
|
+
selectedModel: i.modelSelection
|
|
155
|
+
}), b = [
|
|
156
|
+
{
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
text: F({ context: r, maxLength: n }),
|
|
160
|
+
type: "text"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
// `mediaType` is intentionally omitted. The SDK downloads the image and
|
|
164
|
+
// derives the media type from the response, whereas guessing it from the
|
|
165
|
+
// URL is unreliable for the CDN and signed-URL shapes this CMS produces —
|
|
166
|
+
// query strings, no extension, or an extension that disagrees with what
|
|
167
|
+
// the storage bucket actually serves. A wrong media type is worse than a
|
|
168
|
+
// missing one, because the provider will reject the part outright.
|
|
169
|
+
image: o,
|
|
170
|
+
type: "image"
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
role: "user"
|
|
174
|
+
}
|
|
175
|
+
], l = await w({
|
|
176
|
+
modelIds: T.modelIds,
|
|
177
|
+
shouldRetry: V,
|
|
178
|
+
execute: async (c) => {
|
|
179
|
+
const s = new AbortController(), x = setTimeout(
|
|
180
|
+
() => s.abort(),
|
|
181
|
+
M
|
|
182
|
+
), d = () => s.abort();
|
|
183
|
+
e.abortSignal?.addEventListener("abort", d, { once: !0 }), e.abortSignal?.aborted && s.abort();
|
|
184
|
+
try {
|
|
185
|
+
const A = I(
|
|
186
|
+
{
|
|
187
|
+
abortSignal: s.signal,
|
|
188
|
+
// Alt text is one short line, but several models in the chain emit
|
|
189
|
+
// reasoning tokens against the same budget, so leave headroom rather
|
|
190
|
+
// than truncating the answer to nothing on those.
|
|
191
|
+
maxOutputTokens: 400,
|
|
192
|
+
maxRetries: 0,
|
|
193
|
+
messages: b,
|
|
194
|
+
system: B(n),
|
|
195
|
+
temperature: 0.2
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
modelId: c,
|
|
199
|
+
modelSelection: i.modelSelection
|
|
200
|
+
}
|
|
201
|
+
), E = await _({
|
|
202
|
+
...A,
|
|
203
|
+
model: i.model(c)
|
|
204
|
+
}), u = D(E.text, n);
|
|
205
|
+
if (!u)
|
|
206
|
+
throw new Error("Cortex AI returned empty alt text.");
|
|
207
|
+
return u;
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(x), e.abortSignal?.removeEventListener("abort", d);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
altText: l.result,
|
|
215
|
+
attempts: l.attempts.length,
|
|
216
|
+
credentialSource: i.credentialSource,
|
|
217
|
+
modelId: l.modelId
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export {
|
|
221
|
+
f as CORTEX_AI_DEFAULT_ALT_TEXT_MAX_LENGTH,
|
|
222
|
+
p as CORTEX_AI_VISION_MODEL_FALLBACK_REGISTRY,
|
|
223
|
+
P as buildCortexAiVisionRoutingPolicy,
|
|
224
|
+
H as generateCortexAiAltText,
|
|
225
|
+
X as isKnownVisionCapableCortexAiModelId,
|
|
226
|
+
D as normalizeGeneratedAltText
|
|
227
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextblock-cms/cortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"license": "AGPL-3.0-or-later",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@ai-sdk/openai-compatible": "^2.0.42",
|
|
31
|
-
"@nextblock-cms/db": "^0.
|
|
32
|
-
"@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.
|
|
33
|
-
"@nextblock-cms/utils": "^0.
|
|
31
|
+
"@nextblock-cms/db": "^0.16.2",
|
|
32
|
+
"@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.16.2",
|
|
33
|
+
"@nextblock-cms/utils": "^0.16.2",
|
|
34
34
|
"ai": "^6.0.170",
|
|
35
35
|
"zod": "^4.3.6"
|
|
36
36
|
},
|