@juspay/neurolink 7.7.1 → 7.9.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.
- package/CHANGELOG.md +25 -2
- package/README.md +34 -2
- package/dist/cli/commands/config.d.ts +3 -3
- package/dist/cli/commands/sagemaker.d.ts +11 -0
- package/dist/cli/commands/sagemaker.js +778 -0
- package/dist/cli/factories/commandFactory.js +7 -2
- package/dist/cli/index.js +3 -0
- package/dist/cli/utils/interactiveSetup.js +28 -0
- package/dist/core/baseProvider.d.ts +2 -2
- package/dist/core/types.d.ts +16 -4
- package/dist/core/types.js +24 -3
- package/dist/factories/providerFactory.js +10 -1
- package/dist/factories/providerRegistry.js +6 -1
- package/dist/lib/core/baseProvider.d.ts +2 -2
- package/dist/lib/core/types.d.ts +16 -4
- package/dist/lib/core/types.js +24 -3
- package/dist/lib/factories/providerFactory.js +10 -1
- package/dist/lib/factories/providerRegistry.js +6 -1
- package/dist/lib/neurolink.d.ts +15 -0
- package/dist/lib/neurolink.js +73 -1
- package/dist/lib/providers/amazonSagemaker.d.ts +67 -0
- package/dist/lib/providers/amazonSagemaker.js +149 -0
- package/dist/lib/providers/googleVertex.d.ts +4 -0
- package/dist/lib/providers/googleVertex.js +44 -3
- package/dist/lib/providers/index.d.ts +4 -0
- package/dist/lib/providers/index.js +4 -0
- package/dist/lib/providers/sagemaker/adaptive-semaphore.d.ts +86 -0
- package/dist/lib/providers/sagemaker/adaptive-semaphore.js +212 -0
- package/dist/lib/providers/sagemaker/client.d.ts +156 -0
- package/dist/lib/providers/sagemaker/client.js +462 -0
- package/dist/lib/providers/sagemaker/config.d.ts +73 -0
- package/dist/lib/providers/sagemaker/config.js +308 -0
- package/dist/lib/providers/sagemaker/detection.d.ts +176 -0
- package/dist/lib/providers/sagemaker/detection.js +596 -0
- package/dist/lib/providers/sagemaker/diagnostics.d.ts +37 -0
- package/dist/lib/providers/sagemaker/diagnostics.js +137 -0
- package/dist/lib/providers/sagemaker/error-constants.d.ts +78 -0
- package/dist/lib/providers/sagemaker/error-constants.js +227 -0
- package/dist/lib/providers/sagemaker/errors.d.ts +83 -0
- package/dist/lib/providers/sagemaker/errors.js +216 -0
- package/dist/lib/providers/sagemaker/index.d.ts +35 -0
- package/dist/lib/providers/sagemaker/index.js +67 -0
- package/dist/lib/providers/sagemaker/language-model.d.ts +182 -0
- package/dist/lib/providers/sagemaker/language-model.js +755 -0
- package/dist/lib/providers/sagemaker/parsers.d.ts +136 -0
- package/dist/lib/providers/sagemaker/parsers.js +625 -0
- package/dist/lib/providers/sagemaker/streaming.d.ts +39 -0
- package/dist/lib/providers/sagemaker/streaming.js +320 -0
- package/dist/lib/providers/sagemaker/structured-parser.d.ts +117 -0
- package/dist/lib/providers/sagemaker/structured-parser.js +625 -0
- package/dist/lib/providers/sagemaker/types.d.ts +456 -0
- package/dist/lib/providers/sagemaker/types.js +7 -0
- package/dist/lib/sdk/toolRegistration.d.ts +1 -1
- package/dist/lib/sdk/toolRegistration.js +13 -5
- package/dist/lib/types/cli.d.ts +36 -1
- package/dist/lib/utils/providerHealth.js +19 -4
- package/dist/neurolink.d.ts +15 -0
- package/dist/neurolink.js +73 -1
- package/dist/providers/amazonSagemaker.d.ts +67 -0
- package/dist/providers/amazonSagemaker.js +149 -0
- package/dist/providers/googleVertex.d.ts +4 -0
- package/dist/providers/googleVertex.js +44 -3
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/sagemaker/adaptive-semaphore.d.ts +86 -0
- package/dist/providers/sagemaker/adaptive-semaphore.js +212 -0
- package/dist/providers/sagemaker/client.d.ts +156 -0
- package/dist/providers/sagemaker/client.js +462 -0
- package/dist/providers/sagemaker/config.d.ts +73 -0
- package/dist/providers/sagemaker/config.js +308 -0
- package/dist/providers/sagemaker/detection.d.ts +176 -0
- package/dist/providers/sagemaker/detection.js +596 -0
- package/dist/providers/sagemaker/diagnostics.d.ts +37 -0
- package/dist/providers/sagemaker/diagnostics.js +137 -0
- package/dist/providers/sagemaker/error-constants.d.ts +78 -0
- package/dist/providers/sagemaker/error-constants.js +227 -0
- package/dist/providers/sagemaker/errors.d.ts +83 -0
- package/dist/providers/sagemaker/errors.js +216 -0
- package/dist/providers/sagemaker/index.d.ts +35 -0
- package/dist/providers/sagemaker/index.js +67 -0
- package/dist/providers/sagemaker/language-model.d.ts +182 -0
- package/dist/providers/sagemaker/language-model.js +755 -0
- package/dist/providers/sagemaker/parsers.d.ts +136 -0
- package/dist/providers/sagemaker/parsers.js +625 -0
- package/dist/providers/sagemaker/streaming.d.ts +39 -0
- package/dist/providers/sagemaker/streaming.js +320 -0
- package/dist/providers/sagemaker/structured-parser.d.ts +117 -0
- package/dist/providers/sagemaker/structured-parser.js +625 -0
- package/dist/providers/sagemaker/types.d.ts +456 -0
- package/dist/providers/sagemaker/types.js +7 -0
- package/dist/sdk/toolRegistration.d.ts +1 -1
- package/dist/sdk/toolRegistration.js +13 -5
- package/dist/types/cli.d.ts +36 -1
- package/dist/utils/providerHealth.js +19 -4
- package/package.json +8 -2
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SageMaker Streaming Response Parsers
|
|
3
|
+
*
|
|
4
|
+
* This module provides protocol-specific parsers for different streaming
|
|
5
|
+
* formats used by SageMaker endpoints (HuggingFace, LLaMA, custom models).
|
|
6
|
+
*/
|
|
7
|
+
import { createStructuredOutputParser, isStructuredContent, } from "./structured-parser.js";
|
|
8
|
+
import { SageMakerError } from "./errors.js";
|
|
9
|
+
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
/**
|
|
12
|
+
* Constants for JSON parsing and validation
|
|
13
|
+
*/
|
|
14
|
+
const MIN_JSON_OBJECT_LENGTH = 2; // Minimum length for JSON object "{}"
|
|
15
|
+
/**
|
|
16
|
+
* Process a single character for bracket counting logic
|
|
17
|
+
* Shared utility to avoid code duplication between parsers
|
|
18
|
+
*/
|
|
19
|
+
export function processBracketCharacter(char, state) {
|
|
20
|
+
if (state.escapeNext) {
|
|
21
|
+
state.escapeNext = false;
|
|
22
|
+
return { isValid: true };
|
|
23
|
+
}
|
|
24
|
+
if (char === "\\") {
|
|
25
|
+
state.escapeNext = true;
|
|
26
|
+
return { isValid: true };
|
|
27
|
+
}
|
|
28
|
+
if (char === '"' && !state.escapeNext) {
|
|
29
|
+
state.inString = !state.inString;
|
|
30
|
+
return { isValid: true };
|
|
31
|
+
}
|
|
32
|
+
// Only count brackets outside of strings
|
|
33
|
+
if (!state.inString) {
|
|
34
|
+
switch (char) {
|
|
35
|
+
case "{":
|
|
36
|
+
state.braceCount++;
|
|
37
|
+
break;
|
|
38
|
+
case "}":
|
|
39
|
+
state.braceCount--;
|
|
40
|
+
if (state.braceCount < 0) {
|
|
41
|
+
return { isValid: false, reason: "Unmatched closing brace" };
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case "[":
|
|
45
|
+
state.bracketCount++;
|
|
46
|
+
break;
|
|
47
|
+
case "]":
|
|
48
|
+
state.bracketCount--;
|
|
49
|
+
if (state.bracketCount < 0) {
|
|
50
|
+
return { isValid: false, reason: "Unmatched closing bracket" };
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { isValid: true };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Utility function to validate JSON completeness using efficient bracket counting
|
|
59
|
+
* Extracted from parseArgumentsForToolCall for reusability
|
|
60
|
+
*/
|
|
61
|
+
export function validateJSONCompleteness(jsonString) {
|
|
62
|
+
// Basic length check - minimum for empty object "{}"
|
|
63
|
+
if (jsonString.length < MIN_JSON_OBJECT_LENGTH) {
|
|
64
|
+
return { isComplete: false, reason: "Too short" };
|
|
65
|
+
}
|
|
66
|
+
// Must start and end with braces for object
|
|
67
|
+
if (!jsonString.startsWith("{") || !jsonString.endsWith("}")) {
|
|
68
|
+
return { isComplete: false, reason: "Missing object braces" };
|
|
69
|
+
}
|
|
70
|
+
// Use shared bracket counting logic
|
|
71
|
+
const state = {
|
|
72
|
+
braceCount: 0,
|
|
73
|
+
bracketCount: 0,
|
|
74
|
+
inString: false,
|
|
75
|
+
escapeNext: false,
|
|
76
|
+
};
|
|
77
|
+
for (let i = 0; i < jsonString.length; i++) {
|
|
78
|
+
const char = jsonString[i];
|
|
79
|
+
const result = processBracketCharacter(char, state);
|
|
80
|
+
if (!result.isValid) {
|
|
81
|
+
return { isComplete: false, reason: result.reason };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Check for unterminated string
|
|
85
|
+
if (state.inString) {
|
|
86
|
+
return { isComplete: false, reason: "Unterminated string" };
|
|
87
|
+
}
|
|
88
|
+
// Check if all brackets are balanced
|
|
89
|
+
if (state.braceCount !== 0) {
|
|
90
|
+
return {
|
|
91
|
+
isComplete: false,
|
|
92
|
+
reason: `Unbalanced braces: ${state.braceCount}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (state.bracketCount !== 0) {
|
|
96
|
+
return {
|
|
97
|
+
isComplete: false,
|
|
98
|
+
reason: `Unbalanced brackets: ${state.bracketCount}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { isComplete: true };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Utility function to parse tool call arguments with robust validation
|
|
105
|
+
* Extracted from parseArgumentsForToolCall for reusability
|
|
106
|
+
*/
|
|
107
|
+
export function parseToolCallArguments(args) {
|
|
108
|
+
const trimmedArgs = args.trim();
|
|
109
|
+
// Handle empty arguments
|
|
110
|
+
if (trimmedArgs.length === 0) {
|
|
111
|
+
return { arguments: "{}", complete: true };
|
|
112
|
+
}
|
|
113
|
+
// Use robust bracket counting for JSON validation
|
|
114
|
+
const validationResult = validateJSONCompleteness(trimmedArgs);
|
|
115
|
+
if (validationResult.isComplete) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(trimmedArgs);
|
|
118
|
+
// Additional validation: ensure it's actually an object
|
|
119
|
+
if (typeof parsed === "object" &&
|
|
120
|
+
parsed !== null &&
|
|
121
|
+
!Array.isArray(parsed)) {
|
|
122
|
+
return { arguments: trimmedArgs, complete: true };
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Not a valid object, treat as delta
|
|
126
|
+
return { argumentsDelta: trimmedArgs };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (parseError) {
|
|
130
|
+
// Log parsing error for debugging
|
|
131
|
+
logger.debug("JSON parsing failed for tool arguments", {
|
|
132
|
+
args: trimmedArgs.substring(0, 100),
|
|
133
|
+
error: formatErrorMessage(parseError),
|
|
134
|
+
});
|
|
135
|
+
// Not valid JSON despite looking complete, treat as delta
|
|
136
|
+
return { argumentsDelta: trimmedArgs };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// String doesn't look like complete JSON, treat as delta
|
|
141
|
+
return { argumentsDelta: trimmedArgs };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Abstract base parser with common functionality
|
|
146
|
+
*/
|
|
147
|
+
class BaseStreamingParser {
|
|
148
|
+
buffer = "";
|
|
149
|
+
isCompleted = false;
|
|
150
|
+
totalUsage;
|
|
151
|
+
structuredParser;
|
|
152
|
+
responseSchema;
|
|
153
|
+
isComplete(chunk) {
|
|
154
|
+
return chunk.done === true || chunk.finishReason !== undefined;
|
|
155
|
+
}
|
|
156
|
+
extractUsage(finalChunk) {
|
|
157
|
+
return finalChunk.usage || this.totalUsage;
|
|
158
|
+
}
|
|
159
|
+
reset() {
|
|
160
|
+
this.buffer = "";
|
|
161
|
+
this.isCompleted = false;
|
|
162
|
+
this.totalUsage = undefined;
|
|
163
|
+
this.structuredParser?.reset();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Enable structured output parsing with optional schema
|
|
167
|
+
*/
|
|
168
|
+
enableStructuredOutput(schema) {
|
|
169
|
+
this.responseSchema = schema;
|
|
170
|
+
this.structuredParser = createStructuredOutputParser(schema);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse structured content if enabled
|
|
174
|
+
*/
|
|
175
|
+
parseStructuredContent(content) {
|
|
176
|
+
if (!this.structuredParser || !isStructuredContent(content)) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return this.structuredParser.parseChunk(content);
|
|
180
|
+
}
|
|
181
|
+
decodeChunk(chunk) {
|
|
182
|
+
return new TextDecoder().decode(chunk);
|
|
183
|
+
}
|
|
184
|
+
parseJSON(text) {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(text);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger.warn("Failed to parse JSON in streaming response", {
|
|
190
|
+
text,
|
|
191
|
+
error,
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* HuggingFace Transformers streaming parser (Server-Sent Events)
|
|
199
|
+
*/
|
|
200
|
+
export class HuggingFaceStreamParser extends BaseStreamingParser {
|
|
201
|
+
getName() {
|
|
202
|
+
return "HuggingFace SSE Parser";
|
|
203
|
+
}
|
|
204
|
+
parse(chunk) {
|
|
205
|
+
const text = this.decodeChunk(chunk);
|
|
206
|
+
this.buffer += text;
|
|
207
|
+
const chunks = [];
|
|
208
|
+
const lines = this.buffer.split("\n");
|
|
209
|
+
// Keep the last potentially incomplete line in buffer
|
|
210
|
+
this.buffer = lines.pop() || "";
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
const trimmed = line.trim();
|
|
213
|
+
// Skip empty lines and comments
|
|
214
|
+
if (!trimmed || trimmed.startsWith(":")) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// Parse Server-Sent Events format
|
|
218
|
+
if (trimmed.startsWith("data: ")) {
|
|
219
|
+
const data = trimmed.substring(6);
|
|
220
|
+
// Check for stream end
|
|
221
|
+
if (data === "[DONE]") {
|
|
222
|
+
chunks.push({
|
|
223
|
+
content: "",
|
|
224
|
+
done: true,
|
|
225
|
+
finishReason: "stop",
|
|
226
|
+
});
|
|
227
|
+
this.isCompleted = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Parse JSON data
|
|
231
|
+
const parsed = this.parseJSON(data);
|
|
232
|
+
if (parsed && typeof parsed === "object" && parsed !== null) {
|
|
233
|
+
const chunk = this.parseHuggingFaceChunk(parsed);
|
|
234
|
+
if (chunk) {
|
|
235
|
+
chunks.push(chunk);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return chunks;
|
|
241
|
+
}
|
|
242
|
+
parseHuggingFaceChunk(data) {
|
|
243
|
+
// HuggingFace streaming format
|
|
244
|
+
if (data.token) {
|
|
245
|
+
const token = data.token;
|
|
246
|
+
return {
|
|
247
|
+
content: typeof token.text === "string" ? token.text : String(token),
|
|
248
|
+
done: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Alternative format with generated_text
|
|
252
|
+
if (data.generated_text !== undefined) {
|
|
253
|
+
const details = data.details;
|
|
254
|
+
return {
|
|
255
|
+
content: String(data.generated_text),
|
|
256
|
+
done: details?.finish_reason !== undefined,
|
|
257
|
+
finishReason: this.mapFinishReason(details?.finish_reason),
|
|
258
|
+
usage: details ? this.parseHuggingFaceUsage(details) : undefined,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Error format
|
|
262
|
+
if (data.error) {
|
|
263
|
+
const errorMessage = extractApiErrorMessage(data.error);
|
|
264
|
+
throw new SageMakerError(`HuggingFace streaming error: ${errorMessage}`, "MODEL_ERROR", 500);
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
parseHuggingFaceUsage(details) {
|
|
269
|
+
if (!details.tokens) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const tokens = details.tokens;
|
|
273
|
+
return {
|
|
274
|
+
promptTokens: Number(tokens.input) || 0,
|
|
275
|
+
completionTokens: Number(tokens.generated) || 0,
|
|
276
|
+
totalTokens: Number(tokens.total) || 0,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
mapFinishReason(reason) {
|
|
280
|
+
switch (reason) {
|
|
281
|
+
case "stop":
|
|
282
|
+
return "stop";
|
|
283
|
+
case "length":
|
|
284
|
+
return "length";
|
|
285
|
+
case "eos_token":
|
|
286
|
+
return "stop";
|
|
287
|
+
default:
|
|
288
|
+
return "unknown";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* LLaMA/OpenAI-compatible streaming parser (JSON Lines)
|
|
294
|
+
*/
|
|
295
|
+
export class LlamaStreamParser extends BaseStreamingParser {
|
|
296
|
+
getName() {
|
|
297
|
+
return "LLaMA JSONL Parser";
|
|
298
|
+
}
|
|
299
|
+
parse(chunk) {
|
|
300
|
+
const text = this.decodeChunk(chunk);
|
|
301
|
+
this.buffer += text;
|
|
302
|
+
const chunks = [];
|
|
303
|
+
const lines = this.buffer.split("\n");
|
|
304
|
+
// Keep the last potentially incomplete line in buffer
|
|
305
|
+
this.buffer = lines.pop() || "";
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
const trimmed = line.trim();
|
|
308
|
+
if (!trimmed) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// Parse each line as JSON
|
|
312
|
+
const parsed = this.parseJSON(trimmed);
|
|
313
|
+
if (parsed && typeof parsed === "object" && parsed !== null) {
|
|
314
|
+
const chunk = this.parseLlamaChunk(parsed);
|
|
315
|
+
if (chunk) {
|
|
316
|
+
chunks.push(chunk);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return chunks;
|
|
321
|
+
}
|
|
322
|
+
parseLlamaChunk(data) {
|
|
323
|
+
// OpenAI-compatible format
|
|
324
|
+
if (Array.isArray(data.choices) && data.choices[0]) {
|
|
325
|
+
const choice = data.choices[0];
|
|
326
|
+
// Delta format (streaming)
|
|
327
|
+
if (choice.delta) {
|
|
328
|
+
const delta = choice.delta;
|
|
329
|
+
const content = String(delta.content || "");
|
|
330
|
+
const finishReason = choice.finish_reason;
|
|
331
|
+
const chunk = {
|
|
332
|
+
content,
|
|
333
|
+
done: finishReason !== null && finishReason !== undefined,
|
|
334
|
+
finishReason: this.mapFinishReason(finishReason || null),
|
|
335
|
+
usage: data.usage
|
|
336
|
+
? this.parseLlamaUsage(data.usage)
|
|
337
|
+
: undefined,
|
|
338
|
+
};
|
|
339
|
+
// Phase 2.3: Handle structured output if enabled
|
|
340
|
+
if (content && this.structuredParser) {
|
|
341
|
+
chunk.structuredOutput = this.parseStructuredContent(content);
|
|
342
|
+
}
|
|
343
|
+
// Phase 2.3: Handle streaming tool calls
|
|
344
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls[0]) {
|
|
345
|
+
const toolCall = delta.tool_calls[0];
|
|
346
|
+
chunk.toolCall = this.parseStreamingToolCall(toolCall);
|
|
347
|
+
// If tool call is complete and we have finish_reason, mark chunk complete
|
|
348
|
+
if (finishReason === "function_call" && chunk.toolCall.arguments) {
|
|
349
|
+
chunk.toolCall.complete = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return chunk;
|
|
353
|
+
}
|
|
354
|
+
// Text format (non-streaming fallback)
|
|
355
|
+
if (choice.text !== undefined) {
|
|
356
|
+
return {
|
|
357
|
+
content: String(choice.text),
|
|
358
|
+
done: choice.finish_reason !== null,
|
|
359
|
+
finishReason: this.mapFinishReason(choice.finish_reason),
|
|
360
|
+
usage: data.usage
|
|
361
|
+
? this.parseLlamaUsage(data.usage)
|
|
362
|
+
: undefined,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Direct content format
|
|
367
|
+
if (data.content !== undefined) {
|
|
368
|
+
return {
|
|
369
|
+
content: String(data.content),
|
|
370
|
+
done: Boolean(data.done),
|
|
371
|
+
finishReason: this.mapFinishReason(data.finish_reason),
|
|
372
|
+
usage: data.usage
|
|
373
|
+
? this.parseLlamaUsage(data.usage)
|
|
374
|
+
: undefined,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// Error format
|
|
378
|
+
if (data.error) {
|
|
379
|
+
const errorData = data.error;
|
|
380
|
+
const errorMessage = extractApiErrorMessage(errorData);
|
|
381
|
+
throw new SageMakerError(`LLaMA streaming error: ${errorMessage}`, "MODEL_ERROR", 500);
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Parse tool call arguments with robust validation and error handling
|
|
387
|
+
*/
|
|
388
|
+
parseToolCallArguments(functionData, toolCall) {
|
|
389
|
+
if (typeof functionData.arguments === "string") {
|
|
390
|
+
const result = parseToolCallArguments(functionData.arguments);
|
|
391
|
+
if (result.complete) {
|
|
392
|
+
toolCall.arguments = result.arguments;
|
|
393
|
+
toolCall.complete = true;
|
|
394
|
+
}
|
|
395
|
+
else if (result.argumentsDelta !== undefined) {
|
|
396
|
+
toolCall.argumentsDelta = result.argumentsDelta;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else if (functionData.arguments !== undefined) {
|
|
400
|
+
// Handle non-string arguments (objects, numbers, etc.)
|
|
401
|
+
try {
|
|
402
|
+
toolCall.arguments = JSON.stringify(functionData.arguments);
|
|
403
|
+
toolCall.complete = true;
|
|
404
|
+
}
|
|
405
|
+
catch (stringifyError) {
|
|
406
|
+
logger.warn("Failed to stringify tool arguments", {
|
|
407
|
+
args: functionData.arguments,
|
|
408
|
+
error: formatErrorMessage(stringifyError),
|
|
409
|
+
});
|
|
410
|
+
toolCall.arguments = "{}";
|
|
411
|
+
toolCall.complete = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// No arguments provided, default to empty object
|
|
416
|
+
toolCall.arguments = "{}";
|
|
417
|
+
toolCall.complete = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Parse streaming tool call from OpenAI-compatible format (Phase 2.3)
|
|
422
|
+
*/
|
|
423
|
+
parseStreamingToolCall(toolCallData) {
|
|
424
|
+
const toolCall = {
|
|
425
|
+
id: String(toolCallData.id || `call_${randomUUID()}`),
|
|
426
|
+
type: "function",
|
|
427
|
+
};
|
|
428
|
+
// Handle function name (usually sent in first chunk)
|
|
429
|
+
const functionData = toolCallData.function;
|
|
430
|
+
if (functionData?.name) {
|
|
431
|
+
toolCall.name = String(functionData.name);
|
|
432
|
+
}
|
|
433
|
+
// Handle streaming arguments
|
|
434
|
+
if (functionData?.arguments) {
|
|
435
|
+
this.parseToolCallArguments(functionData, toolCall);
|
|
436
|
+
}
|
|
437
|
+
return toolCall;
|
|
438
|
+
}
|
|
439
|
+
parseLlamaUsage(usage) {
|
|
440
|
+
return {
|
|
441
|
+
promptTokens: Number(usage.prompt_tokens) || 0,
|
|
442
|
+
completionTokens: Number(usage.completion_tokens) || 0,
|
|
443
|
+
totalTokens: Number(usage.total_tokens) || 0,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
mapFinishReason(reason) {
|
|
447
|
+
switch (reason) {
|
|
448
|
+
case "stop":
|
|
449
|
+
return "stop";
|
|
450
|
+
case "length":
|
|
451
|
+
return "length";
|
|
452
|
+
case "function_call":
|
|
453
|
+
return "tool-calls";
|
|
454
|
+
case "content_filter":
|
|
455
|
+
return "content-filter";
|
|
456
|
+
default:
|
|
457
|
+
return reason ? "unknown" : undefined;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Custom/Generic streaming parser (Chunked Transfer)
|
|
463
|
+
*/
|
|
464
|
+
export class CustomStreamParser extends BaseStreamingParser {
|
|
465
|
+
expectedFormat = "json";
|
|
466
|
+
constructor(format = "json") {
|
|
467
|
+
super();
|
|
468
|
+
this.expectedFormat = format;
|
|
469
|
+
}
|
|
470
|
+
getName() {
|
|
471
|
+
return `Custom ${this.expectedFormat.toUpperCase()} Parser`;
|
|
472
|
+
}
|
|
473
|
+
parse(chunk) {
|
|
474
|
+
const text = this.decodeChunk(chunk);
|
|
475
|
+
this.buffer += text;
|
|
476
|
+
if (this.expectedFormat === "json") {
|
|
477
|
+
return this.parseJSONFormat();
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
return this.parseTextFormat();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
parseJSONFormat() {
|
|
484
|
+
const chunks = [];
|
|
485
|
+
// Try to parse complete JSON objects
|
|
486
|
+
let startIndex = 0;
|
|
487
|
+
while (startIndex < this.buffer.length) {
|
|
488
|
+
try {
|
|
489
|
+
const remaining = this.buffer.substring(startIndex);
|
|
490
|
+
const parsed = JSON.parse(remaining);
|
|
491
|
+
// Successfully parsed - this is probably the complete response
|
|
492
|
+
const chunk = {
|
|
493
|
+
content: parsed.text ||
|
|
494
|
+
parsed.generated_text ||
|
|
495
|
+
parsed.output ||
|
|
496
|
+
String(parsed),
|
|
497
|
+
done: true,
|
|
498
|
+
finishReason: "stop",
|
|
499
|
+
};
|
|
500
|
+
chunks.push(chunk);
|
|
501
|
+
this.buffer = "";
|
|
502
|
+
this.isCompleted = true;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// JSON parsing failed - look for newline-separated JSON
|
|
507
|
+
const newlineIndex = this.buffer.indexOf("\n", startIndex);
|
|
508
|
+
if (newlineIndex === -1) {
|
|
509
|
+
// No complete line found, wait for more data
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const line = this.buffer.substring(startIndex, newlineIndex);
|
|
513
|
+
if (line.trim()) {
|
|
514
|
+
const parsed = this.parseJSON(line.trim());
|
|
515
|
+
if (parsed && typeof parsed === "object" && parsed !== null) {
|
|
516
|
+
const chunk = this.parseCustomChunk(parsed);
|
|
517
|
+
if (chunk) {
|
|
518
|
+
chunks.push(chunk);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
startIndex = newlineIndex + 1;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Clean processed content from buffer
|
|
526
|
+
if (startIndex > 0) {
|
|
527
|
+
this.buffer = this.buffer.substring(startIndex);
|
|
528
|
+
}
|
|
529
|
+
return chunks;
|
|
530
|
+
}
|
|
531
|
+
parseTextFormat() {
|
|
532
|
+
// Simple text streaming - treat each chunk as content
|
|
533
|
+
if (this.buffer) {
|
|
534
|
+
const content = this.buffer;
|
|
535
|
+
this.buffer = "";
|
|
536
|
+
return [
|
|
537
|
+
{
|
|
538
|
+
content,
|
|
539
|
+
done: false,
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
parseCustomChunk(data) {
|
|
546
|
+
// Generic parsing for various custom formats
|
|
547
|
+
const content = data.text ||
|
|
548
|
+
data.generated_text ||
|
|
549
|
+
data.output ||
|
|
550
|
+
data.response ||
|
|
551
|
+
data.content ||
|
|
552
|
+
(typeof data === "string" ? data : JSON.stringify(data));
|
|
553
|
+
return {
|
|
554
|
+
content: String(content),
|
|
555
|
+
done: Boolean(data.done || data.finished || data.complete),
|
|
556
|
+
finishReason: data.finish_reason || data.status === "complete" ? "stop" : undefined,
|
|
557
|
+
usage: data.usage || data.tokens ? this.parseCustomUsage(data) : undefined,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
parseCustomUsage(data) {
|
|
561
|
+
const usage = (data.usage || data.tokens || {});
|
|
562
|
+
return {
|
|
563
|
+
promptTokens: Number(usage.prompt_tokens || usage.input_tokens) || 0,
|
|
564
|
+
completionTokens: Number(usage.completion_tokens || usage.output_tokens) || 0,
|
|
565
|
+
totalTokens: Number(usage.total_tokens) || 0,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Parser factory to create appropriate parser for detected protocol
|
|
571
|
+
*/
|
|
572
|
+
export class StreamingParserFactory {
|
|
573
|
+
static createParser(protocol, options) {
|
|
574
|
+
switch (protocol) {
|
|
575
|
+
case "sse":
|
|
576
|
+
return new HuggingFaceStreamParser();
|
|
577
|
+
case "jsonl":
|
|
578
|
+
return new LlamaStreamParser();
|
|
579
|
+
case "chunked": {
|
|
580
|
+
const format = options?.format;
|
|
581
|
+
return new CustomStreamParser(format || "json");
|
|
582
|
+
}
|
|
583
|
+
case "none":
|
|
584
|
+
default:
|
|
585
|
+
// Return a no-op parser that just converts complete responses
|
|
586
|
+
return new CustomStreamParser("text");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
static getSupportedProtocols() {
|
|
590
|
+
return ["sse", "jsonl", "chunked", "text"];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Helper function to safely format error messages for logging and error handling
|
|
595
|
+
* Consolidates the duplicated error formatting pattern used throughout the parsers
|
|
596
|
+
*/
|
|
597
|
+
function formatErrorMessage(error) {
|
|
598
|
+
if (error instanceof Error) {
|
|
599
|
+
return error.message;
|
|
600
|
+
}
|
|
601
|
+
return String(error);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Helper function to extract error messages from API response error data
|
|
605
|
+
* Handles both string and object error formats consistently
|
|
606
|
+
*/
|
|
607
|
+
function extractApiErrorMessage(errorData) {
|
|
608
|
+
if (typeof errorData === "object" && errorData !== null) {
|
|
609
|
+
return errorData.message || String(errorData);
|
|
610
|
+
}
|
|
611
|
+
return String(errorData);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Utility function to estimate token usage when not provided
|
|
615
|
+
*/
|
|
616
|
+
export function estimateTokenUsage(prompt, completion) {
|
|
617
|
+
// Rough estimation: ~4 characters per token for English text
|
|
618
|
+
const promptTokens = Math.ceil(prompt.length / 4);
|
|
619
|
+
const completionTokens = Math.ceil(completion.length / 4);
|
|
620
|
+
return {
|
|
621
|
+
promptTokens,
|
|
622
|
+
completionTokens,
|
|
623
|
+
totalTokens: promptTokens + completionTokens,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming response handling for Amazon SageMaker Provider (Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* This module provides full streaming support with automatic protocol detection
|
|
5
|
+
* and model-specific parsing for various SageMaker deployment patterns.
|
|
6
|
+
*/
|
|
7
|
+
import { ReadableStream } from "stream/web";
|
|
8
|
+
import type { SageMakerStreamChunk, SageMakerUsage, SageMakerConfig } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Create a SageMaker streaming response with automatic protocol detection
|
|
11
|
+
*
|
|
12
|
+
* @param responseStream - Raw response stream from SageMaker endpoint
|
|
13
|
+
* @param endpointName - SageMaker endpoint name for capability detection
|
|
14
|
+
* @param config - SageMaker configuration
|
|
15
|
+
* @param options - Stream options and metadata
|
|
16
|
+
* @returns Promise resolving to ReadableStream compatible with AI SDK
|
|
17
|
+
*/
|
|
18
|
+
export declare function createSageMakerStream(responseStream: AsyncIterable<Uint8Array>, endpointName: string, config: SageMakerConfig, options?: {
|
|
19
|
+
prompt?: string;
|
|
20
|
+
abortSignal?: AbortSignal;
|
|
21
|
+
onChunk?: (chunk: SageMakerStreamChunk) => void;
|
|
22
|
+
onComplete?: (usage: SageMakerUsage) => void;
|
|
23
|
+
onError?: (error: Error) => void;
|
|
24
|
+
}): Promise<ReadableStream<unknown>>;
|
|
25
|
+
/**
|
|
26
|
+
* Create a synthetic stream from complete text (for backward compatibility)
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSyntheticStream(text: string, usage: SageMakerUsage, options?: {
|
|
29
|
+
onChunk?: (chunk: SageMakerStreamChunk) => void;
|
|
30
|
+
onComplete?: (usage: SageMakerUsage) => void;
|
|
31
|
+
}): Promise<ReadableStream<unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* Estimate token usage from text content
|
|
34
|
+
*
|
|
35
|
+
* @param prompt - Input prompt text
|
|
36
|
+
* @param completion - Generated completion text
|
|
37
|
+
* @returns Estimated usage information
|
|
38
|
+
*/
|
|
39
|
+
export declare function estimateTokenUsage(prompt: string, completion: string): SageMakerUsage;
|