@juspay/neurolink 12.46.1 → 12.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +75 -50
  3. package/dist/adapters/video/ffmpegAdapter.d.ts +3 -1
  4. package/dist/adapters/video/ffmpegAdapter.js +13 -2
  5. package/dist/browser/neurolink.min.js +421 -421
  6. package/dist/cli/commands/decide.js +2 -2
  7. package/dist/cli/commands/setup.js +8 -6
  8. package/dist/cli/factories/commandFactory.js +1 -1
  9. package/dist/cli/proxy-clients/copilot.js +55 -1
  10. package/dist/constants/contextWindows.js +12 -0
  11. package/dist/constants/enums.d.ts +16 -0
  12. package/dist/constants/enums.js +17 -0
  13. package/dist/core/toolExecutionGuards.d.ts +1 -9
  14. package/dist/core/toolExecutionGuards.js +1 -9
  15. package/dist/factories/providerDescriptors.js +84 -5
  16. package/dist/factories/providerFactory.js +16 -1
  17. package/dist/factories/providerRegistry.js +10 -1
  18. package/dist/files/fileReferenceRegistry.js +3 -3
  19. package/dist/models/manifestRegistry.js +2 -0
  20. package/dist/models/manifests/cloudflareClef.d.ts +16 -0
  21. package/dist/models/manifests/cloudflareClef.js +42 -0
  22. package/dist/neurolink.js +13 -2
  23. package/dist/processors/media/VideoProcessor.js +16 -4
  24. package/dist/providers/cloudflareClef.d.ts +52 -0
  25. package/dist/providers/cloudflareClef.js +331 -0
  26. package/dist/providers/googleNativeGemini3/utils.d.ts +9 -0
  27. package/dist/providers/googleNativeGemini3/utils.js +9 -0
  28. package/dist/providers/systemOneDecision.d.ts +12 -1
  29. package/dist/providers/systemOneDecision.js +70 -18
  30. package/dist/types/decision.d.ts +22 -0
  31. package/dist/types/providers.d.ts +15 -0
  32. package/dist/utils/modelChoices.js +13 -1
  33. package/dist/utils/pricing.js +12 -0
  34. package/dist/utils/providerConfig.d.ts +7 -0
  35. package/dist/utils/providerConfig.js +19 -0
  36. package/docs-site/static/search-index.json +78 -56
  37. package/package.json +2 -1
@@ -191,7 +191,7 @@ export const decideCommand = {
191
191
  type: "string",
192
192
  array: true,
193
193
  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)",
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)",
195
195
  })
196
196
  .option("video", {
197
197
  type: "string",
@@ -217,7 +217,7 @@ export const decideCommand = {
217
217
  })
218
218
  .example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
219
219
  .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 and Perplexity read images)"),
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)"),
221
221
  handler: async (argv) => {
222
222
  const outputFormat = argv.format ?? "text";
223
223
  // --- Validate before any provider work ---------------------------------
@@ -19,8 +19,9 @@ import { handleBedrockSetup } from "./setup-bedrock.js";
19
19
  import { handleGCPSetup } from "./setup-gcp.js";
20
20
  import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
+ import { OpenRouterModels, } from "../../constants/enums.js";
22
23
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
23
- import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, createXorConfig, createPerplexityDeciderConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
24
+ import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, createXorConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
24
25
  import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
25
26
  // Provider information database
26
27
  const PROVIDERS = [
@@ -172,6 +173,7 @@ export const EXTRA_PROVIDER_CONFIGS = {
172
173
  laya: createLayaConfig(),
173
174
  xor: createXorConfig(),
174
175
  "perplexity-decider": createPerplexityDeciderConfig(),
176
+ "cloudflare-clef": createCloudflareClefConfig(),
175
177
  ...Object.fromEntries(getCatalogJsonEntries()
176
178
  .filter((e) => e.id !== "mistral")
177
179
  .map((e) => [e.id, buildCatalogConfigOptions(e)])),
@@ -570,13 +572,13 @@ async function handleOpenRouterSetup() {
570
572
  logger.always(chalk.cyan(" export OPENROUTER_API_KEY=your_api_key_here"));
571
573
  logger.always("");
572
574
  logger.always(chalk.yellow("Step 3: Test the configuration"));
573
- logger.always(chalk.cyan(' neurolink generate "Hello!" --provider openrouter --model google/gemini-2.0-flash-exp:free'));
575
+ logger.always(chalk.cyan(` neurolink generate "Hello!" --provider openrouter --model ${OpenRouterModels.GEMINI_2_5_FLASH}`));
574
576
  logger.always("");
575
577
  logger.always(chalk.green("Available models include:"));
576
- logger.always(" • anthropic/claude-3.5-sonnet - Best for analysis");
577
- logger.always(" • openai/gpt-4o - Industry standard");
578
- logger.always(" • google/gemini-2.0-flash-exp:free - Free tier");
579
- logger.always(" • meta-llama/llama-3.1-70b-instruct - Open source");
578
+ logger.always(` • ${OpenRouterModels.CLAUDE_SONNET_4_6} - Best for analysis`);
579
+ logger.always(` • ${OpenRouterModels.GPT_4O} - Industry standard`);
580
+ logger.always(` • ${OpenRouterModels.GEMINI_2_5_FLASH} - Fast and efficient`);
581
+ logger.always(` • ${OpenRouterModels.LLAMA_3_1_70B} - Open source`);
580
582
  logger.always("");
581
583
  logger.always(chalk.gray("See all models at: https://openrouter.ai/models"));
582
584
  }
@@ -616,7 +616,7 @@ export class CLICommandFactory {
616
616
  },
617
617
  classifierStrategy: {
618
618
  type: "string",
619
- 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, or PERPLEXITY_API_KEY, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, Laya, XOR or Perplexity — with calibrated confidence).",
619
+ 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).",
620
620
  choices: ["auto", "heuristic", "llm", "jev"],
621
621
  alias: "classifier-strategy",
622
622
  },
@@ -46,12 +46,66 @@ function getCopilotEnvPath() {
46
46
  function getProfileCandidates() {
47
47
  return [".zshrc", ".zprofile", ".bashrc", ".bash_profile", ".profile"].map((name) => join(homedir(), name));
48
48
  }
49
+ /** A command that is `source`/`.` of a path ending in exactly `copilot-env.sh`, optionally after `then`/`else`/`do`. */
50
+ const ENV_SCRIPT_SOURCE_COMMAND = /^\s*(?:(?:then|else|do)\s+)?(?:source|\.)\s+(["']?)(?:[^\s"']*\/)?copilot-env\.sh\1(?:\s|$)/;
51
+ /**
52
+ * One profile line as the commands it runs, comment dropped. Quote-aware, so a
53
+ * `;` or `#` inside quotes neither splits nor starts a comment. Heredocs,
54
+ * multi-line strings and line continuations are not modelled, so a source line
55
+ * inside one is still counted, and a source built with `eval` is missed. Both
56
+ * are rare; this guards against the common mentions (a comment, an `echo`, a
57
+ * file-existence test, a `.bak` name) that suppressed the note before.
58
+ */
59
+ function splitProfileCommands(line) {
60
+ const commands = [];
61
+ let current = "";
62
+ let quote = null;
63
+ for (let i = 0; i < line.length; i++) {
64
+ const char = line[i];
65
+ if (quote) {
66
+ current += char;
67
+ if (quote === '"' && char === "\\") {
68
+ current += line[i + 1] ?? "";
69
+ i++;
70
+ }
71
+ else if (char === quote) {
72
+ quote = null;
73
+ }
74
+ continue;
75
+ }
76
+ if (char === "\\") {
77
+ current += char + (line[i + 1] ?? "");
78
+ i++;
79
+ continue;
80
+ }
81
+ if (char === '"' || char === "'") {
82
+ quote = char;
83
+ current += char;
84
+ continue;
85
+ }
86
+ if (char === "#" &&
87
+ (current === "" || /\s/.test(current[current.length - 1]))) {
88
+ break;
89
+ }
90
+ if (char === ";" || char === "&" || char === "|") {
91
+ commands.push(current);
92
+ current = "";
93
+ continue;
94
+ }
95
+ current += char;
96
+ }
97
+ commands.push(current);
98
+ return commands;
99
+ }
49
100
  /** Whether any shell profile already sources the generated script. */
50
101
  async function isEnvScriptSourced() {
51
102
  const fs = await import("fs");
52
103
  for (const profile of getProfileCandidates()) {
53
104
  try {
54
- if (fs.readFileSync(profile, "utf8").includes("copilot-env.sh")) {
105
+ if (fs
106
+ .readFileSync(profile, "utf8")
107
+ .split(/\r?\n/)
108
+ .some((line) => splitProfileCommands(line).some((command) => ENV_SCRIPT_SOURCE_COMMAND.test(command)))) {
55
109
  return true;
56
110
  }
57
111
  }
@@ -122,6 +122,11 @@ export const MODEL_CONTEXT_WINDOWS = {
122
122
  "claude-fable-5-1": 1_000_000,
123
123
  // Claude 5 (mid 2026) — 1M context window
124
124
  "claude-sonnet-5": 1_000_000,
125
+ "claude-opus-5": 1_000_000,
126
+ "claude-fable-5": 1_000_000,
127
+ // Claude 4.7 / 4.8 — 1M context window (platform.claude.com/docs/en/models)
128
+ "claude-opus-4-8": 1_000_000,
129
+ "claude-opus-4-7": 1_000_000,
125
130
  // Claude 4.6 (Feb 2026) — 1M context window
126
131
  "claude-opus-4-6": 1_000_000,
127
132
  "claude-sonnet-4-6": 1_000_000,
@@ -220,6 +225,13 @@ export const MODEL_CONTEXT_WINDOWS = {
220
225
  "claude-sonnet-5-5": 1_000_000,
221
226
  "claude-fable-5-1": 1_000_000,
222
227
  "claude-sonnet-5": 1_000_000,
228
+ // Placed before the 200K `claude-opus-4` prefix key below: the lookup takes
229
+ // the first prefix that matches, so a dated `claude-opus-4-8@...` id would
230
+ // otherwise land on 200K.
231
+ "claude-opus-5": 1_000_000,
232
+ "claude-fable-5": 1_000_000,
233
+ "claude-opus-4-8": 1_000_000,
234
+ "claude-opus-4-7": 1_000_000,
223
235
  "claude-opus-4-6": 1_000_000,
224
236
  "claude-sonnet-4-6": 1_000_000,
225
237
  "claude-sonnet-4-5": 200_000,
@@ -115,6 +115,12 @@ export declare enum AIProviderName {
115
115
  * inference type only. Distinct from `PERPLEXITY`, the Sonar text provider.
116
116
  */
117
117
  PERPLEXITY_DECIDER = "perplexity-decider",
118
+ /**
119
+ * Cloudflare Clef (`@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`) on
120
+ * Workers AI — serves the `decide` inference type only. Distinct from
121
+ * `CLOUDFLARE`, the Workers AI text provider.
122
+ */
123
+ CLOUDFLARE_CLEF = "cloudflare-clef",
118
124
  AUTO = "auto"
119
125
  }
120
126
  /**
@@ -1781,3 +1787,13 @@ export declare enum XorModels {
1781
1787
  export declare enum PerplexityDeciderModels {
1782
1788
  PPLX_DECIDER_V1_27B = "pplx-decider-v1-27b"
1783
1789
  }
1790
+ /**
1791
+ * Cloudflare Clef decision models, named as the Workers AI API names them in
1792
+ * the request body (the path adds the `@cf/cloudflare/` prefix). Hand-written:
1793
+ * Clef is a Tier-3 provider, so it is not in the provider catalog and codegen
1794
+ * never touches this. `CLEF` is the 27B model, `CLEF_FLASH` the 9B one.
1795
+ */
1796
+ export declare enum CloudflareClefModels {
1797
+ CLEF = "clef",
1798
+ CLEF_FLASH = "clef-flash"
1799
+ }
@@ -121,6 +121,12 @@ export var AIProviderName;
121
121
  * inference type only. Distinct from `PERPLEXITY`, the Sonar text provider.
122
122
  */
123
123
  AIProviderName["PERPLEXITY_DECIDER"] = "perplexity-decider";
124
+ /**
125
+ * Cloudflare Clef (`@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`) on
126
+ * Workers AI — serves the `decide` inference type only. Distinct from
127
+ * `CLOUDFLARE`, the Workers AI text provider.
128
+ */
129
+ AIProviderName["CLOUDFLARE_CLEF"] = "cloudflare-clef";
124
130
  AIProviderName["AUTO"] = "auto";
125
131
  })(AIProviderName || (AIProviderName = {}));
126
132
  /**
@@ -2103,3 +2109,14 @@ export var PerplexityDeciderModels;
2103
2109
  (function (PerplexityDeciderModels) {
2104
2110
  PerplexityDeciderModels["PPLX_DECIDER_V1_27B"] = "pplx-decider-v1-27b";
2105
2111
  })(PerplexityDeciderModels || (PerplexityDeciderModels = {}));
2112
+ /**
2113
+ * Cloudflare Clef decision models, named as the Workers AI API names them in
2114
+ * the request body (the path adds the `@cf/cloudflare/` prefix). Hand-written:
2115
+ * Clef is a Tier-3 provider, so it is not in the provider catalog and codegen
2116
+ * never touches this. `CLEF` is the 27B model, `CLEF_FLASH` the 9B one.
2117
+ */
2118
+ export var CloudflareClefModels;
2119
+ (function (CloudflareClefModels) {
2120
+ CloudflareClefModels["CLEF"] = "clef";
2121
+ CloudflareClefModels["CLEF_FLASH"] = "clef-flash";
2122
+ })(CloudflareClefModels || (CloudflareClefModels = {}));
@@ -13,15 +13,7 @@
13
13
  */
14
14
  import type { Tool, ToolExecutionGuards } from "../types/index.js";
15
15
  /**
16
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
- * discovered tools into the live record between steps; without this refresh
19
- * they stay invisible to the rest of the turn and every call dies as
20
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
- * `toolsConfig` by reference — and returns true when anything was added.
22
- */
23
- /**
24
- * Everything a native Gemini loop wraps around a tool call that the shared
16
+ * Everything a native provider loop wraps around a tool call that the shared
25
17
  * engine does not do itself.
26
18
  *
27
19
  * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
@@ -13,15 +13,7 @@
13
13
  */
14
14
  import { raceWithAbort, withTimeout } from "../utils/async/index.js";
15
15
  /**
16
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
- * discovered tools into the live record between steps; without this refresh
19
- * they stay invisible to the rest of the turn and every call dies as
20
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
- * `toolsConfig` by reference — and returns true when anything was added.
22
- */
23
- /**
24
- * Everything a native Gemini loop wraps around a tool call that the shared
16
+ * Everything a native provider loop wraps around a tool call that the shared
25
17
  * engine does not do itself.
26
18
  *
27
19
  * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
@@ -1,5 +1,5 @@
1
1
  import { AIProviderName } from "../constants/enums.js";
2
- import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
2
+ import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
3
3
  import { API_KEY_FORMATS } from "../utils/providerConfig.js";
4
4
  import { getCatalogJsonEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
5
5
  import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
@@ -451,7 +451,8 @@ const HAND_DESCRIPTORS = [
451
451
  timeouts: { decideMs: 5000 },
452
452
  setupUrl: "https://console.typesafe.ai/keys",
453
453
  },
454
- // Laya MUST stay after TypeSafe, XOR after Laya, and Perplexity after XOR.
454
+ // Laya MUST stay after TypeSafe, XOR after Laya, Perplexity after XOR, and
455
+ // Cloudflare Clef after Perplexity.
455
456
  // resolveDefaultDecisionProvider() returns the first configured
456
457
  // DECISION_PROVIDERS entry, in this order, so a host configured for several
457
458
  // keeps Jev for every built-in consumer and reaches the others only by
@@ -606,6 +607,79 @@ const HAND_DESCRIPTORS = [
606
607
  },
607
608
  setupUrl: "https://console.perplexity.ai",
608
609
  },
610
+ {
611
+ name: AIProviderName.CLOUDFLARE_CLEF,
612
+ aliases: [],
613
+ credentialsKey: "cloudflareClef",
614
+ envVars: {
615
+ // The same token and account id the `cloudflare` text provider reads, so
616
+ // ambient Workers AI settings configure this provider too. It sits last
617
+ // in this list, which is what keeps that from displacing any other
618
+ // decision provider a host has configured.
619
+ apiKey: "CLOUDFLARE_API_KEY",
620
+ // Cloudflare's own API is the endpoint, so a base URL is optional; but
621
+ // the path carries the account id, so a token alone is useless. Both are
622
+ // required for it to count as configured, from the environment or from
623
+ // credentials.cloudflareClef.
624
+ extraRequired: ["CLOUDFLARE_ACCOUNT_ID"],
625
+ extraRequiredCredentialFields: { CLOUDFLARE_ACCOUNT_ID: "accountId" },
626
+ baseURL: "CLOUDFLARE_CLEF_BASE_URL",
627
+ model: "CLOUDFLARE_CLEF_MODEL",
628
+ },
629
+ defaultModel: CloudflareClefModels.CLEF,
630
+ // Serves only `decide`, like the other decision providers — the one
631
+ // declaration that keeps it out of every generation code path, and out of
632
+ // the way of the `cloudflare` text provider.
633
+ inferenceKinds: ["decide"],
634
+ toolSupport: "none",
635
+ localRuntime: false,
636
+ healthCheck: "env-only",
637
+ // Deliberately NO autoSelectPriority / autoSelectPreference /
638
+ // defaultHealthSweepPriority, for the same reason as TypeSafe above.
639
+ //
640
+ // Measured 2026-10-03: 0.3 to 1.0 s for a small request, 1.1 s for 64 questions on
641
+ // clef-flash and 1.3 s on clef, and 2.2 s at most for any of 60 requests
642
+ // sent at once. On 2026-10-04, 64 questions took 1.5 s (flash) / 2.3 s (clef).
643
+ // 5s covers those observations and keeps a fail-open consumer
644
+ // from waiting on a stuck call.
645
+ timeouts: { decideMs: 5_000 },
646
+ // The Workers AI endpoint ignores state text past about 2,048 tokens
647
+ // (hosted service or model: unknown), despite the documented 64K. The local
648
+ // 1,500-token estimate refuses before every measured cut. On 2026-10-04,
649
+ // both models read facts at the original clef-flash lower bounds for logs,
650
+ // number lists, digit arrays and compact JSON, but not about 2.5% further on;
651
+ // English prose and random CJK already matched on both. Digits cost 1,
652
+ // ASCII symbols 0.75, BMP non-ASCII 1.5 and astral characters 3 tokens.
653
+ // Natural Chinese, Japanese, Korean and Hindi prose and emoji-rich English
654
+ // were also safe under those rates on clef. A many-key object cut between
655
+ // 128 and 134 preceding keys on both models: its lower bound was 4,883
656
+ // compact-JSON characters, estimated at 2,299 tokens. The probe used an
657
+ // explicit field-name question and zero-padded keys together after the
658
+ // control failed three times. It then passed; necessity was not established.
659
+ // Other object shapes and Unicode sequences may differ.
660
+ decisionLimits: {
661
+ maxStateTokens: 1_500,
662
+ maxQuestions: 64,
663
+ nonAsciiTokensPerChar: 1.5,
664
+ digitTokensPerChar: 1,
665
+ symbolTokensPerChar: 0.75,
666
+ astralTokensPerChar: 3,
667
+ // Images only. Exactly four tiny images succeeded and five were refused
668
+ // on both models. image/jpg also succeeded and is normalized to JPEG.
669
+ // Retain the conservative 256,000-byte encoded-body cap: on 2026-10-04
670
+ // both models accepted 520,000 text characters and refused 525,000 with
671
+ // 413/code 5021. On 2026-10-03, clef-flash accepted 262,000 and refused
672
+ // 270,000. Estimates match encoded body characters / 4, rounded up. The
673
+ // threshold moved from 65,527 accepted / 67,527 refused to 130,026 /
674
+ // 131,276 (clef) and 130,027 / 131,277 (flash); errors still print 65,536.
675
+ // Inference: the new interval contains 131,072 (twice the printed
676
+ // figure); reason unknown.
677
+ // The new image-byte ceiling was not measured; the old 195/202 KB PNG boundary
678
+ // is historical. See the guide for exact model and date coverage.
679
+ media: { maxImages: 4, video: false, maxRequestBytes: 256_000 },
680
+ },
681
+ setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
682
+ },
609
683
  ];
610
684
  /**
611
685
  * Builds a ProviderDescriptor for every JSON-catalog provider. Every field
@@ -724,9 +798,14 @@ function isDecisionProviderConfigured(descriptor, credentials) {
724
798
  const hasKey = [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv) ||
725
799
  isSet(slice?.apiKey) ||
726
800
  isSet(slice?.gatewayApiKey);
727
- // A required base URL can also come from credentials.<key>.baseURL.
728
- const hasRequired = (descriptor.envVars.extraRequired ?? []).every((name) => inEnv(name) ||
729
- (name === descriptor.envVars.baseURL && isSet(slice?.baseURL)));
801
+ // A required base URL can also come from credentials.<key>.baseURL, and any
802
+ // other required value the descriptor maps to a field of that slice.
803
+ const hasRequired = (descriptor.envVars.extraRequired ?? []).every((name) => {
804
+ const field = descriptor.envVars.extraRequiredCredentialFields?.[name];
805
+ return (inEnv(name) ||
806
+ (name === descriptor.envVars.baseURL && isSet(slice?.baseURL)) ||
807
+ (field !== undefined && isSet(slice?.[field])));
808
+ });
730
809
  return hasKey && hasRequired;
731
810
  }
732
811
  /**
@@ -22,6 +22,17 @@ export function resolveCredentialKey(providerName) {
22
22
  const normalized = providerName.toLowerCase();
23
23
  return (ProviderFactory.getDescriptor(normalized)?.credentialsKey ?? normalized);
24
24
  }
25
+ /**
26
+ * registerProvider() takes a class as well as a factory (ProviderConstructor),
27
+ * and calling a class without `new` throws. A prototype test cannot tell them
28
+ * apart, because every non-arrow function's prototype points back at itself,
29
+ * so the source text decides. A factory is therefore never constructed and
30
+ * never retried after it throws.
31
+ */
32
+ function isClassConstructor(candidate) {
33
+ return (typeof candidate === "function" &&
34
+ /^class[\s{]/.test(Function.prototype.toString.call(candidate)));
35
+ }
25
36
  /**
26
37
  * True Factory Pattern implementation for AI Providers
27
38
  * Uses registration-based approach to eliminate switch statements
@@ -113,7 +124,11 @@ export class ProviderFactory {
113
124
  if (typeof registration.constructor !== "function") {
114
125
  throw new Error(`Invalid constructor for provider ${providerName}: not a function`);
115
126
  }
116
- const factoryResult = registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials);
127
+ // A factory keeps being called as a method of the registration, as it
128
+ // always was; only a class is constructed.
129
+ const factoryResult = isClassConstructor(registration.constructor)
130
+ ? new registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials)
131
+ : registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials);
117
132
  const result = factoryResult instanceof Promise ? await factoryResult : factoryResult;
118
133
  return result;
119
134
  }
@@ -1,6 +1,6 @@
1
1
  import { ProviderFactory } from "./providerFactory.js";
2
2
  import { logger } from "../utils/logger.js";
3
- import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, ReplicateModels, } from "../constants/enums.js";
3
+ import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
4
4
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "./providerDescriptors.js";
5
5
  import { OPENAI_COMPAT_CATALOG } from "../providers/openaiCompatCatalog.js";
6
6
  import { providerChoicesFor } from "./mediaHandlerCatalog.js";
@@ -260,6 +260,15 @@ export class ProviderRegistry {
260
260
  return new PerplexityDeciderProvider(modelName, sdk, undefined, perplexityDeciderCreds);
261
261
  }, process.env.PERPLEXITY_DECIDER_MODEL ||
262
262
  PerplexityDeciderModels.PPLX_DECIDER_V1_27B, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.PERPLEXITY_DECIDER));
263
+ // Register Cloudflare Clef — a `decide` provider reached through the
264
+ // Workers AI REST API. Its descriptor declares inferenceKinds:
265
+ // ["decide"], so nothing in the generation fallback chain can reach it,
266
+ // and it is registered apart from the `cloudflare` text provider.
267
+ ProviderFactory.registerProvider(AIProviderName.CLOUDFLARE_CLEF, async (modelName, _providerName, sdk, _region, credentials) => {
268
+ const cloudflareClefCreds = credentials;
269
+ const { CloudflareClefProvider } = await import("../providers/cloudflareClef.js");
270
+ return new CloudflareClefProvider(modelName, sdk, undefined, cloudflareClefCreds);
271
+ }, process.env.CLOUDFLARE_CLEF_MODEL || CloudflareClefModels.CLEF, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.CLOUDFLARE_CLEF));
263
272
  logger.debug("All AI providers registered successfully");
264
273
  // ===== MEDIA HANDLER REGISTRATION =====
265
274
  // Single registration path (Task 11): each ecosystem barrel (voice,
@@ -827,7 +827,7 @@ export class FileReferenceRegistry {
827
827
  extractedText = await this.extractWordText(buffer, ref);
828
828
  break;
829
829
  case "pptx":
830
- extractedText = await this.extractPptxText(buffer);
830
+ extractedText = await this.extractPptxText(buffer, ref.filename);
831
831
  break;
832
832
  case "video":
833
833
  extractedText = await this.extractVideoContent(buffer, ref);
@@ -992,13 +992,13 @@ export class FileReferenceRegistry {
992
992
  /**
993
993
  * Extract text from a PowerPoint file using PptxProcessor.
994
994
  */
995
- async extractPptxText(buffer) {
995
+ async extractPptxText(buffer, filename) {
996
996
  try {
997
997
  const { PptxProcessor } = await import("../processors/document/PptxProcessor.js");
998
998
  return await PptxProcessor.extractText(buffer);
999
999
  }
1000
1000
  catch (err) {
1001
- logger.warn(`[FileReferenceRegistry] PPTX extraction failed: ${err instanceof Error ? err.message : String(err)}`);
1001
+ logger.warn(`[FileReferenceRegistry] PPTX extraction failed for "${filename}": ${err instanceof Error ? err.message : String(err)}`);
1002
1002
  return null;
1003
1003
  }
1004
1004
  }
@@ -23,6 +23,7 @@ import { typesafeManifest } from "./manifests/typesafe.js";
23
23
  import { layaManifest } from "./manifests/laya.js";
24
24
  import { xorManifest } from "./manifests/xor.js";
25
25
  import { perplexityDeciderManifest } from "./manifests/perplexityDecider.js";
26
+ import { cloudflareClefManifest } from "./manifests/cloudflareClef.js";
26
27
  import { cohereManifest } from "./manifests/cohere.js";
27
28
  import { togetherAiManifest } from "./manifests/together-ai.js";
28
29
  import { fireworksManifest } from "./manifests/fireworks.js";
@@ -124,6 +125,7 @@ export const MANIFEST_REGISTRY = {
124
125
  laya: layaManifest,
125
126
  xor: xorManifest,
126
127
  "perplexity-decider": perplexityDeciderManifest,
128
+ "cloudflare-clef": cloudflareClefManifest,
127
129
  cerebras: catalogManifest("cerebras"),
128
130
  sambanova: catalogManifest("sambanova"),
129
131
  cohere: cohereManifest,
@@ -0,0 +1,16 @@
1
+ import type { ProviderModelManifest } from "../../types/index.js";
2
+ /**
3
+ * Cloudflare Clef on Workers AI — the `decide` inference type, not text
4
+ * generation, and not the Workers AI text models of the `cloudflare` provider.
5
+ *
6
+ * `contextWindow` is the figure Cloudflare documents (65,536 tokens). The
7
+ * Workers AI endpoint ignores state text past about 2,048 tokens without an
8
+ * error (hosted service or model: unknown), so NeuroLink enforces the limit on
9
+ * the descriptor's `decisionLimits`, not this. `vision` describes `generate()`
10
+ * input, which this provider does not serve; its image input is
11
+ * `DecisionRequest.images`.
12
+ *
13
+ * `maxOutputTokens` is required by the manifest type but has no honest value
14
+ * for a model that emits no text, so a small non-zero figure is used.
15
+ */
16
+ export declare const cloudflareClefManifest: ProviderModelManifest;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cloudflare Clef on Workers AI — the `decide` inference type, not text
3
+ * generation, and not the Workers AI text models of the `cloudflare` provider.
4
+ *
5
+ * `contextWindow` is the figure Cloudflare documents (65,536 tokens). The
6
+ * Workers AI endpoint ignores state text past about 2,048 tokens without an
7
+ * error (hosted service or model: unknown), so NeuroLink enforces the limit on
8
+ * the descriptor's `decisionLimits`, not this. `vision` describes `generate()`
9
+ * input, which this provider does not serve; its image input is
10
+ * `DecisionRequest.images`.
11
+ *
12
+ * `maxOutputTokens` is required by the manifest type but has no honest value
13
+ * for a model that emits no text, so a small non-zero figure is used.
14
+ */
15
+ export const cloudflareClefManifest = {
16
+ defaultContextWindow: 65_536,
17
+ models: {
18
+ _default: {
19
+ aliases: [],
20
+ contextWindow: 65_536,
21
+ maxOutputTokens: 256,
22
+ vision: false,
23
+ functionCalling: false,
24
+ },
25
+ clef: {
26
+ aliases: ["@cf/cloudflare/clef"],
27
+ displayName: "Cloudflare Clef (27B)",
28
+ contextWindow: 65_536,
29
+ maxOutputTokens: 256,
30
+ vision: false,
31
+ functionCalling: false,
32
+ },
33
+ "clef-flash": {
34
+ aliases: ["@cf/cloudflare/clef-flash"],
35
+ displayName: "Cloudflare Clef-flash (9B)",
36
+ contextWindow: 65_536,
37
+ maxOutputTokens: 256,
38
+ vision: false,
39
+ functionCalling: false,
40
+ },
41
+ },
42
+ };
package/dist/neurolink.js CHANGED
@@ -8640,11 +8640,22 @@ Current user's request: ${currentInput}`;
8640
8640
  }
8641
8641
  yield fallbackChunk;
8642
8642
  }
8643
+ // Read after the drain: a background-loop provider (native Vertex+Claude)
8644
+ // fills these while the stream is consumed, so the values taken before
8645
+ // the loop are still empty for it. Without this, a fallback that ran
8646
+ // tools reports none on the result and in the end-of-turn events, and
8647
+ // the 0-output gate below counts tool calls that had not appeared yet.
8648
+ const finalToolCalls = fallbackResult.toolCalls ?? [];
8649
+ const finalToolResults = fallbackResult.toolResults ?? [];
8643
8650
  if (fallbackRealOutputChunks === 0 &&
8644
- fallbackToolCalls.length === 0 &&
8645
- fallbackToolResults.length === 0) {
8651
+ finalToolCalls.length === 0 &&
8652
+ finalToolResults.length === 0) {
8646
8653
  throw new Error(`Fallback provider ${fallbackRoute.provider} also returned 0 real output chunks (chunkCount=${fallbackChunkCount}, sentinel-only or empty)`);
8647
8654
  }
8655
+ streamState.toolCalls = finalToolCalls;
8656
+ streamState.toolResults = finalToolResults;
8657
+ streamState.finishReason =
8658
+ fallbackResult.finishReason ?? streamState.finishReason;
8648
8659
  // Fallback succeeded - likely guardrails blocked primary
8649
8660
  metadata.fallbackProvider = fallbackRoute.provider;
8650
8661
  metadata.fallbackModel = fallbackRoute.model;
@@ -660,6 +660,7 @@ export class VideoProcessor extends BaseFileProcessor {
660
660
  */
661
661
  async probeVideoWithFfmpeg(filePath) {
662
662
  let report;
663
+ let exitFailure;
663
664
  try {
664
665
  const { stderr } = await runFfmpeg(["-hide_banner", "-i", filePath, "-t", "0", "-f", "null", "-"], { timeoutMs: VIDEO_CONFIG.FFPROBE_TIMEOUT_MS });
665
666
  report = stderr;
@@ -668,18 +669,29 @@ export class VideoProcessor extends BaseFileProcessor {
668
669
  // A non-zero exit still carries the stream report when the failure came
669
670
  // after the input was opened.
670
671
  const stderr = error.stderr;
672
+ const message = error instanceof Error ? error.message : String(error);
671
673
  if (typeof stderr !== "string") {
672
674
  return {
673
675
  success: false,
674
- error: `ffmpeg probe failed: ${error instanceof Error ? error.message : String(error)}`,
676
+ error: `ffmpeg probe failed: ${message}`,
675
677
  };
676
678
  }
677
679
  report = stderr;
680
+ exitFailure = message;
678
681
  }
679
682
  const data = this.parseFfmpegInputReport(report);
680
- return data
681
- ? { success: true, data }
682
- : { success: false, error: "ffmpeg reported no duration" };
683
+ if (data) {
684
+ return { success: true, data };
685
+ }
686
+ // Keep ffmpeg's own words when it failed as well as found no duration:
687
+ // a file it cannot read at all is the common case, and the reason is the
688
+ // only thing the warning that follows can tell the reader.
689
+ return {
690
+ success: false,
691
+ error: exitFailure
692
+ ? `ffmpeg reported no duration: ${exitFailure}`
693
+ : "ffmpeg reported no duration",
694
+ };
683
695
  }
684
696
  /**
685
697
  * Parse the input section of an ffmpeg banner: `Duration:`, `bitrate:` and