@juspay/neurolink 13.0.3 → 13.1.1

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.
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Static vetting for regular expressions that a caller hands to the
3
+ * guardrails (`badWords.regexPatterns`, `precallEvaluation.sanitizationPatterns`).
4
+ *
5
+ * Both lists are compiled and run synchronously on the event loop against the
6
+ * whole model response, so one pattern that backtracks catastrophically
7
+ * (`(a+)+$` on a run of `a`) stalls every request in the process. A timing
8
+ * probe on a short fixed string cannot find that: the cost only shows on long
9
+ * adversarial input, and it is measured after the call has already returned.
10
+ * This reads the pattern instead.
11
+ *
12
+ * The cause of every exponential case is one input that can be split between
13
+ * the parts of the pattern in many ways. A variable-length part (`a+`, `a?`,
14
+ * `a{1,3}`, `(ab)*`) is ambiguous when what can follow it may start with text
15
+ * the part itself can match. The check refuses a pattern when:
16
+ *
17
+ * - such a part sits inside a repeated group (`(a+)+`, `(a?a?)+`,
18
+ * `(a{1,30}){1,30}`), where the ways multiply on every iteration;
19
+ * - the alternatives of a repeated group may overlap (`(a|aa)+`);
20
+ * - ambiguous parts are chained: more than {@link MAX_NARROW_WAYS} ways in
21
+ * total from optional or short parts (`a?` nine times in a row), or more
22
+ * than {@link MAX_OPEN_AMBIGUITY} open-ended ones (`.*a.*b.*c.*d.*e`);
23
+ * - a repetition bound is above {@link MAX_REPETITION_BOUND}, or the pattern
24
+ * is longer than {@link MAX_REGEX_PATTERN_LENGTH}.
25
+ *
26
+ * A variable-length part that is followed by something it cannot match is
27
+ * unambiguous, so `(\d{1,3}\.){3}`, `([a-z0-9-]+\.)+` and `(?:\d[ -]?){13,16}`
28
+ * run.
29
+ *
30
+ * It is deliberately conservative, not a proof. Two classes are compared on a
31
+ * fixed sample of characters (Latin text plus a few other scripts), so a class
32
+ * that only overlaps outside the sample looks disjoint, and a back-reference
33
+ * is assumed to overlap with everything. A hard guarantee would need a time
34
+ * budget around the match itself (a `node:vm` timeout or a worker), which a
35
+ * synchronous `String.replace` cannot offer; that is a follow-up.
36
+ */
37
+ /** Longest pattern accepted, in characters. */
38
+ export const MAX_REGEX_PATTERN_LENGTH = 1000;
39
+ /** Largest number accepted in a `{n}`, `{n,}` or `{n,m}` bound. */
40
+ const MAX_REPETITION_BOUND = 500;
41
+ /**
42
+ * A group repeated up to this many times has a bounded cost even when its
43
+ * alternatives overlap; above it the repetition counts as open-ended.
44
+ */
45
+ const MAX_AMBIGUOUS_REPETITIONS = 8;
46
+ /**
47
+ * A part whose length varies by at most this much (`a?`, `a{1,3}`) multiplies
48
+ * the ways to match by its range plus one. A wider one (`a*`, `a{2,50}`) adds
49
+ * a polynomial degree instead.
50
+ */
51
+ const MAX_NARROW_RANGE = 8;
52
+ /** Most ways the chained narrow ambiguous parts of a pattern may multiply to. */
53
+ const MAX_NARROW_WAYS = 256;
54
+ /** Most open-ended ambiguous parts a pattern may chain (`.*a.*b.*c.*d`). */
55
+ const MAX_OPEN_AMBIGUITY = 4;
56
+ const BRACE_QUANTIFIER = /^\{(\d+)(?:(,)(\d*))?\}/;
57
+ const SHORTHAND_CLASSES = "dDwWsS";
58
+ const ASSERTION_ESCAPES = "bB";
59
+ const REASON_LOOP = "a repeated group has a variable-length part that can match the same text as what follows it, so the input can be split many ways (such as (a+)+ or (a?a?)+)";
60
+ const REASON_ALTERNATIVES = "a repeated group has alternatives that may overlap (such as (a|aa)+)";
61
+ const REASON_NARROW_CHAIN = `a chain of optional or short variable-length parts that may overlap can be matched in more than ${MAX_NARROW_WAYS} ways (such as a?a?a?a?a?a?a?a?a?)`;
62
+ const REASON_OPEN_CHAIN = `more than ${MAX_OPEN_AMBIGUITY} open-ended repeats may match the same text (such as .*a.*b.*c.*d.*e)`;
63
+ const REASON_BOUND = `a repetition bound is above ${MAX_REPETITION_BOUND}`;
64
+ const parsePattern = (pattern) => {
65
+ let pos = 0;
66
+ /** Skips what follows `(` and reports whether the group is a lookaround. */
67
+ const skipGroupPrefix = () => {
68
+ if (pattern[pos] !== "?") {
69
+ return false;
70
+ }
71
+ const lookbehind = /^\?<[=!]/.test(pattern.slice(pos, pos + 3));
72
+ if (lookbehind) {
73
+ pos += 3;
74
+ return true;
75
+ }
76
+ if (pattern[pos + 1] === "<") {
77
+ const close = pattern.indexOf(">", pos);
78
+ pos = close === -1 ? pattern.length : close + 1;
79
+ return false;
80
+ }
81
+ const lookahead = pattern[pos + 1] === "=" || pattern[pos + 1] === "!";
82
+ pos += 2;
83
+ return lookahead;
84
+ };
85
+ const readClass = () => {
86
+ const start = pos - 1;
87
+ while (pos < pattern.length && pattern[pos] !== "]") {
88
+ pos += pattern[pos] === "\\" ? 2 : 1;
89
+ }
90
+ pos = Math.min(pos + 1, pattern.length);
91
+ return { kind: "class", source: pattern.slice(start, pos) };
92
+ };
93
+ /** Reads a fixed number of hex digits after `\x` or `\u`, if they are there. */
94
+ const readHexEscape = (digits) => {
95
+ const hex = pattern.slice(pos, pos + digits);
96
+ if (hex.length !== digits || !/^[0-9a-fA-F]+$/.test(hex)) {
97
+ return undefined;
98
+ }
99
+ pos += digits;
100
+ return String.fromCharCode(Number.parseInt(hex, 16));
101
+ };
102
+ const readEscape = () => {
103
+ const escaped = pattern[pos];
104
+ if (escaped === undefined) {
105
+ return { kind: "literal", char: "\\" };
106
+ }
107
+ pos += 1;
108
+ if (ASSERTION_ESCAPES.includes(escaped)) {
109
+ return { kind: "assertion" };
110
+ }
111
+ // `\x61` and `a` are the letter `a`: reading them as a class named
112
+ // `x` or `u` would hide `(\x61+)+` from every check below.
113
+ const hexChar = escaped === "x"
114
+ ? readHexEscape(2)
115
+ : escaped === "u"
116
+ ? readHexEscape(4)
117
+ : undefined;
118
+ if (hexChar !== undefined) {
119
+ return { kind: "literal", char: hexChar.toLowerCase() };
120
+ }
121
+ if (escaped === "c" && /[A-Za-z]/.test(pattern[pos] ?? "")) {
122
+ const control = String.fromCharCode(pattern.charCodeAt(pos) % 32);
123
+ pos += 1;
124
+ return { kind: "literal", char: control };
125
+ }
126
+ if (escaped === "0") {
127
+ return { kind: "literal", char: "\0" };
128
+ }
129
+ if (SHORTHAND_CLASSES.includes(escaped) || /[1-9A-Za-z]/.test(escaped)) {
130
+ return { kind: "class", source: `\\${escaped}` };
131
+ }
132
+ return { kind: "literal", char: escaped.toLowerCase() };
133
+ };
134
+ const readQuantifier = () => {
135
+ const ch = pattern[pos];
136
+ let quantifier;
137
+ if (ch === "*") {
138
+ quantifier = { min: 0, max: Infinity };
139
+ pos += 1;
140
+ }
141
+ else if (ch === "+") {
142
+ quantifier = { min: 1, max: Infinity };
143
+ pos += 1;
144
+ }
145
+ else if (ch === "?") {
146
+ quantifier = { min: 0, max: 1 };
147
+ pos += 1;
148
+ }
149
+ else if (ch === "{") {
150
+ const braces = BRACE_QUANTIFIER.exec(pattern.slice(pos));
151
+ if (braces) {
152
+ const min = Number(braces[1]);
153
+ let max = min;
154
+ if (braces[2] !== undefined) {
155
+ max = braces[3] === "" ? Infinity : Number(braces[3]);
156
+ }
157
+ quantifier = { min, max };
158
+ pos += braces[0].length;
159
+ }
160
+ }
161
+ if (quantifier && pattern[pos] === "?") {
162
+ pos += 1;
163
+ }
164
+ return quantifier;
165
+ };
166
+ const parseAlternatives = (depth) => {
167
+ const alternatives = [];
168
+ let sequence = [];
169
+ while (pos < pattern.length) {
170
+ const ch = pattern[pos];
171
+ if (ch === ")" && depth > 0) {
172
+ break;
173
+ }
174
+ if (ch === "|") {
175
+ pos += 1;
176
+ alternatives.push(sequence);
177
+ sequence = [];
178
+ continue;
179
+ }
180
+ const quantifier = sequence.length > 0 ? readQuantifier() : undefined;
181
+ if (quantifier) {
182
+ const child = sequence[sequence.length - 1];
183
+ sequence = [
184
+ ...sequence.slice(0, -1),
185
+ { kind: "repeat", child, ...quantifier },
186
+ ];
187
+ continue;
188
+ }
189
+ pos += 1;
190
+ if (ch === "(") {
191
+ const lookaround = skipGroupPrefix();
192
+ const groupAlternatives = parseAlternatives(depth + 1);
193
+ if (pattern[pos] === ")") {
194
+ pos += 1;
195
+ }
196
+ sequence.push({
197
+ kind: "group",
198
+ alternatives: groupAlternatives,
199
+ ...(lookaround ? { lookaround } : {}),
200
+ });
201
+ }
202
+ else if (ch === "[") {
203
+ sequence.push(readClass());
204
+ }
205
+ else if (ch === "\\") {
206
+ sequence.push(readEscape());
207
+ }
208
+ else if (ch === ".") {
209
+ sequence.push({ kind: "class", source: "." });
210
+ }
211
+ else if (ch === "^" || ch === "$") {
212
+ sequence.push({ kind: "assertion" });
213
+ }
214
+ else {
215
+ sequence.push({ kind: "literal", char: ch.toLowerCase() });
216
+ }
217
+ }
218
+ alternatives.push(sequence);
219
+ return alternatives;
220
+ };
221
+ return parseAlternatives(0);
222
+ };
223
+ // ---------------------------------------------------------------------------
224
+ // Character sets
225
+ //
226
+ // Two classes are compared on a fixed sample of characters rather than by
227
+ // parsing them: the engine itself decides what `[\w-]` or `\S` matches. A set
228
+ // is a bitmap over the sample, so "can these two match the same character" is
229
+ // a word-wise AND.
230
+ // ---------------------------------------------------------------------------
231
+ const EXTRA_SAMPLE_CODES = [
232
+ 0x3b1, 0x410, 0x5d0, 0x627, 0x4e2d, 0xac00, 0x2028, 0x2029, 0xfeff, 0xffff,
233
+ ];
234
+ const SAMPLE_CHARS = [
235
+ ...Array.from({ length: 0x300 }, (_, code) => String.fromCharCode(code)),
236
+ ...EXTRA_SAMPLE_CODES.map((code) => String.fromCharCode(code)),
237
+ ];
238
+ const SET_WORDS = Math.ceil(SAMPLE_CHARS.length / 32);
239
+ const EMPTY_SET = new Uint32Array(SET_WORDS);
240
+ const FULL_SET = new Uint32Array(SET_WORDS).fill(0xffffffff);
241
+ const setOf = (matches) => {
242
+ const set = new Uint32Array(SET_WORDS);
243
+ SAMPLE_CHARS.forEach((char, index) => {
244
+ if (matches(char)) {
245
+ set[index >>> 5] |= 1 << (index & 31);
246
+ }
247
+ });
248
+ return set;
249
+ };
250
+ const unionOf = (left, right) => left.map((word, index) => word | right[index]);
251
+ const overlaps = (left, right) => left.some((word, index) => (word & right[index]) !== 0);
252
+ /**
253
+ * The characters a class matches. A back-reference or a class that does not
254
+ * compile cannot be compared, so it is assumed to match everything.
255
+ */
256
+ const classSetOf = (source) => {
257
+ if (/^\\[1-9k]/.test(source)) {
258
+ return FULL_SET;
259
+ }
260
+ try {
261
+ const matcher = new RegExp(`^(?:${source})$`, "i");
262
+ return setOf((char) => matcher.test(char));
263
+ }
264
+ catch {
265
+ return FULL_SET;
266
+ }
267
+ };
268
+ // ---------------------------------------------------------------------------
269
+ // Ambiguity analysis
270
+ // ---------------------------------------------------------------------------
271
+ const NO_COST = { ways: 1, degree: 0 };
272
+ const TOP_LEVEL = { inLoop: false, unbounded: false };
273
+ const chain = (left, right) => ({
274
+ ways: left.ways * right.ways,
275
+ degree: left.degree + right.degree,
276
+ });
277
+ const worse = (left, right) => ({
278
+ ways: Math.max(left.ways, right.ways),
279
+ degree: Math.max(left.degree, right.degree),
280
+ });
281
+ const literalWord = (sequence) => {
282
+ const chars = sequence.map((node) => node.kind === "literal" ? node.char : undefined);
283
+ return chars.every((char) => char !== undefined) ? chars.join("") : undefined;
284
+ };
285
+ /** True when no word is a prefix of another, so the words decode uniquely. */
286
+ const isPrefixFree = (words) => {
287
+ const sorted = [...words].sort();
288
+ return sorted.every((word, index) => index === 0 || !word.startsWith(sorted[index - 1]));
289
+ };
290
+ const analyze = (alternatives) => {
291
+ const shapes = new WeakMap();
292
+ const shapeOfSequence = (sequence) => {
293
+ let first = EMPTY_SET;
294
+ let nullable = true;
295
+ for (const node of sequence) {
296
+ if (!nullable) {
297
+ break;
298
+ }
299
+ const shape = shapeOf(node);
300
+ first = unionOf(first, shape.first);
301
+ nullable = shape.nullable;
302
+ }
303
+ return { first, nullable };
304
+ };
305
+ const shapeOf = (node) => {
306
+ const known = shapes.get(node);
307
+ if (known) {
308
+ return known;
309
+ }
310
+ let shape;
311
+ switch (node.kind) {
312
+ case "literal":
313
+ shape = {
314
+ first: setOf((char) => char.toLowerCase() === node.char),
315
+ nullable: false,
316
+ };
317
+ break;
318
+ case "class":
319
+ shape = { first: classSetOf(node.source), nullable: false };
320
+ break;
321
+ case "assertion":
322
+ shape = { first: EMPTY_SET, nullable: true };
323
+ break;
324
+ case "group": {
325
+ if (node.lookaround) {
326
+ shape = { first: EMPTY_SET, nullable: true };
327
+ break;
328
+ }
329
+ const parts = node.alternatives.map(shapeOfSequence);
330
+ shape = {
331
+ first: parts.reduce((all, part) => unionOf(all, part.first), EMPTY_SET),
332
+ nullable: parts.some((part) => part.nullable),
333
+ };
334
+ break;
335
+ }
336
+ case "repeat": {
337
+ const child = shapeOf(node.child);
338
+ shape = {
339
+ first: child.first,
340
+ nullable: node.min === 0 || child.nullable,
341
+ };
342
+ break;
343
+ }
344
+ }
345
+ shapes.set(node, shape);
346
+ return shape;
347
+ };
348
+ /** True when two alternatives of a group could match the same text. */
349
+ const alternativesOverlap = (group) => {
350
+ const words = group.map(literalWord);
351
+ if (words.every((word) => word !== undefined)) {
352
+ return !isPrefixFree(words);
353
+ }
354
+ const parts = group.map(shapeOfSequence);
355
+ return parts.some((part, index) => part.nullable ||
356
+ parts
357
+ .slice(index + 1)
358
+ .some((other) => overlaps(part.first, other.first)));
359
+ };
360
+ const analyzeSequence = (sequence, after, context) => {
361
+ let total = NO_COST;
362
+ let follow = after;
363
+ for (let index = sequence.length - 1; index >= 0; index -= 1) {
364
+ const node = sequence[index];
365
+ const outcome = analyzeNode(node, follow, context);
366
+ if (typeof outcome === "string") {
367
+ return outcome;
368
+ }
369
+ total = chain(total, outcome);
370
+ const shape = shapeOf(node);
371
+ follow = shape.nullable ? unionOf(shape.first, follow) : shape.first;
372
+ }
373
+ return total;
374
+ };
375
+ const analyzeAlternatives = (group, after, context) => {
376
+ let worst = NO_COST;
377
+ for (const sequence of group) {
378
+ const outcome = analyzeSequence(sequence, after, context);
379
+ if (typeof outcome === "string") {
380
+ return outcome;
381
+ }
382
+ worst = worse(worst, outcome);
383
+ }
384
+ return worst;
385
+ };
386
+ const analyzeNode = (node, after, context) => {
387
+ if (node.kind === "group") {
388
+ if (node.lookaround) {
389
+ return analyzeAlternatives(node.alternatives, EMPTY_SET, TOP_LEVEL);
390
+ }
391
+ const overlapping = node.alternatives.length > 1 && alternativesOverlap(node.alternatives);
392
+ if (overlapping && context.unbounded) {
393
+ return REASON_ALTERNATIVES;
394
+ }
395
+ const inner = analyzeAlternatives(node.alternatives, after, context);
396
+ if (typeof inner === "string") {
397
+ return inner;
398
+ }
399
+ return overlapping ? chain(inner, { ways: 2, degree: 0 }) : inner;
400
+ }
401
+ if (node.kind !== "repeat") {
402
+ return NO_COST;
403
+ }
404
+ const finiteMax = Number.isFinite(node.max) ? node.max : 0;
405
+ if (node.min > MAX_REPETITION_BOUND || finiteMax > MAX_REPETITION_BOUND) {
406
+ return REASON_BOUND;
407
+ }
408
+ const child = shapeOf(node.child);
409
+ const variable = node.min < node.max;
410
+ const ambiguous = variable && overlaps(child.first, after);
411
+ if (ambiguous && context.inLoop) {
412
+ return REASON_LOOP;
413
+ }
414
+ const repeatsGroup = node.max > 1 &&
415
+ node.child.kind !== "literal" &&
416
+ node.child.kind !== "class";
417
+ let inner;
418
+ if (repeatsGroup) {
419
+ // The next iteration starts where this one ends, so its first
420
+ // characters follow every part of the body.
421
+ inner = analyzeNode(node.child, unionOf(child.first, after), {
422
+ inLoop: true,
423
+ unbounded: context.unbounded || node.max > MAX_AMBIGUOUS_REPETITIONS,
424
+ });
425
+ }
426
+ else {
427
+ inner = analyzeNode(node.child, after, context);
428
+ }
429
+ if (typeof inner === "string") {
430
+ return inner;
431
+ }
432
+ const looped = repeatsGroup
433
+ ? {
434
+ ways: inner.ways ** (Number.isFinite(node.max) ? node.max : 1),
435
+ degree: inner.degree,
436
+ }
437
+ : inner;
438
+ if (!ambiguous) {
439
+ return looped;
440
+ }
441
+ const range = node.max - node.min;
442
+ return chain(looped, range <= MAX_NARROW_RANGE
443
+ ? { ways: range + 1, degree: 0 }
444
+ : { ways: 1, degree: 1 });
445
+ };
446
+ const outcome = analyzeAlternatives(alternatives, EMPTY_SET, TOP_LEVEL);
447
+ if (typeof outcome === "string") {
448
+ return outcome;
449
+ }
450
+ if (outcome.ways > MAX_NARROW_WAYS) {
451
+ return REASON_NARROW_CHAIN;
452
+ }
453
+ if (outcome.degree > MAX_OPEN_AMBIGUITY) {
454
+ return REASON_OPEN_CHAIN;
455
+ }
456
+ return outcome;
457
+ };
458
+ /**
459
+ * Decide whether a caller-supplied pattern may be compiled and run.
460
+ *
461
+ * Total: it returns a verdict for any string and never throws, so a caller can
462
+ * skip a refused pattern the same way it skips an invalid one.
463
+ */
464
+ export function vetRegexPattern(pattern) {
465
+ if (pattern.length > MAX_REGEX_PATTERN_LENGTH) {
466
+ return {
467
+ safe: false,
468
+ reason: `the pattern is ${pattern.length} characters long and the limit is ${MAX_REGEX_PATTERN_LENGTH}`,
469
+ };
470
+ }
471
+ try {
472
+ const outcome = analyze(parsePattern(pattern));
473
+ return typeof outcome === "string"
474
+ ? { safe: false, reason: outcome }
475
+ : { safe: true };
476
+ }
477
+ catch {
478
+ return {
479
+ safe: false,
480
+ reason: "the pattern is too deeply nested to analyse",
481
+ };
482
+ }
483
+ }
@@ -1845,6 +1845,10 @@ export declare class NeuroLink {
1845
1845
  /**
1846
1846
  * Get comprehensive status of all AI providers
1847
1847
  * Primary method for provider health checking and diagnostics
1848
+ *
1849
+ * Bedrock and SageMaker are never called live: their entries come back as
1850
+ * `status: "not-configured"` with `configured: true` and the reason in
1851
+ * `error`. Use `testProvider()` to check either one explicitly.
1848
1852
  */
1849
1853
  getProviderStatus(options?: {
1850
1854
  quiet?: boolean;
package/dist/neurolink.js CHANGED
@@ -11870,6 +11870,10 @@ Current user's request: ${currentInput}`;
11870
11870
  /**
11871
11871
  * Get comprehensive status of all AI providers
11872
11872
  * Primary method for provider health checking and diagnostics
11873
+ *
11874
+ * Bedrock and SageMaker are never called live: their entries come back as
11875
+ * `status: "not-configured"` with `configured: true` and the reason in
11876
+ * `error`. Use `testProvider()` to check either one explicitly.
11873
11877
  */
11874
11878
  async getProviderStatus(options) {
11875
11879
  // Track memory and timing for provider status checks
@@ -11912,6 +11916,19 @@ Current user's request: ${currentInput}`;
11912
11916
  responseTime: Date.now() - startTime,
11913
11917
  };
11914
11918
  }
11919
+ // These probes are billable generate() calls signed with ambient AWS
11920
+ // credentials, against models the account may not be allowed to
11921
+ // call, so they are never made here.
11922
+ if (providerName === "bedrock" || providerName === "sagemaker") {
11923
+ return {
11924
+ provider: providerName,
11925
+ status: "not-configured",
11926
+ configured: true,
11927
+ authenticated: false,
11928
+ error: `Live status check is skipped for ${providerName}`,
11929
+ responseTime: Date.now() - startTime,
11930
+ };
11931
+ }
11915
11932
  // Special handling for Ollama
11916
11933
  if (providerName === "ollama") {
11917
11934
  let ollamaAttempt;
@@ -122,7 +122,7 @@
122
122
  },
123
123
  "baseURLShape": {
124
124
  "enum": ["origin"],
125
- "description": "Refuses a normalised base URL that is not an http(s) URL without userinfo, a query string or a fragment. Omitted means no check."
125
+ "description": "Refuses a normalised base URL that is not an https URL (http is allowed only for localhost, 127.0.0.1 and [::1]) without userinfo, a query string or a fragment. Omitted means no check."
126
126
  },
127
127
  "endpoint": {
128
128
  "type": "string",
@@ -21,7 +21,7 @@
21
21
  "vision": true,
22
22
  "tools": true,
23
23
  "status": "production",
24
- "description": "NeuroLink default. Listed as reka-flash in the baseline model table on https://docs.reka.ai/chat/models.md (intro: \"Our baseline models always available for public access are:\") and named as the model in the code examples on https://docs.reka.ai/chat/overview.md, https://docs.reka.ai/chat/function-calling.md and https://docs.reka.ai/chat/api-reference/create.md (retrieved 2026-09-29). https://docs.reka.ai/pricing.md has a row named \"Reka Flash\" described as \"Fast and cost-efficient model for most tasks\" with $0.80 under \"Input Tokens (per 1M)\" and $2.00 under \"Output Tokens (per 1M)\"; https://docs.reka.ai/chat/models.md ends with \"For full listing of models and pricing, see the Reka API Pricing page\". https://docs.reka.ai/chat/function-calling.md says \"Currently, only Reka Flash supports function calling.\" Vision is true: the first curl example on https://docs.reka.ai/overview.md sends an image_url content part to model \"reka-flash\", and the Quickstart's \"First request\" example (https://docs.reka.ai/quickstart.md) carries the comment '# or \"reka-flash\"' beside an image_url request. No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
24
+ "description": "NeuroLink default. Listed as reka-flash in the baseline model table on https://docs.reka.ai/chat/models.md (intro: \"Our baseline models always available for public access are:\") and named as the model in the code examples on https://docs.reka.ai/chat/overview.md, https://docs.reka.ai/chat/function-calling.md and https://docs.reka.ai/chat/api-reference/create.md (retrieved 2026-09-29). https://docs.reka.ai/pricing.md has a row named \"Reka Flash\" described as \"Fast and cost-efficient model for most tasks\" with $0.80 under \"Input Tokens (per 1M)\" and $2.00 under \"Output Tokens (per 1M)\"; https://docs.reka.ai/chat/models.md ends with \"For full listing of models and pricing, see the Reka API Pricing page\". tools is true because https://docs.reka.ai/chat/function-calling.md said \"Currently, only Reka Flash supports function calling.\" on 2026-09-29. That sentence is gone: on 2026-10-17 https://docs.reka.ai/chat/function-calling.md redirects to https://docs.reka.ai/chat/overview.md, and neither it nor the Models table at https://docs.reka.ai/chat/models.md uses the id reka-flash (they name reka-flash-3), so this flag and this entry are not confirmed against the current docs and are kept as they were pending a check with a key. Vision is true: the first curl example on https://docs.reka.ai/overview.md sends an image_url content part to model \"reka-flash\", and the Quickstart's \"First request\" example (https://docs.reka.ai/quickstart.md) carries the comment '# or \"reka-flash\"' beside an image_url request. No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
25
25
  },
26
26
  "reka-edge": {
27
27
  "pricingPerMTok": {
@@ -29,9 +29,9 @@
29
29
  "output": 0.1
30
30
  },
31
31
  "vision": true,
32
- "tools": false,
32
+ "tools": true,
33
33
  "status": "production",
34
- "description": "Listed as \"reka-edge (or reka-edge-2603)\" in the baseline model table on https://docs.reka.ai/chat/models.md and on https://docs.reka.ai/quickstart.md (retrieved 2026-09-29). The Quickstart's \"First request\" example sends an image_url content part to model \"reka-edge\", and the example response on https://docs.reka.ai/chat/api-reference/get.md lists the id \"reka-edge\". https://docs.reka.ai/pricing.md has a row named \"Reka Edge\" described as \"Compact model ideal for on-device execution\" with $0.10 under \"Input Tokens (per 1M)\" and $0.10 under \"Output Tokens (per 1M)\". https://docs.reka.ai/chat/function-calling.md says \"Currently, only Reka Flash supports function calling.\" No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
34
+ "description": "Listed as \"reka-edge (or reka-edge-2603)\" in the baseline model table on https://docs.reka.ai/chat/models.md and on https://docs.reka.ai/quickstart.md (retrieved 2026-09-29). The Quickstart's \"First request\" example sends an image_url content part to model \"reka-edge\", and the example response on https://docs.reka.ai/chat/api-reference/get.md lists the id \"reka-edge\". https://docs.reka.ai/pricing.md has a row named \"Reka Edge\" described as \"Compact model ideal for on-device execution\" with $0.10 under \"Input Tokens (per 1M)\" and $0.10 under \"Output Tokens (per 1M)\". tools is true because reka-edge is the alias of reka-edge-2603, which the current docs mark as tool-capable (see the reka-edge-2603 entry); it was false while https://docs.reka.ai/chat/function-calling.md said \"Currently, only Reka Flash supports function calling.\" (retrieved 2026-09-29), a sentence that no longer appears. The Models table at https://docs.reka.ai/chat/models.md (retrieved 2026-10-17) has no row named reka-edge, only reka-edge-2603, so whether the API still serves the bare alias is not confirmed. No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
35
35
  },
36
36
  "reka-edge-2603": {
37
37
  "pricingPerMTok": {
@@ -39,9 +39,19 @@
39
39
  "output": 0.1
40
40
  },
41
41
  "vision": true,
42
+ "tools": true,
43
+ "status": "production",
44
+ "description": "Tools are supported per Reka's current docs (retrieved 2026-10-17). The Models table at https://docs.reka.ai/chat/models.md has the header | Model ID | Context | Input | Tools | Structured outputs | Reasoning | and the row | `reka-edge-2603` | 16k | Text, image, video | Yes | Yes | No |, so the Tools column is Yes. The GET /v1/models example on the same page shows \"supported_features\": [\"tools\", \"structured_outputs\", \"streaming\"] for reka-edge-2603, and the page says \"`GET /v1/models` is the authoritative list\". The Tools section of https://docs.reka.ai/chat/overview.md (https://docs.reka.ai/chat/function-calling.md redirects there) says \"Models that list `tools` in their `supported_features` accept OpenAI-format `tools` and `tool_choice`.\" and its example request calls model=\"reka-edge-2603\" with a tools list. This replaces tools: false, which rested on the sentence \"Currently, only Reka Flash supports function calling.\" on https://docs.reka.ai/chat/function-calling.md (retrieved 2026-09-29); that sentence no longer appears. Not wire-verified: no API key was used. Vision is true because the Models table lists \"Text, image, video\" as the input. Prices are those of the \"Reka Edge\" row of https://docs.reka.ai/pricing.md (retrieved 2026-09-29) and match the GET /v1/models example's \"prompt\": \"0.0000001\" and \"completion\": \"0.0000001\" US dollars per token. Earlier evidence (retrieved 2026-09-29): named in the same table row as reka-edge, \"reka-edge (or reka-edge-2603)\", on https://docs.reka.ai/chat/models.md and https://docs.reka.ai/quickstart.md. No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
45
+ },
46
+ "reka-flash-3": {
47
+ "pricingPerMTok": {
48
+ "input": 0.1,
49
+ "output": 0.2
50
+ },
51
+ "vision": false,
42
52
  "tools": false,
43
53
  "status": "production",
44
- "description": "Named in the same table row as reka-edge, \"reka-edge (or reka-edge-2603)\", on https://docs.reka.ai/chat/models.md and https://docs.reka.ai/quickstart.md, and shown as {\"id\": \"reka-edge-2603\"} in the example output of GET https://api.reka.ai/v1/models on https://docs.reka.ai/chat/models.md, which says \"All of these model IDs can be used as values for the Chat API's model parameter\" (retrieved 2026-09-29). Vision and prices are those of the row's reka-edge example and its \"Reka Edge\" pricing row (https://docs.reka.ai/quickstart.md, https://docs.reka.ai/pricing.md). No context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply."
54
+ "description": "Listed in the Models table at https://docs.reka.ai/chat/models.md (retrieved 2026-10-17) with the row | `reka-flash-3` | 64k | Text | No | Yes | Yes |, so input is text only (vision is false) and the Tools column is No (tools is false), the entry that keeps the per-model withhold path covered. Prices are the \"$0.10/M input tokens\" and \"$0.20/M output tokens\" shown on its card at https://developer.reka.ai/models (retrieved 2026-10-17), which the Models page names as the source of current prices. The Models page says 64k for the context, a rounded figure, so no context window or maximum output figure is set for this model; models.defaultContextWindow and models.defaultMaxOutputTokens apply. This is a different model from the reka-flash entry above, whose id no longer appears in the current Models table."
45
55
  }
46
56
  },
47
57
  "topModels": ["reka-flash", "reka-edge", "reka-edge-2603"]
@@ -91,7 +101,7 @@
91
101
  "3. Authentication: the vendor's REST examples send the header X-Api-Key (https://docs.reka.ai/chat/overview.md) and the OpenAPI file https://docs.reka.ai/openapi/api-reference.yaml declares one security scheme, an apiKey in the header X-Api-Key; the OpenAI Python SDK examples pass api_key=\"YOUR_API_KEY\" to the client (https://docs.reka.ai/quickstart.md)",
92
102
  "4. Set {apiKeyEnvVar} in your .env file"
93
103
  ],
94
- "description": "Docs-verified only: the vendor's GET https://api.reka.ai/v1/models needs a key (it answers HTTP 401 without one), so the three model ids come from https://docs.reka.ai/chat/models.md and the roster has not been checked; the entry is not yet live-verified. Endpoint https://api.reka.ai/v1: the Quickstart (https://docs.reka.ai/quickstart.md) says \"our API is fully OpenAI-compatible\" and shows the OpenAI Python SDK with base_url \"https://api.reka.ai/v1\"; https://docs.reka.ai/chat/overview.md shows POST https://api.reka.ai/v1/chat/completions with a messages array and reads the reply from response.choices[0].message.content. Unauthenticated GETs made 2026-09-29: /models answered HTTP 401 with the JSON body {\"error\":{\"message\":\"Unauthorized\",\"type\":\"authentication_error\",\"code\":null,\"param\":null}}; /chat/completions answered HTTP 405 with header allow: POST and the body {\"detail\":\"Method Not Allowed\"}. No POST request was sent. errorRules carry the statuses https://docs.reka.ai/errors.md lists (401 authentication_error, 429 rate_limit_error, 500 server_error) plus a pattern rule on the FAQ's \"P001: Insufficient Balance\" text. capabilities.streaming is true because https://docs.reka.ai/chat/overview.md says \"The Chat API supports streaming\". capabilities.tools is model-dependent because https://docs.reka.ai/chat/function-calling.md documents the tools parameter and tool_calls and says \"Currently, only Reka Flash supports function calling.\" That sentence is why reka-flash's models.catalog entry sets tools: true and reka-edge/reka-edge-2603 set tools: false (see CatalogModelSpec.tools) instead of leaving every model to the provider-level model-dependent default. capabilities.structuredOutput is false: the ChatRequest on https://docs.reka.ai/chat/api-reference/create.md lists messages, model, frequency_penalty, max_tokens, presence_penalty, seed, stop, stream, temperature, tool_choice, tools, top_k and top_p. capabilities.toolsWithStreaming and capabilities.structuredOutputWithTools are false because no request combining them was sent. capabilities.embeddings and capabilities.thinking are false. models.defaultContextWindow (16384) and models.defaultMaxOutputTokens (4096) are placeholders the vendor does not publish; the Chat API reference describes max_tokens as \"limited by the model's context length. Defaults to 1024.\" Each model's status field holds production, a placeholder for a value the catalog schema requires; it is not a vendor statement. billingPolicy holds free-tier, one of the three values the catalog schema allows (free-tier, free-with-card, no-free-tier). The vendor wording behind it: on https://platform.reka.ai/pricing the Pay as you go tile has a \"Start for free\" link to /sign-up and the page carries the text \"Free credits used\"; https://docs.reka.ai/pricing.md says \"Get started with no upfront costs.\"; https://docs.reka.ai/resources/faqs.md answers \"Do I need to pre-pay?\" with \"Yes. Add credits to your account before making requests.\" apiKeyFormat is null, so NeuroLink's key-format check is not applied."
104
+ "description": "Docs-verified only: the vendor's GET https://api.reka.ai/v1/models needs a key (it answers HTTP 401 without one), so the four model ids come from https://docs.reka.ai/chat/models.md (retrieved 2026-09-29 for the first three and 2026-10-17 for reka-flash-3) and the roster has not been checked; the entry is not yet live-verified. Endpoint https://api.reka.ai/v1: the Quickstart (https://docs.reka.ai/quickstart.md) says \"our API is fully OpenAI-compatible\" and shows the OpenAI Python SDK with base_url \"https://api.reka.ai/v1\"; https://docs.reka.ai/chat/overview.md shows POST https://api.reka.ai/v1/chat/completions with a messages array and reads the reply from response.choices[0].message.content. Unauthenticated GETs made 2026-09-29: /models answered HTTP 401 with the JSON body {\"error\":{\"message\":\"Unauthorized\",\"type\":\"authentication_error\",\"code\":null,\"param\":null}}; /chat/completions answered HTTP 405 with header allow: POST and the body {\"detail\":\"Method Not Allowed\"}. No POST request was sent. errorRules carry the statuses https://docs.reka.ai/errors.md lists (401 authentication_error, 429 rate_limit_error, 500 server_error) plus a pattern rule on the FAQ's \"P001: Insufficient Balance\" text. capabilities.streaming is true because https://docs.reka.ai/chat/overview.md says \"The Chat API supports streaming\". capabilities.tools is model-dependent because tool support differs per model: the Models table at https://docs.reka.ai/chat/models.md (retrieved 2026-10-17) has a \"Tools\" column that is Yes for reka-edge-2603 and No for reka-flash-3, and the Tools section of https://docs.reka.ai/chat/overview.md says \"Models that list `tools` in their `supported_features` accept OpenAI-format `tools` and `tool_choice`.\" So the models.catalog entries set tools: true for reka-edge-2603 and its alias reka-edge, and tools: false for reka-flash-3 (see CatalogModelSpec.tools), instead of leaving every model to the provider-level model-dependent default. reka-flash keeps tools: true from the sentence \"Currently, only Reka Flash supports function calling.\" (retrieved 2026-09-29), which the current pages no longer carry; that entry is unconfirmed. capabilities.structuredOutput is false: the ChatRequest on https://docs.reka.ai/chat/api-reference/create.md lists messages, model, frequency_penalty, max_tokens, presence_penalty, seed, stop, stream, temperature, tool_choice, tools, top_k and top_p. capabilities.toolsWithStreaming and capabilities.structuredOutputWithTools are false because no request combining them was sent. capabilities.embeddings and capabilities.thinking are false. models.defaultContextWindow (16384) and models.defaultMaxOutputTokens (4096) are placeholders the vendor does not publish; the Chat API reference describes max_tokens as \"limited by the model's context length. Defaults to 1024.\" Each model's status field holds production, a placeholder for a value the catalog schema requires; it is not a vendor statement. billingPolicy holds free-tier, one of the three values the catalog schema allows (free-tier, free-with-card, no-free-tier). The vendor wording behind it: on https://platform.reka.ai/pricing the Pay as you go tile has a \"Start for free\" link to /sign-up and the page carries the text \"Free credits used\"; https://docs.reka.ai/pricing.md says \"Get started with no upfront costs.\"; https://docs.reka.ai/resources/faqs.md answers \"Do I need to pre-pay?\" with \"Yes. Add credits to your account before making requests.\" apiKeyFormat is null, so NeuroLink's key-format check is not applied."
95
105
  },
96
106
  "evidence": {
97
107
  "rosterVerified": {
@@ -2,6 +2,7 @@ import { CloudflareClefModels } from "../constants/enums.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { redactUrlForError } from "../utils/logSanitize.js";
4
4
  import { getProviderModel } from "../utils/providerConfig.js";
5
+ import { cleartextBaseURLProblem } from "./decide/baseUrl.js";
5
6
  import { isRecord, redactCredentials, SystemOneDecisionProvider, } from "./systemOneDecision.js";
6
7
  const CLOUDFLARE_DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
7
8
  const MODEL_PREFIX = "@cf/cloudflare/";
@@ -61,6 +62,10 @@ function baseURLProblem(baseURL) {
61
62
  if (url.protocol !== "https:" && url.protocol !== "http:") {
62
63
  return `The Cloudflare base URL must start with https:// or http://. ${fix}`;
63
64
  }
65
+ const cleartext = cleartextBaseURLProblem(url, "Cloudflare", fix);
66
+ if (cleartext) {
67
+ return cleartext;
68
+ }
64
69
  // `url.search` and `url.hash` are empty for a bare trailing `?` or `#`, which
65
70
  // would still put the route into the query or the fragment, so the text is
66
71
  // checked as well.
@@ -1,4 +1,12 @@
1
1
  import type { DecideBaseURLShape, DecideBaseURLTransform } from "../../types/index.js";
2
2
  export declare function normalizeBaseURL(raw: string, transforms: readonly DecideBaseURLTransform[]): string;
3
+ /** Whether a `URL.hostname` is this machine, so a plain-http request to it never leaves it. */
4
+ export declare function isLoopbackHost(hostname: string): boolean;
5
+ /**
6
+ * Why a parsed base URL may not be used, or undefined: a plain-http URL that
7
+ * is not loopback would send the bearer key and the decision body in cleartext.
8
+ * The message names no part of the URL.
9
+ */
10
+ export declare function cleartextBaseURLProblem(url: URL, displayName: string, fix: string): string | undefined;
3
11
  /** Why a normalised base URL fails the shape, or undefined when it passes. */
4
12
  export declare function baseURLShapeProblem(shape: DecideBaseURLShape, baseURL: string, displayName: string, fix: string): string | undefined;