@juspay/neurolink 12.12.5 → 12.12.6
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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +319 -319
- package/dist/core/baseProvider.d.ts +10 -0
- package/dist/core/baseProvider.js +12 -2
- package/dist/middleware/builtin/guardrails.js +23 -13
- package/dist/middleware/utils/guardrailsUtils.d.ts +3 -15
- package/dist/middleware/utils/guardrailsUtils.js +14 -7
- package/dist/middleware/wrapLanguageModel.d.ts +3 -4
- package/dist/middleware/wrapLanguageModel.js +3 -4
- package/dist/providers/openaiChatCompletionsBase.js +214 -24
- package/package.json +2 -1
|
@@ -468,6 +468,16 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
468
468
|
* TODO(#1576): Implement global level middlewares that can be used
|
|
469
469
|
*/
|
|
470
470
|
protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
|
|
471
|
+
/**
|
|
472
|
+
* Apply the configured middleware chain to a caller-supplied base model.
|
|
473
|
+
*
|
|
474
|
+
* `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
|
|
475
|
+
* the model the non-streaming path drives. Streaming paths build a
|
|
476
|
+
* different base — one whose `doStream` starts the provider's own stream
|
|
477
|
+
* loop — and need the same chain applied to it, so the wrapping is split
|
|
478
|
+
* out here rather than duplicated per provider.
|
|
479
|
+
*/
|
|
480
|
+
protected applyMiddlewareToModel(baseModel: LanguageModel, options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
|
|
471
481
|
/**
|
|
472
482
|
* Extract middleware options - delegated to Utilities
|
|
473
483
|
*/
|
|
@@ -2014,8 +2014,18 @@ export class BaseProvider {
|
|
|
2014
2014
|
* TODO(#1576): Implement global level middlewares that can be used
|
|
2015
2015
|
*/
|
|
2016
2016
|
async getAISDKModelWithMiddleware(options = {}) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2017
|
+
return this.applyMiddlewareToModel(await this.getAISDKModel(), options);
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Apply the configured middleware chain to a caller-supplied base model.
|
|
2021
|
+
*
|
|
2022
|
+
* `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
|
|
2023
|
+
* the model the non-streaming path drives. Streaming paths build a
|
|
2024
|
+
* different base — one whose `doStream` starts the provider's own stream
|
|
2025
|
+
* loop — and need the same chain applied to it, so the wrapping is split
|
|
2026
|
+
* out here rather than duplicated per provider.
|
|
2027
|
+
*/
|
|
2028
|
+
async applyMiddlewareToModel(baseModel, options = {}) {
|
|
2019
2029
|
logger.debug(`Retrieved base model for ${this.providerName}`, {
|
|
2020
2030
|
provider: this.providerName,
|
|
2021
2031
|
model: this.modelName,
|
|
@@ -43,7 +43,6 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
43
43
|
const blockingState = new WeakMap();
|
|
44
44
|
const middleware = {
|
|
45
45
|
specificationVersion: "v3",
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
46
|
transformParams: async ({ params }) => {
|
|
48
47
|
if (config.precallEvaluation?.enabled) {
|
|
49
48
|
const { shouldBlock, transformedParams } = await handlePrecallGuardrails(params, config.precallEvaluation);
|
|
@@ -53,7 +52,6 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
53
52
|
}
|
|
54
53
|
return params;
|
|
55
54
|
},
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
55
|
wrapGenerate: async ({ doGenerate, params }) => {
|
|
58
56
|
logger.debug(`[GuardrailsMiddleware] Applying to generate call.`);
|
|
59
57
|
// Check if this request should be blocked (set by transformParams)
|
|
@@ -61,14 +59,23 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
61
59
|
return createBlockedResponse();
|
|
62
60
|
}
|
|
63
61
|
let result = await doGenerate();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
result
|
|
67
|
-
|
|
62
|
+
result = {
|
|
63
|
+
...result,
|
|
64
|
+
content: result.content.map((part) => part.type === "text"
|
|
65
|
+
? {
|
|
66
|
+
...part,
|
|
67
|
+
text: applyContentFiltering(part.text, config.badWords, "generate").filteredText,
|
|
68
|
+
}
|
|
69
|
+
: part),
|
|
70
|
+
};
|
|
68
71
|
if (config.modelFilter?.enabled && config.modelFilter.filterModel) {
|
|
69
72
|
logger.debug(`[GuardrailsMiddleware] Invoking model-based filter.`);
|
|
70
73
|
try {
|
|
71
|
-
const
|
|
74
|
+
const text = result.content
|
|
75
|
+
.filter((part) => part.type === "text")
|
|
76
|
+
.map((part) => part.text)
|
|
77
|
+
.join("");
|
|
78
|
+
const filterPrompt = `Is the following text safe? Respond with only "safe" or "unsafe".\n\nText: "${text}"`;
|
|
72
79
|
// `ModelFilterConfig.filterModel` is typed `LanguageModel`, which
|
|
73
80
|
// admits a bare model id, and the documented examples used one.
|
|
74
81
|
// `generateOnceNative` needs a handle exposing doGenerate, so a
|
|
@@ -78,7 +85,12 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
78
85
|
const { text: filterResponse } = await generateOnceNative(filterModel, { prompt: filterPrompt });
|
|
79
86
|
if (filterResponse.toLowerCase().trim() === "unsafe") {
|
|
80
87
|
logger.warn(`[GuardrailsMiddleware] Model-based filter flagged content as unsafe.`);
|
|
81
|
-
result = {
|
|
88
|
+
result = {
|
|
89
|
+
...result,
|
|
90
|
+
content: result.content.map((part) => part.type === "text"
|
|
91
|
+
? { ...part, text: "<REDACTED BY AI GUARDRAIL>" }
|
|
92
|
+
: part),
|
|
93
|
+
};
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
catch (error) {
|
|
@@ -89,7 +101,6 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
89
101
|
}
|
|
90
102
|
return result;
|
|
91
103
|
},
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
104
|
wrapStream: async ({ doStream, params }) => {
|
|
94
105
|
logger.debug(`[GuardrailsMiddleware] Applying to stream call.`);
|
|
95
106
|
// Check if this request should be blocked (set by transformParams)
|
|
@@ -106,13 +117,12 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
106
117
|
transform(chunk, controller) {
|
|
107
118
|
hasYieldedChunks = true;
|
|
108
119
|
let filteredChunk = chunk;
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const filterResult = applyContentFiltering(filteredChunk.textDelta, config.badWords, "stream");
|
|
120
|
+
if (filteredChunk.type === "text-delta") {
|
|
121
|
+
const filterResult = applyContentFiltering(filteredChunk.delta, config.badWords, "stream");
|
|
112
122
|
if (filterResult.hasChanges) {
|
|
113
123
|
filteredChunk = {
|
|
114
124
|
...filteredChunk,
|
|
115
|
-
|
|
125
|
+
delta: filterResult.filteredText,
|
|
116
126
|
};
|
|
117
127
|
}
|
|
118
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BadWordsConfig, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
|
|
1
|
+
import type { BadWordsConfig, LanguageModelV3GenerateResult, LanguageModelV3StreamPart, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
|
|
2
2
|
export declare const DEFAULT_EVALUATION_PROMPT = "\nEvaluate the following user input for safety and appropriateness. Respond ONLY with valid JSON:\n\n{\n \"overall\": \"safe|unsafe|suspicious|inappropriate\",\n \"safetyScore\": <1-10>,\n \"appropriatenessScore\": <1-10>,\n \"confidenceLevel\": <1-10>,\n \"issues\": [\n {\n \"category\": \"explicit_content|hate_speech|violence|personal_info|spam|other\",\n \"severity\": \"low|medium|high|critical\",\n \"description\": \"Brief description\"\n }\n ],\n \"suggestedAction\": \"allow|block|sanitize|warn\",\n \"reasoning\": \"Brief explanation\"\n}\n\nUser Input: \"{USER_INPUT}\"\n";
|
|
3
3
|
/**
|
|
4
4
|
* Extract user input from middleware params
|
|
@@ -25,20 +25,8 @@ export declare function applyEvaluationActions(evaluation: PrecallEvaluationResu
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function applySanitization(params: any, sanitizedInput: string): any;
|
|
27
27
|
export declare function escapeRegExp(string: string): string;
|
|
28
|
-
export declare function createBlockedResponse():
|
|
29
|
-
|
|
30
|
-
usage: {
|
|
31
|
-
promptTokens: number;
|
|
32
|
-
completionTokens: number;
|
|
33
|
-
};
|
|
34
|
-
finishReason: "stop";
|
|
35
|
-
warnings: never[];
|
|
36
|
-
rawCall: {
|
|
37
|
-
rawPrompt: null;
|
|
38
|
-
rawSettings: {};
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
export declare function createBlockedStream(): ReadableStream<any>;
|
|
28
|
+
export declare function createBlockedResponse(): LanguageModelV3GenerateResult;
|
|
29
|
+
export declare function createBlockedStream(): ReadableStream<LanguageModelV3StreamPart>;
|
|
42
30
|
/**
|
|
43
31
|
* Apply content filtering using bad words configuration
|
|
44
32
|
* Handles both regex patterns and string lists with proper priority
|
|
@@ -258,24 +258,31 @@ export function escapeRegExp(string) {
|
|
|
258
258
|
}
|
|
259
259
|
export function createBlockedResponse() {
|
|
260
260
|
return {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: "Request contains inappropriate content and has been blocked.",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
|
|
268
|
+
finishReason: { unified: "stop" },
|
|
264
269
|
warnings: [],
|
|
265
|
-
rawCall: { rawPrompt: null, rawSettings: {} },
|
|
266
270
|
};
|
|
267
271
|
}
|
|
268
272
|
export function createBlockedStream() {
|
|
269
273
|
return new ReadableStream({
|
|
270
274
|
start(controller) {
|
|
275
|
+
controller.enqueue({ type: "text-start", id: "blocked" });
|
|
271
276
|
controller.enqueue({
|
|
272
277
|
type: "text-delta",
|
|
273
|
-
|
|
278
|
+
id: "blocked",
|
|
279
|
+
delta: "Request contains inappropriate content and has been blocked.",
|
|
274
280
|
});
|
|
281
|
+
controller.enqueue({ type: "text-end", id: "blocked" });
|
|
275
282
|
controller.enqueue({
|
|
276
283
|
type: "finish",
|
|
277
|
-
finishReason: "stop",
|
|
278
|
-
usage: {
|
|
284
|
+
finishReason: { unified: "stop" },
|
|
285
|
+
usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
|
|
279
286
|
});
|
|
280
287
|
controller.close();
|
|
281
288
|
},
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
|
|
7
7
|
* factory no longer needs the ai package.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* streaming
|
|
11
|
-
*
|
|
12
|
-
* introduced.
|
|
9
|
+
* The OpenAI-compatible streaming path also uses this wrapper. Other native
|
|
10
|
+
* streaming implementations must opt in explicitly; exposing a middleware
|
|
11
|
+
* option or a model-shaped handle alone does not apply the chain.
|
|
13
12
|
*/
|
|
14
13
|
import type { LanguageModelV3, LanguageModelV3Middleware } from "../types/index.js";
|
|
15
14
|
export declare const wrapLanguageModel: ({ model, middleware, }: {
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
|
|
7
7
|
* factory no longer needs the ai package.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* streaming
|
|
11
|
-
*
|
|
12
|
-
* introduced.
|
|
9
|
+
* The OpenAI-compatible streaming path also uses this wrapper. Other native
|
|
10
|
+
* streaming implementations must opt in explicitly; exposing a middleware
|
|
11
|
+
* option or a model-shaped handle alone does not apply the chain.
|
|
13
12
|
*/
|
|
14
13
|
const doWrap = (model, middleware) => {
|
|
15
14
|
const transform = async (params, type) => middleware.transformParams
|
|
@@ -66,6 +66,73 @@ const yieldsSchemaValidObject = (text, schema) => {
|
|
|
66
66
|
const coerced = coerceJsonToSchema(text, schema);
|
|
67
67
|
return coerced !== null && schemaAccepts(schema, coerced.structuredData);
|
|
68
68
|
};
|
|
69
|
+
// Pull one native chunk at a time and forward cancellation to its iterator.
|
|
70
|
+
const chunksToV3Stream = (source, completion, cancel) => {
|
|
71
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
72
|
+
return new ReadableStream({
|
|
73
|
+
async pull(controller) {
|
|
74
|
+
try {
|
|
75
|
+
const next = await iterator.next();
|
|
76
|
+
if (next.done) {
|
|
77
|
+
controller.enqueue(await completion);
|
|
78
|
+
controller.close();
|
|
79
|
+
}
|
|
80
|
+
else if (next.value.reasoning) {
|
|
81
|
+
controller.enqueue({
|
|
82
|
+
type: "reasoning-delta",
|
|
83
|
+
delta: next.value.reasoning,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
controller.enqueue({ type: "text-delta", delta: next.value.content });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
controller.error(error);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async cancel() {
|
|
95
|
+
cancel();
|
|
96
|
+
await iterator.return?.();
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
async function* v3StreamToChunks(stream, onFinish) {
|
|
101
|
+
const reader = stream.getReader();
|
|
102
|
+
let done = false;
|
|
103
|
+
try {
|
|
104
|
+
while (true) {
|
|
105
|
+
const next = await reader.read();
|
|
106
|
+
if (next.done) {
|
|
107
|
+
done = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const part = next.value;
|
|
111
|
+
if (part.type === "text-delta") {
|
|
112
|
+
yield { content: part.delta };
|
|
113
|
+
}
|
|
114
|
+
else if (part.type === "reasoning-delta") {
|
|
115
|
+
yield { content: "", reasoning: part.delta };
|
|
116
|
+
}
|
|
117
|
+
else if (part.type === "finish") {
|
|
118
|
+
onFinish(part);
|
|
119
|
+
}
|
|
120
|
+
else if (part.type === "error") {
|
|
121
|
+
throw part.error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
try {
|
|
127
|
+
if (!done) {
|
|
128
|
+
await reader.cancel();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
reader.releaseLock();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
69
136
|
export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
70
137
|
config;
|
|
71
138
|
resolvedModel;
|
|
@@ -946,7 +1013,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
946
1013
|
let wireNameMaps;
|
|
947
1014
|
let openAITools;
|
|
948
1015
|
let openAIToolChoice;
|
|
949
|
-
|
|
1016
|
+
// The prompt is kept in its pre-wire shape. Model middleware transforms
|
|
1017
|
+
// `params.prompt`, and the conversion to the chat-completions wire format
|
|
1018
|
+
// has to happen AFTER that or the transform would be discarded.
|
|
1019
|
+
let promptMessages;
|
|
950
1020
|
try {
|
|
951
1021
|
modelId = await this.resolveModelName();
|
|
952
1022
|
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
@@ -962,8 +1032,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
962
1032
|
? buildToolsForOpenAI(toolsRecord, wireNameMaps?.toWire)
|
|
963
1033
|
: undefined;
|
|
964
1034
|
openAIToolChoice = mapNeuroLinkToolChoice(resolveToolChoice(options, toolsRecord, shouldUseTools), wireNameMaps?.toWire);
|
|
965
|
-
|
|
966
|
-
conversation = messageBuilderToOpenAI(initialMessages, wireNameMaps?.toWire);
|
|
1035
|
+
promptMessages = (await this.buildMessagesForStream(options));
|
|
967
1036
|
}
|
|
968
1037
|
catch (setupErr) {
|
|
969
1038
|
timeoutController?.cleanup();
|
|
@@ -979,26 +1048,136 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
979
1048
|
const channel = createStreamChannel();
|
|
980
1049
|
// Per-provider lifecycle hook (e.g. OTel span wrap for LiteLLM).
|
|
981
1050
|
const lifecycle = this.onStreamStart(modelId);
|
|
982
|
-
|
|
983
|
-
|
|
1051
|
+
// Model middleware on the streaming path.
|
|
1052
|
+
//
|
|
1053
|
+
// The base model below is not `buildDelegatingModel()`'s — that one's
|
|
1054
|
+
// `doGenerate` is a single wire call and its `doStream` is a stub. This
|
|
1055
|
+
// one's `doStream` starts the real multi-step stream loop, which is what
|
|
1056
|
+
// "produce the stream for this request" means here. Wrapping it gives the
|
|
1057
|
+
// streaming path the contract the generate path has always had:
|
|
1058
|
+
// `transformParams` can rewrite the prompt before a byte is sent, and
|
|
1059
|
+
// `wrapStream` can observe, filter, or replace the stream outright.
|
|
1060
|
+
//
|
|
1061
|
+
// Honoured on the way back in: `prompt`, `maxOutputTokens`, `temperature`
|
|
1062
|
+
// and `topP`. `tools` is offered read-only — a middleware that rewrites it
|
|
1063
|
+
// gets a WARN rather than a silent drop, because re-deriving the wire tool
|
|
1064
|
+
// list here would diverge from `buildToolsForOpenAI`.
|
|
1065
|
+
const v3Tools = openAITools?.map((t) => ({
|
|
1066
|
+
type: "function",
|
|
1067
|
+
name: t.function.name,
|
|
1068
|
+
description: t.function.description,
|
|
1069
|
+
inputSchema: t.function.parameters,
|
|
1070
|
+
}));
|
|
1071
|
+
const v3Params = {
|
|
1072
|
+
prompt: promptMessages,
|
|
1073
|
+
...(v3Tools ? { tools: v3Tools } : {}),
|
|
1074
|
+
...(options.maxTokens !== undefined
|
|
1075
|
+
? { maxOutputTokens: options.maxTokens }
|
|
1076
|
+
: {}),
|
|
1077
|
+
...(options.temperature !== undefined
|
|
1078
|
+
? { temperature: options.temperature }
|
|
1079
|
+
: {}),
|
|
1080
|
+
...(options.topP !== undefined ? { topP: options.topP } : {}),
|
|
1081
|
+
};
|
|
1082
|
+
let loopPromise;
|
|
1083
|
+
const providerNameForLoop = this.providerName;
|
|
1084
|
+
const streamBaseModel = {
|
|
1085
|
+
specificationVersion: "v3",
|
|
1086
|
+
provider: providerNameForLoop,
|
|
984
1087
|
modelId,
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1088
|
+
supportedUrls: {},
|
|
1089
|
+
doGenerate: async (params) => {
|
|
1090
|
+
const model = await this.getAISDKModel();
|
|
1091
|
+
if (typeof model === "string") {
|
|
1092
|
+
throw new Error("Native model handle required");
|
|
1093
|
+
}
|
|
1094
|
+
return model.doGenerate(params);
|
|
1095
|
+
},
|
|
1096
|
+
doStream: async (params) => {
|
|
1097
|
+
if (params?.tools !== undefined && params.tools !== v3Tools) {
|
|
1098
|
+
logger.warn(`${providerNameForLoop}: middleware rewrote 'tools' on the streaming path; tool rewrites are not applied to the wire request yet — the original tool list was sent.`);
|
|
1099
|
+
}
|
|
1100
|
+
const transformedPrompt = Array.isArray(params?.prompt)
|
|
1101
|
+
? params.prompt
|
|
1102
|
+
: promptMessages;
|
|
1103
|
+
const conversation = messageBuilderToOpenAI(transformedPrompt, wireNameMaps?.toWire);
|
|
1104
|
+
const sampled = {
|
|
1105
|
+
...options,
|
|
1106
|
+
...(typeof params?.maxOutputTokens === "number"
|
|
1107
|
+
? { maxTokens: params.maxOutputTokens }
|
|
1108
|
+
: {}),
|
|
1109
|
+
...(typeof params?.temperature === "number"
|
|
1110
|
+
? { temperature: params.temperature }
|
|
1111
|
+
: {}),
|
|
1112
|
+
...(typeof params?.topP === "number" ? { topP: params.topP } : {}),
|
|
1113
|
+
};
|
|
1114
|
+
loopPromise = this.runStreamLoop({
|
|
1115
|
+
maxSteps,
|
|
1116
|
+
modelId,
|
|
1117
|
+
url,
|
|
1118
|
+
fetchImpl,
|
|
1119
|
+
abortSignal,
|
|
1120
|
+
options: sampled,
|
|
1121
|
+
conversation,
|
|
1122
|
+
openAITools,
|
|
1123
|
+
openAIToolChoice,
|
|
1124
|
+
toolsRecord,
|
|
1125
|
+
toolNameFromWire: wireNameMaps?.fromWire,
|
|
1126
|
+
emitter,
|
|
1127
|
+
toolsUsed,
|
|
1128
|
+
toolExecutionSummaries,
|
|
1129
|
+
pushChunk: channel.push,
|
|
1130
|
+
closeChannel: channel.close,
|
|
1131
|
+
resolveUsage,
|
|
1132
|
+
resolveFinish,
|
|
1133
|
+
});
|
|
1134
|
+
const completion = loopPromise.then(() => Promise.all([usagePromise, finishPromise]).then(([usage, reason]) => ({
|
|
1135
|
+
type: "finish",
|
|
1136
|
+
finishReason: { unified: reason },
|
|
1137
|
+
usage: {
|
|
1138
|
+
inputTokens: {
|
|
1139
|
+
total: usage.promptTokens,
|
|
1140
|
+
cacheRead: usage.cacheReadTokens,
|
|
1141
|
+
},
|
|
1142
|
+
outputTokens: { total: usage.completionTokens },
|
|
1143
|
+
},
|
|
1144
|
+
})));
|
|
1145
|
+
// The producer can reject before the consumer pulls its terminal event.
|
|
1146
|
+
void completion.catch(() => undefined);
|
|
1147
|
+
return {
|
|
1148
|
+
stream: chunksToV3Stream(channel.iterable, completion, () => consumerAbortController.abort()),
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
// A middleware chain that blocks (guardrails' precall path) returns its own
|
|
1153
|
+
// stream without calling `doStream`, so the loop may never start. Every
|
|
1154
|
+
// later reader of `loopPromise` has to tolerate that.
|
|
1155
|
+
let chunkSource;
|
|
1156
|
+
try {
|
|
1157
|
+
const wrappedStreamModel = await this.applyMiddlewareToModel(streamBaseModel, options);
|
|
1158
|
+
if (typeof wrappedStreamModel === "string") {
|
|
1159
|
+
throw new Error("Native stream model handle required");
|
|
1160
|
+
}
|
|
1161
|
+
const { stream } = await wrappedStreamModel.doStream(v3Params);
|
|
1162
|
+
chunkSource = v3StreamToChunks(stream, (part) => {
|
|
1163
|
+
if (!loopPromise) {
|
|
1164
|
+
const input = part.usage.inputTokens.total ?? 0;
|
|
1165
|
+
const output = part.usage.outputTokens.total ?? 0;
|
|
1166
|
+
resolveUsage({
|
|
1167
|
+
promptTokens: input,
|
|
1168
|
+
completionTokens: output,
|
|
1169
|
+
totalTokens: input + output,
|
|
1170
|
+
});
|
|
1171
|
+
resolveFinish(part.finishReason.unified);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
consumerAbortController.abort();
|
|
1177
|
+
channel.close();
|
|
1178
|
+
timeoutController?.cleanup();
|
|
1179
|
+
throw error;
|
|
1180
|
+
}
|
|
1002
1181
|
// Closure-scoped capture: the runStreamLoop's catch block stashes the
|
|
1003
1182
|
// underlying provider error here so we can pass it through to
|
|
1004
1183
|
// buildNoOutputSentinel for richer telemetry (matches the pattern in
|
|
@@ -1025,7 +1204,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1025
1204
|
const transformedStream = async function* () {
|
|
1026
1205
|
let contentYielded = 0;
|
|
1027
1206
|
try {
|
|
1028
|
-
for await (const chunk of
|
|
1207
|
+
for await (const chunk of chunkSource) {
|
|
1029
1208
|
if ("content" in chunk &&
|
|
1030
1209
|
typeof chunk.content === "string" &&
|
|
1031
1210
|
chunk.content.length > 0) {
|
|
@@ -1034,6 +1213,8 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1034
1213
|
yield chunk;
|
|
1035
1214
|
}
|
|
1036
1215
|
// Surface any error that the loop threw after we drained the channel.
|
|
1216
|
+
// `loopPromise` is undefined when a middleware blocked the request
|
|
1217
|
+
// before `doStream` ran, in which case there is no loop to surface.
|
|
1037
1218
|
await loopPromise;
|
|
1038
1219
|
// No-output path: stream completed normally but yielded zero text.
|
|
1039
1220
|
// Build an enriched sentinel + stamp the active OTel span so
|
|
@@ -1062,6 +1243,15 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1062
1243
|
throw streamError;
|
|
1063
1244
|
}
|
|
1064
1245
|
finally {
|
|
1246
|
+
if (!loopPromise) {
|
|
1247
|
+
resolveUsage({
|
|
1248
|
+
promptTokens: 0,
|
|
1249
|
+
completionTokens: 0,
|
|
1250
|
+
totalTokens: 0,
|
|
1251
|
+
});
|
|
1252
|
+
resolveFinish("stop");
|
|
1253
|
+
}
|
|
1254
|
+
timeoutController?.cleanup();
|
|
1065
1255
|
if (!consumerAbortController.signal.aborted) {
|
|
1066
1256
|
consumerAbortController.abort();
|
|
1067
1257
|
}
|
|
@@ -1101,7 +1291,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1101
1291
|
}))),
|
|
1102
1292
|
});
|
|
1103
1293
|
loopPromise
|
|
1104
|
-
|
|
1294
|
+
?.finally(() => timeoutController?.cleanup())
|
|
1105
1295
|
.catch((error) => {
|
|
1106
1296
|
captureProviderError(error);
|
|
1107
1297
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.12.
|
|
3
|
+
"version": "12.12.6",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"test:bedrock-inference-profile": "pnpm exec tsx test/continuous-test-suite-bedrock-inference-profile.ts",
|
|
102
102
|
"test:loop-engine": "pnpm exec tsx test/continuous-test-suite-loop-engine.ts",
|
|
103
103
|
"test:middleware": "pnpm exec tsx test/continuous-test-suite-middleware.ts",
|
|
104
|
+
"test:stream-middleware": "pnpm exec tsx test/continuous-test-suite-stream-middleware.ts",
|
|
104
105
|
"test:observability": "pnpm exec tsx test/continuous-test-suite-observability.ts",
|
|
105
106
|
"test:ppt": "pnpm exec tsx test/continuous-test-suite-ppt.ts",
|
|
106
107
|
"test:vertex-loop-characterization": "tsx test/continuous-test-suite-vertex-loop-characterization.ts",
|