@juspay/neurolink 9.87.2 → 9.87.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/browser/neurolink.min.js +381 -382
- package/dist/cli/commands/proxy.d.ts +16 -1
- package/dist/cli/commands/proxy.js +275 -238
- package/dist/lib/proxy/globalInstaller.d.ts +5 -0
- package/dist/lib/proxy/globalInstaller.js +156 -0
- package/dist/lib/proxy/openaiFormat.d.ts +3 -1
- package/dist/lib/proxy/openaiFormat.js +19 -4
- package/dist/lib/proxy/proxyActivity.d.ts +8 -0
- package/dist/lib/proxy/proxyActivity.js +77 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +173 -6
- package/dist/lib/server/routes/claudeProxyRoutes.js +487 -159
- package/dist/lib/server/routes/openaiProxyRoutes.js +47 -6
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxy.d.ts +62 -0
- package/dist/proxy/globalInstaller.d.ts +5 -0
- package/dist/proxy/globalInstaller.js +155 -0
- package/dist/proxy/openaiFormat.d.ts +3 -1
- package/dist/proxy/openaiFormat.js +19 -4
- package/dist/proxy/proxyActivity.d.ts +8 -0
- package/dist/proxy/proxyActivity.js +76 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +173 -6
- package/dist/server/routes/claudeProxyRoutes.js +487 -159
- package/dist/server/routes/openaiProxyRoutes.js +47 -6
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxy.d.ts +62 -0
- package/package.json +1 -1
|
@@ -146,12 +146,53 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
146
146
|
});
|
|
147
147
|
return buildOpenAIErrorResponse(502, "Anthropic loopback returned empty stream body");
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
let terminalStreamError;
|
|
150
|
+
const transformed = upstream.body.pipeThrough(createClaudeToOpenAIStreamTransform(body.model, {
|
|
151
|
+
onError: (message) => {
|
|
152
|
+
terminalStreamError = sanitizeForLog(message);
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
const reader = transformed.getReader();
|
|
156
|
+
let lifecycleWritten = false;
|
|
157
|
+
const finishLifecycle = async (status, errorType, errorMessage) => {
|
|
158
|
+
if (lifecycleWritten) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
lifecycleWritten = true;
|
|
162
|
+
await writeLifecycle(status, { errorType, errorMessage });
|
|
163
|
+
};
|
|
164
|
+
const trackedStream = new ReadableStream({
|
|
165
|
+
async pull(controller) {
|
|
166
|
+
try {
|
|
167
|
+
const { value, done } = await reader.read();
|
|
168
|
+
if (done) {
|
|
169
|
+
if (terminalStreamError) {
|
|
170
|
+
await finishLifecycle(502, "loopback_stream_error", terminalStreamError);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
await finishLifecycle(200);
|
|
174
|
+
}
|
|
175
|
+
controller.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
controller.enqueue(value);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const message = sanitizeForLog(error instanceof Error ? error.message : String(error));
|
|
182
|
+
await finishLifecycle(502, "loopback_stream_error", message);
|
|
183
|
+
controller.error(error);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
async cancel(reason) {
|
|
187
|
+
try {
|
|
188
|
+
await reader.cancel(reason);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await finishLifecycle(499, "client_cancelled", "Client cancelled the OpenAI-compatible stream");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return new Response(trackedStream, {
|
|
155
196
|
status: 200,
|
|
156
197
|
headers: {
|
|
157
198
|
"content-type": "text/event-stream",
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -800,6 +800,8 @@ export type ProxyGuardArgs = {
|
|
|
800
800
|
failureThreshold?: number;
|
|
801
801
|
pollIntervalMs?: number;
|
|
802
802
|
quiet?: boolean;
|
|
803
|
+
/** Run update checks only; never mutate client settings. */
|
|
804
|
+
updaterOnly?: boolean;
|
|
803
805
|
};
|
|
804
806
|
/** Arguments accepted by `neurolink proxy telemetry <subcommand>` */
|
|
805
807
|
export type ProxyTelemetryArgs = {
|
|
@@ -829,6 +831,8 @@ export type ProxyState = {
|
|
|
829
831
|
accountAllowlist?: string[];
|
|
830
832
|
/** Optional fail-open guard PID that reverts Claude settings if proxy dies */
|
|
831
833
|
guardPid?: number;
|
|
834
|
+
/** Dedicated updater PID for launchd-managed proxy installations. */
|
|
835
|
+
updaterPid?: number;
|
|
832
836
|
/** How the proxy was launched — "launchd" if installed as service, "manual" otherwise */
|
|
833
837
|
managedBy?: "launchd" | "manual";
|
|
834
838
|
/** Whether the proxy is running in transparent passthrough mode */
|
|
@@ -518,6 +518,7 @@ export type AnthropicLoopState = {
|
|
|
518
518
|
} | null;
|
|
519
519
|
authFailureMessage: string | null;
|
|
520
520
|
authCooldownMessage: string | null;
|
|
521
|
+
fallbackFailureMessage?: string;
|
|
521
522
|
attemptNumber: number;
|
|
522
523
|
};
|
|
523
524
|
export type AnthropicUpstreamBody = {
|
|
@@ -586,6 +587,10 @@ export type AccountCooldownPlan = {
|
|
|
586
587
|
* burst), a small number of jittered same-account retries is allowed first. */
|
|
587
588
|
rotateImmediately: boolean;
|
|
588
589
|
};
|
|
590
|
+
export type TransientRateLimitRetryBudget = {
|
|
591
|
+
coolingUntil: number;
|
|
592
|
+
retriesClaimed: number;
|
|
593
|
+
};
|
|
589
594
|
export type AnthropicUpstreamFetchResult = {
|
|
590
595
|
continueLoop: boolean;
|
|
591
596
|
retrySameAccount?: boolean;
|
|
@@ -595,6 +600,15 @@ export type AnthropicUpstreamFetchResult = {
|
|
|
595
600
|
cooldownPlan?: AccountCooldownPlan;
|
|
596
601
|
/** Quota snapshot parsed from the response headers (429 or success), if present. */
|
|
597
602
|
quota?: AccountQuota;
|
|
603
|
+
/** A terminal upstream rejection already captured and classified by the
|
|
604
|
+
* fetch layer. The route must finalize it directly instead of feeding it
|
|
605
|
+
* through the generic non-OK handler a second time. */
|
|
606
|
+
terminalError?: {
|
|
607
|
+
status: number;
|
|
608
|
+
body: string;
|
|
609
|
+
headers: Record<string, string>;
|
|
610
|
+
errorType: "construction_rejection";
|
|
611
|
+
};
|
|
598
612
|
response?: Response;
|
|
599
613
|
lastError: unknown;
|
|
600
614
|
sawRateLimit: boolean;
|
|
@@ -897,6 +911,26 @@ export type QuietStatus = {
|
|
|
897
911
|
lastActivityAt: Date | null;
|
|
898
912
|
silenceDurationMs: number;
|
|
899
913
|
};
|
|
914
|
+
/** In-process proxy request activity used to protect streaming restarts. */
|
|
915
|
+
export type ProxyActivitySnapshot = {
|
|
916
|
+
activeRequests: number;
|
|
917
|
+
lastActivityAt: Date | null;
|
|
918
|
+
};
|
|
919
|
+
/** Activity payload exposed by the running proxy status endpoint. */
|
|
920
|
+
export type ProxyRuntimeActivity = {
|
|
921
|
+
activeRequests: number;
|
|
922
|
+
lastActivityAt: string | null;
|
|
923
|
+
};
|
|
924
|
+
/** Request metadata retained by the HTTP adapter for terminal error logging. */
|
|
925
|
+
export type RuntimeRequestMetadata = {
|
|
926
|
+
requestId: string;
|
|
927
|
+
method: string;
|
|
928
|
+
path: string;
|
|
929
|
+
startedAt: number;
|
|
930
|
+
model: string;
|
|
931
|
+
stream: boolean;
|
|
932
|
+
toolCount: number;
|
|
933
|
+
};
|
|
900
934
|
/** Accumulated upstream body capture from a raw stream. */
|
|
901
935
|
export type RawStreamCapture = {
|
|
902
936
|
totalBytes: number;
|
|
@@ -1060,6 +1094,34 @@ export type UpdateState = {
|
|
|
1060
1094
|
lastUpdateAt: string | null;
|
|
1061
1095
|
lastUpdateVersion: string | null;
|
|
1062
1096
|
};
|
|
1097
|
+
/** Supported global package managers for proxy self-updates. */
|
|
1098
|
+
export type GlobalInstallerKind = "npm" | "pnpm";
|
|
1099
|
+
/** Result of probing one global package-manager executable. */
|
|
1100
|
+
export type GlobalInstallerProbe = {
|
|
1101
|
+
kind: GlobalInstallerKind;
|
|
1102
|
+
bin: string;
|
|
1103
|
+
version?: string;
|
|
1104
|
+
globalRoot?: string;
|
|
1105
|
+
globalBinDir?: string;
|
|
1106
|
+
working: boolean;
|
|
1107
|
+
installable: boolean;
|
|
1108
|
+
matchesCurrentInstall: boolean;
|
|
1109
|
+
reason?: string;
|
|
1110
|
+
};
|
|
1111
|
+
/** Selected package manager plus all candidates considered. */
|
|
1112
|
+
export type GlobalInstallerResolution = {
|
|
1113
|
+
installer?: GlobalInstallerProbe;
|
|
1114
|
+
tried: GlobalInstallerProbe[];
|
|
1115
|
+
};
|
|
1116
|
+
/** Injectable command runner used by global-installer tests. */
|
|
1117
|
+
export type GlobalInstallerExecFile = typeof import("node:child_process").execFileSync;
|
|
1118
|
+
/** Overrides used while resolving the global package manager. */
|
|
1119
|
+
export type ResolveGlobalInstallerOptions = {
|
|
1120
|
+
entryScript?: string;
|
|
1121
|
+
env?: NodeJS.ProcessEnv;
|
|
1122
|
+
homeDir?: string;
|
|
1123
|
+
execFileSync?: GlobalInstallerExecFile;
|
|
1124
|
+
};
|
|
1063
1125
|
/** Shape of the dynamically-imported js-yaml module. `dump` is optional —
|
|
1064
1126
|
* read-only consumers (proxy config loader) only need `load`; writers
|
|
1065
1127
|
* (CLI primary-account commands) check `dump` before calling. */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GlobalInstallerKind, GlobalInstallerResolution, ResolveGlobalInstallerOptions } from "../types/index.js";
|
|
2
|
+
/** Resolve a package manager that can update the installation currently running. */
|
|
3
|
+
export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
|
|
4
|
+
export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
|
|
5
|
+
export declare function describeInstallFailure(error: unknown): string;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
function runText(execFileSync, bin, args) {
|
|
6
|
+
return String(execFileSync(bin, args, {
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
timeout: 10_000,
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
})).trim();
|
|
11
|
+
}
|
|
12
|
+
function writableDirectory(path) {
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(path)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
accessSync(path, constants.W_OK);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isPathInside(path, parent) {
|
|
25
|
+
if (!path) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const candidate = realpathSync(path);
|
|
30
|
+
const root = realpathSync(parent);
|
|
31
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
const candidate = resolve(path);
|
|
35
|
+
const root = resolve(parent);
|
|
36
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function probeInstaller(kind, bin, entryScript, execFileSync) {
|
|
40
|
+
const base = {
|
|
41
|
+
kind,
|
|
42
|
+
bin,
|
|
43
|
+
working: false,
|
|
44
|
+
installable: false,
|
|
45
|
+
matchesCurrentInstall: false,
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
base.version = runText(execFileSync, bin, ["--version"]);
|
|
49
|
+
base.working = base.version.length > 0;
|
|
50
|
+
base.globalRoot = runText(execFileSync, bin, ["root", "-g"]);
|
|
51
|
+
base.globalBinDir =
|
|
52
|
+
kind === "pnpm"
|
|
53
|
+
? runText(execFileSync, bin, ["bin", "-g"])
|
|
54
|
+
: join(runText(execFileSync, bin, ["prefix", "-g"]), "bin");
|
|
55
|
+
if (!base.globalRoot || !writableDirectory(base.globalRoot)) {
|
|
56
|
+
base.reason = "global package root is missing or not writable";
|
|
57
|
+
return base;
|
|
58
|
+
}
|
|
59
|
+
if (!base.globalBinDir || !writableDirectory(base.globalBinDir)) {
|
|
60
|
+
base.reason = "global executable directory is missing or not writable";
|
|
61
|
+
return base;
|
|
62
|
+
}
|
|
63
|
+
base.installable = true;
|
|
64
|
+
base.matchesCurrentInstall = isPathInside(entryScript, base.globalRoot);
|
|
65
|
+
return base;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
base.reason = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return base;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function resolveFromPath(command, execFileSync) {
|
|
73
|
+
try {
|
|
74
|
+
const result = runText(execFileSync, "which", [command]);
|
|
75
|
+
return result || undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Resolve a package manager that can update the installation currently running. */
|
|
82
|
+
export function resolveGlobalInstaller(options = {}) {
|
|
83
|
+
const env = options.env ?? process.env;
|
|
84
|
+
const homeDir = options.homeDir ?? homedir();
|
|
85
|
+
const entryScript = options.entryScript ?? process.argv[1];
|
|
86
|
+
const execFileSync = options.execFileSync ?? nodeExecFileSync;
|
|
87
|
+
const candidates = [];
|
|
88
|
+
if (env.NEUROLINK_PACKAGE_MANAGER_PATH) {
|
|
89
|
+
const configuredKind = env.NEUROLINK_PACKAGE_MANAGER?.toLowerCase();
|
|
90
|
+
const inferredName = basename(env.NEUROLINK_PACKAGE_MANAGER_PATH);
|
|
91
|
+
const kind = configuredKind === "npm" || configuredKind === "pnpm"
|
|
92
|
+
? configuredKind
|
|
93
|
+
: inferredName.startsWith("npm")
|
|
94
|
+
? "npm"
|
|
95
|
+
: "pnpm";
|
|
96
|
+
candidates.push({ kind, bin: env.NEUROLINK_PACKAGE_MANAGER_PATH });
|
|
97
|
+
}
|
|
98
|
+
if (env.NEUROLINK_PNPM_PATH) {
|
|
99
|
+
candidates.push({ kind: "pnpm", bin: env.NEUROLINK_PNPM_PATH });
|
|
100
|
+
}
|
|
101
|
+
if (env.PNPM_HOME) {
|
|
102
|
+
candidates.push({ kind: "pnpm", bin: join(env.PNPM_HOME, "pnpm") });
|
|
103
|
+
}
|
|
104
|
+
const nodeBinDir = dirname(process.execPath);
|
|
105
|
+
candidates.push({ kind: "npm", bin: join(nodeBinDir, "npm") });
|
|
106
|
+
const pathPnpm = resolveFromPath("pnpm", execFileSync);
|
|
107
|
+
const pathNpm = resolveFromPath("npm", execFileSync);
|
|
108
|
+
if (pathPnpm) {
|
|
109
|
+
candidates.push({ kind: "pnpm", bin: pathPnpm });
|
|
110
|
+
}
|
|
111
|
+
if (pathNpm) {
|
|
112
|
+
candidates.push({ kind: "npm", bin: pathNpm });
|
|
113
|
+
}
|
|
114
|
+
candidates.push({ kind: "pnpm", bin: join(homeDir, ".local", "share", "pnpm", "pnpm") }, { kind: "pnpm", bin: join(homeDir, "Library", "pnpm", "pnpm") }, { kind: "npm", bin: "/opt/homebrew/bin/npm" }, { kind: "npm", bin: "/usr/local/bin/npm" });
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const tried = candidates
|
|
117
|
+
.filter(({ kind, bin }) => {
|
|
118
|
+
const key = `${kind}:${bin}`;
|
|
119
|
+
if (!bin || seen.has(key)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
seen.add(key);
|
|
123
|
+
return true;
|
|
124
|
+
})
|
|
125
|
+
.map(({ kind, bin }) => probeInstaller(kind, bin, entryScript, execFileSync));
|
|
126
|
+
const matchingInstaller = tried.find((probe) => probe.installable && probe.matchesCurrentInstall);
|
|
127
|
+
// When the running entry script is known, installing into a different
|
|
128
|
+
// global root cannot update that process and may shadow another install.
|
|
129
|
+
const installer = entryScript
|
|
130
|
+
? matchingInstaller
|
|
131
|
+
: (matchingInstaller ?? tried.find((probe) => probe.installable));
|
|
132
|
+
return { installer, tried };
|
|
133
|
+
}
|
|
134
|
+
export function getGlobalInstallArgs(kind, packageSpec) {
|
|
135
|
+
return kind === "pnpm"
|
|
136
|
+
? ["add", "-g", packageSpec]
|
|
137
|
+
: ["install", "--global", "--no-audit", "--no-fund", packageSpec];
|
|
138
|
+
}
|
|
139
|
+
function capturedOutput(error, key) {
|
|
140
|
+
if (!error || typeof error !== "object" || !(key in error)) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const raw = error[key];
|
|
144
|
+
return String(raw ?? "")
|
|
145
|
+
.trim()
|
|
146
|
+
.slice(0, 1_000);
|
|
147
|
+
}
|
|
148
|
+
export function describeInstallFailure(error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
const stdout = capturedOutput(error, "stdout");
|
|
151
|
+
const stderr = capturedOutput(error, "stderr");
|
|
152
|
+
return [message, stdout && `stdout: ${stdout}`, stderr && `stderr: ${stderr}`]
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.join("\n");
|
|
155
|
+
}
|
|
@@ -134,4 +134,6 @@ export declare function convertClaudeToOpenAIResponse(claude: ClaudeResponse, re
|
|
|
134
134
|
* - message_delta -> captures stop_reason and output token usage
|
|
135
135
|
* - message_stop -> emits the final `finish_reason` chunk + `[DONE]`
|
|
136
136
|
*/
|
|
137
|
-
export declare function createClaudeToOpenAIStreamTransform(requestModel: string
|
|
137
|
+
export declare function createClaudeToOpenAIStreamTransform(requestModel: string, options?: {
|
|
138
|
+
onError?: (message: string) => void;
|
|
139
|
+
}): TransformStream<Uint8Array, Uint8Array>;
|
|
@@ -651,7 +651,7 @@ export function convertClaudeToOpenAIResponse(claude, requestModel) {
|
|
|
651
651
|
* - message_delta -> captures stop_reason and output token usage
|
|
652
652
|
* - message_stop -> emits the final `finish_reason` chunk + `[DONE]`
|
|
653
653
|
*/
|
|
654
|
-
export function createClaudeToOpenAIStreamTransform(requestModel) {
|
|
654
|
+
export function createClaudeToOpenAIStreamTransform(requestModel, options = {}) {
|
|
655
655
|
const serializer = new OpenAIStreamSerializer(requestModel);
|
|
656
656
|
const encoder = new TextEncoder();
|
|
657
657
|
const decoder = new TextDecoder();
|
|
@@ -677,6 +677,9 @@ export function createClaudeToOpenAIStreamTransform(requestModel) {
|
|
|
677
677
|
catch {
|
|
678
678
|
return;
|
|
679
679
|
}
|
|
680
|
+
if (finished) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
680
683
|
switch (eventName) {
|
|
681
684
|
case "message_start": {
|
|
682
685
|
const message = (data.message ?? {});
|
|
@@ -749,8 +752,18 @@ export function createClaudeToOpenAIStreamTransform(requestModel) {
|
|
|
749
752
|
}
|
|
750
753
|
return;
|
|
751
754
|
}
|
|
755
|
+
case "error": {
|
|
756
|
+
const error = (data.error ?? {});
|
|
757
|
+
const message = typeof error.message === "string"
|
|
758
|
+
? error.message
|
|
759
|
+
: "Anthropic stream failed";
|
|
760
|
+
finished = true;
|
|
761
|
+
options.onError?.(message);
|
|
762
|
+
emit(controller, serializer.emitError(message));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
752
765
|
default:
|
|
753
|
-
// ping
|
|
766
|
+
// ping and unknown events are ignored.
|
|
754
767
|
return;
|
|
755
768
|
}
|
|
756
769
|
};
|
|
@@ -790,10 +803,12 @@ export function createClaudeToOpenAIStreamTransform(requestModel) {
|
|
|
790
803
|
// closing `\n\n` is not silently lost.
|
|
791
804
|
buffer += decoder.decode();
|
|
792
805
|
drainBufferedEvents(controller);
|
|
793
|
-
//
|
|
806
|
+
// Closing without message_stop is an interrupted stream, not success.
|
|
794
807
|
if (!finished) {
|
|
795
808
|
finished = true;
|
|
796
|
-
|
|
809
|
+
const message = "Anthropic stream ended before message_stop";
|
|
810
|
+
options.onError?.(message);
|
|
811
|
+
emit(controller, serializer.emitError(message));
|
|
797
812
|
}
|
|
798
813
|
},
|
|
799
814
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProxyActivitySnapshot } from "../types/index.js";
|
|
2
|
+
/** Track one client-facing proxy request until its response body settles. */
|
|
3
|
+
export declare function beginProxyRequest(): () => void;
|
|
4
|
+
export declare function getProxyActivitySnapshot(): ProxyActivitySnapshot;
|
|
5
|
+
export declare function isProxyActivityQuiet(snapshot: ProxyActivitySnapshot, quietThresholdMs: number, nowMs?: number): boolean;
|
|
6
|
+
/** Keep activity open until the response body completes, errors, or is cancelled. */
|
|
7
|
+
export declare function trackProxyResponse(response: Response, finishRequest: () => void): Response;
|
|
8
|
+
export declare function resetProxyActivityForTests(): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
let activeRequests = 0;
|
|
2
|
+
let lastActivityAtMs = null;
|
|
3
|
+
function touchActivity() {
|
|
4
|
+
lastActivityAtMs = Date.now();
|
|
5
|
+
}
|
|
6
|
+
/** Track one client-facing proxy request until its response body settles. */
|
|
7
|
+
export function beginProxyRequest() {
|
|
8
|
+
activeRequests += 1;
|
|
9
|
+
touchActivity();
|
|
10
|
+
let finished = false;
|
|
11
|
+
return () => {
|
|
12
|
+
if (finished) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
finished = true;
|
|
16
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
17
|
+
touchActivity();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function getProxyActivitySnapshot() {
|
|
21
|
+
return {
|
|
22
|
+
activeRequests,
|
|
23
|
+
lastActivityAt: lastActivityAtMs === null ? null : new Date(lastActivityAtMs),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function isProxyActivityQuiet(snapshot, quietThresholdMs, nowMs = Date.now()) {
|
|
27
|
+
if (snapshot.activeRequests > 0) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (snapshot.lastActivityAt === null) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return nowMs - snapshot.lastActivityAt.getTime() >= quietThresholdMs;
|
|
34
|
+
}
|
|
35
|
+
/** Keep activity open until the response body completes, errors, or is cancelled. */
|
|
36
|
+
export function trackProxyResponse(response, finishRequest) {
|
|
37
|
+
if (!response.body) {
|
|
38
|
+
finishRequest();
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
const trackedBody = new ReadableStream({
|
|
43
|
+
async pull(controller) {
|
|
44
|
+
try {
|
|
45
|
+
const { value, done } = await reader.read();
|
|
46
|
+
if (done) {
|
|
47
|
+
finishRequest();
|
|
48
|
+
controller.close();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
controller.enqueue(value);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
finishRequest();
|
|
55
|
+
controller.error(error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async cancel(reason) {
|
|
59
|
+
try {
|
|
60
|
+
await reader.cancel(reason);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
finishRequest();
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return new Response(trackedBody, {
|
|
68
|
+
status: response.status,
|
|
69
|
+
statusText: response.statusText,
|
|
70
|
+
headers: response.headers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function resetProxyActivityForTests() {
|
|
74
|
+
activeRequests = 0;
|
|
75
|
+
lastActivityAtMs = null;
|
|
76
|
+
}
|