@juspay/neurolink 12.56.1 → 12.58.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/README.md +90 -55
  3. package/dist/browser/neurolink.min.js +419 -416
  4. package/dist/cli/commands/decide.d.ts +4 -1
  5. package/dist/cli/commands/decide.js +6 -3
  6. package/dist/cli/commands/setup.d.ts +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/factories/commandFactory.js +1 -1
  9. package/dist/constants/enums.d.ts +8 -0
  10. package/dist/constants/enums.js +10 -0
  11. package/dist/core/factory.d.ts +7 -0
  12. package/dist/core/factory.js +20 -0
  13. package/dist/factories/providerDescriptors.js +103 -9
  14. package/dist/providers/catalog/index.generated.d.ts +1 -1
  15. package/dist/providers/catalog/index.generated.js +10 -1
  16. package/dist/providers/catalog/openai-decisions.json +75 -0
  17. package/dist/providers/catalog/provider-catalog.schema.json +56 -3
  18. package/dist/providers/catalog/schema.js +92 -11
  19. package/dist/providers/catalog/subq.json +59 -0
  20. package/dist/providers/configuredDecision.js +9 -14
  21. package/dist/providers/decide/dialects/index.js +2 -0
  22. package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
  23. package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
  24. package/dist/providers/decide/dialects/systemOne.js +17 -0
  25. package/dist/providers/systemOneDecision.js +6 -1
  26. package/dist/telemetry/attributes.d.ts +10 -3
  27. package/dist/telemetry/attributes.js +142 -10
  28. package/dist/telemetry/index.d.ts +1 -1
  29. package/dist/telemetry/index.js +1 -1
  30. package/dist/types/classifierRouter.d.ts +21 -9
  31. package/dist/types/cli.d.ts +4 -2
  32. package/dist/types/decision.d.ts +67 -3
  33. package/dist/types/providerCatalog.d.ts +7 -11
  34. package/dist/types/providerCatalog.generated.d.ts +2 -2
  35. package/dist/types/providers.d.ts +16 -1
  36. package/dist/utils/pricing.js +9 -0
  37. package/docs-site/static/search-index.json +105 -64
  38. package/package.json +3 -1
@@ -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 = SPAN_ATTRIBUTE_MAX_CHARS) {
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 = SPAN_ATTRIBUTE_MAX_CHARS) {
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 and the newest messages are budgeted first, older messages are
342
- * dropped whole and counted, and the system prompt gets whatever room is left.
343
- * The result is always valid JSON.
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 = SPAN_ATTRIBUTE_MAX_CHARS) {
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
- const kept = [];
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
- kept.unshift(messages[index]);
529
+ turn.unshift(messages[index]);
419
530
  remaining -= size;
420
531
  continue;
421
532
  }
422
- if (kept.length === 0 && remaining > 64) {
533
+ turnComplete = false;
534
+ if (turn.length === 0 && remaining > 64) {
423
535
  const excerpt = fitText(serialize(messages[index]), remaining - 32);
424
- kept.unshift({ truncatedMessage: excerpt });
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,
@@ -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` — TypeSafe's System One model; one ~400ms round trip that returns a
27
- * *calibrated* confidence rather than a self-reported one.
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`, or
31
- * `PERPLEXITY_API_KEY`), otherwise
32
- * `heuristic`.
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`, or `PERPLEXITY_API_KEY`) and "heuristic"
194
- * otherwise, so configuring one upgrades routing without any code change.
195
- * Behaviour for callers with no key is unchanged. `PERPLEXITY_API_KEY` is also
196
- * the Perplexity text provider's key, so it counts here too.
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. */
@@ -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, or PERPLEXITY_API_KEY) and
1784
- * "heuristic" otherwise.
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
  /**
@@ -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
- /** `undefined` means the response has no usable answers container. */
540
- readAnswers: (decoded: Readonly<Record<string, unknown>>, reportedConfidence: Readonly<Record<string, number>>) => DecisionAnswerReading | undefined;
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
  };
@@ -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" | "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" | "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;
@@ -485,6 +489,10 @@ export type NeurolinkCredentials = {
485
489
  apiKey?: string;
486
490
  baseURL?: string;
487
491
  };
492
+ subq?: {
493
+ apiKey?: string;
494
+ baseURL?: string;
495
+ };
488
496
  synthetic?: {
489
497
  apiKey?: string;
490
498
  baseURL?: string;
@@ -2461,6 +2469,13 @@ export type ProviderDescriptor = {
2461
2469
  * descriptor that serves `decide`; checked when the descriptors load.
2462
2470
  */
2463
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;
2464
2479
  /**
2465
2480
  * Ascending priority (1 = tried first) in the auto-select fallback chain
2466
2481
  * used by getBestProvider(). Undefined = not part of the auto-select chain.
@@ -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