@juspay/neurolink 12.28.4 → 12.29.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 (46) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
  3. package/dist/adapters/tts/googleTTSHandler.js +254 -0
  4. package/dist/browser/neurolink.min.js +400 -400
  5. package/dist/cli/commands/voices.d.ts +23 -0
  6. package/dist/cli/commands/voices.js +102 -0
  7. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  8. package/dist/cli/parser.js +3 -0
  9. package/dist/constants/enums.d.ts +13 -0
  10. package/dist/constants/enums.js +15 -0
  11. package/dist/core/baseProvider.d.ts +4 -0
  12. package/dist/core/baseProvider.js +245 -148
  13. package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
  14. package/dist/core/modules/structuredOutputPolicy.js +36 -0
  15. package/dist/core/nativeGenerateLoop.js +7 -1
  16. package/dist/neurolink.d.ts +42 -0
  17. package/dist/neurolink.js +102 -2
  18. package/dist/providers/catalog/index.generated.d.ts +1 -1
  19. package/dist/providers/catalog/index.generated.js +6 -0
  20. package/dist/providers/catalog/morph.json +97 -0
  21. package/dist/providers/catalog/novita.json +114 -0
  22. package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
  23. package/dist/providers/openaiChatCompletionsBase.js +291 -8
  24. package/dist/server/routes/claudeProxyRoutes.js +12 -1
  25. package/dist/server/utils/validation.d.ts +34 -0
  26. package/dist/server/utils/validation.js +27 -0
  27. package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
  28. package/dist/server/websocket/WebSocketHandler.js +185 -18
  29. package/dist/types/cli.d.ts +11 -0
  30. package/dist/types/generate.d.ts +15 -0
  31. package/dist/types/isolatedAgent.d.ts +16 -7
  32. package/dist/types/openaiCompatible.d.ts +6 -0
  33. package/dist/types/providerCatalog.generated.d.ts +2 -2
  34. package/dist/types/providers.d.ts +50 -3
  35. package/dist/types/server.d.ts +8 -0
  36. package/dist/types/stream.d.ts +30 -0
  37. package/dist/types/tts.d.ts +10 -0
  38. package/dist/types/utilities.d.ts +16 -3
  39. package/dist/utils/logger.d.ts +58 -13
  40. package/dist/utils/logger.js +156 -11
  41. package/dist/utils/providerHealth.d.ts +65 -1
  42. package/dist/utils/providerHealth.js +216 -54
  43. package/dist/utils/ttsProcessor.d.ts +30 -1
  44. package/dist/utils/ttsProcessor.js +76 -0
  45. package/docs-site/static/search-index.json +4 -4
  46. package/package.json +6 -1
@@ -38,11 +38,11 @@ import { resolveRequestKind } from "../core/resolveRequestKind.js";
38
38
  import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
39
39
  import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
40
40
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
41
- import { coerceJsonToSchema, schemaAccepts } from "../utils/json/coerce.js";
41
+ import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../utils/json/coerce.js";
42
42
  import { resolveToolChoice } from "../utils/toolChoice.js";
43
43
  import { transformToolExecutions } from "../utils/transformationUtils.js";
44
44
  import { withProviderRetry } from "../utils/providerRetry.js";
45
- import { isSchemaComplexityError, isToolsSchemaConflictError, } from "../core/modules/structuredOutputPolicy.js";
45
+ import { isResponseFormatUnsupportedError, isSchemaComplexityError, isToolsSchemaConflictError, } from "../core/modules/structuredOutputPolicy.js";
46
46
  import { resolveDeferredTool } from "../tools/toolDiscovery.js";
47
47
  import { buildAPIError, buildBody, buildToolsForOpenAI, buildWireToolNameMaps, createDeferredAnalytics, ensureJsonWordInBody, estimateWireTokens, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
48
48
  import { createStreamChannel } from "../core/streamChannel.js";
@@ -75,6 +75,30 @@ const yieldsSchemaValidObject = (text, schema) => {
75
75
  const coerced = coerceJsonToSchema(text, schema);
76
76
  return coerced !== null && schemaAccepts(schema, coerced.structuredData);
77
77
  };
78
+ /**
79
+ * Resolve a model answer against the requested schema, accepting the roots a
80
+ * ValidationSchema actually allows.
81
+ *
82
+ * `coerceJsonToSchema` scans for a balanced object or array span, so it
83
+ * recovers object and array roots but never a scalar one: a schema of
84
+ * `z.string()` answered with `"sunny"` finds no span and yields null. That is
85
+ * why `recoverScalarRoot` exists — it was already the second half of this
86
+ * decision on the neurolink.ts generate path (`recoverStructuredData`), and
87
+ * was simply never wired into the provider path. Without it a scalar-root
88
+ * schema silently produces no structured output no matter how correctly the
89
+ * model answers.
90
+ *
91
+ * Returns `undefined` when neither route accepts, which callers treat as "the
92
+ * model did not produce the value".
93
+ */
94
+ const resolveAgainstSchema = (text, schema) => {
95
+ const coerced = coerceJsonToSchema(text, schema);
96
+ if (coerced !== null && schemaAccepts(schema, coerced.structuredData)) {
97
+ return coerced.structuredData;
98
+ }
99
+ const scalar = recoverScalarRoot(text, schema);
100
+ return scalar.kind === "accepted" ? scalar.value : undefined;
101
+ };
78
102
  // Pull one native chunk at a time and forward cancellation to its iterator.
79
103
  const chunksToV3Stream = (source, completion, cancel) => {
80
104
  const iterator = source[Symbol.asyncIterator]();
@@ -976,7 +1000,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
976
1000
  }
977
1001
  catch (error) {
978
1002
  const recoverable = responseFormat !== undefined &&
979
- (isToolsSchemaConflictError(error) || isSchemaComplexityError(error));
1003
+ (isToolsSchemaConflictError(error) ||
1004
+ isSchemaComplexityError(error) ||
1005
+ isResponseFormatUnsupportedError(error));
980
1006
  if (!recoverable) {
981
1007
  throw error;
982
1008
  }
@@ -1330,6 +1356,176 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1330
1356
  }
1331
1357
  return loop;
1332
1358
  }
1359
+ /**
1360
+ * Resolve the parsed object for a `stream({ schema })` turn, after the
1361
+ * stream has been drained.
1362
+ *
1363
+ * Before this, streaming never sent `response_format` under any condition —
1364
+ * unlike the generate path, where the gap is only tools-dependent (see
1365
+ * suppressResponseFormatWithTools), so `structuredData` was never available
1366
+ * to a streaming caller at all. `executeStream` now sends `response_format`
1367
+ * on the wire whenever a schema is present and tools aren't suppressing it;
1368
+ * this method covers what's left: the common case where the streamed text
1369
+ * already parses against the schema, and the tools-suppressed case, which is
1370
+ * re-asked once without tools — the same recovery `reformatWithoutTools`
1371
+ * performs for generate(), reusing its acceptance rule
1372
+ * (`yieldsSchemaValidObject`) and its two-strategy fallback (native
1373
+ * `response_format` first, the schema spelled into the prompt if a vendor
1374
+ * rejects that).
1375
+ *
1376
+ * Returns `undefined` on every failure path rather than throwing: a turn
1377
+ * whose text the caller has already consumed and streamed to their UI must
1378
+ * not be turned into an error by a follow-up they never asked for.
1379
+ */
1380
+ async resolveStreamStructuredData(args) {
1381
+ const { schema, schemaJson, text, conversation, modelId, abortSignal, options, } = args;
1382
+ // A function, not a captured boolean: the signal is read once before the
1383
+ // re-ask and again after it, and it can legitimately flip in between — a
1384
+ // const read of `abortSignal.aborted` would let the compiler narrow the
1385
+ // second read away (it cannot know the underlying signal mutates between
1386
+ // the two checks).
1387
+ const isCancelled = () => abortSignal?.aborted === true;
1388
+ // Nothing streamed — a middleware short-circuit, or a turn that only
1389
+ // called tools. Nothing to parse and nothing to restate.
1390
+ if (text.trim().length === 0) {
1391
+ return undefined;
1392
+ }
1393
+ // The happy path: the streamed answer already satisfies the schema (this
1394
+ // is what sending `response_format` on the wire was for), so no second
1395
+ // request.
1396
+ const streamedValue = resolveAgainstSchema(text, schema);
1397
+ if (streamedValue !== undefined) {
1398
+ return {
1399
+ data: streamedValue,
1400
+ usage: { inputTokens: 0, outputTokens: 0 },
1401
+ };
1402
+ }
1403
+ // No conversation means `doStream` never ran (a middleware short
1404
+ // -circuited and returned its own stream), so there is no turn for a
1405
+ // re-ask to build on.
1406
+ if (!conversation || isCancelled()) {
1407
+ return undefined;
1408
+ }
1409
+ try {
1410
+ const model = await this.getAISDKModelWithMiddleware(options);
1411
+ if (!hasNativeDoGenerate(model)) {
1412
+ return undefined;
1413
+ }
1414
+ logger.warn(`[${this.providerName}] streamed answer is not schema-valid — re-asking once without tools`, { provider: this.providerName, model: modelId });
1415
+ const doGenerate = model.doGenerate.bind(model);
1416
+ // Every request here is billed, including one whose ANSWER is
1417
+ // discarded (a native-format attempt the vendor rejected, or a re-ask
1418
+ // that came back off-schema). Accumulate as each response lands rather
1419
+ // than reading the survivor, for the same reason the generate path
1420
+ // does: otherwise a two-request recovery reports as one.
1421
+ const billed = { inputTokens: 0, outputTokens: 0 };
1422
+ const runLoop = (conv, format) => runNativeGenerateLoop({
1423
+ doGenerate,
1424
+ conversation: conv,
1425
+ toolsRecord: {},
1426
+ ...(format ? { responseFormat: format } : {}),
1427
+ maxSteps: 1,
1428
+ ...(options.maxTokens
1429
+ ? { maxOutputTokens: options.maxTokens }
1430
+ : {}),
1431
+ ...(abortSignal ? { abortSignal } : {}),
1432
+ runStep: (call) => call(),
1433
+ }, []).then((loopResult) => {
1434
+ billed.inputTokens += loopResult.inputTokens;
1435
+ billed.outputTokens += loopResult.outputTokens;
1436
+ return loopResult;
1437
+ });
1438
+ // `conversation` here is the STREAMING loop's own array (see
1439
+ // executeToolBatch), which is already wire-shaped: an assistant turn
1440
+ // that called a tool carries a real `tool_calls` field and the matching
1441
+ // reply is a `{role: "tool", tool_call_id, content}` message — not the
1442
+ // ai-sdk-parts shape `messageBuilderToOpenAI` expects from `doGenerate`'s
1443
+ // `options.prompt`. Re-running `runNativeGenerateLoop` on those turns
1444
+ // unchanged would send them through `messageBuilderToOpenAI` a second
1445
+ // time, which only ever reads the ai-sdk-parts fields — it silently
1446
+ // drops `tool_calls` (it inspects `content` for `{type:"tool-call",
1447
+ // toolCallId}` parts, never a wire `tool_calls` property) and blanks
1448
+ // `tool_call_id` to `""` (it reads `toolCallId`, the pre-wire field,
1449
+ // never the wire `tool_call_id`). The result is a `tool` message with no
1450
+ // matching preceding tool call, which a real vendor rejects outright.
1451
+ // None of that history is actually needed for this re-ask: the
1452
+ // assistant turn appended below already restates whatever the tool call
1453
+ // produced, in the model's own words, so the tool exchange itself is
1454
+ // dropped rather than repaired.
1455
+ const conversationWithoutToolExchange = conversation.filter((message) => {
1456
+ if (message.role === "tool") {
1457
+ return false;
1458
+ }
1459
+ const toolCalls = message.tool_calls;
1460
+ return !(message.role === "assistant" &&
1461
+ Array.isArray(toolCalls) &&
1462
+ toolCalls.length > 0);
1463
+ });
1464
+ const reaskConversation = [
1465
+ ...conversationWithoutToolExchange,
1466
+ { role: "assistant", content: text },
1467
+ {
1468
+ role: "user",
1469
+ content:
1470
+ // "value", not "object": ValidationSchema also accepts array and
1471
+ // scalar roots (z.array(...), z.string()).
1472
+ "Return that same answer as a single JSON value conforming to " +
1473
+ "the required schema. No prose before or after it, and no " +
1474
+ "markdown code fence.",
1475
+ },
1476
+ ];
1477
+ let reformatted;
1478
+ try {
1479
+ reformatted = await runLoop(reaskConversation, {
1480
+ type: "json",
1481
+ schema: schemaJson,
1482
+ });
1483
+ }
1484
+ catch (nativeFormatError) {
1485
+ if (isCancelled()) {
1486
+ throw nativeFormatError;
1487
+ }
1488
+ logger.warn(`[${this.providerName}] the stream re-ask's response_format was rejected — retrying with the schema in the prompt`, {
1489
+ provider: this.providerName,
1490
+ model: modelId,
1491
+ error: nativeFormatError instanceof Error
1492
+ ? nativeFormatError.message
1493
+ : String(nativeFormatError),
1494
+ });
1495
+ reformatted = await runLoop(appendJsonSchemaInstruction(reaskConversation, schemaJson), undefined);
1496
+ }
1497
+ // Sending response_format is not the same as being obeyed: accept the
1498
+ // reformat only if it actually produced the object.
1499
+ const reaskedValue = resolveAgainstSchema(reformatted.text, schema);
1500
+ if (reaskedValue === undefined) {
1501
+ logger.warn(`[${this.providerName}] the tool-free stream re-ask did not yield a schema-valid value either; leaving structuredData unset`, { provider: this.providerName, model: modelId });
1502
+ return { data: undefined, usage: billed };
1503
+ }
1504
+ return { data: reaskedValue, usage: billed };
1505
+ }
1506
+ catch (error) {
1507
+ // A caller who cancelled deserves their cancellation. Swallowing here
1508
+ // resolved a cancelled turn as though the abort never happened — the
1509
+ // inner handler already rethrows for exactly that case, and this outer
1510
+ // one was catching its rethrow and turning it back into `undefined`.
1511
+ //
1512
+ // Our OWN turn deadline is the one thing that must NOT propagate: the
1513
+ // caller has already consumed the streamed text, and a deadline reached
1514
+ // during a follow-up they never asked for must not retroactively turn
1515
+ // their answer into an error. The two arrive on the same composed
1516
+ // signal but are distinguishable by reason, since the turn timer aborts
1517
+ // with a TimeoutError. Same rule as the generate path's reformat guard.
1518
+ if (isCancelled() && !(abortSignal?.reason instanceof TimeoutError)) {
1519
+ throw error;
1520
+ }
1521
+ logger.warn(`[${this.providerName}] the tool-free structured-output re-ask failed for a stream; leaving structuredData unset`, {
1522
+ provider: this.providerName,
1523
+ model: modelId,
1524
+ error: error instanceof Error ? error.message : String(error),
1525
+ });
1526
+ return undefined;
1527
+ }
1528
+ }
1333
1529
  async executeStream(options, _analysisSchema) {
1334
1530
  this.validateStreamOptions(options);
1335
1531
  const startTime = Date.now();
@@ -1374,6 +1570,23 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1374
1570
  timeoutController?.cleanup();
1375
1571
  throw setupErr;
1376
1572
  }
1573
+ // Structured output rides `response_format`, mirroring the generate
1574
+ // path's policy (see suppressResponseFormatWithTools) — the streaming
1575
+ // path previously never sent it AT ALL, regardless of tools, so
1576
+ // `structuredData` was never available to a streaming caller in any
1577
+ // case. Computed once, up front, exactly like the generate path: mid-turn
1578
+ // tool hydration (tools.discovery) does not retroactively change whether
1579
+ // this turn asked for JSON.
1580
+ const schemaJson = options.schema
1581
+ ? convertZodToJsonSchema(options.schema)
1582
+ : undefined;
1583
+ const hasToolsForStream = !!openAITools && openAITools.length > 0;
1584
+ const responseFormatSuppressed = schemaJson !== undefined &&
1585
+ hasToolsForStream &&
1586
+ this.suppressResponseFormatWithTools();
1587
+ const responseFormat = schemaJson && !responseFormatSuppressed
1588
+ ? this.adjustResponseFormat(v3ResponseFormatToOpenAI({ type: "json", schema: schemaJson }), modelId)
1589
+ : undefined;
1377
1590
  const url = this.getChatCompletionsURL(modelId);
1378
1591
  const fetchImpl = createProxyFetch();
1379
1592
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
@@ -1416,6 +1629,14 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1416
1629
  ...(options.topP !== undefined ? { topP: options.topP } : {}),
1417
1630
  };
1418
1631
  let loopPromise;
1632
+ // The wire conversation only exists once `doStream` actually runs, and it
1633
+ // is built from the MIDDLEWARE-TRANSFORMED prompt, so it cannot be
1634
+ // reconstructed from `promptMessages` afterwards. Captured here so the
1635
+ // post-drain structured-output re-ask (resolveStreamStructuredData) can
1636
+ // build on the same turn the model actually saw. Stays undefined when a
1637
+ // middleware short-circuits the request — the case where there is no
1638
+ // turn to build on at all.
1639
+ let loopConversation;
1419
1640
  const providerNameForLoop = this.providerName;
1420
1641
  const streamBaseModel = {
1421
1642
  specificationVersion: "v3",
@@ -1437,6 +1658,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1437
1658
  ? params.prompt
1438
1659
  : promptMessages;
1439
1660
  const conversation = messageBuilderToOpenAI(transformedPrompt, wireNameMaps?.toWire);
1661
+ loopConversation = conversation;
1440
1662
  const sampled = {
1441
1663
  ...options,
1442
1664
  ...(typeof params?.maxOutputTokens === "number"
@@ -1466,6 +1688,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1466
1688
  closeChannel: channel.close,
1467
1689
  resolveUsage,
1468
1690
  resolveFinish,
1691
+ responseFormat,
1469
1692
  });
1470
1693
  const completion = loopPromise.then(() => Promise.all([usagePromise, finishPromise]).then(([usage, reason]) => ({
1471
1694
  type: "finish",
@@ -1545,14 +1768,32 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1545
1768
  });
1546
1769
  }
1547
1770
  const providerName = this.providerName;
1771
+ // Not an arrow function — `transformedStream` doesn't have `this` bound,
1772
+ // so it needs its own capture to reach resolveStreamStructuredData.
1773
+ const self = this;
1774
+ // Built here rather than inline in `result` so the generator below can
1775
+ // fill in `structuredData` after the stream drains. Same mutable
1776
+ // -reference contract `finishReason`/`stopReason` on this object already
1777
+ // rely on: a wrapper that spreads the result cannot snapshot a value that
1778
+ // does not exist yet.
1779
+ const streamMetadata = {
1780
+ startTime,
1781
+ streamId: `${this.providerName}-${Date.now()}`,
1782
+ };
1548
1783
  const transformedStream = async function* () {
1549
1784
  let contentYielded = 0;
1785
+ // Only accumulated when a schema was actually requested — every other
1786
+ // turn keeps paying nothing for this.
1787
+ let schemaText = "";
1550
1788
  try {
1551
1789
  for await (const chunk of chunkSource) {
1552
1790
  if ("content" in chunk &&
1553
1791
  typeof chunk.content === "string" &&
1554
1792
  chunk.content.length > 0) {
1555
1793
  contentYielded++;
1794
+ if (options.schema) {
1795
+ schemaText += chunk.content;
1796
+ }
1556
1797
  }
1557
1798
  yield chunk;
1558
1799
  }
@@ -1560,6 +1801,47 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1560
1801
  // `loopPromise` is undefined when a middleware blocked the request
1561
1802
  // before `doStream` ran, in which case there is no loop to surface.
1562
1803
  await loopPromise;
1804
+ // Structured output for a `stream({ schema })` turn. Runs HERE —
1805
+ // after the stream is fully drained — so a tool-free re-ask (when the
1806
+ // streamed answer isn't already schema-valid) never reaches
1807
+ // `wrapStream` and a caller's guardrail middleware never sees a
1808
+ // second stream it did not ask for.
1809
+ //
1810
+ // When a middleware short-circuited the request, `loopPromise` is
1811
+ // undefined and `schemaText` is empty, so this resolves to nothing
1812
+ // and the field stays absent rather than hanging a reader.
1813
+ if (options.schema && schemaJson) {
1814
+ const resolved = await self.resolveStreamStructuredData({
1815
+ schema: options.schema,
1816
+ schemaJson,
1817
+ text: schemaText,
1818
+ conversation: loopConversation,
1819
+ modelId,
1820
+ abortSignal,
1821
+ options,
1822
+ });
1823
+ // Assign only when there is something to assign. A bare
1824
+ // `= resolved?.data` creates the own property even when the value
1825
+ // is undefined, so `"structuredData" in metadata` reports true for
1826
+ // a turn that produced no object — which is precisely the check the
1827
+ // field's own contract tells readers to use to detect absence.
1828
+ if (resolved?.data !== undefined) {
1829
+ streamMetadata.structuredData = resolved.data;
1830
+ }
1831
+ // Only when a re-ask actually spent something. The common case is
1832
+ // the streamed answer already parsing, which costs nothing extra
1833
+ // and leaves the field absent rather than reporting a zero.
1834
+ if (resolved &&
1835
+ (resolved.usage.inputTokens > 0 || resolved.usage.outputTokens > 0)) {
1836
+ streamMetadata.structuredDataUsage = resolved.usage;
1837
+ logger.info(`[${providerName}] structured-output re-ask billed separately from the streamed turn`, {
1838
+ provider: providerName,
1839
+ model: modelId,
1840
+ inputTokens: resolved.usage.inputTokens,
1841
+ outputTokens: resolved.usage.outputTokens,
1842
+ });
1843
+ }
1844
+ }
1563
1845
  // No-output path: stream completed normally but yielded zero text.
1564
1846
  // Build an enriched sentinel + stamp the active OTel span so
1565
1847
  // Pipeline B (ContextEnricher) surfaces a WARNING-level Langfuse
@@ -1615,10 +1897,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1615
1897
  streamingMode: true,
1616
1898
  }),
1617
1899
  toolsUsed,
1618
- metadata: {
1619
- startTime,
1620
- streamId: `${this.providerName}-${Date.now()}`,
1621
- },
1900
+ metadata: streamMetadata,
1622
1901
  };
1623
1902
  // Lazy getter: every read transforms the live `toolExecutionSummaries`
1624
1903
  // through the canonical `transformToolExecutions()` so consumers see
@@ -1642,7 +1921,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1642
1921
  return result;
1643
1922
  }
1644
1923
  async runStreamLoop(args) {
1645
- const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, } = args;
1924
+ const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, } = args;
1646
1925
  // Hoisted above the try so the catch can resolve the usage accumulated
1647
1926
  // by steps that completed BEFORE the failure — those steps were billed.
1648
1927
  let stepFinish = null;
@@ -1743,6 +2022,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1743
2022
  openAITools,
1744
2023
  openAIToolChoice,
1745
2024
  pushChunk,
2025
+ responseFormat,
1746
2026
  });
1747
2027
  lastObservedPromptTokens = stepResult.usage?.prompt_tokens;
1748
2028
  stepFinish = stepResult.finishReason;
@@ -1800,6 +2080,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1800
2080
  ? { toolChoice: args.openAIToolChoice }
1801
2081
  : {}),
1802
2082
  streaming: true,
2083
+ ...(args.responseFormat
2084
+ ? { responseFormat: args.responseFormat }
2085
+ : {}),
1803
2086
  }), args.modelId));
1804
2087
  // The initial fetch gets 429/5xx retry-with-backoff via the same
1805
2088
  // primitive the non-streaming path already uses (withProviderRetry).
@@ -3556,7 +3556,18 @@ async function loadClaudeProxyAccounts(args) {
3556
3556
  existingState.consecutiveRefreshFailures = 0;
3557
3557
  }
3558
3558
  else {
3559
- logger.debug(`[proxy] skipping disabled account=${key.split(":")[1] ?? key}`);
3559
+ const label = key.split(":")[1] ?? key;
3560
+ if (existingState.permanentlyDisabled) {
3561
+ logger.debug(`[proxy] skipping disabled account=${label}`);
3562
+ }
3563
+ else {
3564
+ // A flag set outside this worker (the CLI, another worker, an older
3565
+ // build) is otherwise invisible at the default level. Say once that
3566
+ // the account left the pool; re-enabling clears the marker, so the
3567
+ // next disable is announced again.
3568
+ const disabledAt = inventory[key].disabledAt;
3569
+ logger.always(`[proxy] account=${label} is disabled in the token store (reason=${disabledReason ?? "unknown"}${disabledAt ? `, since ${new Date(disabledAt).toISOString()}` : ""}); left out of the pool. Re-enable with: neurolink auth enable ${key}`);
3570
+ }
3560
3571
  existingState.permanentlyDisabled = true;
3561
3572
  if (disabledReason === "entitlement_blocked") {
3562
3573
  entitlementBlockedLabels.push(key.split(":")[1] ?? key);
@@ -23,6 +23,40 @@ export declare const AgentExecuteRequestSchema: z.ZodObject<{
23
23
  sessionId: z.ZodOptional<z.ZodString>;
24
24
  userId: z.ZodOptional<z.ZodString>;
25
25
  }, z.core.$strip>;
26
+ /**
27
+ * Options a WebSocket `generate`/`stream` message may pass through to
28
+ * `neurolink.generate()`/`.stream()`. Deliberately an allowlist, not the full
29
+ * `GenerateOptions`/`StreamOptions` shape: the raw client JSON is untrusted,
30
+ * and `GenerateOptions.credentials` in particular carries a per-provider
31
+ * `baseURL` (see `NeurolinkCredentials`) that would otherwise let any caller
32
+ * redirect the server's own outbound request to an arbitrary host (SSRF) or
33
+ * swap in their own API key. `z.object` strips unknown keys by default, so
34
+ * `credentials` and anything else outside this list never reaches the SDK
35
+ * call — the same posture `AgentExecuteRequestSchema` already takes for the
36
+ * equivalent HTTP route.
37
+ */
38
+ export declare const WebSocketAgentOptionsSchema: z.ZodObject<{
39
+ provider: z.ZodOptional<z.ZodString>;
40
+ model: z.ZodOptional<z.ZodString>;
41
+ systemPrompt: z.ZodOptional<z.ZodString>;
42
+ temperature: z.ZodOptional<z.ZodNumber>;
43
+ maxTokens: z.ZodOptional<z.ZodNumber>;
44
+ maxSteps: z.ZodOptional<z.ZodNumber>;
45
+ }, z.core.$strip>;
46
+ /**
47
+ * WebSocket `generate`/`stream` message payload schema.
48
+ */
49
+ export declare const WebSocketAgentRequestSchema: z.ZodObject<{
50
+ prompt: z.ZodString;
51
+ options: z.ZodOptional<z.ZodObject<{
52
+ provider: z.ZodOptional<z.ZodString>;
53
+ model: z.ZodOptional<z.ZodString>;
54
+ systemPrompt: z.ZodOptional<z.ZodString>;
55
+ temperature: z.ZodOptional<z.ZodNumber>;
56
+ maxTokens: z.ZodOptional<z.ZodNumber>;
57
+ maxSteps: z.ZodOptional<z.ZodNumber>;
58
+ }, z.core.$strip>>;
59
+ }, z.core.$strip>;
26
60
  /**
27
61
  * Tool execute request schema
28
62
  */
@@ -28,6 +28,33 @@ export const AgentExecuteRequestSchema = z.object({
28
28
  sessionId: z.string().optional(),
29
29
  userId: z.string().optional(),
30
30
  });
31
+ /**
32
+ * Options a WebSocket `generate`/`stream` message may pass through to
33
+ * `neurolink.generate()`/`.stream()`. Deliberately an allowlist, not the full
34
+ * `GenerateOptions`/`StreamOptions` shape: the raw client JSON is untrusted,
35
+ * and `GenerateOptions.credentials` in particular carries a per-provider
36
+ * `baseURL` (see `NeurolinkCredentials`) that would otherwise let any caller
37
+ * redirect the server's own outbound request to an arbitrary host (SSRF) or
38
+ * swap in their own API key. `z.object` strips unknown keys by default, so
39
+ * `credentials` and anything else outside this list never reaches the SDK
40
+ * call — the same posture `AgentExecuteRequestSchema` already takes for the
41
+ * equivalent HTTP route.
42
+ */
43
+ export const WebSocketAgentOptionsSchema = z.object({
44
+ provider: z.string().optional(),
45
+ model: z.string().optional(),
46
+ systemPrompt: z.string().optional(),
47
+ temperature: z.number().min(0).max(2).optional(),
48
+ maxTokens: z.number().positive().optional(),
49
+ maxSteps: z.number().positive().optional(),
50
+ });
51
+ /**
52
+ * WebSocket `generate`/`stream` message payload schema.
53
+ */
54
+ export const WebSocketAgentRequestSchema = z.object({
55
+ prompt: z.string().min(1, "Prompt is required"),
56
+ options: WebSocketAgentOptionsSchema.optional(),
57
+ });
31
58
  /**
32
59
  * Tool execute request schema
33
60
  */
@@ -4,7 +4,8 @@
4
4
  * Provides cross-framework WebSocket handling for real-time AI interactions.
5
5
  * Supports connection management, message routing, and graceful shutdown.
6
6
  */
7
- import type { WebSocketConfig, WebSocketHandler as IWebSocketHandler, WebSocketConnection, WebSocketMessage, AuthenticatedUser } from "../../types/index.js";
7
+ import type { WebSocketConfig, WebSocketHandler as IWebSocketHandler, WebSocketConnection, WebSocketMessage, WebSocketRequestId, AuthenticatedUser } from "../../types/index.js";
8
+ import type { NeuroLink } from "../../neurolink.js";
8
9
  /**
9
10
  * WebSocket connection manager
10
11
  */
@@ -91,7 +92,7 @@ export declare class WebSocketMessageRouter {
91
92
  /**
92
93
  * Register a message route
93
94
  */
94
- route(type: string, handler: (connection: WebSocketConnection, payload: unknown) => Promise<unknown>): void;
95
+ route(type: string, handler: (connection: WebSocketConnection, payload: unknown, requestId?: WebSocketRequestId) => Promise<unknown>): void;
95
96
  /**
96
97
  * Handle incoming message
97
98
  */
@@ -101,7 +102,4 @@ export declare class WebSocketMessageRouter {
101
102
  */
102
103
  getRoutes(): string[];
103
104
  }
104
- /**
105
- * Create a WebSocket handler for AI agent interactions
106
- */
107
- export declare function createAgentWebSocketHandler(_neurolink: unknown): IWebSocketHandler;
105
+ export declare function createAgentWebSocketHandler(neurolink: NeuroLink): IWebSocketHandler;