@juspay/neurolink 10.8.5 → 10.8.7

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +403 -403
  3. package/dist/core/baseProvider.js +5 -1
  4. package/dist/lib/core/baseProvider.js +5 -1
  5. package/dist/lib/providers/googleVertex/client.d.ts +20 -0
  6. package/dist/lib/providers/googleVertex/client.js +39 -15
  7. package/dist/lib/proxy/modelRouter.d.ts +10 -0
  8. package/dist/lib/proxy/modelRouter.js +17 -0
  9. package/dist/lib/proxy/proxyConfig.js +46 -0
  10. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  11. package/dist/lib/proxy/rollingWorkerSupervisor.js +41 -15
  12. package/dist/lib/proxy/routingPolicy.d.ts +4 -2
  13. package/dist/lib/proxy/routingPolicy.js +8 -5
  14. package/dist/lib/proxy/runtimeConfig.js +2 -0
  15. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +52 -1
  16. package/dist/lib/server/routes/claudeProxyRoutes.js +467 -221
  17. package/dist/lib/server/routes/openaiProxyRoutes.js +1 -1
  18. package/dist/lib/types/proxy.d.ts +26 -0
  19. package/dist/lib/types/subscription.d.ts +4 -0
  20. package/dist/lib/utils/messageBuilder.d.ts +25 -0
  21. package/dist/lib/utils/messageBuilder.js +33 -12
  22. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +5 -1
  23. package/dist/lib/utils/multimodalOptionsBuilder.js +8 -1
  24. package/dist/providers/googleVertex/client.d.ts +20 -0
  25. package/dist/providers/googleVertex/client.js +39 -15
  26. package/dist/proxy/modelRouter.d.ts +10 -0
  27. package/dist/proxy/modelRouter.js +17 -0
  28. package/dist/proxy/proxyConfig.js +46 -0
  29. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  30. package/dist/proxy/rollingWorkerSupervisor.js +41 -15
  31. package/dist/proxy/routingPolicy.d.ts +4 -2
  32. package/dist/proxy/routingPolicy.js +8 -5
  33. package/dist/proxy/runtimeConfig.js +2 -0
  34. package/dist/server/routes/claudeProxyRoutes.d.ts +52 -1
  35. package/dist/server/routes/claudeProxyRoutes.js +467 -221
  36. package/dist/server/routes/openaiProxyRoutes.js +1 -1
  37. package/dist/types/proxy.d.ts +26 -0
  38. package/dist/types/subscription.d.ts +4 -0
  39. package/dist/utils/messageBuilder.d.ts +25 -0
  40. package/dist/utils/messageBuilder.js +33 -12
  41. package/dist/utils/multimodalOptionsBuilder.d.ts +5 -1
  42. package/dist/utils/multimodalOptionsBuilder.js +8 -1
  43. package/package.json +1 -1
@@ -166,7 +166,11 @@ export class BaseProvider {
166
166
  timestamp: Date.now(),
167
167
  });
168
168
  // ===== EARLY MULTIMODAL DETECTION =====
169
- const hasFileInput = !!options.input?.files?.length || !!options.input?.videoFiles?.length;
169
+ // #1259: audioFiles was missing here while videoFiles was present, so an
170
+ // audio-only stream skipped this branch entirely.
171
+ const hasFileInput = !!options.input?.files?.length ||
172
+ !!options.input?.videoFiles?.length ||
173
+ !!options.input?.audioFiles?.length;
170
174
  if (hasFileInput) {
171
175
  // ===== VIDEO ANALYSIS DETECTION =====
172
176
  // Check if video frames are present and handle with fake streaming
@@ -166,7 +166,11 @@ export class BaseProvider {
166
166
  timestamp: Date.now(),
167
167
  });
168
168
  // ===== EARLY MULTIMODAL DETECTION =====
169
- const hasFileInput = !!options.input?.files?.length || !!options.input?.videoFiles?.length;
169
+ // #1259: audioFiles was missing here while videoFiles was present, so an
170
+ // audio-only stream skipped this branch entirely.
171
+ const hasFileInput = !!options.input?.files?.length ||
172
+ !!options.input?.videoFiles?.length ||
173
+ !!options.input?.audioFiles?.length;
170
174
  if (hasFileInput) {
171
175
  // ===== VIDEO ANALYSIS DETECTION =====
172
176
  // Check if video frames are present and handle with fake streaming
@@ -141,6 +141,26 @@ export declare class GoogleVertexProvider extends BaseProvider {
141
141
  * Validate stream options
142
142
  */
143
143
  private validateStreamOptionsOnly;
144
+ /**
145
+ * Preprocess file input before routing to the native SDKs.
146
+ *
147
+ * BaseProvider runs this via `buildMultimodalMessagesArray`, but Vertex
148
+ * overrides both `generate()` and `executeStream()` to reach the native
149
+ * @google/genai / @anthropic-ai/vertex-sdk clients directly, so neither
150
+ * inherits it. Without this the file content never reaches the model and
151
+ * the reply is an entirely plausible "no document is attached" — a silent
152
+ * wrong answer rather than an error.
153
+ *
154
+ * #1258: only `generate()` used to call this, so the same document that
155
+ * `generate()` read back correctly came back as "no documents attached"
156
+ * through `stream()`. Sharing one method is what keeps the two paths from
157
+ * drifting apart again.
158
+ *
159
+ * #1259: the alias fold has to happen *before* the `files` check, or
160
+ * requests carrying only `audioFiles`/`videoFiles` look empty here and skip
161
+ * preprocessing entirely.
162
+ */
163
+ private preprocessNativeFileInput;
144
164
  protected executeStream(options: StreamOptions, _analysisSchema?: ZodType<unknown> | Schema<unknown>): Promise<StreamResult>;
145
165
  /**
146
166
  * Emit `stream:end` so the Pipeline B observability listener creates a
@@ -14,7 +14,7 @@ import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, Ra
14
14
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
15
15
  import { applyVertexAnthropicCacheBreakpoints } from "../../utils/anthropicCacheBreakpoints.js";
16
16
  import { FileDetector } from "../../utils/fileDetector.js";
17
- import { processUnifiedFilesArray } from "../../utils/messageBuilder.js";
17
+ import { mergeMediaFileAliases, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
18
18
  import { logger } from "../../utils/logger.js";
19
19
  import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, toVertexAnthropicModelId, } from "../../utils/modelDetection.js";
20
20
  import { detectImageMimeType } from "../../utils/imageDetection.js";
@@ -868,6 +868,40 @@ export class GoogleVertexProvider extends BaseProvider {
868
868
  validateStreamOptionsOnly(options) {
869
869
  this.validateStreamOptions(options);
870
870
  }
871
+ /**
872
+ * Preprocess file input before routing to the native SDKs.
873
+ *
874
+ * BaseProvider runs this via `buildMultimodalMessagesArray`, but Vertex
875
+ * overrides both `generate()` and `executeStream()` to reach the native
876
+ * @google/genai / @anthropic-ai/vertex-sdk clients directly, so neither
877
+ * inherits it. Without this the file content never reaches the model and
878
+ * the reply is an entirely plausible "no document is attached" — a silent
879
+ * wrong answer rather than an error.
880
+ *
881
+ * #1258: only `generate()` used to call this, so the same document that
882
+ * `generate()` read back correctly came back as "no documents attached"
883
+ * through `stream()`. Sharing one method is what keeps the two paths from
884
+ * drifting apart again.
885
+ *
886
+ * #1259: the alias fold has to happen *before* the `files` check, or
887
+ * requests carrying only `audioFiles`/`videoFiles` look empty here and skip
888
+ * preprocessing entirely.
889
+ */
890
+ async preprocessNativeFileInput(options) {
891
+ if (options.input) {
892
+ mergeMediaFileAliases(options.input);
893
+ }
894
+ if (!options.input?.files?.length) {
895
+ return;
896
+ }
897
+ try {
898
+ // Mutates options.input.text / .images / .pdfFiles in place.
899
+ await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
900
+ }
901
+ catch (fileError) {
902
+ logger.warn(`[GoogleVertex] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
903
+ }
904
+ }
871
905
  async executeStream(options, _analysisSchema) {
872
906
  // ALL models now use native SDKs - no more @ai-sdk/google-vertex dependency
873
907
  const modelName = options.model || this.modelName || getDefaultVertexModel();
@@ -889,6 +923,9 @@ export class GoogleVertexProvider extends BaseProvider {
889
923
  // Tool filter (a0269210): trust options.tools — caller (BaseProvider.stream)
890
924
  // already merged MCP/built-in tools and applied any enabledToolNames filter.
891
925
  const optionTools = options.tools || {};
926
+ // #1258: stream() must run the same file preprocessing generate()
927
+ // does, or attached files are dropped on this path alone.
928
+ await this.preprocessNativeFileInput(options);
892
929
  // Emit a `neurolink.message.build` span for the native stream path
893
930
  // so observability tooling sees the same hierarchy it sees on
894
931
  // Pipeline A. Without this, test:tracing's "Message Build Span"
@@ -5742,20 +5779,7 @@ export class GoogleVertexProvider extends BaseProvider {
5742
5779
  const baseTools = !options.disableTools
5743
5780
  ? await this.getToolsForStream(options)
5744
5781
  : {};
5745
- // Process the unified `input.files` array before routing to the
5746
- // native SDK. BaseProvider.generate() runs this preprocessing via
5747
- // buildMultimodalMessagesArray, but Vertex's override skips it,
5748
- // which would otherwise drop text-file content (and the
5749
- // mimetype-hint contract) on the floor. Mutates options.input.text /
5750
- // options.input.images / options.input.pdfFiles in place.
5751
- if (options.input?.files && options.input.files.length > 0) {
5752
- try {
5753
- await processUnifiedFilesArray(options, 100 * 1024 * 1024, this.providerName);
5754
- }
5755
- catch (fileError) {
5756
- logger.warn(`[GoogleVertex] processUnifiedFilesArray threw, continuing without file content: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
5757
- }
5758
- }
5782
+ await this.preprocessNativeFileInput(options);
5759
5783
  // Emit a `neurolink.message.build` span so observability tooling
5760
5784
  // sees the message-construction phase even on the native (Pipeline B)
5761
5785
  // Vertex path. Pipeline A normally produces this via MessageBuilder;
@@ -1,12 +1,22 @@
1
1
  import type { FallbackEntry, ModelMapping, ProxyRoutingConfig, RouteResult } from "../types/index.js";
2
+ /** Default and accepted range for concurrent upstream requests per OAuth account. */
3
+ export declare const MIN_MAX_INFLIGHT_PER_ACCOUNT = 1;
4
+ export declare const MAX_MAX_INFLIGHT_PER_ACCOUNT = 20;
5
+ export declare const DEFAULT_MAX_INFLIGHT_PER_ACCOUNT = 2;
2
6
  export declare class ModelRouter {
3
7
  private readonly mappings;
4
8
  private readonly passthrough;
5
9
  private readonly fallback;
10
+ private readonly autoFallback;
11
+ private readonly maxInflightPerAccount;
6
12
  constructor(config: ProxyRoutingConfig);
7
13
  resolve(requestedModel: string): RouteResult;
8
14
  isClaudeTarget(requestedModel: string): boolean;
9
15
  getFallbackChain(): FallbackEntry[];
16
+ /** Whether translation-layer auto-provider fallback is explicitly enabled. */
17
+ isAutoFallbackEnabled(): boolean;
18
+ /** Maximum concurrent upstream requests admitted for each OAuth account. */
19
+ getMaxInflightPerAccount(): number;
10
20
  /** Return the raw model mapping entries (used by /v1/models). */
11
21
  getModelMappings(): ModelMapping[];
12
22
  /** Return models configured for passthrough (used by /v1/models). */
@@ -1,11 +1,20 @@
1
+ /** Default and accepted range for concurrent upstream requests per OAuth account. */
2
+ export const MIN_MAX_INFLIGHT_PER_ACCOUNT = 1;
3
+ export const MAX_MAX_INFLIGHT_PER_ACCOUNT = 20;
4
+ export const DEFAULT_MAX_INFLIGHT_PER_ACCOUNT = 2;
1
5
  export class ModelRouter {
2
6
  mappings;
3
7
  passthrough;
4
8
  fallback;
9
+ autoFallback;
10
+ maxInflightPerAccount;
5
11
  constructor(config) {
6
12
  this.mappings = new Map(config.modelMappings.map((m) => [m.from, m]));
7
13
  this.passthrough = new Set(config.passthroughModels ?? []);
8
14
  this.fallback = config.fallbackChain;
15
+ this.autoFallback = config.autoFallback === true;
16
+ this.maxInflightPerAccount =
17
+ config.maxInflightPerAccount ?? DEFAULT_MAX_INFLIGHT_PER_ACCOUNT;
9
18
  }
10
19
  resolve(requestedModel) {
11
20
  const mapping = this.mappings.get(requestedModel);
@@ -29,6 +38,14 @@ export class ModelRouter {
29
38
  getFallbackChain() {
30
39
  return this.fallback;
31
40
  }
41
+ /** Whether translation-layer auto-provider fallback is explicitly enabled. */
42
+ isAutoFallbackEnabled() {
43
+ return this.autoFallback;
44
+ }
45
+ /** Maximum concurrent upstream requests admitted for each OAuth account. */
46
+ getMaxInflightPerAccount() {
47
+ return this.maxInflightPerAccount;
48
+ }
32
49
  /** Return the raw model mapping entries (used by /v1/models). */
33
50
  getModelMappings() {
34
51
  return Array.from(this.mappings.values());
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { readFile } from "node:fs/promises";
14
14
  import { extname } from "node:path";
15
+ import { MAX_MAX_INFLIGHT_PER_ACCOUNT, MIN_MAX_INFLIGHT_PER_ACCOUNT, } from "./modelRouter.js";
15
16
  import { logger } from "../utils/logger.js";
16
17
  // ---------------------------------------------------------------------------
17
18
  // Environment variable resolution
@@ -237,6 +238,24 @@ export function validateProxyConfig(config) {
237
238
  normalizedQuotaRouting !== "false") {
238
239
  errors.push("routing.quota-routing must be a boolean");
239
240
  }
241
+ const rawAutoFallback = routing["auto-fallback"] ?? routing.autoFallback;
242
+ const normalizedAutoFallback = typeof rawAutoFallback === "string"
243
+ ? rawAutoFallback.trim().toLowerCase()
244
+ : undefined;
245
+ if (rawAutoFallback !== undefined &&
246
+ typeof rawAutoFallback !== "boolean" &&
247
+ normalizedAutoFallback !== "true" &&
248
+ normalizedAutoFallback !== "false") {
249
+ errors.push("routing.auto-fallback must be a boolean");
250
+ }
251
+ const rawMaxInflight = routing["max-inflight-per-account"] ?? routing.maxInflightPerAccount;
252
+ if (rawMaxInflight !== undefined &&
253
+ (typeof rawMaxInflight !== "number" ||
254
+ !Number.isInteger(rawMaxInflight) ||
255
+ rawMaxInflight < MIN_MAX_INFLIGHT_PER_ACCOUNT ||
256
+ rawMaxInflight > MAX_MAX_INFLIGHT_PER_ACCOUNT)) {
257
+ errors.push("routing.max-inflight-per-account must be an integer between 1 and 20");
258
+ }
240
259
  const rawSessionSoftLimit = routing["session-soft-limit"] ?? routing.sessionSoftLimit;
241
260
  if (rawSessionSoftLimit !== undefined) {
242
261
  const sessionSoftLimit = Number(rawSessionSoftLimit);
@@ -322,6 +341,8 @@ function warnPlaintextApiKeys(accounts) {
322
341
  * - `strategy` ("round-robin" | "fill-first")
323
342
  * - `model-mappings` / `modelMappings` — array of {from, to, provider}
324
343
  * - `fallback-chain` / `fallbackChain` — array of {provider, model}
344
+ * - `auto-fallback` / `autoFallback` — opt in to an unspecified provider
345
+ * - `max-inflight-per-account` / `maxInflightPerAccount` — concurrency cap
325
346
  * - `passthroughModels` / `passthrough-models` — array of model IDs
326
347
  * - `quota-routing` / `quotaRouting` — quota-aware fill-first ordering
327
348
  * - `session-soft-limit` / `sessionSoftLimit` — proactive handoff threshold
@@ -396,6 +417,31 @@ function parseRoutingConfig(raw) {
396
417
  logger.warn(`[proxy-config] Ignoring routing.quotaRouting: expected boolean, got ${typeof rawQuotaRouting}`);
397
418
  }
398
419
  }
420
+ const rawAutoFallback = raw["auto-fallback"] ?? raw.autoFallback;
421
+ if (rawAutoFallback !== undefined) {
422
+ if (typeof rawAutoFallback === "boolean") {
423
+ result.autoFallback = rawAutoFallback;
424
+ }
425
+ else if (typeof rawAutoFallback === "string" &&
426
+ ["true", "false"].includes(rawAutoFallback.trim().toLowerCase())) {
427
+ result.autoFallback = rawAutoFallback.trim().toLowerCase() === "true";
428
+ }
429
+ else {
430
+ logger.warn(`[proxy-config] Ignoring routing.autoFallback: expected boolean, got ${typeof rawAutoFallback}`);
431
+ }
432
+ }
433
+ const rawMaxInflight = raw["max-inflight-per-account"] ?? raw.maxInflightPerAccount;
434
+ if (rawMaxInflight !== undefined) {
435
+ if (typeof rawMaxInflight === "number" &&
436
+ Number.isInteger(rawMaxInflight) &&
437
+ rawMaxInflight >= MIN_MAX_INFLIGHT_PER_ACCOUNT &&
438
+ rawMaxInflight <= MAX_MAX_INFLIGHT_PER_ACCOUNT) {
439
+ result.maxInflightPerAccount = rawMaxInflight;
440
+ }
441
+ else {
442
+ logger.warn(`[proxy-config] Ignoring routing.maxInflightPerAccount: expected integer between 1 and 20, got ${String(rawMaxInflight)}`);
443
+ }
444
+ }
399
445
  const rawSessionSoftLimit = raw["session-soft-limit"] ?? raw.sessionSoftLimit;
400
446
  if (rawSessionSoftLimit !== undefined) {
401
447
  const sessionSoftLimit = Number(rawSessionSoftLimit);
@@ -32,6 +32,7 @@ export declare class RollingWorkerSupervisor {
32
32
  private spawnCandidate;
33
33
  private flushQueuedSockets;
34
34
  private transferSocket;
35
+ private maybeDrainWorker;
35
36
  private handleTransferFailure;
36
37
  private rejectSocket;
37
38
  private describeTransferError;
@@ -287,21 +287,19 @@ export class RollingWorkerSupervisor {
287
287
  generation,
288
288
  version: expectedVersion,
289
289
  dispose,
290
+ pendingTransfers: 0,
291
+ drainRequested: false,
290
292
  };
291
293
  this.active = activated;
292
294
  this.candidate = null;
293
295
  this.flushQueuedSockets();
294
296
  if (previous) {
295
297
  this.draining.set(previous.generation, previous);
296
- try {
297
- previous.handle.sendControl({
298
- type: "proxy-worker:drain",
299
- generation: previous.generation,
300
- });
301
- }
302
- catch {
303
- previous.handle.terminate("SIGTERM");
304
- }
298
+ // A worker can still own socket transfers accepted before this
299
+ // activation. Draining it before their IPC commits makes those
300
+ // clients fail even though the replacement is healthy.
301
+ previous.drainRequested = true;
302
+ this.maybeDrainWorker(previous);
305
303
  }
306
304
  this.options.log?.(`[proxy-supervisor] activated generation=${generation} pid=${handle.pid} version=${expectedVersion}`);
307
305
  this.publishState();
@@ -340,6 +338,8 @@ export class RollingWorkerSupervisor {
340
338
  handle,
341
339
  generation,
342
340
  version: expectedVersion,
341
+ pendingTransfers: 0,
342
+ drainRequested: false,
343
343
  expectedVersion,
344
344
  activationRequested: false,
345
345
  dispose,
@@ -362,15 +362,41 @@ export class RollingWorkerSupervisor {
362
362
  }
363
363
  }
364
364
  transferSocket(worker, socket) {
365
+ worker.pendingTransfers += 1;
366
+ let settled = false;
367
+ const complete = (error) => {
368
+ if (settled) {
369
+ return;
370
+ }
371
+ settled = true;
372
+ worker.pendingTransfers = Math.max(0, worker.pendingTransfers - 1);
373
+ if (error) {
374
+ this.handleTransferFailure(worker, socket, error);
375
+ }
376
+ this.maybeDrainWorker(worker);
377
+ };
365
378
  try {
366
- worker.handle.sendSocket(worker.generation, socket, (error) => {
367
- if (error) {
368
- this.handleTransferFailure(worker, socket, error);
369
- }
370
- });
379
+ worker.handle.sendSocket(worker.generation, socket, complete);
371
380
  }
372
381
  catch (error) {
373
- this.handleTransferFailure(worker, socket, error);
382
+ complete(error instanceof Error ? error : new Error(String(error)));
383
+ }
384
+ }
385
+ maybeDrainWorker(worker) {
386
+ if (!worker.drainRequested ||
387
+ worker.pendingTransfers > 0 ||
388
+ !this.draining.has(worker.generation)) {
389
+ return;
390
+ }
391
+ worker.drainRequested = false;
392
+ try {
393
+ worker.handle.sendControl({
394
+ type: "proxy-worker:drain",
395
+ generation: worker.generation,
396
+ });
397
+ }
398
+ catch {
399
+ worker.handle.terminate("SIGTERM");
374
400
  }
375
401
  }
376
402
  handleTransferFailure(worker, socket, error) {
@@ -4,12 +4,14 @@ export declare function inferClaudeProxyModelTier(modelName: string): ClaudeProx
4
4
  * Build a translation plan for a Claude-compatible proxy request.
5
5
  * The plan lists the primary provider followed by eligible fallback targets.
6
6
  * All configured fallback entries are always eligible — no contract-based gating.
7
- * When no fallback chain is configured, an "auto-provider" entry is appended.
7
+ * An "auto-provider" entry is appended only when explicitly enabled by the
8
+ * caller. This keeps an empty fallback chain from silently escaping to an
9
+ * unrelated provider.
8
10
  */
9
11
  export declare function buildProxyTranslationPlan(primary: {
10
12
  provider: string;
11
13
  model?: string;
12
- }, fallbackChain: FallbackEntry[], requestedModel: string, _parsed: ParsedClaudeRequest): ProxyTranslationPlan;
14
+ }, fallbackChain: FallbackEntry[], requestedModel: string, _parsed: ParsedClaudeRequest, allowAutoFallback?: boolean): ProxyTranslationPlan;
13
15
  /**
14
16
  * Parse the retry-after header from an upstream 429 response.
15
17
  * Returns milliseconds to wait, or 0 if no valid header present.
@@ -15,9 +15,11 @@ export function inferClaudeProxyModelTier(modelName) {
15
15
  * Build a translation plan for a Claude-compatible proxy request.
16
16
  * The plan lists the primary provider followed by eligible fallback targets.
17
17
  * All configured fallback entries are always eligible — no contract-based gating.
18
- * When no fallback chain is configured, an "auto-provider" entry is appended.
18
+ * An "auto-provider" entry is appended only when explicitly enabled by the
19
+ * caller. This keeps an empty fallback chain from silently escaping to an
20
+ * unrelated provider.
19
21
  */
20
- export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel, _parsed) {
22
+ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel, _parsed, allowAutoFallback = false) {
21
23
  const attempts = [
22
24
  {
23
25
  provider: primary.provider,
@@ -36,9 +38,10 @@ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel
36
38
  label: `${fallback.provider}/${fallback.model}`,
37
39
  });
38
40
  }
39
- // Append auto-provider when no configured fallback chain exists,
40
- // or when all configured entries were deduped (same as primary).
41
- if (fallbackChain.length === 0 || attempts.length === 1) {
41
+ // A provider chosen by the translation layer is an explicit opt-in. It is
42
+ // intentionally not a default when configured entries are absent or deduped.
43
+ if (allowAutoFallback &&
44
+ (fallbackChain.length === 0 || attempts.length === 1)) {
42
45
  attempts.push({ label: "auto-provider" });
43
46
  }
44
47
  return {
@@ -212,6 +212,8 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
212
212
  strategy,
213
213
  modelMappings: routing.modelMappings ?? [],
214
214
  fallbackChain: routing.fallbackChain ?? [],
215
+ autoFallback: routing.autoFallback,
216
+ maxInflightPerAccount: routing.maxInflightPerAccount,
215
217
  passthroughModels: routing.passthroughModels,
216
218
  quotaRouting: routing.quotaRouting,
217
219
  sessionSoftLimit: routing.sessionSoftLimit,
@@ -12,7 +12,13 @@
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
14
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
15
- import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicLoopState, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
+ declare function tryAcquireAccountAdmission(accountKey: string, capacity: number): AccountAdmissionLease | undefined;
17
+ declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
18
+ declare function acquireFirstAvailableAccountAdmission(accountKeys: string[], capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<{
19
+ accountKey: string;
20
+ lease: AccountAdmissionLease;
21
+ } | undefined>;
16
22
  /** Resolve the configured primary's stable key to its current index in the
17
23
  * request's enabledAccounts list. Returns 0 (insertion-order fallback) when
18
24
  * no key is configured or the key cannot be matched (account disabled/
@@ -158,6 +164,7 @@ declare function handleAnthropicStreamingSuccessResponse(args: {
158
164
  upstreamSpan?: import("@opentelemetry/api").Span;
159
165
  logAttempt: AnthropicAttemptLogger;
160
166
  logProxyBody: ProxyBodyCaptureLogger;
167
+ onStreamTerminal?: () => void;
161
168
  logFinalRequest: (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
162
169
  inputTokens?: number;
163
170
  outputTokens?: number;
@@ -189,6 +196,7 @@ declare function handleAnthropicAuthRetry(args: {
189
196
  upstreamSpan?: import("@opentelemetry/api").Span;
190
197
  logAttempt: AnthropicAttemptLogger;
191
198
  logProxyBody: ProxyBodyCaptureLogger;
199
+ onStreamTerminal?: () => void;
192
200
  logFinalRequest: (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
193
201
  inputTokens?: number;
194
202
  outputTokens?: number;
@@ -210,6 +218,33 @@ declare function finalizeAnthropicTerminalFetchError(args: {
210
218
  logProxyBody: ProxyBodyCaptureLogger;
211
219
  logFinalRequest: ClaudeFinalRequestLogger;
212
220
  }): Response | unknown;
221
+ declare function handleAnthropicNonOkResponse(args: {
222
+ response: Response;
223
+ account: ProxyPassthroughAccount;
224
+ accountState: RuntimeAccountState;
225
+ enabledAccounts: ProxyPassthroughAccount[];
226
+ orderedAccounts: ProxyPassthroughAccount[];
227
+ tracer?: ProxyTracer;
228
+ requestStartTime: number;
229
+ fetchStartMs: number;
230
+ attemptNumber: number;
231
+ logAttempt: AnthropicAttemptLogger;
232
+ logProxyBody: ProxyBodyCaptureLogger;
233
+ logFinalRequest: (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
234
+ inputTokens?: number;
235
+ outputTokens?: number;
236
+ cacheCreationTokens?: number;
237
+ cacheReadTokens?: number;
238
+ }) => void;
239
+ lastError: unknown;
240
+ authFailureMessage: string | null;
241
+ sawTransientFailure: boolean;
242
+ invalidRequestFailure: {
243
+ status: number;
244
+ body: string;
245
+ contentType?: string;
246
+ } | null;
247
+ }): Promise<AnthropicNonOkResult>;
213
248
  /**
214
249
  * Detect Anthropic's anti-abuse / request-construction 429.
215
250
  *
@@ -284,6 +319,12 @@ export declare const buildProxyFallbackOptions: typeof buildTranslationOptions;
284
319
  * carry transient HTML responses (e.g. 520 pages) inside `error.message`.
285
320
  */
286
321
  export declare function isTransientHttpFailure(status: number, errBody: string): boolean;
322
+ /**
323
+ * An upstream overload is already a capacity signal. Retrying it on the same
324
+ * OAuth account only consumes its remaining concurrency and delays rotation.
325
+ */
326
+ export declare function isUpstreamOverload(status: number, errBody: string): boolean;
327
+ export declare function redactProviderErrorMessage(message: string): string;
287
328
  export declare const __testHooks: {
288
329
  resolveHomeIndex: typeof resolveHomeIndex;
289
330
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
@@ -309,12 +350,22 @@ export declare const __testHooks: {
309
350
  isAntiAbuseConstruction429: typeof isAntiAbuseConstruction429;
310
351
  fetchAnthropicAccountResponse: typeof fetchAnthropicAccountResponse;
311
352
  finalizeAnthropicTerminalFetchError: typeof finalizeAnthropicTerminalFetchError;
353
+ handleAnthropicNonOkResponse: typeof handleAnthropicNonOkResponse;
312
354
  handleAnthropicAuthRetry: typeof handleAnthropicAuthRetry;
313
355
  handleAnthropicStreamingSuccessResponse: typeof handleAnthropicStreamingSuccessResponse;
314
356
  claimTransientRateLimitRetry: typeof claimTransientRateLimitRetry;
315
357
  claimTransientCooldownAdmission: typeof claimTransientCooldownAdmission;
316
358
  waitForTransientAccountAvailability: typeof waitForTransientAccountAvailability;
359
+ acquireAccountAdmission: typeof acquireAccountAdmission;
360
+ acquireFirstAvailableAccountAdmission: typeof acquireFirstAvailableAccountAdmission;
361
+ tryAcquireAccountAdmission: typeof tryAcquireAccountAdmission;
362
+ getAccountAdmissionSnapshot: (accountKey: string) => {
363
+ active: number;
364
+ waiting: number;
365
+ };
317
366
  describeTransportError: typeof describeTransportError;
367
+ redactProviderErrorMessage: typeof redactProviderErrorMessage;
368
+ isUpstreamOverload: typeof isUpstreamOverload;
318
369
  shouldAttemptClaudeFallback: typeof shouldAttemptClaudeFallback;
319
370
  executeClaudeFallbackWithRetry: typeof executeClaudeFallbackWithRetry;
320
371
  buildClaudeAnthropicFailureResponse: typeof buildClaudeAnthropicFailureResponse;