@juspay/neurolink 12.57.0 → 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 (37) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/README.md +75 -51
  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 +4 -0
  10. package/dist/constants/enums.js +5 -0
  11. package/dist/core/factory.d.ts +7 -0
  12. package/dist/core/factory.js +20 -0
  13. package/dist/factories/providerDescriptors.js +96 -7
  14. package/dist/providers/catalog/index.generated.d.ts +1 -1
  15. package/dist/providers/catalog/index.generated.js +8 -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/configuredDecision.js +9 -14
  20. package/dist/providers/decide/dialects/index.js +2 -0
  21. package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
  22. package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
  23. package/dist/providers/decide/dialects/systemOne.js +17 -0
  24. package/dist/providers/systemOneDecision.js +6 -1
  25. package/dist/telemetry/attributes.d.ts +10 -3
  26. package/dist/telemetry/attributes.js +142 -10
  27. package/dist/telemetry/index.d.ts +1 -1
  28. package/dist/telemetry/index.js +1 -1
  29. package/dist/types/classifierRouter.d.ts +21 -9
  30. package/dist/types/cli.d.ts +4 -2
  31. package/dist/types/decision.d.ts +67 -3
  32. package/dist/types/providerCatalog.d.ts +7 -11
  33. package/dist/types/providerCatalog.generated.d.ts +2 -2
  34. package/dist/types/providers.d.ts +12 -1
  35. package/dist/utils/pricing.js +9 -0
  36. package/docs-site/static/search-index.json +84 -59
  37. package/package.json +3 -1
@@ -9,7 +9,10 @@
9
9
  *
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
11
  * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
- * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, or PERPLEXITY_API_KEY) itself.
12
+ * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
13
+ * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY) itself. An
14
+ * OPENAI_API_KEY beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL that names a host
15
+ * other than https://api.openai.com is a gateway's and is not used by default.
13
16
  */
14
17
  import type { CommandModule } from "yargs";
15
18
  import type { CliDecideArgs } from "../../types/index.js";
@@ -9,7 +9,10 @@
9
9
  *
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
11
  * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
- * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, or PERPLEXITY_API_KEY) itself.
12
+ * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
13
+ * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY) itself. An
14
+ * OPENAI_API_KEY beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL that names a host
15
+ * other than https://api.openai.com is a gateway's and is not used by default.
13
16
  */
14
17
  import chalk from "chalk";
15
18
  import ora from "ora";
@@ -191,7 +194,7 @@ export const decideCommand = {
191
194
  type: "string",
192
195
  array: true,
193
196
  nargs: 1,
194
- describe: "Image for the model to read: a file path or a data: URL. Repeat for several (XOR and Perplexity take up to 8, Cloudflare Clef up to 4)",
197
+ describe: "Image for the model to read: a file path or a data: URL. Repeat for several (XOR, Perplexity and OpenAI Decisions take up to 8, Cloudflare Clef up to 4)",
195
198
  })
196
199
  .option("video", {
197
200
  type: "string",
@@ -217,7 +220,7 @@ export const decideCommand = {
217
220
  })
218
221
  .example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
219
222
  .example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON")
220
- .example('$0 decide "What color is this?" --provider xor --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR, Perplexity and Cloudflare Clef read images)"),
223
+ .example('$0 decide "What color is this?" --provider xor --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR, Perplexity, Cloudflare Clef and OpenAI Decisions read images)"),
221
224
  handler: async (argv) => {
222
225
  const outputFormat = argv.format ?? "text";
223
226
  // --- Validate before any provider work ---------------------------------
@@ -13,7 +13,7 @@ import type { ProviderConfigOptions, SetupArgs } from "../../types/index.js";
13
13
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
14
14
  * provider but keeps its own handleMistralSetup() flow above, so it's
15
15
  * excluded here), spread directly from the catalog JSON via
16
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq) are
16
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions) are
17
17
  * spread the same way from getDecideCatalogEntries(). The hand rows listed
18
18
  * first reuse the existing createXConfig() factories in providerConfig.ts; the
19
19
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
@@ -150,7 +150,7 @@ const PROVIDERS = [
150
150
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
151
151
  * provider but keeps its own handleMistralSetup() flow above, so it's
152
152
  * excluded here), spread directly from the catalog JSON via
153
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq) are
153
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions) are
154
154
  * spread the same way from getDecideCatalogEntries(). The hand rows listed
155
155
  * first reuse the existing createXConfig() factories in providerConfig.ts; the
156
156
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
@@ -649,7 +649,7 @@ export class CLICommandFactory {
649
649
  },
650
650
  classifierStrategy: {
651
651
  type: "string",
652
- description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY, or CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, Laya, XOR, SubQ, Perplexity or Cloudflare Clef — with calibrated confidence).",
652
+ description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY, CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a decision model — whichever decision provider is configured: TypeSafe Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef or OpenAI Decisions — with the confidence it reports).",
653
653
  choices: ["auto", "heuristic", "llm", "jev"],
654
654
  alias: "classifier-strategy",
655
655
  },
@@ -73,6 +73,7 @@ export declare enum AIProviderName {
73
73
  NEBIUS = "nebius",
74
74
  NEURALWATT = "neuralwatt",
75
75
  NOVITA = "novita",
76
+ OPENAI_DECISIONS = "openai-decisions",
76
77
  OVHCLOUD = "ovhcloud",
77
78
  PARASAIL = "parasail",
78
79
  PARETO_INFERENCE = "pareto-inference",
@@ -1476,6 +1477,9 @@ export declare enum NovitaModels {
1476
1477
  QWEN_QWEN_2_5_72B_INSTRUCT = "qwen/qwen-2.5-72b-instruct",
1477
1478
  GOOGLE_GEMMA_3_27B_IT = "google/gemma-3-27b-it"
1478
1479
  }
1480
+ export declare enum OpenAIDecisionsModels {
1481
+ GPT_6_LUNA = "gpt-6-luna"
1482
+ }
1479
1483
  export declare enum OvhcloudModels {
1480
1484
  GPT_OSS_120B = "gpt-oss-120b",
1481
1485
  GPT_OSS_20B = "gpt-oss-20b",
@@ -78,6 +78,7 @@ export var AIProviderName;
78
78
  AIProviderName["NEBIUS"] = "nebius";
79
79
  AIProviderName["NEURALWATT"] = "neuralwatt";
80
80
  AIProviderName["NOVITA"] = "novita";
81
+ AIProviderName["OPENAI_DECISIONS"] = "openai-decisions";
81
82
  AIProviderName["OVHCLOUD"] = "ovhcloud";
82
83
  AIProviderName["PARASAIL"] = "parasail";
83
84
  AIProviderName["PARETO_INFERENCE"] = "pareto-inference";
@@ -1769,6 +1770,10 @@ export var NovitaModels;
1769
1770
  NovitaModels["QWEN_QWEN_2_5_72B_INSTRUCT"] = "qwen/qwen-2.5-72b-instruct";
1770
1771
  NovitaModels["GOOGLE_GEMMA_3_27B_IT"] = "google/gemma-3-27b-it";
1771
1772
  })(NovitaModels || (NovitaModels = {}));
1773
+ export var OpenAIDecisionsModels;
1774
+ (function (OpenAIDecisionsModels) {
1775
+ OpenAIDecisionsModels["GPT_6_LUNA"] = "gpt-6-luna";
1776
+ })(OpenAIDecisionsModels || (OpenAIDecisionsModels = {}));
1772
1777
  export var OvhcloudModels;
1773
1778
  (function (OvhcloudModels) {
1774
1779
  OvhcloudModels["GPT_OSS_120B"] = "gpt-oss-120b";
@@ -14,6 +14,13 @@ export declare class AIProviderFactory {
14
14
  * Prevents hanging on non-responsive endpoints
15
15
  */
16
16
  private static initializeDynamicProviderWithTimeout;
17
+ /**
18
+ * A provider whose descriptor declares `inferenceKinds` of exactly `["decide"]`
19
+ * names its own model (its registered default reads its own variable), so a
20
+ * chat provider's model variable has no claim on it. Exactly `["decide"]`: a
21
+ * provider that also serves text keeps its chat-model fallbacks.
22
+ */
23
+ private static isDecisionOnly;
17
24
  private static resolveModelFromEnvironment;
18
25
  private static resolveDynamicModelName;
19
26
  private static createResolvedProvider;
@@ -3,6 +3,7 @@
3
3
  import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
4
4
  import { tracers } from "../telemetry/tracers.js";
5
5
  import { ProviderFactory } from "../factories/providerFactory.js";
6
+ import { PROVIDER_ALIAS_INDEX, PROVIDER_DESCRIPTORS_BY_NAME, } from "../factories/providerDescriptors.js";
6
7
  import { ProviderRegistry } from "../factories/providerRegistry.js";
7
8
  import { getBestProvider } from "../utils/providerUtils.js";
8
9
  import { logger } from "../utils/logger.js";
@@ -47,11 +48,30 @@ export class AIProviderFactory {
47
48
  // This ensures the factory continues to work even if dynamic models fail
48
49
  }
49
50
  }
51
+ /**
52
+ * A provider whose descriptor declares `inferenceKinds` of exactly `["decide"]`
53
+ * names its own model (its registered default reads its own variable), so a
54
+ * chat provider's model variable has no claim on it. Exactly `["decide"]`: a
55
+ * provider that also serves text keeps its chat-model fallbacks.
56
+ */
57
+ static isDecisionOnly(providerName) {
58
+ const canonical = PROVIDER_ALIAS_INDEX.get(providerName.toLowerCase());
59
+ const kinds = canonical === undefined
60
+ ? undefined
61
+ : PROVIDER_DESCRIPTORS_BY_NAME.get(canonical)?.inferenceKinds;
62
+ return kinds?.length === 1 && kinds[0] === "decide";
63
+ }
50
64
  static resolveModelFromEnvironment(providerName, modelName, functionTag) {
51
65
  if (modelName && modelName !== "default") {
52
66
  logger.debug(`[${functionTag}] Skipping environment variable check - explicit model provided: ${modelName}`);
53
67
  return modelName;
54
68
  }
69
+ // The group below is chosen by `includes`, so "openai-decisions" would be
70
+ // handed OPENAI_MODEL, the chat provider's variable, ahead of its own.
71
+ if (this.isDecisionOnly(providerName)) {
72
+ logger.debug(`[${functionTag}] Skipping chat-model environment variables - ${providerName} serves decisions only and reads its own model setting`);
73
+ return modelName;
74
+ }
55
75
  logger.debug(`[${functionTag}] Checking environment variables for provider: ${providerName}`);
56
76
  const normalizedProvider = providerName.toLowerCase();
57
77
  const envConfigs = [
@@ -469,16 +469,18 @@ const HAND_DESCRIPTORS = [
469
469
  },
470
470
  // Decision precedence is decideOrder, not position: typesafe 1, laya 2 and
471
471
  // xor 3 (from catalog/laya.json and catalog/xor.json), subq 4 (from
472
- // catalog/subq.json), perplexity-decider 5, cloudflare-clef 6.
472
+ // catalog/subq.json), perplexity-decider 5, cloudflare-clef 6 and
473
+ // openai-decisions 7 (catalog/openai-decisions.json).
473
474
  // resolveDefaultDecisionProvider() returns the first configured
474
475
  // DECISION_PROVIDERS entry in that order, so a host configured for several
475
476
  // keeps Jev for every built-in consumer and reaches the others only by
476
477
  // naming them. Renumbering silently changes which model routes, compacts
477
478
  // and plans for every such host.
478
- // perplexity-decider and cloudflare-clef come last because each shares its
479
- // environment key with a text provider: a key set only for that text provider
480
- // must not displace a decision provider a host set up on purpose. subq is
481
- // ahead of them only because SUBQ_API_KEY belongs to no other provider.
479
+ // perplexity-decider, cloudflare-clef and openai-decisions come last because
480
+ // each shares its environment key with a text provider: a key set only for
481
+ // that text provider must not displace a decision provider a host set up on
482
+ // purpose. subq is ahead of them only because SUBQ_API_KEY belongs to no other
483
+ // provider.
482
484
  {
483
485
  name: AIProviderName.PERPLEXITY_DECIDER,
484
486
  aliases: [],
@@ -727,6 +729,19 @@ export function buildDecideDescriptor(entry) {
727
729
  defaultModel: entry.models.default,
728
730
  inferenceKinds: ["decide"],
729
731
  decideOrder: entry.decide.order,
732
+ ...(entry.decide.ambientKeyGuard
733
+ ? {
734
+ ambientKeyGuard: {
735
+ baseURLEnvVars: [...entry.decide.ambientKeyGuard.baseURLEnvVars],
736
+ trustedOrigins: [...entry.decide.ambientKeyGuard.trustedOrigins],
737
+ ...(entry.decide.ambientKeyGuard.credentialBaseURLs
738
+ ? {
739
+ credentialBaseURLs: entry.decide.ambientKeyGuard.credentialBaseURLs.map((credentialField) => ({ ...credentialField })),
740
+ }
741
+ : {}),
742
+ },
743
+ }
744
+ : {}),
730
745
  toolSupport: "none",
731
746
  localRuntime: false,
732
747
  healthCheck: "env-only",
@@ -796,11 +811,61 @@ assertUniqueDecideOrder(PROVIDER_DESCRIPTORS);
796
811
  * provider that declares nothing is a text provider, so it is not here.
797
812
  */
798
813
  export const DECISION_PROVIDERS = orderDecisionProviders(PROVIDER_DESCRIPTORS);
814
+ /**
815
+ * The scheme, host and port of a base URL, lower-cased and without the default
816
+ * port, or undefined when the text is not an http(s) URL. The path, query and a
817
+ * trailing slash do not matter, and userinfo is not part of an origin, so
818
+ * `https://api.openai.com@evil.test/v1` reads as `https://evil.test`.
819
+ */
820
+ function originOf(text) {
821
+ try {
822
+ const url = new URL(text.trim());
823
+ return url.protocol === "http:" || url.protocol === "https:"
824
+ ? url.origin
825
+ : undefined;
826
+ }
827
+ catch {
828
+ return undefined;
829
+ }
830
+ }
831
+ /**
832
+ * Whether the guard lets a key found in the environment stand. It does not
833
+ * when a listed base-URL variable, or a listed credentials field, is set to
834
+ * something other than a trusted origin; an unparseable value is untrusted, a
835
+ * blank or absent one is unset. A credentials field that is not text cannot be
836
+ * read as a URL, so it is untrusted too.
837
+ */
838
+ function ambientKeyTrusted(guard, credentials) {
839
+ const trusted = new Set(guard.trustedOrigins
840
+ .map(originOf)
841
+ .filter((origin) => origin !== undefined));
842
+ const standsFor = (value) => {
843
+ if (value === undefined || value === null) {
844
+ return true;
845
+ }
846
+ if (typeof value !== "string") {
847
+ return false;
848
+ }
849
+ if (value.trim() === "") {
850
+ return true;
851
+ }
852
+ const origin = originOf(value);
853
+ return origin !== undefined && trusted.has(origin);
854
+ };
855
+ const slices = credentials;
856
+ return (guard.baseURLEnvVars.every((name) => standsFor(process.env[name])) &&
857
+ (guard.credentialBaseURLs ?? []).every(({ key, field }) => standsFor(slices?.[key]?.[field])));
858
+ }
799
859
  /**
800
860
  * Whether a decision provider is fully configured: its key and every
801
861
  * extraRequired variable are set, each either in the environment or in
802
862
  * `credentials` — the same config a caller passes to
803
863
  * `new NeuroLink({ credentials })` or per call. Whitespace counts as unset.
864
+ *
865
+ * A key found only in the environment does not count while the descriptor's
866
+ * `ambientKeyGuard` finds that it belongs to something else, unless the host
867
+ * routed the provider on purpose with its own base-URL variable or a
868
+ * `credentials` base URL. A key in `credentials` always counts.
804
869
  */
805
870
  function isDecisionProviderConfigured(descriptor, credentials) {
806
871
  const isSet = (value) => typeof value === "string" && value.trim() !== "";
@@ -808,8 +873,13 @@ function isDecisionProviderConfigured(descriptor, credentials) {
808
873
  const slice = descriptor.credentialsKey
809
874
  ? credentials?.[descriptor.credentialsKey]
810
875
  : undefined;
876
+ const routedOnPurpose = inEnv(descriptor.envVars.baseURL) || isSet(slice?.baseURL);
877
+ const envKeyStands = descriptor.ambientKeyGuard === undefined ||
878
+ routedOnPurpose ||
879
+ ambientKeyTrusted(descriptor.ambientKeyGuard, credentials);
811
880
  // `gatewayApiKey` is TypeSafe's config form of AI_GATEWAY_API_KEY.
812
- const hasKey = [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv) ||
881
+ const hasKey = (envKeyStands &&
882
+ [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv)) ||
813
883
  isSet(slice?.apiKey) ||
814
884
  isSet(slice?.gatewayApiKey);
815
885
  // A required base URL can also come from credentials.<key>.baseURL, and any
@@ -845,6 +915,12 @@ export function listMediaDecisionProviders(needs) {
845
915
  return media !== undefined && (!needs?.video || media.video);
846
916
  }).map((descriptor) => descriptor.name);
847
917
  }
918
+ /** "a", "a or b", "a, b or c". */
919
+ function listOr(items) {
920
+ return items.length < 2
921
+ ? items.join("")
922
+ : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
923
+ }
848
924
  /**
849
925
  * The variables that would configure each decision provider, one clause per
850
926
  * provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
@@ -859,6 +935,19 @@ export function describeDecisionProviderKeys() {
859
935
  ...(descriptor.envVars.fallbacks ?? []),
860
936
  ].filter((name) => typeof name === "string" && name !== "");
861
937
  const required = descriptor.envVars.extraRequired ?? [];
862
- return `${[keys.join(" or "), ...required].join(" and ")} for ${descriptor.name}`;
938
+ const guard = descriptor.ambientKeyGuard;
939
+ const unlessRouted = descriptor.envVars.baseURL
940
+ ? `, unless ${descriptor.envVars.baseURL} is set`
941
+ : "";
942
+ const named = guard
943
+ ? [
944
+ ...guard.baseURLEnvVars,
945
+ ...(guard.credentialBaseURLs ?? []).map(({ key, field }) => `credentials.${key}.${field}`),
946
+ ]
947
+ : [];
948
+ const caveat = guard
949
+ ? ` (ignored while ${listOr(named)} names a host outside ${listOr(guard.trustedOrigins)}${unlessRouted})`
950
+ : "";
951
+ return `${[keys.join(" or "), ...required].join(" and ")} for ${descriptor.name}${caveat}`;
863
952
  }).join(", or ");
864
953
  }
@@ -2,4 +2,4 @@ import type { DecideCatalogJson, ProviderCatalogJson } from "../../types/index.j
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
3
  export declare const CATALOG_PROVIDER_IDS: readonly ["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", "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", "z-ai"];
4
4
  export declare const DECIDE_CATALOG_JSON_ENTRIES: DecideCatalogJson[];
5
- export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya", "subq", "xor"];
5
+ export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya", "openai-decisions", "subq", "xor"];
@@ -57,6 +57,7 @@ import naverClovaStudioJson from "./naver-clova-studio.json" with { type: "json"
57
57
  import nebiusJson from "./nebius.json" with { type: "json" };
58
58
  import neuralwattJson from "./neuralwatt.json" with { type: "json" };
59
59
  import novitaJson from "./novita.json" with { type: "json" };
60
+ import openaiDecisionsJson from "./openai-decisions.json" with { type: "json" };
60
61
  import ovhcloudJson from "./ovhcloud.json" with { type: "json" };
61
62
  import parasailJson from "./parasail.json" with { type: "json" };
62
63
  import paretoInferenceJson from "./pareto-inference.json" with { type: "json" };
@@ -278,7 +279,13 @@ export const CATALOG_PROVIDER_IDS = [
278
279
  ];
279
280
  export const DECIDE_CATALOG_JSON_ENTRIES = [
280
281
  layaJson,
282
+ openaiDecisionsJson,
281
283
  subqJson,
282
284
  xorJson,
283
285
  ].map(asDecideEntry);
284
- export const DECIDE_CATALOG_PROVIDER_IDS = ["laya", "subq", "xor"];
286
+ export const DECIDE_CATALOG_PROVIDER_IDS = [
287
+ "laya",
288
+ "openai-decisions",
289
+ "subq",
290
+ "xor",
291
+ ];
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "openai-decisions",
4
+ "displayName": "OpenAI Decisions",
5
+ "enumTypeName": "OpenAIDecisionsModels",
6
+ "aliases": [],
7
+ "inferenceKinds": ["decide"],
8
+ "wire": {
9
+ "baseURL": "https://api.openai.com",
10
+ "envOverrides": { "apiKey": "OPENAI_API_KEY" }
11
+ },
12
+ "models": {
13
+ "default": "gpt-6-luna",
14
+ "topModels": ["gpt-6-luna"],
15
+ "catalog": {
16
+ "gpt-6-luna": {
17
+ "contextWindow": 1050000,
18
+ "maxOutputTokens": 256,
19
+ "vision": true,
20
+ "status": "preview",
21
+ "description": "GPT-6 Luna on OpenAI's Decisions API (public beta): text and images"
22
+ }
23
+ }
24
+ },
25
+ "timeouts": { "decideMs": 10000 },
26
+ "decisionLimits": {
27
+ "maxStateTokens": 922000,
28
+ "advisory": true,
29
+ "media": { "maxImages": 8, "video": false, "maxRequestBytes": 33554432 }
30
+ },
31
+ "decide": {
32
+ "dialect": "openai-decisions",
33
+ "order": 7,
34
+ "baseURLNormalize": ["strip-trailing-slash", "strip-v1-suffix"],
35
+ "baseURLShape": "origin",
36
+ "ambientKeyGuard": {
37
+ "baseURLEnvVars": ["OPENAI_BASE_URL", "OPENAI_API_BASE"],
38
+ "trustedOrigins": ["https://api.openai.com"],
39
+ "credentialBaseURLs": [{ "key": "openai", "field": "baseURL" }]
40
+ },
41
+ "endpoint": "{baseURL}/v1/decisions",
42
+ "model": { "placement": "body" },
43
+ "requestIdHeaders": ["x-request-id"],
44
+ "errors": {
45
+ "rules": [
46
+ { "status": 401, "kind": "authentication" },
47
+ { "status": 429, "kind": "rate_limit", "retryable": false },
48
+ { "status": 503, "kind": "overloaded" },
49
+ { "statusMin": 500, "kind": "server" }
50
+ ],
51
+ "default": "invalid_request",
52
+ "bodyShapes": [{ "path": "error.message" }]
53
+ }
54
+ },
55
+ "setup": {
56
+ "url": "https://platform.openai.com/api-keys",
57
+ "apiKeyFormat": null,
58
+ "billingPolicy": "no-free-tier",
59
+ "description": "OpenAI API key (the same OPENAI_API_KEY the openai chat provider reads)",
60
+ "instructions": [
61
+ "1. Create an API key at https://platform.openai.com/api-keys",
62
+ "2. Set {apiKeyEnvVar}; it is shared with the openai chat provider",
63
+ "3. With no other decision provider configured, {apiKeyEnvVar} alone makes openai-decisions the default decision provider (the last resort, after every other one, as PERPLEXITY_API_KEY does for perplexity-decider), unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com: the key is then taken to be a gateway's and is not used by default. To use it while another provider is configured, name it in a call (provider: \"openai-decisions\" or --provider openai-decisions), but only if the key is OpenAI's, because a named call sends the environment key to api.openai.com. Behind a gateway, route it on purpose with OPENAI_DECISIONS_BASE_URL or credentials.openaiDecisions, or do not use it",
64
+ "4. Optional: OPENAI_DECISIONS_BASE_URL (an origin) and OPENAI_DECISIONS_MODEL (default gpt-6-luna)"
65
+ ]
66
+ },
67
+ "evidence": {
68
+ "rosterVerified": {
69
+ "date": "2026-10-07",
70
+ "method": "OpenAI's Decisions guide and gpt-6-luna model page saved 2026-10-07: gpt-6-luna is the only model the endpoint supports. Read from documentation; no live call"
71
+ },
72
+ "liveMatrix": null,
73
+ "addedInPR": "https://github.com/juspay/neurolink/pull/1996"
74
+ }
75
+ }
@@ -64,7 +64,7 @@
64
64
  "type": "object",
65
65
  "additionalProperties": false,
66
66
  "required": ["maxImages", "video", "maxRequestBytes"],
67
- "description": "What a request may carry besides text. Requires decide.media, which names the body keys the prepared media is written under.",
67
+ "description": "What a request may carry besides text. With dialect system-one it requires decide.media, which names the body keys the prepared media is written under; with openai-decisions it stands alone and video must be false.",
68
68
  "properties": {
69
69
  "maxImages": { "type": "integer", "minimum": 1 },
70
70
  "video": {
@@ -102,7 +102,10 @@
102
102
  "required": ["dialect", "order", "endpoint", "model", "errors"],
103
103
  "description": "Decide entries only: how a decision provider is called and how its errors are classified.",
104
104
  "properties": {
105
- "dialect": { "enum": ["system-one"] },
105
+ "dialect": {
106
+ "enum": ["system-one", "openai-decisions"],
107
+ "description": "The wire family. openai-decisions places images in the request input itself, so it refuses decide.media, a video allowance and any model placement other than body (enforced by the zod schema)."
108
+ },
106
109
  "order": {
107
110
  "type": "integer",
108
111
  "minimum": 1,
@@ -143,12 +146,62 @@
143
146
  "type": "object",
144
147
  "additionalProperties": false,
145
148
  "required": ["imagesKey"],
146
- "description": "Body keys prepared media is written under, after model, state and questions: imagesKey only when the request carries an image, videoKey only when it carries a video, and the two differ. Requires decisionLimits.media; videoKey is required exactly when that allowance's video is true.",
149
+ "description": "Body keys prepared media is written under, after model, state and questions: imagesKey only when the request carries an image, videoKey only when it carries a video, and the two differ. With dialect system-one it requires decisionLimits.media, and videoKey is required exactly when that allowance's video is true. Not valid with openai-decisions.",
147
150
  "properties": {
148
151
  "imagesKey": { "$ref": "#/definitions/decideMediaKey" },
149
152
  "videoKey": { "$ref": "#/definitions/decideMediaKey" }
150
153
  }
151
154
  },
155
+ "ambientKeyGuard": {
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "required": ["baseURLEnvVars", "trustedOrigins"],
159
+ "description": "Keeps a key found only in the environment from becoming the default decision provider while another host is named. The key is skipped for default resolution when any baseURLEnvVars variable, or any credentials field in credentialBaseURLs (read from the credentials the resolver is given: the instance's merged with the call's), holds a URL whose origin (scheme, host and port, compared case-insensitively; path and trailing slash ignored) is not in trustedOrigins; a value that does not parse as an http(s) URL counts as untrusted and a blank one as unset. It does not apply when the provider's own base-URL variable is set or credentials supply a key or a base URL, and never to a call that names the provider. The zod schema also refuses what JSON Schema cannot say: an origin without a valid host (such as 999.999.999.999) or with a port above 65535, a variable, an origin or a credentials field listed twice, and the provider's own base-URL variable in baseURLEnvVars.",
160
+ "properties": {
161
+ "baseURLEnvVars": {
162
+ "type": "array",
163
+ "minItems": 1,
164
+ "maxItems": 8,
165
+ "uniqueItems": true,
166
+ "items": {
167
+ "type": "string",
168
+ "maxLength": 64,
169
+ "pattern": "^[A-Z][A-Z0-9_]*$"
170
+ }
171
+ },
172
+ "trustedOrigins": {
173
+ "type": "array",
174
+ "minItems": 1,
175
+ "maxItems": 8,
176
+ "uniqueItems": true,
177
+ "items": {
178
+ "type": "string",
179
+ "maxLength": 100,
180
+ "pattern": "^https?://[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:[0-9]{1,5})?$"
181
+ }
182
+ },
183
+ "credentialBaseURLs": {
184
+ "type": "array",
185
+ "minItems": 1,
186
+ "maxItems": 8,
187
+ "uniqueItems": true,
188
+ "description": "Credentials fields that move the key's other users off the vendor's host, such as the chat provider's credentials.openai.baseURL. Omitted means only the environment is read.",
189
+ "items": {
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": ["key", "field"],
193
+ "properties": {
194
+ "key": {
195
+ "type": "string",
196
+ "maxLength": 64,
197
+ "pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
198
+ },
199
+ "field": { "enum": ["baseURL"] }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ },
152
205
  "requestIdHeaders": {
153
206
  "type": "array",
154
207
  "items": {