@juspay/neurolink 12.57.0 → 12.58.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.
- package/CHANGELOG.md +3 -3
- package/README.md +75 -51
- package/dist/browser/neurolink.min.js +419 -416
- package/dist/cli/commands/decide.d.ts +4 -1
- package/dist/cli/commands/decide.js +6 -3
- package/dist/cli/commands/setup.d.ts +1 -1
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/enums.d.ts +4 -0
- package/dist/constants/enums.js +5 -0
- package/dist/core/factory.d.ts +7 -0
- package/dist/core/factory.js +20 -0
- package/dist/factories/providerDescriptors.js +96 -7
- package/dist/processors/media/VideoProcessor.d.ts +2 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +8 -1
- package/dist/providers/catalog/openai-decisions.json +75 -0
- package/dist/providers/catalog/provider-catalog.schema.json +56 -3
- package/dist/providers/catalog/schema.js +92 -11
- package/dist/providers/configuredDecision.js +9 -14
- package/dist/providers/decide/dialects/index.js +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
- package/dist/providers/decide/dialects/systemOne.js +17 -0
- package/dist/providers/systemOneDecision.js +6 -1
- package/dist/telemetry/attributes.d.ts +10 -3
- package/dist/telemetry/attributes.js +142 -10
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/types/classifierRouter.d.ts +21 -9
- package/dist/types/cli.d.ts +4 -2
- package/dist/types/decision.d.ts +67 -3
- package/dist/types/processor.d.ts +10 -0
- package/dist/types/providerCatalog.d.ts +7 -11
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +12 -1
- package/dist/utils/pricing.js +9 -0
- package/docs-site/static/search-index.json +84 -59
- package/package.json +6 -2
- package/dist/client/reactHooks.tsx +0 -1473
|
@@ -140,8 +140,24 @@ export const LANGFUSE_ATTR = {
|
|
|
140
140
|
};
|
|
141
141
|
/** Default ceiling for serialized span attribute values. */
|
|
142
142
|
export const SPAN_ATTRIBUTE_MAX_CHARS = 40_000;
|
|
143
|
+
/** Smallest ceiling accepted, so the system and question openings still fit. */
|
|
144
|
+
const SPAN_ATTRIBUTE_MIN_CHARS = 10_000;
|
|
145
|
+
/**
|
|
146
|
+
* The ceiling in force: `NEUROLINK_SPAN_ATTRIBUTE_MAX_CHARS` when it is a
|
|
147
|
+
* positive whole number (raised to the minimum if lower), otherwise the
|
|
148
|
+
* default. Read on every call, so a change applies to the next span.
|
|
149
|
+
*/
|
|
150
|
+
export function spanAttributeMaxChars() {
|
|
151
|
+
const configured = Number(process.env.NEUROLINK_SPAN_ATTRIBUTE_MAX_CHARS?.trim());
|
|
152
|
+
if (!Number.isSafeInteger(configured) || configured <= 0) {
|
|
153
|
+
return SPAN_ATTRIBUTE_MAX_CHARS;
|
|
154
|
+
}
|
|
155
|
+
return Math.max(SPAN_ATTRIBUTE_MIN_CHARS, configured);
|
|
156
|
+
}
|
|
143
157
|
/** Opening of the system prompt a model-request attribute always keeps. */
|
|
144
158
|
const SYSTEM_PROMPT_FLOOR_CHARS = 2_000;
|
|
159
|
+
/** Room the current question always gets in a model-request attribute. */
|
|
160
|
+
const QUESTION_FLOOR_CHARS = 4_000;
|
|
145
161
|
/** Room for the "...[truncated N chars]" note, kept outside that opening. */
|
|
146
162
|
const TRUNCATION_NOTE_ALLOWANCE_CHARS = 48;
|
|
147
163
|
const MIN_SHRINKABLE_STRING_CHARS = 64;
|
|
@@ -288,7 +304,7 @@ function shrinkLongestString(root, excess) {
|
|
|
288
304
|
* A structured value over the cap is shortened by trimming its longest
|
|
289
305
|
* strings, so the attribute stays valid JSON instead of ending mid-token.
|
|
290
306
|
*/
|
|
291
|
-
export function spanContentAttribute(value, maxChars =
|
|
307
|
+
export function spanContentAttribute(value, maxChars = spanAttributeMaxChars()) {
|
|
292
308
|
const redacted = redactSafely(value);
|
|
293
309
|
if (redacted === null || typeof redacted !== "object") {
|
|
294
310
|
return spanJsonAttribute(redacted, maxChars);
|
|
@@ -314,7 +330,7 @@ export function spanContentAttribute(value, maxChars = SPAN_ATTRIBUTE_MAX_CHARS)
|
|
|
314
330
|
* on a single span. Strings pass through unserialized; everything else is
|
|
315
331
|
* JSON-stringified with a String() fallback for circular structures.
|
|
316
332
|
*/
|
|
317
|
-
export function spanJsonAttribute(value, maxChars =
|
|
333
|
+
export function spanJsonAttribute(value, maxChars = spanAttributeMaxChars()) {
|
|
318
334
|
let serialized;
|
|
319
335
|
try {
|
|
320
336
|
serialized =
|
|
@@ -332,17 +348,72 @@ export function spanJsonAttribute(value, maxChars = SPAN_ATTRIBUTE_MAX_CHARS) {
|
|
|
332
348
|
}
|
|
333
349
|
return serialized;
|
|
334
350
|
}
|
|
351
|
+
/** The content blocks of an Anthropic-shaped message; none for plain text. */
|
|
352
|
+
function messageBlocks(message) {
|
|
353
|
+
if (message === null || typeof message !== "object") {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
return "content" in message && Array.isArray(message.content)
|
|
357
|
+
? message.content
|
|
358
|
+
: [];
|
|
359
|
+
}
|
|
360
|
+
/** One field of a content block, or undefined when the block lacks it. */
|
|
361
|
+
function blockField(block, field) {
|
|
362
|
+
return block !== null && typeof block === "object" && field in block
|
|
363
|
+
? Reflect.get(block, field)
|
|
364
|
+
: undefined;
|
|
365
|
+
}
|
|
366
|
+
/** Whether a message is the user's, as opposed to the assistant's. */
|
|
367
|
+
function isUserMessage(message) {
|
|
368
|
+
return (message !== null &&
|
|
369
|
+
typeof message === "object" &&
|
|
370
|
+
"role" in message &&
|
|
371
|
+
message.role === "user");
|
|
372
|
+
}
|
|
373
|
+
/** A user message that says something, not one that only returns tool results. */
|
|
374
|
+
function isUserQuestion(message) {
|
|
375
|
+
if (!isUserMessage(message)) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return (("content" in message && typeof message.content === "string") ||
|
|
379
|
+
messageBlocks(message).some((block) => blockField(block, "type") === "text"));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Removes every tool result whose call is not among `messages`, including one
|
|
383
|
+
* that shares a message with text, and drops messages left with nothing.
|
|
384
|
+
*/
|
|
385
|
+
function withoutUnansweredResults(messages) {
|
|
386
|
+
const calls = new Set(messages
|
|
387
|
+
.flatMap(messageBlocks)
|
|
388
|
+
.filter((block) => blockField(block, "type") === "tool_use")
|
|
389
|
+
.map((block) => blockField(block, "id")));
|
|
390
|
+
return messages.flatMap((message) => {
|
|
391
|
+
const blocks = messageBlocks(message);
|
|
392
|
+
const answered = blocks.filter((block) => blockField(block, "type") !== "tool_result" ||
|
|
393
|
+
calls.has(blockField(block, "tool_use_id")));
|
|
394
|
+
if (answered.length === blocks.length) {
|
|
395
|
+
return [message];
|
|
396
|
+
}
|
|
397
|
+
if (answered.length === 0 ||
|
|
398
|
+
message === null ||
|
|
399
|
+
typeof message !== "object") {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
return [{ ...message, content: answered }];
|
|
403
|
+
});
|
|
404
|
+
}
|
|
335
405
|
/**
|
|
336
406
|
* Serialize a model request for a span attribute without losing the
|
|
337
407
|
* conversation to the cap.
|
|
338
408
|
*
|
|
339
409
|
* A head cut keeps the system prompt and drops everything after it, so a long
|
|
340
410
|
* system prompt hides every message, the user's question included. Here the
|
|
341
|
-
* prompt
|
|
342
|
-
* dropped whole and counted, and the system prompt
|
|
343
|
-
*
|
|
411
|
+
* prompt, the current question and this turn's newest messages are budgeted
|
|
412
|
+
* first, earlier messages are dropped whole and counted, and the system prompt
|
|
413
|
+
* gets whatever room is left. A tool result is never kept without the call it
|
|
414
|
+
* answers. The result is always valid JSON.
|
|
344
415
|
*/
|
|
345
|
-
export function spanModelRequestAttribute(rawRequest, maxChars =
|
|
416
|
+
export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMaxChars()) {
|
|
346
417
|
// Credentials are masked before anything is measured, so budgets apply to
|
|
347
418
|
// what is actually written. Each message is masked on its own: one that
|
|
348
419
|
// cannot be walked is omitted without losing the rest of the conversation.
|
|
@@ -410,22 +481,83 @@ export function spanModelRequestAttribute(rawRequest, maxChars = SPAN_ATTRIBUTE_
|
|
|
410
481
|
prompt = fitText(request.prompt, remaining);
|
|
411
482
|
remaining -= jsonLength(prompt);
|
|
412
483
|
}
|
|
484
|
+
// Shortens a copy of the message's longest strings; the shape survives.
|
|
485
|
+
const fitMessage = (message, budget) => {
|
|
486
|
+
if (jsonLength(message) <= budget) {
|
|
487
|
+
return message;
|
|
488
|
+
}
|
|
489
|
+
let copy;
|
|
490
|
+
try {
|
|
491
|
+
copy = JSON.parse(serialize(message));
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
copy = undefined;
|
|
495
|
+
}
|
|
496
|
+
if (copy !== null && typeof copy === "object") {
|
|
497
|
+
for (let attempt = 0; jsonLength(copy) > budget && attempt < MAX_SHRINK_ATTEMPTS; attempt++) {
|
|
498
|
+
if (!shrinkLongestString(copy, jsonLength(copy) - budget)) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (jsonLength(copy) <= budget) {
|
|
503
|
+
return copy;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
truncatedMessage: fitText(serialize(message), Math.max(0, budget - 32)),
|
|
508
|
+
};
|
|
509
|
+
};
|
|
413
510
|
const messages = request.messages ?? [];
|
|
414
|
-
|
|
511
|
+
// The current question is the newest user message with something to say;
|
|
512
|
+
// everything after it is this turn's tool calls and their results.
|
|
513
|
+
let questionIndex = -1;
|
|
415
514
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
515
|
+
if (isUserQuestion(messages[index])) {
|
|
516
|
+
questionIndex = index;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const questionFloor = questionIndex >= 0
|
|
521
|
+
? Math.min(jsonLength(messages[questionIndex]) + 1, QUESTION_FLOOR_CHARS + TRUNCATION_NOTE_ALLOWANCE_CHARS)
|
|
522
|
+
: 0;
|
|
523
|
+
remaining -= questionFloor;
|
|
524
|
+
const turn = [];
|
|
525
|
+
let turnComplete = true;
|
|
526
|
+
for (let index = messages.length - 1; index > questionIndex; index--) {
|
|
416
527
|
const size = jsonLength(messages[index]) + 1;
|
|
417
528
|
if (size <= remaining) {
|
|
418
|
-
|
|
529
|
+
turn.unshift(messages[index]);
|
|
419
530
|
remaining -= size;
|
|
420
531
|
continue;
|
|
421
532
|
}
|
|
422
|
-
|
|
533
|
+
turnComplete = false;
|
|
534
|
+
if (turn.length === 0 && remaining > 64) {
|
|
423
535
|
const excerpt = fitText(serialize(messages[index]), remaining - 32);
|
|
424
|
-
|
|
536
|
+
turn.unshift({ truncatedMessage: excerpt });
|
|
425
537
|
remaining -= jsonLength(excerpt) + 32;
|
|
426
538
|
}
|
|
427
539
|
break;
|
|
428
540
|
}
|
|
541
|
+
remaining += questionFloor;
|
|
542
|
+
let kept = [];
|
|
543
|
+
if (questionIndex >= 0) {
|
|
544
|
+
const question = fitMessage(messages[questionIndex], remaining - 1);
|
|
545
|
+
remaining -= jsonLength(question) + 1;
|
|
546
|
+
const history = [];
|
|
547
|
+
for (let index = questionIndex - 1; turnComplete && index >= 0; index--) {
|
|
548
|
+
const size = jsonLength(messages[index]) + 1;
|
|
549
|
+
if (size > remaining) {
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
history.unshift(messages[index]);
|
|
553
|
+
remaining -= size;
|
|
554
|
+
}
|
|
555
|
+
kept.push(...history, question);
|
|
556
|
+
}
|
|
557
|
+
kept.push(...turn);
|
|
558
|
+
if (kept.length < messages.length) {
|
|
559
|
+
kept = withoutUnansweredResults(kept);
|
|
560
|
+
}
|
|
429
561
|
const omittedMessages = messages.length - kept.length;
|
|
430
562
|
remaining += systemFloor;
|
|
431
563
|
const body = {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { TelemetryService } from "./telemetryService.js";
|
|
2
2
|
export { tracers } from "./tracers.js";
|
|
3
3
|
export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, } from "./withSpan.js";
|
|
4
|
-
export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
4
|
+
export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
5
5
|
/**
|
|
6
6
|
* Initialize telemetry for NeuroLink
|
|
7
7
|
* Reuses an existing global TracerProvider when one is already registered,
|
package/dist/telemetry/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { TelemetryService } from "./telemetryService.js";
|
|
3
3
|
export { tracers } from "./tracers.js";
|
|
4
4
|
export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, } from "./withSpan.js";
|
|
5
|
-
export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
5
|
+
export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
/**
|
|
8
8
|
* Initialize telemetry for NeuroLink
|
|
@@ -23,13 +23,17 @@ export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" |
|
|
|
23
23
|
*
|
|
24
24
|
* - `heuristic` — keyword/length scoring. Deterministic, zero latency.
|
|
25
25
|
* - `llm` — a cheap classifier model via the injected `generate`.
|
|
26
|
-
* - `jev` —
|
|
27
|
-
*
|
|
26
|
+
* - `jev` — a decision model, whichever decision provider is configured
|
|
27
|
+
* (TypeSafe's Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef or OpenAI
|
|
28
|
+
* Decisions); one round trip that returns the confidence the provider
|
|
29
|
+
* reports (*calibrated* on TypeSafe's Jev, ~400ms there) rather than a
|
|
30
|
+
* self-reported one.
|
|
28
31
|
* - `auto` — `jev` when a decision provider is configured, in the environment
|
|
29
32
|
* or in SDK credentials (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with
|
|
30
|
-
* `LAYA_BASE_URL`, `XOR_API_KEY` with `XOR_BASE_URL`,
|
|
31
|
-
* `
|
|
32
|
-
* `
|
|
33
|
+
* `LAYA_BASE_URL`, `XOR_API_KEY` with `XOR_BASE_URL`, `SUBQ_API_KEY`, `PERPLEXITY_API_KEY`,
|
|
34
|
+
* `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, or `OPENAI_API_KEY`,
|
|
35
|
+
* which does not count while `OPENAI_BASE_URL`, `OPENAI_API_BASE` or `credentials.openai.baseURL` names a
|
|
36
|
+
* host other than `https://api.openai.com`), otherwise `heuristic`.
|
|
33
37
|
*/
|
|
34
38
|
export type ClassifierStrategyKind = "heuristic" | "llm" | "jev" | "auto";
|
|
35
39
|
/**
|
|
@@ -190,10 +194,18 @@ export type ClassifierRouterConfig = {
|
|
|
190
194
|
* Classification strategy. Default: "auto" — which resolves to "jev" when a
|
|
191
195
|
* decision provider is configured, in the environment or in SDK credentials
|
|
192
196
|
* (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with `LAYA_BASE_URL`,
|
|
193
|
-
* `XOR_API_KEY` with `XOR_BASE_URL`,
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
+
* `XOR_API_KEY` with `XOR_BASE_URL`, `SUBQ_API_KEY`, `PERPLEXITY_API_KEY`,
|
|
198
|
+
* `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, or `OPENAI_API_KEY`) and
|
|
199
|
+
* "heuristic" otherwise, so configuring one upgrades routing without any code
|
|
200
|
+
* change. Behaviour for a caller with none of these is unchanged. Three of them
|
|
201
|
+
* are shared with a text provider and count here too: `PERPLEXITY_API_KEY`
|
|
202
|
+
* (Perplexity's), the Cloudflare pair (Workers AI's) and `OPENAI_API_KEY` (the
|
|
203
|
+
* OpenAI chat provider's). A caller whose only key is `OPENAI_API_KEY` now
|
|
204
|
+
* resolves to "jev", answered by OpenAI Decisions as the last-resort decision
|
|
205
|
+
* provider; each classification is then a paid call that still falls back to
|
|
206
|
+
* the heuristic on any failure. The exception is a key that sits beside an
|
|
207
|
+
* `OPENAI_BASE_URL`, `OPENAI_API_BASE` or `credentials.openai.baseURL` naming a host other than
|
|
208
|
+
* `https://api.openai.com`: that key is a gateway's and does not count.
|
|
197
209
|
*/
|
|
198
210
|
classifier?: ClassifierStrategyKind;
|
|
199
211
|
/** Model used by the "llm" strategy. Defaults to provider/model auto. */
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1780,8 +1780,10 @@ export type CliClassifierRouterFlags = {
|
|
|
1780
1780
|
* Strategy: "auto" (default), "heuristic", "llm" or "jev"
|
|
1781
1781
|
* (--classifier-strategy). "auto" resolves to "jev" when a decision
|
|
1782
1782
|
* provider is configured (TYPESAFE_API_KEY, LAYA_API_KEY with
|
|
1783
|
-
* LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL,
|
|
1784
|
-
*
|
|
1783
|
+
* LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
|
|
1784
|
+
* CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY, which does
|
|
1785
|
+
* not count beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL naming a host other
|
|
1786
|
+
* than https://api.openai.com) and "heuristic" otherwise.
|
|
1785
1787
|
*/
|
|
1786
1788
|
classifierStrategy?: string;
|
|
1787
1789
|
/**
|
package/dist/types/decision.d.ts
CHANGED
|
@@ -523,11 +523,70 @@ export type DecisionBooleanGate = {
|
|
|
523
523
|
minConfidence?: number;
|
|
524
524
|
};
|
|
525
525
|
/** The wire family a decide provider speaks. */
|
|
526
|
-
export type DecideDialectName = "system-one";
|
|
526
|
+
export type DecideDialectName = "system-one" | "openai-decisions";
|
|
527
527
|
export type DecisionAnswerReading = {
|
|
528
528
|
readonly answers: Readonly<Record<string, DecisionAnswer>>;
|
|
529
529
|
/** Ids of answers the server returned that could not be parsed. */
|
|
530
530
|
readonly dropped: readonly string[];
|
|
531
|
+
/** Ids of questions the model declined to answer; they have no answer. */
|
|
532
|
+
readonly refused?: readonly string[];
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* The request body keys prepared media is written under. Set together with
|
|
536
|
+
* `decisionLimits.media`, which says how much of it a request may carry.
|
|
537
|
+
*/
|
|
538
|
+
export type DecideMedia = {
|
|
539
|
+
/** Written only when the request carries at least one image. */
|
|
540
|
+
imagesKey: string;
|
|
541
|
+
/** Written only when the request carries a video; required exactly when `decisionLimits.media.video` is true. */
|
|
542
|
+
videoKey?: string;
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Says when a decide provider's key may be used as the default decision
|
|
546
|
+
* provider. A key found only in the environment may belong to something else:
|
|
547
|
+
* `OPENAI_API_KEY` is also the key of every OpenAI-compatible gateway, and
|
|
548
|
+
* sending a gateway's key and the decision state to the vendor's own host would
|
|
549
|
+
* disclose both. With a guard, the environment key is skipped for default
|
|
550
|
+
* resolution while any variable in `baseURLEnvVars`, or any credentials field in
|
|
551
|
+
* `credentialBaseURLs`, holds a URL whose origin (scheme, host and port,
|
|
552
|
+
* compared case-insensitively; path and trailing slash ignored) is not in
|
|
553
|
+
* `trustedOrigins`; a value that does not parse as an http(s) URL counts as
|
|
554
|
+
* untrusted, and a blank one as unset.
|
|
555
|
+
*
|
|
556
|
+
* It does not apply once the host routed the provider on purpose: the
|
|
557
|
+
* provider's own base-URL variable is set, or `credentials.<key>` supplies a key
|
|
558
|
+
* or a base URL. It never applies to a call that names the provider.
|
|
559
|
+
*/
|
|
560
|
+
export type DecideAmbientKeyGuard = {
|
|
561
|
+
/** Environment variables that move the key's other users off the vendor's host. */
|
|
562
|
+
baseURLEnvVars: readonly string[];
|
|
563
|
+
/** Origins that are the vendor's own, such as `https://api.openai.com`. */
|
|
564
|
+
trustedOrigins: readonly string[];
|
|
565
|
+
/**
|
|
566
|
+
* Credentials fields that move the key's other users off the vendor's host,
|
|
567
|
+
* read from the same `credentials` the resolver is given (the instance's
|
|
568
|
+
* merged with the call's), such as the chat provider's `credentials.openai`
|
|
569
|
+
* `baseURL`. Omitted means only the environment is read.
|
|
570
|
+
*/
|
|
571
|
+
credentialBaseURLs?: readonly DecideAmbientKeyCredentialField[];
|
|
572
|
+
};
|
|
573
|
+
/** One credentials field a {@link DecideAmbientKeyGuard} reads: `credentials[key][field]`. */
|
|
574
|
+
export type DecideAmbientKeyCredentialField = {
|
|
575
|
+
/** The `NeurolinkCredentials` key of the slice, such as `openai`. */
|
|
576
|
+
key: string;
|
|
577
|
+
/** The field in that slice. Only `baseURL` is read today. */
|
|
578
|
+
field: "baseURL";
|
|
579
|
+
};
|
|
580
|
+
/** What a dialect lays out as one request body. */
|
|
581
|
+
export type DecisionDialectBody = {
|
|
582
|
+
readonly state: DecisionState;
|
|
583
|
+
/** Each question already written by the dialect's `encodeQuestion`, by id. */
|
|
584
|
+
readonly questions: Readonly<Record<string, Record<string, unknown>>>;
|
|
585
|
+
/** Undefined means the request names no model. */
|
|
586
|
+
readonly model?: string;
|
|
587
|
+
readonly media?: DecisionPreparedMedia;
|
|
588
|
+
/** Where a layout with named media fields puts them; other layouts ignore it. */
|
|
589
|
+
readonly mediaKeys?: Readonly<DecideMedia>;
|
|
531
590
|
};
|
|
532
591
|
/**
|
|
533
592
|
* How questions are written to, and answers read from, a decide wire. One
|
|
@@ -536,6 +595,11 @@ export type DecisionAnswerReading = {
|
|
|
536
595
|
export type DecisionDialect = {
|
|
537
596
|
readonly name: DecideDialectName;
|
|
538
597
|
encodeQuestion: (question: DecisionQuestion) => Record<string, unknown>;
|
|
539
|
-
|
|
540
|
-
|
|
598
|
+
buildBody: (input: DecisionDialectBody) => Record<string, unknown>;
|
|
599
|
+
/**
|
|
600
|
+
* `undefined` means the response has no usable answers container. `asked`
|
|
601
|
+
* is the request's own question map, for a wire that echoes names rather
|
|
602
|
+
* than the ids sent.
|
|
603
|
+
*/
|
|
604
|
+
readAnswers: (decoded: Readonly<Record<string, unknown>>, reportedConfidence: Readonly<Record<string, number>>, asked: DecisionQuestionMap) => DecisionAnswerReading | undefined;
|
|
541
605
|
};
|
|
@@ -570,6 +570,16 @@ export type FfprobeData = {
|
|
|
570
570
|
[key: string]: unknown;
|
|
571
571
|
};
|
|
572
572
|
};
|
|
573
|
+
/**
|
|
574
|
+
* Structural shape of the `fluent-ffmpeg` export: a callable carrying the
|
|
575
|
+
* `ffprobe` and `setFfmpegPath` statics. Public declarations use this instead of
|
|
576
|
+
* `typeof import("fluent-ffmpeg")`, whose types live in a devDependency that
|
|
577
|
+
* consumers do not install.
|
|
578
|
+
*/
|
|
579
|
+
export type ProcessorFluentFfmpegModule = ((...args: never[]) => unknown) & {
|
|
580
|
+
ffprobe: (...args: never[]) => unknown;
|
|
581
|
+
setFfmpegPath: (...args: never[]) => unknown;
|
|
582
|
+
};
|
|
573
583
|
/**
|
|
574
584
|
* Structural types for exceljs objects.
|
|
575
585
|
* Defined here so the optional exceljs package is not required at typecheck time.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* src/lib/providers/catalog/<id>.json file per provider is the single
|
|
5
5
|
* source of truth; see docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md.
|
|
6
6
|
*/
|
|
7
|
-
import type { DecideDialectName, DecisionErrorKind, DecisionLimits } from "./decision.js";
|
|
7
|
+
import type { DecideAmbientKeyGuard, DecideDialectName, DecideMedia, DecisionErrorKind, DecisionLimits } from "./decision.js";
|
|
8
8
|
export type CatalogPricingPerMTok = {
|
|
9
9
|
input: number;
|
|
10
10
|
output: number;
|
|
@@ -300,16 +300,6 @@ export type DecideModel = {
|
|
|
300
300
|
/** With `body-unless-alias`: the model name that means "send no model". */
|
|
301
301
|
alias?: string;
|
|
302
302
|
};
|
|
303
|
-
/**
|
|
304
|
-
* The request body keys prepared media is written under. Set together with
|
|
305
|
-
* `decisionLimits.media`, which says how much of it a request may carry.
|
|
306
|
-
*/
|
|
307
|
-
export type DecideMedia = {
|
|
308
|
-
/** Written only when the request carries at least one image. */
|
|
309
|
-
imagesKey: string;
|
|
310
|
-
/** Written only when the request carries a video; required exactly when `decisionLimits.media.video` is true. */
|
|
311
|
-
videoKey?: string;
|
|
312
|
-
};
|
|
313
303
|
export type DecideBlock = {
|
|
314
304
|
dialect: DecideDialectName;
|
|
315
305
|
/** Decision order, lower runs first; unique across decide providers. */
|
|
@@ -325,6 +315,12 @@ export type DecideBlock = {
|
|
|
325
315
|
model: DecideModel;
|
|
326
316
|
/** Body keys for images and video, after `model`, `state` and `questions`. Omitted means the body never carries media. */
|
|
327
317
|
media?: DecideMedia;
|
|
318
|
+
/**
|
|
319
|
+
* Keeps a key found only in the environment from becoming the default
|
|
320
|
+
* decision provider when it plainly belongs to something else. Omitted means
|
|
321
|
+
* no check.
|
|
322
|
+
*/
|
|
323
|
+
ambientKeyGuard?: DecideAmbientKeyGuard;
|
|
328
324
|
requestIdHeaders?: readonly string[];
|
|
329
325
|
response?: {
|
|
330
326
|
modelPaths?: readonly string[];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "laya" | "lemonfox-ai" | "lilac" | "llmtech" | "longcat" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "runinfra" | "sakana-ai" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "xiaomi-mimo" | "xor" | "z-ai";
|
|
2
|
-
export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "laya" | "lemonfoxAi" | "lilac" | "llmtech" | "longcat" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "runinfra" | "sakanaAi" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "xiaomiMimo" | "xor" | "zAi";
|
|
1
|
+
export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "laya" | "lemonfox-ai" | "lilac" | "llmtech" | "longcat" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "openai-decisions" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "runinfra" | "sakana-ai" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "xiaomi-mimo" | "xor" | "z-ai";
|
|
2
|
+
export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "laya" | "lemonfoxAi" | "lilac" | "llmtech" | "longcat" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "openaiDecisions" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "runinfra" | "sakanaAi" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "xiaomiMimo" | "xor" | "zAi";
|
|
@@ -6,7 +6,7 @@ import type { NeuroLink } from "../neurolink.js";
|
|
|
6
6
|
import { AIProviderName, AnthropicModels, BedrockModels, DeepSeekModels, GoogleAIModels, LlamaCppModels, LMStudioModels, NvidiaNimModels, OpenAIModels, VertexModels } from "../constants/enums.js";
|
|
7
7
|
import type { ValidationSchema } from "./aliases.js";
|
|
8
8
|
import type { STTCredentials } from "./transcribe.js";
|
|
9
|
-
import type { DecisionLimits, DecisionRequest, DecisionResult } from "./decision.js";
|
|
9
|
+
import type { DecideAmbientKeyGuard, DecisionLimits, DecisionRequest, DecisionResult } from "./decision.js";
|
|
10
10
|
import type { FileToolRootPolicy } from "./fileToolRoots.js";
|
|
11
11
|
import type { EnhancedGenerateResult, TextGenerationOptions } from "./generate.js";
|
|
12
12
|
import type { MultimodalAudioEntry, MultimodalVideoEntry } from "./file.js";
|
|
@@ -421,6 +421,10 @@ export type NeurolinkCredentials = {
|
|
|
421
421
|
apiKey?: string;
|
|
422
422
|
baseURL?: string;
|
|
423
423
|
};
|
|
424
|
+
openaiDecisions?: {
|
|
425
|
+
apiKey?: string;
|
|
426
|
+
baseURL?: string;
|
|
427
|
+
};
|
|
424
428
|
ovhcloud?: {
|
|
425
429
|
apiKey?: string;
|
|
426
430
|
baseURL?: string;
|
|
@@ -2465,6 +2469,13 @@ export type ProviderDescriptor = {
|
|
|
2465
2469
|
* descriptor that serves `decide`; checked when the descriptors load.
|
|
2466
2470
|
*/
|
|
2467
2471
|
decideOrder?: number;
|
|
2472
|
+
/**
|
|
2473
|
+
* Skips this provider's environment key for default resolution while a
|
|
2474
|
+
* listed base-URL variable names a host outside the trusted origins. Only a
|
|
2475
|
+
* decide provider whose key is also another provider's can use it. See
|
|
2476
|
+
* {@link DecideAmbientKeyGuard}.
|
|
2477
|
+
*/
|
|
2478
|
+
ambientKeyGuard?: DecideAmbientKeyGuard;
|
|
2468
2479
|
/**
|
|
2469
2480
|
* Ascending priority (1 = tried first) in the auto-select fallback chain
|
|
2470
2481
|
* used by getBestProvider(). Undefined = not part of the auto-select chain.
|
package/dist/utils/pricing.js
CHANGED
|
@@ -803,6 +803,15 @@ const PRICING = {
|
|
|
803
803
|
_default: { input: 0.04 / 1_000_000, output: 0 },
|
|
804
804
|
"pplx-decider-v1-27b": { input: 0.04 / 1_000_000, output: 0 },
|
|
805
805
|
},
|
|
806
|
+
"openai-decisions": {
|
|
807
|
+
// $0.10 per million input tokens, input only: `output: 0` is a price, not a
|
|
808
|
+
// token count, so do not "correct" it from `usage.output_tokens`. Source:
|
|
809
|
+
// https://developers.openai.com/api/docs/guides/decisions (read 2026-10-07).
|
|
810
|
+
// Regional and long-context multipliers are not modelled. Only `_default`:
|
|
811
|
+
// LiteLLM, OpenRouter and openai-compatible search every table by model
|
|
812
|
+
// name, so a `"gpt-6-luna"` key would price that chat model at this rate.
|
|
813
|
+
_default: { input: 0.1 / 1_000_000, output: 0 },
|
|
814
|
+
},
|
|
806
815
|
"cloudflare-clef": {
|
|
807
816
|
// Workers AI bills Clef per million INPUT tokens ($0.24 for clef, $0.09 for
|
|
808
817
|
// clef-flash); no output price is listed. `output: 0` is a price, not a token
|