@juspay/neurolink 12.17.1 → 12.17.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +430 -421
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/neurolink.js +8 -3
  7. package/dist/providers/openaiChatCompletionsBase.js +60 -51
  8. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  9. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  10. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  11. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  12. package/dist/proxy/bodyCaptureWorker.js +20 -2
  13. package/dist/proxy/codexFallback.d.ts +10 -0
  14. package/dist/proxy/codexFallback.js +108 -24
  15. package/dist/proxy/codexUsage.js +2 -0
  16. package/dist/proxy/globalInstaller.d.ts +32 -1
  17. package/dist/proxy/globalInstaller.js +386 -3
  18. package/dist/proxy/otelLogSink.d.ts +3 -3
  19. package/dist/proxy/otelLogSink.js +137 -144
  20. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  21. package/dist/proxy/otlpLogTransport.js +334 -0
  22. package/dist/proxy/proxyActivity.d.ts +22 -2
  23. package/dist/proxy/proxyActivity.js +114 -9
  24. package/dist/proxy/proxyAnalysis.js +82 -52
  25. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  26. package/dist/proxy/proxyClientCancellation.js +23 -0
  27. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  28. package/dist/proxy/proxyContextPreflight.js +337 -0
  29. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  30. package/dist/proxy/proxyFailureDetails.js +96 -0
  31. package/dist/proxy/proxyLifecycle.js +7 -0
  32. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  33. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  34. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  35. package/dist/proxy/proxyTokenBudget.js +523 -0
  36. package/dist/proxy/proxyTracer.d.ts +3 -2
  37. package/dist/proxy/proxyTracer.js +46 -20
  38. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  39. package/dist/proxy/proxyTranslationEngine.js +736 -376
  40. package/dist/proxy/requestLogger.d.ts +1 -0
  41. package/dist/proxy/requestLogger.js +107 -5
  42. package/dist/proxy/restartControl.js +9 -4
  43. package/dist/proxy/rollingWorkerProcess.js +65 -0
  44. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  45. package/dist/proxy/updateCoordinator.d.ts +14 -1
  46. package/dist/proxy/updateCoordinator.js +57 -2
  47. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  48. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  49. package/dist/server/routes/codexProxyRoutes.js +191 -9
  50. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  51. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  52. package/dist/types/cli.d.ts +3 -0
  53. package/dist/types/codex.d.ts +5 -1
  54. package/dist/types/index.d.ts +2 -0
  55. package/dist/types/index.js +2 -0
  56. package/dist/types/proxy.d.ts +148 -3
  57. package/dist/types/proxy.js +0 -15
  58. package/dist/types/proxyBudget.d.ts +74 -0
  59. package/dist/types/proxyBudget.js +1 -0
  60. package/dist/types/proxyContext.d.ts +35 -0
  61. package/dist/types/proxyContext.js +1 -0
  62. package/dist/utils/fileDetector.js +28 -7
  63. package/dist/utils/messageBuilder.js +51 -26
  64. package/dist/utils/pdfProcessor.js +26 -4
  65. package/docs/assets/dashboards/README.md +37 -0
  66. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  67. package/docs-site/static/search-index.json +1 -1
  68. package/package.json +7 -2
  69. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  70. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  71. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  72. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  73. package/scripts/observability/query-proxy-history.mjs +34 -5
@@ -1,3 +1,5 @@
1
+ import type { ProxyTokenBudgetSnapshot } from "./proxyBudget.js";
2
+ import type { ProxyContextEvidence, ProxyPreparedContext } from "./proxyContext.js";
1
3
  /**
2
4
  * Proxy type definitions for NeuroLink
3
5
  *
@@ -556,6 +558,27 @@ export type ProxyAccountSortMetrics = {
556
558
  scopedSaturated: boolean;
557
559
  };
558
560
  export type RequestLogEntry = {
561
+ /** Whether model identifies observed serving output rather than a routing target. */
562
+ servingModelStatus?: "observed" | "unavailable";
563
+ /** Whether retrying unchanged input can resolve this terminal failure. */
564
+ retryable?: boolean;
565
+ tokenBudget?: ProxyTokenBudgetSnapshot;
566
+ contextPreflight?: ProxyContextEvidence;
567
+ /** Internal adapter requests link to their one client-facing parent. */
568
+ parentRequestId?: string;
569
+ accountingScope?: "client" | "internal";
570
+ /** Exactly this request owns usage; bridge parents never duplicate it. */
571
+ usageOwnerRequestId?: string;
572
+ /** API price-table estimate, never a subscription charge or quota measure.
573
+ * Null when complete usage and an exact model rate are not both available. */
574
+ apiEquivalentCostUsd?: number | null;
575
+ /** API-equivalent cache-read savings against uncached input at the same rate. */
576
+ apiEquivalentCacheSavingsUsd?: number | null;
577
+ pricingStatus?: "exact" | "inferred" | "unavailable" | "usage_incomplete" | "owned_by_child";
578
+ pricingBasis?: "api_price_table";
579
+ pricingProvider?: string;
580
+ /** Credential identity is unavailable when the SDK does not expose it. */
581
+ accountIdentityStatus?: "observed" | "unavailable";
559
582
  /** First text, refusal or populated tool output, excluding SSE control frames. */
560
583
  firstUsefulOutputMs?: number;
561
584
  /** Why first-useful-output timing is absent; never substitute a zero. */
@@ -646,7 +669,10 @@ export type ProcessedProxyBodyCapture = {
646
669
  headers?: Record<string, string>;
647
670
  stored: StoredBodyArtifact;
648
671
  error?: string;
672
+ /** Admission plus worker queue time; retained for existing consumers. */
649
673
  queueWaitMs?: number;
674
+ admissionWaitMs?: number;
675
+ workerQueueWaitMs?: number;
650
676
  processingMs?: number;
651
677
  admission?: ProxyBodyCaptureAdmission;
652
678
  };
@@ -662,6 +688,11 @@ export type ProxyBodyCaptureAdmission = {
662
688
  maxPendingBytes: number;
663
689
  };
664
690
  export type ProxyBodyCaptureWorkerSnapshot = {
691
+ processing?: number;
692
+ publishing?: number;
693
+ oldestAdmissionWaitMs?: number;
694
+ oldestProcessingMs?: number;
695
+ oldestPublicationMs?: number;
665
696
  attempted: number;
666
697
  completed: number;
667
698
  rejected: number;
@@ -686,6 +717,13 @@ export type ProxyBodyDeliveryResult = {
686
717
  unconfirmedChunks: number;
687
718
  droppedChunks: number;
688
719
  notSubmittedChunks?: number;
720
+ /** Total publication elapsed time, including capacity and exporter waits. */
721
+ publicationMs?: number;
722
+ capacityWaitMs?: number;
723
+ /** Maximum per-chunk residence before an OTLP export starts. */
724
+ maxChunkQueueWaitMs?: number;
725
+ /** Maximum per-chunk transport duration; never sums overlapping exports. */
726
+ maxChunkExportMs?: number;
689
727
  reason?: string;
690
728
  };
691
729
  /** One bounded body publication, tracked across exporter callbacks. */
@@ -694,6 +732,8 @@ export type ProxyBodyPublicationProgress = {
694
732
  unconfirmed: number;
695
733
  dropped: number;
696
734
  emitted: number;
735
+ maxChunkQueueWaitMs?: number;
736
+ maxChunkExportMs?: number;
697
737
  notify?: () => void;
698
738
  };
699
739
  /** Bounded metadata-only evidence for an unconfirmed or locally rejected OTLP batch. */
@@ -714,6 +754,8 @@ export type ProxyTelemetryBackend = {
714
754
  baseUrl: string;
715
755
  organization: string;
716
756
  stream: string;
757
+ /** Optional separate backend storage for bulk body chunks. */
758
+ bodyStream?: string;
717
759
  authorization?: string;
718
760
  collectorMetricsUrl?: string;
719
761
  };
@@ -741,6 +783,10 @@ export type ProxyTelemetryDoctorOptions = {
741
783
  endTime: number;
742
784
  proxyUrl?: string;
743
785
  maxRows?: number;
786
+ /** Explicit age-search horizon; older admissions are outside this proof. */
787
+ admissionLookbackMs?: number;
788
+ requestTimeoutMs?: number;
789
+ ingestionGraceMs?: number;
744
790
  fetchImpl?: typeof fetch;
745
791
  };
746
792
  export type ProxyTelemetryCheck = {
@@ -766,6 +812,8 @@ export type ProxyTelemetryStoredRecord = Partial<RequestLogEntry> & {
766
812
  event?: string;
767
813
  telemetryStatus?: string;
768
814
  outcomeSource?: string;
815
+ requestTimeoutMs?: number;
816
+ redactionLossy?: boolean;
769
817
  };
770
818
  export type ProxyTelemetryFieldCoverage = {
771
819
  records: number;
@@ -786,6 +834,14 @@ export type ProxyRequestLoggerSnapshot = {
786
834
  debug: ProxyRequestLogSinkSnapshot;
787
835
  };
788
836
  export type RequestAttemptLogEntry = {
837
+ /** False for a local context/budget refusal before a provider call. */
838
+ upstreamDispatched?: boolean;
839
+ /** Client model name retained independently of the dispatched model. */
840
+ requestedModel?: string;
841
+ tokenBudget?: ProxyTokenBudgetSnapshot;
842
+ contextPreflight?: ProxyContextEvidence;
843
+ accountingScope?: "client" | "internal";
844
+ usageOwnerRequestId?: string;
789
845
  timestamp: string;
790
846
  requestId: string;
791
847
  /** Parent client request for an internal fallback invocation. */
@@ -825,10 +881,14 @@ export type RequestAttemptLogEntry = {
825
881
  quotaResetAt?: number;
826
882
  /** Unknown scope never implies an account-wide long-window rejection. */
827
883
  quotaScope?: "session" | "weekly" | "unknown";
884
+ /** True when input includes the cache breakdown (native Codex wire usage). */
885
+ inputIncludesCachedTokens?: boolean;
828
886
  inputTokens?: number;
829
887
  outputTokens?: number;
830
888
  cacheCreationTokens?: number;
831
889
  cacheReadTokens?: number;
890
+ /** Reasoning tokens are a subset of output, never additional usage. */
891
+ reasoningTokens?: number;
832
892
  /** Provider that received this upstream attempt. */
833
893
  provider?: string;
834
894
  /** OTel trace ID for correlation with distributed traces */
@@ -839,9 +899,9 @@ export type RequestAttemptLogEntry = {
839
899
  traceFlags?: number;
840
900
  };
841
901
  /** Additional fields recorded when a Codex response becomes client-final. */
842
- export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent">>;
902
+ export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent" | "retryable">>;
843
903
  /** Additional fields recorded for each upstream Codex account attempt. */
844
- export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason" | "quotaResetAt" | "quotaScope">>;
904
+ export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "inputIncludesCachedTokens" | "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason" | "quotaResetAt" | "quotaScope">>;
845
905
  /** Minimal persistence contract needed by Codex rotating-token refreshes. */
846
906
  export type CodexRefreshTokenStore = {
847
907
  peekTokens(provider: string): Promise<StoredOAuthTokens | null>;
@@ -876,6 +936,7 @@ export type ClaudeFinalRequestLogger = (status: number, accountLabel: string, ac
876
936
  cacheReadTokens?: number;
877
937
  reasoningTokens?: number;
878
938
  errorCode?: string;
939
+ retryable?: boolean;
879
940
  transportScope?: ProxyTransportScope;
880
941
  }) => void;
881
942
  export type ClaudeLoggedErrorBuilder = (status: number, message: string, errorType?: string, extra?: {
@@ -883,6 +944,7 @@ export type ClaudeLoggedErrorBuilder = (status: number, message: string, errorTy
883
944
  accountType?: string;
884
945
  attempt?: number;
885
946
  errorCode?: string;
947
+ retryable?: boolean;
886
948
  transportScope?: ProxyTransportScope;
887
949
  }) => ClaudeErrorResponse;
888
950
  export type ClaudeRequestRuntimeContext = {
@@ -935,6 +997,7 @@ export type AnthropicLoopState = {
935
997
  export type AnthropicUpstreamBody = {
936
998
  bodyStr: string;
937
999
  sessionId?: string;
1000
+ preparedContext: ProxyPreparedContext<ClaudeRequest>;
938
1001
  };
939
1002
  export type AnthropicUpstreamBodyBuilder = (token: string) => AnthropicUpstreamBody;
940
1003
  export type LoadedClaudeAccountContext = {
@@ -1044,6 +1107,7 @@ export type PreparedAnthropicAccountAttempt = {
1044
1107
  headers?: Record<string, string>;
1045
1108
  buildUpstreamBody?: AnthropicUpstreamBodyBuilder;
1046
1109
  finalBodyStr?: string;
1110
+ preparedContext?: ProxyPreparedContext<ClaudeRequest>;
1047
1111
  fetchStartMs?: number;
1048
1112
  upstreamSpan?: Span;
1049
1113
  };
@@ -1681,8 +1745,12 @@ export type ProxyRequestContext = {
1681
1745
  * every non-Anthropic model prices to $0.
1682
1746
  */
1683
1747
  provider?: string;
1684
- /** An internal fallback is an attempt; its parent owns request/token metrics. */
1748
+ /** Whether this span owns client request, latency, and error metrics. */
1685
1749
  recordRequestMetrics?: boolean;
1750
+ /** Provider usage ownership. Defaults to recordRequestMetrics for backwards
1751
+ * compatibility: internal Codex fallback spans suppress both, while bridge
1752
+ * children explicitly retain usage and their parent owns client outcomes. */
1753
+ recordUsageMetrics?: boolean;
1686
1754
  };
1687
1755
  /** Response-side details parsed from the upstream reply (model, finish, tools). */
1688
1756
  export type ResponseInfoContext = {
@@ -1722,6 +1790,11 @@ export type UsageContext = {
1722
1790
  rateLimitAfter5h?: number;
1723
1791
  rateLimitAfter7d?: number;
1724
1792
  };
1793
+ /** Keep trace usage when a child request owns its token and cost metrics. */
1794
+ export type ProxyUsageAttributionOptions = {
1795
+ /** Defaults to the tracer's configured ownership when omitted. */
1796
+ recordMetrics?: boolean;
1797
+ };
1725
1798
  /** Where a proxy env file path was sourced from. */
1726
1799
  export type ProxyEnvSource = "cli" | "environment" | "default" | "none";
1727
1800
  /** Result of resolving which proxy env file to load. */
@@ -1765,6 +1838,11 @@ export type ProxyActivitySnapshot = {
1765
1838
  export type ProxyRuntimeActivity = {
1766
1839
  activeRequests: number;
1767
1840
  lastActivityAt: string | null;
1841
+ /** Supervisor-wide resources that must settle before process replacement. */
1842
+ drainingWorkers?: number;
1843
+ queuedSockets?: number;
1844
+ pendingTransfers?: number;
1845
+ candidateWorkers?: number;
1768
1846
  };
1769
1847
  /** Terminal state observed while the HTTP adapter relays a response body. */
1770
1848
  export type ProxyResponseTerminalOutcome = "completed" | "bodyless" | "client_cancelled" | "stream_error";
@@ -1790,6 +1868,13 @@ export type ProxyLifecycleEventName = "runtime_sample" | "supervisor_event" | "r
1790
1868
  export type ProxyLifecycleTerminalOutcome = ProxyResponseTerminalOutcome | "handler_error" | "unknown";
1791
1869
  /** Content-free lifecycle event accepted by the bounded metadata logger. */
1792
1870
  export type ProxyLifecycleEventInput = {
1871
+ /** Hard admission-to-terminal deadline used by reconciliation. */
1872
+ requestTimeoutMs?: number;
1873
+ /** Internal adapter requests link to their one client-facing parent. */
1874
+ parentRequestId?: string;
1875
+ accountingScope?: "client" | "internal";
1876
+ /** Exactly this request owns usage; bridge parents never duplicate it. */
1877
+ usageOwnerRequestId?: string;
1793
1878
  event: ProxyLifecycleEventName;
1794
1879
  requestId: string;
1795
1880
  method: string;
@@ -1976,6 +2061,9 @@ export type ProxyAnalysisReport = {
1976
2061
  providerFinalRecorded: boolean;
1977
2062
  }>;
1978
2063
  accepted: number;
2064
+ internalAccepted?: number;
2065
+ internalTerminal?: number;
2066
+ internalUnsettled?: number;
1979
2067
  /** Accepted metadata requests that do not require model final records. */
1980
2068
  auxiliaryRequests: number;
1981
2069
  headers: number;
@@ -1988,6 +2076,8 @@ export type ProxyAnalysisReport = {
1988
2076
  };
1989
2077
  requests: {
1990
2078
  completed: number;
2079
+ /** Linked internal adapter finals excluded from client request totals. */
2080
+ internalCompleted?: number;
1991
2081
  success: number;
1992
2082
  errors: number;
1993
2083
  finalRateLimits: number;
@@ -2075,6 +2165,9 @@ export type ProxyAnalysisAttemptRecord = {
2075
2165
  };
2076
2166
  /** Final request fields retained while joining offline proxy log records. */
2077
2167
  export type ProxyAnalysisFinalRequestRecord = {
2168
+ accountingScope?: "client" | "internal";
2169
+ parentRequestId?: string;
2170
+ usageOwnerRequestId?: string;
2078
2171
  firstUsefulOutputMs: number | null;
2079
2172
  timestamp: string;
2080
2173
  status: number;
@@ -2114,6 +2207,9 @@ export type ProxyCancellableTransformer<I, O> = Transformer<I, O> & {
2114
2207
  * a null result as "not observed", never as "zero tokens".
2115
2208
  */
2116
2209
  export type CodexStreamUsage = {
2210
+ /** Distinguish missing provider usage from a reported zero. */
2211
+ inputTokensObserved?: boolean;
2212
+ outputTokensObserved?: boolean;
2117
2213
  inputTokens: number;
2118
2214
  outputTokens: number;
2119
2215
  cacheReadTokens: number;
@@ -2146,6 +2242,12 @@ export type ProxyAnalysisRoutingRecord = {
2146
2242
  };
2147
2243
  /** Request metadata retained by the HTTP adapter for terminal error logging. */
2148
2244
  export type RuntimeRequestMetadata = {
2245
+ /** Internal adapter requests link to their one client-facing parent. */
2246
+ parentRequestId?: string;
2247
+ accountingScope?: "client" | "internal";
2248
+ /** Exactly this request owns usage; bridge parents never duplicate it. */
2249
+ usageOwnerRequestId?: string;
2250
+ abortController?: AbortController;
2149
2251
  /** Last dispatched attempt, retained until this HTTP request terminates. */
2150
2252
  lastUpstreamAttempt?: RequestAttemptLogEntry;
2151
2253
  requestId: string;
@@ -2331,6 +2433,14 @@ export type CompareProxyReplayOptions = {
2331
2433
  };
2332
2434
  /** Persisted artifact produced when a body is stored to disk. */
2333
2435
  export type StoredBodyArtifact = {
2436
+ /** Bytes supplied to redaction, after any source observer limit. */
2437
+ inputRetainedBytes?: number;
2438
+ inputEncoding?: "utf8_text" | "structured_object";
2439
+ sourceTruncated?: boolean;
2440
+ processingTruncated?: boolean;
2441
+ /** Invalid structured frames were removed for secret safety. */
2442
+ redactionLossy?: boolean;
2443
+ unparseableRedactedFrames?: number;
2334
2444
  bodyPath?: string;
2335
2445
  bodySha256?: string;
2336
2446
  redactedBodyBytes?: number;
@@ -4029,4 +4139,39 @@ type ProxyBodyCaptureWaitingSnapshot = {
4029
4139
  admissionWaits: number;
4030
4140
  admissionTimeouts: number;
4031
4141
  };
4142
+ /** Immutable package selection used by the proxy launcher, never by global npm. */
4143
+ export type ProxyPackageSelection = {
4144
+ version: string;
4145
+ entryScript: string;
4146
+ nodePath: string;
4147
+ };
4148
+ /** Atomically published active and rollback package selections. */
4149
+ export type ProxyPackageSelectionState = {
4150
+ schemaVersion: 1;
4151
+ active: ProxyPackageSelection | null;
4152
+ previous: ProxyPackageSelection | null;
4153
+ };
4154
+ /** Bounded asynchronous package installation with injectable subprocesses. */
4155
+ export type ProxyStagedInstallOptions = {
4156
+ version: string;
4157
+ packagesDir: string;
4158
+ installer: Pick<GlobalInstallerProbe, "kind" | "bin">;
4159
+ idleTimeoutMs?: number;
4160
+ maxDurationMs?: number;
4161
+ killGraceMs?: number;
4162
+ spawn?: typeof import("node:child_process").spawn;
4163
+ execFileSync?: GlobalInstallerExecFile;
4164
+ onProgress?: (progress: {
4165
+ elapsedMs: number;
4166
+ outputBytes: number;
4167
+ }) => void;
4168
+ };
4169
+ /** Saved launchd settings retained when installing the service again. */
4170
+ export type ProxyServiceInstallSettings = {
4171
+ envFile?: string;
4172
+ configFile?: string;
4173
+ host?: string;
4174
+ port?: number;
4175
+ environment: Record<string, string>;
4176
+ };
4032
4177
  export {};
@@ -1,16 +1 @@
1
- /**
2
- * Proxy type definitions for NeuroLink
3
- *
4
- * Consolidates all proxy-related types from:
5
- * - src/lib/proxy/claudeFormat.ts (Claude API types, SSE types, internal result)
6
- * - src/lib/proxy/cloaking/types.ts (cloaking pipeline types)
7
- * - src/lib/proxy/cloaking/plugins/ (plugin option types)
8
- * - src/lib/proxy/modelRouter.ts (routing types)
9
- * - src/lib/proxy/proxyConfig.ts (config file types)
10
- * - src/lib/proxy/requestLogger.ts (log entry type)
11
- * - src/lib/proxy/usageStats.ts (stats types)
12
- * - src/lib/proxy/tokenRefresh.ts (refresh types)
13
- * - src/lib/proxy/accountQuota.ts (quota type)
14
- * - src/lib/server/routes/claudeProxyRoutes.ts (runtime state, deps)
15
- */
16
1
  export {};
@@ -0,0 +1,74 @@
1
+ /** Operator-configured proxy token caps. All caps are optional and disabled by default. */
2
+ export type ProxyTokenBudgetPolicy = {
3
+ /** Per-provider/account estimated outstanding input plus maximum output. */
4
+ maxInFlightTokens?: number;
5
+ /** Charged tokens in the account's fixed window, including outstanding reservations. */
6
+ accountWindowTokens?: number;
7
+ /** Charged tokens across providers/accounts sharing the same client session. */
8
+ sessionWindowTokens?: number;
9
+ windowMs?: number;
10
+ };
11
+ export type ProxyTokenBudgetReservation = {
12
+ provider: string;
13
+ accountKey: string;
14
+ sessionKey: string;
15
+ requestId: string;
16
+ reservationTokens: number;
17
+ policy?: ProxyTokenBudgetPolicy;
18
+ estimateProvenance?: string;
19
+ };
20
+ export type ProxyTokenBudgetSnapshot = {
21
+ scope: "disabled" | "process" | "supervisor";
22
+ reservationTokens: number;
23
+ accountChargedTokens: number;
24
+ accountInFlightTokens: number;
25
+ sessionChargedTokens: number;
26
+ sessionInFlightTokens: number;
27
+ windowMs: number;
28
+ estimateProvenance: string;
29
+ settlement?: "provider_reported" | "estimate_retained" | "cancelled_before_dispatch" | "unconfirmed";
30
+ };
31
+ export type ProxyTokenBudgetLease = {
32
+ leaseId: string;
33
+ snapshot: ProxyTokenBudgetSnapshot;
34
+ settle: (actualTotalTokens?: number) => Promise<void>;
35
+ cancelBeforeDispatch: () => Promise<void>;
36
+ };
37
+ export type ProxyTokenBudgetBucket = {
38
+ chargedTokens: number;
39
+ inFlightTokens: number;
40
+ startedAt: number;
41
+ windowMs: number;
42
+ lastTouched: number;
43
+ };
44
+ export type ProxyTokenBudgetRecord = {
45
+ owner: string;
46
+ accountKey: string;
47
+ sessionKey: string;
48
+ estimate: number;
49
+ snapshot: ProxyTokenBudgetSnapshot;
50
+ };
51
+ export type ProxyTokenBudgetRpcRequest = {
52
+ type: "proxy-budget:request";
53
+ rpcId: string;
54
+ pid: number;
55
+ generation: number;
56
+ action: "reserve" | "settle" | "cancel";
57
+ reservation?: ProxyTokenBudgetReservation;
58
+ leaseId?: string;
59
+ actualTotalTokens?: number;
60
+ };
61
+ export type ProxyTokenBudgetRpcResponse = {
62
+ type: "proxy-budget:response";
63
+ rpcId: string;
64
+ ok: boolean;
65
+ leaseId?: string;
66
+ snapshot?: ProxyTokenBudgetSnapshot;
67
+ errorCode?: string;
68
+ error?: string;
69
+ };
70
+ export type ProxyTokenBudgetRpcPending = {
71
+ resolve: (result: ProxyTokenBudgetRpcResponse) => void;
72
+ reject: (error: Error) => void;
73
+ timer: NodeJS.Timeout;
74
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ /** Explicit proxy context controls; all token quantities remain estimates. */
2
+ export type ProxyContextPolicy = {
3
+ maxInputTokens?: number;
4
+ outputReserveTokens?: number;
5
+ enforceDiscoveredLimits?: boolean;
6
+ toolAllowlist?: string[];
7
+ models?: Record<string, {
8
+ contextWindow: number;
9
+ maxOutputTokens?: number;
10
+ }>;
11
+ };
12
+ export type ProxyContextEvidence = {
13
+ provider: string;
14
+ model: string;
15
+ inputTokensEstimate: number;
16
+ toolsTokensEstimate: number;
17
+ instructionsTokensEstimate: number;
18
+ schemaTokensEstimate: number;
19
+ outputTokensReserve: number;
20
+ reasoningIncludedInOutputReserve: true;
21
+ contextWindow?: number;
22
+ contextLimitSource: "configured" | "discovered" | "unknown";
23
+ tokenCountSource: "estimated";
24
+ multimodalEstimate: boolean;
25
+ originalToolCount: number;
26
+ retainedToolCount: number;
27
+ historyModified: false;
28
+ };
29
+ export type ProxyPreparedContext<T> = {
30
+ body: T;
31
+ inputTokensEstimate: number;
32
+ outputTokensReserve: number;
33
+ totalTokensReservation: number;
34
+ evidence: ProxyContextEvidence;
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,7 @@
4
4
  * Uses multi-strategy approach for reliable type identification
5
5
  */
6
6
  import { open, readFile, realpath } from "fs/promises";
7
+ import { STATUS_CODES } from "node:http";
7
8
  import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
8
9
  import { request } from "undici";
9
10
  import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
@@ -675,21 +676,35 @@ export class FileDetector {
675
676
  new ContentHeuristicStrategy(),
676
677
  ];
677
678
  let best = null;
679
+ // #397: track which strategy produced `best` so the eventual log line
680
+ // can name it — the strategy instances themselves are gone by the time
681
+ // logging happens below, only their results survive.
682
+ let bestStrategyName = "none";
678
683
  for (const strategy of strategies) {
684
+ const strategyName = strategy.constructor.name;
679
685
  const result = await strategy.detect(strategy instanceof ExtensionStrategy && extensionInput
680
686
  ? extensionInput
681
687
  : input);
682
688
  if (!best || result.metadata.confidence > best.metadata.confidence) {
683
689
  best = result;
690
+ bestStrategyName = strategyName;
684
691
  }
685
692
  if (result.metadata.confidence >= confidenceThreshold) {
686
- logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}%)`);
693
+ logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}%, strategy: ${strategyName})`);
687
694
  return FileDetector.withResolvedExtension(result, input, options);
688
695
  }
689
696
  }
690
- // Below-threshold detection is the common case for any file under the
691
- // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
692
- logger.debug(`[FileDetector] Best-effort type below threshold: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
697
+ // A genuinely unidentifiable input — every strategy failed to produce
698
+ // even a type — is a real failure, not a best-effort guess, so it is
699
+ // surfaced at error level instead of being buried in debug output.
700
+ if (!best || best.type === "unknown") {
701
+ logger.error(`[FileDetector] Detection failed: no strategy identified a type (best: ${bestStrategyName}, ${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
702
+ }
703
+ else {
704
+ // Below-threshold detection is the common case for any file under the
705
+ // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
706
+ logger.debug(`[FileDetector] Best-effort type below threshold: ${best.type} (${best.metadata.confidence}%, threshold ${confidenceThreshold}%, strategy: ${bestStrategyName})`);
707
+ }
693
708
  return FileDetector.withResolvedExtension(best, input, options);
694
709
  }
695
710
  /**
@@ -1543,9 +1558,15 @@ export class FileDetector {
1543
1558
  bodyTimeout: timeout,
1544
1559
  });
1545
1560
  if (response.statusCode !== 200) {
1546
- // Query string / fragment stripped — a presigned URL's token must
1547
- // not be echoed into a thrown error.
1548
- throw new Error(`HTTP ${response.statusCode} fetching ${redactUrlForError(url)}`);
1561
+ // #360: pair the code with its standard reason phrase (e.g. 404 →
1562
+ // "Not Found") so the message is readable rather than a bare
1563
+ // number. Sourced from Node's own `http.STATUS_CODES` table
1564
+ // rather than a hand-written map that could drift from it, and
1565
+ // falls back gracefully for a non-standard code that table
1566
+ // doesn't recognize. Query string / fragment stays stripped — a
1567
+ // presigned URL's token must not be echoed into a thrown error.
1568
+ const statusText = STATUS_CODES[response.statusCode];
1569
+ throw new Error(`HTTP ${response.statusCode}${statusText ? ` ${statusText}` : ""} fetching ${redactUrlForError(url)}`);
1549
1570
  }
1550
1571
  const contentType = response.headers["content-type"] || "";
1551
1572
  const declaredLength = Number(response.headers["content-length"]);
@@ -1877,10 +1877,14 @@ async function processImageToBase64(image, index) {
1877
1877
  const match = image.match(/^data:([^;]+);base64,(.+)$/);
1878
1878
  if (match) {
1879
1879
  const declaredMime = match[1];
1880
- // #348: only accept image/* data URIs; reject a non-image MIME before
1881
- // it reaches a provider API rather than passing it through unchecked.
1882
- if (!declaredMime.startsWith("image/")) {
1883
- throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (expected image/*)`);
1880
+ // #348: validate against the actual supported-image allowlist, not
1881
+ // just the "image/*" prefix — "image/invalid-format" started with
1882
+ // "image/" and passed here, only to fail later at the provider.
1883
+ // Reuses ImageProcessor's allowlist (itself sourced from
1884
+ // imageFormatSupport's SUPPORTED_INPUT_IMAGE_MIME_TYPES) so intake
1885
+ // validation can never drift from the set NeuroLink actually accepts.
1886
+ if (!ImageProcessor.validateImageFormat(declaredMime)) {
1887
+ throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (not a supported image format)`);
1884
1888
  }
1885
1889
  mimeType = declaredMime;
1886
1890
  imageData = match[2]; // Raw base64 only — NOT the full data: URI
@@ -2323,7 +2327,16 @@ pdfFiles, provider, model, audioFiles = []) {
2323
2327
  for (const pdf of pdfFiles) {
2324
2328
  try {
2325
2329
  const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
2326
- const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
2330
+ // #302: stream pages one at a time instead of materialising every
2331
+ // rendered page before any of them reaches `content` — this is the
2332
+ // path generate()/stream() callers actually exercise, so it must
2333
+ // carry the memory characteristic convertToImagesStream was built
2334
+ // for. Per-page ordering, error isolation (#294) and the resulting
2335
+ // message content are kept identical to the batch path below;
2336
+ // only how the pages are produced changes.
2337
+ const pageErrors = [];
2338
+ let convertedCount = 0;
2339
+ for await (const page of PDFImageConverter.convertToImagesStream(pdf.buffer, {
2327
2340
  // #297: this is the only PDF→image call the product actually makes,
2328
2341
  // and it used to hardcode scale 2.0 — silently overriding the
2329
2342
  // lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
@@ -2337,7 +2350,34 @@ pdfFiles, provider, model, audioFiles = []) {
2337
2350
  ...(pdf.maxCanvasPixels
2338
2351
  ? { maxCanvasPixels: pdf.maxCanvasPixels }
2339
2352
  : {}), // #260
2340
- });
2353
+ })) {
2354
+ if (page.error) {
2355
+ // #294: per-page isolation — a failed page is recorded and
2356
+ // skipped, not thrown, so the rest of the document still
2357
+ // reaches the model.
2358
+ pageErrors.push({ page: page.pageIndex, error: page.error });
2359
+ continue;
2360
+ }
2361
+ convertedCount++;
2362
+ // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2363
+ content.push({
2364
+ type: "image",
2365
+ image: page.image,
2366
+ mimeType: "image/png",
2367
+ });
2368
+ logger.debug(`[PDF→Image] Added page ${page.pageIndex} of ${pdf.filename}`);
2369
+ }
2370
+ // Batch's convertToImages throws when nothing converted (either a
2371
+ // zero-page PDF or every page failed) instead of returning an empty
2372
+ // result; convertToImagesStream has no such guard (it simply yields
2373
+ // nothing / all-error pages), so replicate that contract explicitly
2374
+ // to keep the two paths' failure behavior identical.
2375
+ if (convertedCount === 0) {
2376
+ if (pageErrors.length > 0) {
2377
+ throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
2378
+ }
2379
+ throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
2380
+ }
2341
2381
  // The renderer stops at maxPages, so a longer document is silently
2342
2382
  // truncated — say so rather than letting the model answer from a
2343
2383
  // partial document as though it had the whole thing.
@@ -2348,31 +2388,16 @@ pdfFiles, provider, model, audioFiles = []) {
2348
2388
  // there. Reaching the cap is also unambiguous — a short count caused by
2349
2389
  // per-page render failures (#294 isolates those into `errors`) would
2350
2390
  // otherwise be misreported as a maxPages truncation.
2351
- if (conversionResult.pageCount >= effectiveMaxPages) {
2391
+ if (convertedCount >= effectiveMaxPages) {
2352
2392
  logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
2353
2393
  `conversion limit. Any pages beyond that were not sent — the model may be ` +
2354
2394
  `answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
2355
2395
  }
2356
- if (conversionResult.errors && conversionResult.errors.length > 0) {
2357
- logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
2358
- `failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
2359
- }
2360
- logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
2361
- // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2362
- conversionResult.images.forEach((base64Image, pageIndex) => {
2363
- content.push({
2364
- type: "image",
2365
- image: base64Image,
2366
- mimeType: "image/png",
2367
- });
2368
- logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
2369
- });
2370
- // Log any warnings from conversion
2371
- if (conversionResult.warnings) {
2372
- conversionResult.warnings.forEach((warning) => {
2373
- logger.warn(`[PDF→Image] ${warning}`);
2374
- });
2396
+ if (pageErrors.length > 0) {
2397
+ logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${pageErrors.length} page(s) ` +
2398
+ `failed to render and were omitted (page ${pageErrors.map((e) => e.page).join(", ")}).`);
2375
2399
  }
2400
+ logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${convertedCount} page(s) → images`);
2376
2401
  }
2377
2402
  catch (error) {
2378
2403
  const errorMessage = error instanceof Error ? error.message : String(error);