@juspay/neurolink 9.92.1 → 9.92.3

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.
@@ -105,11 +105,24 @@ export declare class SageMakerLanguageModel implements SageMakerAsLanguageModel
105
105
  */
106
106
  private convertToSageMakerRequest;
107
107
  /**
108
- * Convert Vercel AI SDK tools to SageMaker format
108
+ * Convert AI SDK tools (`LanguageModelV3FunctionTool | LanguageModelV3ProviderTool`,
109
+ * the same union every other provider's tool converter accepts — see
110
+ * `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts) into the SageMaker
111
+ * wire format, which mirrors the OpenAI Chat Completions nested
112
+ * `{ function: { name, description, parameters } }` convention.
113
+ *
114
+ * Function tools carry their fields flat (`tool.name`, `tool.inputSchema`,
115
+ * etc.) — there is no `tool.function` sub-object on the AI SDK side.
116
+ * `type: "provider"` tools (provider-defined tools like web search) have
117
+ * no SageMaker equivalent; they're dropped explicitly with a debug log
118
+ * rather than crashing or being sent malformed.
109
119
  */
110
120
  private convertToolsToSageMakerFormat;
111
121
  /**
112
- * Convert Vercel AI SDK tool choice to SageMaker format
122
+ * Convert an AI SDK tool choice (`LanguageModelV3ToolChoice`) into the
123
+ * SageMaker/OpenAI-style wire format. The AI SDK shape is
124
+ * `{ type: "auto" | "none" | "required" }` or `{ type: "tool", toolName }`
125
+ * — never the nested `{ type: "function", function: { name } }` shape.
113
126
  */
114
127
  private convertToolChoiceToSageMakerFormat;
115
128
  /**
@@ -370,12 +370,19 @@ export class SageMakerLanguageModel {
370
370
  stop: options.stopSequences || [],
371
371
  },
372
372
  };
373
- // Add tool support if tools are present
373
+ // Add tool support if tools are present. `options.tools` arrives here in
374
+ // the AI SDK's LanguageModelV2/V3 call-options shape — a flat
375
+ // `{ type: "function", name, description?, inputSchema }` per tool (see
376
+ // `LanguageModelV3FunctionTool` in @ai-sdk/provider), the same shape
377
+ // every other provider's tool converter consumes (compare
378
+ // `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts). It is NOT the
379
+ // nested OpenAI Chat Completions wire format.
374
380
  const tools = options.tools;
375
381
  if (tools && Array.isArray(tools) && tools.length > 0) {
376
382
  request.tools = this.convertToolsToSageMakerFormat(tools);
377
383
  // Add tool choice if specified
378
- const toolChoice = options.toolChoice;
384
+ const toolChoice = options
385
+ .toolChoice;
379
386
  if (toolChoice) {
380
387
  request.tool_choice =
381
388
  this.convertToolChoiceToSageMakerFormat(toolChoice);
@@ -403,39 +410,59 @@ export class SageMakerLanguageModel {
403
410
  return request;
404
411
  }
405
412
  /**
406
- * Convert Vercel AI SDK tools to SageMaker format
413
+ * Convert AI SDK tools (`LanguageModelV3FunctionTool | LanguageModelV3ProviderTool`,
414
+ * the same union every other provider's tool converter accepts — see
415
+ * `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts) into the SageMaker
416
+ * wire format, which mirrors the OpenAI Chat Completions nested
417
+ * `{ function: { name, description, parameters } }` convention.
418
+ *
419
+ * Function tools carry their fields flat (`tool.name`, `tool.inputSchema`,
420
+ * etc.) — there is no `tool.function` sub-object on the AI SDK side.
421
+ * `type: "provider"` tools (provider-defined tools like web search) have
422
+ * no SageMaker equivalent; they're dropped explicitly with a debug log
423
+ * rather than crashing or being sent malformed.
407
424
  */
408
425
  convertToolsToSageMakerFormat(tools) {
409
- return tools.map((tool) => {
410
- if (tool.type === "function") {
411
- return {
412
- type: "function",
413
- function: {
414
- name: tool.function.name,
415
- description: tool.function.description || "",
416
- parameters: tool.function.parameters || {},
417
- },
418
- };
426
+ const converted = [];
427
+ for (const tool of tools) {
428
+ if (tool.type !== "function") {
429
+ logger.debug("SageMaker: dropping tool with no SageMaker equivalent", {
430
+ toolType: tool.type,
431
+ toolName: tool.name,
432
+ });
433
+ continue;
419
434
  }
420
- return tool; // Pass through other tool types
421
- });
435
+ converted.push({
436
+ type: "function",
437
+ function: {
438
+ name: tool.name,
439
+ description: tool.description || "",
440
+ parameters: tool.inputSchema || {},
441
+ },
442
+ });
443
+ }
444
+ return converted;
422
445
  }
423
446
  /**
424
- * Convert Vercel AI SDK tool choice to SageMaker format
447
+ * Convert an AI SDK tool choice (`LanguageModelV3ToolChoice`) into the
448
+ * SageMaker/OpenAI-style wire format. The AI SDK shape is
449
+ * `{ type: "auto" | "none" | "required" }` or `{ type: "tool", toolName }`
450
+ * — never the nested `{ type: "function", function: { name } }` shape.
425
451
  */
426
452
  convertToolChoiceToSageMakerFormat(toolChoice) {
427
453
  if (typeof toolChoice === "string") {
428
- return toolChoice; // 'auto', 'none', etc.
454
+ return toolChoice; // Defensive: tolerate a raw string if ever passed.
429
455
  }
430
- if (toolChoice?.type === "function") {
431
- return {
432
- type: "function",
433
- function: {
434
- name: toolChoice.function.name,
435
- },
436
- };
456
+ switch (toolChoice.type) {
457
+ case "auto":
458
+ case "none":
459
+ case "required":
460
+ return toolChoice.type;
461
+ case "tool":
462
+ return { type: "function", function: { name: toolChoice.toolName } };
463
+ default:
464
+ return "auto";
437
465
  }
438
- return toolChoice;
439
466
  }
440
467
  /**
441
468
  * Convert Vercel AI SDK response format to SageMaker format (Phase 4)
@@ -18,12 +18,12 @@ import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, AnthropicAtte
18
18
  * no key is configured or the key cannot be matched (account disabled/
19
19
  * removed). The resolution is per-request because enabledAccounts membership
20
20
  * can shift between requests. */
21
- declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey?: string | undefined): number;
21
+ declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): number;
22
22
  /** If the configured home primary is no longer cooling, reset
23
23
  * primaryAccountIndex back to its index so traffic returns to the preferred
24
24
  * account once its rate limit window expires. Called at the start of each
25
25
  * request. Home is resolved fresh per call via resolveHomeIndex. */
26
- declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey?: string | undefined): void;
26
+ declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): void;
27
27
  declare function claimTransientRateLimitRetry(accountKey: string, coolingUntil: number, now?: number): number | undefined;
28
28
  declare function claimTransientCooldownAdmission(accountKey: string, coolingUntil: number, now?: number): number | undefined;
29
29
  declare function waitForTransientAccountAvailability(orderedAccounts: ProxyPassthroughAccount[]): Promise<ProxyPassthroughAccount[]>;
@@ -61,28 +61,30 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
61
61
  declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
62
62
  /**
63
63
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
64
- * spend the window that expires SOONEST first, so its about-to-reset
65
- * allowance isn't wasted, then move to accounts with longer-dated resets.
64
+ * spend the overall weekly allowance that expires SOONEST first, so quota
65
+ * cannot disappear while traffic is consuming a newer weekly window. The 5h
66
+ * window remains an availability boundary: a session at its soft limit is
67
+ * temporarily demoted until that session resets.
66
68
  *
67
69
  * Priority among usable accounts:
68
70
  * 1. no quota data yet — probe first: one request reveals its windows and
69
71
  * self-corrects the ordering. (Ranking unknowns last would starve them
70
72
  * forever: never picked → never observed → never comparable.)
71
73
  * 2. session headroom before session-saturated (>= soft limit or
72
- * "throttled") — saturated accounts then follow the same bucketed
73
- * session ordering below: soonest back in service first, weekly
74
- * deciding same-bucket ties.
75
- * 3. soonest SESSION (5h) reset — the capacity expiring soonest; compared
76
- * in tolerance buckets so near-simultaneous resets count as equal.
77
- * 4. soonest WEEKLY (7d) reset — decides between same-bucket sessions.
78
- * 5. highest weekly utilization — finish off the one closest to done
74
+ * "throttled") — do not re-hammer an urgent weekly account while its 5h
75
+ * capacity is temporarily unavailable.
76
+ * 3. soonest WEEKLY (7d) reset — consume the oldest overall allowance
77
+ * before it expires.
78
+ * 4. soonest SESSION (5h) reset — decides equal-weekly or fresh-weekly ties,
79
+ * using tolerance buckets for comparator stability.
80
+ * 5. highest weekly utilization — finish off the one closest to done.
79
81
  * 6. configured primary account, then insertion order
80
- * An account with NO ticking session window (fresh, not yet started) has
81
- * nothing expiring and sorts after ticking windows in step 3.
82
+ * Saturated pairs are ordered by soonest 5h recovery first, then weekly reset,
83
+ * because neither can consume more quota until session capacity returns.
82
84
  * Cooling/rejected accounts sort last, soonest-back-to-service first, as
83
85
  * last resort.
84
86
  */
85
- declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey?: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
87
+ declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
86
88
  declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>): {
87
89
  stream: ReadableStream<Uint8Array>;
88
90
  outcome: Promise<StreamTerminalOutcome>;
@@ -266,10 +268,6 @@ export declare const __testHooks: {
266
268
  resetEpochToMs: typeof resetEpochToMs;
267
269
  seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
268
270
  getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
269
- setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
270
- getConfiguredPrimaryAccountKey: () => string | undefined;
271
- setConfiguredAccountAllowlist: (allowlist: AccountAllowlist | undefined) => void;
272
- getConfiguredAccountAllowlist: () => string[] | undefined;
273
271
  setPrimaryAccountIndex: (index: number) => void;
274
272
  getPrimaryAccountIndex: () => number;
275
273
  setAccountRuntimeState: (key: string, state: Partial<RuntimeAccountState>) => void;
@@ -53,11 +53,6 @@ const BLOCKED_UPSTREAM_HEADERS = new Set([
53
53
  let primaryAccountIndex = 0;
54
54
  /** Track account count so we can reset primaryAccountIndex when it changes. */
55
55
  let lastKnownAccountCount = 0;
56
- /** Stable account key used by legacy fixed-config route factories. Runtime
57
- * config providers pass the request generation's primary key explicitly.
58
- * When undefined, home semantics retain insertion-order behavior. */
59
- let configuredPrimaryAccountKey;
60
- let configuredAccountAllowlist;
61
56
  const MAX_AUTH_RETRIES = 5;
62
57
  const MAX_TRANSIENT_SAME_ACCOUNT_RETRIES = 2;
63
58
  const TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS = [250, 1_000];
@@ -129,7 +124,7 @@ function advancePrimaryIfCurrent(accountKey, enabledCount, primaryAccountKey) {
129
124
  * no key is configured or the key cannot be matched (account disabled/
130
125
  * removed). The resolution is per-request because enabledAccounts membership
131
126
  * can shift between requests. */
132
- function resolveHomeIndex(enabledAccounts, primaryAccountKey = configuredPrimaryAccountKey) {
127
+ function resolveHomeIndex(enabledAccounts, primaryAccountKey) {
133
128
  if (!primaryAccountKey) {
134
129
  return 0;
135
130
  }
@@ -140,7 +135,7 @@ function resolveHomeIndex(enabledAccounts, primaryAccountKey = configuredPrimary
140
135
  * primaryAccountIndex back to its index so traffic returns to the preferred
141
136
  * account once its rate limit window expires. Called at the start of each
142
137
  * request. Home is resolved fresh per call via resolveHomeIndex. */
143
- function maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey = configuredPrimaryAccountKey) {
138
+ function maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey) {
144
139
  if (enabledAccounts.length <= 1) {
145
140
  return;
146
141
  }
@@ -463,28 +458,30 @@ function accountSortMetrics(accountKey, now, sessionSoftLimit, sessionResetToler
463
458
  }
464
459
  /**
465
460
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
466
- * spend the window that expires SOONEST first, so its about-to-reset
467
- * allowance isn't wasted, then move to accounts with longer-dated resets.
461
+ * spend the overall weekly allowance that expires SOONEST first, so quota
462
+ * cannot disappear while traffic is consuming a newer weekly window. The 5h
463
+ * window remains an availability boundary: a session at its soft limit is
464
+ * temporarily demoted until that session resets.
468
465
  *
469
466
  * Priority among usable accounts:
470
467
  * 1. no quota data yet — probe first: one request reveals its windows and
471
468
  * self-corrects the ordering. (Ranking unknowns last would starve them
472
469
  * forever: never picked → never observed → never comparable.)
473
470
  * 2. session headroom before session-saturated (>= soft limit or
474
- * "throttled") — saturated accounts then follow the same bucketed
475
- * session ordering below: soonest back in service first, weekly
476
- * deciding same-bucket ties.
477
- * 3. soonest SESSION (5h) reset — the capacity expiring soonest; compared
478
- * in tolerance buckets so near-simultaneous resets count as equal.
479
- * 4. soonest WEEKLY (7d) reset — decides between same-bucket sessions.
480
- * 5. highest weekly utilization — finish off the one closest to done
471
+ * "throttled") — do not re-hammer an urgent weekly account while its 5h
472
+ * capacity is temporarily unavailable.
473
+ * 3. soonest WEEKLY (7d) reset — consume the oldest overall allowance
474
+ * before it expires.
475
+ * 4. soonest SESSION (5h) reset — decides equal-weekly or fresh-weekly ties,
476
+ * using tolerance buckets for comparator stability.
477
+ * 5. highest weekly utilization — finish off the one closest to done.
481
478
  * 6. configured primary account, then insertion order
482
- * An account with NO ticking session window (fresh, not yet started) has
483
- * nothing expiring and sorts after ticking windows in step 3.
479
+ * Saturated pairs are ordered by soonest 5h recovery first, then weekly reset,
480
+ * because neither can consume more quota until session capacity returns.
484
481
  * Cooling/rejected accounts sort last, soonest-back-to-service first, as
485
482
  * last resort.
486
483
  */
487
- function orderAccountsByQuota(accounts, now, primaryKey = configuredPrimaryAccountKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs()) {
484
+ function orderAccountsByQuota(accounts, now, primaryKey, sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs()) {
488
485
  const metrics = (key) => accountSortMetrics(key, now, sessionSoftLimit, sessionResetToleranceMs);
489
486
  return [...accounts].sort((a, b) => {
490
487
  const ma = metrics(a.key);
@@ -503,14 +500,21 @@ function orderAccountsByQuota(accounts, now, primaryKey = configuredPrimaryAccou
503
500
  if (ma.saturated !== mb.saturated) {
504
501
  return ma.saturated ? 1 : -1;
505
502
  }
506
- // Saturated pairs fall through to the same bucketed session ordering:
507
- // soonest-back-in-service at bucket granularity, weekly deciding ties —
508
- // honoring the configured tolerance instead of exact-epoch comparison.
509
- if (ma.sessionResetBucket !== mb.sessionResetBucket) {
510
- return ma.sessionResetBucket - mb.sessionResetBucket;
503
+ if (ma.saturated && mb.saturated) {
504
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
505
+ return ma.sessionResetBucket - mb.sessionResetBucket;
506
+ }
507
+ if (ma.weeklyReset !== mb.weeklyReset) {
508
+ return ma.weeklyReset - mb.weeklyReset;
509
+ }
511
510
  }
512
- if (ma.weeklyReset !== mb.weeklyReset) {
513
- return ma.weeklyReset - mb.weeklyReset;
511
+ else {
512
+ if (ma.weeklyReset !== mb.weeklyReset) {
513
+ return ma.weeklyReset - mb.weeklyReset;
514
+ }
515
+ if (ma.sessionResetBucket !== mb.sessionResetBucket) {
516
+ return ma.sessionResetBucket - mb.sessionResetBucket;
517
+ }
514
518
  }
515
519
  if (ma.weeklyUsed !== mb.weeklyUsed) {
516
520
  return mb.weeklyUsed - ma.weeklyUsed;
@@ -1538,7 +1542,7 @@ async function handleClaudePassthroughJsonResponse(args) {
1538
1542
  return responseJson;
1539
1543
  }
1540
1544
  async function loadClaudeProxyAccounts(args) {
1541
- const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey = configuredPrimaryAccountKey, accountAllowlist = configuredAccountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, } = args;
1545
+ const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, } = args;
1542
1546
  const fs = await import("fs");
1543
1547
  const os = await import("os");
1544
1548
  const accounts = [];
@@ -1722,10 +1726,16 @@ async function loadClaudeProxyAccounts(args) {
1722
1726
  const quotaOrdered = accountStrategy === "fill-first" &&
1723
1727
  orderedAccounts.length > 1 &&
1724
1728
  quotaRoutingEnabled;
1729
+ if (!quotaOrdered && accountStrategy === "fill-first") {
1730
+ // Apply the request-scoped home before deriving this request's order. A
1731
+ // hot-reloaded primary change must not lag by one request. Round-robin
1732
+ // deliberately skips this reset so its rotating index remains strict.
1733
+ maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey);
1734
+ }
1725
1735
  if (quotaOrdered) {
1726
- // Fill-first with a smart fill order: spend the account whose window resets
1727
- // soonest first (max utilization), proactively skipping any whose window is
1728
- // rejected until its reset. Supersedes the static home/primary index.
1736
+ // Fill-first with a smart fill order: spend the account whose weekly window
1737
+ // expires soonest while temporarily demoting sessions without headroom.
1738
+ // Supersedes the static home/primary index.
1729
1739
  orderedAccounts = orderAccountsByQuota(enabledAccounts, Date.now(), primaryAccountKey, sessionSoftLimit, sessionResetToleranceMs);
1730
1740
  if (logger.shouldLog("debug")) {
1731
1741
  logger.debug(`[proxy] quota-ordered fill sequence: ${orderedAccounts
@@ -3791,15 +3801,6 @@ async function handleAnthropicRoutedClaudeRequest(args) {
3791
3801
  attemptNumber: 0,
3792
3802
  };
3793
3803
  const acctSelectionSpan = tracer?.startAccountSelection();
3794
- // Try to return to the home primary account if its cooling has expired.
3795
- // Skipped under quota routing, where the fill order is derived per-request
3796
- // from live quota (soonest-reset-first) rather than a static home index.
3797
- const usingQuotaOrder = accountStrategy === "fill-first" &&
3798
- enabledAccounts.length > 1 &&
3799
- quotaRoutingEnabled;
3800
- if (!usingQuotaOrder) {
3801
- maybeResetPrimaryToHome(enabledAccounts, primaryAccountKey);
3802
- }
3803
3804
  // Never re-hammer accounts with a known active cooldown. When every account
3804
3805
  // is cooling, report the earliest persisted retry time without an upstream
3805
3806
  // call; restarting the proxy must not erase or bypass this quarantine.
@@ -4174,10 +4175,6 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
4174
4175
  const runtimeConfigProvider = isClaudeProxyRouteRuntimeOptions(accountAllowlistOrRuntimeOptions)
4175
4176
  ? accountAllowlistOrRuntimeOptions.runtimeConfigProvider
4176
4177
  : undefined;
4177
- configuredPrimaryAccountKey = primaryAccountKey;
4178
- configuredAccountAllowlist = accountAllowlist
4179
- ? new Set(accountAllowlist)
4180
- : undefined;
4181
4178
  return {
4182
4179
  prefix: `${basePath}/v1`,
4183
4180
  routes: [
@@ -4608,14 +4605,6 @@ export const __testHooks = {
4608
4605
  const state = accountRuntimeState.get(key);
4609
4606
  return state ? { ...state } : undefined;
4610
4607
  },
4611
- setConfiguredPrimaryAccountKey: (key) => {
4612
- configuredPrimaryAccountKey = key;
4613
- },
4614
- getConfiguredPrimaryAccountKey: () => configuredPrimaryAccountKey,
4615
- setConfiguredAccountAllowlist: (allowlist) => {
4616
- configuredAccountAllowlist = allowlist ? new Set(allowlist) : undefined;
4617
- },
4618
- getConfiguredAccountAllowlist: () => configuredAccountAllowlist ? [...configuredAccountAllowlist] : undefined,
4619
4608
  setPrimaryAccountIndex: (index) => {
4620
4609
  primaryAccountIndex = index;
4621
4610
  },
@@ -4631,8 +4620,6 @@ export const __testHooks = {
4631
4620
  transientCooldownAdmissionSchedules.clear();
4632
4621
  primaryAccountIndex = 0;
4633
4622
  lastKnownAccountCount = 0;
4634
- configuredPrimaryAccountKey = undefined;
4635
- configuredAccountAllowlist = undefined;
4636
4623
  },
4637
4624
  polyfillOAuthBody: (bodyStr, isClaudeClientRequest) => polyfillOAuthBody(bodyStr, "test-account-token", null, isClaudeClientRequest),
4638
4625
  isAntiAbuseConstruction429,
@@ -105,11 +105,24 @@ export declare class SageMakerLanguageModel implements SageMakerAsLanguageModel
105
105
  */
106
106
  private convertToSageMakerRequest;
107
107
  /**
108
- * Convert Vercel AI SDK tools to SageMaker format
108
+ * Convert AI SDK tools (`LanguageModelV3FunctionTool | LanguageModelV3ProviderTool`,
109
+ * the same union every other provider's tool converter accepts — see
110
+ * `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts) into the SageMaker
111
+ * wire format, which mirrors the OpenAI Chat Completions nested
112
+ * `{ function: { name, description, parameters } }` convention.
113
+ *
114
+ * Function tools carry their fields flat (`tool.name`, `tool.inputSchema`,
115
+ * etc.) — there is no `tool.function` sub-object on the AI SDK side.
116
+ * `type: "provider"` tools (provider-defined tools like web search) have
117
+ * no SageMaker equivalent; they're dropped explicitly with a debug log
118
+ * rather than crashing or being sent malformed.
109
119
  */
110
120
  private convertToolsToSageMakerFormat;
111
121
  /**
112
- * Convert Vercel AI SDK tool choice to SageMaker format
122
+ * Convert an AI SDK tool choice (`LanguageModelV3ToolChoice`) into the
123
+ * SageMaker/OpenAI-style wire format. The AI SDK shape is
124
+ * `{ type: "auto" | "none" | "required" }` or `{ type: "tool", toolName }`
125
+ * — never the nested `{ type: "function", function: { name } }` shape.
113
126
  */
114
127
  private convertToolChoiceToSageMakerFormat;
115
128
  /**
@@ -370,12 +370,19 @@ export class SageMakerLanguageModel {
370
370
  stop: options.stopSequences || [],
371
371
  },
372
372
  };
373
- // Add tool support if tools are present
373
+ // Add tool support if tools are present. `options.tools` arrives here in
374
+ // the AI SDK's LanguageModelV2/V3 call-options shape — a flat
375
+ // `{ type: "function", name, description?, inputSchema }` per tool (see
376
+ // `LanguageModelV3FunctionTool` in @ai-sdk/provider), the same shape
377
+ // every other provider's tool converter consumes (compare
378
+ // `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts). It is NOT the
379
+ // nested OpenAI Chat Completions wire format.
374
380
  const tools = options.tools;
375
381
  if (tools && Array.isArray(tools) && tools.length > 0) {
376
382
  request.tools = this.convertToolsToSageMakerFormat(tools);
377
383
  // Add tool choice if specified
378
- const toolChoice = options.toolChoice;
384
+ const toolChoice = options
385
+ .toolChoice;
379
386
  if (toolChoice) {
380
387
  request.tool_choice =
381
388
  this.convertToolChoiceToSageMakerFormat(toolChoice);
@@ -403,39 +410,59 @@ export class SageMakerLanguageModel {
403
410
  return request;
404
411
  }
405
412
  /**
406
- * Convert Vercel AI SDK tools to SageMaker format
413
+ * Convert AI SDK tools (`LanguageModelV3FunctionTool | LanguageModelV3ProviderTool`,
414
+ * the same union every other provider's tool converter accepts — see
415
+ * `v3ToolsToOpenAI` in openaiChatCompletionsClient.ts) into the SageMaker
416
+ * wire format, which mirrors the OpenAI Chat Completions nested
417
+ * `{ function: { name, description, parameters } }` convention.
418
+ *
419
+ * Function tools carry their fields flat (`tool.name`, `tool.inputSchema`,
420
+ * etc.) — there is no `tool.function` sub-object on the AI SDK side.
421
+ * `type: "provider"` tools (provider-defined tools like web search) have
422
+ * no SageMaker equivalent; they're dropped explicitly with a debug log
423
+ * rather than crashing or being sent malformed.
407
424
  */
408
425
  convertToolsToSageMakerFormat(tools) {
409
- return tools.map((tool) => {
410
- if (tool.type === "function") {
411
- return {
412
- type: "function",
413
- function: {
414
- name: tool.function.name,
415
- description: tool.function.description || "",
416
- parameters: tool.function.parameters || {},
417
- },
418
- };
426
+ const converted = [];
427
+ for (const tool of tools) {
428
+ if (tool.type !== "function") {
429
+ logger.debug("SageMaker: dropping tool with no SageMaker equivalent", {
430
+ toolType: tool.type,
431
+ toolName: tool.name,
432
+ });
433
+ continue;
419
434
  }
420
- return tool; // Pass through other tool types
421
- });
435
+ converted.push({
436
+ type: "function",
437
+ function: {
438
+ name: tool.name,
439
+ description: tool.description || "",
440
+ parameters: tool.inputSchema || {},
441
+ },
442
+ });
443
+ }
444
+ return converted;
422
445
  }
423
446
  /**
424
- * Convert Vercel AI SDK tool choice to SageMaker format
447
+ * Convert an AI SDK tool choice (`LanguageModelV3ToolChoice`) into the
448
+ * SageMaker/OpenAI-style wire format. The AI SDK shape is
449
+ * `{ type: "auto" | "none" | "required" }` or `{ type: "tool", toolName }`
450
+ * — never the nested `{ type: "function", function: { name } }` shape.
425
451
  */
426
452
  convertToolChoiceToSageMakerFormat(toolChoice) {
427
453
  if (typeof toolChoice === "string") {
428
- return toolChoice; // 'auto', 'none', etc.
454
+ return toolChoice; // Defensive: tolerate a raw string if ever passed.
429
455
  }
430
- if (toolChoice?.type === "function") {
431
- return {
432
- type: "function",
433
- function: {
434
- name: toolChoice.function.name,
435
- },
436
- };
456
+ switch (toolChoice.type) {
457
+ case "auto":
458
+ case "none":
459
+ case "required":
460
+ return toolChoice.type;
461
+ case "tool":
462
+ return { type: "function", function: { name: toolChoice.toolName } };
463
+ default:
464
+ return "auto";
437
465
  }
438
- return toolChoice;
439
466
  }
440
467
  /**
441
468
  * Convert Vercel AI SDK response format to SageMaker format (Phase 4)
@@ -18,12 +18,12 @@ import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, AnthropicAtte
18
18
  * no key is configured or the key cannot be matched (account disabled/
19
19
  * removed). The resolution is per-request because enabledAccounts membership
20
20
  * can shift between requests. */
21
- declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey?: string | undefined): number;
21
+ declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): number;
22
22
  /** If the configured home primary is no longer cooling, reset
23
23
  * primaryAccountIndex back to its index so traffic returns to the preferred
24
24
  * account once its rate limit window expires. Called at the start of each
25
25
  * request. Home is resolved fresh per call via resolveHomeIndex. */
26
- declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey?: string | undefined): void;
26
+ declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): void;
27
27
  declare function claimTransientRateLimitRetry(accountKey: string, coolingUntil: number, now?: number): number | undefined;
28
28
  declare function claimTransientCooldownAdmission(accountKey: string, coolingUntil: number, now?: number): number | undefined;
29
29
  declare function waitForTransientAccountAvailability(orderedAccounts: ProxyPassthroughAccount[]): Promise<ProxyPassthroughAccount[]>;
@@ -61,28 +61,30 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
61
61
  declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
62
62
  /**
63
63
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
64
- * spend the window that expires SOONEST first, so its about-to-reset
65
- * allowance isn't wasted, then move to accounts with longer-dated resets.
64
+ * spend the overall weekly allowance that expires SOONEST first, so quota
65
+ * cannot disappear while traffic is consuming a newer weekly window. The 5h
66
+ * window remains an availability boundary: a session at its soft limit is
67
+ * temporarily demoted until that session resets.
66
68
  *
67
69
  * Priority among usable accounts:
68
70
  * 1. no quota data yet — probe first: one request reveals its windows and
69
71
  * self-corrects the ordering. (Ranking unknowns last would starve them
70
72
  * forever: never picked → never observed → never comparable.)
71
73
  * 2. session headroom before session-saturated (>= soft limit or
72
- * "throttled") — saturated accounts then follow the same bucketed
73
- * session ordering below: soonest back in service first, weekly
74
- * deciding same-bucket ties.
75
- * 3. soonest SESSION (5h) reset — the capacity expiring soonest; compared
76
- * in tolerance buckets so near-simultaneous resets count as equal.
77
- * 4. soonest WEEKLY (7d) reset — decides between same-bucket sessions.
78
- * 5. highest weekly utilization — finish off the one closest to done
74
+ * "throttled") — do not re-hammer an urgent weekly account while its 5h
75
+ * capacity is temporarily unavailable.
76
+ * 3. soonest WEEKLY (7d) reset — consume the oldest overall allowance
77
+ * before it expires.
78
+ * 4. soonest SESSION (5h) reset — decides equal-weekly or fresh-weekly ties,
79
+ * using tolerance buckets for comparator stability.
80
+ * 5. highest weekly utilization — finish off the one closest to done.
79
81
  * 6. configured primary account, then insertion order
80
- * An account with NO ticking session window (fresh, not yet started) has
81
- * nothing expiring and sorts after ticking windows in step 3.
82
+ * Saturated pairs are ordered by soonest 5h recovery first, then weekly reset,
83
+ * because neither can consume more quota until session capacity returns.
82
84
  * Cooling/rejected accounts sort last, soonest-back-to-service first, as
83
85
  * last resort.
84
86
  */
85
- declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey?: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
87
+ declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
86
88
  declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>): {
87
89
  stream: ReadableStream<Uint8Array>;
88
90
  outcome: Promise<StreamTerminalOutcome>;
@@ -266,10 +268,6 @@ export declare const __testHooks: {
266
268
  resetEpochToMs: typeof resetEpochToMs;
267
269
  seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
268
270
  getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
269
- setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
270
- getConfiguredPrimaryAccountKey: () => string | undefined;
271
- setConfiguredAccountAllowlist: (allowlist: AccountAllowlist | undefined) => void;
272
- getConfiguredAccountAllowlist: () => string[] | undefined;
273
271
  setPrimaryAccountIndex: (index: number) => void;
274
272
  getPrimaryAccountIndex: () => number;
275
273
  setAccountRuntimeState: (key: string, state: Partial<RuntimeAccountState>) => void;