@juspay/neurolink 11.7.0 → 11.8.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 +2 -2
- package/dist/browser/neurolink.min.js +208 -208
- package/dist/lib/providers/amazonSagemaker.d.ts +26 -4
- package/dist/lib/providers/amazonSagemaker.js +105 -8
- package/dist/lib/providers/sagemaker/config.d.ts +13 -1
- package/dist/lib/providers/sagemaker/config.js +50 -13
- package/dist/lib/utils/providerRetry.js +16 -0
- package/dist/providers/amazonSagemaker.d.ts +26 -4
- package/dist/providers/amazonSagemaker.js +105 -8
- package/dist/providers/sagemaker/config.d.ts +13 -1
- package/dist/providers/sagemaker/config.js +50 -13
- package/dist/utils/providerRetry.js +16 -0
- package/package.json +3 -2
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
1
|
import type { AIProviderName } from "../constants/enums.js";
|
|
3
2
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
3
|
import type { NeuroLink } from "../neurolink.js";
|
|
5
|
-
import type { StreamOptions
|
|
6
|
-
import type { LanguageModel
|
|
4
|
+
import type { StreamOptions } from "../types/index.js";
|
|
5
|
+
import type { LanguageModel } from "../types/index.js";
|
|
7
6
|
/**
|
|
8
7
|
* Amazon SageMaker Provider extending BaseProvider
|
|
9
8
|
*/
|
|
@@ -21,7 +20,30 @@ export declare class AmazonSageMakerProvider extends BaseProvider {
|
|
|
21
20
|
protected getProviderName(): AIProviderName;
|
|
22
21
|
protected getDefaultModel(): string;
|
|
23
22
|
protected getAISDKModel(): LanguageModel;
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Streaming was previously an `executeStream` override that unconditionally
|
|
25
|
+
* threw "not yet fully implemented" — while `SageMakerLanguageModel.doStream`
|
|
26
|
+
* sat one property access away, complete and working, with its own fallback
|
|
27
|
+
* to a synthetic stream when the endpoint does not support true streaming.
|
|
28
|
+
*
|
|
29
|
+
* This adapts that AI-SDK-shaped result to `BaseProvider`'s `doStream` hook,
|
|
30
|
+
* and the inherited default supplies `executeStream`. The two shapes differ:
|
|
31
|
+
* the language model emits typed parts (`text-delta`, `finish`) on a
|
|
32
|
+
* `ReadableStream`, while the hook wants text chunks plus promises for how
|
|
33
|
+
* the turn ended. Those promises are resolved from the `finish` part by a
|
|
34
|
+
* detached pump, so they settle whether or not the caller reads a chunk.
|
|
35
|
+
*/
|
|
36
|
+
protected doStream(options: StreamOptions): Promise<{
|
|
37
|
+
stream: AsyncIterable<{
|
|
38
|
+
content: string;
|
|
39
|
+
}>;
|
|
40
|
+
finishReason: Promise<string>;
|
|
41
|
+
usage: Promise<{
|
|
42
|
+
inputTokens: number;
|
|
43
|
+
outputTokens: number;
|
|
44
|
+
}>;
|
|
45
|
+
warnings?: string[];
|
|
46
|
+
}>;
|
|
25
47
|
protected formatProviderError(error: unknown): Error;
|
|
26
48
|
/**
|
|
27
49
|
* Get SageMaker-specific provider information
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
2
|
+
import { createStreamChannel } from "../core/streamChannel.js";
|
|
2
3
|
import { logger } from "../utils/logger.js";
|
|
3
4
|
import { withSpan } from "../telemetry/withSpan.js";
|
|
4
5
|
import { tracers } from "../telemetry/tracers.js";
|
|
@@ -20,7 +21,25 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
20
21
|
super(modelName, "sagemaker", neurolink);
|
|
21
22
|
try {
|
|
22
23
|
// Load and validate configuration, then overlay per-request credentials
|
|
23
|
-
|
|
24
|
+
// Credentials are passed in rather than overlaid afterwards, so they
|
|
25
|
+
// are present when the config is validated.
|
|
26
|
+
const baseConfig = getSageMakerConfig(credentials?.region ?? region, {
|
|
27
|
+
...(credentials?.region !== undefined && {
|
|
28
|
+
region: credentials.region,
|
|
29
|
+
}),
|
|
30
|
+
...(credentials?.accessKeyId !== undefined && {
|
|
31
|
+
accessKeyId: credentials.accessKeyId,
|
|
32
|
+
}),
|
|
33
|
+
...(credentials?.secretAccessKey !== undefined && {
|
|
34
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
35
|
+
}),
|
|
36
|
+
...(credentials?.sessionToken !== undefined && {
|
|
37
|
+
sessionToken: credentials.sessionToken,
|
|
38
|
+
}),
|
|
39
|
+
...(credentials?.endpoint !== undefined && {
|
|
40
|
+
endpoint: credentials.endpoint,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
24
43
|
this.sagemakerConfig = {
|
|
25
44
|
...baseConfig,
|
|
26
45
|
...(credentials?.region !== undefined && {
|
|
@@ -74,7 +93,20 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
74
93
|
const smModel = this.sagemakerModel;
|
|
75
94
|
return smModel;
|
|
76
95
|
}
|
|
77
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Streaming was previously an `executeStream` override that unconditionally
|
|
98
|
+
* threw "not yet fully implemented" — while `SageMakerLanguageModel.doStream`
|
|
99
|
+
* sat one property access away, complete and working, with its own fallback
|
|
100
|
+
* to a synthetic stream when the endpoint does not support true streaming.
|
|
101
|
+
*
|
|
102
|
+
* This adapts that AI-SDK-shaped result to `BaseProvider`'s `doStream` hook,
|
|
103
|
+
* and the inherited default supplies `executeStream`. The two shapes differ:
|
|
104
|
+
* the language model emits typed parts (`text-delta`, `finish`) on a
|
|
105
|
+
* `ReadableStream`, while the hook wants text chunks plus promises for how
|
|
106
|
+
* the turn ended. Those promises are resolved from the `finish` part by a
|
|
107
|
+
* detached pump, so they settle whether or not the caller reads a chunk.
|
|
108
|
+
*/
|
|
109
|
+
async doStream(options) {
|
|
78
110
|
return withSpan({
|
|
79
111
|
name: "neurolink.provider.sagemaker.stream",
|
|
80
112
|
tracer: tracers.stream,
|
|
@@ -83,16 +115,81 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
83
115
|
"model.name": this.modelName,
|
|
84
116
|
"sagemaker.endpoint": this.modelConfig.endpointName,
|
|
85
117
|
"sagemaker.region": this.sagemakerConfig.region,
|
|
86
|
-
"sagemaker.not_implemented": true,
|
|
87
118
|
},
|
|
88
119
|
}, async () => {
|
|
89
120
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
const messages = await this.buildMessagesForStream(options);
|
|
122
|
+
const result = await this.sagemakerModel.doStream({
|
|
123
|
+
prompt: messages,
|
|
124
|
+
maxTokens: options.maxTokens,
|
|
125
|
+
temperature: options.temperature,
|
|
126
|
+
});
|
|
127
|
+
// Settled from the `finish` part below. A turn that ends without one
|
|
128
|
+
// — a truncated or errored stream — still settles, so a caller
|
|
129
|
+
// awaiting either promise cannot hang.
|
|
130
|
+
let settleFinishReason;
|
|
131
|
+
let settleUsage;
|
|
132
|
+
const finishReason = new Promise((resolve) => {
|
|
133
|
+
settleFinishReason = resolve;
|
|
134
|
+
});
|
|
135
|
+
const usage = new Promise((resolve) => {
|
|
136
|
+
settleUsage = resolve;
|
|
95
137
|
});
|
|
138
|
+
// The source is drained by a detached pump rather than lazily by the
|
|
139
|
+
// consumer. Resolving `finishReason`/`usage` from inside a generator
|
|
140
|
+
// ties them to somebody iterating it, and the inherited
|
|
141
|
+
// `executeStream` chains analytics off exactly those promises — so a
|
|
142
|
+
// caller that awaits `result.analytics` without consuming the stream
|
|
143
|
+
// would wait forever. Pumping here means the turn completes, and
|
|
144
|
+
// both promises settle, whether or not anyone reads a chunk.
|
|
145
|
+
const channel = createStreamChannel();
|
|
146
|
+
void (async () => {
|
|
147
|
+
const reader = result.stream.getReader();
|
|
148
|
+
let sawFinish = false;
|
|
149
|
+
try {
|
|
150
|
+
while (true) {
|
|
151
|
+
const { done, value } = await reader.read();
|
|
152
|
+
if (done) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const part = value;
|
|
156
|
+
if (part.type === "text-delta" && part.textDelta) {
|
|
157
|
+
channel.push({ content: part.textDelta });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (part.type === "finish") {
|
|
161
|
+
sawFinish = true;
|
|
162
|
+
settleFinishReason(part.finishReason ?? "stop");
|
|
163
|
+
settleUsage({
|
|
164
|
+
inputTokens: part.usage?.inputTokens ?? part.usage?.promptTokens ?? 0,
|
|
165
|
+
outputTokens: part.usage?.outputTokens ??
|
|
166
|
+
part.usage?.completionTokens ??
|
|
167
|
+
0,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
channel.close();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
channel.error(error);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
reader.releaseLock();
|
|
178
|
+
// A stream that ended without a finish part — truncated or
|
|
179
|
+
// errored — must still settle both promises, or the same hang
|
|
180
|
+
// returns by a different route.
|
|
181
|
+
if (!sawFinish) {
|
|
182
|
+
settleFinishReason("unknown");
|
|
183
|
+
settleUsage({ inputTokens: 0, outputTokens: 0 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return {
|
|
188
|
+
stream: channel.iterable,
|
|
189
|
+
finishReason,
|
|
190
|
+
usage,
|
|
191
|
+
warnings: (result.warnings ?? []).map((warning) => warning.message ?? String(warning)),
|
|
192
|
+
};
|
|
96
193
|
}
|
|
97
194
|
catch (error) {
|
|
98
195
|
throw this.handleProviderError(error);
|
|
@@ -18,7 +18,19 @@ import type { SageMakerConfig, SageMakerModelConfig } from "../../types/index.js
|
|
|
18
18
|
* @returns Validated SageMaker configuration
|
|
19
19
|
* @throws {Error} When required configuration is missing or invalid
|
|
20
20
|
*/
|
|
21
|
-
export declare function getSageMakerConfig(region?: string
|
|
21
|
+
export declare function getSageMakerConfig(region?: string,
|
|
22
|
+
/**
|
|
23
|
+
* Per-request credentials, applied BEFORE validation.
|
|
24
|
+
*
|
|
25
|
+
* They have to participate in validation rather than be overlaid onto an
|
|
26
|
+
* already-validated result: this function throws when accessKeyId or
|
|
27
|
+
* secretAccessKey is empty, so a caller passing credentials explicitly
|
|
28
|
+
* still had to have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set in the
|
|
29
|
+
* environment or construction failed before their values were ever looked
|
|
30
|
+
* at. That made `credentials.sagemaker` unusable on its own, which is the
|
|
31
|
+
* entire point of per-request credentials.
|
|
32
|
+
*/
|
|
33
|
+
overrides?: Partial<Pick<SageMakerConfig, "region" | "accessKeyId" | "secretAccessKey" | "sessionToken" | "endpoint">>): SageMakerConfig;
|
|
22
34
|
/**
|
|
23
35
|
* Load and validate SageMaker model configuration
|
|
24
36
|
*
|
|
@@ -55,28 +55,65 @@ const modelConfigCache = new Map();
|
|
|
55
55
|
* @returns Validated SageMaker configuration
|
|
56
56
|
* @throws {Error} When required configuration is missing or invalid
|
|
57
57
|
*/
|
|
58
|
-
export function getSageMakerConfig(region
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
export function getSageMakerConfig(region,
|
|
59
|
+
/**
|
|
60
|
+
* Per-request credentials, applied BEFORE validation.
|
|
61
|
+
*
|
|
62
|
+
* They have to participate in validation rather than be overlaid onto an
|
|
63
|
+
* already-validated result: this function throws when accessKeyId or
|
|
64
|
+
* secretAccessKey is empty, so a caller passing credentials explicitly
|
|
65
|
+
* still had to have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set in the
|
|
66
|
+
* environment or construction failed before their values were ever looked
|
|
67
|
+
* at. That made `credentials.sagemaker` unusable on its own, which is the
|
|
68
|
+
* entire point of per-request credentials.
|
|
69
|
+
*/
|
|
70
|
+
overrides) {
|
|
71
|
+
// The cache holds the environment-derived config, so it can only be used
|
|
72
|
+
// when this call adds nothing of its own. Emptiness is decided by whether
|
|
73
|
+
// any field is actually set, not by whether an object was passed: the
|
|
74
|
+
// provider always passes an object literal, which is `{}` — and truthy —
|
|
75
|
+
// when the caller supplied no credentials. Testing the object itself would
|
|
76
|
+
// bypass the cache on every ordinary call and discard configuration loaded
|
|
77
|
+
// from file along with it.
|
|
78
|
+
// The `region` ARGUMENT counts as request-specific too, not just the
|
|
79
|
+
// overrides object. The provider passes its constructor region through it,
|
|
80
|
+
// so treating such a call as environment-derived would let it return a
|
|
81
|
+
// cached config carrying a different region — or cache its own explicit
|
|
82
|
+
// region and hand that to every later caller.
|
|
83
|
+
const hasRequestSpecificInput = region !== undefined ||
|
|
84
|
+
(overrides !== undefined &&
|
|
85
|
+
Object.values(overrides).some((value) => value !== undefined));
|
|
86
|
+
if (configCache && !hasRequestSpecificInput) {
|
|
61
87
|
return configCache;
|
|
62
88
|
}
|
|
89
|
+
const explicitRegion = overrides?.region ?? region;
|
|
63
90
|
const config = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
// An explicitly supplied region is preserved even when empty, so the
|
|
92
|
+
// schema's min(1) rejects it rather than it being silently replaced by
|
|
93
|
+
// the environment. Only an absent one falls through.
|
|
94
|
+
region: explicitRegion !== undefined
|
|
95
|
+
? explicitRegion
|
|
96
|
+
: process.env.SAGEMAKER_REGION || process.env.AWS_REGION || "us-east-1",
|
|
97
|
+
// `??`, not `||`: an explicitly supplied empty credential must reach
|
|
98
|
+
// validation and be rejected there. With `||` it reads as absent and the
|
|
99
|
+
// request silently runs on whatever ambient AWS credentials the machine
|
|
100
|
+
// happens to have — which is precisely the per-request isolation this
|
|
101
|
+
// parameter exists to provide.
|
|
102
|
+
accessKeyId: overrides?.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? "",
|
|
103
|
+
secretAccessKey: overrides?.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? "",
|
|
104
|
+
sessionToken: overrides?.sessionToken ?? process.env.AWS_SESSION_TOKEN,
|
|
71
105
|
timeout: parseInt(process.env.SAGEMAKER_TIMEOUT || "30000"),
|
|
72
106
|
maxRetries: parseInt(process.env.SAGEMAKER_MAX_RETRIES || "3"),
|
|
73
|
-
endpoint: process.env.SAGEMAKER_ENDPOINT,
|
|
107
|
+
endpoint: overrides?.endpoint ?? process.env.SAGEMAKER_ENDPOINT,
|
|
74
108
|
};
|
|
75
109
|
// Validate configuration using Zod schema
|
|
76
110
|
try {
|
|
77
111
|
const validatedConfig = SageMakerConfigSchema.parse(config);
|
|
78
|
-
//
|
|
79
|
-
|
|
112
|
+
// Only the environment-derived config is cacheable; a per-request result
|
|
113
|
+
// must not become the answer for every later caller.
|
|
114
|
+
if (!hasRequestSpecificInput) {
|
|
115
|
+
configCache = validatedConfig;
|
|
116
|
+
}
|
|
80
117
|
return validatedConfig;
|
|
81
118
|
}
|
|
82
119
|
catch (error) {
|
|
@@ -59,6 +59,22 @@ export function duckTypedStatusCode(error) {
|
|
|
59
59
|
if (typeof err.status === "number") {
|
|
60
60
|
return err.status;
|
|
61
61
|
}
|
|
62
|
+
// AWS SDK v3 puts the HTTP status on `$metadata.httpStatusCode` and nowhere
|
|
63
|
+
// else, so without this branch no AWS error carries a status code as far as
|
|
64
|
+
// anything here is concerned. That is not only a retry question: this
|
|
65
|
+
// function also feeds `classifyProviderError` and the retry telemetry, so a
|
|
66
|
+
// Bedrock or SageMaker 429 was not recognisable as a rate limit, and a 5xx
|
|
67
|
+
// not recognisable as transient, by any status-based path. Name-based
|
|
68
|
+
// classification (ThrottlingException, AccessDeniedException) still worked,
|
|
69
|
+
// which is why this stayed hidden.
|
|
70
|
+
const metadata = err.$metadata;
|
|
71
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
72
|
+
const httpStatusCode = metadata
|
|
73
|
+
.httpStatusCode;
|
|
74
|
+
if (typeof httpStatusCode === "number") {
|
|
75
|
+
return httpStatusCode;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
62
78
|
return undefined;
|
|
63
79
|
}
|
|
64
80
|
/**
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
1
|
import type { AIProviderName } from "../constants/enums.js";
|
|
3
2
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
3
|
import type { NeuroLink } from "../neurolink.js";
|
|
5
|
-
import type { StreamOptions
|
|
6
|
-
import type { LanguageModel
|
|
4
|
+
import type { StreamOptions } from "../types/index.js";
|
|
5
|
+
import type { LanguageModel } from "../types/index.js";
|
|
7
6
|
/**
|
|
8
7
|
* Amazon SageMaker Provider extending BaseProvider
|
|
9
8
|
*/
|
|
@@ -21,7 +20,30 @@ export declare class AmazonSageMakerProvider extends BaseProvider {
|
|
|
21
20
|
protected getProviderName(): AIProviderName;
|
|
22
21
|
protected getDefaultModel(): string;
|
|
23
22
|
protected getAISDKModel(): LanguageModel;
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Streaming was previously an `executeStream` override that unconditionally
|
|
25
|
+
* threw "not yet fully implemented" — while `SageMakerLanguageModel.doStream`
|
|
26
|
+
* sat one property access away, complete and working, with its own fallback
|
|
27
|
+
* to a synthetic stream when the endpoint does not support true streaming.
|
|
28
|
+
*
|
|
29
|
+
* This adapts that AI-SDK-shaped result to `BaseProvider`'s `doStream` hook,
|
|
30
|
+
* and the inherited default supplies `executeStream`. The two shapes differ:
|
|
31
|
+
* the language model emits typed parts (`text-delta`, `finish`) on a
|
|
32
|
+
* `ReadableStream`, while the hook wants text chunks plus promises for how
|
|
33
|
+
* the turn ended. Those promises are resolved from the `finish` part by a
|
|
34
|
+
* detached pump, so they settle whether or not the caller reads a chunk.
|
|
35
|
+
*/
|
|
36
|
+
protected doStream(options: StreamOptions): Promise<{
|
|
37
|
+
stream: AsyncIterable<{
|
|
38
|
+
content: string;
|
|
39
|
+
}>;
|
|
40
|
+
finishReason: Promise<string>;
|
|
41
|
+
usage: Promise<{
|
|
42
|
+
inputTokens: number;
|
|
43
|
+
outputTokens: number;
|
|
44
|
+
}>;
|
|
45
|
+
warnings?: string[];
|
|
46
|
+
}>;
|
|
25
47
|
protected formatProviderError(error: unknown): Error;
|
|
26
48
|
/**
|
|
27
49
|
* Get SageMaker-specific provider information
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
2
|
+
import { createStreamChannel } from "../core/streamChannel.js";
|
|
2
3
|
import { logger } from "../utils/logger.js";
|
|
3
4
|
import { withSpan } from "../telemetry/withSpan.js";
|
|
4
5
|
import { tracers } from "../telemetry/tracers.js";
|
|
@@ -20,7 +21,25 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
20
21
|
super(modelName, "sagemaker", neurolink);
|
|
21
22
|
try {
|
|
22
23
|
// Load and validate configuration, then overlay per-request credentials
|
|
23
|
-
|
|
24
|
+
// Credentials are passed in rather than overlaid afterwards, so they
|
|
25
|
+
// are present when the config is validated.
|
|
26
|
+
const baseConfig = getSageMakerConfig(credentials?.region ?? region, {
|
|
27
|
+
...(credentials?.region !== undefined && {
|
|
28
|
+
region: credentials.region,
|
|
29
|
+
}),
|
|
30
|
+
...(credentials?.accessKeyId !== undefined && {
|
|
31
|
+
accessKeyId: credentials.accessKeyId,
|
|
32
|
+
}),
|
|
33
|
+
...(credentials?.secretAccessKey !== undefined && {
|
|
34
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
35
|
+
}),
|
|
36
|
+
...(credentials?.sessionToken !== undefined && {
|
|
37
|
+
sessionToken: credentials.sessionToken,
|
|
38
|
+
}),
|
|
39
|
+
...(credentials?.endpoint !== undefined && {
|
|
40
|
+
endpoint: credentials.endpoint,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
24
43
|
this.sagemakerConfig = {
|
|
25
44
|
...baseConfig,
|
|
26
45
|
...(credentials?.region !== undefined && {
|
|
@@ -74,7 +93,20 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
74
93
|
const smModel = this.sagemakerModel;
|
|
75
94
|
return smModel;
|
|
76
95
|
}
|
|
77
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Streaming was previously an `executeStream` override that unconditionally
|
|
98
|
+
* threw "not yet fully implemented" — while `SageMakerLanguageModel.doStream`
|
|
99
|
+
* sat one property access away, complete and working, with its own fallback
|
|
100
|
+
* to a synthetic stream when the endpoint does not support true streaming.
|
|
101
|
+
*
|
|
102
|
+
* This adapts that AI-SDK-shaped result to `BaseProvider`'s `doStream` hook,
|
|
103
|
+
* and the inherited default supplies `executeStream`. The two shapes differ:
|
|
104
|
+
* the language model emits typed parts (`text-delta`, `finish`) on a
|
|
105
|
+
* `ReadableStream`, while the hook wants text chunks plus promises for how
|
|
106
|
+
* the turn ended. Those promises are resolved from the `finish` part by a
|
|
107
|
+
* detached pump, so they settle whether or not the caller reads a chunk.
|
|
108
|
+
*/
|
|
109
|
+
async doStream(options) {
|
|
78
110
|
return withSpan({
|
|
79
111
|
name: "neurolink.provider.sagemaker.stream",
|
|
80
112
|
tracer: tracers.stream,
|
|
@@ -83,16 +115,81 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
83
115
|
"model.name": this.modelName,
|
|
84
116
|
"sagemaker.endpoint": this.modelConfig.endpointName,
|
|
85
117
|
"sagemaker.region": this.sagemakerConfig.region,
|
|
86
|
-
"sagemaker.not_implemented": true,
|
|
87
118
|
},
|
|
88
119
|
}, async () => {
|
|
89
120
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
const messages = await this.buildMessagesForStream(options);
|
|
122
|
+
const result = await this.sagemakerModel.doStream({
|
|
123
|
+
prompt: messages,
|
|
124
|
+
maxTokens: options.maxTokens,
|
|
125
|
+
temperature: options.temperature,
|
|
126
|
+
});
|
|
127
|
+
// Settled from the `finish` part below. A turn that ends without one
|
|
128
|
+
// — a truncated or errored stream — still settles, so a caller
|
|
129
|
+
// awaiting either promise cannot hang.
|
|
130
|
+
let settleFinishReason;
|
|
131
|
+
let settleUsage;
|
|
132
|
+
const finishReason = new Promise((resolve) => {
|
|
133
|
+
settleFinishReason = resolve;
|
|
134
|
+
});
|
|
135
|
+
const usage = new Promise((resolve) => {
|
|
136
|
+
settleUsage = resolve;
|
|
95
137
|
});
|
|
138
|
+
// The source is drained by a detached pump rather than lazily by the
|
|
139
|
+
// consumer. Resolving `finishReason`/`usage` from inside a generator
|
|
140
|
+
// ties them to somebody iterating it, and the inherited
|
|
141
|
+
// `executeStream` chains analytics off exactly those promises — so a
|
|
142
|
+
// caller that awaits `result.analytics` without consuming the stream
|
|
143
|
+
// would wait forever. Pumping here means the turn completes, and
|
|
144
|
+
// both promises settle, whether or not anyone reads a chunk.
|
|
145
|
+
const channel = createStreamChannel();
|
|
146
|
+
void (async () => {
|
|
147
|
+
const reader = result.stream.getReader();
|
|
148
|
+
let sawFinish = false;
|
|
149
|
+
try {
|
|
150
|
+
while (true) {
|
|
151
|
+
const { done, value } = await reader.read();
|
|
152
|
+
if (done) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const part = value;
|
|
156
|
+
if (part.type === "text-delta" && part.textDelta) {
|
|
157
|
+
channel.push({ content: part.textDelta });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (part.type === "finish") {
|
|
161
|
+
sawFinish = true;
|
|
162
|
+
settleFinishReason(part.finishReason ?? "stop");
|
|
163
|
+
settleUsage({
|
|
164
|
+
inputTokens: part.usage?.inputTokens ?? part.usage?.promptTokens ?? 0,
|
|
165
|
+
outputTokens: part.usage?.outputTokens ??
|
|
166
|
+
part.usage?.completionTokens ??
|
|
167
|
+
0,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
channel.close();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
channel.error(error);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
reader.releaseLock();
|
|
178
|
+
// A stream that ended without a finish part — truncated or
|
|
179
|
+
// errored — must still settle both promises, or the same hang
|
|
180
|
+
// returns by a different route.
|
|
181
|
+
if (!sawFinish) {
|
|
182
|
+
settleFinishReason("unknown");
|
|
183
|
+
settleUsage({ inputTokens: 0, outputTokens: 0 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return {
|
|
188
|
+
stream: channel.iterable,
|
|
189
|
+
finishReason,
|
|
190
|
+
usage,
|
|
191
|
+
warnings: (result.warnings ?? []).map((warning) => warning.message ?? String(warning)),
|
|
192
|
+
};
|
|
96
193
|
}
|
|
97
194
|
catch (error) {
|
|
98
195
|
throw this.handleProviderError(error);
|
|
@@ -18,7 +18,19 @@ import type { SageMakerConfig, SageMakerModelConfig } from "../../types/index.js
|
|
|
18
18
|
* @returns Validated SageMaker configuration
|
|
19
19
|
* @throws {Error} When required configuration is missing or invalid
|
|
20
20
|
*/
|
|
21
|
-
export declare function getSageMakerConfig(region?: string
|
|
21
|
+
export declare function getSageMakerConfig(region?: string,
|
|
22
|
+
/**
|
|
23
|
+
* Per-request credentials, applied BEFORE validation.
|
|
24
|
+
*
|
|
25
|
+
* They have to participate in validation rather than be overlaid onto an
|
|
26
|
+
* already-validated result: this function throws when accessKeyId or
|
|
27
|
+
* secretAccessKey is empty, so a caller passing credentials explicitly
|
|
28
|
+
* still had to have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set in the
|
|
29
|
+
* environment or construction failed before their values were ever looked
|
|
30
|
+
* at. That made `credentials.sagemaker` unusable on its own, which is the
|
|
31
|
+
* entire point of per-request credentials.
|
|
32
|
+
*/
|
|
33
|
+
overrides?: Partial<Pick<SageMakerConfig, "region" | "accessKeyId" | "secretAccessKey" | "sessionToken" | "endpoint">>): SageMakerConfig;
|
|
22
34
|
/**
|
|
23
35
|
* Load and validate SageMaker model configuration
|
|
24
36
|
*
|
|
@@ -55,28 +55,65 @@ const modelConfigCache = new Map();
|
|
|
55
55
|
* @returns Validated SageMaker configuration
|
|
56
56
|
* @throws {Error} When required configuration is missing or invalid
|
|
57
57
|
*/
|
|
58
|
-
export function getSageMakerConfig(region
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
export function getSageMakerConfig(region,
|
|
59
|
+
/**
|
|
60
|
+
* Per-request credentials, applied BEFORE validation.
|
|
61
|
+
*
|
|
62
|
+
* They have to participate in validation rather than be overlaid onto an
|
|
63
|
+
* already-validated result: this function throws when accessKeyId or
|
|
64
|
+
* secretAccessKey is empty, so a caller passing credentials explicitly
|
|
65
|
+
* still had to have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set in the
|
|
66
|
+
* environment or construction failed before their values were ever looked
|
|
67
|
+
* at. That made `credentials.sagemaker` unusable on its own, which is the
|
|
68
|
+
* entire point of per-request credentials.
|
|
69
|
+
*/
|
|
70
|
+
overrides) {
|
|
71
|
+
// The cache holds the environment-derived config, so it can only be used
|
|
72
|
+
// when this call adds nothing of its own. Emptiness is decided by whether
|
|
73
|
+
// any field is actually set, not by whether an object was passed: the
|
|
74
|
+
// provider always passes an object literal, which is `{}` — and truthy —
|
|
75
|
+
// when the caller supplied no credentials. Testing the object itself would
|
|
76
|
+
// bypass the cache on every ordinary call and discard configuration loaded
|
|
77
|
+
// from file along with it.
|
|
78
|
+
// The `region` ARGUMENT counts as request-specific too, not just the
|
|
79
|
+
// overrides object. The provider passes its constructor region through it,
|
|
80
|
+
// so treating such a call as environment-derived would let it return a
|
|
81
|
+
// cached config carrying a different region — or cache its own explicit
|
|
82
|
+
// region and hand that to every later caller.
|
|
83
|
+
const hasRequestSpecificInput = region !== undefined ||
|
|
84
|
+
(overrides !== undefined &&
|
|
85
|
+
Object.values(overrides).some((value) => value !== undefined));
|
|
86
|
+
if (configCache && !hasRequestSpecificInput) {
|
|
61
87
|
return configCache;
|
|
62
88
|
}
|
|
89
|
+
const explicitRegion = overrides?.region ?? region;
|
|
63
90
|
const config = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
// An explicitly supplied region is preserved even when empty, so the
|
|
92
|
+
// schema's min(1) rejects it rather than it being silently replaced by
|
|
93
|
+
// the environment. Only an absent one falls through.
|
|
94
|
+
region: explicitRegion !== undefined
|
|
95
|
+
? explicitRegion
|
|
96
|
+
: process.env.SAGEMAKER_REGION || process.env.AWS_REGION || "us-east-1",
|
|
97
|
+
// `??`, not `||`: an explicitly supplied empty credential must reach
|
|
98
|
+
// validation and be rejected there. With `||` it reads as absent and the
|
|
99
|
+
// request silently runs on whatever ambient AWS credentials the machine
|
|
100
|
+
// happens to have — which is precisely the per-request isolation this
|
|
101
|
+
// parameter exists to provide.
|
|
102
|
+
accessKeyId: overrides?.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? "",
|
|
103
|
+
secretAccessKey: overrides?.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? "",
|
|
104
|
+
sessionToken: overrides?.sessionToken ?? process.env.AWS_SESSION_TOKEN,
|
|
71
105
|
timeout: parseInt(process.env.SAGEMAKER_TIMEOUT || "30000"),
|
|
72
106
|
maxRetries: parseInt(process.env.SAGEMAKER_MAX_RETRIES || "3"),
|
|
73
|
-
endpoint: process.env.SAGEMAKER_ENDPOINT,
|
|
107
|
+
endpoint: overrides?.endpoint ?? process.env.SAGEMAKER_ENDPOINT,
|
|
74
108
|
};
|
|
75
109
|
// Validate configuration using Zod schema
|
|
76
110
|
try {
|
|
77
111
|
const validatedConfig = SageMakerConfigSchema.parse(config);
|
|
78
|
-
//
|
|
79
|
-
|
|
112
|
+
// Only the environment-derived config is cacheable; a per-request result
|
|
113
|
+
// must not become the answer for every later caller.
|
|
114
|
+
if (!hasRequestSpecificInput) {
|
|
115
|
+
configCache = validatedConfig;
|
|
116
|
+
}
|
|
80
117
|
return validatedConfig;
|
|
81
118
|
}
|
|
82
119
|
catch (error) {
|
|
@@ -58,6 +58,22 @@ export function duckTypedStatusCode(error) {
|
|
|
58
58
|
if (typeof err.status === "number") {
|
|
59
59
|
return err.status;
|
|
60
60
|
}
|
|
61
|
+
// AWS SDK v3 puts the HTTP status on `$metadata.httpStatusCode` and nowhere
|
|
62
|
+
// else, so without this branch no AWS error carries a status code as far as
|
|
63
|
+
// anything here is concerned. That is not only a retry question: this
|
|
64
|
+
// function also feeds `classifyProviderError` and the retry telemetry, so a
|
|
65
|
+
// Bedrock or SageMaker 429 was not recognisable as a rate limit, and a 5xx
|
|
66
|
+
// not recognisable as transient, by any status-based path. Name-based
|
|
67
|
+
// classification (ThrottlingException, AccessDeniedException) still worked,
|
|
68
|
+
// which is why this stayed hidden.
|
|
69
|
+
const metadata = err.$metadata;
|
|
70
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
71
|
+
const httpStatusCode = metadata
|
|
72
|
+
.httpStatusCode;
|
|
73
|
+
if (typeof httpStatusCode === "number") {
|
|
74
|
+
return httpStatusCode;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
61
77
|
return undefined;
|
|
62
78
|
}
|
|
63
79
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.8.0",
|
|
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": {
|
|
@@ -212,7 +212,8 @@
|
|
|
212
212
|
"test:vector-chroma": "npx tsx test/continuous-test-suite-vector-chroma.ts",
|
|
213
213
|
"test:vector-pgvector": "npx tsx test/continuous-test-suite-vector-pgvector.ts",
|
|
214
214
|
"test:vector-pinecone": "npx tsx test/continuous-test-suite-vector-pinecone.ts",
|
|
215
|
-
"test:bedrock-loop-characterization": "tsx test/continuous-test-suite-bedrock-loop-characterization.ts"
|
|
215
|
+
"test:bedrock-loop-characterization": "tsx test/continuous-test-suite-bedrock-loop-characterization.ts",
|
|
216
|
+
"test:sagemaker-streaming": "tsx test/continuous-test-suite-sagemaker-streaming.ts"
|
|
216
217
|
},
|
|
217
218
|
"files": [
|
|
218
219
|
"dist",
|