@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.
@@ -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, StreamResult } from "../types/index.js";
6
- import type { LanguageModel, Schema } from "../types/index.js";
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
- protected executeStream(_options: StreamOptions, _analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
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
- const baseConfig = getSageMakerConfig(credentials?.region ?? region);
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
- async executeStream(_options, _analysisSchema) {
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
- // For now, throw an error indicating this is not yet implemented
91
- throw new SageMakerError("SageMaker streaming not yet fully implemented. Coming in next phase.", {
92
- code: "MODEL_ERROR",
93
- statusCode: 501,
94
- endpoint: this.modelConfig.endpointName,
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): SageMakerConfig;
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
- // Return cached config if available
60
- if (configCache) {
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
- region: region ||
65
- process.env.SAGEMAKER_REGION ||
66
- process.env.AWS_REGION ||
67
- "us-east-1",
68
- accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
69
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
70
- sessionToken: process.env.AWS_SESSION_TOKEN,
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
- // Cache the validated configuration
79
- configCache = validatedConfig;
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, StreamResult } from "../types/index.js";
6
- import type { LanguageModel, Schema } from "../types/index.js";
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
- protected executeStream(_options: StreamOptions, _analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
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
- const baseConfig = getSageMakerConfig(credentials?.region ?? region);
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
- async executeStream(_options, _analysisSchema) {
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
- // For now, throw an error indicating this is not yet implemented
91
- throw new SageMakerError("SageMaker streaming not yet fully implemented. Coming in next phase.", {
92
- code: "MODEL_ERROR",
93
- statusCode: 501,
94
- endpoint: this.modelConfig.endpointName,
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): SageMakerConfig;
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
- // Return cached config if available
60
- if (configCache) {
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
- region: region ||
65
- process.env.SAGEMAKER_REGION ||
66
- process.env.AWS_REGION ||
67
- "us-east-1",
68
- accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
69
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
70
- sessionToken: process.env.AWS_SESSION_TOKEN,
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
- // Cache the validated configuration
79
- configCache = validatedConfig;
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.7.0",
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",