@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
@@ -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) 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) 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, 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, 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",
@@ -89,6 +90,7 @@ export declare enum AIProviderName {
89
90
  STACKIT = "stackit",
90
91
  STEPFUN = "stepfun",
91
92
  SUBCONSCIOUS = "subconscious",
93
+ SUBQ = "subq",
92
94
  SYNTHETIC = "synthetic",
93
95
  TELNYX = "telnyx",
94
96
  THINKING_MACHINES = "thinking-machines",
@@ -1475,6 +1477,9 @@ export declare enum NovitaModels {
1475
1477
  QWEN_QWEN_2_5_72B_INSTRUCT = "qwen/qwen-2.5-72b-instruct",
1476
1478
  GOOGLE_GEMMA_3_27B_IT = "google/gemma-3-27b-it"
1477
1479
  }
1480
+ export declare enum OpenAIDecisionsModels {
1481
+ GPT_6_LUNA = "gpt-6-luna"
1482
+ }
1478
1483
  export declare enum OvhcloudModels {
1479
1484
  GPT_OSS_120B = "gpt-oss-120b",
1480
1485
  GPT_OSS_20B = "gpt-oss-20b",
@@ -1611,6 +1616,9 @@ export declare enum SubconsciousModels {
1611
1616
  SUBCONSCIOUS_DEEPSEEK_V4_1_FLASH_MARATHON = "subconscious/deepseek-v4.1-flash-marathon",
1612
1617
  SUBCONSCIOUS_TIM_QWEN3_6_27B = "subconscious/tim-qwen3.6-27b"
1613
1618
  }
1619
+ export declare enum SubqModels {
1620
+ SUBQ_SNAP_PREVIEW = "subq-snap-preview"
1621
+ }
1614
1622
  export declare enum SyntheticModels {
1615
1623
  SYN_LARGE_TEXT = "syn:large:text",
1616
1624
  SYN_SMALL_TEXT = "syn:small:text",
@@ -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";
@@ -94,6 +95,7 @@ export var AIProviderName;
94
95
  AIProviderName["STACKIT"] = "stackit";
95
96
  AIProviderName["STEPFUN"] = "stepfun";
96
97
  AIProviderName["SUBCONSCIOUS"] = "subconscious";
98
+ AIProviderName["SUBQ"] = "subq";
97
99
  AIProviderName["SYNTHETIC"] = "synthetic";
98
100
  AIProviderName["TELNYX"] = "telnyx";
99
101
  AIProviderName["THINKING_MACHINES"] = "thinking-machines";
@@ -1768,6 +1770,10 @@ export var NovitaModels;
1768
1770
  NovitaModels["QWEN_QWEN_2_5_72B_INSTRUCT"] = "qwen/qwen-2.5-72b-instruct";
1769
1771
  NovitaModels["GOOGLE_GEMMA_3_27B_IT"] = "google/gemma-3-27b-it";
1770
1772
  })(NovitaModels || (NovitaModels = {}));
1773
+ export var OpenAIDecisionsModels;
1774
+ (function (OpenAIDecisionsModels) {
1775
+ OpenAIDecisionsModels["GPT_6_LUNA"] = "gpt-6-luna";
1776
+ })(OpenAIDecisionsModels || (OpenAIDecisionsModels = {}));
1771
1777
  export var OvhcloudModels;
1772
1778
  (function (OvhcloudModels) {
1773
1779
  OvhcloudModels["GPT_OSS_120B"] = "gpt-oss-120b";
@@ -1920,6 +1926,10 @@ export var SubconsciousModels;
1920
1926
  SubconsciousModels["SUBCONSCIOUS_DEEPSEEK_V4_1_FLASH_MARATHON"] = "subconscious/deepseek-v4.1-flash-marathon";
1921
1927
  SubconsciousModels["SUBCONSCIOUS_TIM_QWEN3_6_27B"] = "subconscious/tim-qwen3.6-27b";
1922
1928
  })(SubconsciousModels || (SubconsciousModels = {}));
1929
+ export var SubqModels;
1930
+ (function (SubqModels) {
1931
+ SubqModels["SUBQ_SNAP_PREVIEW"] = "subq-snap-preview";
1932
+ })(SubqModels || (SubqModels = {}));
1923
1933
  export var SyntheticModels;
1924
1934
  (function (SyntheticModels) {
1925
1935
  SyntheticModels["SYN_LARGE_TEXT"] = "syn:large:text";
@@ -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 = [
@@ -468,22 +468,29 @@ const HAND_DESCRIPTORS = [
468
468
  setupUrl: "https://console.typesafe.ai/keys",
469
469
  },
470
470
  // Decision precedence is decideOrder, not position: typesafe 1, laya 2 and
471
- // xor 3 (from catalog/laya.json and catalog/xor.json), perplexity-decider 4,
472
- // cloudflare-clef 5.
471
+ // xor 3 (from catalog/laya.json and catalog/xor.json), subq 4 (from
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.
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.
478
484
  {
479
485
  name: AIProviderName.PERPLEXITY_DECIDER,
480
486
  aliases: [],
481
487
  credentialsKey: "perplexityDecider",
482
488
  envVars: {
483
489
  // The same key the `perplexity` text provider reads, so an ambient
484
- // PERPLEXITY_API_KEY configures this provider too. Its decideOrder (4)
485
- // sits after XOR's (3), which is what keeps that from displacing any other
486
- // decision provider a host has configured.
490
+ // PERPLEXITY_API_KEY configures this provider too. Its decideOrder (5)
491
+ // sits after XOR's (3) and SubQ's (4), which have keys of their own, which
492
+ // is what keeps that from displacing any other decision provider a host
493
+ // has configured.
487
494
  apiKey: "PERPLEXITY_API_KEY",
488
495
  // Optional: the API is hosted at a public endpoint, so a key alone is
489
496
  // enough and there is no extraRequired.
@@ -495,7 +502,7 @@ const HAND_DESCRIPTORS = [
495
502
  // declaration that keeps it out of every generation code path, and out of
496
503
  // the way of the `perplexity` text provider.
497
504
  inferenceKinds: ["decide"],
498
- decideOrder: 4,
505
+ decideOrder: 5,
499
506
  toolSupport: "none",
500
507
  localRuntime: false,
501
508
  healthCheck: "env-only",
@@ -563,7 +570,7 @@ const HAND_DESCRIPTORS = [
563
570
  // declaration that keeps it out of every generation code path, and out of
564
571
  // the way of the `cloudflare` text provider.
565
572
  inferenceKinds: ["decide"],
566
- decideOrder: 5,
573
+ decideOrder: 6,
567
574
  toolSupport: "none",
568
575
  localRuntime: false,
569
576
  healthCheck: "env-only",
@@ -722,6 +729,19 @@ export function buildDecideDescriptor(entry) {
722
729
  defaultModel: entry.models.default,
723
730
  inferenceKinds: ["decide"],
724
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
+ : {}),
725
745
  toolSupport: "none",
726
746
  localRuntime: false,
727
747
  healthCheck: "env-only",
@@ -791,11 +811,61 @@ assertUniqueDecideOrder(PROVIDER_DESCRIPTORS);
791
811
  * provider that declares nothing is a text provider, so it is not here.
792
812
  */
793
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
+ }
794
859
  /**
795
860
  * Whether a decision provider is fully configured: its key and every
796
861
  * extraRequired variable are set, each either in the environment or in
797
862
  * `credentials` — the same config a caller passes to
798
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.
799
869
  */
800
870
  function isDecisionProviderConfigured(descriptor, credentials) {
801
871
  const isSet = (value) => typeof value === "string" && value.trim() !== "";
@@ -803,8 +873,13 @@ function isDecisionProviderConfigured(descriptor, credentials) {
803
873
  const slice = descriptor.credentialsKey
804
874
  ? credentials?.[descriptor.credentialsKey]
805
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);
806
880
  // `gatewayApiKey` is TypeSafe's config form of AI_GATEWAY_API_KEY.
807
- const hasKey = [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv) ||
881
+ const hasKey = (envKeyStands &&
882
+ [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv)) ||
808
883
  isSet(slice?.apiKey) ||
809
884
  isSet(slice?.gatewayApiKey);
810
885
  // A required base URL can also come from credentials.<key>.baseURL, and any
@@ -840,6 +915,12 @@ export function listMediaDecisionProviders(needs) {
840
915
  return media !== undefined && (!needs?.video || media.video);
841
916
  }).map((descriptor) => descriptor.name);
842
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
+ }
843
924
  /**
844
925
  * The variables that would configure each decision provider, one clause per
845
926
  * provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
@@ -854,6 +935,19 @@ export function describeDecisionProviderKeys() {
854
935
  ...(descriptor.envVars.fallbacks ?? []),
855
936
  ].filter((name) => typeof name === "string" && name !== "");
856
937
  const required = descriptor.envVars.extraRequired ?? [];
857
- 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}`;
858
952
  }).join(", or ");
859
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", "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" };
@@ -73,6 +74,7 @@ import siliconflowJson from "./siliconflow.json" with { type: "json" };
73
74
  import stackitJson from "./stackit.json" with { type: "json" };
74
75
  import stepfunJson from "./stepfun.json" with { type: "json" };
75
76
  import subconsciousJson from "./subconscious.json" with { type: "json" };
77
+ import subqJson from "./subq.json" with { type: "json" };
76
78
  import syntheticJson from "./synthetic.json" with { type: "json" };
77
79
  import telnyxJson from "./telnyx.json" with { type: "json" };
78
80
  import thinkingMachinesJson from "./thinking-machines.json" with { type: "json" };
@@ -277,6 +279,13 @@ export const CATALOG_PROVIDER_IDS = [
277
279
  ];
278
280
  export const DECIDE_CATALOG_JSON_ENTRIES = [
279
281
  layaJson,
282
+ openaiDecisionsJson,
283
+ subqJson,
280
284
  xorJson,
281
285
  ].map(asDecideEntry);
282
- export const DECIDE_CATALOG_PROVIDER_IDS = ["laya", "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": {